Skip to content

[scanner] findRemoteReferences misses CSS comment- and escape-obfuscated url()/@import targets in style blocks #1091

Closed

Description

@hivecommons-hive

Finding

findRemoteReferences() in scripts/lib/svg-active-content.mjs — the gate that keeps a visitor's IP, User-Agent and Referer from reaching a host an SVG's author chose (per the validator's own threat note in scripts/validate-architecture-assets.mjs:262-269) — scans SVG style-block CSS raw. Attribute values get entity-decoding and whitespace stripping (normalizeUri, :458), but CSS text passed to cssTargets() (:405) gets neither comment removal nor escape decoding, and CSS_URL_PATTERN/CSS_IMPORT_PATTERN only match the unobfuscated forms.

Per CSS Syntax Level 3, comments are removed before tokenization and escapes in url tokens are decoded, so a browser fetches all of these — confirmed live against main @ 8afaa67:

Steps to Reproduce / Evidence

node --input-type=module -e "
import { findRemoteReferences } from './scripts/lib/svg-active-content.mjs';
findRemoteReferences('<svg><style>.a{background:url( /**/ \"https://evil.example/x\" )}</style></svg>')  // => []  MISSED
findRemoteReferences('<svg><style>@import /**/ \"https://evil.example/x\";</style></svg>')                 // => []  MISSED
findRemoteReferences('<svg><style>.a{background:url(\\68 ttps://evil.example/x)}</style></svg>')              // => []  MISSED (\\68 = 'h')
"

The unobfuscated baseline url("https://evil.example/x") is detected, so this is a gap in normalization, not in the reporting path.

This is the residual bypass class of the recently landed hardening series (#1039, #1044, #1056, #1063, #1068): each closed one parser-divergence between the scanner and a real CSS/XML parser; comment and escape handling inside CSS values is the divergence still open.

Recommendation

Normalize CSS before pattern matching in cssTargets(): strip /* ... */ comments (careful: not inside strings), and decode CSS escapes (\\XX hex plus the simple \\<char> form) in url tokens before testing with remoteTarget(). Add regression tests for the three cases above. One deliverable — a single PR against scripts/lib/svg-active-content.mjs plus tests/svg-active-content.test.mjs closes it.


Filed by scanner agent (ACMM L4 — issues-only mode)


🐝 Hive Agent: scanner | Instance: hosted-available-lke648397-260827-5n31 | SHA: unknown

— hive: agent=scanner backend=copilot model=kimi-k3 copilot=1.0.88

Activity

  1. hivecommons-hive commented on Oct 6, 2026

    @hivecommons-hive
    ContributorAuthor

    Scanner re-verification (2026-10-06, main @ 7ab301e) — confirmed, plus two additional evasion forms

    Re-ran the original three cases live against scripts/lib/svg-active-content.mjs — all still missed, control still caught:

    Case Result
    url( /**/ "https://evil.example/x.png" ) MISSED
    @import /**/ "https://evil.example/y.css" MISSED
    url("\68 ttps://evil.example/z.png") MISSED
    url("https://evil.example/ok.png") (control) caught

    Two more confirmed evasion forms in the same cssTargets/remoteTarget path, both consequences of the same missing CSS-escape decode:

    1. Six-digit escape form: url("\000068ttps://evil.example/a.png") — MISSED. CSS allows both the short \68 and the explicit six-digit \000068 spellings; a fix that only handles the short form (e.g. a single regex) leaves this open.
    2. Escape-coded // separators: url("https:\00002F\00002Fevil.example/c.png") — MISSED. Decoding escapes only before scheme matching is not enough; remoteTarget's backslash/separator normalization must run on the decoded value, because the authority separator itself can be escape-coded.

    For completeness, these variants ARE caught today and a fix must not regress them: tab-padded url(\thttps://…\t), @import url("…") functional form, URL( uppercase, and a comment embedded in the host (https://ev/**/il.example/…, flagged verbatim).

    Repro: node --input-type=module -e 'import {findRemoteReferences} from "./scripts/lib/svg-active-content.mjs"; …' with the strings above.

    Fix guidance unchanged (strip comments + full CSS-escape decode before pattern matching in cssTargets, then run the existing remoteTarget normalization on the decoded value) — just note the acceptance tests need to cover all five missed forms, not only the original three.


    scanner agent re-verification (ACMM L4 — issues-only mode)


    🐝 Hive Agent: scanner | Instance: hosted-available-lke648397-260827-5n31 | SHA: 7ab301e

    — hive: agent=scanner backend=copilot model=kimi-k3 copilot=1.0.88

  2. hivecommons-hive commented on Oct 6, 2026

    @hivecommons-hive
    ContributorAuthor

    Re-verified at 7ab301e (2026-10-06): all three evasions still pass live — url( /**/ "https://evil.example/x" ), @import /**/ "…", and \\68 ttps:// escape-coded schemes each return [] from findRemoteReferences while the plain form is caught.

    Impact has widened since filing: mirrorArtworkUrls() in scripts/lib/architecture-content.mjs now gates mirrored-SVG writes on findRemoteReferences(source) returning empty (added after 900592b). A cncf/artwork SVG using the CSS-comment spelling sails through that new write-gate and validate-architecture-assets, landing in static/ where it beacons visitor IP/UA/Referer to the remote host — the exact hot-link the mirror exists to prevent. This makes the cssTargets comment-strip/escape-decode gap a publish path, not just a validator gap.


    scanner agent (ACMM L4 — issues-only mode)


    🐝 Hive Agent: scanner | Instance: hosted-available-lke648397-260827-5n31 | SHA: unknown

    — hive: agent=scanner backend=copilot model=kimi-k3 copilot=1.0.88

  3. mrbobbytables commented on Oct 7, 2026

    @mrbobbytables
    Member

    Scope note: PR #1090 (for #1089) decodes CSS escapes before the remote-reference gate, which addresses the escape-obfuscation vector here. This issue stays open for the residue: comment-obfuscated url()/@import targets (e.g. url(ht/**/tps://...)), which #1090 does not strip.

  4. hivecommons-hive commented on Oct 7, 2026

    @hivecommons-hive
    ContributorAuthor

    Re-verified at main 8e3516f (2026-10-07). Partially fixed since the last check at 7ab301e:

    • Escape-coded schemes are now caught: cssTargets (scripts/lib/svg-active-content.mjs:477) pipes values through decodeCssEscapes (:308), and CSS_URL_PATTERN (:348) carries an escape-aware bare branch. Live test: url("\\68 ttps://evil.example/x.css") in a <style> block is reported.
    • CSS comment obfuscation is still missed: CSS_URL_PATTERN allows only \s* after url( and CSS_IMPORT_PATTERN only \s+ after @import, so a comment between tokens defeats both. Live results at HEAD:
      • url(/**/ "https://evil.example/x.css") → no finding
      • @import /**/ "https://evil.example/x.css"; → no finding
      • plain url("https://…") → reported (control)

    CSS strips comments before tokenization, so a pre-pass removing /*…*/ (or allowing comment spans in the patterns' whitespace positions) closes the residual gap.


    🐝 Hive Agent: scanner | Instance: hosted-available-lke648397-260827-5n31 | SHA: 8e3516f

    — hive: agent=scanner backend=copilot model=kimi-k3 copilot=1.0.88

  5. hivecommons-hive commented on Oct 7, 2026

    @hivecommons-hive
    ContributorAuthor

    Re-verified at main 03cfcfe (2026-10-07, post-#1159). Status unchanged since the 8e3516f check:

    • CSS comment obfuscation still missed — live at HEAD:
      • url(/**/ "https://evil.example/x.css") → no finding
      • @import /**/ "https://evil.example/y.css"; → no finding
    • All escape forms remain caught: short \\68 , six-digit \\000068, escape-coded \\00002F\\00002F separators, plus comment-in-host and plain controls.

    The residual fix is still a comment-strip pre-pass (or comment spans allowed in the patterns' whitespace positions) in cssTargets before pattern matching.


    scanner agent re-verification (ACMM L4 — issues-only mode)


    🐝 Hive Agent: scanner | Instance: hosted-available-lke648397-260827-5n31 | SHA: 03cfcfe


    🐝 Hive Agent: scanner | Instance: hosted-available-lke648397-260827-5n31 | SHA: unknown

    — hive: agent=scanner backend=copilot model=kimi-k3 copilot=1.0.88

  6. added
    hive/covered-by-prHive verified that an open PR references or claims this issue; still actionable until confirmed
    on Oct 8, 2026
  7. added
    hive/likely-doneHive verified that a merged PR references or claims this issue; pending confirmation
    and removed
    hive/covered-by-prHive verified that an open PR references or claims this issue; still actionable until confirmed
    on Oct 8, 2026
  8. hivecommons-hive commented on Oct 8, 2026

    @hivecommons-hive
    ContributorAuthor

    test comment routing


    🐝 Hive Agent: scanner | Instance: hosted-available-lke648397-260827-5n31 | SHA: e13f506

    — hive: agent=scanner backend=copilot model=kimi-k3 copilot=1.0.88

  9. hivecommons-hive commented on Oct 8, 2026

    @hivecommons-hive
    ContributorAuthor

    Verified resolved at main e13f506 (fix merged in #1176). Live-checked the three reported vectors against scripts/lib/svg-active-content.mjs: url(/**/"https://…"), @import /**/ "…", and escape-coded \\68 ttps:// are all now detected in <style> blocks — cssTargets strips CSS comments and decodes escapes before matching. tests/svg-active-content.test.mjs passes (157/157 across the four security test files). Issue closed. (Supersedes the accidental 'test comment routing' comment above, which I cannot delete with scanner-tier credentials.)

    — scanner agent (verify-once)


    🐝 Hive Agent: scanner | Instance: hosted-available-lke648397-260827-5n31 | SHA: e13f506

    — hive: agent=scanner backend=copilot model=kimi-k3 copilot=1.0.88

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent/scannerApproved by a Hive merger/owner for auto-merge on green CIbugSomething isn't workinghive/hosted-available-lke648397-260827-5n31Approved by a Hive merger/owner for auto-merge on green CIhive/likely-doneHive verified that a merged PR references or claims this issue; pending confirmation

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions