Skip to content

Commit

Permalink
Partition Blob Registry by the top-level main document origin
Browse files Browse the repository at this point in the history
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 <URL, SecurityOriginData> 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] w3c/FileAPI#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
  • Loading branch information
sysrqb authored and Matthew Finkel committed Aug 23, 2023
1 parent b4e7bc6 commit 7f2ea8f
Show file tree
Hide file tree
Showing 25 changed files with 787 additions and 37 deletions.
1 change: 1 addition & 0 deletions LayoutTests/TestExpectations
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]
Expand Down
Original file line number Diff line number Diff line change
@@ -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.

103 changes: 103 additions & 0 deletions LayoutTests/http/tests/local/blob/download-blob-from-iframe.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN"><!-- webkit-test-runner [ BlobRegistryTopOriginPartitioningEnabled=true ] -->
<html>
<head>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<script src="../../../../resources/js-test-pre.js"></script>
</head>
<body>
<p id="description"></p>
<div id="console"></div>
<script>
var test = async_test("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.");

if (window.testRunner)
testRunner.setShouldLogDownloadCallbacks(true);

let blobURL = "";
let timeoutId;
let handle;
let shouldDownloadSameOriginBlob = true;
let shouldDownloadSameSiteBlob = true;
let shouldDownloadCrossSiteBlob = true;
let shouldDownloadSecondCrossSiteBlob = true;
let shouldCreateCrossSiteBlob = true;

function openBlobCreatingFrame(origin)
{
debug(`Opening ${origin} as main frame with iframe origin https://localhost:8443, creating blob`);
handle = open(`${origin}/local/blob/resources/main-frame-with-iframe-downloading-blob.html`, "test-main-frame-create-blob");
assert_true(!!handle, `Opening ${origin} for blob creation failed`);
timeoutId = setTimeout(() => window.postMessage({ "status": "donefail", "message": `Opening ${origin} timed out.` }, '*'), 2000);
}

function openBlobDownloadingFrame(origin)
{
debug(`Opening ${origin} as main frame with iframe origin https://localhost:8443, downloading blob`);
handle = open(`${origin}/local/blob/resources/main-frame-with-iframe-downloading-blob.html?url=${blobURL}`, "test-main-frame-download-blob");
assert_true(!!handle, `Opening ${origin} main frame for downloading blob failed`);
timeoutId = setTimeout(() => window.postMessage({ "status": "donefail", "message": `Opening ${origin} timed out.` }, '*'), 2000);
}

window.onload = () => {
// Load main frame from localhost, iframe is loaded from localhost, blob is partitioned as https://localhost:8443 under https://localhost:8443. Blob is accessible from https://localhost:8443.
openBlobCreatingFrame(`https://localhost:8443`);
}

window.addEventListener("message", (e) => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = undefined;
handle = undefined;
} else if (handle == e.source) {
// If the timeout callback was already called, then don't handle this message, it's too late.
return;
}

