tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 })();