From 2b97471811e62222c3bbee2393b955e5c3812983 Mon Sep 17 00:00:00 2001 From: khangnghiem <56039341+khangnghiem@users.noreply.github.com> Date: Thu, 25 Jun 2026 20:11:56 +0000 Subject: [PATCH] Fix XSS attribute breakout by adding missing single quote escaping Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- .jules/sentinel.md | 4 ++++ fd-vscode/webview/main.js | 4 ++-- fd-vscode/webview/src/state.js | 4 ++-- site/layers.js | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 5e0d967e..d6c11d75 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -1,3 +1,7 @@ +## 2026-06-25 - Prevent XSS Attribute Breakout in escHtml +**Vulnerability:** The custom HTML escaping function `escHtml` in `site/layers.js` incorrectly omitted the single quote replacement entirely. This allowed for attribute breakout vulnerabilities when inserting untrusted input into single-quoted HTML attributes. +**Learning:** All escape functions must include the single quote replacement to ensure completeness. +**Prevention:** Consistently use `.replace(/'/g, ''')` (or `'`) alongside replacements for `&`, `<`, `>`, and `"` in all string escaping utilities designed to prevent XSS attribute breakout. Ensure inputs are cast to `String` first. ## 2026-03-10 - Cross-Site Scripting (XSS) in Renamify Panel **Vulnerability:** Found an XSS vulnerability in `fd-vscode/src/webview-html.ts` where untrusted node IDs (`p.oldId` and `p.newId`) from the extension's `.fd` files were directly interpolated into `row.innerHTML` in the Renamify panel. **Learning:** Even though the source is internal extension files, untrusted input rendered directly as `innerHTML` causes an XSS injection risk. diff --git a/fd-vscode/webview/main.js b/fd-vscode/webview/main.js index 3ee0795a..3741f8d4 100644 --- a/fd-vscode/webview/main.js +++ b/fd-vscode/webview/main.js @@ -1711,7 +1711,7 @@ function escapeHtml(s) { .replace(//g, ">") .replace(/"/g, """) - .replace(/'/g, "'"); + .replace(/'/g, "'"); } /** @@ -1724,7 +1724,7 @@ function escapeAttr(s) { .replace(//g, ">") .replace(/"/g, """) - .replace(/'/g, "'"); + .replace(/'/g, "'"); } /** Mark the canvas as needing a re-render on the next animation frame. */ diff --git a/fd-vscode/webview/src/state.js b/fd-vscode/webview/src/state.js index 3082e936..92c3dead 100644 --- a/fd-vscode/webview/src/state.js +++ b/fd-vscode/webview/src/state.js @@ -90,7 +90,7 @@ function escapeHtml(s) { .replace(//g, ">") .replace(/"/g, """) - .replace(/'/g, "'"); + .replace(/'/g, "'"); } /** @@ -103,7 +103,7 @@ function escapeAttr(s) { .replace(//g, ">") .replace(/"/g, """) - .replace(/'/g, "'"); + .replace(/'/g, "'"); } /** Mark the canvas as needing a re-render on the next animation frame. */ diff --git a/site/layers.js b/site/layers.js index ce74c59f..56d21e84 100644 --- a/site/layers.js +++ b/site/layers.js @@ -10,7 +10,7 @@ export function initLayersPanel(api) { path: '〜', text: 'T', style: '◆', edge: '⟶', note: '◇', spec: '◇' }; - function escHtml(s) { return s.replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } + function escHtml(s) { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g, '''); } /** Parse FD source into a hierarchical layer tree. */ function parseLayerTree(source) {