if (e.data.status) {
if (e.data.status == "pass" || e.data.status == "done")
testPassed(`${e.data.message}`);
else if (e.data.status == "fail" || e.data.status == "donefail")
testFailed(`${e.data.message}`);
else
testFailed(`Unexpected status: ${e.data.status}`);
if (e.data.status == "done" || e.data.status == "donefail") {
if (shouldDownloadSameOriginBlob) {
assert_true(e.data.url && e.data.url !== "", `Blob URL is not defined in same-origin download`);
blobURL = encodeURI(e.data.url);
openBlobDownloadingFrame(`https://localhost:8443`);
shouldDownloadSameOriginBlob = false;
} else if (shouldDownloadSameSiteBlob) {
assert_true(e.data.url && e.data.url !== "", `Blob URL is not defined in same-site download`);
blobURL = encodeURI(e.data.url);
openBlobDownloadingFrame(`http://localhost:8000`);
shouldDownloadSameSiteBlob = false;
} else if (shouldDownloadCrossSiteBlob) {
assert_true(e.data.url && e.data.url !== "", `Blob URL is not defined in first cross-site download`);
blobURL = encodeURI(e.data.url);
openBlobDownloadingFrame(`http://127.0.0.1:8000`);
shouldDownloadCrossSiteBlob = false;
} else if (shouldDownloadSecondCrossSiteBlob) {
assert_true(e.data.url && e.data.url !== "", `Blob URL is not defined in second cross-site download`);
blobURL = encodeURI(e.data.url);
openBlobDownloadingFrame(`https://127.0.0.1:8443`);
shouldDownloadSecondCrossSiteBlob = false;
} else if (shouldCreateCrossSiteBlob) {
// Load main frame from localhost, iframe is loaded from localhost, blob is partitioned as https://localhost:8443 under https://127.0.0.1:8443. Blob is not accessible from https://localhost:8443.
openBlobCreatingFrame(`https://127.0.0.1:8443`);
shouldCreateCrossSiteBlob = false;
shouldDownloadSameOriginBlob = true;
shouldDownloadSameSiteBlob = true;
shouldDownloadCrossSiteBlob = true;
shouldDownloadSecondCrossSiteBlob = true;
} else {
test.done();
}
}
} else
testFailed(`Unexpected message: ${e.data}`);
});
</script>
<script src="../../../../resources/js-test-post.js"></script>
</body>
</html>
48 changes: 48 additions & 0 deletions LayoutTests/http/tests/local/blob/navigate-blob-expected.txt
Original file line number Diff line number Diff line change
@@ -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.

117 changes: 117 additions & 0 deletions LayoutTests/http/tests/local/blob/navigate-blob.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN"><!-- webkit-test-runner [ BlobRegistryTopOriginPartitioningEnabled=true BroadcastChannelOriginPartitioningEnabled=true ] -->
<html>
<head>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<script src="../../../../resources/js-test-pre.js"></script>
</head>
<body>
<p id="description"></p>
<div id="console"></div>
<script>
var test = async_test("Test for creating blob in iframe and then navigating to it from same-origin iframe and cross-origin main frames.");

let blobURL = "";
let step = 0;
let timeoutId;
let handle;

function openBlobCreatingFrame(origin, step)
{
debug(`Opening ${origin} as main frame with iframe origin https://localhost:8443 creating blob`);
handle = open(`${origin}/local/blob/resources/main-frame-with-iframe-creating-or-navigating-to-blob.html`, "test-main-frame-create-blob");
assert_true(!!handle, `Opening ${origin} main frame for blob creation in step ${step} failed`);
timeoutId = setTimeout(() => window.postMessage({ "status": "donefail", "message": `step ${step} timed out.` }, '*'), 2000);
}

function handleMessage(e, nextOrigin)
{
let shouldStep = false;
if (e.data.status) {
if (e.data.status == "pass" || e.data.status == "done")
testPassed(`${e.data.message}`);
else if (e.data.status == "fail" || e.data.status == "donefail")
testFailed(`${e.data.message}`);
else
testfailed(`Unexpected status: ${e.data.status}`);
if (e.data.status == "done" || e.data.status == "donefail") {
assert_true(e.data.url && e.data.url !== "", `Blob URL is not defined in step ${step}, status: ${e.data.status}, message: ${e.data.message}`);
blobURL = encodeURI(e.data.url);
shouldStep = true;
}
} else {
testFailed(`Unexpected message: ${e.data.message}`);
shouldStep = true;
}

if (shouldStep) {
if (handle) {
handle.close();
handle = undefined;
}
step = step + 1;
if (step == 4) {
// Load main frame from 127.0.0.1, iframe is loaded from localhost, create blob that is partitioned as https://localhost:8443 under http://127.0.0.1:8000.
openBlobCreatingFrame(nextOrigin, step);
} else if (!nextOrigin)
return;
else {
debug(`Opening ${nextOrigin} as main frame with iframe origin https://localhost:8443`);
assert_true(blobURL && blobURL !== "");
handle = open(`${nextOrigin}/local/blob/resources/main-frame-with-iframe-creating-or-navigating-to-blob.html?url=${blobURL}`, "test-main-frame");
assert_true(!!handle, `Opening ${origin} main frame for blob navigation in step ${step} failed`);
timeoutId = setTimeout(() => window.postMessage({ "status": "donefail", "message": `step ${step} timed out.`, "url": blobURL }, '*'), 2000);
}
}
}

