CVE-2025-31205
Overview
Background
- `CSSStyleSheet`
- The CSSOM wrapper object exposing a stylesheet’s
cssRules,insertRule, anddeleteRuleto JavaScript. - `canAccessRules()`
- The origin gate on
CSSStyleSheetthat decides whether script may read or mutate a sheet’s rules based on same-origin/CORS checks. - Origin-clean flag
- The
m_isOriginClean(isOriginClean) tri-state (std::optional<bool>) that records whether a sheet’s bytes were fetched same-origin or via a successful CORS check. - `isCORSSameOrigin()`
- The
CachedCSSStyleSheetaccessor reporting whether the loaded resource satisfies CORS/same-origin, the authoritative source for the origin-clean flag.
Root Cause Analysis
When JavaScript reaches a CSSStyleSheet whose owning node or import rule has been detached from the document, canAccessRules() computed access from ownerDocument(), and when that document was nullptr it returned true, treating an orphaned sheet as freely accessible. Because cross-origin sheets loaded without CORS never had their origin-clean state persisted onto the wrapper (HTMLLinkElement::initializeStyleSheet only set originClean in FetchOptions::Mode::Cors, and CSSImportRule/ProcessingInstruction set nothing at all), a page could load a cross-site stylesheet, drop the <link>/<style> from the DOM, and then read cssRules on the now-parentless sheet, exfiltrating cross-origin CSS content.
The fix carries the true CORS-same-origin result from CachedCSSStyleSheet::isCORSSameOrigin() into the CSSStyleSheet at construction for every load path (link, @import, and processing instruction) and flips the detached-document case in canAccessRules() to return false, so an orphaned sheet fails closed rather than open. It additionally hardens the mutators insertRule and deleteRule with the same canAccessRules() gate that already protected reads, closing the write side of the cross-origin surface. The invariant restored is that a sheet’s origin-clean status is fixed by its fetch and must survive detachment, never inferred from a mutable document link.
ownerDocument() at call time while failing to persist the fetch-time origin-clean bit onto the wrapper, so detaching the sheet from the DOM erased the security boundary. The fix records isCORSSameOrigin() into m_isOriginClean at construction and makes the missing-document branch of canAccessRules() deny access.Attack Path
- Embed a cross-site stylesheet
The attacker page loads a victim origin’s CSS via
<link rel="stylesheet">(nocrossorigin),@import, or an XML processing instruction, whose response bytes are not origin-clean. - Detach the owner from the DOM
Script removes the
<link>/<style>/PI node so the resultingCSSStyleSheetwrapper has noownerDocument(). - Read the orphaned sheet
Script calls
stylesheet.cssRules(or previously-ungatedinsertRule/deleteRule), and the oldcanAccessRules()returnstruefor the null-document case. - Exfiltrate cross-origin CSS The attacker serializes the victim’s rule text and ships it off-origin, leaking data the CSS content encodes.
Impact Assessment
<link>, @import, or PI) and then detach its owner node before reading it.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifSource/WebCore/html/HTMLLinkElement.cpp |
modified | |
forLayoutTests/http/tests/security/access-cssstylesheet-after-removing-from-document.html |
modified | |
ifLayoutTests/http/tests/security/access-cssstylesheet-after-removing-from-document.html |
modified |
Files Changed
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.txtSource/WebCore/css/CSSImportRule.cppSource/WebCore/css/CSSStyleSheet.cppSource/WebCore/css/CSSStyleSheet.hSource/WebCore/dom/ProcessingInstruction.cppSource/WebCore/html/HTMLLinkElement.cpp
Audit Directions
- Fetch-time security state persisted on wrappersVerify that any origin/CORS bit determining later access (like
m_isOriginClean) is captured at load time and stored on the object, never recomputed from mutable DOM links that can be severed. - Fail-closed on missing contextAudit access-control predicates for branches that
return truewhen a document, frame, or origin isnullptr; detachment-based null contexts should deny, not grant. - Symmetric gating of readers and mutatorsEnsure every read gate (
canAccessRules()oncssRules) has a matching gate on the mutation APIs (insertRule,deleteRule) and similar setters, since attackers exploit whichever entry point is left ungated.
Patch
diff --git a/Source/WebCore/css/CSSImportRule.cpp b/Source/WebCore/css/CSSImportRule.cpp
index 9356bbc7e758..aae10a569d7f 100644
--- a/Source/WebCore/css/CSSImportRule.cpp
+++ b/Source/WebCore/css/CSSImportRule.cpp
@@ -26,6 +26,7 @@
#include "CSSMarkup.h"
#include "CSSSerializationContext.h"
#include "CSSStyleSheet.h"
+#include "CachedCSSStyleSheet.h"
#include "MediaList.h"
#include "MediaQueryParser.h"
#include "StyleRuleImport.h"
@@ -121,8 +122,13 @@ CSSStyleSheet* CSSImportRule::styleSheet() const
{
if (!m_importRule.get().styleSheet())
return nullptr;
+
+ std::optional<bool> isOriginClean;
+ if (const auto* cachedSheet = m_importRule->cachedCSSStyleSheet())
+ isOriginClean = cachedSheet->isCORSSameOrigin();
+
if (!m_styleSheetCSSOMWrapper)
- m_styleSheetCSSOMWrapper = CSSStyleSheet::create(*m_importRule.get().styleSheet(), const_cast<CSSImportRule*>(this));
+ m_styleSheetCSSOMWrapper = CSSStyleSheet::create(*m_importRule.get().styleSheet(), const_cast<CSSImportRule*>(this), isOriginClean);
return m_styleSheetCSSOMWrapper.get();
}
diff --git a/Source/WebCore/css/CSSStyleSheet.cpp b/Source/WebCore/css/CSSStyleSheet.cpp
index d2df9aa422ff..8dbb03d13225 100644
--- a/Source/WebCore/css/CSSStyleSheet.cpp
+++ b/Source/WebCore/css/CSSStyleSheet.cpp
@@ -90,9 +90,9 @@ static bool isAcceptableCSSStyleSheetParent(Node* parentNode)
}
#endif // ASSERT_ENABLED
-Ref<CSSStyleSheet> CSSStyleSheet::create(Ref<StyleSheetContents>&& sheet, CSSImportRule* ownerRule)
+Ref<CSSStyleSheet> CSSStyleSheet::create(Ref<StyleSheetContents>&& sheet, CSSImportRule* ownerRule, std::optional<bool> isOriginClean)
{
- return adoptRef(*new CSSStyleSheet(WTFMove(sheet), ownerRule));
+ return adoptRef(*new CSSStyleSheet(WTFMove(sheet), ownerRule, isOriginClean));
}
Ref<CSSStyleSheet> CSSStyleSheet::create(Ref<StyleSheetContents>&& sheet, Node& ownerNode, const std::optional<bool>& isCleanOrigin)
@@ -121,8 +121,9 @@ ExceptionOr<Ref<CSSStyleSheet>> CSSStyleSheet::create(Document& document, Init&&
return adoptRef(*new CSSStyleSheet(StyleSheetContents::create(parserContext), document, WTFMove(init)));
}
-CSSStyleSheet::CSSStyleSheet(Ref<StyleSheetContents>&& contents, CSSImportRule* ownerRule)
+CSSStyleSheet::CSSStyleSheet(Ref<StyleSheetContents>&& contents, CSSImportRule* ownerRule, std::optional<bool> isOriginClean)
: m_contents(WTFMove(contents))
+ , m_isOriginClean(isOriginClean)
, m_ownerRule(ownerRule)
{
if (auto* parent = parentStyleSheet())
@@ -337,9 +338,11 @@ bool CSSStyleSheet::canAccessRules() const
URL baseURL = m_contents->baseURL();
if (baseURL.isEmpty())
return true;
+
Document* document = ownerDocument();
if (!document)
- return true;
+ return false;
+
return document->protectedSecurityOrigin()->canRequest(baseURL, OriginAccessPatternsForWebProcess::singleton());
}
@@ -349,6 +352,9 @@ ExceptionOr<unsigned> CSSStyleSheet::insertRule(const String& ruleString, unsign
ASSERT(m_childRuleCSSOMWrappers.isEmpty() || m_childRuleCSSOMWrappers.size() == m_contents->ruleCount());
+ if (!canAccessRules())
+ return Exception { ExceptionCode::SecurityError, "Not allowed to insert rule into cross-origin stylesheet"_s };
+
if (index > length())
return Exception { ExceptionCode::IndexSizeError };
RefPtr rule = CSSParser::parseRule(ruleString, m_contents.get().parserContext(), m_contents.ptr(), CSSParser::AllowedRules::ImportRules);
@@ -376,6 +382,9 @@ ExceptionOr<void> CSSStyleSheet::deleteRule(unsigned index)
ASSERT(m_childRuleCSSOMWrappers.isEmpty() || m_childRuleCSSOMWrappers.size() == m_contents->ruleCount());
+ if (!canAccessRules())
+ return Exception { ExceptionCode::SecurityError, "Not allowed to delete rule from cross-origin stylesheet"_s };
+
if (index >= length())
return Exception { ExceptionCode::IndexSizeError };
RuleMutationScope mutationScope(this);
diff --git a/Source/WebCore/css/CSSStyleSheet.h b/Source/WebCore/css/CSSStyleSheet.h
index 5a3438c05f57..4db0ac776ff9 100644
--- a/Source/WebCore/css/CSSStyleSheet.h
+++ b/Source/WebCore/css/CSSStyleSheet.h
@@ -64,7 +64,7 @@ public:
Variant<RefPtr<MediaList>, String> media { emptyString() };
bool disabled { false };
};
- static Ref<CSSStyleSheet> create(Ref<StyleSheetContents>&&, CSSImportRule* ownerRule = 0);
+ static Ref<CSSStyleSheet> create(Ref<StyleSheetContents>&&, CSSImportRule* ownerRule = nullptr, std::optional<bool>isOriginClean = std::nullopt);
static Ref<CSSStyleSheet> create(Ref<StyleSheetContents>&&, Node& ownerNode, const std::optional<bool>& isOriginClean = std::nullopt);
static Ref<CSSStyleSheet> createInline(Ref<StyleSheetContents>&&, Element& owner, const TextPosition& startPosition);
static ExceptionOr<Ref<CSSStyleSheet>> create(Document&, Init&&);
@@ -164,7 +164,7 @@ public:
bool isDetached() const;
private:
- CSSStyleSheet(Ref<StyleSheetContents>&&, CSSImportRule* ownerRule);
+ CSSStyleSheet(Ref<StyleSheetContents>&&, CSSImportRule* ownerRule, std::optional<bool> isOriginClean);
CSSStyleSheet(Ref<StyleSheetContents>&&, Node* ownerNode, const TextPosition& startPosition, bool isInlineStylesheet);
CSSStyleSheet(Ref<StyleSheetContents>&&, Node& ownerNode, const TextPosition& startPosition, bool isInlineStylesheet, const std::optional<bool>&);
CSSStyleSheet(Ref<StyleSheetContents>&&, Document&, Init&&);
diff --git a/Source/WebCore/dom/ProcessingInstruction.cpp b/Source/WebCore/dom/ProcessingInstruction.cpp
index 8c479fa86bef..14ba0c0596f2 100644
--- a/Source/WebCore/dom/ProcessingInstruction.cpp
+++ b/Source/WebCore/dom/ProcessingInstruction.cpp
@@ -190,11 +190,13 @@ void ProcessingInstruction::setCSSStyleSheet(const String& href, const URL& base
return;
}
+ ASSERT(sheet);
+
Ref document = this->document();
ASSERT(m_isCSS);
CSSParserContext parserContext(document, baseURL, charset);
- Ref cssSheet = CSSStyleSheet::create(StyleSheetContents::create(href, parserContext), *this);
+ Ref cssSheet = CSSStyleSheet::create(StyleSheetContents::create(href, parserContext), *this, sheet->isCORSSameOrigin());
cssSheet->setDisabled(m_alternate);
cssSheet->setTitle(m_title);
cssSheet->setMediaQueries(MQ::MediaQueryParser::parse(m_media, MediaQueryParserContext(document)));
diff --git a/Source/WebCore/html/HTMLLinkElement.cpp b/Source/WebCore/html/HTMLLinkElement.cpp
index 652f1f7cfb34..d05470b9be45 100644
--- a/Source/WebCore/html/HTMLLinkElement.cpp
+++ b/Source/WebCore/html/HTMLLinkElement.cpp
@@ -579,17 +579,12 @@ void HTMLLinkElement::finishParsingChildren()
void HTMLLinkElement::initializeStyleSheet(Ref<StyleSheetContents>&& styleSheet, const CachedCSSStyleSheet& cachedStyleSheet, MediaQueryParserContext context)
{
- // FIXME: originClean should be turned to false except if fetch mode is CORS.
- std::optional<bool> originClean;
- if (cachedStyleSheet.options().mode == FetchOptions::Mode::Cors)
- originClean = cachedStyleSheet.isCORSSameOrigin();
-
if (m_sheet) {
ASSERT(m_sheet->ownerNode() == this);
m_sheet->clearOwnerNode();
}
- m_sheet = CSSStyleSheet::create(WTFMove(styleSheet), *this, originClean);
+ m_sheet = CSSStyleSheet::create(WTFMove(styleSheet), *this, cachedStyleSheet.isCORSSameOrigin());
m_sheet->setMediaQueries(MQ::MediaQueryParser::parse(m_media, context));
if (!isInShadowTree())
m_sheet->setTitle(title());
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]
... (file diff truncated)
Regression Test / PoC
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