mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-02-05 12:45:11 -05:00
# 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>
177 lines
7.6 KiB
JavaScript
177 lines
7.6 KiB
JavaScript
// Copyright 2025 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
import { escapeHtml } from './sanitize.js';
|
|
|
|
/**
|
|
* Renders the Google Sign-In button using the GIS library.
|
|
* @param {string} toolId The ID of the tool.
|
|
* @param {string} clientId The Google OAuth Client ID.
|
|
* @param {string} authProfileName The name of the auth service in tools file.
|
|
*/
|
|
function renderGoogleSignInButton(toolId, clientId, authProfileName) {
|
|
const UNIQUE_ID_BASE = `${toolId}-${authProfileName}`;
|
|
const GIS_CONTAINER_ID = `gisContainer-${UNIQUE_ID_BASE}`;
|
|
const gisContainer = document.getElementById(GIS_CONTAINER_ID);
|
|
const setupGisBtn = document.querySelector(`#google-auth-details-${UNIQUE_ID_BASE} .btn--setup-gis`);
|
|
|
|
if (!gisContainer) {
|
|
console.error('GIS container not found:', GIS_CONTAINER_ID);
|
|
return;
|
|
}
|
|
|
|
if (!clientId) {
|
|
alert('Please enter an OAuth Client ID first.');
|
|
return;
|
|
}
|
|
|
|
// hide the setup button and show the container for the GIS button
|
|
if (setupGisBtn) setupGisBtn.style.display = 'none';
|
|
gisContainer.innerHTML = '';
|
|
gisContainer.style.display = 'flex';
|
|
if (window.google && window.google.accounts && window.google.accounts.id) {
|
|
try {
|
|
const handleResponse = (response) => handleCredentialResponse(response, toolId, authProfileName);
|
|
window.google.accounts.id.initialize({
|
|
client_id: clientId,
|
|
callback: handleResponse,
|
|
auto_select: false
|
|
});
|
|
window.google.accounts.id.renderButton(
|
|
gisContainer,
|
|
{ theme: "outline", size: "large", text: "signin_with" }
|
|
);
|
|
} catch (error) {
|
|
console.error("Error initializing Google Sign-In:", error);
|
|
alert("Error initializing Google Sign-In. Check the Client ID and browser console.");
|
|
gisContainer.innerHTML = '<p style="color: red;">Error loading Sign-In button.</p>';
|
|
if (setupGisBtn) setupGisBtn.style.display = '';
|
|
}
|
|
} else {
|
|
console.error("GIS library not fully loaded yet.");
|
|
alert("Google Identity Services library not ready. Please try again in a moment.");
|
|
gisContainer.innerHTML = '<p style="color: red;">GIS library not ready.</p>';
|
|
if (setupGisBtn) setupGisBtn.style.display = '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles the credential response from the Google Sign-In library.
|
|
* @param {!CredentialResponse} response The credential response object from GIS.
|
|
* @param {string} toolId The ID of the tool.
|
|
* @param {string} authProfileName The name of the auth service in tools file.
|
|
*/
|
|
function handleCredentialResponse(response, toolId, authProfileName) {
|
|
console.debug("handleCredentialResponse called with:", { response, toolId, authProfileName });
|
|
const headersTextarea = document.getElementById(`headers-textarea-${toolId}`);
|
|
if (!headersTextarea) {
|
|
console.error('Headers textarea not found for toolId:', toolId);
|
|
return;
|
|
}
|
|
|
|
const UNIQUE_ID_BASE = `${toolId}-${authProfileName}`;
|
|
const setupGisBtn = document.querySelector(`#google-auth-details-${UNIQUE_ID_BASE} .setup-gis-btn`);
|
|
const gisContainer = document.getElementById(`gisContainer-${UNIQUE_ID_BASE}`);
|
|
|
|
if (response.credential) {
|
|
const idToken = response.credential;
|
|
|
|
try {
|
|
let currentHeaders = {};
|
|
if (headersTextarea.value) {
|
|
currentHeaders = JSON.parse(headersTextarea.value);
|
|
}
|
|
const HEADER_KEY = `${authProfileName}_token`;
|
|
currentHeaders[HEADER_KEY] = `${idToken}`;
|
|
headersTextarea.value = JSON.stringify(currentHeaders, null, 2);
|
|
|
|
if (gisContainer) gisContainer.style.display = 'none';
|
|
if (setupGisBtn) setupGisBtn.style.display = '';
|
|
|
|
} catch (e) {
|
|
alert('Headers are not valid JSON. Please correct and try again.');
|
|
console.error("Header JSON parse error:", e);
|
|
}
|
|
} else {
|
|
console.error("Error: No credential in response", response);
|
|
alert('Error: No ID Token received. Check console for details.');
|
|
|
|
if (gisContainer) gisContainer.style.display = 'none';
|
|
if (setupGisBtn) setupGisBtn.style.display = '';
|
|
}
|
|
}
|
|
|
|
// creates the Google Auth method dropdown
|
|
export function createGoogleAuthMethodItem(toolId, authProfileName) {
|
|
const safeProfileName = escapeHtml(authProfileName);
|
|
const UNIQUE_ID_BASE = `${toolId}-${authProfileName}`;
|
|
const item = document.createElement('div');
|
|
|
|
item.className = 'auth-method-item';
|
|
item.innerHTML = `
|
|
<div class="auth-method-header">
|
|
<span class="auth-method-label">Google ID Token (${safeProfileName})</span>
|
|
<button class="toggle-details-tab">Auto Setup</button>
|
|
</div>
|
|
<div class="auth-method-details" id="google-auth-details-${UNIQUE_ID_BASE}" style="display: none;">
|
|
<div class="auth-controls">
|
|
<div class="auth-input-row">
|
|
<label for="clientIdInput-${UNIQUE_ID_BASE}">OAuth Client ID:</label>
|
|
<input type="text" id="clientIdInput-${UNIQUE_ID_BASE}" placeholder="Enter Client ID" class="auth-input">
|
|
</div>
|
|
<div class="auth-instructions">
|
|
<strong>Action Required:</strong> Add this URL (e.g., http://localhost:PORT) to the Client ID's <strong>Authorized JavaScript origins</strong> to avoid a 401 error. If using Google OAuth,
|
|
navigate to <a href="https://console.cloud.google.com/apis/credentials" target="_blank">Google Cloud Console</a> for this setting.
|
|
</div>
|
|
<div class="auth-method-actions">
|
|
<button class="btn btn--setup-gis">Continue</button>
|
|
<div id="gisContainer-${UNIQUE_ID_BASE}" class="auth-interactive-element gis-container" style="display: none;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const toggleBtn = item.querySelector('.toggle-details-tab');
|
|
const detailsDiv = item.querySelector(`#google-auth-details-${UNIQUE_ID_BASE}`);
|
|
const setupGisBtn = item.querySelector('.btn--setup-gis');
|
|
const clientIdInput = item.querySelector(`#clientIdInput-${UNIQUE_ID_BASE}`);
|
|
const gisContainer = item.querySelector(`#gisContainer-${UNIQUE_ID_BASE}`);
|
|
|
|
toggleBtn.addEventListener('click', () => {
|
|
const isVisible = detailsDiv.style.display === 'flex';
|
|
detailsDiv.style.display = isVisible ? 'none' : 'flex';
|
|
toggleBtn.textContent = isVisible ? 'Auto Setup' : 'Close';
|
|
if (!isVisible) {
|
|
if (gisContainer) {
|
|
gisContainer.innerHTML = '';
|
|
gisContainer.style.display = 'none';
|
|
}
|
|
if (setupGisBtn) {
|
|
setupGisBtn.style.display = '';
|
|
}
|
|
}
|
|
});
|
|
|
|
setupGisBtn.addEventListener('click', () => {
|
|
const clientId = clientIdInput.value;
|
|
if (!clientId) {
|
|
alert('Please enter an OAuth Client ID first.');
|
|
return;
|
|
}
|
|
renderGoogleSignInButton(toolId, clientId, authProfileName);
|
|
});
|
|
|
|
return item;
|
|
}
|