storage-access-headers.tentative.https.sub.window.js (24100B)
1 // META: script=helpers.js 2 // META: script=/resources/testdriver.js 3 // META: script=/resources/testdriver-vendor.js 4 // META: timeout=long 5 "use strict"; 6 7 // These are secure origins with different relations to the current document. 8 const https_origin = 'https://{{host}}:{{ports[https][0]}}'; 9 const same_site = 'https://{{hosts[][www]}}:{{ports[https][0]}}'; 10 const cross_site = 'https://{{hosts[alt][]}}:{{ports[https][0]}}'; 11 const alt_cross_site = 'https://{{hosts[alt][www]}}:{{ports[https][0]}}'; 12 13 const responder_script = 'embedded_responder.js'; 14 const nested_path = '/storage-access-api/resources/nested-handle-storage-access-headers.py'; 15 const retry_path = '/storage-access-api/resources/handle-headers-retry.py'; 16 const non_retry_path = '/storage-access-api/resources/handle-headers-non-retry.py'; 17 18 async function areCrossSiteCookiesAllowedByDefault() { 19 const url = `${cross_site}/storage-access-api/resources/script-with-cookie-header.py?script=${responder_script}`; 20 const frame = await CreateFrame(url); 21 return FrameHasStorageAccess(frame); 22 } 23 24 function makeURL(key, domain, path, params) { 25 const request_params = new URLSearchParams(params); 26 request_params.append('key', key); 27 return domain + path + '?' + request_params.toString(); 28 } 29 30 async function grantStorageAccessForEmbedSite(test, origin) { 31 const iframe_params = new URLSearchParams([['script', responder_script]]); 32 const iframe = await CreateFrame(origin + 33 '/storage-access-api/resources/script-with-cookie-header.py?' + 34 iframe_params.toString()); 35 test.add_cleanup( async () => { 36 await SetPermissionInFrame(iframe, 37 [{ name: 'storage-access' }, 'prompt']); 38 iframe.parentNode.removeChild(iframe); 39 }) 40 await SetPermissionInFrame(iframe, 41 [{ name: 'storage-access' }, 'granted']); 42 } 43 44 // Sends a request whose headers can be read in cross-site contexts. 45 async function sendReadableHeaderRequest(url) { 46 return fetch(url, {credentials: 'include', mode: 'no-cors'}); 47 } 48 49 // Sends a request `resources/retrieve-storage-access-headers.py` and parses 50 // the response as JSON. Will return `undefined` if no headers were set at the 51 // given key, or if the headers have already been retrieved from that key. 52 async function sendRetrieveRequest(key) { 53 const retrieval_path = '/storage-access-api/resources/retrieve-storage-access-headers.py?'; 54 const request_params = new URLSearchParams([['key', key]]); 55 const response = await fetch(retrieval_path + request_params.toString()); 56 57 return response.status !== 200 ? undefined : response.json(); 58 } 59 60 // Checks that the values of `actual_headers` match those passed in the 61 // `expected_headers` at their respective header keys. Headers with the value 62 // of `undefined` in `expected_headers` are expected to be absent from 63 // `actual_headers`. 64 function assertHeaderValuesMatch(actual_headers, expected_headers) { 65 for (const [expected_name, expected_value] of Object.entries( 66 expected_headers)) { 67 const actual_value = actual_headers[expected_name]; 68 if (expected_value === undefined) { 69 assert_equals(actual_value, undefined); 70 } else { 71 assert_array_equals(actual_value, expected_value); 72 } 73 } 74 } 75 76 function addCommonCleanupCallback(test) { 77 test.add_cleanup(async () => { 78 await test_driver.delete_all_cookies(); 79 }); 80 } 81 82 function activeKey(key) { 83 return key + 'active'; 84 } 85 86 function redirectedKey(key) { 87 return key + 'redirected'; 88 } 89 90 (async function() { 91 promise_test(async (t) => { 92 const key = '{{uuid()}}'; 93 addCommonCleanupCallback(t); 94 95 await fetch(makeURL(key, cross_site, non_retry_path), 96 {credentials: 'omit', mode: 'no-cors'}); 97 const headers = await sendRetrieveRequest(key); 98 assertHeaderValuesMatch(headers, {'sec-fetch-storage-access': undefined}); 99 }, "Sec-Fetch-Storage-Access is omitted when credentials are omitted"); 100 101 promise_test(async (t) => { 102 const key = '{{uuid()}}'; 103 addCommonCleanupCallback(t); 104 105 await grantStorageAccessForEmbedSite(t, cross_site); 106 const load_header_iframe = await CreateFrame(makeURL(key, cross_site, 107 non_retry_path, 108 [['load', ''], 109 ['script', responder_script]])); 110 assert_true(await FrameHasStorageAccess(load_header_iframe), 111 "frame should have storage access because of the `load` header"); 112 }, "Activate-Storage-Access `load` header grants storage access to frame."); 113 114 promise_test(async (t) => { 115 const key = '{{uuid()}}'; 116 addCommonCleanupCallback(t); 117 118 const iframe = await CreateFrame(makeURL(key, cross_site, 119 non_retry_path, 120 [['script', responder_script]])); 121 t.add_cleanup(async () => { 122 await SetPermissionInFrame(iframe, 123 [{ name: 'storage-access' }, 'prompt']); 124 }); 125 await SetPermissionInFrame(iframe, 126 [{ name: 'storage-access' }, 'granted']); 127 await RequestStorageAccessInFrame(iframe); 128 // Create a child iframe with the same source, that causes the server to 129 // respond with the `load` header. 130 const nested_iframe = await CreateFrameHelper((frame) => { 131 // Need a unique `key` on the request or else the server will fail it. 132 frame.src = makeURL(key + 'load', cross_site, non_retry_path, 133 [['load', ''], ['script', responder_script]]); 134 iframe.appendChild(frame); 135 }, false); 136 // The nested frame will have storage access because of the `load` response. 137 assert_true(await FrameHasStorageAccess(nested_iframe)); 138 }, "Activate-Storage-Access `load` is honored for `active` cases."); 139 140 if (await areCrossSiteCookiesAllowedByDefault()) { 141 promise_test(async (t) => { 142 const key = '{{uuid()}}'; 143 await SetFirstPartyCookie(cross_site); 144 addCommonCleanupCallback(t); 145 146 await sendReadableHeaderRequest(makeURL(key, cross_site, retry_path, 147 [['retry-allowed-origin', 148 https_origin]])); 149 150 // The server stores requests with the "active" status at a 151 // different key, so the "raw" key should be unoccupied in the 152 // stash. 153 assert_equals(await sendRetrieveRequest(key), undefined); 154 assertHeaderValuesMatch(await sendRetrieveRequest(activeKey(key)), { 155 'sec-fetch-storage-access': ['active'], 156 'origin': undefined, 157 'cookie': ['cookie=unpartitioned'], 158 }); 159 }, "Sec-Fetch-Storage-Access is `active` by default for cross-site requests."); 160 161 return; 162 } 163 164 promise_test(async (t) => { 165 const key = '{{uuid()}}'; 166 addCommonCleanupCallback(t); 167 168 await sendReadableHeaderRequest(makeURL(key, cross_site, non_retry_path)); 169 const headers = await sendRetrieveRequest(key); 170 assertHeaderValuesMatch(headers, {'sec-fetch-storage-access': ['none']}); 171 }, "Sec-Fetch-Storage-Access is `none` when unpartitioned cookies are unavailable."); 172 173 promise_test(async (t) => { 174 const key = '{{uuid()}}'; 175 176 // Create an iframe and grant it storage access permissions. 177 await grantStorageAccessForEmbedSite(t, cross_site); 178 // A cross-site request to the same site as the above iframe should have an 179 // `inactive` storage access status since a permission grant exists for the 180 // context. 181 await sendReadableHeaderRequest(makeURL(key, cross_site, non_retry_path)); 182 const headers = await sendRetrieveRequest(key); 183 // We should see the origin header on the inactive case. 184 assertHeaderValuesMatch(headers, {'sec-fetch-storage-access': ['inactive'], 185 'origin': [https_origin]}); 186 }, "Sec-Fetch-Storage-Access is `inactive` when unpartitioned cookies are available but not in use."); 187 188 promise_test(async (t) => { 189 const key = '{{uuid()}}'; 190 await SetFirstPartyCookie(cross_site); 191 addCommonCleanupCallback(t); 192 193 await grantStorageAccessForEmbedSite(t, cross_site); 194 await sendReadableHeaderRequest(makeURL(key, cross_site, retry_path, 195 [['retry-allowed-origin', 196 https_origin]])); 197 // Retrieve the pre-retry headers. 198 const headers = await sendRetrieveRequest(key); 199 // Unpartitioned cookie should not be included before the retry. 200 assertHeaderValuesMatch(headers, {'sec-fetch-storage-access': ['inactive'], 201 'origin': [https_origin], 'cookie': undefined}); 202 // Retrieve the headers for the retried request. 203 const retried_headers = await sendRetrieveRequest(activeKey(key)); 204 // The unpartitioned cookie should have been included in the retry. 205 assertHeaderValuesMatch(retried_headers, { 206 'sec-fetch-storage-access': ['active'], 207 'origin': [https_origin], 208 'cookie': ['cookie=unpartitioned'] 209 }); 210 }, "Sec-Fetch-Storage-Access is `active` after a valid retry with matching explicit allowed-origin."); 211 212 promise_test(async (t) => { 213 const key = '{{uuid()}}'; 214 await SetFirstPartyCookie(cross_site); 215 addCommonCleanupCallback(t); 216 217 await grantStorageAccessForEmbedSite(t, cross_site); 218 await sendReadableHeaderRequest(makeURL(key, cross_site, retry_path, 219 [['retry-allowed-origin','*']])); 220 // Retrieve the pre-retry headers. 221 const headers = await sendRetrieveRequest(key); 222 assertHeaderValuesMatch(headers, { 223 'sec-fetch-storage-access': ['inactive'], 224 'origin': [https_origin], 225 'cookie': undefined 226 }); 227 // Retrieve the headers for the retried request. 228 const retried_headers = await sendRetrieveRequest(activeKey(key)); 229 assertHeaderValuesMatch(retried_headers, { 230 'sec-fetch-storage-access': ['active'], 231 'origin': [https_origin], 232 'cookie': ['cookie=unpartitioned'] 233 }); 234 }, "Sec-Fetch-Storage-Access is active after retry with wildcard `allowed-origin` value."); 235 236 promise_test(async (t) => { 237 const key = '{{uuid()}}'; 238 addCommonCleanupCallback(t); 239 240 await grantStorageAccessForEmbedSite(t, cross_site); 241 await sendReadableHeaderRequest( 242 makeURL(key, cross_site, retry_path, 243 [['retry-allowed-origin', '']])); 244 245 // The server behavior when retrieving a header that was never sent is 246 // indistinguishable from its behavior when retrieving a header that was 247 // sent but was previously retrieved. To ensure the request to retrieve the 248 // post-retry header occurs only because they were never sent, always 249 // test its retrieval first. 250 const retried_headers = await sendRetrieveRequest(activeKey(key)); 251 assert_equals(retried_headers, undefined); 252 // Retrieve the pre-retry headers. 253 const headers = await sendRetrieveRequest(key); 254 assertHeaderValuesMatch(headers, {'sec-fetch-storage-access': ['inactive']}); 255 }, "'Activate-Storage-Access: retry' is a no-op on a request without an `allowed-origin` value."); 256 257 promise_test(async (t) => { 258 const key = '{{uuid()}}'; 259 addCommonCleanupCallback(t); 260 261 await grantStorageAccessForEmbedSite(t, cross_site); 262 await sendReadableHeaderRequest(makeURL(key, cross_site, retry_path, 263 [['retry-allowed-origin', 264 same_site]])); 265 // Should not be able to retrieve any headers at the post-retry key. 266 const retried_headers = await sendRetrieveRequest(activeKey(key)); 267 assert_equals(retried_headers, undefined); 268 // Retrieve the pre-retry headers. 269 const headers = await sendRetrieveRequest(key); 270 assertHeaderValuesMatch(headers, {'sec-fetch-storage-access': ['inactive']}); 271 }, "'Activate-Storage-Access: retry' is a no-op on a request from an origin that does not match its `allowed-origin` value."); 272 273 promise_test(async (t) => { 274 const key = '{{uuid()}}'; 275 addCommonCleanupCallback(t); 276 277 await sendReadableHeaderRequest(makeURL(key, cross_site, retry_path, 278 [['retry-allowed-origin', 279 https_origin]])); 280 // Should not be able to retrieve any headers at the post-retry key. 281 const retried_headers = await sendRetrieveRequest(activeKey(key)); 282 assert_equals(retried_headers, undefined); 283 // Retrieve the pre-retry headers. 284 const headers = await sendRetrieveRequest(key); 285 assertHeaderValuesMatch(headers, {'sec-fetch-storage-access': ['none'], 286 'origin': undefined}); 287 }, "Activate-Storage-Access `retry` is a no-op on a request with a `none` Storage Access status."); 288 289 promise_test(async (t) => { 290 const key = '{{uuid()}}'; 291 addCommonCleanupCallback(t); 292 293 const load_header_iframe = await CreateFrame(makeURL(key, cross_site, 294 non_retry_path, 295 [['load', ''], 296 ['script', responder_script]])); 297 assert_false(await FrameHasStorageAccess(load_header_iframe), 298 "frame should not have received storage access."); 299 }, "Activate-Storage-Access `load` header is a no-op for requests without storage access."); 300 301 promise_test(async t => { 302 const key = '{{uuid()}}'; 303 addCommonCleanupCallback(t); 304 305 const iframe_params = new URLSearchParams([['script', 306 'embedded_responder.js']]); 307 const iframe = await CreateFrame(cross_site + nested_path + '?' + 308 iframe_params.toString()); 309 310 // Create a cross-site request within the iframe 311 const nested_url_params = new URLSearchParams([['key', key]]); 312 const nested_url = https_origin + non_retry_path + '?' + 313 nested_url_params.toString(); 314 await NoCorsFetchFromFrame(iframe, nested_url); 315 316 const headers = await sendRetrieveRequest(key); 317 assertHeaderValuesMatch(headers, {'sec-fetch-storage-access': ['inactive'], 318 'origin': [cross_site]}); 319 }, "Sec-Fetch-Storage-Access is `inactive` for ABA case."); 320 321 promise_test(async t => { 322 const key = '{{uuid()}}'; 323 await SetFirstPartyCookie(https_origin); 324 addCommonCleanupCallback(t); 325 326 const iframe_params = new URLSearchParams([['script', 327 'embedded_responder.js']]); 328 const iframe = await CreateFrame(cross_site + nested_path + '?' + 329 iframe_params.toString()); 330 331 const nested_url_params = new URLSearchParams([ 332 ['key', key], 333 ['retry-allowed-origin', cross_site]]); 334 const nested_url = https_origin + retry_path + 335 '?' + nested_url_params.toString(); 336 await NoCorsFetchFromFrame(iframe, nested_url); 337 const headers = await sendRetrieveRequest(key); 338 assertHeaderValuesMatch(headers, { 339 'sec-fetch-storage-access': ['inactive'], 340 'origin': [cross_site], 341 'cookie': undefined 342 }); 343 344 // Storage access should have been activated, without the need for a grant, 345 // on the ABA case. 346 const retried_headers = await sendRetrieveRequest(activeKey(key)); 347 assertHeaderValuesMatch(retried_headers, { 348 'sec-fetch-storage-access': ['active'], 349 'origin': [cross_site], 350 'cookie': ['cookie=unpartitioned'] 351 }); 352 }, "Storage Access can be activated for ABA cases by retrying."); 353 354 promise_test(async (t) => { 355 const key = '{{uuid()}}'; 356 await SetFirstPartyCookie(cross_site); 357 addCommonCleanupCallback(t); 358 359 await grantStorageAccessForEmbedSite(t, cross_site); 360 361 // Create a redirect destination that is same origin to the initial 362 // request. 363 const redirect_url = makeURL(key, 364 cross_site, 365 retry_path, 366 [['redirected', '']]); 367 // Send a request instructing the server include the `retry` response, 368 // and then redirect when storage access has been activated. 369 await sendReadableHeaderRequest(makeURL(key, cross_site, retry_path, 370 [['retry-allowed-origin', 371 https_origin], 372 ['once-active-redirect-location', 373 redirect_url]])); 374 // Confirm the normal retry behavior. 375 const headers = await sendRetrieveRequest(key); 376 assertHeaderValuesMatch(headers, {'sec-fetch-storage-access': ['inactive'], 377 'origin': [https_origin], 'cookie': undefined}); 378 const retried_headers = await sendRetrieveRequest(activeKey(key)); 379 assertHeaderValuesMatch(retried_headers, { 380 'sec-fetch-storage-access': ['active'], 381 'origin': [https_origin], 382 'cookie': ['cookie=unpartitioned'] 383 }); 384 // Retrieve the headers for the post-retry redirect request. 385 const redirected_headers = await sendRetrieveRequest(redirectedKey(activeKey(key))); 386 assertHeaderValuesMatch(redirected_headers, { 387 'sec-fetch-storage-access': ['active'], 388 'origin': [https_origin], 389 'cookie': ['cookie=unpartitioned'] 390 }); 391 }, "Sec-Fetch-Storage-Access maintains value on same-origin redirect."); 392 393 promise_test(async (t) => { 394 const key = '{{uuid()}}'; 395 await SetFirstPartyCookie(cross_site); 396 addCommonCleanupCallback(t); 397 398 await grantStorageAccessForEmbedSite(t, cross_site); 399 400 // Create a redirect destination that is cross-origin same-site to the 401 // initial request. 402 const redirect_url = makeURL(key, alt_cross_site, retry_path, 403 [['redirected', '']]); 404 await sendReadableHeaderRequest(makeURL(key, cross_site, retry_path, 405 [['retry-allowed-origin', 406 https_origin], 407 ['once-active-redirect-location', 408 redirect_url]])); 409 410 const headers = await sendRetrieveRequest(key); 411 assertHeaderValuesMatch(headers, { 412 'sec-fetch-storage-access': ['inactive'], 413 'origin': [https_origin], 414 'cookie': undefined 415 }); 416 const retried_headers = await sendRetrieveRequest(activeKey(key)); 417 assertHeaderValuesMatch(retried_headers, { 418 'sec-fetch-storage-access': ['active'], 419 'origin': [https_origin], 420 'cookie': ['cookie=unpartitioned'] 421 }); 422 // Retrieve the headers for the post-retry redirect request. 423 const redirected_headers = await sendRetrieveRequest(redirectedKey(key)); 424 assertHeaderValuesMatch(redirected_headers, { 425 'sec-fetch-storage-access': ['inactive'], 426 'origin': ['null'], 427 'cookie': undefined 428 }); 429 }, "Sec-Fetch-Storage-Access is not 'active' after cross-origin same-site redirection."); 430 431 promise_test(async (t) => { 432 const key = '{{uuid()}}'; 433 await SetFirstPartyCookie(cross_site); 434 addCommonCleanupCallback(t); 435 await grantStorageAccessForEmbedSite(t, cross_site); 436 437 // Create a redirect destination that is cross-site to the 438 // initial request. 439 const redirect_url = makeURL(key, https_origin, retry_path, 440 [['redirected', '']]); 441 await sendReadableHeaderRequest(makeURL(key, cross_site, retry_path, 442 [['retry-allowed-origin', 443 https_origin], 444 ['once-active-redirect-location', 445 redirect_url]])); 446 447 const headers = await sendRetrieveRequest(key); 448 assertHeaderValuesMatch(headers, { 449 'sec-fetch-storage-access': ['inactive'], 450 'origin': [https_origin], 451 'cookie': undefined 452 }); 453 const retried_headers = await sendRetrieveRequest(activeKey(key)); 454 assertHeaderValuesMatch(retried_headers, { 455 'sec-fetch-storage-access': ['active'], 456 'origin': [https_origin], 457 'cookie': ['cookie=unpartitioned'] 458 }); 459 // Retrieve the headers for the post-retry redirect request. 460 const redirected_headers = await sendRetrieveRequest(redirectedKey(key)); 461 // These values will be empty because the frame is now same origin with 462 // the top level. 463 assertHeaderValuesMatch(redirected_headers, { 464 'sec-fetch-storage-access': undefined, 465 'origin': ['null'], 466 'cookie': undefined 467 }); 468 }, "Sec-Fetch-Storage-Access loses value on a cross-site redirection."); 469 470 promise_test(async (t) => { 471 const key = '{{uuid()}}'; 472 await SetFirstPartyCookie(cross_site); 473 addCommonCleanupCallback(t); 474 await grantStorageAccessForEmbedSite(t, cross_site); 475 476 // Create a redirect destination that is cross-origin same-site to the 477 // initial request. 478 const redirect_url = makeURL(key, https_origin, retry_path, [['redirected', '']]); 479 // Send a request that instructs the server to respond with both retry and 480 // response headers. 481 await sendReadableHeaderRequest(makeURL(key, cross_site, retry_path, 482 [['retry-allowed-origin', 483 https_origin], 484 ['redirect-location', 485 redirect_url]])); 486 // No redirect should have occurred, so a retrieval request for the 487 // redirect request should fail. 488 const redirected_headers = await sendRetrieveRequest(redirectedKey(key)); 489 assert_equals(redirected_headers, undefined); 490 // Confirm the normal retry behavior. 491 const headers = await sendRetrieveRequest(key); 492 assertHeaderValuesMatch(headers, {'sec-fetch-storage-access': ['inactive'], 493 'origin': [https_origin], 494 'cookie': undefined}); 495 const retried_headers = await sendRetrieveRequest(activeKey(key)); 496 assertHeaderValuesMatch(retried_headers, { 497 'sec-fetch-storage-access': ['active'], 498 'origin': [https_origin], 499 'cookie': ['cookie=unpartitioned'] 500 }); 501 }, "Activate-Storage-Access retry is handled before any redirects are followed."); 502 })();