From 7f2ea8fcf41a68add90efab89609218407e1a824 Mon Sep 17 00:00:00 2001 From: Matthew Finkel Date: Tue, 22 Aug 2023 19:19:19 -0700 Subject: [PATCH] Partition Blob Registry by the top-level main document origin https://bugs.webkit.org/show_bug.cgi?id=260035 rdar://problem/113705298 Reviewed by Alex Christensen and Sihui Liu. Public blob URLs are only accessible from same-origin dcuments, but access is not restricted by the top-level origin. This means that Blob URLs can be used as a cross-origin tracking mechanism within iframes. In this patch we partition public blob URLs within the Blob Registry by top-level origin. This partitioning is controlled by a feature flag that is disabled by default. I took a few approaches at solving this. The most difficult challenge was finding a solution that allowed retrieving BlobData using a public blob URL from WKWebView APIs. In that case, the relevant top document may not be obvious, or may not exist. As a result, the design of this partitioning is more like access control rather than adding another key into the hashmap. Two alternative designs I considered include creating a second hashmap that is keyed by and we lookup the BlobData in that map if we have a SecurityOriginData, otherwise we use the unpartitioned map. Or, we create a new map from URL -> SecurityOriginData where we can lookup the associated top origin SecurityOriginData if we don't already know it. However, both of these options are more complex than the chosen implementation, and neither of them seemed safer. This change also enforces a noopener policy on new windows when the top origin of the opener is cross-origin with the blob's security origin. This is a mitigation that was discussed in the blob URL storage partitioning issue [0] with cross-engine support, and that seemed reasonable to me. [0] https://github.com/w3c/FileAPI/issues/153 * LayoutTests/TestExpectations: * LayoutTests/http/tests/local/blob/download-blob-from-iframe-expected.txt: Added. * LayoutTests/http/tests/local/blob/download-blob-from-iframe.html: Added. * LayoutTests/http/tests/local/blob/navigate-blob-expected.txt: Added. * LayoutTests/http/tests/local/blob/navigate-blob.html: Added. * LayoutTests/http/tests/local/blob/resources/broadcast-channel-proxy.html: Added. * LayoutTests/http/tests/local/blob/resources/iframe-creating-or-downloading-blob.html: Added. * LayoutTests/http/tests/local/blob/resources/iframe-for-creating-and-navigating-to-blob.html: Added. * LayoutTests/http/tests/local/blob/resources/main-frame-with-iframe-creating-or-navigating-to-blob.html: Added. * LayoutTests/http/tests/local/blob/resources/main-frame-with-iframe-downloading-blob.html: Added. * LayoutTests/http/tests/security/blob-null-url-location-origin-expected.txt: * LayoutTests/http/tests/security/blob-null-url-location-origin.html: * LayoutTests/http/tests/security/cross-origin-blob-transfer-expected.txt: Added. * LayoutTests/http/tests/security/cross-origin-blob-transfer.html: Added. * LayoutTests/http/tests/security/resources/iframe-cross-origin-blob-transfer.html: Added. * LayoutTests/http/tests/security/top-level-unique-origin2.https.html: * LayoutTests/platform/gtk-wk2/http/tests/local/blob/download-blob-from-iframe-expected.txt: Added. * LayoutTests/platform/mac-wk1/TestExpectations: * Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml: * Source/WebCore/fileapi/BlobURL.cpp: (WebCore::BlobURL::isInternalURL): * Source/WebCore/fileapi/BlobURL.h: * Source/WebCore/fileapi/ThreadableBlobRegistry.cpp: (WebCore::ThreadableBlobRegistry::registerInternalFileBlobURL): (WebCore::ThreadableBlobRegistry::registerInternalBlobURL): (WebCore::ThreadableBlobRegistry::registerInternalBlobURLOptionallyFileBacked): (WebCore::ThreadableBlobRegistry::registerInternalBlobURLForSlice): (WebCore::isInternalBlobURL): Deleted. * Source/WebCore/loader/FrameLoader.cpp: (WebCore::FrameLoader::loadURL): (WebCore::FrameLoader::loadPostRequest): (WebCore::createWindow): * Source/WebCore/platform/network/BlobRegistryImpl.cpp: (WebCore::BlobRegistryImpl::registerBlobURLOptionallyFileBacked): (WebCore::BlobRegistryImpl::unregisterBlobURL): (WebCore::BlobRegistryImpl::getBlobDataFromURL const): (WebCore::BlobRegistryImpl::addBlobData): (WebCore::BlobRegistryImpl::registerBlobURLHandle): (WebCore::BlobRegistryImpl::unregisterBlobURLHandle): * Source/WebCore/platform/network/BlobRegistryImpl.h: Canonical link: https://commits.webkit.org/267172@main --- LayoutTests/TestExpectations | 1 + .../download-blob-from-iframe-expected.txt | 60 +++++++++ .../local/blob/download-blob-from-iframe.html | 103 +++++++++++++++ .../local/blob/navigate-blob-expected.txt | 48 +++++++ .../http/tests/local/blob/navigate-blob.html | 117 ++++++++++++++++++ .../resources/broadcast-channel-proxy.html | 12 ++ .../iframe-creating-or-downloading-blob.html | 28 +++++ ...e-for-creating-and-navigating-to-blob.html | 66 ++++++++++ ...iframe-creating-or-navigating-to-blob.html | 111 +++++++++++++++++ ...in-frame-with-iframe-downloading-blob.html | 24 ++++ ...blob-null-url-location-origin-expected.txt | 6 +- .../blob-null-url-location-origin.html | 2 +- .../cross-origin-blob-transfer-expected.txt | 16 +++ .../security/cross-origin-blob-transfer.html | 29 +++++ .../iframe-cross-origin-blob-transfer.html | 8 ++ .../top-level-unique-origin2.https.html | 2 +- .../download-blob-from-iframe-expected.txt | 60 +++++++++ LayoutTests/platform/mac-wk1/TestExpectations | 4 + .../Preferences/UnifiedWebPreferences.yaml | 6 +- Source/WebCore/fileapi/BlobURL.cpp | 7 ++ Source/WebCore/fileapi/BlobURL.h | 3 + .../fileapi/ThreadableBlobRegistry.cpp | 16 +-- Source/WebCore/loader/FrameLoader.cpp | 15 +++ .../platform/network/BlobRegistryImpl.cpp | 75 ++++++++--- .../platform/network/BlobRegistryImpl.h | 5 +- 25 files changed, 787 insertions(+), 37 deletions(-) create mode 100644 LayoutTests/http/tests/local/blob/download-blob-from-iframe-expected.txt create mode 100644 LayoutTests/http/tests/local/blob/download-blob-from-iframe.html create mode 100644 LayoutTests/http/tests/local/blob/navigate-blob-expected.txt create mode 100644 LayoutTests/http/tests/local/blob/navigate-blob.html create mode 100644 LayoutTests/http/tests/local/blob/resources/broadcast-channel-proxy.html create mode 100644 LayoutTests/http/tests/local/blob/resources/iframe-creating-or-downloading-blob.html create mode 100644 LayoutTests/http/tests/local/blob/resources/iframe-for-creating-and-navigating-to-blob.html create mode 100644 LayoutTests/http/tests/local/blob/resources/main-frame-with-iframe-creating-or-navigating-to-blob.html create mode 100644 LayoutTests/http/tests/local/blob/resources/main-frame-with-iframe-downloading-blob.html create mode 100644 LayoutTests/http/tests/security/cross-origin-blob-transfer-expected.txt create mode 100644 LayoutTests/http/tests/security/cross-origin-blob-transfer.html create mode 100644 LayoutTests/http/tests/security/resources/iframe-cross-origin-blob-transfer.html create mode 100644 LayoutTests/platform/gtk/http/tests/local/blob/download-blob-from-iframe-expected.txt diff --git a/LayoutTests/TestExpectations b/LayoutTests/TestExpectations index 6214500565f46..1e65322c87950 100644 --- a/LayoutTests/TestExpectations +++ b/LayoutTests/TestExpectations @@ -5046,6 +5046,7 @@ webkit.org/b/227086 imported/w3c/web-platform-tests/webxr/xrWebGLLayer_construct # Extra logging that needs to be silenced: imported/w3c/web-platform-tests/webxr/webxr_availability.http.sub.html [ DumpJSConsoleLogInStdErr ] +http/tests/local/blob/navigate-blob.html [ DumpJSConsoleLogInStdErr ] # "Opacity on an inline element should apply on float child". webkit.org/b/234690 imported/w3c/web-platform-tests/css/css-color/inline-opacity-float-child.html [ ImageOnlyFailure ] diff --git a/LayoutTests/http/tests/local/blob/download-blob-from-iframe-expected.txt b/LayoutTests/http/tests/local/blob/download-blob-from-iframe-expected.txt new file mode 100644 index 0000000000000..4aeccb4f57bb4 --- /dev/null +++ b/LayoutTests/http/tests/local/blob/download-blob-from-iframe-expected.txt @@ -0,0 +1,60 @@ +Download started. +Downloading URL with suggested filename "testBlobFileName.html" +Download completed. +Download started. +Download failed. +Failed: WebKitBlobResource, code=1, description=The operation couldn’t be completed. (WebKitBlobResource error 1.) +Download started. +Download failed. +Failed: WebKitBlobResource, code=1, description=The operation couldn’t be completed. (WebKitBlobResource error 1.) +Download started. +Download failed. +Failed: WebKitBlobResource, code=1, description=The operation couldn’t be completed. (WebKitBlobResource error 1.) +Download started. +Download failed. +Failed: WebKitBlobResource, code=1, description=The operation couldn’t be completed. (WebKitBlobResource error 1.) +Download started. +Download failed. +Failed: WebKitBlobResource, code=1, description=The operation couldn’t be completed. (WebKitBlobResource error 1.) +Download started. +Download failed. +Failed: WebKitBlobResource, code=1, description=The operation couldn’t be completed. (WebKitBlobResource error 1.) +Download started. +Downloading URL with suggested filename "testBlobFileName.html" +Download completed. +PASS successfullyParsed is true + +TEST COMPLETE +Opening https://localhost:8443 as main frame with iframe origin https://localhost:8443, creating blob +PASS Opened window +PASS iframe: created blob +Opening https://localhost:8443 as main frame with iframe origin https://localhost:8443, downloading blob +PASS Opened window +PASS iframe: downloading blob +Opening http://localhost:8000 as main frame with iframe origin https://localhost:8443, downloading blob +PASS Opened window +PASS iframe: downloading blob +Opening http://127.0.0.1:8000 as main frame with iframe origin https://localhost:8443, downloading blob +PASS Opened window +PASS iframe: downloading blob +Opening https://127.0.0.1:8443 as main frame with iframe origin https://localhost:8443, downloading blob +PASS Opened window +PASS iframe: downloading blob +Opening https://127.0.0.1:8443 as main frame with iframe origin https://localhost:8443, creating blob +PASS Opened window +PASS iframe: created blob +Opening https://localhost:8443 as main frame with iframe origin https://localhost:8443, downloading blob +PASS Opened window +PASS iframe: downloading blob +Opening http://localhost:8000 as main frame with iframe origin https://localhost:8443, downloading blob +PASS Opened window +PASS iframe: downloading blob +Opening http://127.0.0.1:8000 as main frame with iframe origin https://localhost:8443, downloading blob +PASS Opened window +PASS iframe: downloading blob +Opening https://127.0.0.1:8443 as main frame with iframe origin https://localhost:8443, downloading blob +PASS Opened window +PASS iframe: downloading blob + +PASS Test for creating blob in iframe and then downloading to it in same-origin iframe and same-origin, same-site, and cross-site main frames. + diff --git a/LayoutTests/http/tests/local/blob/download-blob-from-iframe.html b/LayoutTests/http/tests/local/blob/download-blob-from-iframe.html new file mode 100644 index 0000000000000..2e84858b2447d --- /dev/null +++ b/LayoutTests/http/tests/local/blob/download-blob-from-iframe.html @@ -0,0 +1,103 @@ + + + + + + + + +

