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