window.onload = () => {
// Load main frame from localhost, iframe is loaded from localhost, blob is partitioned as https://localhost:8443 under https://localhost:8443. Blob is accessible from https://localhost:8443.
openBlobCreatingFrame(`https://localhost:8443`, step);
}

window.addEventListener("message", (e) => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = undefined;
handle = undefined;
} else if (handle == e.source) {
// If the timeout callback was already called, then don't handle this message, it's too late.
return;
}

if (step == 0) {
// On next step, load main frame from 127.0.0.1, iframe is loaded from localhost. Blob is not accessible from http://127.0.0.1:8000 iframe.
handleMessage(e, "http://127.0.0.1:8000");
} else if (step == 1) {
// On next step, load main frame from http://localhost, iframe is loaded from https://localhost. Blob is not accessible from http://localhost:8000 iframe.
handleMessage(e, "http://localhost:8000");
} else if (step == 2) {
// On next step, load main frame from https://localhost, iframe is loaded from https://localhost. Blob is accessible from https://localhost:8443 iframe.
handleMessage(e, "https://localhost:8443");
} else if (step == 3) {
// On next step, load main frame from http://127.0.0.1, iframe is loaded from https://localhost. Blob is not accessible from https://localhost:8443 iframe.
handleMessage(e, "http://127.0.0.1:8000");
} else if (step == 4) {
// On next step, load main frame from https://localhost, iframe is loaded from https://localhost. Blob is not accessible from https://localhost iframe.
handleMessage(e, "https://localhost:8443");
} else if (step == 5) {
// On next step, load main frame from http://localhost, iframe is loaded from https://localhost. Blob is not accessible from http://localhost:8000 iframe.
handleMessage(e, "http://localhost:8000");
} else if (step == 6) {
// On next step, load main frame from http://127.0.0.1, iframe is loaded from https://localhost. Blob is accessible from http://localhost:8000 iframe.
handleMessage(e, "http://127.0.0.1:8000");
} else if (step == 7) {
handleMessage(e);
if (step == 8 && e.data.status) {
if (e.data.status == "done" || e.data.status == "donefail")
test.done();
}
} else {
testFailed(`Unexpected step: ${step}, ${e.data.message}`);
}
});
</script>
<script src="../../../../resources/js-test-post.js"></script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<head>
<script>
if (window.opener) {
const bc = new BroadcastChannel("blob-bc");
bc.onmessage = (e) => window.opener.postMessage(e.data, '*');
}
</script>
</head>
</html>

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<body>
<script>

function download(blobURL)
{
let downloadAnchor = document.createElement("a");
downloadAnchor.href = blobURL;
downloadAnchor.download = "testBlobFileName";
document.body.appendChild(downloadAnchor);
downloadAnchor.click();
}

if (window.location.search) {
let params = new URLSearchParams(document.location.search);
let blobURL = decodeURI(params.get("url"));

download(blobURL);
window.top.postMessage({ "from": "iframe", "status": "done", "message": "iframe: downloading blob", "url": blobURL }, "*");;
} else {
const documentContent = 'Downloaded!';
let blob = new Blob([documentContent], { "type" : "text/html" });
window.top.postMessage({ "from": "iframe", "status": "done", "message": "iframe: created blob", "url": URL.createObjectURL(blob) }, "*");;
}
</script>
</body>
</html>
Loading

0 comments on commit 7f2ea8f

Please sign in to comment.