Skip to content

Cover the SVG vectors that defeat regex sanitizing - #163

Merged
ccfiel merged 1 commit into
mainfrom
test/svg-sanitizer-vectors
Jul 30, 2026
Merged

Cover the SVG vectors that defeat regex sanitizing#163
ccfiel merged 1 commit into
mainfrom
test/svg-sanitizer-vectors

Conversation

@ccfiel

@ccfiel ccfiel commented Jul 30, 2026

Copy link
Copy Markdown
Member

Summary

sanitizeSvg here is a real parser — blocked-element stack, attribute allowlisting, safeUrlAttributeValue that strips control characters before validating. But its tests were never extended past the original three, and those three pass against the old five-regex implementation too. Nothing in CI distinguishes the hardened version from the naive one, so a well-meaning simplification would reopen every hole silently.

This adds the vectors the regex version let through. I verified each one bypasses the old implementation before writing the assertion:

BYPASSES OLD  unquoted javascript: href       -> <a href=javascript:alert(1)>
BYPASSES OLD  leading-space javascript:       -> <a href=" javascript:alert(1)">
BYPASSES OLD  iframe element                  -> <iframe src="https://evil.test/x">
BYPASSES OLD  embed element                   -> <embed src="https://evil.test/x"/>
BYPASSES OLD  object element                  -> <object data="https://evil.test/x">
BYPASSES OLD  self-closing foreignObject      -> <foreignObject/>
BYPASSES OLD  style attribute                 -> style="background:url(https://evil.test/x)"
BYPASSES OLD  external xlink:href             -> <use xlink:href="https://evil.test/x#a"/>

All pass against the current parser. Two cases go the other way — same-document #fragment refs and ordinary viewBox/path/fill markup must survive untouched — so the tests pin behavior, not just rejection.

Tests only. No production code changes.

Why now

The cloud fork is still running the old regex version of this function, and cloud serves both workspace logos and user signatures through it. The port is PageDen/pageden-cloud#281. These tests go in first so both repos land on the same guarantees, and so public can't drift back.

To be clear about severity: the serve routes send default-src 'none'; sandbox plus nosniff, which is the actual barrier — the code comment says as much and it's correct. This is defense-in-depth, not a live exploit.

Numbers

  • Server unit: 184 / 184 ✓ (18 in logo.test.ts, up from 5)
  • Typecheck: ✓
  • Lint: ✓

Test plan

  • pnpm -r typecheck
  • pnpm -r lint
  • pnpm --filter @pageden/server test
  • CI: e2e + CodeQL

sanitizeSvg was rewritten from five regex replaces into a real parser, but
the tests were never extended -- the existing three cases pass against both
implementations, so nothing stops someone "simplifying" it back and
silently reopening the holes.

Adds the vectors the regex version let through, each verified to bypass it:
unquoted javascript: hrefs, javascript: behind leading whitespace or a tab,
iframe/embed/object/video/audio/canvas elements, self-closing
foreignObject, inline style attributes, and external xlink:href. Plus two
cases in the other direction -- same-document #fragment refs and ordinary
drawing markup must survive untouched.
@ccfiel
ccfiel merged commit 7416365 into main Jul 30, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant