Skip to content

Commit d135891

Browse files
threatpointerthreatpointergemini-code-assist[bot]Yuan325
authored
chore(ui): prevent script execution in Toolbox UI rendering (googleapis#2331)
# Defensive Security Hardening: Prevent Script Execution in Toolbox UI Rendering > **Note:** This issue was identified during security research and reviewed previously. > While typical deployments operate within a trusted configuration model, addressing this behavior was recommended as a defense-in-depth improvement. This PR describes the implemented fix. ## Overview This change improves the safety of the GenAI Toolbox UI by preventing unintended JavaScript execution when rendering values derived from tool configuration files. Previously, certain fields from tool definitions were rendered directly into HTML contexts without escaping. As a result, tool definitions containing embedded HTML or script payloads could trigger JavaScript execution when viewed in the dashboard. While this occurs within the same trust boundary as the configuration owner, escaping these values by default avoids unexpected execution and improves robustness. ## Changes Implemented ### 1. New Utility - Added `sanitize.js` which exports a strict `escapeHtml()` function. - Escapes dangerous characters: `&`, `<`, `>`, `"`, `'`, `/`, `` ` ``. - Performs strict type checking, rendering `null` and `undefined` values as empty strings. ### 2. Input Handling - Updated `internal/server/static/js/toolDisplay.js` to wrap `tool.name` and `tool.description` with `escapeHtml()` prior to rendering them into the DOM. ### 3. Error Handling - Updated `internal/server/static/js/loadTools.js` to sanitize error messages that may reflect user-controlled or derived input before rendering. ## Validation - Verified behavior using tool definition files containing common script execution vectors. - Confirmed that embedded HTML and script payloads are rendered as literal text. - Verified that standard and existing tool definitions continue to render correctly without functional regression. ## Notes This change is a defense-in-depth hardening measure. It does not modify the existing trust model or intended usage patterns, but ensures safer default rendering behavior and avoids unintended script execution in the UI. ## Attribution **Contributor:** Mohammed Tanveer (threatpointer) --------- Co-authored-by: threatpointer <mohammed.tanveer1@gmail.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Yuan Teoh <45984206+Yuan325@users.noreply.github.com>
1 parent 2d5d333 commit d135891

4 files changed

Lines changed: 53 additions & 5 deletions

File tree

internal/server/static/js/auth.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
import { escapeHtml } from './sanitize.js';
16+
1517
/**
1618
* Renders the Google Sign-In button using the GIS library.
1719
* @param {string} toolId The ID of the tool.
@@ -112,13 +114,14 @@ function handleCredentialResponse(response, toolId, authProfileName) {
112114

113115
// creates the Google Auth method dropdown
114116
export function createGoogleAuthMethodItem(toolId, authProfileName) {
117+
const safeProfileName = escapeHtml(authProfileName);
115118
const UNIQUE_ID_BASE = `${toolId}-${authProfileName}`;
116119
const item = document.createElement('div');
117120

118121
item.className = 'auth-method-item';
119122
item.innerHTML = `
120123
<div class="auth-method-header">
121-
<span class="auth-method-label">Google ID Token (${authProfileName})</span>
124+
<span class="auth-method-label">Google ID Token (${safeProfileName})</span>
122125
<button class="toggle-details-tab">Auto Setup</button>
123126
</div>
124127
<div class="auth-method-details" id="google-auth-details-${UNIQUE_ID_BASE}" style="display: none;">

internal/server/static/js/loadTools.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
import { renderToolInterface } from "./toolDisplay.js";
16+
import { escapeHtml } from "./sanitize.js";
1617

1718
let toolDetailsAbortController = null;
1819

@@ -34,7 +35,7 @@ export async function loadTools(secondNavContent, toolDisplayArea, toolsetName)
3435
renderToolList(apiResponse, secondNavContent, toolDisplayArea);
3536
} catch (error) {
3637
console.error('Failed to load tools:', error);
37-
secondNavContent.innerHTML = `<p class="error">Failed to load tools: <pre><code>${error}</code></pre></p>`;
38+
secondNavContent.innerHTML = `<p class="error">Failed to load tools: <pre><code>${escapeHtml(String(error))}</code></pre></p>`;
3839
}
3940
}
4041

@@ -168,7 +169,7 @@ async function fetchToolDetails(toolName, toolDisplayArea) {
168169
console.debug("Previous fetch was aborted, expected behavior.");
169170
} else {
170171
console.error(`Failed to load details for tool "${toolName}":`, error);
171-
toolDisplayArea.innerHTML = `<p class="error">Failed to load details for ${toolName}. ${error.message}</p>`;
172+
toolDisplayArea.innerHTML = `<p class="error">Failed to load details for ${escapeHtml(toolName)}. ${escapeHtml(error.message)}</p>`;
172173
}
173174
}
174175
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
/**
16+
* Escapes special characters for safe rendering in HTML text contexts.
17+
*
18+
* This utility encodes user-controlled values to avoid unintended script
19+
* execution when rendering content as HTML. It is intended as a defensive
20+
* measure and does not perform HTML sanitization.
21+
*
22+
* @param {*} input The value to escape.
23+
* @return {string} The escaped string safe for HTML rendering.
24+
*/
25+
const htmlEscapes = {
26+
'&': '&amp;',
27+
'<': '&lt;',
28+
'>': '&gt;',
29+
'"': '&quot;',
30+
"'": '&#x27;',
31+
'`': '&#x60;'
32+
};
33+
34+
const escapeCharsRegex = /[&<>"'`]/g;
35+
36+
export function escapeHtml(input) {
37+
if (input === null || input === undefined) {
38+
return '';
39+
}
40+
41+
const str = String(input);
42+
return str.replace(escapeCharsRegex, (char) => htmlEscapes[char]);
43+
}

internal/server/static/js/toolDisplay.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import { handleRunTool, displayResults } from './runTool.js';
1616
import { createGoogleAuthMethodItem } from './auth.js'
17+
import { escapeHtml } from './sanitize.js'
1718

1819
/**
1920
* Helper function to create form inputs for parameters.
@@ -357,9 +358,9 @@ export function renderToolInterface(tool, containerElement) {
357358
const descBox = document.createElement('div');
358359

359360
nameBox.className = 'tool-box tool-name';
360-
nameBox.innerHTML = `<h5>Name:</h5><p>${tool.name}</p>`;
361+
nameBox.innerHTML = `<h5>Name:</h5><p>${escapeHtml(tool.name)}</p>`;
361362
descBox.className = 'tool-box tool-description';
362-
descBox.innerHTML = `<h5>Description:</h5><p>${tool.description}</p>`;
363+
descBox.innerHTML = `<h5>Description:</h5><p>${escapeHtml(tool.description)}</p>`;
363364

364365
toolInfoContainer.className = 'tool-info';
365366
toolInfoContainer.appendChild(nameBox);

0 commit comments

Comments
 (0)