LibWeb/HTML: Implement the exception checks for Document.domain setter

This commit is contained in:
Shannon Booth 2025-06-26 15:10:40 +12:00 committed by Shannon Booth
commit 20d369b96d
Notes: github-actions[bot] 2025-06-27 06:46:56 +00:00
7 changed files with 258 additions and 2 deletions

View file

@ -3813,9 +3813,85 @@ String Document::domain() const
return effective_domain->serialize();
}
void Document::set_domain(String const& domain)
// https://html.spec.whatwg.org/multipage/browsers.html#is-a-registrable-domain-suffix-of-or-is-equal-to
static bool is_a_registrable_domain_suffix_of_or_is_equal_to(StringView host_suffix_string, URL::Host const& original_host)
{
// 1. If hostSuffixString is the empty string, then return false.
if (host_suffix_string.is_empty())
return false;
// 2. Let hostSuffix be the result of parsing hostSuffixString.
auto host_suffix = URL::Parser::parse_host(host_suffix_string);
// 3. If hostSuffix is failure, then return false.
if (!host_suffix.has_value())
return false;
// 4. If hostSuffix does not equal originalHost, then:
if (host_suffix.value() != original_host) {
// 1. If hostSuffix or originalHost is not a domain, then return false.
// NOTE: This excludes hosts that are IP addresses.
if (!host_suffix->has<String>() || !original_host.has<String>())
return false;
auto const& host_suffix_string = host_suffix->get<String>();
auto const& original_host_string = original_host.get<String>();
// 2. If hostSuffix, prefixed by U+002E (.), does not match the end of originalHost, then return false.
auto prefixed_host_suffix = MUST(String::formatted(".{}", host_suffix_string));
if (!original_host_string.ends_with_bytes(prefixed_host_suffix))
return false;
// 3. If any of the following are true:
// * hostSuffix equals hostSuffix's public suffix; or
// * hostSuffix, prefixed by U+002E (.), matches the end of originalHost's public suffix,
// then return false. [URL]
if (host_suffix_string == URL::get_public_suffix(host_suffix_string))
return false;
auto original_host_public_suffix = URL::get_public_suffix(original_host_string);
VERIFY(original_host_public_suffix.has_value());
if (original_host_public_suffix->ends_with_bytes(prefixed_host_suffix))
return false;
// 4. Assert: originalHost's public suffix, prefixed by U+002E (.), matches the end of hostSuffix.
VERIFY(host_suffix_string.ends_with_bytes(MUST(String::formatted(".{}", *original_host_public_suffix))));
}
// 5. Return true.
return true;
}
// https://html.spec.whatwg.org/multipage/browsers.html#dom-document-domain
WebIDL::ExceptionOr<void> Document::set_domain(String const& domain)
{
auto& realm = this->realm();
// 1. If this's browsing context is null, then throw a "SecurityError" DOMException.
if (!m_browsing_context)
return WebIDL::SecurityError::create(realm, "Document.domain setter requires a browsing context"_string);
// 2. If this's active sandboxing flag set has its sandboxed document.domain browsing context flag set, then throw a "SecurityError" DOMException.
if (has_flag(active_sandboxing_flag_set(), HTML::SandboxingFlagSet::SandboxedDocumentDomain))
return WebIDL::SecurityError::create(realm, "Document.domain setter is sandboxed"_string);
// 3. Let effectiveDomain be this's origin's effective domain.
auto effective_domain = origin().effective_domain();
// 4. If effectiveDomain is null, then throw a "SecurityError" DOMException.
if (!effective_domain.has_value())
return WebIDL::SecurityError::create(realm, "Document.domain setter called on a Document with a null effective domain"_string);
// 5. If the given value is not a registrable domain suffix of and is not equal to effectiveDomain, then throw a "SecurityError" DOMException.
if (!is_a_registrable_domain_suffix_of_or_is_equal_to(domain, effective_domain.value()))
return WebIDL::SecurityError::create(realm, "Document.domain setter called for an invalid domain"_string);
// FIXME: 6. If the surrounding agent's agent cluster's is origin-keyed is true, then return.
// FIXME: 7. Set this's origin's domain to the result of parsing the given value.
dbgln("(STUBBED) Document::set_domain(domain='{}')", domain);
return {};
}
void Document::set_navigation_id(Optional<String> navigation_id)

View file

@ -606,7 +606,7 @@ public:
void set_about_base_url(Optional<URL::URL> url) { m_about_base_url = url; }
String domain() const;
void set_domain(String const&);
WebIDL::ExceptionOr<void> set_domain(String const&);
auto& pending_scroll_event_targets() { return m_pending_scroll_event_targets; }
auto& pending_scrollend_event_targets() { return m_pending_scrollend_event_targets; }

View file

