Apply safe_href scheme allowlist to images in safe_mode#714
Conversation
process_anchor() validates link URLs against _safe_href (allowlist: http(s)/ftp/mailto/tel) in safe_mode, but process_image() only ran the src through _protect_url(), which escapes <>"' but does not restrict the URL scheme. As a result, in safe_mode an image like `)` rendered as `<img src="javascript:alert(1)">` — inconsistent with how `<a href>` is handled. This is defense-in-depth (javascript: does not execute in <img src> on modern browsers), but the inconsistency is surprising for a sanitizer and could matter for non-browser / legacy HTML consumers. Apply the same allowlist to images; legitimate absolute/relative image URLs are unaffected, and default (non-safe) mode is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Sounds sane but has some failing tests that look a bit concerning |
The previous version applied the full `_safe_href` allowlist to images, which stripped legitimate `data:image/...` and quote-escaped `http` src values that markdown2 intentionally supports (broke data_urls_in_safe_mode, basic_safe_mode, issue603_xss). Keep markdown2's existing image model (quote-escaping via _protect_url) and only neutralize `javascript:`/`vbscript:` schemes, which are never valid for an <img src> and exist purely as XSS vectors. All 234 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Good catch, thanks — you were right to be wary. The original diff was too broad: it applied the full I have narrowed it to keep markdown2's existing image model (quote-escaping via To be transparent: this is defense-in-depth — 🤖 Addressed by Claude Code |
|
@Crozzers would love your opinion on this one |
| # `src` and only serve as XSS vectors. `data:` images, http(s) and | ||
| # relative URLs are preserved (and quote-escaped by `_protect_url`), | ||
| # matching markdown2's existing image handling. | ||
| normalized = re.sub(r"[\s\x00-\x1f]+", "", url).lower() |
There was a problem hiding this comment.
Is this line necessary? Seems to be subbing out any whitespace or special ascii chars like NULL, but from my testing, a char like that makes the schema invalid.
I tested these snippets in a browser:
<a href="\x00javascript:alert(1)">abc</a>
<a href="java\x00script:alert(1)">abc</a>
<a href="javascript\x00:alert(1)">abc</a>
<a href="java script:alert(1)">abc</a>None of them produced an alert when clicked.
This should either have a comment added that explains why this is needed (existing comment just explains what is happening, not why), or should be replaced with if url.lower().startswith(...):
| # matching markdown2's existing image handling. | ||
| normalized = re.sub(r"[\s\x00-\x1f]+", "", url).lower() | ||
| if normalized.startswith(("javascript:", "vbscript:")): | ||
| safe_src = "" |
There was a problem hiding this comment.
For anchors, the URL is normalised to href="#". Should we do that here as well for consistency, or is there a reason to leave it empty?
Summary
In
safe_mode,process_anchor()validates link URLs against_safe_href(allowlist:
http(s),ftp,mailto,tel), butprocess_image()only passesthe
srcthrough_protect_url(), which escapes<>"'but does not restrict theURL scheme. So in
safe_mode:while the equivalent link is correctly neutralized to
href="#". This is aninconsistency within the sanitizer's own model.
Impact
To be clear, this is not an exploitable XSS:
javascript:/data:URIs do notexecute in
<img src>on modern browsers. I'm raising it as defense-in-depth /consistency — the behavior is surprising for a sanitizer and could matter for
non-browser or legacy HTML consumers of the output.
Fix
Apply the same
_safe_hrefscheme allowlist to images that links already use.Behavior before/after (
safe_mode='escape'))src="javascript:alert(1)"src=""src=""safe_mode)Legitimate absolute/relative image URLs are unaffected, and default (non-safe) mode
is unchanged.