Skip to content

cppa-cursor-browser: browser-level test that a rendered payload does not execute #142

Description

@clean6378-max-it

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

  • A headless-browser test renders content through the real page path (Marked + DOMPurify + DOM insertion).
  • It asserts <img src=x onerror=alert(1)>, <script>, a javascript: URI, and an SVG vector do not execute.
  • The test fails if DOMPurify.sanitize is removed from the render path; passes on current code.
  • Playwright is pinned and wired into CI.
  • Static-grep tests retained as a backstop.
  • CI green; PR approved by at least 1 reviewer.

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

  • A headless-browser test renders content through the real page path (Marked + DOMPurify + DOM insertion).
  • It asserts <img src=x onerror=alert(1)>, <script>, a javascript: URI, and an SVG vector do not execute.
  • The test fails if DOMPurify.sanitize is removed from the render path; passes on current code.
  • Playwright is pinned and wired into CI.
  • Static-grep tests retained as a backstop.
  • CI green; PR approved by at least 1 reviewer.

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

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions