Skip to content

sanitize urls: svg/xlink href, formaction, and data: on iframe/object#21442

Open
rootvector2 wants to merge 4 commits into
emberjs:mainfrom
rootvector2:svg-anchor-url-sanitize
Open

sanitize urls: svg/xlink href, formaction, and data: on iframe/object#21442
rootvector2 wants to merge 4 commits into
emberjs:mainfrom
rootvector2:svg-anchor-url-sanitize

Conversation

@rootvector2
Copy link
Copy Markdown

@rootvector2 rootvector2 commented Jun 1, 2026

Consolidates the open URL-sanitizer fixes into one PR (per review). Each change closes a way an attacker-controlled value can reach a dangerous protocol that the sanitizer currently misses.

  • SVG anchors (case sensitivity). checkURI/checkDataURI matched element.tagName against the uppercase badTags list, but SVG tagNames come through lowercase (a), so <a href={{value}}> inside an <svg> skipped the javascript:/vbscript: check. Normalization now happens inside checkURI/checkDataURI so it is single-sourced. Also added xlink:href to the attribute list, since that is the SVG href alias and was not covered at all.
  • formaction (sanitize javascript: urls in formaction on button and input #21438). button[formaction] and input[formaction] submit to their URL, so a javascript: value there executes. Added BUTTON/INPUT to badTags and formaction to the attribute list.
  • data: on iframe[src] and object[data] (mark data: urls as unsafe on iframe[src] and object[data] #21441). A data: URL in these loads as a nested document and can run script. Added a data:-protocol check for iframe/object.
  • tab/newline stripping (strip ascii tab/newline from urls before protocol check #21433). Browsers strip ASCII tab/newline/CR from a url before navigating, so java\nscript: runs as javascript:. The fastboot url.parse path kept those chars and reported a null protocol, slipping past the check. Strip them there to match the WHATWG URL parser used on the browser path.

Tests added for each case except the tab/newline strip, which only runs on the fastboot url.parse path (the browser URL parser already strips those chars, so no integration path exercises it).

Reproductions on limber for each case are in the comment below.

// SVG element tagNames are lowercase (e.g. `a`), so they never match the
// uppercase `badTags` entries unless we normalize first.
let normalizedTag = tagName === null ? tagName : tagName.toUpperCase();
return checkURI(normalizedTag, attribute) || checkDataURI(normalizedTag, attribute);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if checkURI and checkDataURI did this normalization instead, would all your PRs be able to just be one PR?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Moved the normalization into checkURI/checkDataURI and dropped the toUpperCase from requiresSanitization and sanitizeAttributeValue, so it's single sourced now.

It wouldn't fold all of them into one though. Normalizing here only closes the case-sensitivity gap. The other open ones add new entries to the lists (formaction in #21438, data: on iframe/object in #21441) or change the url stripping itself (#21433), so they're independent of the tag/attribute matching. Happy to squash the related ones if that's easier to review.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add the tests from all your other PRs?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Folded in formaction (#21438) and data: on iframe[src]/object[data] (#21441) with their tests, plus the tab/newline strip from #21433. That last one only touches the fastboot url.parse path (the WHATWG parser already strips those chars), so there's no integration test that actually exercises it, which is why the original had none. Skipped #21430 since you already rebased it into #21434. Happy to put together limber reproductions for these if that helps.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you close the other PRs?

yes, reproduction will be required for each issue <3

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Closed #21433 just now. #21438, #21441 and #21430 were already closed, so this is the only sanitizer PR left. The one I left open is #21436, which is the routing prefix regex-escaping fix and unrelated to the sanitizer, so it's independent of this - happy to close that too if you'd rather. Reproductions for each case are posted in a comment below.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you update the PR description and title with the new contents of this PR?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Retitled it and rewrote the description to cover all four cases (svg/xlink, formaction, data: on iframe/object, and the tab/newline strip) with the limber repros linked.

return (tagName === null || has(badTags, tagName)) && has(badAttributes, attribute);
// SVG element tagNames are lowercase (e.g. `a`), so they never match the
// uppercase `badTags` entries unless we normalize first.
let normalizedTag = tagName === null ? tagName : tagName.toUpperCase();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this line probably isn't needed anymore

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, gone. checkURI just does has(badTags, tagName.toUpperCase()) behind the existing null check now.

simplify checkURI to inline the tag normalization, and bring in the
other open sanitization gaps so they live behind the same tag/attribute
matching:
- formaction on button/input
- data: protocol on iframe[src] and object[data]
- strip ascii tab/newline/cr before the fastboot url protocol check
@rootvector2
Copy link
Copy Markdown
Author

Reproductions on limber, one per case. Each binds the attacker value to the attribute so it goes through the sanitizer. On a released Glimmer the dangerous protocol survives in the DOM; with this PR it comes back as unsafe:.

The tab/newline strip from #21433 doesn't have a browser repro - that path only runs under fastboot's url.parse. In the browser the WHATWG URL parser already strips \t\n\r, so java\nscript: is caught there regardless.

@rootvector2 rootvector2 changed the title sanitize urls on svg anchor href and xlink:href sanitize urls: svg/xlink href, formaction, and data: on iframe/object Jun 1, 2026
@rootvector2
Copy link
Copy Markdown
Author

Folded the AREA case in here too (it's the same badTags selection, an <area href> in an image map navigates on click just like an <a>). Repro, same shape as the others:

Click the box and on a released Glimmer it fires; with this PR the href comes back as unsafe:.

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.

2 participants