Webview XSS hardening: HTML/attribute escaping for untrusted sinks, URL scheme guard, safe code-block restore, regression test suites - #224
Open
leonidasbarkas98-cpu wants to merge 22 commits into
Conversation
…only)
displayMCPServers() built configDisplay (server command, args, url and
type) by interpolating config.command/config.args.join(' ')/config.url/
serverType straight into serverItem.innerHTML with no escaping. Any of
those four values can come from external ~/.claude.json / .mcp.json
data, so a malicious config.type like <img src=x onerror=alert(1)>
rendered as a live element.
Wrap all four interpolations in escapeHtml(), matching the display-
hardening half of upstream commit f759a955 (fork-issue-39, "harden MCP
configuration handling"). That commit also contains a Windows
spawn-quoting fix and a "show CLI local-scope servers" feature; both
are unrelated config-path changes that belong in the MCP-cluster
bundle, not in this XSS-hardening branch, so only the escaping lines
are picked here. Placed first so every later commit in this branch
builds on top of an already-escaped configDisplay.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… (fork-issue-40) parseSimpleMarkdown escaped fenced code blocks but passed prose and inline-code content through unescaped into innerHTML, so a literal HTML tag in a response — for example a <select> dropdown Claude wrote outside a code fence — was parsed as real markup, and an unbalanced tag broke the rendering of that message and everything after it. Escape the remaining text right after code blocks are pulled out into placeholders and before inline markup is applied, so raw < > & become entities while markdown syntax and the __CODEBLOCK__ placeholders stay intact. This also closes the same gap in inline code, whose content was unescaped too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The conversation list interpolated firstUserMessage/lastUserMessage raw into innerHTML, so raw HTML typed in chat rendered as live elements in the history. Escape after truncation so entities are never cut apart. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…k-issue-48) Fenced code blocks over a configurable line threshold (default 20) render as native <details>/<summary> with a caret, language and line count in the header; shorter blocks keep byte-identical markup. New claudeCodeChat.ui.collapseLongCodeBlocks / .collapseCodeBlockLines settings, with a catch-up pass in the webview because settingsData arrives after history replay. Every user/claude/error message also gets a manual fold button in its header, independent of the auto-collapse. Threshold/line-count logic lives in the self-contained, .toString()-spliced src/collapse-rules.ts, covered by a new test:collapse-rules suite (18 cases, including a splice-sandbox test). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…bview (fork-issue-49) Follow-up to fork-issue-40: the permission bubble, the tool-call input view, the prompt-snippet list and the file picker all interpolated model- or user-controlled text straight into innerHTML, so a Bash command like echo "<b>hi</b>" tore the permission bubble apart and a command containing a double quote broke out of the always-allow title attribute. escapeHtml() serialises through textContent/innerHTML and therefore leaves " and ' untouched, so attribute sinks need their own escaper: add escapeAttr in a self-contained html-escape.ts, spliced into the webview via .toString() like the collapse helper, and cover it with unit tests under plain mocha. Also drop the manual "/&andrepimenta#39; decoding in toggleExpand -- getAttribute already returns decoded values, so that second pass destroyed any value containing entity text -- and render the TodoWrite summary as textContent, which never needed markup in the first place. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
script.ts declared function enableYoloMode twice in the same scope; the
later declaration won and dereferenced permissionMenu-undefined on the
argument-less inline chat button, so the button died with a TypeError.
Consolidated into one function: with a permissionId it keeps the
permission-menu behavior (now with a null guard on the menu element),
without one it posts the lightweight {type:'enableYoloMode'} message --
the extension host writes the single permissions.yoloMode key and the
settingsData round-trip updates checkbox and warning banner. The
formerly dead early variant's updateSettings() path was deliberately
not revived: it would push 21 unrelated keys to global settings.
No automated test asserts the declaration appears exactly once in this
branch; verified manually that the button no longer throws. Known edge
(workspace-less window, pre-existing on the menu path) tracked as
fork-issue-59.
The earlier build of this commit's conflict resolution left two unrelated
functions, isOutputTokenLimitError() and openMaxOutputTokensSettings(), in
place of the deleted duplicate -- foreign context from elsewhere with no
caller in src/ and no matching 'openMaxOutputTokensSettings' case in
extension.ts. That dead code is dropped here; the duplicate early
enableYoloMode() is simply removed, per the description above.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…onclick (fork-issue-58) The always-allow list in the settings modal interpolated toolName and command into an inline onclick handler through escapeHtml, which leaves ', " and \ untouched. Any pattern containing quotes or backslashes -- practically every Windows path pattern like python "C:\Users\..." * -- broke the attribute or the JS string literal, so clicking Remove did nothing. Move the values into data-tool/data-command via escapeAttr and read them back through this.dataset (same pattern as the snippet delete button), which also hands the extension the exact raw string its removal filter compares against. Escape the tool name and command shown in the list while at it. No dedicated automated regression test covers renderPermissions/ removePermission in this branch yet; verified manually that the always-allow Remove button works again for tool names and commands containing quotes and backslashes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
escapeHtml only escapes & < >, so every attribute built with it stayed open for
a " breakout. formatToolInputUI({file_path: 'a" onmouseover="alert(1)" zz="'})
produced a real onmouseover attribute on the emitted span, and the webview CSP
(default-src * 'unsafe-inline' 'unsafe-eval') would have fired it -- file_path
is model-generated tool input.
All attribute sinks in script.ts, skills-script.ts and plugins-script.ts now use
escapeAttr. Inline handlers that interpolated a value (openFileInEditor,
sendPlanAction) move to the data-* + this.dataset.* pattern established in fork-issue-58.
That also fixes two older bugs at those call sites: a path containing an
apostrophe broke the handler syntactically, and backslashes were never escaped,
so C:\tmp\a.txt reached the handler with \t expanded to a tab.
Covered by six new tests that run the emitted getScript() output in a vm sandbox
and parse the result with parse5 (src/test/webview-attr-escape.test.ts). parse5
was only available transitively via @vscode/vsce -> cheerio and is now an
explicit devDependency, because the test lives under src/ and tsc would fail
the build if it ever disappeared.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…issue-59) _enableYoloMode wrote permissions.yoloMode with ConfigurationTarget.Workspace only, without the global fallback _updateSettings has for the same key. In a window with no workspace folder open, config.update() threw, the bare catch swallowed it and _sendCurrentSettings() never ran -- the setting was silently not persisted. The chat said "YOLO Mode enabled!" anyway, because that confirmation fired client-side the moment the button was clicked and never depended on a response from the extension host. That was the actual defect: the fallback alone would still have left the message unconditional. The workspace-then-global decision now lives in one pure, vscode-free function (settings-batch.ts), shared with _updateSettings' handling of the same key, and _enableYoloMode reports success only after the write went through -- otherwise _reportYoloModeEnableFailure logs the error, shows a VS Code error message and sends a yoloModeEnableFailed response. An outer try/catch makes that hold even for failures outside the update itself, e.g. a mismatched deploy where settings-batch.js is stale: previously that would have been an unhandled rejection with no feedback at all. Also dropped the module comment's "same pattern as shell-utils/perm-log-redact" comparison -- neither module exists in this branch (shell-utils.ts belongs to the separate MCP-cluster bundle; perm-log-redact.ts was removed here as a fork-issue-51 fix with no caller in this branch, see that commit's absence in this history). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
displayMCPServers interpolated the server name straight into a JS string
literal inside an HTML attribute -- onclick="editMCPServer('${name}', ...)" --
with no escaping at all, and passed the whole config object alongside it as
JSON.stringify(...).replace(/"/g,'"'). A single quote in the name broke
out of the call; a double quote broke out of the attribute entirely. Server
names can come from a cloned workspace's own .mcp.json (extension.ts:2740,
read in at extension.ts:2770), and the webview CSP is default-src *
'unsafe-inline' 'unsafe-eval'.
The config object can never be escapeAttr'd as an attribute value, so it no
longer touches an attribute: displayMCPServers records it in a per-render map
and editMCPServer(name) looks it up. Name and scope travel through
data-server-name / data-server-scope with escapeAttr plus this.dataset.*, and
serverType is escapeHtml'd before it reaches innerHTML.
Nine tests (src/test/webview-attr-escape.test.ts) cover both payload classes
-- x'); alert(1); (' does not break out of the attribute but is valid
multi-statement JS in the handler, while x" onmouseover="alert(1)" y="
injects a real attribute -- all verified to fail against the previous code.
extractFunction now skips comments: an apostrophe in an existing comment
inside editMCPServer desynchronised its brace matching.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rk-issue-61) Three sinks handled data from outside the extension with no escaping at all. renderDropdown, renderAllModels and renderOpenCreditsModelCards wrote model.id, model.name and model.owned_by/provider straight into data attributes and into innerHTML. The data comes from fetch(OPENCREDITS_API_URL + '/v1/models'): model-updater's resolveLatestModels() overwrites the bundled names with the API values, the extension posts them as updateRecommendedModels, and the webview replaces its model list and re-renders the cards without any user interaction. A name of <img src=x onerror=...> produced a live element in the DOM. href and src from the open MCP registries were escaped after fork-issue-57 but their scheme was never checked, so a registry entry with "url": "javascript:..." was passed through verbatim. The new safeHttpUrl() only lets http:/https: through, strips tab/CR/LF and leading C0 control characters first and compares the scheme case-insensitively; anything it cannot make sense of is dropped rather than rendered, and callers omit the attribute instead of emitting a dead one. addEnvVariableRow built value="..." unescaped, which truncated any value containing a double quote. Covered by 21 tests (src/test/webview-attr-escape.test.ts), each one verified to fail against the previous code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…issue-62) copyCodeBlock read data-raw-code with getAttribute -- which the HTML parser has already decoded -- and then decoded "/</>/& a second time by hand. For ordinary code that was a no-op, but any code containing those entity texts literally came out of the clipboard mangled: "literal " entity" was pasted as 'literal " entity'. There is exactly one place that writes the attribute (parseSimpleMarkdown, via escapeAttr) and all four callers pass raw text, so getAttribute already returns the original. Verified over 19 inputs -- CRLF, Unicode, NUL, 200k characters, the $& replacement specials from fork-issue-55, placeholder lookalikes -- that the clipboard text now equals getAttribute exactly in every case. createExpandableInput was dead code from upstream 73c4a38: no caller anywhere, no dynamic access, and formatToolInputUI already provides the same expand/collapse markup with proper escapeAttr escaping, whereas the dead copy carried a hand-written escaper that missed &. Removed. A future upstream merge will conflict at that spot. Covered by five tests (src/test/webview-attr-escape.test.ts) driving the real extracted parseSimpleMarkdown and copyCodeBlock through parse5. No automated needle test guards against createExpandableInput reappearing in this branch; a future upstream merge conflicting at that spot is the practical safeguard. The removal comment left in script.ts pointed at build/check-webview-syntax.js, a file that is not part of this branch. Reworded to drop the dangling reference; the comment's actual point (don't spell out the old identifier in the emitted <script> text) still stands on its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…fork-issue-55) Replacement strings containing $&, $`, $' or $$ were interpreted as special replacement patterns by String.replace, corrupting restored code blocks. Extract the restore loop into markdown-restore.ts using a function replacement (same pattern as escapeAttr's build-time splice), add test:markdown-restore (9 tests). Cherry-picked from fork-issue-55's original branch onto this branch's own history (this branch never had fork-issue-47/KaTeX, so build/check-webview-syntax.js and its fork-issue-55 assertion additions, plus the now-irrelevant test:restore-commit-utils/test:perm-log-redact/ test:webview-syntax package.json lines and the renderMathEnabled restore guard, are intentionally dropped here -- foreign context from fork-issue-55's original branch that doesn't apply to this one). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e-63)
User messages went through parseSimpleMarkdown, so asterisks, underscores and
backticks in a prompt were rendered away: a path like src/_test_.ts came back
italicised, and what you sent was no longer what you read in the transcript.
They now render as raw text -- escapeHtml only, with white-space: pre-wrap so
line breaks survive. Fenced code blocks are the one intentional exception:
pasting code should keep its language label, copy button and fork-issue-48 collapse. To
avoid a second implementation, the fence extraction moved out of
parseSimpleMarkdown into a shared extractCodeBlocks(), which both paths call;
user text then goes through escapeHtml and the placeholders are put back with
restoreCodeBlockPlaceholders (fork-issue-55), so data-raw-code keeps its fork-issue-62 behaviour.
Claude and thinking messages are untouched. The refactor was verified to be a
pure move: 28 targeted plus 4000 random inputs produce byte-identical
parseSimpleMarkdown output before and after, and 6000 adversarial payloads
through the user path produce no tag or attribute outside the fixed code-block
vocabulary.
.message.user .message-content also sets line-height 1.6, which the text
inherited from .message p before it stopped being wrapped in paragraphs.
Also dropped the sandbox helper's stub list 'function isOutputTokenLimitError()
{ return false; }' line for a function addMessage() never actually calls in
this branch (see fork-issue-52's dead-code note) -- along with the comment
referencing it.
fork-issue-55 had not actually been cherry-picked into this branch when
fork-issue-63 first landed here, so renderUserMessageContent's placeholder
restore was rebuilt inline as a plain html.replace(placeholder, str) --
reintroducing the exact "$&"/"$`"/"$'"/"$$" substitution-pattern bug
fork-issue-55 exists to fix, in a second, newly-added call site. fork-issue-55
is now cherry-picked directly before this commit; renderUserMessageContent
calls the real restoreCodeBlockPlaceholders() like the message above always
claimed it did.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e-64) extractFunction found a function's source by counting braces. The scanner knew about strings and comments but not about regex literals, so escapeAttr's own .replace(/'/g, '&andrepimenta#39;') made it read the apostrophe as a string start and lose the count: 362 characters of escapeAttr came back as 2727, dragging safeHttpUrl, openFileInEditor, formatFilePath, toggleDiffExpansion and toggleResultExpansion along and loading formatFilePath into the vm sandbox twice. That was harmless only by accident -- escapeAttr sits at the front of the chunk and the later, correctly extracted copy won. Editing or moving toggleResultExpansion would have ended the extraction somewhere else or thrown "unbalanced braces" and taken every test in the file with it. TypeScript is already a devDependency, so the helper now asks its parser for the declaration's span instead of re-implementing a tokenizer. 24 of the 25 extracted functions come back byte-identical; only escapeAttr changes, which is the fix. The five helpers that had been hand-copied into two test files now live in src/test/webview-dom-helpers.ts. No production code changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ers (fork-issue-70)
Issue fork-issue-70 claimed parseSimpleMarkdown/renderUserMessageContent had reintroduced
the fork-issue-55 html.replace(placeholder, string) bug. At this branch's current tip
both call sites already used restoreCodeBlockPlaceholders (fork-issue-55's
function-replacer, already part of this branch's history), cherry-picked before fork-issue-63
landed -- fork-issue-63's own commit message documents that this had already been fixed up
within the same commit. No production code change was needed for the fork-issue-70 finding
itself.
Added the missing full-pipeline regression coverage fork-issue-70 asked for: a code
block containing "$&"/"$`"/"$'"/"$$" must not duplicate the surrounding
prose, exercised through the real parseSimpleMarkdown AND the real
renderUserMessageContent (not just a direct unit call to
restoreCodeBlockPlaceholders, which markdown-restore.test.ts already covered).
While wiring that up, found and fixed an unrelated, pre-existing gap in two
vm-sandbox test loaders: loadCodeBlockSandbox (webview-attr-escape.test.ts)
and loadUserInputPipelineSandbox (user-message-rawtext.test.ts) extracted
parseSimpleMarkdown/renderUserMessageContent but never extracted
restoreCodeBlockPlaceholders itself (script.ts splices it in via
`${restoreCodeBlockPlaceholders.toString()}`, a separate statement, not part
of either function's body) -- every test calling those two functions through
either sandbox threw "ReferenceError: restoreCodeBlockPlaceholders is not
defined" (14 failures, confirmed present before this commit via git stash).
Both loaders now include it alongside their other extracted dependencies.
141 passing, 0 failing (was 127 passing / 14 failing). eslint: 0 errors (25
pre-existing warnings, none in touched files). git grep -il agentdeck: empty.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tch handling Translates German-language code comments to English and removes internal process/tracker references (review-tool name, tracker name, personal name) so the branch reads cleanly as a standalone PR. Also fixes a real bug found while correcting a stale comment in _updateSettings: the loop over settings keys had a single outer try/catch, so one failing key silently discarded all subsequent keys and skipped resending settings/balance to the webview. Each key now gets its own try/catch instead.
The previous "translate internal review comments" pass removed jargon and personal names but left every bare #NN comment reference untouched, so GitHub would still auto-link them to unrelated issues on this repo. Each bare reference (140 across 9 source and 6 test files) is requalified as fork-issue-NN. Genuine "upstream #NNN" references (8x "upstream andrepimenta#151", 1x "upstream andrepimenta#63" in script.ts's extractCodeBlocks/parseSimpleMarkdown comment) are left untouched since they point at the upstream project's own issue tracker, not ours.
Every other new test file (collapse-rules, html-escape, webview-attr-escape, markdown-restore, user-message-rawtext) has its own npm run test:<name> script; settings-batch.test.ts was missing one. No test-coverage change -- the wildcard mocha run in this repo's verification already picks it up.
Several comments in this branch pointed at modules/functions that only exist on sibling branches (getMathScript()/math-script.ts/math-segments.ts/ findMathSegments/restoreMathSegments/diff-utils/shell-utils/ auto-model-switch) -- none of that exists here; the analogies now point at collapse-rules.ts/html-escape.ts/markdown-restore.ts/settings-batch.ts, the sibling pure-logic modules that actually exist in this tree. script.ts's sendOnEnter/renderMathEnabled comment referenced identifiers that never existed anywhere else in this file; pointed it at the real collapseLongCodeBlocks/collapseCodeBlockLines slots instead. Also dropped the two leftover renderMathEnabled: false stub properties (and the comments explaining them) from the webview-attr-escape/user-message-rawtext test sandboxes -- dead, the code under test never reads that variable. Removed leftover internal process-tracking markers from collapse-script.ts and extension.ts comments; reworded without changing behaviour. No production logic changed, comments/test-sandbox-setup only. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
script.ts's escapeAttr/restoreCodeBlockPlaceholders splice comments still compared against "math-script", a module that does not exist on this branch (only collapse-script.ts does); collapse-script.ts's per-message fold comment still carried an internal "Phase 2:" planning-round marker. Neither had a production effect, comments only. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Several code comments pointed at lines that no longer match after earlier edits/rebases; the referenced behaviour itself is unchanged. - settings-batch.ts:16 dropped a dead "extension.ts's _permLog" reference (git grep finds no such symbol anywhere else). - script.ts's enableYoloMode comment now points at the real permission-menu item (~4060, was ~4604), the real inline chat button sites (~219/~476, was ~254/~639), and the real settingsData handler / updateYoloWarning() locations (~5360/~5392, was ~6147/~6150 -- past EOF, the file has 5569 lines). - script.ts:181's ui-styles.ts reference for .copy-btn's margin-left:auto now points at 1129 (was 1152, which is .message-collapse-btn's font-size). - ui-styles.ts:1210's reference for .message p's line-height now points at 3855 (was ~4045, past that rule's actual location). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hardens the chat webview against HTML/JS injection from every sink we identified that renders user-, model- or third-party-controlled text, plus a few directly related correctness fixes and the test infrastructure to keep those sinks closed. Context for severity: the webview CSP is
default-src * 'unsafe-inline' 'unsafe-eval', so anything that reachesinnerHTMLunescaped executes. Grouped thematically, roughly in commit order:parseSimpleMarkdownescaped fenced code blocks but passed prose and inline-code content through unescaped intoinnerHTML, so a literal HTML tag in a response — e.g. a<select>Claude wrote outside a code fence — was parsed as real markup, and an unbalanced tag broke that message and everything after it. Text is now escaped right after code blocks are pulled into placeholders and before inline markup is applied. The conversation-history list had the same gap for user-message previews; those are escaped after truncation so entities are never cut apart.displayMCPServers()interpolated the server command/args/url/type straight intoinnerHTML, and the server name into an inline handler —onclick="editMCPServer('${name}', ...)"— with the whole config object alongside as JSON in the attribute. All of these can come from external~/.claude.json/.mcp.jsondata (e.g. a cloned workspace's own.mcp.json); a single quote in a name broke out of the call, a double quote out of the attribute. Display values are now escaped, the config object never touches an attribute anymore (a per-render map is looked up by name instead), and name/scope travel viadata-*+this.dataset. The display-escaping half matches upstream commit f759a955; that commit's unrelated Windows spawn-quoting and CLI-scope changes belong to a separate PR in this series and are not included here.escapeHtmlonly escapes& < >, so every attribute built with it stayed open to a"breakout:formatToolInputUI({file_path: 'a" onmouseover="alert(1)" zz="'})produced a realonmouseoverattribute on the emitted span — andfile_pathis model-generated tool input. A newescapeAttrin a self-containedsrc/html-escape.ts(spliced into the webview via.toString()) now covers all attribute sinks inscript.ts,skills-script.tsandplugins-script.ts; inline handlers that interpolated values (open-file, plan actions, MCP edit) move to thedata-*+datasetpattern. That conversion also fixes pre-existing plain bugs at those call sites: a path containing an apostrophe broke the handler syntactically, and backslashes were never escaped, soC:\tmp\a.txtreached the handler with\texpanded to a tab.onclickthroughescapeHtml, which leaves',"and\untouched — practically every Windows path pattern (python "C:\Users\..." *) broke the attribute or the JS string literal, so clicking Remove silently did nothing. Values now travel viadata-tool/data-command+dataset, which also hands the extension the exact raw string its removal filter compares against; the displayed name/command are escaped too. Verified manually for names and commands containing quotes and backslashes.<img src=x onerror=...>produced a live element. Escaped now.href/srcvalues from the open MCP registries were escaped but their scheme was never checked, so"url": "javascript:..."passed through verbatim; a newsafeHttpUrl()allows onlyhttp:/https:(strips tab/CR/LF and leading control characters first, compares the scheme case-insensitively), and callers omit the attribute entirely instead of emitting a dead one. Also fixed: an env-var row value containing"was truncated by an unescapedvalue="...".copyCodeBlockdecoded entities a second time by hand aftergetAttributehad already decoded them, so code containing literal entity text ("etc.) came out of the clipboard mangled — the manual decode is dropped, with the invariant (exactly one attribute writer, all callers pass raw text) checked over 19 inputs including CRLF, Unicode, NUL and 200k-character payloads. DeadcreateExpandableInput(upstream 73c4a38: no caller, and a hand-written escaper that missed&) is removed — a future upstream merge will conflict at that spot. Separately, the code-block placeholder restore usedString.prototype.replace(placeholder, htmlString), which interprets$&/$`/$'/$$in the replacement as substitution patterns — a code block containing one of these could splice already-rendered HTML across block boundaries. The restore loop moved intosrc/markdown-restore.tswith a function replacer (literal substitution), plus a full-pipeline regression test through both call sites.src/_test_.tscame back italicised. They now render escaped withwhite-space: pre-wrap; fenced code blocks are the one intentional exception (keeping language label, copy button and collapse), via a sharedextractCodeBlocks()instead of a second implementation. The refactor was verified as a pure move: 28 targeted plus 4000 random inputs produce byte-identicalparseSimpleMarkdownoutput before and after, and 6000 adversarial payloads through the user path produce no tag or attribute outside the fixed code-block vocabulary.claudeCodeChat.ui.collapseLongCodeBlocks/.collapseCodeBlockLinessettings) render as native<details>/<summary>with language and line count in the header, shorter blocks keep byte-identical markup, and every message gets a manual fold button independent of the auto-collapse.script.tsdeclaredenableYoloModetwice in the same scope; the surviving declaration dereferenced an undefined menu element on the argument-less inline chat button, which died with a TypeError — consolidated into one function (verified manually). Worse, the chat reported "YOLO Mode enabled!" purely client-side, before and regardless of whether the setting was actually persisted — in a window with no workspace folder the write threw and a bare catch swallowed it. The workspace-then-global write decision now lives in a pure, vscode-freesrc/settings-batch.ts, and success is only reported after the write went through; failures log, show a VS Code error message and notify the webview instead of vanishing._updateSettingslooped over keys under one outer try/catch, so one failing key silently discarded every subsequent key and skipped resending settings to the webview; each key now has its own try/catch.npm run test:<name>script) run the emittedgetScript()output in a Nodevmsandbox and parse the resulting DOM with parse5; each injection test was verified to fail against the previous code. The brace-counting helper that extracted webview functions for the sandbox didn't understand regex literals and silently extracted a wrong 2727-character chunk; it now asks the TypeScript compiler API (already a devDependency) for the declaration span, with shared DOM helpers deduplicated intosrc/test/webview-dom-helpers.ts. Full unit run at this tip: 131 passing, 0 failing (97 across the six new suites, plus the 34 pre-existing downloader/model-updater unit tests).The remaining chore commits translate internal review comments to English, requalify bare issue references in code comments as
fork-issue-NNso GitHub doesn't auto-link them to unrelated issues on this repository (genuine upstream references are kept), and fix stale cross-references in comments — no production logic.This is one of several PRs from our fork submitted together as part of a broader, thematically grouped series (permission/session handling, settings/UI, security hardening, rendering, etc. split across separate branches/PRs). This is the security-hardening branch of that series; it is based directly on current
main(ab6e307) and does not stack on any other PR in the series. Note:src/markdown-restore.ts(introduced here) also appears independently, with slightly different content, in the LaTeX/KaTeX rendering PR of this series — a merge conflict is expected if both are merged, as noted there.tsc --noEmitclean. No new runtime dependencies; parse5 (used only by the new DOM-parsing tests) was previously available transitively via@vscode/vsce→ cheerio and is now an explicit devDependency so the build fails loudly if it ever disappears.🤖 Generated with Claude Code