Released May 12, 2025

CVE-2025-31205

CVSS 6.5 severity medium CWE-352 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N

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.cpp
  • Source/WebCore/css/CSSStyleSheet.cpp
  • Source/WebCore/css/CSSStyleSheet.h
  • Source/WebCore/dom/ProcessingInstruction.cpp
  • Source/WebCore/html/HTMLLinkElement.cpp

Test Files

  • LayoutTests/http/tests/security/access-cssstylesheet-after-removing-from-document-expected.txt
  • LayoutTests/http/tests/security/access-cssstylesheet-after-removing-from-document.html
  • LayoutTests/http/tests/security/access-imported-cssstylesheet-after-removing-from-document-expected.txt
  • LayoutTests/http/tests/security/access-imported-cssstylesheet-after-removing-from-document.html
  • LayoutTests/http/tests/security/cannot-read-cssrules-redirect-expected.txt
  • LayoutTests/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