@ -0,0 +1,9 @@
Harness status: OK
Found 3 tests
2 Pass
1 Fail
Pass failed setting of document.domain
Fail same-origin-domain iframe
Pass failed setting of document.domain for documents without browsing context

View file

@ -0,0 +1,10 @@
Harness status: OK
Found 5 tests
5 Pass
Pass Sandboxed document.domain
Pass Sandboxed document.domain 1
Pass Sandboxed document.domain 2
Pass Sandboxed document.domain 3
Pass Sandboxed document.domain 4

View file

@ -0,0 +1,64 @@
/**
* Host information for cross-origin tests.
* @returns {Object} with properties for different host information.
*/
function get_host_info() {
var HTTP_PORT = '80';
var HTTP_PORT2 = '8000';
var HTTPS_PORT = '443';
var HTTPS_PORT2 = '8443';
var PROTOCOL = self.location.protocol;
var IS_HTTPS = (PROTOCOL == "https:");
var PORT = IS_HTTPS ? HTTPS_PORT : HTTP_PORT;
var PORT2 = IS_HTTPS ? HTTPS_PORT2 : HTTP_PORT2;
var HTTP_PORT_ELIDED = HTTP_PORT == "80" ? "" : (":" + HTTP_PORT);
var HTTP_PORT2_ELIDED = HTTP_PORT2 == "80" ? "" : (":" + HTTP_PORT2);
var HTTPS_PORT_ELIDED = HTTPS_PORT == "443" ? "" : (":" + HTTPS_PORT);
var PORT_ELIDED = IS_HTTPS ? HTTPS_PORT_ELIDED : HTTP_PORT_ELIDED;
var ORIGINAL_HOST = 'wpt.live';
var REMOTE_HOST = (ORIGINAL_HOST === 'localhost') ? '127.0.0.1' : ('www1.' + ORIGINAL_HOST);
var OTHER_HOST = 'www2.wpt.live';
var NOTSAMESITE_HOST = (ORIGINAL_HOST === 'localhost') ? '127.0.0.1' : ('not-wpt.live');
return {
HTTP_PORT: HTTP_PORT,
HTTP_PORT2: HTTP_PORT2,
HTTPS_PORT: HTTPS_PORT,
HTTPS_PORT2: HTTPS_PORT2,
PORT: PORT,
PORT2: PORT2,
ORIGINAL_HOST: ORIGINAL_HOST,
REMOTE_HOST: REMOTE_HOST,
NOTSAMESITE_HOST,
ORIGIN: PROTOCOL + "//" + ORIGINAL_HOST + PORT_ELIDED,
HTTP_ORIGIN: 'http://' + ORIGINAL_HOST + HTTP_PORT_ELIDED,
HTTPS_ORIGIN: 'https://' + ORIGINAL_HOST + HTTPS_PORT_ELIDED,
HTTPS_ORIGIN_WITH_CREDS: 'https://foo:bar@' + ORIGINAL_HOST + HTTPS_PORT_ELIDED,
HTTP_ORIGIN_WITH_DIFFERENT_PORT: 'http://' + ORIGINAL_HOST + HTTP_PORT2_ELIDED,
REMOTE_ORIGIN: PROTOCOL + "//" + REMOTE_HOST + PORT_ELIDED,
OTHER_ORIGIN: PROTOCOL + "//" + OTHER_HOST + PORT_ELIDED,
HTTP_REMOTE_ORIGIN: 'http://' + REMOTE_HOST + HTTP_PORT_ELIDED,
HTTP_NOTSAMESITE_ORIGIN: 'http://' + NOTSAMESITE_HOST + HTTP_PORT_ELIDED,
HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT: 'http://' + REMOTE_HOST + HTTP_PORT2_ELIDED,
HTTPS_REMOTE_ORIGIN: 'https://' + REMOTE_HOST + HTTPS_PORT_ELIDED,
HTTPS_REMOTE_ORIGIN_WITH_CREDS: 'https://foo:bar@' + REMOTE_HOST + HTTPS_PORT_ELIDED,
HTTPS_NOTSAMESITE_ORIGIN: 'https://' + NOTSAMESITE_HOST + HTTPS_PORT_ELIDED,
UNAUTHENTICATED_ORIGIN: 'http://' + OTHER_HOST + HTTP_PORT_ELIDED,
AUTHENTICATED_ORIGIN: 'https://' + OTHER_HOST + HTTPS_PORT_ELIDED
};
}
/**
* When a default port is used, location.port returns the empty string.
* This function attempts to provide an exact port, assuming we are running under wptserve.
* @param {*} loc - can be Location/<a>/<area>/URL, but assumes http/https only.
* @returns {string} The port number.
*/
function get_port(loc) {
if (loc.port) {
return loc.port;
}
return loc.protocol === 'https:' ? '443' : '80';
}

View file

