Severity: 🔴 Critical (verified)
Location
src/ssr/renderer.ts:195-197 (setText) and :418 (bq-text handler), interacting with src/ssr/html-parser.ts:358-362 (serializeTree raw-element branch). This is the default backend on all server runtimes without a global DOMParser (Node/Bun/Deno — see src/ssr/config.ts).
Description
setText writes the evaluated value directly as a text child with no escaping:
const setText = (el: SSRElement, value: string): void => {
el.children = [{ type: 'text', value }];
};
For raw-text elements (script, style, textarea, title — see html-parser.ts:34), serializeTree emits text children verbatim, without escaping:
if (el.raw) {
for (const child of el.children) {
if (child.type === 'text') inner += child.value; // no escaping
}
}
So an untrusted bq-text value on a <textarea> or <title> can close the element and inject live markup. The author was aware of exactly this hazard for bq-model — renderer.ts:234-238 escapes via el.raw ? escapeText(...) with a comment naming the XSS — but the identical protection is missing on the bq-text path (renderer.ts:418 calls setText(el, String(value ?? ''))).
Reproduction (executed)
Input: <textarea bq-text="msg"></textarea> msg = '</textarea><img src=x onerror=alert(1)>'
Output: <textarea bq-text="msg"></textarea><img src=x onerror=alert(1)></textarea>
Input: <title bq-text="msg"></title> msg = '</title><script>alert(1)</script>'
Output: <title bq-text="msg"></title><script>alert(1)</script></title>
The browser closes the textarea/title at the injected close tag, then executes the following <img onerror> / <script>. bq-text on a <textarea>/<title> is a completely ordinary pattern requiring no developer misuse, and the value is exactly where untrusted data normally flows. (The legacy DOM-backed path in render.ts, which uses textContent + escapeHtmlText, is not affected — only the default pure renderer.)
Suggested fix
Escape when the target is a raw-text element, mirroring the existing bq-model handling:
const setText = (el: SSRElement, value: string): void => {
el.children = [{ type: 'text', value: el.raw ? escapeText(value) : value }];
};
Filed as part of a full-codebase security & correctness audit.
Severity: 🔴 Critical (verified)
Location
src/ssr/renderer.ts:195-197(setText) and:418(bq-texthandler), interacting withsrc/ssr/html-parser.ts:358-362(serializeTreeraw-element branch). This is the default backend on all server runtimes without a globalDOMParser(Node/Bun/Deno — seesrc/ssr/config.ts).Description
setTextwrites the evaluated value directly as a text child with no escaping:For raw-text elements (
script,style,textarea,title— seehtml-parser.ts:34),serializeTreeemits text children verbatim, without escaping:So an untrusted
bq-textvalue on a<textarea>or<title>can close the element and inject live markup. The author was aware of exactly this hazard forbq-model—renderer.ts:234-238escapes viael.raw ? escapeText(...)with a comment naming the XSS — but the identical protection is missing on thebq-textpath (renderer.ts:418callssetText(el, String(value ?? ''))).Reproduction (executed)
The browser closes the
textarea/titleat the injected close tag, then executes the following<img onerror>/<script>.bq-texton a<textarea>/<title>is a completely ordinary pattern requiring no developer misuse, and the value is exactly where untrusted data normally flows. (The legacy DOM-backed path inrender.ts, which usestextContent+escapeHtmlText, is not affected — only the default pure renderer.)Suggested fix
Escape when the target is a raw-text element, mirroring the existing
bq-modelhandling:Filed as part of a full-codebase security & correctness audit.