CVE-2026-113051
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ImageResourceObserverthird_party/blink/renderer/core/css/css_crossfade_value.h |
modified | |
ResourceFetcherthird_party/blink/renderer/core/css/css_crossfade_value.h |
modified | |
StyleResolverStatethird_party/blink/renderer/core/css/css_crossfade_value.h |
modified | |
ifthird_party/blink/renderer/core/css/css_font_face_src_value.cc |
modified | |
ExecutionContextthird_party/blink/renderer/core/css/css_font_face_src_value.h |
modified | |
ResourceFetcherthird_party/blink/renderer/core/css/css_font_face_src_value.h |
modified | |
CORE_EXPORTthird_party/blink/renderer/core/css/css_font_face_src_value.h |
modified | |
ifthird_party/blink/renderer/core/css/css_image_set_value.cc |
modified | |
CSSImageSetOptionValuethird_party/blink/renderer/core/css/css_image_set_value.h |
modified | |
CSSLengthResolverthird_party/blink/renderer/core/css/css_image_set_value.h |
modified | |
ResourceFetcherthird_party/blink/renderer/core/css/css_image_set_value.h |
modified | |
StyleImagethird_party/blink/renderer/core/css/css_image_set_value.h |
modified | |
StyleResolverStatethird_party/blink/renderer/core/css/css_image_set_value.h |
modified |
Files Changed
third_party/blink/renderer/core/css/style_rule_counter_style.hthird_party/blink/renderer/core/css/css_image_set_value.hthird_party/blink/renderer/core/css/css_font_face_src_value.ccthird_party/blink/web_tests/external/wpt/content-security-policy/font-src/support/shared-stylesheet-font-cache-bypass-permissive.htmlthird_party/blink/renderer/core/css/css_value.ccthird_party/blink/web_tests/external/wpt/content-security-policy/img-src/support/shared-stylesheet-image-set-cache-bypass.css.headersthird_party/blink/renderer/core/css/cssom/style_property_map_read_only_main_thread.ccthird_party/blink/web_tests/external/wpt/content-security-policy/img-src/support/shared-stylesheet-mask-cache-bypass.svgthird_party/blink/renderer/core/css/build.gnithird_party/blink/web_tests/external/wpt/content-security-policy/img-src/shared-stylesheet-image-cache-bypass-strict-first.htmlthird_party/blink/web_tests/external/wpt/content-security-policy/img-src/support/shared-stylesheet-mask-cache-bypass.svg.headersthird_party/blink/renderer/core/css/cssom/css_url_image_value.ccthird_party/blink/renderer/core/css/resolver/style_resolver_test.ccthird_party/blink/web_tests/external/wpt/content-security-policy/font-src/shared-stylesheet-font-cache-bypass.htmlthird_party/blink/renderer/core/css/style_sheet_contents.hthird_party/blink/web_tests/external/wpt/content-security-policy/img-src/support/shared-stylesheet-image-set-cache-bypass.cssthird_party/blink/web_tests/external/wpt/content-security-policy/img-src/support/shared-stylesheet-image-cache-bypass.cssthird_party/blink/renderer/core/css/resolver/element_style_resources.ccthird_party/blink/renderer/core/frame/frame_serializer.ccthird_party/blink/web_tests/external/wpt/content-security-policy/img-src/support/shared-stylesheet-image-cache-bypass-blank.htmlthird_party/blink/web_tests/external/wpt/content-security-policy/img-src/shared-stylesheet-mask-cache-bypass-ref.htmlthird_party/blink/web_tests/external/wpt/content-security-policy/img-src/support/shared-stylesheet-image-set-cache-bypass-strict.htmlthird_party/blink/web_tests/external/wpt/content-security-policy/font-src/support/shared-stylesheet-font-cache-bypass.css.headersthird_party/blink/web_tests/external/wpt/content-security-policy/font-src/support/shared-stylesheet-font-cache-bypass-strict.htmlthird_party/blink/renderer/core/css/style_rule.ccthird_party/blink/web_tests/external/wpt/content-security-policy/img-src/shared-stylesheet-image-set-cache-bypass.htmlthird_party/blink/web_tests/external/wpt/content-security-policy/img-src/support/shared-stylesheet-mask-cache-bypass.cssthird_party/blink/renderer/core/css/cssom/style_value_factory.ccthird_party/blink/web_tests/external/wpt/content-security-policy/font-src/support/shared-stylesheet-font-cache-bypass.cssthird_party/blink/renderer/core/css/css_value.hthird_party/blink/renderer/core/css/css_image_set_value.ccthird_party/blink/web_tests/external/wpt/content-security-policy/img-src/shared-stylesheet-image-cache-bypass-loaded-ref.htmlthird_party/blink/renderer/core/css/css_value_list.ccthird_party/blink/renderer/core/css/css_property_value_set.hthird_party/blink/renderer/core/css/style_sheet_contents.ccthird_party/blink/web_tests/external/wpt/content-security-policy/img-src/support/shared-stylesheet-image-cache-bypass-permissive.htmlthird_party/blink/web_tests/external/wpt/content-security-policy/img-src/support/shared-stylesheet-image-cache-bypass.css.headersthird_party/blink/web_tests/external/wpt/content-security-policy/img-src/support/shared-stylesheet-mask-cache-bypass-permissive.htmlthird_party/blink/renderer/core/css/css_crossfade_value.hthird_party/blink/renderer/core/css/css_property_value_set.ccthird_party/blink/renderer/core/css/css_font_face_src_value.hthird_party/blink/web_tests/external/wpt/content-security-policy/img-src/support/shared-stylesheet-image-cache-bypass-strict.htmlthird_party/blink/renderer/core/css/cssom/css_url_image_value.hthird_party/blink/web_tests/external/wpt/content-security-policy/img-src/shared-stylesheet-mask-cache-bypass-loaded-ref.htmlthird_party/blink/web_tests/external/wpt/content-security-policy/img-src/shared-stylesheet-image-cache-bypass-ref.htmlthird_party/blink/renderer/core/css/css_style_resource_fetcher_identity_test.ccthird_party/blink/renderer/core/css/css_value_list.hthird_party/blink/web_tests/external/wpt/content-security-policy/img-src/shared-stylesheet-image-cache-bypass.htmlthird_party/blink/web_tests/external/wpt/content-security-policy/img-src/shared-stylesheet-mask-cache-bypass-strict-first.htmlthird_party/blink/web_tests/external/wpt/content-security-policy/img-src/shared-stylesheet-mask-cache-bypass.htmlthird_party/blink/renderer/core/css/cssom/style_value_factory.hthird_party/blink/web_tests/external/wpt/content-security-policy/font-src/shared-stylesheet-font-cache-bypass-strict-first.htmlthird_party/blink/web_tests/external/wpt/content-security-policy/img-src/support/shared-stylesheet-mask-cache-bypass-blank.htmlthird_party/blink/renderer/core/css/cssom/css_style_image_value_test.ccthird_party/blink/renderer/core/loader/resource/css_style_sheet_resource.ccthird_party/blink/renderer/platform/runtime_enabled_features.json5third_party/blink/renderer/core/css/properties/css_property.ccthird_party/blink/renderer/core/css/css_image_value.hthird_party/blink/renderer/core/css/style_rule.hthird_party/blink/renderer/core/css/css_image_value.ccthird_party/blink/web_tests/external/wpt/content-security-policy/img-src/support/shared-stylesheet-mask-cache-bypass-strict.htmlthird_party/blink/renderer/core/css/css_crossfade_value.ccthird_party/blink/web_tests/external/wpt/content-security-policy/img-src/support/shared-stylesheet-mask-cache-bypass.css.headersthird_party/blink/renderer/core/css/style_sheet_contents_test.ccthird_party/blink/web_tests/external/wpt/content-security-policy/img-src/shared-stylesheet-image-set-cache-bypass-strict-first.htmlthird_party/blink/web_tests/external/wpt/content-security-policy/img-src/support/shared-stylesheet-image-set-cache-bypass-permissive.html
Patch
From 52abc6b169e35906b68ce59df4c4c306c9fac61a Mon Sep 17 00:00:00 2001 From: Hoch Hochkeppel <[email protected]> Date: Wed, 23 Sep 2026 09:27:36 -0700 Subject: [PATCH] [Blink] Resolve CSS Image and Font values per-document CSS style resources referenced from a stylesheet were cached directly on the `CSSValue` objects owned by `StyleSheetContents`. Because two same-origin documents that link the same stylesheet share a single `StyleSheetContents` instance, a resource already fetched by the first document was reused by the second document without any fetch of its own. This let the second document display a resource that its own Content Security Policy would have blocked, defeating per-document CSP enforcement. This change caches each fetched style resource in a per-ResourceFetcher map keyed on the requesting document's fetcher, rather than in a single shared slot. A document whose fetcher has not fetched the resource now re-fetches it under its own CSP. The performance and memory impact should usually be minimal due to the re-fetch resolving in the `MemoryCache` instead of the network, but that is not guaranteed (e.g. if the resource was marked no-store). Same-fetcher lookups still hit the cache, so there is no additional fetching within a single document. A null `ResourceFetcher` route is still supported for contexts that have no fetcher (e.g. cross-thread style values). A route for taking a snapshot (used by `StyleFetchedImage`) is also introduced, with the snapshot being available in all contexts regardless of `ResourceFetcher`. The new behavior is on-by-default, but gated behind the `StyleResourceFetcherIdentityCheck` runtime-enabled feature in case it needs to be stopped. This change does not attempt to key based on the CSP or an initial `CanRequest` result. That would validate only the starting CSP decision, not the full set of per-document request checks an actual fetch performs (e.g. each stage of redirects, or resulting URL differing from requested URL). Authorship of these WPTs is attributed to Dileep, based on crrev.com/c/7867457 Fixed: 504107363 Change-Id: Icd98ac13320cfc7238a7236c23e83143b7359144 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8410893 Reviewed-by: Yoav Weiss (@Shopify) <[email protected]> Reviewed-by: Rune Lillesveen <[email protected]> Commit-Queue: Hoch Hochkeppel <[email protected]> Cr-Commit-Position: refs/heads/main@{#1703519} --- diff --git a/third_party/blink/renderer/core/css/css_crossfade_value.cc b/third_party/blink/renderer/core/css/css_crossfade_value.cc index 6e441572..bc8cb48b 100644 --- a/third_party/blink/renderer/core/css/css_crossfade_value.cc +++ b/third_party/blink/renderer/core/css/css_crossfade_value.cc @@ -73,11 +73,13 @@ return result.ReleaseString(); } -bool CSSCrossfadeValue::HasFailedOrCanceledSubresources() const { +bool CSSCrossfadeValue::HasFailedOrCanceledSubresources( + ResourceFetcher* fetcher) const { return std::any_of( image_and_percentages_.begin(), image_and_percentages_.end(), - [](const auto& image_and_percent) { - return image_and_percent.first->HasFailedOrCanceledSubresources(); + [fetcher](const auto& image_and_percent) { + return image_and_percent.first->HasFailedOrCanceledSubresources( + fetcher); }); } diff --git a/third_party/blink/renderer/core/css/css_crossfade_value.h b/third_party/blink/renderer/core/css/css_crossfade_value.h index 8ccf4d04..0e4ba35 100644 --- a/third_party/blink/renderer/core/css/css_crossfade_value.h +++ b/third_party/blink/renderer/core/css/css_crossfade_value.h @@ -34,6 +34,7 @@ namespace blink { class ImageResourceObserver; +class ResourceFetcher; class StyleResolverState; namespace cssvalue { @@ -56,7 +57,7 @@ ImageResourceObserver* GetObserverProxy(); String CustomCSSText() const; - bool HasFailedOrCanceledSubresources() const; + bool HasFailedOrCanceledSubresources(ResourceFetcher*) const; bool Equals(const CSSCrossfadeValue&) const; const CSSCrossfadeValue& ResolveValuesIfNeeded( diff --git a/third_party/blink/renderer/core/css/css_font_face_src_value.cc b/third_party/blink/renderer/core/css/css_font_face_src_value.cc index bd8f865..844a673 100644 --- a/third_party/blink/renderer/core/css/css_font_face_src_value.cc +++ b/third_party/blink/renderer/core/css/css_font_face_src_value.cc @@ -46,6 +46,7 @@ #include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h" #include "third_party/blink/renderer/platform/loader/fetch/resource_loader_options.h" #include "third_party/blink/renderer/platform/loader/subresource_integrity.h" +#include "third_party/blink/renderer/platform/runtime_enabled_features.h" #include "third_party/blink/renderer/platform/wtf/text/string_builder.h" namespace blink { @@ -139,13 +140,36 @@ return result.ReleaseString(); } -bool CSSFontFaceSrcValue::HasFailedOrCanceledSubresources() const { - return fetched_ && fetched_->LoadFailedOrCanceled(); +bool CSSFontFaceSrcValue::HasFailedOrCanceledSubresources( + ResourceFetcher* fetcher) const { + FontResource* resource = nullptr; + if (fetcher && + RuntimeEnabledFeatures::StyleResourceFetcherIdentityCheckEnabled()) { + auto it = fetched_resources_.find(fetcher); + if (it != fetched_resources_.end()) { + resource = it->value.Get(); + } + } else if (fetched_) { + resource = fetched_.Get(); + } + return resource && resource->LoadFailedOrCanceled(); } FontResource& CSSFontFaceSrcValue::Fetch(ExecutionContext* context, FontResourceClient* client) const { - if (!fetched_ || fetched_->Options().world_for_csp != world_) { + FontResource* resource = nullptr; + ResourceFetcher* fetcher = context->Fetcher(); + if (fetcher && + RuntimeEnabledFeatures::StyleResourceFetcherIdentityCheckEnabled()) { + auto it = fetched_resources_.find(fetcher); + if (it != fetched_resources_.end()) { + resource = it->value.Get(); + } + } else if (fetched_) { + resource = fetched_.Get(); + } + + if (!resource || resource->Options().world_for_csp != world_) { const CSSUrlData& url_data = src_value_->UrlData(); const CSSUrlRequestModifiers& modifiers = url_data.GetModifiers(); const Referrer& referrer = url_data.GetReferrer(); @@ -196,27 +220,33 @@ context); } - fetched_ = FontResource::Fetch(params, context->Fetcher(), client); + resource = FontResource::Fetch(params, fetcher, client); + if (fetcher && + RuntimeEnabledFeatures::StyleResourceFetcherIdentityCheckEnabled()) { + fetched_resources_.Set(fetcher, resource); + } else { + fetched_ = resource; + } } else { // FIXME: CSSFontFaceSrcValue::Fetch is invoked when @font-face rule // is processed by StyleResolver / StyleEngine. - RestoreCachedResourceIfNeeded(context); + RestoreCachedResourceIfNeeded(context, resource); if (client) { client->SetResource( - fetched_.Get(), - context->GetTaskRunner(TaskType::kInternalLoading).get()); + resource, context->GetTaskRunner(TaskType::kInternalLoading).get()); } } - return *fetched_; + return *resource; } void CSSFontFaceSrcValue::RestoreCachedResourceIfNeeded( - ExecutionContext* context) const { - DCHECK(fetched_); + ExecutionContext* context, + FontResource* resource) const { + DCHECK(resource); DCHECK(context); DCHECK(context->Fetcher()); context->Fetcher()->EmulateLoadStartedForInspector( - fetched_, mojom::blink::RequestContextType::FONT, + resource, mojom::blink::RequestContextType::FONT, network::mojom::RequestDestination::kFont, fetch_initiator_type_names::kCSS); } @@ -230,6 +260,7 @@ void CSSFontFaceSrcValue::TraceAfterDispatch(Visitor* visitor) const { visitor->Trace(src_value_); visitor->Trace(fetched_); + visitor->Trace(fetched_resources_); visitor->Trace(world_); CSSValue::TraceAfterDispatch(visitor); } diff --git a/third_party/blink/renderer/core/css/css_font_face_src_value.h b/third_party/blink/renderer/core/css/css_font_face_src_value.h index 4ff9388b..73ebb02 100644 --- a/third_party/blink/renderer/core/css/css_font_face_src_value.h +++ b/third_party/blink/renderer/core/css/css_font_face_src_value.h @@ -31,6 +31,7 @@ #include "third_party/blink/renderer/core/css/css_value.h" #include "third_party/blink/renderer/core/loader/resource/font_resource.h" #include "third_party/blink/renderer/platform/bindings/dom_wrapper_world.h" +#include "third_party/blink/renderer/platform/heap/collection_support/heap_hash_map.h" #include "third_party/blink/renderer/platform/wtf/casting.h" #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" @@ -41,6 +42,7 @@ } // namespace cssvalue class ExecutionContext; +class ResourceFetcher; class CORE_EXPORT CSSFontFaceSrcValue : public CSSValue { public: @@ -89,7 +91,7 @@ String CustomCSSText() const; - bool HasFailedOrCanceledSubresources() const; + bool HasFailedOrCanceledSubresources(ResourceFetcher*) const; FontResource& Fetch(ExecutionContext*, FontResourceClient*) const; @@ -98,7 +100,7 @@ void TraceAfterDispatch(Visitor* visitor) const; private: - void RestoreCachedResourceIfNeeded(ExecutionContext*) const; + void RestoreCachedResourceIfNeeded(ExecutionContext*, FontResource*) const; Vector<FontTechnology> technologies_; Member<cssvalue::CSSURIValue> src_value_; // Non-null if remote (src()). @@ -106,6 +108,8 @@ String format_; const Member<const DOMWrapperWorld> world_; mutable Member<FontResource> fetched_; + mutable HeapHashMap<WeakMember<ResourceFetcher>, Member<FontResource>> + fetched_resources_; }; template <> diff --git a/third_party/blink/renderer/core/css/css_image_set_value.cc b/third_party/blink/renderer/core/css/css_image_set_value.cc index 6e560062..9c2f556 100644 --- a/third_party/blink/renderer/core/css/css_image_set_value.cc +++ b/third_party/blink/renderer/core/css/css_image_set_value.cc @@ -26,6 +26,7 @@ #include "third_party/blink/renderer/core/css/css_image_set_value.h" #include <algorithm> +#include <utility> #include "third_party/blink/renderer/core/css/css_image_set_option_value.h" #include "third_party/blink/renderer/core/css/css_length_resolver.h" @@ -34,6 +35,8 @@ #include "third_party/blink/renderer/core/loader/resource/image_resource_content.h" #include "third_party/blink/renderer/core/paint/timing/paint_timing.h" #include "third_party/blink/renderer/core/style/style_image_set.h" +#include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h" +#include "third_party/blink/renderer/platform/runtime_enabled_features.h" #include "third_party/blink/renderer/platform/wtf/text/string_builder.h" namespace blink { @@ -104,22 +107,48 @@ return options_.back().Get(); } -bool CSSImageSetValue::IsCachePending(const float device_scale_factor) const { +bool CSSImageSetValue::IsCachePending(ResourceFetcher* fetcher, + const float device_scale_factor) const { + if (fetcher && + RuntimeEnabledFeatures::StyleResourceFetcherIdentityCheckEnabled()) { + auto it = cached_images_.find(fetcher); + return it == cached_images_.end() || + !EqualResolutions(device_scale_factor, + it->value->device_scale_factor); + } return !cached_image_ || !EqualResolutions(device_scale_factor, cached_device_scale_factor_); } StyleImage* CSSImageSetValue::CachedImage( + ResourceFetcher* fetcher, const float device_scale_factor) const { - DCHECK(!IsCachePending(device_scale_factor)); - return cached_image_.Get(); + DCHECK(!IsCachePending(fetcher, device_scale_factor)); + if (fetcher && + RuntimeEnabledFeatures::StyleResourceFetcherIdentityCheckEnabled()) { + auto it = cached_images_.find(fetcher); + if (it != cached_images_.end()) { + return it->value->image.Get(); + } + return nullptr; + } else { + return cached_image_.Get(); + } } -StyleImage* CSSImageSetValue::CacheImage(StyleImage* style_image, +StyleImage* CSSImageSetValue::CacheImage(ResourceFetcher* fetcher, + StyleImage* style_image, const float device_scale_factor) { - cached_image_ = MakeGarbageCollected<StyleImageSet>(style_image, this); - cached_device_scale_factor_ = device_scale_factor; - return cached_image_.Get(); + StyleImage* image = MakeGarbageCollected<StyleImageSet>(style_image, this); + if (fetcher && + RuntimeEnabledFeatures::StyleResourceFetcherIdentityCheckEnabled()) { + cached_images_.Set(fetcher, MakeGarbageCollected<CachedImageAndScale>( + image, device_scale_factor)); + } else { + cached_image_ = image; + cached_device_scale_factor_ = device_scale_factor; + } + return image; } String CSSImageSetValue::CustomCSSText() const { @@ -139,16 +168,36 @@ return result.ReleaseString(); } -bool CSSImageSetValue::HasFailedOrCanceledSubresources() const { - if (!cached_image_) { +bool CSSImageSetValue::HasFailedOrCanceledSubresources( + ResourceFetcher* fetcher) const { + // This lookup is intentionally not gated on the device scale factor. The + // per-fetcher map holds a single entry (the most recently cached image and + // its device scale factor), so we simply report the status of whatever is + // currently cached for this fetcher. + // + // The scenario including DSF in this evaluation would impact is loading the + // same stylesheet twice from the same document at two different DSF where the + // first load failed. (If the first load succeeded there would be no failure + // to report, the same as if we attempted to match DSF and found no load + // attempt). In this fail+success case including a DSF check would allow us to + // avoid an extra re-parse of the stylesheet, but that scenario is too + // edge-case to warrant further complicating the code for a small perf + // optimization. + StyleImage* image = nullptr; + if (fetcher && + RuntimeEnabledFeatures::StyleResourceFetcherIdentityCheckEnabled()) { + auto it = cached_images_.find(fetcher); + if (it != cached_images_.end()) { + image = it->value->image.Get(); + } + } else if (cached_image_) { + image = cached_image_.Get(); + } + if (!image) { return false; } - - if (ImageResourceContent* cached_content = cached_image_->CachedImage()) { - return cached_content->LoadFailedOrCanceled(); - } - - return true; + ImageResourceContent* content = image->CachedImage(); + return !content || content->LoadFailedOrCanceled(); } CSSImageSetValue* CSSImageSetValue::ResolveValuesAndCreateCopyIfNeeded( @@ -203,6 +252,7 @@ void CSSImageSetValue::TraceAfterDispatch(blink::Visitor* visitor) const { visitor->Trace(cached_image_); + visitor->Trace(cached_images_); visitor->Trace(options_); CSSValueList::TraceAfterDispatch(visitor); } diff --git a/third_party/blink/renderer/core/css/css_image_set_value.h b/third_party/blink/renderer/core/css/css_image_set_value.h index e290fe8..884faa1 100644 --- a/third_party/blink/renderer/core/css/css_image_set_value.h +++ b/third_party/blink/renderer/core/css/css_image_set_value.h @@ -27,7 +27,9 @@ #define THIRD_PARTY_BLINK_RENDERER_CORE_CSS_CSS_IMAGE_SET_VALUE_H_ #include "third_party/blink/renderer/core/css/css_value_list.h" +#include "third_party/blink/renderer/platform/heap/collection_support/heap_hash_map.h" #include "third_party/blink/renderer/platform/heap/collection_support/heap_vector.h" +#include "third_party/blink/renderer/platform/heap/garbage_collected.h" #include "third_party/blink/renderer/platform/wtf/allocator/allocator.h" #include "third_party/blink/renderer/platform/wtf/casting.h" @@ -35,6 +37,7 @@ class CSSImageSetOptionValue; class CSSLengthResolver; +class ResourceFetcher; class StyleImage; class StyleResolverState; @@ -51,16 +54,20 @@ cached_device_scale_factor_(device_scale_factor), options_(std::move(options)) {} - bool IsCachePending(const float device_scale_factor) const; - StyleImage* CachedImage(const float device_scale_factor) const; ... (file diff truncated)
Regression Test / PoC
diff --git a/third_party/blink/renderer/core/css/css_style_resource_fetcher_identity_test.cc b/third_party/blink/renderer/core/css/css_style_resource_fetcher_identity_test.cc
new file mode 100644
index 0000000..312b76e
--- /dev/null
+++ b/third_party/blink/renderer/core/css/css_style_resource_fetcher_identity_test.cc
@@ -0,0 +1,324 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/core/css/css_crossfade_value.h"
+#include "third_party/blink/renderer/core/css/css_font_face_src_value.h"
+#include "third_party/blink/renderer/core/css/css_image_set_value.h"
+#include "third_party/blink/renderer/core/css/css_image_value.h"
+#include "third_party/blink/renderer/core/css/css_numeric_literal_value.h"
+#include "third_party/blink/renderer/core/css/css_primitive_value.h"
+#include "third_party/blink/renderer/core/css/css_uri_value.h"
+#include "third_party/blink/renderer/core/css/css_url_data.h"
+#include "third_party/blink/renderer/core/css/css_value.h"
+#include "third_party/blink/renderer/core/css/parser/css_parser_context.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/execution_context/execution_context.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
+#include "third_party/blink/renderer/core/loader/empty_clients.h"
+#include "third_party/blink/renderer/core/loader/resource/font_resource.h"
+#include "third_party/blink/renderer/core/loader/resource/image_resource_content.h"
+#include "third_party/blink/renderer/core/style/style_fetched_image.h"
+#include "third_party/blink/renderer/core/style/style_image.h"
+#include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
+#include "third_party/blink/renderer/platform/heap/collection_support/heap_vector.h"
+#include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h"
+#include "third_party/blink/renderer/platform/loader/fetch/url_loader/url_loader.h"
+#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
+#include "third_party/blink/renderer/platform/testing/task_environment.h"
+#include "third_party/blink/renderer/platform/testing/url_loader_mock_factory.h"
+#include "third_party/blink/renderer/platform/testing/url_test_helpers.h"
+#include "third_party/blink/renderer/platform/weborigin/kurl.h"
+#include "third_party/blink/renderer/platform/wtf/functional.h"
+#include "third_party/blink/renderer/platform/wtf/text/atomic_string.h"
+#include "ui/gfx/geometry/size.h"
+
+namespace blink {
+
+namespace {
+
+const char kImageUrl[] = "https://example.com/a.png";
+const char kFontUrl[] = "https://example.com/a.woff2";
+
+class TestFontResourceClient final
+ : public GarbageCollected<TestFontResourceClient>,
+ public FontResourceClient {
+ public:
+ String DebugName() const override { return "TestFontResourceClient"; }
+};
+
+// A frame client that routes subresource loads through the URLLoaderMockFactory
+// so that image/font fetches can be driven to a deterministic completion (in
+// particular, an error) via url_test_helpers::ServeAsynchronousRequests().
+class MockLoaderFrameClient final : public EmptyLocalFrameClient {
+ public:
+ std::unique_ptr<URLLoader> CreateURLLoaderForTesting() override {
+ return URLLoaderMockFactory::GetSingletonInstance()->CreateURLLoader();
+ }
+};
+
+void ConfigureSettings(Settings& settings) {
+ // Images are otherwise deferred and never actually loaded, which would
+ // prevent us from observing a completed (failed) load.
+ settings.SetLoadsImagesAutomatically(true);
+}
+
+} // namespace
+
+// Tests for the caching behavior of style classes that use the ResourceFetcher
+// as an identifier for their caching.
+class StyleResourceFetcherIdentityTest : public testing::Test {
+ protected:
+ void SetUp() override {
+ holder_a_ = std::make_unique<DummyPageHolder>(
+ gfx::Size(), /*chrome_client=*/nullptr,
+ MakeGarbageCollected<MockLoaderFrameClient>(),
+ BindOnce(&ConfigureSettings));
+ holder_b_ = std::make_unique<DummyPageHolder>(
+ gfx::Size(), /*chrome_client=*/nullptr,
+ MakeGarbageCollected<MockLoaderFrameClient>(),
+ BindOnce(&ConfigureSettings));
+ }
+
+ void TearDown() override {
+ url_test_helpers::UnregisterAllURLsAndClearMemoryCache();
+ }
+
+ Document& DocA() const { return holder_a_->GetDocument(); }
+ Document& DocB() const { return holder_b_->GetDocument(); }
+ ResourceFetcher* FetcherA() const { return DocA().Fetcher(); }
+ ResourceFetcher* FetcherB() const { return DocB().Fetcher(); }
+
+ CSSImageValue* MakeImageValue(StyleImage* fetcher_agnostic_image = nullptr) {
+ auto* url_data = MakeGarbageCollected<CSSUrlData>(AtomicString(kImageUrl));
+ return MakeGarbageCollected<CSSImageValue>(*url_data,
+ fetcher_agnostic_image);
+ }
+
+ // Builds a concrete StyleImage backed by a not-yet-started
+ // ImageResourceContent. Its CachedImage() is non-null and reports no
+ // failure, so it represents a "cached, healthy" resource.
+ StyleImage* MakeStyleImage() {
+ auto* content = ImageResourceContent::CreateNotStarted();
+ auto* url_data = MakeGarbageCollected<CSSUrlData>(AtomicString(kImageUrl));
+ return MakeGarbageCollected<StyleFetchedImage>(content, *url_data, DocA(),
+ KURL(kImageUrl));
+ }
+
+ CSSFontFaceSrcValue* MakeFontValue() {
+ auto* url_data = MakeGarbageCollected<CSSUrlData>(AtomicString(kFontUrl));
+ auto* uri_value = MakeGarbageCollected<cssvalue::CSSURIValue>(*url_data);
+ const DOMWrapperWorld* world =
+ MakeGarbageCollected<CSSParserContext>(DocA())->JavascriptWorld();
+ return CSSFontFaceSrcValue::Create(uri_value, world);
+ }
+
+ test::TaskEnvironment task_environment_;
+ std::unique_ptr<DummyPageHolder> holder_a_;
+ std::unique_ptr<DummyPageHolder> holder_b_;
+};
+
+TEST_F(StyleResourceFetcherIdentityTest, CSSImageValueCaching) {
+ url_test_helpers::RegisterMockedErrorURLLoad(
+ url_test_helpers::ToKURL(kImageUrl));
+ CSSImageValue* value = MakeImageValue();
+
+ EXPECT_TRUE(value->IsCachePending(FetcherA()));
+ EXPECT_TRUE(value->IsCachePending(FetcherB()));
+ EXPECT_TRUE(value->IsCachePending(nullptr));
+ EXPECT_EQ(value->CachedImage(FetcherA()), nullptr);
+ EXPECT_EQ(value->CachedImage(FetcherB()), nullptr);
+ EXPECT_EQ(value->CachedImage(nullptr), nullptr);
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(FetcherA()));
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(FetcherB()));
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(nullptr));
+
+ StyleImage* cached_a = value->CacheImage(DocA());
+ ASSERT_TRUE(cached_a);
+
+ EXPECT_FALSE(value->IsCachePending(FetcherA()));
+ EXPECT_TRUE(value->IsCachePending(FetcherB()));
+ EXPECT_TRUE(value->IsCachePending(nullptr));
+ EXPECT_EQ(value->CachedImage(FetcherA()), cached_a);
+ EXPECT_EQ(value->CachedImage(FetcherB()), nullptr);
+ EXPECT_EQ(value->CachedImage(nullptr), nullptr);
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(FetcherA()));
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(FetcherB()));
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(nullptr));
+
+ url_test_helpers::ServeAsynchronousRequests();
+ EXPECT_TRUE(value->HasFailedOrCanceledSubresources(FetcherA()));
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(FetcherB()));
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(nullptr));
+
+ StyleImage* cached_b = value->CacheImage(DocB());
+ ASSERT_TRUE(cached_b);
+ EXPECT_NE(cached_a, cached_b);
+ EXPECT_FALSE(value->IsCachePending(FetcherB()));
+ EXPECT_EQ(value->CachedImage(FetcherA()), cached_a);
+ EXPECT_EQ(value->CachedImage(FetcherB()), cached_b);
+ EXPECT_EQ(value->CachedImage(nullptr), nullptr);
+
+ url_test_helpers::ServeAsynchronousRequests();
+ EXPECT_TRUE(value->HasFailedOrCanceledSubresources(FetcherA()));
+ EXPECT_TRUE(value->HasFailedOrCanceledSubresources(FetcherB()));
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(nullptr));
+
+ EXPECT_EQ(value->CacheImage(DocA()), cached_a);
+}
+
+TEST_F(StyleResourceFetcherIdentityTest,
+ CSSImageValueCachingWithFetcherAgnosticImage) {
+ url_test_helpers::RegisterMockedErrorURLLoad(
+ url_test_helpers::ToKURL(kImageUrl));
+ StyleImage* fetcher_agnostic_image = MakeStyleImage();
+ CSSImageValue* value = MakeImageValue(fetcher_agnostic_image);
+
+ // All contexts can use the fetcher_agnostic_image
+ EXPECT_FALSE(value->IsCachePending(FetcherA()));
+ EXPECT_FALSE(value->IsCachePending(FetcherB()));
+ EXPECT_FALSE(value->IsCachePending(nullptr));
+ EXPECT_EQ(value->CachedImage(FetcherA()), fetcher_agnostic_image);
+ EXPECT_EQ(value->CachedImage(FetcherB()), fetcher_agnostic_image);
+ EXPECT_EQ(value->CachedImage(nullptr), fetcher_agnostic_image);
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(FetcherA()));
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(FetcherB()));
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(nullptr));
+
+ // A per-fetcher fetch takes precedence over the fetcher_agnostic_image
+ StyleImage* cached_a = value->CacheImage(DocA());
+ ASSERT_TRUE(cached_a);
+ EXPECT_NE(cached_a, fetcher_agnostic_image);
+ EXPECT_EQ(value->CachedImage(FetcherA()), cached_a);
+ EXPECT_EQ(value->CachedImage(FetcherB()), fetcher_agnostic_image);
+ EXPECT_EQ(value->CachedImage(nullptr), fetcher_agnostic_image);
+
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(FetcherA()));
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(FetcherB()));
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(nullptr));
+}
+
+TEST_F(StyleResourceFetcherIdentityTest,
+ CSSImageValueHasFailedFetcherAgnosticImage) {
+ url_test_helpers::RegisterMockedErrorURLLoad(
+ url_test_helpers::ToKURL(kImageUrl));
+
+ // Produce a StyleImage in a failed state
+ CSSImageValue* source = MakeImageValue();
+ StyleImage* failed_agnostic_image = source->CacheImage(DocA());
+ ASSERT_TRUE(failed_agnostic_image);
+ url_test_helpers::ServeAsynchronousRequests();
+ ASSERT_TRUE(failed_agnostic_image->ErrorOccurred());
+
+ CSSImageValue* value = MakeImageValue(failed_agnostic_image);
+
+ // All contexts can use the (failed) fetcher_agnostic_image
+ EXPECT_FALSE(value->IsCachePending(FetcherA()));
+ EXPECT_FALSE(value->IsCachePending(FetcherB()));
+ EXPECT_FALSE(value->IsCachePending(nullptr));
+ EXPECT_EQ(value->CachedImage(FetcherA()), failed_agnostic_image);
+ EXPECT_EQ(value->CachedImage(FetcherB()), failed_agnostic_image);
+ EXPECT_EQ(value->CachedImage(nullptr), failed_agnostic_image);
+ EXPECT_TRUE(value->HasFailedOrCanceledSubresources(FetcherA()));
+ EXPECT_TRUE(value->HasFailedOrCanceledSubresources(FetcherB()));
+ EXPECT_TRUE(value->HasFailedOrCanceledSubresources(nullptr));
+}
+
+TEST_F(StyleResourceFetcherIdentityTest, CSSImageSetValueCaching) {
+ auto* value = MakeGarbageCollected<CSSImageSetValue>();
+
+ EXPECT_TRUE(value->IsCachePending(FetcherA(), 1.0f));
+ EXPECT_TRUE(value->IsCachePending(FetcherB(), 1.0f));
+ EXPECT_TRUE(value->IsCachePending(nullptr, 1.0f));
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(FetcherA()));
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(FetcherB()));
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(nullptr));
+
+ StyleImage* image_a = MakeStyleImage();
+ StyleImage* cached_a = value->CacheImage(FetcherA(), image_a, 1.0f);
+ ASSERT_TRUE(cached_a);
+
+ // CachedImage() is only queried where IsCachePending() is false, since it
+ // DCHECKs the entry exists.
+ EXPECT_FALSE(value->IsCachePending(FetcherA(), 1.0f));
+ EXPECT_TRUE(value->IsCachePending(FetcherA(), 2.0f));
+ EXPECT_TRUE(value->IsCachePending(FetcherB(), 1.0f));
+ EXPECT_TRUE(value->IsCachePending(nullptr, 1.0f));
+ EXPECT_EQ(value->CachedImage(FetcherA(), 1.0f), cached_a);
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(FetcherA()));
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(FetcherB()));
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(nullptr));
+
+ StyleImage* image_null = MakeStyleImage();
+ StyleImage* cached_null = value->CacheImage(nullptr, image_null, 1.0f);
+ ASSERT_TRUE(cached_null);
+ EXPECT_NE(cached_a, cached_null);
+ EXPECT_FALSE(value->IsCachePending(FetcherA(), 1.0f));
+ EXPECT_TRUE(value->IsCachePending(FetcherB(), 1.0f));
+ EXPECT_FALSE(value->IsCachePending(nullptr, 1.0f));
+ EXPECT_EQ(value->CachedImage(FetcherA(), 1.0f), cached_a);
+ EXPECT_EQ(value->CachedImage(nullptr, 1.0f), cached_null);
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(FetcherA()));
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(FetcherB()));
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(nullptr));
+
+ StyleImage* image_b = MakeStyleImage();
+ StyleImage* cached_b = value->CacheImage(FetcherB(), image_b, 1.0f);
+ ASSERT_TRUE(cached_b);
+ EXPECT_NE(cached_a, cached_b);
+ EXPECT_FALSE(value->IsCachePending(FetcherB(), 1.0f));
+ EXPECT_EQ(value->CachedImage(FetcherA(), 1.0f), cached_a);
+ EXPECT_EQ(value->CachedImage(FetcherB(), 1.0f), cached_b);
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(FetcherA()));
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(FetcherB()));
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(nullptr));
+
+ // Re-caching FetcherA at a new DPR replaces its single entry.
+ StyleImage* image_a_2 = MakeStyleImage();
+ StyleImage* cached_a_2 = value->CacheImage(FetcherA(), image_a_2, 2.0f);
+ ASSERT_TRUE(cached_a_2);
+ EXPECT_NE(cached_a, cached_a_2);
+ EXPECT_FALSE(value->IsCachePending(FetcherA(), 2.0f));
+ EXPECT_EQ(value->CachedImage(FetcherA(), 2.0f), cached_a_2);
+
+ StyleImage* cached_a_3 =
+ value->CacheImage(FetcherA(), /*style_image=*/nullptr, 2.0f);
+ ASSERT_TRUE(cached_a_3);
+ EXPECT_TRUE(value->HasFailedOrCanceledSubresources(FetcherA()));
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(FetcherB()));
+ EXPECT_FALSE(value->HasFailedOrCanceledSubresources(nullptr));
+}
+
+TEST_F(StyleResourceFetcherIdentityTest, CSSFontFaceSrcValueCaching) {
+ url_test_helpers::RegisterMockedErrorURLLoad(
... (truncated)
Original Bug Report
CSP bypass via shared CSSFontFaceSrcValue and CSSImageValue caches
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.
Overview: Blink improperly shares cached CSS subresources (fonts and images) across documents in the same renderer process without enforcing Content Security Policies. This allows a document with a strict CSP to render resources previously fetched by a lax document, bypassing its own CSP restrictions. The flaw stems from ignoring the return value of security checks during emulated resource loads from the cache.
Affected files:
third_party/blink/renderer/core/css/css_font_face_src_value.ccthird_party/blink/renderer/core/css/css_image_value.ccthird_party/blink/renderer/core/css/parser/css_parser_context.ccthird_party/blink/renderer/platform/loader/fetch/resource_fetcher.ccthird_party/blink/renderer/core/css/style_sheet_contents.ccthird_party/blink/renderer/core/loader/resource/css_style_sheet_resource.cc
Estimated timestamp from git blame: 2025-04-08
Description
A logic flaw in Blink’s CSS resource fetching pipeline allows documents to bypass their own Content Security Policy (CSP) when sharing parsed stylesheets within the same renderer process.
When a document loads a stylesheet, Blink parses it into a StyleSheetContents object and caches it in a CSSStyleSheetResource within the global MemoryCache. Subsequent requests for the same stylesheet by other documents in the same process will reuse this parsed object if CSSParserContext::operator== returns true. Because this equality check intentionally ignores the document and its CSP context, documents with differing CSPs can share the same StyleSheetContents.
Inside the shared StyleSheetContents, resources like @font-face sources (CSSFontFaceSrcValue) and background images (CSSImageValue) maintain internal state about their fetched resources (e.g., fetched_ and cached_image_).
If a lax document fetches the resource first, the internal state is populated. When a strict document (e.g., one with font-src 'none') later attempts to resolve the same CSS value, the class identifies that the resource is already fetched. To notify the inspector, it calls RestoreCachedResourceIfNeeded(), which delegates to ResourceFetcher::EmulateLoadStartedForInspector().
Inside EmulateLoadStartedForInspector(), Blink correctly validates the request against the new document’s policies by calling Context().CanRequest(...). This check accurately identifies the CSP violation and fires a violation report. However, EmulateLoadStartedForInspector() discards the blocked reason returned by CanRequest() and returns void. Consequently, the cached font or image is unconditionally passed to the client and rendered, effectively bypassing the strict document’s CSP.
Potential Reproduction Steps
Note: Our tooling agent does not run code; the following are suggested steps an attacker would follow to trigger the vulnerability.
- Host two pages on the same origin:
lax.html(no CSP) andstrict.html(with headerContent-Security-Policy: font-src 'none'). - Both pages include
<link rel="stylesheet" href="/style.css">and style a text element with a custom font. /style.csscontains@font-face { font-family: F; src: url(/font.woff2) }.- A victim visits
lax.html. The browser fetches the stylesheet, fetches/font.woff2, and caches theFontResourceinside the parsedCSSFontFaceSrcValue. lax.htmlopensstrict.htmlviawindow.open()(or the user navigates same-site in a way that preserves the renderer process).strict.htmlrequests/style.css. It hits theMemoryCache, reuses the sharedStyleSheetContents, and shares theCSSFontFaceSrcValue.- When
strict.htmlapplies the font,CSSFontFaceSrcValue::Fetchdetects the font is already fetched and callsEmulateLoadStartedForInspector. - A CSP violation report is generated, but the font resource is passed to the renderer anyway, displaying the text with the blocked custom font.
Suggested Fix
The core issue is that ResourceFetcher::EmulateLoadStartedForInspector (or a similar mechanism used for cached CSS subresources) does not provide a way to halt the usage of the resource if a security check fails.
- Modify
EmulateLoadStartedForInspectorto return aboolor anstd::optional<ResourceRequestBlockedReason>indicating whether the emulated load is permitted by the current context. - Update
CSSFontFaceSrcValue::RestoreCachedResourceIfNeededandCSSImageValue::RestoreCachedResourceIfNeededto propagate this status back to the caller. - In
CSSFontFaceSrcValue::FetchandCSSImageValue::CacheImage, evaluate the result. If the security check fails, do not return thefetched_/cached_image_resource, and instead handle it as a blocked resource load.
Evaluated with Chrome root at commit: 7353d249d9cacf9c7218e1d7b8a39cf39c72d646
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.