tor-browser

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

permissions-policy.js (20371B)


      1 // Feature test to avoid timeouts
      2 function assert_permissions_policy_supported() {
      3  assert_true("allow" in HTMLIFrameElement.prototype, 'permissions policy is supported');
      4 }
      5 // Tests whether a feature that is enabled/disabled by permissions policy works
      6 // as expected.
      7 // Arguments:
      8 //    feature_descriptionOrObject: either and object, containing the following
      9 //        properties, or a string describing what feature is being tested.
     10 //        Examples: "usb.GetDevices()", "PaymentRequest()".
     11 //    test: test created by testharness. Examples: async_test, promise_test.
     12 //    src: URL where a feature's availability is checked. Examples:
     13 //        "/permissions-policy/resources/permissions-policy-payment.html",
     14 //        "/permissions-policy/resources/permissions-policy-usb.html".
     15 //    expect_feature_available: a callback(data, feature_description) to
     16 //        verify if a feature is available or unavailable as expected.
     17 //        The file under the path "src" defines what "data" is sent back via
     18 //        postMessage with type: 'availability-result'.
     19 //        Inside the callback, some tests (e.g., EXPECT_EQ, EXPECT_TRUE, etc)
     20 //        are run accordingly to test a feature's availability.
     21 //        Example: expect_feature_available_default(data, feature_description).
     22 //    feature_name: Optional argument, only provided when testing iframe allow
     23 //      attribute. "feature_name" is the feature name of a policy controlled
     24 //      feature (https://w3c.github.io/webappsec-permissions-policy/#features).
     25 //      See examples at:
     26 //      https://github.com/w3c/webappsec-permissions-policy/blob/main/features.md
     27 //    allowfullscreen: Optional argument, only used for testing fullscreen
     28 //      by passing "allowfullscreen".
     29 //    is_promise_test: Optional argument, true if this call should return a
     30 //    promise. Used by test_feature_availability_with_post_message_result()
     31 function test_feature_availability(
     32    feature_descriptionOrObject, test, src, expect_feature_available, feature_name,
     33    allowfullscreen, is_promise_test = false, needs_focus = false) {
     34 
     35  if (feature_descriptionOrObject && feature_descriptionOrObject instanceof Object) {
     36    const {
     37      feature_description,
     38      test,
     39      src,
     40      expect_feature_available,
     41      feature_name,
     42      allowfullscreen,
     43      is_promise_test,
     44      needs_focus,
     45    } = feature_descriptionOrObject;
     46    return test_feature_availability(
     47      feature_description,
     48      test,
     49      src,
     50      expect_feature_available,
     51      feature_name,
     52      allowfullscreen,
     53      is_promise_test,
     54      needs_focus,
     55    );
     56  }
     57 
     58  const feature_description = feature_descriptionOrObject;
     59  let frame = document.createElement('iframe');
     60  frame.src = src;
     61 
     62  if (typeof feature_name !== 'undefined') {
     63    frame.allow = frame.allow.concat(";" + feature_name);
     64  }
     65 
     66  if (typeof allowfullscreen !== 'undefined') {
     67    frame.setAttribute(allowfullscreen, true);
     68  }
     69 
     70  function expectFeatureAvailable(evt) {
     71    if (evt.source === frame.contentWindow &&
     72        evt.data.type === 'availability-result') {
     73      expect_feature_available(evt.data, feature_description);
     74      document.body.removeChild(frame);
     75      test.done();
     76    }
     77  }
     78 
     79  if (!is_promise_test) {
     80    window.addEventListener('message', test.step_func(expectFeatureAvailable));
     81    document.body.appendChild(frame);
     82    return;
     83  }
     84 
     85  const promise = new Promise((resolve) => {
     86                    window.addEventListener('message', resolve);
     87                  }).then(expectFeatureAvailable);
     88  document.body.appendChild(frame);
     89  if (needs_focus) {
     90    frame.focus();
     91  }
     92  return promise;
     93 }
     94 
     95 // Default helper functions to test a feature's availability:
     96 function expect_feature_available_default(data, feature_description) {
     97  assert_true(data.enabled, feature_description);
     98 }
     99 
    100 function expect_feature_unavailable_default(data, feature_description) {
    101  assert_false(data.enabled, feature_description);
    102 }
    103 
    104 // This is the same as test_feature_availability() but instead of passing in a
    105 // function to check the result of the message sent back from an iframe, instead
    106 // just compares the result to an expected result passed in.
    107 // Arguments:
    108 //     test: test created by testharness. Examples: async_test, promise_test.
    109 //     src: the URL to load in an iframe in which to test the feature.
    110 //     expected_result: the expected value to compare to the data passed back
    111 //         from the src page by postMessage.
    112 //     allow_attribute: Optional argument, only provided when an allow
    113 //         attribute should be specified on the iframe.
    114 function test_feature_availability_with_post_message_result(
    115    test, src, expected_result, allow_attribute) {
    116  const test_result = ({ name, message }, feature_description) => {
    117    assert_equals(name, expected_result, message + '.');
    118  };
    119  return test_feature_availability(
    120      null, test, src, test_result, allow_attribute, undefined, true);
    121 }
    122 
    123 // If this page is intended to test the named feature (according to the URL),
    124 // tests the feature availability and posts the result back to the parent.
    125 // Otherwise, does nothing.
    126 async function test_feature_in_iframe(feature_name, feature_promise_factory) {
    127  if (location.hash.endsWith(`#${feature_name}`)) {
    128    let message = 'Available';
    129    let name = '#OK';
    130    try {
    131      await feature_promise_factory();
    132    } catch (e) {
    133      ({ name, message } = e);
    134    }
    135    window.parent.postMessage(
    136      { type: 'availability-result', name, message }, '*');
    137  }
    138 }
    139 
    140 // Returns true if the URL for this page indicates that it is embedded in an
    141 // iframe.
    142 function page_loaded_in_iframe() {
    143  return new URLSearchParams(location.search).get('in-iframe');
    144 }
    145 
    146 // Returns a same-origin (relative) URL suitable for embedding in an iframe for
    147 // testing the availability of the feature.
    148 function same_origin_url(feature_name) {
    149  // Add an "in-iframe" query parameter so that we can detect the iframe'd
    150  // version of the page and testharness script loading can be disabled in
    151  // that version, as required for use of testdriver in non-toplevel browsing
    152  // contexts.
    153  return location.pathname + '?in-iframe=yes#' + feature_name;
    154 }
    155 
    156 // Returns a cross-origin (absolute) URL suitable for embedding in an iframe for
    157 // testing the availability of the feature.
    158 function cross_origin_url(base_url, feature_name) {
    159  return base_url + same_origin_url(feature_name);
    160 }
    161 
    162 // This function runs all permissions policy tests for a particular feature that
    163 // has a default policy of "self". This includes testing:
    164 // 1. Feature usage succeeds by default in the top level frame.
    165 // 2. Feature usage succeeds by default in a same-origin iframe.
    166 // 3. Feature usage fails by default in a cross-origin iframe.
    167 // 4. Feature usage succeeds when an allow attribute is specified on a
    168 //    cross-origin iframe.
    169 // 5. Feature usage fails when an allow attribute is specified on a
    170 //    same-origin iframe with a value of "feature-name 'none'".
    171 //
    172 // The same page which called this function will be loaded in the iframe in
    173 // order to test feature usage there. When this function is called in that
    174 // context it will simply run the feature and return a result back via
    175 // postMessage.
    176 //
    177 // Arguments:
    178 //     cross_origin: A cross-origin URL base to be used to load the page which
    179 //         called into this function.
    180 //     feature_name: The name of the feature as it should be specified in an
    181 //         allow attribute.
    182 //     error_name: If feature usage does not succeed, this is the string
    183 //         representation of the error that will be passed in the rejected
    184 //         promise.
    185 //     feature_promise_factory: A function which returns a promise which tests
    186 //         feature usage. If usage succeeds, the promise should resolve. If it
    187 //         fails, the promise should reject with an error that can be
    188 //         represented as a string.
    189 function run_all_fp_tests_allow_self(
    190    cross_origin, feature_name, error_name, feature_promise_factory) {
    191  // This may be the version of the page loaded up in an iframe. If so, just
    192  // post the result of running the feature promise back to the parent.
    193  if (page_loaded_in_iframe()) {
    194    test_feature_in_iframe(feature_name, feature_promise_factory);
    195    return;
    196  }
    197 
    198  // Run the various tests.
    199  // 1. Allowed in top-level frame.
    200  promise_test(
    201      () => feature_promise_factory(),
    202      'Default "' + feature_name +
    203          '" permissions policy ["self"] allows the top-level document.');
    204 
    205  // 2. Allowed in same-origin iframe.
    206  const same_origin_frame_pathname = same_origin_url(feature_name);
    207  promise_test(
    208      t => {
    209        return test_feature_availability_with_post_message_result(
    210            t, same_origin_frame_pathname, '#OK');
    211      },
    212      'Default "' + feature_name +
    213          '" permissions policy ["self"] allows same-origin iframes.');
    214 
    215  // 3. Blocked in cross-origin iframe.
    216  const cross_origin_frame_url = cross_origin_url(cross_origin, feature_name);
    217  promise_test(
    218      t => {
    219        return test_feature_availability_with_post_message_result(
    220            t, cross_origin_frame_url, error_name);
    221      },
    222      'Default "' + feature_name +
    223          '" permissions policy ["self"] disallows cross-origin iframes.');
    224 
    225  // 4. Allowed in cross-origin iframe with "allow" attribute.
    226  promise_test(
    227      t => {
    228        return test_feature_availability_with_post_message_result(
    229            t, cross_origin_frame_url, '#OK', feature_name);
    230      },
    231      'permissions policy "' + feature_name +
    232          '" can be enabled in cross-origin iframes using "allow" attribute.');
    233 
    234  // 5. Blocked in same-origin iframe with "allow" attribute set to 'none'.
    235  promise_test(
    236      t => {
    237        return test_feature_availability_with_post_message_result(
    238            t, same_origin_frame_pathname, error_name,
    239            feature_name + ' \'none\'');
    240      },
    241      'permissions policy "' + feature_name +
    242          '" can be disabled in same-origin iframes using "allow" attribute.');
    243 }
    244 
    245 // This function runs all permissions policy tests for a particular feature that
    246 // has a default policy of "*". This includes testing:
    247 // 1. Feature usage succeeds by default in the top level frame.
    248 // 2. Feature usage succeeds by default in a same-origin iframe.
    249 // 3. Feature usage succeeds by default in a cross-origin iframe.
    250 // 4. Feature usage fails when an allow attribute is specified on a
    251 //    cross-origin iframe with a value of "feature-name 'none'".
    252 // 5. Feature usage fails when an allow attribute is specified on a
    253 //    same-origin iframe with a value of "feature-name 'none'".
    254 //
    255 // The same page which called this function will be loaded in the iframe in
    256 // order to test feature usage there. When this function is called in that
    257 // context it will simply run the feature and return a result back via
    258 // postMessage.
    259 //
    260 // Arguments:
    261 //     cross_origin: A cross-origin URL base to be used to load the page which
    262 //         called into this function.
    263 //     feature_name: The name of the feature as it should be specified in an
    264 //         allow attribute.
    265 //     error_name: If feature usage does not succeed, this is the string
    266 //         representation of the error that will be passed in the rejected
    267 //         promise.
    268 //     feature_promise_factory: A function which returns a promise which tests
    269 //         feature usage. If usage succeeds, the promise should resolve. If it
    270 //         fails, the promise should reject with an error that can be
    271 //         represented as a string.
    272 function run_all_fp_tests_allow_all(
    273    cross_origin, feature_name, error_name, feature_promise_factory) {
    274  // This may be the version of the page loaded up in an iframe. If so, just
    275  // post the result of running the feature promise back to the parent.
    276  if (page_loaded_in_iframe()) {
    277    test_feature_in_iframe(feature_name, feature_promise_factory);
    278    return;
    279  }
    280 
    281  // Run the various tests.
    282  // 1. Allowed in top-level frame.
    283  promise_test(
    284      () => feature_promise_factory(),
    285      'Default "' + feature_name +
    286          '" permissions policy ["*"] allows the top-level document.');
    287 
    288  // 2. Allowed in same-origin iframe.
    289  const same_origin_frame_pathname = same_origin_url(feature_name);
    290  promise_test(
    291      t => {
    292        return test_feature_availability_with_post_message_result(
    293            t, same_origin_frame_pathname, '#OK');
    294      },
    295      'Default "' + feature_name +
    296          '" permissions policy ["*"] allows same-origin iframes.');
    297 
    298  // 3. Allowed in cross-origin iframe.
    299  const cross_origin_frame_url = cross_origin_url(cross_origin, feature_name);
    300  promise_test(
    301      t => {
    302        return test_feature_availability_with_post_message_result(
    303            t, cross_origin_frame_url, '#OK');
    304      },
    305      'Default "' + feature_name +
    306          '" permissions policy ["*"] allows cross-origin iframes.');
    307 
    308  // 4. Blocked in cross-origin iframe with "allow" attribute set to 'none'.
    309  promise_test(
    310      t => {
    311        return test_feature_availability_with_post_message_result(
    312            t, cross_origin_frame_url, error_name, feature_name + ' \'none\'');
    313      },
    314      'permissions policy "' + feature_name +
    315          '" can be disabled in cross-origin iframes using "allow" attribute.');
    316 
    317  // 5. Blocked in same-origin iframe with "allow" attribute set to 'none'.
    318  promise_test(
    319      t => {
    320        return test_feature_availability_with_post_message_result(
    321            t, same_origin_frame_pathname, error_name,
    322            feature_name + ' \'none\'');
    323      },
    324      'permissions policy "' + feature_name +
    325          '" can be disabled in same-origin iframes using "allow" attribute.');
    326 }
    327 
    328 // This function tests that a subframe's document policy allows a given feature.
    329 // A feature is allowed in a frame either through inherited policy or specified
    330 // by iframe allow attribute.
    331 // Arguments:
    332 //     test: test created by testharness. Examples: async_test, promise_test.
    333 //     feature: feature name that should be allowed in the frame.
    334 //     src: the URL to load in the frame.
    335 //     allow: the allow attribute (container policy) of the iframe
    336 function test_allowed_feature_for_subframe(message, feature, src, allow) {
    337  let frame = document.createElement('iframe');
    338  if (typeof allow !== 'undefined') {
    339    frame.allow = allow;
    340  }
    341  promise_test(function() {
    342    assert_permissions_policy_supported();
    343    frame.src = src;
    344    return new Promise(function(resolve, reject) {
    345      window.addEventListener('message', function handler(evt) {
    346        resolve(evt.data);
    347      }, { once: true });
    348      document.body.appendChild(frame);
    349    }).then(function(data) {
    350      assert_true(data.includes(feature), feature);
    351    });
    352  }, message);
    353 }
    354 
    355 // This function tests that a subframe's document policy disallows a given
    356 // feature. A feature is allowed in a frame either through inherited policy or
    357 // specified by iframe allow attribute.
    358 // Arguments:
    359 //     test: test created by testharness. Examples: async_test, promise_test.
    360 //     feature: feature name that should not be allowed in the frame.
    361 //     src: the URL to load in the frame.
    362 //     allow: the allow attribute (container policy) of the iframe
    363 function test_disallowed_feature_for_subframe(message, feature, src, allow) {
    364  let frame = document.createElement('iframe');
    365  if (typeof allow !== 'undefined') {
    366    frame.allow = allow;
    367  }
    368  promise_test(function() {
    369    assert_permissions_policy_supported();
    370    frame.src = src;
    371    return new Promise(function(resolve, reject) {
    372      window.addEventListener('message', function handler(evt) {
    373        resolve(evt.data);
    374      }, { once: true });
    375      document.body.appendChild(frame);
    376    }).then(function(data) {
    377      assert_false(data.includes(feature), feature);
    378    });
    379  }, message);
    380 }
    381 
    382 // This function tests that a subframe with header policy defined on a given
    383 // feature allows and disallows the feature as expected.
    384 // Arguments:
    385 //     feature: feature name.
    386 //     frame_header_policy: either *, self or \\(\\), defines the frame
    387 //                          document's header policy on |feature|.
    388 //                          '(' and ')' need to be escaped because of server end
    389 //                          header parameter syntax limitation.
    390 //     src: the URL to load in the frame.
    391 //     test_expects: contains 6 expected results of either |feature| is allowed
    392 //                   or not inside of a local or remote iframe nested inside
    393 //                   the subframe given the header policy to be either *,
    394 //                   self, or ().
    395 //     test_name: name of the test.
    396 function test_subframe_header_policy(
    397    feature, frame_header_policy, src, test_expects, test_name) {
    398  let frame = document.createElement('iframe');
    399  promise_test(function() {
    400    assert_permissions_policy_supported()
    401    frame.src = src + '?pipe=sub|header(Permissions-Policy,' + feature + '='
    402        + frame_header_policy + ')';
    403    return new Promise(function(resolve) {
    404      window.addEventListener('message', function handler(evt) {
    405        resolve(evt.data);
    406      });
    407      document.body.appendChild(frame);
    408    }).then(function(results) {
    409      for (var j = 0; j < results.length; j++) {
    410        var data = results[j];
    411 
    412        function test_result(message, test_expect) {
    413          if (test_expect) {
    414            assert_true(data.allowedfeatures.includes(feature), message);
    415          } else {
    416            assert_false(data.allowedfeatures.includes(feature), message);
    417          }
    418        }
    419 
    420        if (data.frame === 'local') {
    421          if (data.policy === '*') {
    422            test_result('local_all:', test_expects.local_all);
    423          }
    424          if (data.policy === 'self') {
    425            test_result('local_self:', test_expects.local_self);
    426          }
    427          if (data.policy === '\\(\\)') {
    428            test_result('local_none:', test_expects.local_none);
    429          }
    430        }
    431 
    432        if (data.frame === 'remote') {
    433          if (data.policy === '*') {
    434            test_result('remote_all:', test_expects.remote_all);
    435          }
    436          if (data.policy === 'self') {
    437            test_result('remote_self:', test_expects.remote_self);
    438          }
    439          if (data.policy === '\\(\\)') {
    440            test_result('remote_none:', test_expects.remote_none);
    441          }
    442        }
    443      }
    444    });
    445  }, test_name);
    446 }
    447 
    448 // This function tests that frame policy allows a given feature correctly. A
    449 // feature is allowed in a frame either through inherited policy or specified
    450 // by iframe allow attribute.
    451 // Arguments:
    452 //     feature: feature name.
    453 //     src: the URL to load in the frame. If undefined, the iframe will have a
    454 //         srcdoc="" attribute
    455 //     test_expect: boolean value of whether the feature should be allowed.
    456 //     allow: optional, the allow attribute (container policy) of the iframe.
    457 //     allowfullscreen: optional, boolean value of allowfullscreen attribute.
    458 //     sandbox: optional boolean. If true, the frame will be sandboxed (with
    459 //         allow-scripts, so that tests can run in it.)
    460 function test_frame_policy(
    461    feature, src, srcdoc, test_expect, allow, allowfullscreen, sandbox) {
    462  let frame = document.createElement('iframe');
    463  document.body.appendChild(frame);
    464  // frame_policy should be dynamically updated as allow and allowfullscreen is
    465  // updated.
    466  var frame_policy = frame.featurePolicy;
    467  if (typeof allow !== 'undefined') {
    468    frame.setAttribute('allow', allow);
    469  }
    470  if (!!allowfullscreen) {
    471    frame.setAttribute('allowfullscreen', true);
    472  }
    473  if (!!sandbox) {
    474    frame.setAttribute('sandbox', 'allow-scripts');
    475  }
    476  if (!!src) {
    477    frame.src = src;
    478  }
    479  if (!!srcdoc) {
    480    frame.srcdoc = "<h1>Hello world!</h1>";
    481  }
    482  if (test_expect) {
    483    assert_true(frame_policy.allowedFeatures().includes(feature));
    484  } else {
    485    assert_false(frame_policy.allowedFeatures().includes(feature));
    486  }
    487 }
    488 
    489 function expect_reports(report_count, policy_name, description) {
    490  async_test(t => {
    491    var num_received_reports = 0;
    492    new ReportingObserver(t.step_func((reports, observer) => {
    493        const relevant_reports = reports.filter(r => (r.body.featureId === policy_name));
    494        num_received_reports += relevant_reports.length;
    495        if (num_received_reports >= report_count) {
    496            t.done();
    497        }
    498   }), {types: ['permissions-policy-violation'], buffered: true}).observe();
    499  }, description);
    500 }