CVE-2025-31205
Overview
Impact:
Description: A malicious website may exfiltrate data cross-origin
Researchers: Ivan Fratric of Google Project Zero
Key insight. canAccessRules() fell open (returned true) when a stylesheet had no owner document, and CSSOM wrappers weren’t given the correct origin-clean flag, so cross-origin stylesheet rules became readable — an SOP bypass; the fix denies access when the owner document is gone and propagates the real origin-clean state.
Patch Details
diff --git a/LayoutTests/http/tests/security/access-cssstylesheet-after-removing-from-document-expected.txt b/LayoutTests/http/tests/security/access-cssstylesheet-after-removing-from-document-expected.txt
new file mode 100644
index 000000000000..cc77e09161e7
--- /dev/null
+++ b/LayoutTests/http/tests/security/access-cssstylesheet-after-removing-from-document-expected.txt
@@ -0,0 +1,33 @@
+Tests accessing CSSStyleSheet after removing from the document
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS () => stylesheet.cssRules did not throw exception.
+PASS () => stylesheet.insertRule('#element2 { margin: 10px; }') did not throw exception.
+PASS () => stylesheet.deleteRule(0) did not throw exception.
+PASS () => stylesheet.cssRules threw exception SecurityError: Not allowed to access cross-origin stylesheet.
+PASS () => stylesheet.insertRule('body { margin: 10px; } ') threw exception SecurityError: Not allowed to insert rule into cross-origin stylesheet.
+PASS () => stylesheet.deleteRule(0) threw exception SecurityError: Not allowed to delete rule from cross-origin stylesheet.
+PASS () => stylesheet.cssRules did not throw exception.
+PASS () => stylesheet.insertRule('#element2 { margin: 10px; }') did not throw exception.
+PASS () => stylesheet.deleteRule(0) did not throw exception.
+PASS () => stylesheet.cssRules did not throw exception.
+PASS () => stylesheet.insertRule('#element2 { margin: 10px; }') did not throw exception.
+PASS () => stylesheet.deleteRule(0) did not throw exception.
+PASS () => stylesheet.cssRules threw exception SecurityError: Not allowed to access cross-origin stylesheet.
+PASS () => stylesheet.insertRule('body { margin: 10px; } ') threw exception SecurityError: Not allowed to insert rule into cross-origin stylesheet.
+PASS () => stylesheet.deleteRule(0) threw exception SecurityError: Not allowed to delete rule from cross-origin stylesheet.
+PASS () => stylesheet.cssRules threw exception SecurityError: Not allowed to access cross-origin stylesheet.
+PASS () => stylesheet.insertRule('body { margin: 10px; } ') threw exception SecurityError: Not allowed to insert rule into cross-origin stylesheet.
+PASS () => stylesheet.deleteRule(0) threw exception SecurityError: Not allowed to delete rule from cross-origin stylesheet.
+PASS () => stylesheet.cssRules did not throw exception.
+PASS () => stylesheet.insertRule('#element2 { margin: 10px; }') did not throw exception.
+PASS () => stylesheet.deleteRule(0) did not throw exception.
+PASS () => stylesheet.cssRules threw exception SecurityError: Not allowed to access cross-origin stylesheet.
+PASS () => stylesheet.insertRule('body { margin: 10px; } ') threw exception SecurityError: Not allowed to insert rule into cross-origin stylesheet.
+PASS () => stylesheet.deleteRule(0) threw exception SecurityError: Not allowed to delete rule from cross-origin stylesheet.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/security/access-cssstylesheet-after-removing-from-document.html b/LayoutTests/http/tests/security/access-cssstylesheet-after-removing-from-document.html
new file mode 100644
index 000000000000..4be151a8584c
--- /dev/null
+++ b/LayoutTests/http/tests/security/access-cssstylesheet-after-removing-from-document.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+
+<head>
+
+<title>Accessing CSSStyleSheet after removing from document</title>
+<script src="/resources/js-test-pre.js"></script>
+
+<!--
+ Explicitly use <link> here instead of dynamically creating <link> elements
+ in JavaScript, so they're loaded before the JavaScript runs.
+-->
+
+<!-- Inline style -->
+<style id="local-style">#element { margin: 10px; }</style>
+
+<!-- Same origin -->
+<link class="test-stylesheet"
+ rel="stylesheet"
+ href="/security/resources/cssStyle.css"
+ data-accessible="true">
+
+<!-- Cross origin, no CORS -->
+<link class="test-stylesheet"
+ rel="stylesheet"
+ href="http://localhost:8000/security/resources/cssStyle.css"
+ data-accessible="false">
+
+<!-- Cross origin, CORS succeeds -->
+<link class="test-stylesheet"
+ rel="stylesheet"
+ href="http://localhost:8000/security/resources/xorigincss1-allow-star.py"
+ crossorigin=""
+ data-accessible="true">
+
+<!-- Same origin -> same origin -->
+<link class="test-stylesheet"
+ rel="stylesheet"
+ href="/resources/redirect.py?url=http://127.0.0.1:8000/security/resources/cssStyle.css"
+ data-accessible="true">
+
+<!-- Same origin -> cross origin, no CORS -->
+<link class="test-stylesheet"
+ rel="stylesheet"
+ href="/resources/redirect.py?url=http://localhost:8000/security/resources/cssStyle.css"
+ data-accessible="false">
+
+<!-- Cross origin -> same origin, no CORS -->
+<link class="test-stylesheet"
+ rel="stylesheet"
+ href="http://localhost:8000/resources/redirect.py?url=http://127.0.0.1:8000/security/resources/cssStyle.css"
+ data-accessible="false">
+
+<!-- Cross origin -> same origin, CORS succeeds -->
+<link class="test-stylesheet"
+ rel="stylesheet"
+ href="http://localhost:8000/security/resources/redirect-allow-star.py?url=http://127.0.0.1:8000/security/resources/xorigincss1-allow-star.py"
+ crossorigin=""
+ data-accessible="true">
+
+<!-- Same origin -> cross origin -> same origin -->
+<link class="test-stylesheet"
+ rel="stylesheet"
+ href="http://127.0.0.1:8000/resources/redirect.py?url=http://localhost:8000/resources/redirect.py?url=http://127.0.0.1:8000/security/resources/cssStyle.css"
+ data-accessible="false">
+
+</head>
+
+<body>
+
+<script>
+ description("Tests accessing CSSStyleSheet after removing from the document");
+
+ const link_elements = Array.from(document.getElementsByClassName("test-stylesheet"));
+ for (const element of link_elements) {
+ const stylesheet = element.sheet;
+
+ window.document.head.removeChild(element);
+
+ if (element.dataset.accessible === "true") {
+ shouldNotThrow(() => stylesheet.cssRules);
+ shouldNotThrow(() => stylesheet.insertRule('#element2 { margin: 10px; }'));
+ shouldNotThrow(() => stylesheet.deleteRule(0));
+ } else {
+ shouldThrowErrorName(() => stylesheet.cssRules, "SecurityError");
+ shouldThrowErrorName(() => stylesheet.insertRule('body { margin: 10px; } '), "SecurityError");
+ shouldThrowErrorName(() => stylesheet.deleteRule(0), "SecurityError");
+ }
+ }
+</script>
+
+<script src="/resources/js-test-post.js"></script>
+
+</body>
diff --git a/LayoutTests/http/tests/security/access-imported-cssstylesheet-after-removing-from-document-expected.txt b/LayoutTests/http/tests/security/access-imported-cssstylesheet-after-removing-from-document-expected.txt
new file mode 100644
index 000000000000..321f2c3eab2a
--- /dev/null
+++ b/LayoutTests/http/tests/security/access-imported-cssstylesheet-after-removing-from-document-expected.txt
@@ -0,0 +1,24 @@
+Tests accessing @import-ed CSSStyleSheet after
+
+PASS () => stylesheet.cssRules did not throw exception.
+PASS () => stylesheet.insertRule('#element2 { margin: 10px; }') did not throw exception.
+PASS () => stylesheet.deleteRule(0) did not throw exception.
+PASS () => stylesheet.cssRules threw exception SecurityError: Not allowed to access cross-origin stylesheet.
+PASS () => stylesheet.insertRule('body { margin: 10px; } ') threw exception SecurityError: Not allowed to insert rule into cross-origin stylesheet.
+PASS () => stylesheet.deleteRule(0) threw exception SecurityError: Not allowed to delete rule from cross-origin stylesheet.
+PASS () => stylesheet.cssRules did not throw exception.
+PASS () => stylesheet.insertRule('#element2 { margin: 10px; }') did not throw exception.
+PASS () => stylesheet.deleteRule(0) did not throw exception.
+PASS () => stylesheet.cssRules threw exception SecurityError: Not allowed to access cross-origin stylesheet.
+PASS () => stylesheet.insertRule('body { margin: 10px; } ') threw exception SecurityError: Not allowed to insert rule into cross-origin stylesheet.
+PASS () => stylesheet.deleteRule(0) threw exception SecurityError: Not allowed to delete rule from cross-origin stylesheet.
+PASS () => stylesheet.cssRules threw exception SecurityError: Not allowed to access cross-origin stylesheet.
+PASS () => stylesheet.insertRule('body { margin: 10px; } ') threw exception SecurityError: Not allowed to insert rule into cross-origin stylesheet.
+PASS () => stylesheet.deleteRule(0) threw exception SecurityError: Not allowed to delete rule from cross-origin stylesheet.
+PASS () => stylesheet.cssRules threw exception SecurityError: Not allowed to access cross-origin stylesheet.
+PASS () => stylesheet.insertRule('body { margin: 10px; } ') threw exception SecurityError: Not allowed to insert rule into cross-origin stylesheet.
+PASS () => stylesheet.deleteRule(0) threw exception SecurityError: Not allowed to delete rule from cross-origin stylesheet.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/security/access-imported-cssstylesheet-after-removing-from-document.html b/LayoutTests/http/tests/security/access-imported-cssstylesheet-after-removing-from-document.html
new file mode 100644
index 000000000000..d396755fe6b0
--- /dev/null
+++ b/LayoutTests/http/tests/security/access-imported-cssstylesheet-after-removing-from-document.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+
+<head>
+
+<title>Accessing @import-ed CSSStyleSheet after removing from document</title>
+<script src="/resources/js-test-pre.js"></script>
+
+<style id="local-style">
+ /* Rule 0: same origin */
+ @import url('/security/resources/cssStyle.css');
+
+ /* Rule 1: cross origin */
+ @import url('http://localhost:8000/security/resources/cssStyle.css');
+
+ /* Rule 3: same origin -> same origin */
+ @import url('/resources/redirect.py?url=http://127.0.0.1:8000/security/resources/cssStyle.css');
+
+ /* Rule 4: same origin -> cross origin */
+ @import url('/resources/redirect.py?url=http://localhost:8000/security/resources/cssStyle.css');
+
+ /* Rule 5: cross origin -> same origin */
+ @import url('http://localhost:8000/resources/redirect.py?url=http://127.0.0.1:8000/security/resources/cssStyle.css');
+
+ /* Rule 6: same origin -> cross origin -> same origin */
+ @import url('http://127.0.0.1:8000/resources/redirect.py?url=http://localhost:8000/resources/redirect.py?url=http://127.0.0.1:8000/security/resources/cssStyle.css');
+</style>
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2025-31205 |
| Bugzilla | 290992 |
| Component | WebCore |
| Bug Class | CrossOrigin |
| Severity | medium |
| Commit | 647e80ac22b36756… |
| Advisory | Apple Advisory |
Terminology
- Origin-clean / CORS-same-origin stylesheet — A stylesheet is origin-clean if it is same-origin or fetched with CORS and CORS-same-origin; only then may script read its rules.
- canAccessRules() — The CSSOM guard that decides whether script may read/modify a stylesheet’s rules based on origin.
- CSSOM wrapper — The CSSStyleSheet script object wrapping engine StyleSheetContents; created for link, @import and xml-stylesheet sheets.
- Same-origin policy (SOP) — The rule that script may not read cross-origin resource contents, including cross-origin CSS rule text.
Root Cause Analysis
CSSOM access to a stylesheet’s rules is gated by CSSStyleSheet::canAccessRules(), which must return false for a cross-origin (not origin-clean) sheet so script cannot read or mutate cross-origin CSS. Two defects made that gate leaky. First, canAccessRules() returned true when ownerDocument() was null: Document* document = ownerDocument(); if (!document) return true;. A CSSStyleSheet wrapper can outlive its owner node / be detached from the document (e.g. the owning element is removed), at which point ownerDocument() is null and the check fell open, granting access to a sheet whose origin can no longer be verified. Second, the origin-clean flag was not propagated correctly when constructing CSSStyleSheet CSSOM wrappers for imported/linked sheets: CSSImportRule::styleSheet(), ProcessingInstruction::setCSSStyleSheet() and HTMLLinkElement::initializeStyleSheet() created wrappers without threading the real CachedCSSStyleSheet::isCORSSameOrigin() value (HTMLLinkElement even had a FIXME and only set originClean when fetch mode was CORS), so a cross-origin sheet could be treated as origin-clean. The fix (a) makes canAccessRules() return false when there is no owner document, (b) adds explicit canAccessRules() checks to insertRule()/deleteRule() that throw SecurityError for cross-origin sheets, and (c) plumbs the true isCORSSameOrigin/origin-clean flag into CSSStyleSheet::create for import rules, processing instructions and link elements. The restored invariant is that rule read/write on a stylesheet is permitted only when the sheet is provably same-origin (or CORS-clean), including after it is detached from its document. The added layout test reads a cross-origin sheet after removing it from the document.
Attack Path
1. Load a cross-origin stylesheet
Reference a stylesheet from another origin without CORS (e.g. <link rel=stylesheet> or @import to a cross-origin URL).
2. Get its CSSOM wrapper
Obtain the CSSStyleSheet object (document.styleSheets / the link’s .sheet), which pre-patch may be wrapped without the correct origin-clean flag.
3. Detach it from the document
Remove the owning link/element so ownerDocument() becomes null, making canAccessRules() fall open (return true).
4. Read/modify cross-origin rules
Access cssRules/insertRule/deleteRule to read the cross-origin CSS text (which can encode sensitive, per-user data) or mutate it — a same-origin-policy bypass / information disclosure.
Impact Assessment
A same-origin-policy bypass allowing script to read (and modify) the rules of a cross-origin stylesheet, particularly after detaching it from the document. This is an information-disclosure primitive — cross-origin CSS can encode user-specific state — with no direct memory-safety component; it executes in the WebContent process. Rated medium.
Audit Directions
Other fall-open origin checks
grep WebCore for security predicates that return true on a null document/frame/origin (canAccessRules, canRequest wrappers), where the safe default is deny.
Origin-clean propagation
Audit every CSSStyleSheet::create call site (import, link, xml-stylesheet, constructed sheets) to confirm isOriginClean/isCORSSameOrigin is threaded correctly.
Detached-object access
Review CSSOM/DOM objects that can outlive their owner node for security checks that depend on the now-null owner (ownerDocument/ownerNode).
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
CSSStyleSheet::canAccessRules |
Source/WebCore/css/CSSStyleSheet.cpp |
modified | Returns false (was true) when ownerDocument() is null, so a detached sheet can no longer be read cross-origin. |
CSSStyleSheet::insertRule / deleteRule |
Source/WebCore/css/CSSStyleSheet.cpp |
modified | Now call canAccessRules() and throw SecurityError for cross-origin sheets before mutating rules. |
CSSStyleSheet::create / constructor (+ m_isOriginClean) |
Source/WebCore/css/CSSStyleSheet.cpp |
modified | Accept and store an isOriginClean flag for CSSImportRule-owned sheets so the origin-clean state is set correctly. |
CSSImportRule::styleSheet |
Source/WebCore/css/CSSImportRule.cpp |
modified | Derives isOriginClean from cachedCSSStyleSheet()->isCORSSameOrigin() and passes it into the wrapper. |
ProcessingInstruction::setCSSStyleSheet / HTMLLinkElement::initializeStyleSheet |
Source/WebCore/dom/ProcessingInstruction.cpp |
modified | Pass the CachedCSSStyleSheet::isCORSSameOrigin() value into CSSStyleSheet::create, removing the incomplete CORS-only origin-clean logic (FIXME) in HTMLLinkElement. |
Files Changed
Source Files
Source/WebCore/css/CSSImportRule.cppSource/WebCore/css/CSSStyleSheet.cppSource/WebCore/css/CSSStyleSheet.hSource/WebCore/dom/ProcessingInstruction.cppSource/WebCore/html/HTMLLinkElement.cpp
Test Files
LayoutTests/http/tests/security/access-cssstylesheet-after-removing-from-document-expected.txtLayoutTests/http/tests/security/access-cssstylesheet-after-removing-from-document.htmlLayoutTests/http/tests/security/access-imported-cssstylesheet-after-removing-from-document-expected.txtLayoutTests/http/tests/security/access-imported-cssstylesheet-after-removing-from-document.htmlLayoutTests/http/tests/security/cannot-read-cssrules-redirect-expected.txtLayoutTests/imported/w3c/web-platform-tests/service-workers/service-worker/fetch-request-css-cross-origin.https-expected.txt
Regression Test
The fix ships this proof-of-concept / regression test:
diff --git a/LayoutTests/http/tests/security/access-cssstylesheet-after-removing-from-document-expected.txt b/LayoutTests/http/tests/security/access-cssstylesheet-after-removing-from-document-expected.txt
new file mode 100644
index 000000000000..cc77e09161e7
--- /dev/null
+++ b/LayoutTests/http/tests/security/access-cssstylesheet-after-removing-from-document-expected.txt
@@ -0,0 +1,33 @@
+Tests accessing CSSStyleSheet after removing from the document
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS () => stylesheet.cssRules did not throw exception.
+PASS () => stylesheet.insertRule('#element2 { margin: 10px; }') did not throw exception.
+PASS () => stylesheet.deleteRule(0) did not throw exception.
+PASS () => stylesheet.cssRules threw exception SecurityError: Not allowed to access cross-origin stylesheet.
+PASS () => stylesheet.insertRule('body { margin: 10px; } ') threw exception SecurityError: Not allowed to insert rule into cross-origin stylesheet.
+PASS () => stylesheet.deleteRule(0) threw exception SecurityError: Not allowed to delete rule from cross-origin stylesheet.
+PASS () => stylesheet.cssRules did not throw exception.
+PASS () => stylesheet.insertRule('#element2 { margin: 10px; }') did not throw exception.
+PASS () => stylesheet.deleteRule(0) did not throw exception.
+PASS () => stylesheet.cssRules did not throw exception.
+PASS () => stylesheet.insertRule('#element2 { margin: 10px; }') did not throw exception.
+PASS () => stylesheet.deleteRule(0) did not throw exception.
+PASS () => stylesheet.cssRules threw exception SecurityError: Not allowed to access cross-origin stylesheet.
+PASS () => stylesheet.insertRule('body { margin: 10px; } ') threw exception SecurityError: Not allowed to insert rule into cross-origin stylesheet.
+PASS () => stylesheet.deleteRule(0) threw exception SecurityError: Not allowed to delete rule from cross-origin stylesheet.
+PASS () => stylesheet.cssRules threw exception SecurityError: Not allowed to access cross-origin stylesheet.
+PASS () => stylesheet.insertRule('body { margin: 10px; } ') threw exception SecurityError: Not allowed to insert rule into cross-origin stylesheet.
+PASS () => stylesheet.deleteRule(0) threw exception SecurityError: Not allowed to delete rule from cross-origin stylesheet.
+PASS () => stylesheet.cssRules did not throw exception.
+PASS () => stylesheet.insertRule('#element2 { margin: 10px; }') did not throw exception.
+PASS () => stylesheet.deleteRule(0) did not throw exception.
+PASS () => stylesheet.cssRules threw exception SecurityError: Not allowed to access cross-origin stylesheet.
+PASS () => stylesheet.insertRule('body { margin: 10px; } ') threw exception SecurityError: Not allowed to insert rule into cross-origin stylesheet.
+PASS () => stylesheet.deleteRule(0) threw exception SecurityError: Not allowed to delete rule from cross-origin stylesheet.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/security/access-cssstylesheet-after-removing-from-document.html b/LayoutTests/http/tests/security/access-cssstylesheet-after-removing-from-document.html
new file mode 100644
index 000000000000..4be151a8584c
--- /dev/null
+++ b/LayoutTests/http/tests/security/access-cssstylesheet-after-removing-from-document.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+
+<head>
+
+<title>Accessing CSSStyleSheet after removing from document</title>
+<script src="/resources/js-test-pre.js"></script>
+
+<!--
+ Explicitly use <link> here instead of dynamically creating <link> elements
+ in JavaScript, so they're loaded before the JavaScript runs.
+-->
+
+<!-- Inline style -->
+<style id="local-style">#element { margin: 10px; }</style>
+
+<!-- Same origin -->
+<link class="test-stylesheet"
+ rel="stylesheet"
+ href="/security/resources/cssStyle.css"
+ data-accessible="true">
+
+<!-- Cross origin, no CORS -->
+<link class="test-stylesheet"
+ rel="stylesheet"
+ href="http://localhost:8000/security/resources/cssStyle.css"
+ data-accessible="false">
+
+<!-- Cross origin, CORS succeeds -->
+<link class="test-stylesheet"
+ rel="stylesheet"
+ href="http://localhost:8000/security/resources/xorigincss1-allow-star.py"
+ crossorigin=""
+ data-accessible="true">
+
+<!-- Same origin -> same origin -->
+<link class="test-stylesheet"
+ rel="stylesheet"
+ href="/resources/redirect.py?url=http://127.0.0.1:8000/security/resources/cssStyle.css"
+ data-accessible="true">
+
+<!-- Same origin -> cross origin, no CORS -->
+<link class="test-stylesheet"
+ rel="stylesheet"
+ href="/resources/redirect.py?url=http://localhost:8000/security/resources/cssStyle.css"
+ data-accessible="false">
+
+<!-- Cross origin -> same origin, no CORS -->
+<link class="test-stylesheet"
+ rel="stylesheet"
+ href="http://localhost:8000/resources/redirect.py?url=http://127.0.0.1:8000/security/resources/cssStyle.css"
+ data-accessible="false">
+
+<!-- Cross origin -> same origin, CORS succeeds -->
+<link class="test-stylesheet"
+ rel="stylesheet"
+ href="http://localhost:8000/security/resources/redirect-allow-star.py?url=http://127.0.0.1:8000/security/resources/xorigincss1-allow-star.py"
+ crossorigin=""
+ data-accessible="true">
+
+<!-- Same origin -> cross origin -> same origin -->
+<link class="test-stylesheet"
+ rel="stylesheet"
+ href="http://127.0.0.1:8000/resources/redirect.py?url=http://localhost:8000/resources/redirect.py?url=http://127.0.0.1:8000/security/resources/cssStyle.css"
+ data-accessible="false">
+
+</head>
+
+<body>
+
+<script>
+ description("Tests accessing CSSStyleSheet after removing from the document");
+
+ const link_elements = Array.from(document.getElementsByClassName("test-stylesheet"));
+ for (const element of link_elements) {
+ const stylesheet = element.sheet;
+
+ window.document.head.removeChild(element);
+
+ if (element.dataset.accessible === "true") {
+ shouldNotThrow(() => stylesheet.cssRules);
+ shouldNotThrow(() => stylesheet.insertRule('#element2 { margin: 10px; }'));
+ shouldNotThrow(() => stylesheet.deleteRule(0));
+ } else {
+ shouldThrowErrorName(() => stylesheet.cssRules, "SecurityError");
+ shouldThrowErrorName(() => stylesheet.insertRule('body { margin: 10px; } '), "SecurityError");
+ shouldThrowErrorName(() => stylesheet.deleteRule(0), "SecurityError");
+ }
+ }
+</script>
+
+<script src="/resources/js-test-post.js"></script>
+
+</body>
diff --git a/LayoutTests/http/tests/security/access-imported-cssstylesheet-after-removing-from-document-expected.txt b/LayoutTests/http/tests/security/access-imported-cssstylesheet-after-removing-from-document-expected.txt
new file mode 100644
index 000000000000..321f2c3eab2a
--- /dev/null
+++ b/LayoutTests/http/tests/security/access-imported-cssstylesheet-after-removing-from-document-expected.txt
@@ -0,0 +1,24 @@
+Tests accessing @import-ed CSSStyleSheet after
+
+PASS () => stylesheet.cssRules did not throw exception.
+PASS () => stylesheet.insertRule('#element2 { margin: 10px; }') did not throw exception.
+PASS () => stylesheet.deleteRule(0) did not throw exception.
+PASS () => stylesheet.cssRules threw exception SecurityError: Not allowed to access cross-origin stylesheet.
+PASS () => stylesheet.insertRule('body { margin: 10px; } ') threw exception SecurityError: Not allowed to insert rule into cross-origin stylesheet.
+PASS () => stylesheet.deleteRule(0) threw exception SecurityError: Not allowed to delete rule from cross-origin stylesheet.
+PASS () => stylesheet.cssRules did not throw exception.
+PASS () => stylesheet.insertRule('#element2 { margin: 10px; }') did not throw exception.
+PASS () => stylesheet.deleteRule(0) did not throw exception.
+PASS () => stylesheet.cssRules threw exception SecurityError: Not allowed to access cross-origin stylesheet.
+PASS () => stylesheet.insertRule('body { margin: 10px; } ') threw exception SecurityError: Not allowed to insert rule into cross-origin stylesheet.
+PASS () => stylesheet.deleteRule(0) threw exception SecurityError: Not allowed to delete rule from cross-origin stylesheet.
+PASS () => stylesheet.cssRules threw exception SecurityError: Not allowed to access cross-origin stylesheet.
+PASS () => stylesheet.insertRule('body { margin: 10px; } ') threw exception SecurityError: Not allowed to insert rule into cross-origin stylesheet.
+PASS () => stylesheet.deleteRule(0) threw exception SecurityError: Not allowed to delete rule from cross-origin stylesheet.
+PASS () => stylesheet.cssRules threw exception SecurityError: Not allowed to access cross-origin stylesheet.
+PASS () => stylesheet.insertRule('body { margin: 10px; } ') threw exception SecurityError: Not allowed to insert rule into cross-origin stylesheet.
+PASS () => stylesheet.deleteRule(0) threw exception SecurityError: Not allowed to delete rule from cross-origin stylesheet.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/security/access-imported-cssstylesheet-after-removing-from-document.html b/LayoutTests/http/tests/security/access-imported-cssstylesheet-after-removing-from-document.html
new file mode 100644
index 000000000000..d396755fe6b0
--- /dev/null
+++ b/LayoutTests/http/tests/security/access-imported-cssstylesheet-after-removing-from-document.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+
+<head>
+
+<title>Accessing @import-ed CSSStyleSheet after removing from document</title>
+<script src="/resources/js-test-pre.js"></script>
+
+<style id="local-style">
+ /* Rule 0: same origin */
+ @import url('/security/resources/cssStyle.css');
+
+ /* Rule 1: cross origin */
+ @import url('http://localhost:8000/security/resources/cssStyle.css');
+
+ /* Rule 3: same origin -> same origin */
+ @import url('/resources/redirect.py?url=http://127.0.0.1:8000/security/resources/cssStyle.css');
+
+ /* Rule 4: same origin -> cross origin */
+ @import url('/resources/redirect.py?url=http://localhost:8000/security/resources/cssStyle.css');
+
+ /* Rule 5: cross origin -> same origin */
+ @import url('http://localhost:8000/resources/redirect.py?url=http://127.0.0.1:8000/security/resources/cssStyle.css');
+
+ /* Rule 6: same origin -> cross origin -> same origin */
+ @import url('http://127.0.0.1:8000/resources/redirect.py?url=http://localhost:8000/resources/redirect.py?url=http://127.0.0.1:8000/security/resources/cssStyle.css');
+</style>
+
+</head>
+
+<body>
+
+<script>
+ description("Tests accessing @import-ed CSSStyleSheet after <style> is removed from the document");
+
+ let local_style = document.getElementById("local-style");
+ let local_stylesheet = local_style.sheet;
+
+ // Remove the styles from the document
+ window.document.head.removeChild(local_style);
+
+ function check_accessible(stylesheet) {
+ shouldNotThrow(() => stylesheet.cssRules);
+ shouldNotThrow(() => stylesheet.insertRule('#element2 { margin: 10px; }'));
+ shouldNotThrow(() => stylesheet.deleteRule(0));
+ }
+
+ function check_inaccessible(stylesheet) {
+ shouldThrowErrorName(() => stylesheet.cssRules, "SecurityError");
+ shouldThrowErrorName(() => stylesheet.insertRule('body { margin: 10px; } '), "SecurityError");
+ shouldThrowErrorName(() => stylesheet.deleteRule(0), "SecurityError");
+ }
+
+ check_accessible(local_stylesheet.rules[0].styleSheet);
+ check_inaccessible(local_stylesheet.rules[1].styleSheet);
+ check_accessible(local_stylesheet.rules[2].styleSheet);
+ check_inaccessible(local_stylesheet.rules[3].styleSheet);
+ check_inaccessible(local_stylesheet.rules[4].styleSheet);
+ check_inaccessible(local_stylesheet.rules[5].styleSheet);
+</script>
+
+<script src="/resources/js-test-post.js"></script>
+
+</body>
+
diff --git a/LayoutTests/http/tests/security/cannot-read-cssrules-redirect-expected.txt b/LayoutTests/http/tests/security/cannot-read-cssrules-redirect-expected.txt
index 6cbdfa1e0175..4d77f44527cc 100644
--- a/LayoutTests/http/tests/security/cannot-read-cssrules-redirect-expected.txt
+++ b/LayoutTests/http/tests/security/cannot-read-cssrules-redirect-expected.txt
@@ -6,10 +6,8 @@ Test begins.
exception thrown for cssRules: SecurityError: Not allowed to access cross-origin stylesheet
exception thrown for rules: SecurityError: Not allowed to access cross-origin stylesheet
== Cross-Origin to Same-Origin, no-cors mode ==
-cssRules: [object CSSRuleList]
-cssRules length: 1
-rules: [object CSSRuleList]
-rules length: 1
+exception thrown for cssRules: SecurityError: Not allowed to access cross-origin stylesheet
+exception thrown for rules: SecurityError: Not allowed to access cross-origin stylesheet
== Same-Origin to Same-Origin, cors mode ==
cssRules: [object CSSRuleList]
cssRules length: 1
diff --git a/LayoutTests/imported/w3c/web-platform-tests/service-workers/service-worker/fetch-request-css-cross-origin.https-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/service-workers/service-worker/fetch-request-css-cross-origin.https-expected.txt
index da5202f054ab..f1cea4c665be 100644
--- a/LayoutTests/imported/w3c/web-platform-tests/service-workers/service-worker/fetch-request-css-cross-origin.https-expected.txt
+++ b/LayoutTests/imported/w3c/web-platform-tests/service-workers/service-worker/fetch-request-css-cross-origin.https-expected.txt
@@ -1,6 +1,6 @@
PASS setup global state
PASS MIME checking of CSS resources fetched via service worker when Content-Type is not set.
-FAIL Same-origin policy for access to CSS resources fetched via service worker promise_test: Unhandled rejection with value: object "SecurityError: Not allowed to access cross-origin stylesheet"
+PASS Same-origin policy for access to CSS resources fetched via service worker
PASS cleanup global state