Sprint Item
#3 of 8 — Medium severity — after #1 + #2 (Monday PR 3 of 3)
Block Relations
|
|
| Blocked by |
#1, #2 (soft on #2) — prove the full defense-in-depth render path |
| Blocks |
— |
| Repo/day |
Independent of #4–#8 |
Calendar Day
Monday, July 20, 2026 (PR 3 of 3)
Planned Effort
5 story points (Medium) — sprint item #3
Problem
tests/test_xss_sanitization.py only greps source for the sanitize call — it proves the code contains DOMPurify.sanitize, not that a payload is neutralized. Nothing runs the frontend to prove an untrusted payload does not execute in a real DOM. This is the "verification stops at the DOM boundary" gap: unit greps cannot catch a broken render path.
Goal
One PR that introduces a headless-browser test driving the real render path (renderMarkdownSafe → Marked + DOMPurify + DOM insertion) and asserts that representative XSS vectors do not execute, and that the guard fails if DOMPurify.sanitize is removed.
Scope
Part A — headless-browser harness
- Introduce Playwright (headless, pinned in CI).
- Drive the real page path — render untrusted content through
renderMarkdownSafe (static/js/app.js) exactly as production does, not a reimplemented path.
Part B — execution assertions
Render each vector and assert no execution (e.g. no alert, no error handler firing, sink neutralized):
<img src=x onerror=alert(1)>
<script> tag
- a
javascript: URI
- an SVG-based vector
Part C — guard-proves-the-guard
- The test must fail if
DOMPurify.sanitize is removed from the render path, and pass on current code.
Part D — keep the cheap backstop
- Retain the existing static-grep tests as a fast pre-check; the browser test is the authoritative gate.
Out of scope
Acceptance Criteria
Verification
cd C:\Users\Jasen\CppAliance\cppa-cursor-browser
.\.venv\Scripts\Activate.ps1
# install pinned Playwright browsers per the added CI step, then:
pytest -q tests/test_xss_sanitization.py
# negative control: temporarily remove DOMPurify.sanitize from the render path -> test must FAIL
Sprint Item
#3 of 8 — Medium severity — after #1 + #2 (Monday PR 3 of 3)
Block Relations
|
|
| Blocked by |
#1, #2 (soft on #2) — prove the full defense-in-depth render path |
| Blocks |
— |
| Repo/day |
Independent of #4–#8 |
Calendar Day
Monday, July 20, 2026 (PR 3 of 3)
Planned Effort
5 story points (Medium) — sprint item #3
Problem
tests/test_xss_sanitization.py only greps source for the sanitize call — it proves the code contains DOMPurify.sanitize, not that a payload is neutralized. Nothing runs the frontend to prove an untrusted payload does not execute in a real DOM. This is the "verification stops at the DOM boundary" gap: unit greps cannot catch a broken render path.
Goal
One PR that introduces a headless-browser test driving the real render path (renderMarkdownSafe → Marked + DOMPurify + DOM insertion) and asserts that representative XSS vectors do not execute, and that the guard fails if DOMPurify.sanitize is removed.
Scope
Part A — headless-browser harness
- Introduce Playwright (headless, pinned in CI).
- Drive the real page path — render untrusted content through
renderMarkdownSafe (static/js/app.js) exactly as production does, not a reimplemented path.
Part B — execution assertions
Render each vector and assert no execution (e.g. no alert, no error handler firing, sink neutralized):
<img src=x onerror=alert(1)>
<script> tag
- a
javascript: URI
- an SVG-based vector
Part C — guard-proves-the-guard
- The test must fail if
DOMPurify.sanitize is removed from the render path, and pass on current code.
Part D — keep the cheap backstop
- Retain the existing static-grep tests as a fast pre-check; the browser test is the authoritative gate.
Out of scope
Acceptance Criteria
Verification
cd C:\Users\Jasen\CppAliance\cppa-cursor-browser
.\.venv\Scripts\Activate.ps1
# install pinned Playwright browsers per the added CI step, then:
pytest -q tests/test_xss_sanitization.py
# negative control: temporarily remove DOMPurify.sanitize from the render path -> test must FAIL
Sprint Item
#3 of 8 — Medium severity — after #1 + #2 (Monday PR 3 of 3)
Block Relations
Calendar Day
Monday, July 20, 2026 (PR 3 of 3)
Planned Effort
5 story points (Medium) — sprint item #3
Problem
tests/test_xss_sanitization.pyonly greps source for the sanitize call — it proves the code containsDOMPurify.sanitize, not that a payload is neutralized. Nothing runs the frontend to prove an untrusted payload does not execute in a real DOM. This is the "verification stops at the DOM boundary" gap: unit greps cannot catch a broken render path.Goal
One PR that introduces a headless-browser test driving the real render path (
renderMarkdownSafe→ Marked + DOMPurify + DOM insertion) and asserts that representative XSS vectors do not execute, and that the guard fails ifDOMPurify.sanitizeis removed.Scope
Part A — headless-browser harness
renderMarkdownSafe(static/js/app.js) exactly as production does, not a reimplemented path.Part B — execution assertions
Render each vector and assert no execution (e.g. no
alert, no error handler firing, sink neutralized):<img src=x onerror=alert(1)><script>tagjavascript:URIPart C — guard-proves-the-guard
DOMPurify.sanitizeis removed from the render path, and pass on current code.Part D — keep the cheap backstop
Out of scope
feat/csp-header) — this test may additionally assert the CSP header is present, but it does not implement CSP.Acceptance Criteria
<img src=x onerror=alert(1)>,<script>, ajavascript:URI, and an SVG vector do not execute.DOMPurify.sanitizeis removed from the render path; passes on current code.Verification
Sprint Item
#3 of 8 — Medium severity — after #1 + #2 (Monday PR 3 of 3)
Block Relations
Calendar Day
Monday, July 20, 2026 (PR 3 of 3)
Planned Effort
5 story points (Medium) — sprint item #3
Problem
tests/test_xss_sanitization.pyonly greps source for the sanitize call — it proves the code containsDOMPurify.sanitize, not that a payload is neutralized. Nothing runs the frontend to prove an untrusted payload does not execute in a real DOM. This is the "verification stops at the DOM boundary" gap: unit greps cannot catch a broken render path.Goal
One PR that introduces a headless-browser test driving the real render path (
renderMarkdownSafe→ Marked + DOMPurify + DOM insertion) and asserts that representative XSS vectors do not execute, and that the guard fails ifDOMPurify.sanitizeis removed.Scope
Part A — headless-browser harness
renderMarkdownSafe(static/js/app.js) exactly as production does, not a reimplemented path.Part B — execution assertions
Render each vector and assert no execution (e.g. no
alert, no error handler firing, sink neutralized):<img src=x onerror=alert(1)><script>tagjavascript:URIPart C — guard-proves-the-guard
DOMPurify.sanitizeis removed from the render path, and pass on current code.Part D — keep the cheap backstop
Out of scope
feat/csp-header) — this test may additionally assert the CSP header is present, but it does not implement CSP.Acceptance Criteria
<img src=x onerror=alert(1)>,<script>, ajavascript:URI, and an SVG vector do not execute.DOMPurify.sanitizeis removed from the render path; passes on current code.Verification