Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@

<body>
<release version="5.8.0" date="xxxx xx, 2026" description="Chrome/Edge 157, Bugfixes">
<action type="fix" dev="Vidya m">
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.
</action>
<action type="fix" dev="rbri">
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
Expand Down
15 changes: 14 additions & 1 deletion src/main/java/org/htmlunit/javascript/host/History.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down
81 changes: 81 additions & 0 deletions src/test/java/org/htmlunit/javascript/host/History2Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
+ "<html>\n"
+ "<head>\n"
+ "<script>\n"
+ LOG_TITLE_FUNCTION
+ " function test() {\n"
+ " log('href=' + location.href);\n"
+ " try {\n"
+ " window.history.pushState(null, '', '" + URL_CROSS_ORIGIN_BASE + "');\n"
+ " } catch(e) { log(e.name); }\n"
+ " log('href=' + location.href);\n"
+ " }\n"
+ "</script>\n"
+ "</head>\n"
+ "<body onload='test()'>\n"
+ "</body></html>";

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
+ "<html>\n"
+ "<head>\n"
+ "<script>\n"
+ LOG_TITLE_FUNCTION
+ " function test() {\n"
+ " log('href=' + location.href);\n"
+ " try {\n"
+ " window.history.replaceState(null, '', '" + URL_CROSS_ORIGIN_BASE + "');\n"
+ " } catch(e) { log(e.name); }\n"
+ " log('href=' + location.href);\n"
+ " }\n"
+ "</script>\n"
+ "</head>\n"
+ "<body onload='test()'>\n"
+ "</body></html>";

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
+ "<html>\n"
+ "<head>\n"
+ "<script>\n"
+ LOG_TITLE_FUNCTION
+ " function test() {\n"
+ " log('href=' + location.href);\n"
+ " try {\n"
+ " window.history.replaceState(null, '', 'file:///');\n"
+ " } catch(e) { log(e.name); }\n"
+ " log('href=' + location.href);\n"
+ " }\n"
+ "</script>\n"
+ "</head>\n"
+ "<body onload='test()'>\n"
+ "</body></html>";

expandExpectedAlertsVariables(URL_FIRST);
loadPageVerifyTitle2(html);
}

/**
* @throws Exception if an error occurs
*/
Expand Down
Loading