+
+ + + + diff --git a/LayoutTests/http/tests/local/blob/navigate-blob-expected.txt b/LayoutTests/http/tests/local/blob/navigate-blob-expected.txt new file mode 100644 index 0000000000000..1fe4769c058b3 --- /dev/null +++ b/LayoutTests/http/tests/local/blob/navigate-blob-expected.txt @@ -0,0 +1,48 @@ +PASS successfullyParsed is true + +TEST COMPLETE +Opening https://localhost:8443 as main frame with iframe origin https://localhost:8443 creating blob +PASS iframe: created blob +PASS main frame: Fetched Blob +PASS main frame: open() succeeded. Received message from blob: successfully navigated, have opener: true +Opening http://127.0.0.1:8000 as main frame with iframe origin https://localhost:8443 +FAIL iframe: Fetching blob failed: Load failed +FAIL iframe: WindowProxy handle is null (probably opened blob url with noopener) +FAIL iframe: Could not open blob url, timed out +FAIL main frame: Fetching blob failed: Load failed +FAIL main frame: Could not open blob url, timed out +Opening http://localhost:8000 as main frame with iframe origin https://localhost:8443 +FAIL iframe: Fetching blob failed: Load failed +FAIL iframe: WindowProxy handle is null (probably opened blob url with noopener) +FAIL iframe: Could not open blob url, timed out +FAIL main frame: Fetching blob failed: Load failed +FAIL main frame: Could not open blob url, timed out +Opening https://localhost:8443 as main frame with iframe origin https://localhost:8443 +PASS iframe: Fetched Blob +PASS iframe: open() succeeded. Received message from blob: successfully navigated, have opener: true +PASS main frame: Fetched Blob +PASS main frame: open() succeeded. Received message from blob: successfully navigated, have opener: true +Opening http://127.0.0.1:8000 as main frame with iframe origin https://localhost:8443 creating blob +PASS iframe: created blob +FAIL main frame: Fetching blob failed: Load failed +FAIL main frame: Could not open blob url, timed out +Opening https://localhost:8443 as main frame with iframe origin https://localhost:8443 +FAIL iframe: Fetching blob failed: Load failed +FAIL iframe: Could not open blob url, timed out +FAIL main frame: Fetching blob failed: Load failed +FAIL main frame: Could not open blob url, timed out +Opening http://localhost:8000 as main frame with iframe origin https://localhost:8443 +FAIL iframe: Fetching blob failed: Load failed +FAIL iframe: WindowProxy handle is null (probably opened blob url with noopener) +FAIL iframe: Could not open blob url, timed out +FAIL main frame: Fetching blob failed: Load failed +FAIL main frame: Could not open blob url, timed out +Opening http://127.0.0.1:8000 as main frame with iframe origin https://localhost:8443 +PASS iframe: Fetched Blob +FAIL iframe: WindowProxy handle is null (probably opened blob url with noopener) +PASS iframe: open() succeeded. Received message from blob: successfully navigated, have opener: false +FAIL main frame: Fetching blob failed: Load failed +FAIL main frame: Could not open blob url, timed out + +PASS Test for creating blob in iframe and then navigating to it from same-origin iframe and cross-origin main frames. + diff --git a/LayoutTests/http/tests/local/blob/navigate-blob.html b/LayoutTests/http/tests/local/blob/navigate-blob.html new file mode 100644 index 0000000000000..5b218d167beb5 --- /dev/null +++ b/LayoutTests/http/tests/local/blob/navigate-blob.html @@ -0,0 +1,117 @@ + + + + + + + + +

