From eb049a924f6de1e715296354d9ede83433d18f84 Mon Sep 17 00:00:00 2001 From: Vidya m Date: Fri, 13 Nov 2026 21:14:29 +0530 Subject: [PATCH] Reject cross-origin URLs in history.pushState() and replaceState() 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. --- src/changes/changes.xml | 5 ++ .../org/htmlunit/javascript/host/History.java | 15 +++- .../javascript/host/History2Test.java | 81 +++++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index add66177e84..0754214a6f2 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -8,6 +8,11 @@ + + History.pushState() and History.replaceState() throw a SecurityError if the url is not same origin + with the document; a page was able to change its url (and by this its origin for cookies, + XMLHttpRequest and local files) to any other url. + DomNode.querySelector(String) and DomNode.querySelectorAll(String) are available again (removed in 5.7.0 by mistake); like element.querySelector() in the browsers, the node itself is the scope diff --git a/src/main/java/org/htmlunit/javascript/host/History.java b/src/main/java/org/htmlunit/javascript/host/History.java index 570ed34a6f1..b6d98c9e4c8 100644 --- a/src/main/java/org/htmlunit/javascript/host/History.java +++ b/src/main/java/org/htmlunit/javascript/host/History.java @@ -27,7 +27,9 @@ import org.htmlunit.javascript.configuration.JsxFunction; import org.htmlunit.javascript.configuration.JsxGetter; import org.htmlunit.javascript.configuration.JsxSetter; +import org.htmlunit.javascript.host.dom.DOMException; import org.htmlunit.util.StringUtils; +import org.htmlunit.util.UrlUtils; /** * JavaScript host object for the client's browsing history. @@ -156,13 +158,24 @@ public void pushState(final Object object, final String title, final Object url) } } - private static URL buildNewStateUrl(final WebWindow webWindow, final Object url) throws MalformedURLException { + private URL buildNewStateUrl(final WebWindow webWindow, final Object url) throws MalformedURLException { URL newStateUrl = null; if (url != null && !JavaScriptEngine.isUndefined(url)) { final String urlString = JavaScriptEngine.toString(url); if (StringUtils.isNotBlank(urlString)) { final HtmlPage page = (HtmlPage) webWindow.getEnclosedPage(); newStateUrl = page.getFullyQualifiedUrl(urlString); + + // the document url can only be changed inside the origin of the document + // https://html.spec.whatwg.org/multipage/nav-history-apis.html#can-have-its-url-rewritten + if (!UrlUtils.isSameOrigin(page.getUrl(), newStateUrl)) { + throw JavaScriptEngine.asJavaScriptException( + getWindow(), + "A history state object with URL '" + newStateUrl.toExternalForm() + + "' cannot be created in a document with URL '" + + page.getUrl().toExternalForm() + "'.", + DOMException.SECURITY_ERR); + } } } return newStateUrl; diff --git a/src/test/java/org/htmlunit/javascript/host/History2Test.java b/src/test/java/org/htmlunit/javascript/host/History2Test.java index 0fee4fbfd3b..745d6715fde 100644 --- a/src/test/java/org/htmlunit/javascript/host/History2Test.java +++ b/src/test/java/org/htmlunit/javascript/host/History2Test.java @@ -963,6 +963,87 @@ public void replaceStateChangeHashNoStore() throws Exception { verifyTitle2(driver, getExpectedAlerts()); } + /** + * @throws Exception if an error occurs + */ + @Test + @Alerts({"href=§§URL§§", "SecurityError", "href=§§URL§§"}) + public void pushStateCrossOrigin() throws Exception { + final String html = DOCTYPE_HTML + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + ""; + + expandExpectedAlertsVariables(URL_FIRST); + loadPageVerifyTitle2(html); + } + + /** + * @throws Exception if an error occurs + */ + @Test + @Alerts({"href=§§URL§§", "SecurityError", "href=§§URL§§"}) + public void replaceStateCrossOrigin() throws Exception { + final String html = DOCTYPE_HTML + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + ""; + + expandExpectedAlertsVariables(URL_FIRST); + loadPageVerifyTitle2(html); + } + + /** + * @throws Exception if an error occurs + */ + @Test + @Alerts({"href=§§URL§§", "SecurityError", "href=§§URL§§"}) + public void replaceStateFileUrl() throws Exception { + final String html = DOCTYPE_HTML + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + ""; + + expandExpectedAlertsVariables(URL_FIRST); + loadPageVerifyTitle2(html); + } + /** * @throws Exception if an error occurs */