diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index add66177e8..0754214a6f 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 570ed34a6f..b6d98c9e4c 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 0fee4fbfd3..745d6715fd 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
*/