@ -0,0 +1,76 @@
<!doctype html>
<html>
<head>
<title>document.domain's setter</title>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<script src="../../../../common/get-host-info.sub.js"></script>
</head>
<body>
<iframe id="iframe"></iframe>
<script>
var host_info = get_host_info();
var HTTP_PORT = host_info.HTTP_PORT;
var ORIGINAL_HOST = host_info.ORIGINAL_HOST;
var SUFFIX_HOST = ORIGINAL_HOST.substring(ORIGINAL_HOST.lastIndexOf('.') + 1); // e.g. "test"
var REMOTE_HOST = host_info.REMOTE_HOST;
var iframe = document.getElementById("iframe");
var iframe_url = new URL("support/document_domain_setter_iframe.html", document.location);
iframe_url.hostname = REMOTE_HOST;
iframe.src = iframe_url;
test(function() {
assert_throws_dom("SecurityError", function() { document.domain = SUFFIX_HOST; });
assert_throws_dom("SecurityError", function() { document.domain = "." + SUFFIX_HOST; });
assert_throws_dom("SecurityError", function() { document.domain = REMOTE_HOST; });
assert_throws_dom("SecurityError", function() { document.domain = "example.com"; });
}, "failed setting of document.domain");
async_test(function(t) {
iframe.addEventListener("load", t.step_func_done(function() {
// Before setting document.domain, the iframe is not
// same-origin-domain, so security checks fail.
assert_equals(iframe.contentDocument, null);
assert_throws_dom("SecurityError", () => iframe.contentWindow.frameElement);
assert_throws_dom("SecurityError", function() { iframe.contentWindow.location.origin; });
assert_throws_dom("SecurityError", function() { iframe.contentWindow.location.href; });
assert_throws_dom("SecurityError", function() { iframe.contentWindow.location.protocol; });
assert_throws_dom("SecurityError", function() { iframe.contentWindow.location.host; });
assert_throws_dom("SecurityError", function() { iframe.contentWindow.location.port; });
assert_throws_dom("SecurityError", function() { iframe.contentWindow.location.hostname; });
assert_throws_dom("SecurityError", function() { iframe.contentWindow.location.pathname; });
assert_throws_dom("SecurityError", function() { iframe.contentWindow.location.hash; });
assert_throws_dom("SecurityError", function() { iframe.contentWindow.location.search; });
assert_throws_dom("SecurityError", function() { iframe.contentWindow.location.toString(); });
// Set document.domain
document.domain = ORIGINAL_HOST;
// After setting document.domain, the iframe is
// same-origin-domain, so security checks pass.
assert_equals(iframe.contentDocument.domain, document.domain);
assert_equals(iframe.contentWindow.frameElement, iframe);
assert_equals(iframe.contentWindow.origin, iframe_url.origin);
assert_equals(iframe.contentWindow.location.href, iframe_url.href);
assert_equals(iframe.contentWindow.location.protocol, iframe_url.protocol);
assert_equals(iframe.contentWindow.location.host, iframe_url.host);
assert_equals(iframe.contentWindow.location.port, iframe_url.port);
assert_equals(iframe.contentWindow.location.hostname, iframe_url.hostname);
assert_equals(iframe.contentWindow.location.pathname, iframe_url.pathname);
assert_equals(iframe.contentWindow.location.hash, iframe_url.hash);
assert_equals(iframe.contentWindow.location.search, iframe_url.search);
assert_equals(iframe.contentWindow.location.search, iframe_url.search);
assert_equals(iframe.contentWindow.location.toString(), iframe_url.toString());
// document.open checks for same-origin, not same-origin-domain,
// https://github.com/whatwg/html/issues/2282
assert_throws_dom("SecurityError", iframe.contentWindow.DOMException,
function() { iframe.contentDocument.open(); });
}));
}, "same-origin-domain iframe");
test(() => {
assert_throws_dom("SecurityError", () => { (new Document).domain = document.domain });
assert_throws_dom("SecurityError", () => { document.implementation.createHTMLDocument().domain = document.domain });
assert_throws_dom("SecurityError", () => { document.implementation.createDocument(null, "").domain = document.domain });
}, "failed setting of document.domain for documents without browsing context");
</script>
</body>
</html>

View file

@ -0,0 +1,21 @@
<!doctype html>
<title>Sandboxed document.domain</title>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<script>
test(() => {
assert_throws_dom("SecurityError", () => { document.domain = document.domain });
});
test(() => {
assert_throws_dom("SecurityError", () => { (new Document).domain = document.domain });
});
test(() => {
assert_throws_dom("SecurityError", () => { document.implementation.createHTMLDocument().domain = document.domain });
});
test(() => {
assert_throws_dom("SecurityError", () => { document.implementation.createDocument(null, "").domain = document.domain });
});
test(() => {
assert_throws_dom("SecurityError", () => { document.createElement("template").content.ownerDocument.domain = document.domain });
});
</script>