Skip to content

Reject cross-origin URLs in history.pushState() and replaceState() - #1194

Open
Vidya-m78 wants to merge 1 commit into
HtmlUnit:masterfrom
Vidya-m78:history-state-same-origin
Open

Vidya-m78 wants to merge 1 commit into
HtmlUnit:masterfrom
Vidya-m78:history-state-same-origin

Conversation

@Vidya-m78

Copy link
Copy Markdown
Contributor

Repro: a page on http://attacker.test/ calls history.replaceState(null, '', 'http://victim.test/'). From then on location.href and document.domain report victim.test, document.cookie returns the (non-HttpOnly) cookies the WebClient holds for victim.test, and a plain XMLHttpRequest to http://victim.test/api without any CORS headers returns 200 with the body, where the same request failed before the call. After replaceState(null, '', 'file:///') the page can add an <iframe src="file:///...html"> and read the local file, because the local resource check in BaseFrameElement looks at the page url. Chrome and Firefox throw a SecurityError for these calls.
Cause: History.buildNewStateUrl() resolves the url argument and pushState()/replaceState() write it into the url of the page without comparing origins. HtmlUnit takes the origin of a page from that url (cookies, the same-origin decision of XMLHttpRequest, local file access), so the page can pick any origin it likes.
Fix: throw a SecurityError from buildNewStateUrl() if the new url is not same origin (UrlUtils.isSameOrigin(): scheme, host and port) with the document url, following the "can have its URL rewritten" steps of the HTML spec. Both methods go through this helper; same-origin changes of path, query and fragment work as before. The stricter spec rules for file: paths and non-http schemes are not part of this change.

pushStateCrossOrigin, replaceStateCrossOrigin and replaceStateFileUrl in History2Test fail without the change (the page url becomes http://localhost:22223/ or file:///). History2Test, HistoryTest, org.htmlunit.History2Test, VueTest, the htmx 1.7.0, 1.9.12 and 2.0.10 library tests and CodeStyleTest pass. I also added a changes.xml entry.

buildNewStateUrl() resolved the url argument and History then wrote it
into the URL of the page without any origin check, so a page could set
its URL to any other origin or scheme. HtmlUnit takes the origin of a
page from that URL (document.cookie, the same-origin check of
XMLHttpRequest, the local resource check for frames), so after
history.replaceState(null, '', 'http://other/') a page could read the
cookies of the other site and read its responses without CORS.

Throw a SecurityError if the new url is not same origin with the
document url, like the browsers do.
@sonarqubecloud

Copy link
Copy Markdown

@rbri

rbri commented Nov 13, 2026

Copy link
Copy Markdown
Member

Thanks, will have a look on monday

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