+
+ + + + diff --git a/LayoutTests/http/tests/local/blob/resources/broadcast-channel-proxy.html b/LayoutTests/http/tests/local/blob/resources/broadcast-channel-proxy.html new file mode 100644 index 0000000000000..fc8767ec110e4 --- /dev/null +++ b/LayoutTests/http/tests/local/blob/resources/broadcast-channel-proxy.html @@ -0,0 +1,12 @@ + + + + + + + diff --git a/LayoutTests/http/tests/local/blob/resources/iframe-creating-or-downloading-blob.html b/LayoutTests/http/tests/local/blob/resources/iframe-creating-or-downloading-blob.html new file mode 100644 index 0000000000000..cdb4e7507bdd1 --- /dev/null +++ b/LayoutTests/http/tests/local/blob/resources/iframe-creating-or-downloading-blob.html @@ -0,0 +1,28 @@ + + + + + + diff --git a/LayoutTests/http/tests/local/blob/resources/iframe-for-creating-and-navigating-to-blob.html b/LayoutTests/http/tests/local/blob/resources/iframe-for-creating-and-navigating-to-blob.html new file mode 100644 index 0000000000000..646d63f43d4a3 --- /dev/null +++ b/LayoutTests/http/tests/local/blob/resources/iframe-for-creating-and-navigating-to-blob.html @@ -0,0 +1,66 @@ + + + + + + diff --git a/LayoutTests/http/tests/local/blob/resources/main-frame-with-iframe-creating-or-navigating-to-blob.html b/LayoutTests/http/tests/local/blob/resources/main-frame-with-iframe-creating-or-navigating-to-blob.html new file mode 100644 index 0000000000000..63f8fba087adc --- /dev/null +++ b/LayoutTests/http/tests/local/blob/resources/main-frame-with-iframe-creating-or-navigating-to-blob.html @@ -0,0 +1,111 @@ + + + + + + diff --git a/LayoutTests/http/tests/local/blob/resources/main-frame-with-iframe-downloading-blob.html b/LayoutTests/http/tests/local/blob/resources/main-frame-with-iframe-downloading-blob.html new file mode 100644 index 0000000000000..0b1228661616e --- /dev/null +++ b/LayoutTests/http/tests/local/blob/resources/main-frame-with-iframe-downloading-blob.html @@ -0,0 +1,24 @@ + + + + + + diff --git a/LayoutTests/http/tests/security/blob-null-url-location-origin-expected.txt b/LayoutTests/http/tests/security/blob-null-url-location-origin-expected.txt index 070b1d23ca182..d15eebc12f9f4 100644 --- a/LayoutTests/http/tests/security/blob-null-url-location-origin-expected.txt +++ b/LayoutTests/http/tests/security/blob-null-url-location-origin-expected.txt @@ -1,5 +1,5 @@ CONSOLE MESSAGE: data URL frame loaded -CONSOLE MESSAGE: blob popup opened -CONSOLE MESSAGE: blob popup loadednull -CONSOLE MESSAGE: blob popup message posted +CONSOLE MESSAGE: blob iframe opened +CONSOLE MESSAGE: blob iframe loadednull +CONSOLE MESSAGE: blob iframe message posted PASS diff --git a/LayoutTests/http/tests/security/blob-null-url-location-origin.html b/LayoutTests/http/tests/security/blob-null-url-location-origin.html index 23aaa03382ed6..417ad61a4d793 100644 --- a/LayoutTests/http/tests/security/blob-null-url-location-origin.html +++ b/LayoutTests/http/tests/security/blob-null-url-location-origin.html @@ -31,5 +31,5 @@ testRunner.notifyDone(); }, 10000); - + diff --git a/LayoutTests/http/tests/security/cross-origin-blob-transfer-expected.txt b/LayoutTests/http/tests/security/cross-origin-blob-transfer-expected.txt new file mode 100644 index 0000000000000..6e3bdf7787c8e --- /dev/null +++ b/LayoutTests/http/tests/security/cross-origin-blob-transfer-expected.txt @@ -0,0 +1,16 @@ +PASS successfullyParsed is true + +TEST COMPLETE + +Summary + +Harness status: OK + +Found 1 tests + +1 Pass +Details + +Result Test Name Message +Pass Test for creating blob in iframe and then transferring it cross-origin. +Asserts run diff --git a/LayoutTests/http/tests/security/cross-origin-blob-transfer.html b/LayoutTests/http/tests/security/cross-origin-blob-transfer.html new file mode 100644 index 0000000000000..bc4f0a177a580 --- /dev/null +++ b/LayoutTests/http/tests/security/cross-origin-blob-transfer.html @@ -0,0 +1,29 @@ + + + + + + + + + + diff --git a/LayoutTests/platform/gtk/http/tests/local/blob/download-blob-from-iframe-expected.txt b/LayoutTests/platform/gtk/http/tests/local/blob/download-blob-from-iframe-expected.txt new file mode 100644 index 0000000000000..0b39ae750464e --- /dev/null +++ b/LayoutTests/platform/gtk/http/tests/local/blob/download-blob-from-iframe-expected.txt @@ -0,0 +1,60 @@ +Download started. +Downloading URL with suggested filename "testBlobFileName.html" +Download completed. +Download started. +Download failed. +Failed: WebKitBlobResource, code=1, description= +Download started. +Download failed. +Failed: WebKitBlobResource, code=1, description= +Download started. +Download failed. +Failed: WebKitBlobResource, code=1, description= +Download started. +Download failed. +Failed: WebKitBlobResource, code=1, description= +Download started. +Download failed. +Failed: WebKitBlobResource, code=1, description= +Download started. +Download failed. +Failed: WebKitBlobResource, code=1, description= +Download started. +Downloading URL with suggested filename "testBlobFileName.html" +Download completed. +PASS successfullyParsed is true + +TEST COMPLETE +Opening https://localhost:8443 as main frame with iframe origin https://localhost:8443, creating blob +PASS Opened window +PASS iframe: created blob +Opening https://localhost:8443 as main frame with iframe origin https://localhost:8443, downloading blob +PASS Opened window +PASS iframe: downloading blob +Opening http://localhost:8000 as main frame with iframe origin https://localhost:8443, downloading blob +PASS Opened window +PASS iframe: downloading blob +Opening http://127.0.0.1:8000 as main frame with iframe origin https://localhost:8443, downloading blob +PASS Opened window +PASS iframe: downloading blob +Opening https://127.0.0.1:8443 as main frame with iframe origin https://localhost:8443, downloading blob +PASS Opened window +PASS iframe: downloading blob +Opening https://127.0.0.1:8443 as main frame with iframe origin https://localhost:8443, creating blob +PASS Opened window +PASS iframe: created blob +Opening https://localhost:8443 as main frame with iframe origin https://localhost:8443, downloading blob +PASS Opened window +PASS iframe: downloading blob +Opening http://localhost:8000 as main frame with iframe origin https://localhost:8443, downloading blob +PASS Opened window +PASS iframe: downloading blob +Opening http://127.0.0.1:8000 as main frame with iframe origin https://localhost:8443, downloading blob +PASS Opened window +PASS iframe: downloading blob +Opening https://127.0.0.1:8443 as main frame with iframe origin https://localhost:8443, downloading blob +PASS Opened window +PASS iframe: downloading blob + +PASS Test for creating blob in iframe and then downloading to it in same-origin iframe and same-origin, same-site, and cross-site main frames. + diff --git a/LayoutTests/platform/mac-wk1/TestExpectations b/LayoutTests/platform/mac-wk1/TestExpectations index f2f2c06ca6516..a77b84e492fe6 100644 --- a/LayoutTests/platform/mac-wk1/TestExpectations +++ b/LayoutTests/platform/mac-wk1/TestExpectations @@ -895,6 +895,10 @@ webkit.org/b/156069 http/tests/download/anchor-download-attribute-content-dispos webkit.org/b/156069 http/tests/download/anchor-download-attribute-content-disposition-no-extension-text-plain.html [ Skip ] webkit.org/b/156069 http/tests/security/anchor-download-octet-stream-no-extension.html [ Skip ] webkit.org/b/156069 http/wpt/html/semantics/text-level-semantics/the-a-element/a-download-click-404.html [ Skip ] +webkit.org/b/156069 http/tests/local/blob/download-blob-from-iframe.html [ Skip ] + +# Blob URL registry partitioning not supported +http/tests/local/blob/navigate-blob.html [ Skip ] # Not supported on WK1 media/no-fullscreen-when-hidden.html [ Skip ] diff --git a/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml b/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml index 9bdf1732eba9a..ca7a07504027c 100644 --- a/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml +++ b/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml @@ -783,11 +783,13 @@ BlobRegistryTopOriginPartitioningEnabled: status: unstable humanReadableName: "Partition Blob URL Registry" humanReadableDescription: "Partition Blob URL Registry by Top-Level Origin" - webcoreBinding: none - exposed: [ WebKit ] defaultValue: + WebKitLegacy: + default: false WebKit: default: false + WebCore: + default: false BlockIOKitInWebContentSandbox: type: bool diff --git a/Source/WebCore/fileapi/BlobURL.cpp b/Source/WebCore/fileapi/BlobURL.cpp index e74eef9738760..b7b82419fe626 100644 --- a/Source/WebCore/fileapi/BlobURL.cpp +++ b/Source/WebCore/fileapi/BlobURL.cpp @@ -90,4 +90,11 @@ URL BlobURL::createBlobURL(StringView originString) return URL({ }, urlString); } +#if ASSERT_ENABLED +bool BlobURL::isInternalURL(const URL& url) +{ + return url.string().startsWith("blob:blobinternal://"_s); +} +#endif + } // namespace WebCore diff --git a/Source/WebCore/fileapi/BlobURL.h b/Source/WebCore/fileapi/BlobURL.h index a044ed8d5d424..780ead1c8e2a6 100644 --- a/Source/WebCore/fileapi/BlobURL.h +++ b/Source/WebCore/fileapi/BlobURL.h @@ -52,6 +52,9 @@ class BlobURL { static URL getOriginURL(const URL&); static bool isSecureBlobURL(const URL&); +#if ASSERT_ENABLED + static bool isInternalURL(const URL&); +#endif private: static URL createBlobURL(StringView originString); diff --git a/Source/WebCore/fileapi/ThreadableBlobRegistry.cpp b/Source/WebCore/fileapi/ThreadableBlobRegistry.cpp index 2751d290f714a..25f7ea672ef37 100644 --- a/Source/WebCore/fileapi/ThreadableBlobRegistry.cpp +++ b/Source/WebCore/fileapi/ThreadableBlobRegistry.cpp @@ -76,14 +76,6 @@ static inline bool isBlobURLContainingNullOrigin(const URL& url) return StringView(url.string()).substring(startIndex, endIndex - startIndex - 1) == "null"_s; } -#if ASSERT_ENABLED -static inline bool isInternalBlobURL(const URL& url) -{ - constexpr auto prefix = "blob:blobinternal://"_s; - return url.string().startsWith(prefix); -} -#endif - // If the blob URL contains null origin, as in the context with unique security origin or file URL, save the mapping between url and origin so that the origin can be retrived when doing security origin check. static void addToOriginMapIfNecessary(const URL& url, RefPtr&& origin) { @@ -97,7 +89,7 @@ static void addToOriginMapIfNecessary(const URL& url, RefPtr&& o void ThreadableBlobRegistry::registerInternalFileBlobURL(const URL& url, const String& path, const String& replacementPath, const String& contentType) { - ASSERT(isInternalBlobURL(url)); + ASSERT(BlobURL::isInternalURL(url)); String effectivePath = !replacementPath.isNull() ? replacementPath : path; if (isMainThread()) { @@ -112,7 +104,7 @@ void ThreadableBlobRegistry::registerInternalFileBlobURL(const URL& url, const S void ThreadableBlobRegistry::registerInternalBlobURL(const URL& url, Vector&& blobParts, const String& contentType) { - ASSERT(isInternalBlobURL(url)); + ASSERT(BlobURL::isInternalURL(url)); if (isMainThread()) { blobRegistry().registerInternalBlobURL(url, WTFMove(blobParts), contentType); return; @@ -160,7 +152,7 @@ void ThreadableBlobRegistry::registerBlobURL(SecurityOrigin* origin, PolicyConta void ThreadableBlobRegistry::registerInternalBlobURLOptionallyFileBacked(const URL& url, const URL& srcURL, const String& fileBackedPath, const String& contentType) { - ASSERT(isInternalBlobURL(url)); + ASSERT(BlobURL::isInternalURL(url)); if (isMainThread()) { blobRegistry().registerInternalBlobURLOptionallyFileBacked(url, srcURL, BlobDataFileReference::create(fileBackedPath), contentType); return; @@ -172,7 +164,7 @@ void ThreadableBlobRegistry::registerInternalBlobURLOptionallyFileBacked(const U void ThreadableBlobRegistry::registerInternalBlobURLForSlice(const URL& newURL, const URL& srcURL, long long start, long long end, const String& contentType) { - ASSERT(isInternalBlobURL(newURL)); + ASSERT(BlobURL::isInternalURL(newURL)); if (isMainThread()) { blobRegistry().registerInternalBlobURLForSlice(newURL, srcURL, start, end, contentType); return; diff --git a/Source/WebCore/loader/FrameLoader.cpp b/Source/WebCore/loader/FrameLoader.cpp index 2c6405c7a1cbd..db892ade30f85 100644 --- a/Source/WebCore/loader/FrameLoader.cpp +++ b/Source/WebCore/loader/FrameLoader.cpp @@ -1450,6 +1450,11 @@ void FrameLoader::loadURL(FrameLoadRequest&& frameLoadRequest, const String& ref openerPolicy = NewFrameOpenerPolicy::Suppress; } + if (m_frame.document()->settingsValues().blobRegistryTopOriginPartitioningEnabled && frameLoadRequest.resourceRequest().url().protocolIsBlob() && !m_frame.document()->securityOrigin().isSameOriginAs(m_frame.document()->topOrigin())) { + effectiveFrameName = blankTargetFrameName(); + openerPolicy = NewFrameOpenerPolicy::Suppress; + } + policyChecker().checkNewWindowPolicy(WTFMove(action), WTFMove(request), WTFMove(formState), effectiveFrameName, [this, allowNavigationToInvalidURL, openerPolicy, completionHandler = completionHandlerCaller.release()] (const ResourceRequest& request, WeakPtr&& formState, const AtomString& frameName, const NavigationAction& action, ShouldContinuePolicyCheck shouldContinue) mutable { continueLoadAfterNewWindowPolicy(request, formState.get(), frameName, action, shouldContinue, allowNavigationToInvalidURL, openerPolicy); completionHandler(); @@ -3267,6 +3272,11 @@ void FrameLoader::loadPostRequest(FrameLoadRequest&& request, const String& refe openerPolicy = NewFrameOpenerPolicy::Suppress; } + if (m_frame.document()->settingsValues().blobRegistryTopOriginPartitioningEnabled && request.resourceRequest().url().protocolIsBlob() && !m_frame.document()->securityOrigin().isSameOriginAs(m_frame.document()->topOrigin())) { + frameName = blankTargetFrameName(); + openerPolicy = NewFrameOpenerPolicy::Suppress; + } + policyChecker().checkNewWindowPolicy(WTFMove(action), WTFMove(workingResourceRequest), WTFMove(formState), frameName, [this, allowNavigationToInvalidURL, openerPolicy, completionHandler = WTFMove(completionHandler)] (const ResourceRequest& request, WeakPtr&& formState, const AtomString& frameName, const NavigationAction& action, ShouldContinuePolicyCheck shouldContinue) mutable { continueLoadAfterNewWindowPolicy(request, formState.get(), frameName, action, shouldContinue, allowNavigationToInvalidURL, openerPolicy); completionHandler(); @@ -4381,6 +4391,11 @@ RefPtr createWindow(LocalFrame& openerFrame, LocalFrame& lookupFrame features.noopener = true; } + if (openerFrame.document()->settingsValues().blobRegistryTopOriginPartitioningEnabled && request.resourceRequest().url().protocolIsBlob() && !openerFrame.document()->securityOrigin().isSameOriginAs(openerFrame.document()->topOrigin())) { + request.setFrameName(blankTargetFrameName()); + features.noopener = true; + } + // Sandboxed frames cannot open new auxiliary browsing contexts. if (isDocumentSandboxed(openerFrame, SandboxPopups)) { // FIXME: This message should be moved off the console once a solution to https://bugs.webkit.org/show_bug.cgi?id=103274 exists. diff --git a/Source/WebCore/platform/network/BlobRegistryImpl.cpp b/Source/WebCore/platform/network/BlobRegistryImpl.cpp index c99e7e065e2de..9b52ab321036b 100644 --- a/Source/WebCore/platform/network/BlobRegistryImpl.cpp +++ b/Source/WebCore/platform/network/BlobRegistryImpl.cpp @@ -35,11 +35,14 @@ #include "BlobData.h" #include "BlobPart.h" #include "BlobResourceHandle.h" +#include "BlobURL.h" +#include "Logging.h" #include "PolicyContainer.h" #include "ResourceError.h" #include "ResourceHandle.h" #include "ResourceRequest.h" #include "ResourceResponse.h" +#include "SecurityOriginData.h" #include #include #include @@ -112,6 +115,14 @@ void BlobRegistryImpl::appendStorageItems(BlobData* blobData, const BlobDataItem ASSERT(!length); } +void BlobRegistryImpl::setPartitioningEnabled(bool enabled) +{ + if (enabled && m_allowedBlobURLTopOrigins) + return; + RELEASE_LOG(Network, "BlobRegistryImpl::setPartitioningEnabled: (%p) enabled: %d.", this, enabled); + m_allowedBlobURLTopOrigins = enabled ? std::optional { std::in_place, URLToTopOriginHashMap { } } : std::nullopt; +} + void BlobRegistryImpl::registerInternalFileBlobURL(const URL& url, Ref&& file, const String& contentType) { ASSERT(isMainThread()); @@ -201,6 +212,7 @@ void BlobRegistryImpl::registerInternalBlobURLOptionallyFileBacked(const URL& ur void BlobRegistryImpl::registerBlobURLOptionallyFileBacked(const URL& url, const URL& srcURL, RefPtr&& file, const String& contentType, const PolicyContainer& policyContainer, const std::optional& topOrigin) { ASSERT(isMainThread()); + ASSERT(BlobURL::isInternalURL(url) || topOrigin); registerBlobResourceHandleConstructor(); BlobData* src = getBlobDataFromURL(srcURL); @@ -261,19 +273,32 @@ void BlobRegistryImpl::registerInternalBlobURLForSlice(const URL& url, const URL addBlobData(url.string(), WTFMove(newData)); } -void BlobRegistryImpl::unregisterBlobURL(const URL& url, const std::optional&) +void BlobRegistryImpl::unregisterBlobURL(const URL& url, const std::optional& topOrigin) { ASSERT(isMainThread()); - if (m_blobReferences.remove(url.string())) - m_blobs.remove(url.string()); + ASSERT(BlobURL::isInternalURL(url) || topOrigin); + auto& urlKey = url.string(); + if (m_allowedBlobURLTopOrigins && topOrigin && topOrigin != m_allowedBlobURLTopOrigins->get(urlKey)) { + RELEASE_LOG_ERROR(Network, "BlobRegistryImpl::unregisterBlobURL: (%p) Rejecting unregistering blob URL with incorrect top origin.", this); + return; + } + if (!m_blobReferences.remove(urlKey)) + return; + m_blobs.remove(urlKey); + if (m_allowedBlobURLTopOrigins) + m_allowedBlobURLTopOrigins->remove(urlKey); } -BlobData* BlobRegistryImpl::getBlobDataFromURL(const URL& url, const std::optional&) const +BlobData* BlobRegistryImpl::getBlobDataFromURL(const URL& url, const std::optional& topOrigin) const { ASSERT(isMainThread()); - if (url.hasFragmentIdentifier()) - return m_blobs.get(url.viewWithoutFragmentIdentifier()); - return m_blobs.get(url.string()); + auto urlKey = url.stringWithoutFragmentIdentifier(); + auto* blobData = m_blobs.get(urlKey); + if (m_allowedBlobURLTopOrigins && topOrigin && topOrigin != m_allowedBlobURLTopOrigins->get(urlKey)) { + RELEASE_LOG_ERROR(Network, "BlobRegistryImpl::getBlobDataFromURL: (%p) Requested blob URL with incorrect top origin.", this); + return nullptr; + } + return blobData; } unsigned long long BlobRegistryImpl::blobSize(const URL& url) @@ -392,25 +417,43 @@ Vector> BlobRegistryImpl::filesInBlob(const URL& u return result; } -void BlobRegistryImpl::addBlobData(const String& url, RefPtr&& blobData, const std::optional&) +void BlobRegistryImpl::addBlobData(const String& url, RefPtr&& blobData, const std::optional& topOrigin) { + ASSERT(BlobURL::isInternalURL(URL { { }, url }) || topOrigin); auto addResult = m_blobs.set(url, WTFMove(blobData)); - if (addResult.isNewEntry) - m_blobReferences.add(url); + if (!addResult.isNewEntry) + return; + m_blobReferences.add(url); + if (m_allowedBlobURLTopOrigins && topOrigin) + m_allowedBlobURLTopOrigins->set(url, *topOrigin); } -void BlobRegistryImpl::registerBlobURLHandle(const URL& url, const std::optional&) +void BlobRegistryImpl::registerBlobURLHandle(const URL& url, const std::optional& topOrigin) { + ASSERT(BlobURL::isInternalURL(url) || topOrigin); auto urlKey = url.stringWithoutFragmentIdentifier(); - if (m_blobs.contains(urlKey)) - m_blobReferences.add(urlKey); + if (!m_blobs.contains(urlKey)) + return; + if (m_allowedBlobURLTopOrigins && topOrigin && topOrigin != m_allowedBlobURLTopOrigins->get(urlKey)) { + RELEASE_LOG_ERROR(Network, "BlobRegistryImpl::registerBlobURLHandle: (%p) Rejecting registering blob URL handle with incorrect top origin", this); + return; + } + m_blobReferences.add(urlKey); } -void BlobRegistryImpl::unregisterBlobURLHandle(const URL& url, const std::optional&) +void BlobRegistryImpl::unregisterBlobURLHandle(const URL& url, const std::optional& topOrigin) { + ASSERT(BlobURL::isInternalURL(url) || topOrigin); auto urlKey = url.stringWithoutFragmentIdentifier(); - if (m_blobReferences.remove(urlKey)) - m_blobs.remove(urlKey); + if (m_allowedBlobURLTopOrigins && topOrigin && topOrigin != m_allowedBlobURLTopOrigins->get(urlKey)) { + RELEASE_LOG_ERROR(Network, "BlobRegistryImpl::unregisterBlobURLHandle: (%p) Rejecting unregistering blob URL handle with incorrect top origin", this); + return; + } + if (!m_blobReferences.remove(urlKey)) + return; + m_blobs.remove(urlKey); + if (m_allowedBlobURLTopOrigins) + m_allowedBlobURLTopOrigins->remove(urlKey); } } // namespace WebCore diff --git a/Source/WebCore/platform/network/BlobRegistryImpl.h b/Source/WebCore/platform/network/BlobRegistryImpl.h index e0c5f995cfda0..13e33bbcd61ae 100644 --- a/Source/WebCore/platform/network/BlobRegistryImpl.h +++ b/Source/WebCore/platform/network/BlobRegistryImpl.h @@ -83,7 +83,7 @@ class WEBCORE_EXPORT BlobRegistryImpl { Vector> filesInBlob(const URL&, const std::optional& topOrigin = std::nullopt) const; void setFileDirectory(String&&); - void setPartitioningEnabled(bool enabled) { m_isPartitioningEnabled = enabled; } + void setPartitioningEnabled(bool); private: void registerBlobURLOptionallyFileBacked(const URL&, const URL& srcURL, RefPtr&&, const String& contentType, const PolicyContainer&, const std::optional& topOrigin = std::nullopt); @@ -92,8 +92,9 @@ class WEBCORE_EXPORT BlobRegistryImpl { HashCountedSet m_blobReferences; MemoryCompactRobinHoodHashMap> m_blobs; + using URLToTopOriginHashMap = MemoryCompactRobinHoodHashMap; + std::optional m_allowedBlobURLTopOrigins; String m_fileDirectory; - bool m_isPartitioningEnabled { false }; }; inline void BlobRegistryImpl::setFileDirectory(String&& filePath)