Several examples currently use DOM HTML sinks for strings that come from users, model output, or page context. That means HTML-looking text can be parsed as markup instead of being displayed as plain text.
This is in examples rather than core library code, but examples are copied into real integrations, so the examples should model a safe trust boundary: generated text, user prompts, and page-derived context should be treated as plain text unless an example explicitly opts into sanitized HTML rendering.
Issues Found
-
User/model text rendered through HTML sinks
The simple chat and Chrome extension examples used patterns like innerHTML = ... and insertAdjacentHTML(...) for strings that can include user prompts, model output, or page-derived context. This can turn HTML-looking text into markup instead of displaying it as text.
-
The bug had already reappeared once
This is a class issue, not just a one-off cleanup:
-
The original prevention guard needed to cover React examples too
The examples include React/TSX files. A robust lint rule should cover *.js, *.jsx, *.ts, and *.tsx, and should also reject React's equivalent HTML sink, dangerouslySetInnerHTML.
-
The WebGPU Chrome extension passed raw page HTML as context
examples/chrome-extension-webgpu-service-worker/src/content.js used document.body.innerHTML when passing active-tab context. The sibling Chrome extension uses document.body.innerText. For this example, visible page text is the safer default and better matches the plain-text trust boundary.
-
Copy button event listeners were registered repeatedly during streaming
In both Chrome extension popup examples, the copy-answer click listener was registered from inside updateAnswer(). Since updateAnswer() runs for streamed chunks, this could add many duplicate listeners during a single answer.
-
Adjacent example correctness issues were found while fixing this
- Several examples displayed
[System Initalize] instead of [System Initialize].
- The Next simple chat component rendered the literal string
${value.text} instead of the message text.
Expected Fix
A robust fix should:
- Render model/user/page-controlled strings with
textContent, JSX text, or direct DOM node construction, not HTML parsing.
- Preserve line breaks with CSS such as
white-space: pre-wrap rather than converting newlines to <br> and assigning innerHTML.
- Add examples-only lint enforcement so future PRs cannot reintroduce
innerHTML / outerHTML assignments, insertAdjacentHTML() calls, or React dangerouslySetInnerHTML.
- Register copy-answer event listeners once, outside streamed answer updates.
- Keep page context extraction in examples on visible text unless a future example explicitly demonstrates sanitized HTML handling.
This does not require an HTML sanitizer because these examples do not need to support model-rendered HTML. The safer default is to treat generated and user-provided strings as plain text.
Several examples currently use DOM HTML sinks for strings that come from users, model output, or page context. That means HTML-looking text can be parsed as markup instead of being displayed as plain text.
This is in examples rather than core library code, but examples are copied into real integrations, so the examples should model a safe trust boundary: generated text, user prompts, and page-derived context should be treated as plain text unless an example explicitly opts into sanitized HTML rendering.
Issues Found
User/model text rendered through HTML sinks
The simple chat and Chrome extension examples used patterns like
innerHTML = ...andinsertAdjacentHTML(...)for strings that can include user prompts, model output, or page-derived context. This can turn HTML-looking text into markup instead of displaying it as text.The bug had already reappeared once
This is a class issue, not just a one-off cleanup:
87b58ed/ PR [SimpleChat] Treat user input as text rather than HTML #370 fixedsimple-chatwith the title[SimpleChat] Treat user input as text rather than HTML.c6e6f1c/ PR [Cache] Add simple-chat upload example #366 addedsimple-chat-upload, and it reintroduced the raw template pattern by passing the prompt throughappendMessage("right", prompt), whereappendMessage()interpolated text into an HTML string and passed it toinsertAdjacentHTML().The original prevention guard needed to cover React examples too
The examples include React/TSX files. A robust lint rule should cover
*.js,*.jsx,*.ts, and*.tsx, and should also reject React's equivalent HTML sink,dangerouslySetInnerHTML.The WebGPU Chrome extension passed raw page HTML as context
examples/chrome-extension-webgpu-service-worker/src/content.jsuseddocument.body.innerHTMLwhen passing active-tab context. The sibling Chrome extension usesdocument.body.innerText. For this example, visible page text is the safer default and better matches the plain-text trust boundary.Copy button event listeners were registered repeatedly during streaming
In both Chrome extension popup examples, the copy-answer click listener was registered from inside
updateAnswer(). SinceupdateAnswer()runs for streamed chunks, this could add many duplicate listeners during a single answer.Adjacent example correctness issues were found while fixing this
[System Initalize]instead of[System Initialize].${value.text}instead of the message text.Expected Fix
A robust fix should:
textContent, JSX text, or direct DOM node construction, not HTML parsing.white-space: pre-wraprather than converting newlines to<br>and assigninginnerHTML.innerHTML/outerHTMLassignments,insertAdjacentHTML()calls, or ReactdangerouslySetInnerHTML.This does not require an HTML sanitizer because these examples do not need to support model-rendered HTML. The safer default is to treat generated and user-provided strings as plain text.