Skip to content

Commit 7e701bc

Browse files
[fetch-later] Fix quota allocation for sandbox iframes. (web-platform-tests#54777)
When the [`reserve deferred-fetch quota`][1] step for a iframe, i.e. `GetContainerDeferredFetchPolicyOnNavigation()`, is triggered, it requires a `originToNavigateTo` of the target url. If the iframe is sandboxed, its final url origin after navigation would be "null" in terms of parent's view. However, it is not avaiable at this step which is under `FrameLoader::StartNavigation()`. This CL manually provides such info using `sandbox_flags` from the owner (iframe)'s frame policy, and pass that into `GetContainerDeferredFetchPolicyOnNavigation()`. [1]: https://fetch.spec.whatwg.org/#reserve-deferred-fetch-quota Bug: 410528357 Change-Id: Ia1041e486f55e56b1cced64affd124de63a264a4 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6905117 Commit-Queue: Ming-Ying Chung <mych@chromium.org> Reviewed-by: Adam Rice <ricea@chromium.org> Reviewed-by: Nate Chapin <japhet@chromium.org> Cr-Commit-Position: refs/heads/main@{#1513441} Co-authored-by: Ming-Ying Chung <mych@chromium.org>
1 parent 7f15bba commit 7e701bc

File tree

3 files changed

+139
-0
lines changed

3 files changed

+139
-0
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// META: script=/common/get-host-info.sub.js
2+
// META: script=/common/utils.js
3+
// META: script=/fetch/fetch-later/resources/fetch-later-helper.js
4+
// META: script=/fetch/fetch-later/quota/resources/helper.js
5+
'use strict';
6+
7+
const {HTTPS_ORIGIN, HTTPS_NOTSAMESITE_ORIGIN} = get_host_info();
8+
9+
// Skips FormData & URLSearchParams, as browser adds extra bytes to them
10+
// in addition to the user-provided content. It is difficult to test a
11+
// request right at the quota limit.
12+
// Skips File & Blob as it's difficult to estimate what additional data are
13+
// added into them.
14+
const dataType = BeaconDataType.String;
15+
16+
// Request headers are counted into total request size.
17+
const headers = new Headers({'Content-Type': 'text/plain;charset=UTF-8'});
18+
19+
const requestUrl = `${HTTPS_ORIGIN}/`;
20+
const quota = getRemainingQuota(QUOTA_PER_ORIGIN, requestUrl, headers);
21+
const SMALL_REQUEST_BODY_SIZE = 4 * 1024; // 4KB, well within minimal quota.
22+
23+
// This test validates the correct behavior for a sandboxed iframe without the
24+
// 'allow-same-origin' token.
25+
//
26+
// Such an iframe should be treated as cross-origin, even if its `src` attribute
27+
// points to a same-origin URL. Therefore, it should be allocated its own
28+
// separate "minimal quota" (8KB) for fetchLater() requests and should NOT share
29+
// the parent document's primary quota pool.
30+
//
31+
// The test works by first completely exhausting the parent document's quota.
32+
// Then, it creates the sandboxed iframe and attempts to send a small request
33+
// from it.
34+
//
35+
// The expected result is that the iframe's request SUCCEEDS, because it should
36+
// have its own independent 8KB quota to use.
37+
//
38+
// NOTE: This test will FAIL until the underlying bug is fixed. The bug causes
39+
// the sandboxed iframe to be incorrectly treated as same-origin, making it try
40+
// to use the parent's already-exhausted quota, which leads to a premature
41+
// QuotaExceededError.
42+
promise_test(async test => {
43+
const controller = new AbortController();
44+
test.add_cleanup(() => controller.abort());
45+
46+
// Step 1: Exhaust the parent frame's entire fetchLater() quota.
47+
fetchLater(requestUrl, {
48+
method: 'POST',
49+
signal: controller.signal,
50+
body: makeBeaconData(generatePayload(quota), dataType),
51+
referrer: '', // Referrer is part of the quota, so we control it.
52+
});
53+
54+
// Step 2: Create a sandboxed iframe and attempt a small fetchLater()
55+
// call from it. This should succeed as it should have its own 8KB quota.
56+
await loadFetchLaterIframe(
57+
HTTPS_ORIGIN, // The iframe's src is same-origin.
58+
{
59+
targetUrl: requestUrl,
60+
activateAfter: 0,
61+
method: 'POST',
62+
bodyType: dataType,
63+
bodySize: SMALL_REQUEST_BODY_SIZE,
64+
referrer: '',
65+
sandbox: 'allow-scripts', // Sandboxed, but NOT allow-same-origin.
66+
});
67+
}, `A sandboxed iframe (without allow-same-origin) should be treated as cross-origin and have its own minimal quota.`);
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// META: script=/common/get-host-info.sub.js
2+
// META: script=/common/utils.js
3+
// META: script=/fetch/fetch-later/resources/fetch-later-helper.js
4+
// META: script=/fetch/fetch-later/quota/resources/helper.js
5+
'use strict';
6+
7+
const {HTTPS_ORIGIN} = get_host_info();
8+
9+
// Skips FormData & URLSearchParams, as browser adds extra bytes to them
10+
// in addition to the user-provided content. It is difficult to test a
11+
// request right at the quota limit.
12+
// Skips File & Blob as it's difficult to estimate what additional data are
13+
// added into them.
14+
const dataType = BeaconDataType.String;
15+
16+
// Request headers are counted into total request size.
17+
const headers = new Headers({'Content-Type': 'text/plain;charset=UTF-8'});
18+
19+
const requestUrl = `${HTTPS_ORIGIN}/`;
20+
const quota = getRemainingQuota(QUOTA_PER_ORIGIN, requestUrl, headers);
21+
const SMALL_REQUEST_BODY_SIZE = 4 * 1024; // 4KB.
22+
23+
// This test validates the correct behavior for a sandboxed iframe that includes
24+
// the 'allow-same-origin' token.
25+
//
26+
// Such an iframe should be treated as same-origin. Therefore, it should share
27+
// the parent document's primary 64KB quota pool for fetchLater() requests.
28+
//
29+
// The test works by first having the parent document consume its entire quota.
30+
// Then, it creates the 'allow-same-origin' sandboxed iframe and attempts to
31+
// send a small request.
32+
//
33+
// The expected result is that the iframe's request is REJECTED with a
34+
// QuotaExceededError, proving that it is correctly sharing the parent's
35+
// (already exhausted) quota.
36+
promise_test(async test => {
37+
const controller = new AbortController();
38+
test.add_cleanup(() => controller.abort());
39+
40+
// Step 1: Exhaust the parent frame's entire fetchLater() quota.
41+
fetchLater(requestUrl, {
42+
method: 'POST',
43+
signal: controller.signal,
44+
body: makeBeaconData(generatePayload(quota), dataType),
45+
referrer: '', // Referrer is part of the quota, so we control it.
46+
});
47+
48+
// Step 2: From a sandboxed 'allow-same-origin' iframe, attempt to send a
49+
// small request. This should fail as the shared quota is already gone.
50+
await loadFetchLaterIframe(
51+
HTTPS_ORIGIN, // The iframe's src is same-origin.
52+
{
53+
targetUrl: requestUrl,
54+
activateAfter: 0,
55+
method: 'POST',
56+
bodyType: dataType,
57+
bodySize: SMALL_REQUEST_BODY_SIZE,
58+
referrer: '',
59+
sandbox: 'allow-scripts allow-same-origin',
60+
expect: new FetchLaterIframeExpectation(
61+
FetchLaterExpectationType.ERROR_DOM, 'QuotaExceededError'),
62+
});
63+
}, `A sandboxed iframe with 'allow-same-origin' should be treated as same-origin and share the parent's quota.`);

fetch/fetch-later/resources/fetch-later-helper.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,11 @@ class FetchLaterIframeOptions {
305305
*/
306306
this.allowDeferredFetch;
307307

308+
/**
309+
* @type {string=} The sandbox attribute to apply to the iframe.
310+
*/
311+
this.sandbox;
312+
308313
/**
309314
* @type {FetchLaterIframeExpectation=} The expectation on the iframe's
310315
* behavior.
@@ -460,6 +465,7 @@ async function loadFetchLaterIframe(origin, {
460465
bodyType = undefined,
461466
bodySize = undefined,
462467
allowDeferredFetch = false,
468+
sandbox = undefined,
463469
expect = undefined
464470
} = {}) {
465471
if (uuid && targetUrl && !targetUrl.includes(uuid)) {
@@ -489,6 +495,9 @@ async function loadFetchLaterIframe(origin, {
489495
if (allowDeferredFetch) {
490496
iframe.allow = 'deferred-fetch';
491497
}
498+
if (sandbox) {
499+
iframe.sandbox = sandbox;
500+
}
492501
iframe.src = url;
493502

494503
const messageReceived = new Promise((resolve, reject) => {

0 commit comments

Comments
 (0)