summaryrefslogtreecommitdiffstats
path: root/dom/security
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/security/CSPEvalChecker.cpp190
-rw-r--r--dom/security/CSPEvalChecker.h32
-rw-r--r--dom/security/DOMSecurityMonitor.cpp136
-rw-r--r--dom/security/DOMSecurityMonitor.h43
-rw-r--r--dom/security/FramingChecker.cpp237
-rw-r--r--dom/security/FramingChecker.h51
-rw-r--r--dom/security/PolicyTokenizer.cpp70
-rw-r--r--dom/security/PolicyTokenizer.h78
-rw-r--r--dom/security/ReferrerInfo.cpp1729
-rw-r--r--dom/security/ReferrerInfo.h470
-rw-r--r--dom/security/SRICheck.cpp511
-rw-r--r--dom/security/SRICheck.h102
-rw-r--r--dom/security/SRILogHelper.h24
-rw-r--r--dom/security/SRIMetadata.cpp187
-rw-r--r--dom/security/SRIMetadata.h90
-rw-r--r--dom/security/SecFetch.cpp411
-rw-r--r--dom/security/SecFetch.h27
-rw-r--r--dom/security/featurepolicy/Feature.cpp76
-rw-r--r--dom/security/featurepolicy/Feature.h65
-rw-r--r--dom/security/featurepolicy/FeaturePolicy.cpp334
-rw-r--r--dom/security/featurepolicy/FeaturePolicy.h204
-rw-r--r--dom/security/featurepolicy/FeaturePolicyParser.cpp157
-rw-r--r--dom/security/featurepolicy/FeaturePolicyParser.h30
-rw-r--r--dom/security/featurepolicy/FeaturePolicyUtils.cpp315
-rw-r--r--dom/security/featurepolicy/FeaturePolicyUtils.h91
-rw-r--r--dom/security/featurepolicy/fuzztest/fp_fuzzer.cpp67
-rw-r--r--dom/security/featurepolicy/fuzztest/fp_fuzzer.dict54
-rw-r--r--dom/security/featurepolicy/fuzztest/moz.build18
-rw-r--r--dom/security/featurepolicy/moz.build36
-rw-r--r--dom/security/featurepolicy/test/gtest/TestFeaturePolicyParser.cpp162
-rw-r--r--dom/security/featurepolicy/test/gtest/moz.build13
-rw-r--r--dom/security/featurepolicy/test/mochitest/empty.html1
-rw-r--r--dom/security/featurepolicy/test/mochitest/mochitest.toml14
-rw-r--r--dom/security/featurepolicy/test/mochitest/test_featureList.html48
-rw-r--r--dom/security/featurepolicy/test/mochitest/test_parser.html418
-rw-r--r--dom/security/featurepolicy/test/mochitest/test_parser.html^headers^1
-rw-r--r--dom/security/fuzztest/csp_fuzzer.cpp41
-rw-r--r--dom/security/fuzztest/csp_fuzzer.dict95
-rw-r--r--dom/security/fuzztest/moz.build18
-rw-r--r--dom/security/moz.build82
-rw-r--r--dom/security/nsCSPContext.cpp1974
-rw-r--r--dom/security/nsCSPContext.h240
-rw-r--r--dom/security/nsCSPParser.cpp1239
-rw-r--r--dom/security/nsCSPParser.h217
-rw-r--r--dom/security/nsCSPService.cpp374
-rw-r--r--dom/security/nsCSPService.h46
-rw-r--r--dom/security/nsCSPUtils.cpp1777
-rw-r--r--dom/security/nsCSPUtils.h676
-rw-r--r--dom/security/nsContentSecurityManager.cpp1715
-rw-r--r--dom/security/nsContentSecurityManager.h94
-rw-r--r--dom/security/nsContentSecurityUtils.cpp1719
-rw-r--r--dom/security/nsContentSecurityUtils.h100
-rw-r--r--dom/security/nsHTTPSOnlyStreamListener.cpp278
-rw-r--r--dom/security/nsHTTPSOnlyStreamListener.h50
-rw-r--r--dom/security/nsHTTPSOnlyUtils.cpp1071
-rw-r--r--dom/security/nsHTTPSOnlyUtils.h247
-rw-r--r--dom/security/nsIHttpsOnlyModePermission.idl26
-rw-r--r--dom/security/nsMixedContentBlocker.cpp1065
-rw-r--r--dom/security/nsMixedContentBlocker.h100
-rw-r--r--dom/security/sanitizer/Sanitizer.cpp187
-rw-r--r--dom/security/sanitizer/Sanitizer.h107
-rw-r--r--dom/security/sanitizer/moz.build37
-rw-r--r--dom/security/sanitizer/tests/mochitest/mochitest.toml8
-rw-r--r--dom/security/sanitizer/tests/mochitest/test_sanitizer_api.html138
-rw-r--r--dom/security/test/cors/browser.toml10
-rw-r--r--dom/security/test/cors/browser_CORS-console-warnings.js101
-rw-r--r--dom/security/test/cors/bug1456721.sjs20
-rw-r--r--dom/security/test/cors/file_CrossSiteXHR_cache_server.sjs59
-rw-r--r--dom/security/test/cors/file_CrossSiteXHR_inner.html121
-rw-r--r--dom/security/test/cors/file_CrossSiteXHR_inner.jarbin0 -> 1105 bytes
-rw-r--r--dom/security/test/cors/file_CrossSiteXHR_inner_data.sjs103
-rw-r--r--dom/security/test/cors/file_CrossSiteXHR_server.sjs230
-rw-r--r--dom/security/test/cors/file_bug1456721.html74
-rw-r--r--dom/security/test/cors/file_cors_logging_test.html1311
-rw-r--r--dom/security/test/cors/file_cors_logging_test.html.css0
-rw-r--r--dom/security/test/cors/mochitest.toml26
-rw-r--r--dom/security/test/cors/test_CrossSiteXHR.html1549
-rw-r--r--dom/security/test/cors/test_CrossSiteXHR_cache.html610
-rw-r--r--dom/security/test/cors/test_CrossSiteXHR_origin.html180
-rw-r--r--dom/security/test/crashtests/1577572.html10
-rw-r--r--dom/security/test/crashtests/1583044.html11
-rw-r--r--dom/security/test/crashtests/crashtests.list2
-rw-r--r--dom/security/test/csp/Ahem.ttfbin0 -> 12480 bytes
-rw-r--r--dom/security/test/csp/File0
-rw-r--r--dom/security/test/csp/browser.toml30
-rw-r--r--dom/security/test/csp/browser_manifest-src-override-default-src.js126
-rw-r--r--dom/security/test/csp/browser_pdfjs_not_subject_to_csp.js45
-rw-r--r--dom/security/test/csp/browser_test_bookmarklets.js82
-rw-r--r--dom/security/test/csp/browser_test_uir_optional_clicks.js36
-rw-r--r--dom/security/test/csp/browser_test_web_manifest.js239
-rw-r--r--dom/security/test/csp/browser_test_web_manifest_mixed_content.js57
-rw-r--r--dom/security/test/csp/dummy.pdfbin0 -> 150611 bytes
-rw-r--r--dom/security/test/csp/file_CSP.css20
-rw-r--r--dom/security/test/csp/file_CSP.sjs24
-rw-r--r--dom/security/test/csp/file_allow_https_schemes.html14
-rw-r--r--dom/security/test/csp/file_base_uri_server.sjs58
-rw-r--r--dom/security/test/csp/file_blob_data_schemes.html49
-rw-r--r--dom/security/test/csp/file_blob_top_nav_block_modals.html18
-rw-r--r--dom/security/test/csp/file_blob_top_nav_block_modals.html^headers^1
-rw-r--r--dom/security/test/csp/file_blob_uri_blocks_modals.html27
-rw-r--r--dom/security/test/csp/file_blob_uri_blocks_modals.html^headers^1
-rw-r--r--dom/security/test/csp/file_block_all_mcb.sjs78
-rw-r--r--dom/security/test/csp/file_block_all_mixed_content_frame_navigation1.html19
-rw-r--r--dom/security/test/csp/file_block_all_mixed_content_frame_navigation2.html15
-rw-r--r--dom/security/test/csp/file_blocked_uri_in_violation_event_after_redirects.html39
-rw-r--r--dom/security/test/csp/file_blocked_uri_in_violation_event_after_redirects.sjs51
-rw-r--r--dom/security/test/csp/file_blocked_uri_redirect_frame_src.html10
-rw-r--r--dom/security/test/csp/file_blocked_uri_redirect_frame_src.html^headers^1
-rw-r--r--dom/security/test/csp/file_blocked_uri_redirect_frame_src_server.sjs13
-rw-r--r--dom/security/test/csp/file_bug1229639.html7
-rw-r--r--dom/security/test/csp/file_bug1229639.html^headers^1
-rw-r--r--dom/security/test/csp/file_bug1312272.html13
-rw-r--r--dom/security/test/csp/file_bug1312272.html^headers^1
-rw-r--r--dom/security/test/csp/file_bug1312272.js8
-rw-r--r--dom/security/test/csp/file_bug1452037.html9
-rw-r--r--dom/security/test/csp/file_bug1505412.sjs34
-rw-r--r--dom/security/test/csp/file_bug1505412_frame.html14
-rw-r--r--dom/security/test/csp/file_bug1505412_frame.html^headers^1
-rw-r--r--dom/security/test/csp/file_bug1505412_reporter.sjs18
-rw-r--r--dom/security/test/csp/file_bug1738418_child.html11
-rw-r--r--dom/security/test/csp/file_bug1738418_parent.html11
-rw-r--r--dom/security/test/csp/file_bug1738418_parent.html^headers^1
-rw-r--r--dom/security/test/csp/file_bug1764343.html11
-rw-r--r--dom/security/test/csp/file_bug1777572.html43
-rw-r--r--dom/security/test/csp/file_bug663567.xsl27
-rw-r--r--dom/security/test/csp/file_bug663567_allows.xml28
-rw-r--r--dom/security/test/csp/file_bug663567_allows.xml^headers^1
-rw-r--r--dom/security/test/csp/file_bug663567_blocks.xml28
-rw-r--r--dom/security/test/csp/file_bug663567_blocks.xml^headers^1
-rw-r--r--dom/security/test/csp/file_bug802872.html12
-rw-r--r--dom/security/test/csp/file_bug802872.html^headers^1
-rw-r--r--dom/security/test/csp/file_bug802872.js47
-rw-r--r--dom/security/test/csp/file_bug802872.sjs6
-rw-r--r--dom/security/test/csp/file_bug836922_npolicies.html12
-rw-r--r--dom/security/test/csp/file_bug836922_npolicies.html^headers^2
-rw-r--r--dom/security/test/csp/file_bug836922_npolicies_ro_violation.sjs53
-rw-r--r--dom/security/test/csp/file_bug836922_npolicies_violation.sjs64
-rw-r--r--dom/security/test/csp/file_bug885433_allows.html39
-rw-r--r--dom/security/test/csp/file_bug885433_allows.html^headers^1
-rw-r--r--dom/security/test/csp/file_bug885433_blocks.html38
-rw-r--r--dom/security/test/csp/file_bug885433_blocks.html^headers^1
-rw-r--r--dom/security/test/csp/file_bug886164.html15
-rw-r--r--dom/security/test/csp/file_bug886164.html^headers^1
-rw-r--r--dom/security/test/csp/file_bug886164_2.html14
-rw-r--r--dom/security/test/csp/file_bug886164_2.html^headers^1
-rw-r--r--dom/security/test/csp/file_bug886164_3.html12
-rw-r--r--dom/security/test/csp/file_bug886164_3.html^headers^1
-rw-r--r--dom/security/test/csp/file_bug886164_4.html12
-rw-r--r--dom/security/test/csp/file_bug886164_4.html^headers^1
-rw-r--r--dom/security/test/csp/file_bug886164_5.html26
-rw-r--r--dom/security/test/csp/file_bug886164_5.html^headers^1
-rw-r--r--dom/security/test/csp/file_bug886164_6.html35
-rw-r--r--dom/security/test/csp/file_bug886164_6.html^headers^1
-rw-r--r--dom/security/test/csp/file_bug888172.html29
-rw-r--r--dom/security/test/csp/file_bug888172.sjs49
-rw-r--r--dom/security/test/csp/file_bug909029_none.html20
-rw-r--r--dom/security/test/csp/file_bug909029_none.html^headers^1
-rw-r--r--dom/security/test/csp/file_bug909029_star.html19
-rw-r--r--dom/security/test/csp/file_bug909029_star.html^headers^1
-rw-r--r--dom/security/test/csp/file_bug910139.sjs56
-rw-r--r--dom/security/test/csp/file_bug910139.xml28
-rw-r--r--dom/security/test/csp/file_bug910139.xsl27
-rw-r--r--dom/security/test/csp/file_bug941404.html27
-rw-r--r--dom/security/test/csp/file_bug941404_xhr.html5
-rw-r--r--dom/security/test/csp/file_bug941404_xhr.html^headers^1
-rw-r--r--dom/security/test/csp/file_child-src_iframe.html61
-rw-r--r--dom/security/test/csp/file_child-src_inner_frame.html21
-rw-r--r--dom/security/test/csp/file_child-src_service_worker.html30
-rw-r--r--dom/security/test/csp/file_child-src_service_worker.js3
-rw-r--r--dom/security/test/csp/file_child-src_shared_worker-redirect.html47
-rw-r--r--dom/security/test/csp/file_child-src_shared_worker.html35
-rw-r--r--dom/security/test/csp/file_child-src_shared_worker.js8
-rw-r--r--dom/security/test/csp/file_child-src_shared_worker_data.html37
-rw-r--r--dom/security/test/csp/file_child-src_worker-redirect.html47
-rw-r--r--dom/security/test/csp/file_child-src_worker.html34
-rw-r--r--dom/security/test/csp/file_child-src_worker.js3
-rw-r--r--dom/security/test/csp/file_child-src_worker_data.html33
-rw-r--r--dom/security/test/csp/file_connect-src-fetch.html16
-rw-r--r--dom/security/test/csp/file_connect-src.html21
-rw-r--r--dom/security/test/csp/file_csp_frame_ancestors_about_blank.html9
-rw-r--r--dom/security/test/csp/file_csp_frame_ancestors_about_blank.html^headers^2
-rw-r--r--dom/security/test/csp/file_csp_meta_uir.html13
-rw-r--r--dom/security/test/csp/file_data-uri_blocked.html15
-rw-r--r--dom/security/test/csp/file_data-uri_blocked.html^headers^1
-rw-r--r--dom/security/test/csp/file_data_csp_inheritance.html24
-rw-r--r--dom/security/test/csp/file_data_csp_merge.html26
-rw-r--r--dom/security/test/csp/file_data_doc_ignore_meta_csp.html22
-rw-r--r--dom/security/test/csp/file_doccomment_meta.html28
-rw-r--r--dom/security/test/csp/file_docwrite_meta.css3
-rw-r--r--dom/security/test/csp/file_docwrite_meta.html26
-rw-r--r--dom/security/test/csp/file_docwrite_meta.js3
-rw-r--r--dom/security/test/csp/file_dual_header_testserver.sjs45
-rw-r--r--dom/security/test/csp/file_dummy_pixel.pngbin0 -> 70 bytes
-rw-r--r--dom/security/test/csp/file_empty_directive.html11
-rw-r--r--dom/security/test/csp/file_empty_directive.html^headers^1
-rw-r--r--dom/security/test/csp/file_evalscript_main.html12
-rw-r--r--dom/security/test/csp/file_evalscript_main.html^headers^2
-rw-r--r--dom/security/test/csp/file_evalscript_main.js243
-rw-r--r--dom/security/test/csp/file_evalscript_main_allowed.html12
-rw-r--r--dom/security/test/csp/file_evalscript_main_allowed.html^headers^2
-rw-r--r--dom/security/test/csp/file_evalscript_main_allowed.js195
-rw-r--r--dom/security/test/csp/file_fontloader.sjs57
-rw-r--r--dom/security/test/csp/file_fontloader.woffbin0 -> 11140 bytes
-rw-r--r--dom/security/test/csp/file_form-action.html15
-rw-r--r--dom/security/test/csp/file_form_action_server.sjs32
-rw-r--r--dom/security/test/csp/file_frame_ancestors_ro.html1
-rw-r--r--dom/security/test/csp/file_frame_ancestors_ro.html^headers^1
-rw-r--r--dom/security/test/csp/file_frame_src.js20
-rw-r--r--dom/security/test/csp/file_frame_src_child_governs.html10
-rw-r--r--dom/security/test/csp/file_frame_src_frame_governs.html10
-rw-r--r--dom/security/test/csp/file_frame_src_inner.html5
-rw-r--r--dom/security/test/csp/file_frameancestors.sjs69
-rw-r--r--dom/security/test/csp/file_frameancestors_main.html44
-rw-r--r--dom/security/test/csp/file_frameancestors_main.js134
-rw-r--r--dom/security/test/csp/file_frameancestors_userpass.html10
-rw-r--r--dom/security/test/csp/file_frameancestors_userpass_frame_a.html12
-rw-r--r--dom/security/test/csp/file_frameancestors_userpass_frame_b.html12
-rw-r--r--dom/security/test/csp/file_frameancestors_userpass_frame_c.html8
-rw-r--r--dom/security/test/csp/file_frameancestors_userpass_frame_c.html^headers^1
-rw-r--r--dom/security/test/csp/file_frameancestors_userpass_frame_d.html8
-rw-r--r--dom/security/test/csp/file_frameancestors_userpass_frame_d.html^headers^1
-rw-r--r--dom/security/test/csp/file_hash_source.html65
-rw-r--r--dom/security/test/csp/file_hash_source.html^headers^2
-rw-r--r--dom/security/test/csp/file_iframe_parent_location_js.html10
-rw-r--r--dom/security/test/csp/file_iframe_sandbox_document_write.html21
-rw-r--r--dom/security/test/csp/file_iframe_sandbox_srcdoc.html11
-rw-r--r--dom/security/test/csp/file_iframe_sandbox_srcdoc.html^headers^1
-rw-r--r--dom/security/test/csp/file_iframe_srcdoc.sjs86
-rw-r--r--dom/security/test/csp/file_ignore_unsafe_inline.html26
-rw-r--r--dom/security/test/csp/file_ignore_unsafe_inline_multiple_policies_server.sjs58
-rw-r--r--dom/security/test/csp/file_ignore_xfo.html10
-rw-r--r--dom/security/test/csp/file_ignore_xfo.html^headers^3
-rw-r--r--dom/security/test/csp/file_image_document_pixel.pngbin0 -> 70 bytes
-rw-r--r--dom/security/test/csp/file_image_document_pixel.png^headers^2
-rw-r--r--dom/security/test/csp/file_image_nonce.html39
-rw-r--r--dom/security/test/csp/file_image_nonce.html^headers^2
-rw-r--r--dom/security/test/csp/file_independent_iframe_csp.html43
-rw-r--r--dom/security/test/csp/file_inlinescript.html15
-rw-r--r--dom/security/test/csp/file_inlinestyle_main.html79
-rw-r--r--dom/security/test/csp/file_inlinestyle_main.html^headers^2
-rw-r--r--dom/security/test/csp/file_inlinestyle_main_allowed.html84
-rw-r--r--dom/security/test/csp/file_inlinestyle_main_allowed.html^headers^2
-rw-r--r--dom/security/test/csp/file_invalid_source_expression.html11
-rw-r--r--dom/security/test/csp/file_leading_wildcard.html11
-rw-r--r--dom/security/test/csp/file_link_rel_preload.html19
-rw-r--r--dom/security/test/csp/file_main.html55
-rw-r--r--dom/security/test/csp/file_main.html^headers^1
-rw-r--r--dom/security/test/csp/file_main.js26
-rw-r--r--dom/security/test/csp/file_meta_element.html27
-rw-r--r--dom/security/test/csp/file_meta_header_dual.sjs101
-rw-r--r--dom/security/test/csp/file_meta_whitespace_skipping.html31
-rw-r--r--dom/security/test/csp/file_multi_policy_injection_bypass.html15
-rw-r--r--dom/security/test/csp/file_multi_policy_injection_bypass.html^headers^1
-rw-r--r--dom/security/test/csp/file_multi_policy_injection_bypass_2.html15
-rw-r--r--dom/security/test/csp/file_multi_policy_injection_bypass_2.html^headers^1
-rw-r--r--dom/security/test/csp/file_multipart_testserver.sjs160
-rw-r--r--dom/security/test/csp/file_no_log_ignore_xfo.html10
-rw-r--r--dom/security/test/csp/file_no_log_ignore_xfo.html^headers^2
-rw-r--r--dom/security/test/csp/file_nonce_redirector.sjs28
-rw-r--r--dom/security/test/csp/file_nonce_redirects.html23
-rw-r--r--dom/security/test/csp/file_nonce_snapshot.sjs54
-rw-r--r--dom/security/test/csp/file_nonce_source.html73
-rw-r--r--dom/security/test/csp/file_nonce_source.html^headers^2
-rw-r--r--dom/security/test/csp/file_null_baseuri.html21
-rw-r--r--dom/security/test/csp/file_object_inherit.html21
-rw-r--r--dom/security/test/csp/file_parent_location_js.html18
-rw-r--r--dom/security/test/csp/file_path_matching.html10
-rw-r--r--dom/security/test/csp/file_path_matching.js1
-rw-r--r--dom/security/test/csp/file_path_matching_incl_query.html10
-rw-r--r--dom/security/test/csp/file_path_matching_redirect.html10
-rw-r--r--dom/security/test/csp/file_path_matching_redirect_server.sjs12
-rw-r--r--dom/security/test/csp/file_pdfjs_not_subject_to_csp.html21
-rw-r--r--dom/security/test/csp/file_ping.html19
-rw-r--r--dom/security/test/csp/file_policyuri_regression_from_multipolicy.html9
-rw-r--r--dom/security/test/csp/file_policyuri_regression_from_multipolicy.html^headers^1
-rw-r--r--dom/security/test/csp/file_policyuri_regression_from_multipolicy_policy1
-rw-r--r--dom/security/test/csp/file_punycode_host_src.js2
-rw-r--r--dom/security/test/csp/file_punycode_host_src.sjs46
-rw-r--r--dom/security/test/csp/file_redirect_content.sjs39
-rw-r--r--dom/security/test/csp/file_redirect_report.sjs16
-rw-r--r--dom/security/test/csp/file_redirect_worker.sjs34
-rw-r--r--dom/security/test/csp/file_redirects_main.html37
-rw-r--r--dom/security/test/csp/file_redirects_page.sjs140
-rw-r--r--dom/security/test/csp/file_redirects_resource.sjs171
-rw-r--r--dom/security/test/csp/file_report.html13
-rw-r--r--dom/security/test/csp/file_report_chromescript.js68
-rw-r--r--dom/security/test/csp/file_report_font_cache-1.html26
-rw-r--r--dom/security/test/csp/file_report_font_cache-2.html25
-rw-r--r--dom/security/test/csp/file_report_font_cache-2.html^headers^1
-rw-r--r--dom/security/test/csp/file_report_for_import.css1
-rw-r--r--dom/security/test/csp/file_report_for_import.html10
-rw-r--r--dom/security/test/csp/file_report_for_import_server.sjs50
-rw-r--r--dom/security/test/csp/file_report_uri_missing_in_report_only_header.html0
-rw-r--r--dom/security/test/csp/file_report_uri_missing_in_report_only_header.html^headers^1
-rw-r--r--dom/security/test/csp/file_ro_ignore_xfo.html10
-rw-r--r--dom/security/test/csp/file_ro_ignore_xfo.html^headers^3
-rw-r--r--dom/security/test/csp/file_sandbox_1.html16
-rw-r--r--dom/security/test/csp/file_sandbox_10.html12
-rw-r--r--dom/security/test/csp/file_sandbox_11.html25
-rw-r--r--dom/security/test/csp/file_sandbox_12.html40
-rw-r--r--dom/security/test/csp/file_sandbox_13.html25
-rw-r--r--dom/security/test/csp/file_sandbox_2.html16
-rw-r--r--dom/security/test/csp/file_sandbox_3.html13
-rw-r--r--dom/security/test/csp/file_sandbox_4.html13
-rw-r--r--dom/security/test/csp/file_sandbox_5.html26
-rw-r--r--dom/security/test/csp/file_sandbox_6.html35
-rw-r--r--dom/security/test/csp/file_sandbox_7.html15
-rw-r--r--dom/security/test/csp/file_sandbox_8.html15
-rw-r--r--dom/security/test/csp/file_sandbox_9.html12
-rw-r--r--dom/security/test/csp/file_sandbox_allow_scripts.html12
-rw-r--r--dom/security/test/csp/file_sandbox_allow_scripts.html^headers^1
-rw-r--r--dom/security/test/csp/file_sandbox_fail.js7
-rw-r--r--dom/security/test/csp/file_sandbox_pass.js7
-rw-r--r--dom/security/test/csp/file_scheme_relative_sources.js1
-rw-r--r--dom/security/test/csp/file_scheme_relative_sources.sjs44
-rw-r--r--dom/security/test/csp/file_script_template.html16
-rw-r--r--dom/security/test/csp/file_script_template.js1
-rw-r--r--dom/security/test/csp/file_self_none_as_hostname_confusion.html11
-rw-r--r--dom/security/test/csp/file_self_none_as_hostname_confusion.html^headers^1
-rw-r--r--dom/security/test/csp/file_sendbeacon.html21
-rw-r--r--dom/security/test/csp/file_service_worker.html21
-rw-r--r--dom/security/test/csp/file_service_worker.js1
-rw-r--r--dom/security/test/csp/file_spawn_service_worker.js1
-rw-r--r--dom/security/test/csp/file_spawn_shared_worker.js7
-rw-r--r--dom/security/test/csp/file_spawn_worker.js1
-rw-r--r--dom/security/test/csp/file_strict_dynamic.js1
-rw-r--r--dom/security/test/csp/file_strict_dynamic_default_src.html20
-rw-r--r--dom/security/test/csp/file_strict_dynamic_default_src.js1
-rw-r--r--dom/security/test/csp/file_strict_dynamic_js_url.html15
-rw-r--r--dom/security/test/csp/file_strict_dynamic_non_parser_inserted.html17
-rw-r--r--dom/security/test/csp/file_strict_dynamic_non_parser_inserted_inline.html16
-rw-r--r--dom/security/test/csp/file_strict_dynamic_parser_inserted_doc_write.html15
-rw-r--r--dom/security/test/csp/file_strict_dynamic_parser_inserted_doc_write_correct_nonce.html15
-rw-r--r--dom/security/test/csp/file_strict_dynamic_script_events.html14
-rw-r--r--dom/security/test/csp/file_strict_dynamic_script_events_marquee.html14
-rw-r--r--dom/security/test/csp/file_strict_dynamic_script_extern.html10
-rw-r--r--dom/security/test/csp/file_strict_dynamic_script_inline.html14
-rw-r--r--dom/security/test/csp/file_strict_dynamic_unsafe_eval.html15
-rw-r--r--dom/security/test/csp/file_subframe_run_js_if_allowed.html13
-rw-r--r--dom/security/test/csp/file_subframe_run_js_if_allowed.html^headers^1
-rw-r--r--dom/security/test/csp/file_svg_inline_style_base.html9
-rw-r--r--dom/security/test/csp/file_svg_inline_style_csp.html10
-rw-r--r--dom/security/test/csp/file_svg_inline_style_server.sjs43
-rw-r--r--dom/security/test/csp/file_svg_srcset_inline_style_base.html9
-rw-r--r--dom/security/test/csp/file_svg_srcset_inline_style_csp.html10
-rw-r--r--dom/security/test/csp/file_test_browser_bookmarklets.html12
-rw-r--r--dom/security/test/csp/file_test_browser_bookmarklets.html^headers^2
-rw-r--r--dom/security/test/csp/file_testserver.sjs66
-rw-r--r--dom/security/test/csp/file_uir_top_nav.html17
-rw-r--r--dom/security/test/csp/file_uir_top_nav_dummy.html12
-rw-r--r--dom/security/test/csp/file_upgrade_insecure.html90
-rw-r--r--dom/security/test/csp/file_upgrade_insecure_cors.html49
-rw-r--r--dom/security/test/csp/file_upgrade_insecure_cors_server.sjs61
-rw-r--r--dom/security/test/csp/file_upgrade_insecure_docwrite_iframe.sjs55
-rw-r--r--dom/security/test/csp/file_upgrade_insecure_loopback.html25
-rw-r--r--dom/security/test/csp/file_upgrade_insecure_loopback_form.html17
-rw-r--r--dom/security/test/csp/file_upgrade_insecure_loopback_server.sjs22
-rw-r--r--dom/security/test/csp/file_upgrade_insecure_meta.html86
-rw-r--r--dom/security/test/csp/file_upgrade_insecure_navigation.sjs78
-rw-r--r--dom/security/test/csp/file_upgrade_insecure_navigation_redirect.sjs50
-rw-r--r--dom/security/test/csp/file_upgrade_insecure_navigation_redirect_cross_origin.html10
-rw-r--r--dom/security/test/csp/file_upgrade_insecure_navigation_redirect_same_origin.html10
-rw-r--r--dom/security/test/csp/file_upgrade_insecure_report_only.html33
-rw-r--r--dom/security/test/csp/file_upgrade_insecure_report_only_server.sjs114
-rw-r--r--dom/security/test/csp/file_upgrade_insecure_reporting.html23
-rw-r--r--dom/security/test/csp/file_upgrade_insecure_reporting_server.sjs89
-rw-r--r--dom/security/test/csp/file_upgrade_insecure_server.sjs112
-rw-r--r--dom/security/test/csp/file_upgrade_insecure_wsh.py6
-rw-r--r--dom/security/test/csp/file_web_manifest.html6
-rw-r--r--dom/security/test/csp/file_web_manifest.json1
-rw-r--r--dom/security/test/csp/file_web_manifest.json^headers^1
-rw-r--r--dom/security/test/csp/file_web_manifest_https.html4
-rw-r--r--dom/security/test/csp/file_web_manifest_https.json1
-rw-r--r--dom/security/test/csp/file_web_manifest_mixed_content.html9
-rw-r--r--dom/security/test/csp/file_web_manifest_remote.html8
-rw-r--r--dom/security/test/csp/file_websocket_csp_upgrade.html20
-rw-r--r--dom/security/test/csp/file_websocket_explicit.html31
-rw-r--r--dom/security/test/csp/file_websocket_self.html31
-rw-r--r--dom/security/test/csp/file_websocket_self_wsh.py6
-rw-r--r--dom/security/test/csp/file_win_open_blocked.html3
-rw-r--r--dom/security/test/csp/file_windowwatcher_frameA.html17
-rw-r--r--dom/security/test/csp/file_windowwatcher_subframeB.html12
-rw-r--r--dom/security/test/csp/file_windowwatcher_subframeC.html9
-rw-r--r--dom/security/test/csp/file_windowwatcher_subframeD.html6
-rw-r--r--dom/security/test/csp/file_windowwatcher_win_open.html15
-rw-r--r--dom/security/test/csp/file_worker_src.js73
-rw-r--r--dom/security/test/csp/file_worker_src_child_governs.html9
-rw-r--r--dom/security/test/csp/file_worker_src_script_governs.html9
-rw-r--r--dom/security/test/csp/file_worker_src_worker_governs.html9
-rw-r--r--dom/security/test/csp/file_xslt_inherits_csp.xml6
-rw-r--r--dom/security/test/csp/file_xslt_inherits_csp.xml^headers^2
-rw-r--r--dom/security/test/csp/file_xslt_inherits_csp.xsl26
-rw-r--r--dom/security/test/csp/main_csp_worker.html439
-rw-r--r--dom/security/test/csp/main_csp_worker.html^headers^1
-rw-r--r--dom/security/test/csp/mochitest.toml821
-rw-r--r--dom/security/test/csp/referrerdirective.sjs40
-rw-r--r--dom/security/test/csp/test_301_redirect.html74
-rw-r--r--dom/security/test/csp/test_302_redirect.html74
-rw-r--r--dom/security/test/csp/test_303_redirect.html74
-rw-r--r--dom/security/test/csp/test_307_redirect.html75
-rw-r--r--dom/security/test/csp/test_CSP.html130
-rw-r--r--dom/security/test/csp/test_allow_https_schemes.html76
-rw-r--r--dom/security/test/csp/test_base-uri.html124
-rw-r--r--dom/security/test/csp/test_blob_data_schemes.html89
-rw-r--r--dom/security/test/csp/test_blob_uri_blocks_modals.html75
-rw-r--r--dom/security/test/csp/test_block_all_mixed_content.html99
-rw-r--r--dom/security/test/csp/test_block_all_mixed_content_frame_navigation.html46
-rw-r--r--dom/security/test/csp/test_blocked_uri_in_reports.html80
-rw-r--r--dom/security/test/csp/test_blocked_uri_in_violation_event_after_redirects.html56
-rw-r--r--dom/security/test/csp/test_blocked_uri_redirect_frame_src.html60
-rw-r--r--dom/security/test/csp/test_bug1229639.html51
-rw-r--r--dom/security/test/csp/test_bug1242019.html51
-rw-r--r--dom/security/test/csp/test_bug1312272.html32
-rw-r--r--dom/security/test/csp/test_bug1388015.html46
-rw-r--r--dom/security/test/csp/test_bug1452037.html41
-rw-r--r--dom/security/test/csp/test_bug1505412.html50
-rw-r--r--dom/security/test/csp/test_bug1579094.html31
-rw-r--r--dom/security/test/csp/test_bug1738418.html28
-rw-r--r--dom/security/test/csp/test_bug1764343.html116
-rw-r--r--dom/security/test/csp/test_bug1777572.html40
-rw-r--r--dom/security/test/csp/test_bug663567.html76
-rw-r--r--dom/security/test/csp/test_bug802872.html53
-rw-r--r--dom/security/test/csp/test_bug836922_npolicies.html235
-rw-r--r--dom/security/test/csp/test_bug885433.html61
-rw-r--r--dom/security/test/csp/test_bug886164.html172
-rw-r--r--dom/security/test/csp/test_bug888172.html73
-rw-r--r--dom/security/test/csp/test_bug909029.html129
-rw-r--r--dom/security/test/csp/test_bug910139.html66
-rw-r--r--dom/security/test/csp/test_bug941404.html107
-rw-r--r--dom/security/test/csp/test_child-src_iframe.html113
-rw-r--r--dom/security/test/csp/test_child-src_worker-redirect.html125
-rw-r--r--dom/security/test/csp/test_child-src_worker.html147
-rw-r--r--dom/security/test/csp/test_child-src_worker_data.html126
-rw-r--r--dom/security/test/csp/test_connect-src.html129
-rw-r--r--dom/security/test/csp/test_csp_frame_ancestors_about_blank.html59
-rw-r--r--dom/security/test/csp/test_csp_style_src_empty_hash.html32
-rw-r--r--dom/security/test/csp/test_csp_worker_inheritance.html20
-rw-r--r--dom/security/test/csp/test_data_csp_inheritance.html36
-rw-r--r--dom/security/test/csp/test_data_csp_merge.html36
-rw-r--r--dom/security/test/csp/test_data_doc_ignore_meta_csp.html39
-rw-r--r--dom/security/test/csp/test_docwrite_meta.html86
-rw-r--r--dom/security/test/csp/test_dual_header.html66
-rw-r--r--dom/security/test/csp/test_empty_directive.html51
-rw-r--r--dom/security/test/csp/test_evalscript.html59
-rw-r--r--dom/security/test/csp/test_evalscript_allowed_by_strict_dynamic.html37
-rw-r--r--dom/security/test/csp/test_evalscript_blocked_by_strict_dynamic.html37
-rw-r--r--dom/security/test/csp/test_fontloader.html98
-rw-r--r--dom/security/test/csp/test_form-action.html105
-rw-r--r--dom/security/test/csp/test_form_action_blocks_url.html76
-rw-r--r--dom/security/test/csp/test_frame_ancestors_ro.html69
-rw-r--r--dom/security/test/csp/test_frame_src.html84
-rw-r--r--dom/security/test/csp/test_frameancestors.html160
-rw-r--r--dom/security/test/csp/test_frameancestors_userpass.html148
-rw-r--r--dom/security/test/csp/test_hash_source.html135
-rw-r--r--dom/security/test/csp/test_iframe_sandbox.html240
-rw-r--r--dom/security/test/csp/test_iframe_sandbox_srcdoc.html62
-rw-r--r--dom/security/test/csp/test_iframe_sandbox_top_1.html80
-rw-r--r--dom/security/test/csp/test_iframe_sandbox_top_1.html^headers^1
-rw-r--r--dom/security/test/csp/test_iframe_srcdoc.html140
-rw-r--r--dom/security/test/csp/test_ignore_unsafe_inline.html122
-rw-r--r--dom/security/test/csp/test_ignore_xfo.html120
-rw-r--r--dom/security/test/csp/test_image_document.html35
-rw-r--r--dom/security/test/csp/test_image_nonce.html60
-rw-r--r--dom/security/test/csp/test_independent_iframe_csp.html79
-rw-r--r--dom/security/test/csp/test_inlinescript.html123
-rw-r--r--dom/security/test/csp/test_inlinestyle.html107
-rw-r--r--dom/security/test/csp/test_invalid_source_expression.html57
-rw-r--r--dom/security/test/csp/test_leading_wildcard.html101
-rw-r--r--dom/security/test/csp/test_link_rel_preload.html73
-rw-r--r--dom/security/test/csp/test_meta_csp_self.html63
-rw-r--r--dom/security/test/csp/test_meta_element.html91
-rw-r--r--dom/security/test/csp/test_meta_header_dual.html135
-rw-r--r--dom/security/test/csp/test_meta_whitespace_skipping.html81
-rw-r--r--dom/security/test/csp/test_multi_policy_injection_bypass.html119
-rw-r--r--dom/security/test/csp/test_multipartchannel.html68
-rw-r--r--dom/security/test/csp/test_nonce_redirects.html47
-rw-r--r--dom/security/test/csp/test_nonce_snapshot.html35
-rw-r--r--dom/security/test/csp/test_nonce_source.html122
-rw-r--r--dom/security/test/csp/test_null_baseuri.html67
-rw-r--r--dom/security/test/csp/test_object_inherit.html30
-rw-r--r--dom/security/test/csp/test_parent_location_js.html38
-rw-r--r--dom/security/test/csp/test_path_matching.html115
-rw-r--r--dom/security/test/csp/test_path_matching_redirect.html89
-rw-r--r--dom/security/test/csp/test_ping.html103
-rw-r--r--dom/security/test/csp/test_policyuri_regression_from_multipolicy.html27
-rw-r--r--dom/security/test/csp/test_punycode_host_src.html81
-rw-r--r--dom/security/test/csp/test_redirects.html142
-rw-r--r--dom/security/test/csp/test_report.html113
-rw-r--r--dom/security/test/csp/test_report_font_cache.html56
-rw-r--r--dom/security/test/csp/test_report_for_import.html109
-rw-r--r--dom/security/test/csp/test_report_uri_missing_in_report_only_header.html54
-rw-r--r--dom/security/test/csp/test_sandbox.html249
-rw-r--r--dom/security/test/csp/test_sandbox_allow_scripts.html31
-rw-r--r--dom/security/test/csp/test_scheme_relative_sources.html91
-rw-r--r--dom/security/test/csp/test_script_template.html60
-rw-r--r--dom/security/test/csp/test_security_policy_violation_event.html15
-rw-r--r--dom/security/test/csp/test_self_none_as_hostname_confusion.html53
-rw-r--r--dom/security/test/csp/test_sendbeacon.html34
-rw-r--r--dom/security/test/csp/test_service_worker.html61
-rw-r--r--dom/security/test/csp/test_strict_dynamic.html133
-rw-r--r--dom/security/test/csp/test_strict_dynamic_default_src.html136
-rw-r--r--dom/security/test/csp/test_strict_dynamic_parser_inserted.html94
-rw-r--r--dom/security/test/csp/test_subframe_run_js_if_allowed.html33
-rw-r--r--dom/security/test/csp/test_svg_inline_style.html135
-rw-r--r--dom/security/test/csp/test_uir_top_nav.html53
-rw-r--r--dom/security/test/csp/test_uir_windowwatcher.html31
-rw-r--r--dom/security/test/csp/test_upgrade_insecure.html192
-rw-r--r--dom/security/test/csp/test_upgrade_insecure_cors.html86
-rw-r--r--dom/security/test/csp/test_upgrade_insecure_docwrite_iframe.html54
-rw-r--r--dom/security/test/csp/test_upgrade_insecure_loopback.html91
-rw-r--r--dom/security/test/csp/test_upgrade_insecure_navigation.html105
-rw-r--r--dom/security/test/csp/test_upgrade_insecure_navigation_redirect.html66
-rw-r--r--dom/security/test/csp/test_upgrade_insecure_report_only.html98
-rw-r--r--dom/security/test/csp/test_upgrade_insecure_reporting.html69
-rw-r--r--dom/security/test/csp/test_websocket_localhost.html40
-rw-r--r--dom/security/test/csp/test_websocket_self.html61
-rw-r--r--dom/security/test/csp/test_win_open_blocked.html52
-rw-r--r--dom/security/test/csp/test_worker_src.html105
-rw-r--r--dom/security/test/csp/test_xslt_inherits_csp.html33
-rw-r--r--dom/security/test/csp/worker.sjs111
-rw-r--r--dom/security/test/csp/worker_helper.js91
-rw-r--r--dom/security/test/general/browser.toml81
-rw-r--r--dom/security/test/general/browser_file_nonscript.js38
-rw-r--r--dom/security/test/general/browser_restrict_privileged_about_script.js70
-rw-r--r--dom/security/test/general/browser_same_site_cookies_bug1748693.js61
-rw-r--r--dom/security/test/general/browser_test_assert_systemprincipal_documents.js41
-rw-r--r--dom/security/test/general/browser_test_data_download.js113
-rw-r--r--dom/security/test/general/browser_test_data_text_csv.js108
-rw-r--r--dom/security/test/general/browser_test_framing_error_pages.js53
-rw-r--r--dom/security/test/general/browser_test_gpc_privateBrowsingMode.js67
-rw-r--r--dom/security/test/general/browser_test_referrer_loadInOtherProcess.js156
-rw-r--r--dom/security/test/general/browser_test_report_blocking.js218
-rw-r--r--dom/security/test/general/browser_test_toplevel_data_navigations.js70
-rw-r--r--dom/security/test/general/browser_test_view_image_data_navigation.js71
-rw-r--r--dom/security/test/general/browser_test_xfo_embed_object.js41
-rw-r--r--dom/security/test/general/bug1277803.html11
-rw-r--r--dom/security/test/general/chrome.toml15
-rw-r--r--dom/security/test/general/closeWindow.sjs24
-rw-r--r--dom/security/test/general/favicon_bug1277803.icobin0 -> 1406 bytes
-rw-r--r--dom/security/test/general/file_1767581.js1
-rw-r--r--dom/security/test/general/file_about_child.html11
-rw-r--r--dom/security/test/general/file_assert_systemprincipal_documents.html11
-rw-r--r--dom/security/test/general/file_assert_systemprincipal_documents_iframe.html9
-rw-r--r--dom/security/test/general/file_block_script_wrong_mime_server.sjs37
-rw-r--r--dom/security/test/general/file_block_subresource_redir_to_data.sjs33
-rw-r--r--dom/security/test/general/file_block_toplevel_data_navigation.html16
-rw-r--r--dom/security/test/general/file_block_toplevel_data_navigation2.html17
-rw-r--r--dom/security/test/general/file_block_toplevel_data_navigation3.html16
-rw-r--r--dom/security/test/general/file_block_toplevel_data_redirect.sjs13
-rw-r--r--dom/security/test/general/file_cache_splitting_isloaded.sjs35
-rw-r--r--dom/security/test/general/file_cache_splitting_server.sjs27
-rw-r--r--dom/security/test/general/file_cache_splitting_window.html17
-rw-r--r--dom/security/test/general/file_contentpolicytype_targeted_link_iframe.sjs45
-rw-r--r--dom/security/test/general/file_data_download.html14
-rw-r--r--dom/security/test/general/file_data_text_csv.html14
-rw-r--r--dom/security/test/general/file_empty.html1
-rw-r--r--dom/security/test/general/file_framing_error_pages.sjs27
-rw-r--r--dom/security/test/general/file_framing_error_pages_csp.html7
-rw-r--r--dom/security/test/general/file_framing_error_pages_xfo.html7
-rw-r--r--dom/security/test/general/file_framing_xfo_embed.html7
-rw-r--r--dom/security/test/general/file_framing_xfo_embed_object.sjs7
-rw-r--r--dom/security/test/general/file_framing_xfo_object.html7
-rw-r--r--dom/security/test/general/file_gpc_server.sjs14
-rw-r--r--dom/security/test/general/file_loads_nonscript.html49
-rw-r--r--dom/security/test/general/file_meta_referrer_in_head.html13
-rw-r--r--dom/security/test/general/file_meta_referrer_notin_head.html14
-rw-r--r--dom/security/test/general/file_nonscript1
-rw-r--r--dom/security/test/general/file_nonscript.html1
-rw-r--r--dom/security/test/general/file_nonscript.json1
-rw-r--r--dom/security/test/general/file_nonscript.txt1
-rw-r--r--dom/security/test/general/file_nonscript.xyz1
-rw-r--r--dom/security/test/general/file_nosniff_navigation.sjs39
-rw-r--r--dom/security/test/general/file_nosniff_testserver.sjs60
-rw-r--r--dom/security/test/general/file_same_site_cookies_about.sjs99
-rw-r--r--dom/security/test/general/file_same_site_cookies_blob_iframe_inclusion.html34
-rw-r--r--dom/security/test/general/file_same_site_cookies_blob_iframe_navigation.html30
-rw-r--r--dom/security/test/general/file_same_site_cookies_bug1748693.sjs31
-rw-r--r--dom/security/test/general/file_same_site_cookies_cross_origin_context.sjs54
-rw-r--r--dom/security/test/general/file_same_site_cookies_from_script.sjs48
-rw-r--r--dom/security/test/general/file_same_site_cookies_iframe.sjs99
-rw-r--r--dom/security/test/general/file_same_site_cookies_redirect.sjs103
-rw-r--r--dom/security/test/general/file_same_site_cookies_subrequest.sjs82
-rw-r--r--dom/security/test/general/file_same_site_cookies_toplevel_nav.sjs96
-rw-r--r--dom/security/test/general/file_same_site_cookies_toplevel_set_cookie.sjs68
-rw-r--r--dom/security/test/general/file_script.js1
-rw-r--r--dom/security/test/general/file_toplevel_data_meta_redirect.html10
-rw-r--r--dom/security/test/general/file_toplevel_data_navigations.sjs13
-rw-r--r--dom/security/test/general/file_view_bg_image_data_navigation.html16
-rw-r--r--dom/security/test/general/file_view_image_data_navigation.html12
-rw-r--r--dom/security/test/general/file_xfo_error_page.sjs8
-rw-r--r--dom/security/test/general/mochitest.toml148
-rw-r--r--dom/security/test/general/test_allow_opening_data_json.html39
-rw-r--r--dom/security/test/general/test_allow_opening_data_pdf.html41
-rw-r--r--dom/security/test/general/test_assert_about_page_no_csp.html30
-rw-r--r--dom/security/test/general/test_block_script_wrong_mime.html92
-rw-r--r--dom/security/test/general/test_block_subresource_redir_to_data.html66
-rw-r--r--dom/security/test/general/test_block_toplevel_data_img_navigation.html53
-rw-r--r--dom/security/test/general/test_block_toplevel_data_navigation.html134
-rw-r--r--dom/security/test/general/test_bug1277803.xhtml65
-rw-r--r--dom/security/test/general/test_bug1450853.html91
-rw-r--r--dom/security/test/general/test_bug1660452_http.html39
-rw-r--r--dom/security/test/general/test_bug1660452_https.html39
-rw-r--r--dom/security/test/general/test_cache_split.html153
-rw-r--r--dom/security/test/general/test_contentpolicytype_targeted_link_iframe.html103
-rw-r--r--dom/security/test/general/test_gpc.html51
-rw-r--r--dom/security/test/general/test_innerhtml_sanitizer.html74
-rw-r--r--dom/security/test/general/test_innerhtml_sanitizer.xhtml73
-rw-r--r--dom/security/test/general/test_meta_referrer.html55
-rw-r--r--dom/security/test/general/test_nosniff.html88
-rw-r--r--dom/security/test/general/test_nosniff_navigation.html35
-rw-r--r--dom/security/test/general/test_same_site_cookies_about.html116
-rw-r--r--dom/security/test/general/test_same_site_cookies_cross_origin_context.html93
-rw-r--r--dom/security/test/general/test_same_site_cookies_from_script.html86
-rw-r--r--dom/security/test/general/test_same_site_cookies_iframe.html168
-rw-r--r--dom/security/test/general/test_same_site_cookies_laxByDefault.html85
-rw-r--r--dom/security/test/general/test_same_site_cookies_redirect.html101
-rw-r--r--dom/security/test/general/test_same_site_cookies_subrequest.html113
-rw-r--r--dom/security/test/general/test_same_site_cookies_toplevel_nav.html117
-rw-r--r--dom/security/test/general/test_same_site_cookies_toplevel_set_cookie.html57
-rw-r--r--dom/security/test/general/test_xfo_error_page.html35
-rw-r--r--dom/security/test/general/window_nosniff_navigation.html96
-rw-r--r--dom/security/test/gtest/TestCSPParser.cpp1148
-rw-r--r--dom/security/test/gtest/TestFilenameEvalParser.cpp453
-rw-r--r--dom/security/test/gtest/TestSecureContext.cpp121
-rw-r--r--dom/security/test/gtest/TestSmartCrashTrimmer.cpp44
-rw-r--r--dom/security/test/gtest/TestUnexpectedPrivilegedLoads.cpp305
-rw-r--r--dom/security/test/gtest/moz.build25
-rw-r--r--dom/security/test/https-first/browser.toml53
-rw-r--r--dom/security/test/https-first/browser_beforeunload_permit_http.js208
-rw-r--r--dom/security/test/https-first/browser_downgrade_mixed_content_auto_upgrade_console.js82
-rw-r--r--dom/security/test/https-first/browser_downgrade_view_source.js81
-rw-r--r--dom/security/test/https-first/browser_download_attribute.js125
-rw-r--r--dom/security/test/https-first/browser_httpsfirst.js74
-rw-r--r--dom/security/test/https-first/browser_httpsfirst_console_logging.js72
-rw-r--r--dom/security/test/https-first/browser_httpsfirst_speculative_connect.js71
-rw-r--r--dom/security/test/https-first/browser_mixed_content_console.js104
-rw-r--r--dom/security/test/https-first/browser_mixed_content_download.js156
-rw-r--r--dom/security/test/https-first/browser_navigation.js97
-rw-r--r--dom/security/test/https-first/browser_schemeless.js191
-rw-r--r--dom/security/test/https-first/browser_slow_download.js158
-rw-r--r--dom/security/test/https-first/browser_superfluos_auth.js67
-rw-r--r--dom/security/test/https-first/browser_upgrade_onion.js60
-rw-r--r--dom/security/test/https-first/download_page.html22
-rw-r--r--dom/security/test/https-first/download_server.sjs16
-rw-r--r--dom/security/test/https-first/file_bad_cert.sjs34
-rw-r--r--dom/security/test/https-first/file_beforeunload_permit_http.html9
-rw-r--r--dom/security/test/https-first/file_break_endless_upgrade_downgrade_loop.sjs61
-rw-r--r--dom/security/test/https-first/file_data_uri.html16
-rw-r--r--dom/security/test/https-first/file_downgrade_500_responses.sjs70
-rw-r--r--dom/security/test/https-first/file_downgrade_bad_responses.sjs73
-rw-r--r--dom/security/test/https-first/file_downgrade_request_upgrade_request.sjs52
-rw-r--r--dom/security/test/https-first/file_downgrade_view_source.sjs30
-rw-r--r--dom/security/test/https-first/file_downgrade_with_different_path.sjs27
-rw-r--r--dom/security/test/https-first/file_download_attribute.html14
-rw-r--r--dom/security/test/https-first/file_download_attribute.sjs12
-rw-r--r--dom/security/test/https-first/file_form_submission.sjs84
-rw-r--r--dom/security/test/https-first/file_fragment.html43
-rw-r--r--dom/security/test/https-first/file_httpsfirst_speculative_connect.html1
-rw-r--r--dom/security/test/https-first/file_httpsfirst_timeout_server.sjs13
-rw-r--r--dom/security/test/https-first/file_mixed_content_auto_upgrade.html12
-rw-r--r--dom/security/test/https-first/file_mixed_content_console.html13
-rw-r--r--dom/security/test/https-first/file_multiple_redirection.sjs87
-rw-r--r--dom/security/test/https-first/file_navigation.html6
-rw-r--r--dom/security/test/https-first/file_redirect.sjs58
-rw-r--r--dom/security/test/https-first/file_redirect_downgrade.sjs87
-rw-r--r--dom/security/test/https-first/file_referrer_policy.sjs102
-rw-r--r--dom/security/test/https-first/file_slow_download.html14
-rw-r--r--dom/security/test/https-first/file_slow_download.sjs26
-rw-r--r--dom/security/test/https-first/file_toplevel_cookies.sjs233
-rw-r--r--dom/security/test/https-first/file_upgrade_insecure.html72
-rw-r--r--dom/security/test/https-first/file_upgrade_insecure_server.sjs114
-rw-r--r--dom/security/test/https-first/mochitest.toml56
-rw-r--r--dom/security/test/https-first/pass.pngbin0 -> 1689 bytes
-rw-r--r--dom/security/test/https-first/test.ogvbin0 -> 2344665 bytes
-rw-r--r--dom/security/test/https-first/test.wavbin0 -> 353022 bytes
-rw-r--r--dom/security/test/https-first/test_bad_cert.html67
-rw-r--r--dom/security/test/https-first/test_break_endless_upgrade_downgrade_loop.html80
-rw-r--r--dom/security/test/https-first/test_data_uri.html51
-rw-r--r--dom/security/test/https-first/test_downgrade_500_responses.html63
-rw-r--r--dom/security/test/https-first/test_downgrade_bad_responses.html63
-rw-r--r--dom/security/test/https-first/test_downgrade_request_upgrade_request.html52
-rw-r--r--dom/security/test/https-first/test_form_submission.html122
-rw-r--r--dom/security/test/https-first/test_fragment.html59
-rw-r--r--dom/security/test/https-first/test_multiple_redirection.html76
-rw-r--r--dom/security/test/https-first/test_redirect_downgrade.html59
-rw-r--r--dom/security/test/https-first/test_redirect_upgrade.html58
-rw-r--r--dom/security/test/https-first/test_referrer_policy.html237
-rw-r--r--dom/security/test/https-first/test_resource_upgrade.html115
-rw-r--r--dom/security/test/https-first/test_toplevel_cookies.html116
-rw-r--r--dom/security/test/https-only/browser.toml62
-rw-r--r--dom/security/test/https-only/browser_background_redirect.js64
-rw-r--r--dom/security/test/https-only/browser_bug1874801.js56
-rw-r--r--dom/security/test/https-only/browser_console_logging.js153
-rw-r--r--dom/security/test/https-only/browser_continue_button_delay.js59
-rw-r--r--dom/security/test/https-only/browser_cors_mixedcontent.js127
-rw-r--r--dom/security/test/https-only/browser_hsts_host.js203
-rw-r--r--dom/security/test/https-only/browser_httpsonly_prefs.js118
-rw-r--r--dom/security/test/https-only/browser_httpsonly_speculative_connect.js71
-rw-r--r--dom/security/test/https-only/browser_iframe_test.js223
-rw-r--r--dom/security/test/https-only/browser_navigation.js94
-rw-r--r--dom/security/test/https-only/browser_redirect_tainting.js39
-rw-r--r--dom/security/test/https-only/browser_save_as.js187
-rw-r--r--dom/security/test/https-only/browser_triggering_principal_exemption.js72
-rw-r--r--dom/security/test/https-only/browser_upgrade_exceptions.js86
-rw-r--r--dom/security/test/https-only/browser_upgrade_exemption.js80
-rw-r--r--dom/security/test/https-only/browser_user_gesture.js55
-rw-r--r--dom/security/test/https-only/browser_websocket_exceptions.js68
-rw-r--r--dom/security/test/https-only/file_background_redirect.sjs32
-rw-r--r--dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs67
-rw-r--r--dom/security/test/https-only/file_bug1874801.html11
-rw-r--r--dom/security/test/https-only/file_bug1874801.sjs17
-rw-r--r--dom/security/test/https-only/file_console_logging.html16
-rw-r--r--dom/security/test/https-only/file_cors_mixedcontent.html42
-rw-r--r--dom/security/test/https-only/file_fragment.html47
-rw-r--r--dom/security/test/https-only/file_fragment_noscript.html9
-rw-r--r--dom/security/test/https-only/file_http_background_auth_request.sjs16
-rw-r--r--dom/security/test/https-only/file_http_background_request.sjs15
-rw-r--r--dom/security/test/https-only/file_httpsonly_speculative_connect.html1
-rw-r--r--dom/security/test/https-only/file_iframe_test.sjs58
-rw-r--r--dom/security/test/https-only/file_insecure_reload.sjs27
-rw-r--r--dom/security/test/https-only/file_redirect.sjs37
-rw-r--r--dom/security/test/https-only/file_redirect_tainting.sjs39
-rw-r--r--dom/security/test/https-only/file_redirect_to_insecure.sjs16
-rw-r--r--dom/security/test/https-only/file_save_as.html10
-rw-r--r--dom/security/test/https-only/file_upgrade_insecure.html91
-rw-r--r--dom/security/test/https-only/file_upgrade_insecure_server.sjs112
-rw-r--r--dom/security/test/https-only/file_upgrade_insecure_wsh.py6
-rw-r--r--dom/security/test/https-only/file_user_gesture.html11
-rw-r--r--dom/security/test/https-only/file_websocket_exceptions.html9
-rw-r--r--dom/security/test/https-only/file_websocket_exceptions_iframe.html30
-rw-r--r--dom/security/test/https-only/hsts_headers.sjs24
-rw-r--r--dom/security/test/https-only/mochitest.toml65
-rw-r--r--dom/security/test/https-only/test_break_endless_upgrade_downgrade_loop.html89
-rw-r--r--dom/security/test/https-only/test_fragment.html72
-rw-r--r--dom/security/test/https-only/test_http_background_auth_request.html113
-rw-r--r--dom/security/test/https-only/test_http_background_request.html125
-rw-r--r--dom/security/test/https-only/test_insecure_reload.html74
-rw-r--r--dom/security/test/https-only/test_redirect_upgrade.html58
-rw-r--r--dom/security/test/https-only/test_resource_upgrade.html122
-rw-r--r--dom/security/test/https-only/test_user_suggestion_box.html81
-rw-r--r--dom/security/test/mixedcontentblocker/auto_upgrading_identity.html11
-rw-r--r--dom/security/test/mixedcontentblocker/auto_upgrading_identity.pngbin0 -> 70 bytes
-rw-r--r--dom/security/test/mixedcontentblocker/browser.toml35
-rw-r--r--dom/security/test/mixedcontentblocker/browser_auto_upgrading_identity.js58
-rw-r--r--dom/security/test/mixedcontentblocker/browser_csp_block_all_mixedcontent_and_mixed_content_display_upgrade.js171
-rw-r--r--dom/security/test/mixedcontentblocker/browser_mixed_content_auth_download.js169
-rw-r--r--dom/security/test/mixedcontentblocker/browser_mixed_content_auto_upgrade_display_console.js54
-rw-r--r--dom/security/test/mixedcontentblocker/browser_test_mixed_content_download.js341
-rw-r--r--dom/security/test/mixedcontentblocker/download_page.html41
-rw-r--r--dom/security/test/mixedcontentblocker/download_server.sjs20
-rw-r--r--dom/security/test/mixedcontentblocker/file_auth_download_page.html22
-rw-r--r--dom/security/test/mixedcontentblocker/file_auth_download_server.sjs61
-rw-r--r--dom/security/test/mixedcontentblocker/file_bug1550792.html39
-rw-r--r--dom/security/test/mixedcontentblocker/file_bug1551886.html25
-rw-r--r--dom/security/test/mixedcontentblocker/file_bug803225_test_mailto.html13
-rw-r--r--dom/security/test/mixedcontentblocker/file_csp_block_all_mixedcontent_and_mixed_content_display_upgrade.html14
-rw-r--r--dom/security/test/mixedcontentblocker/file_frameNavigation.html72
-rw-r--r--dom/security/test/mixedcontentblocker/file_frameNavigation_blankTarget.html33
-rw-r--r--dom/security/test/mixedcontentblocker/file_frameNavigation_grandchild.html55
-rw-r--r--dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html74
-rw-r--r--dom/security/test/mixedcontentblocker/file_frameNavigation_secure.html72
-rw-r--r--dom/security/test/mixedcontentblocker/file_frameNavigation_secure_grandchild.html56
-rw-r--r--dom/security/test/mixedcontentblocker/file_main.html336
-rw-r--r--dom/security/test/mixedcontentblocker/file_main_bug803225.html175
-rw-r--r--dom/security/test/mixedcontentblocker/file_main_bug803225_websocket_wsh.py6
-rw-r--r--dom/security/test/mixedcontentblocker/file_mixed_content_auto_upgrade_display_console.html10
-rw-r--r--dom/security/test/mixedcontentblocker/file_redirect.html31
-rw-r--r--dom/security/test/mixedcontentblocker/file_redirect_handler.sjs34
-rw-r--r--dom/security/test/mixedcontentblocker/file_server.sjs131
-rw-r--r--dom/security/test/mixedcontentblocker/file_windowOpen.html9
-rw-r--r--dom/security/test/mixedcontentblocker/mochitest.toml66
-rw-r--r--dom/security/test/mixedcontentblocker/pass.pngbin0 -> 1689 bytes
-rw-r--r--dom/security/test/mixedcontentblocker/test.ogvbin0 -> 2344665 bytes
-rw-r--r--dom/security/test/mixedcontentblocker/test.wavbin0 -> 353022 bytes
-rw-r--r--dom/security/test/mixedcontentblocker/test_bug1550792.html35
-rw-r--r--dom/security/test/mixedcontentblocker/test_bug1551886.html33
-rw-r--r--dom/security/test/mixedcontentblocker/test_bug803225.html157
-rw-r--r--dom/security/test/mixedcontentblocker/test_frameNavigation.html127
-rw-r--r--dom/security/test/mixedcontentblocker/test_main.html231
-rw-r--r--dom/security/test/mixedcontentblocker/test_redirect.html45
-rw-r--r--dom/security/test/mixedcontentblocker/test_windowOpen.html82
-rw-r--r--dom/security/test/moz.build43
-rw-r--r--dom/security/test/referrer-policy/browser.toml9
-rw-r--r--dom/security/test/referrer-policy/browser_fragment_navigation.js42
-rw-r--r--dom/security/test/referrer-policy/browser_referrer_disallow_cross_site_relaxing.js458
-rw-r--r--dom/security/test/referrer-policy/browser_referrer_telemetry.js126
-rw-r--r--dom/security/test/referrer-policy/file_fragment_navigation.sjs21
-rw-r--r--dom/security/test/referrer-policy/img_referrer_testserver.sjs337
-rw-r--r--dom/security/test/referrer-policy/mochitest.toml28
-rw-r--r--dom/security/test/referrer-policy/referrer_header.sjs6
-rw-r--r--dom/security/test/referrer-policy/referrer_header_current_document_iframe.html12
-rw-r--r--dom/security/test/referrer-policy/referrer_helper.js129
-rw-r--r--dom/security/test/referrer-policy/referrer_page.sjs39
-rw-r--r--dom/security/test/referrer-policy/referrer_testserver.sjs704
-rw-r--r--dom/security/test/referrer-policy/test_img_referrer.html190
-rw-r--r--dom/security/test/referrer-policy/test_referrer_header_current_document.html35
-rw-r--r--dom/security/test/referrer-policy/test_referrer_redirect.html171
-rw-r--r--dom/security/test/sec-fetch/browser.toml10
-rw-r--r--dom/security/test/sec-fetch/browser_external_loads.js176
-rw-r--r--dom/security/test/sec-fetch/browser_navigation.js182
-rw-r--r--dom/security/test/sec-fetch/file_dummy_link.html9
-rw-r--r--dom/security/test/sec-fetch/file_dummy_link_location.html9
-rw-r--r--dom/security/test/sec-fetch/file_no_cache.sjs28
-rw-r--r--dom/security/test/sec-fetch/file_redirect.sjs34
-rw-r--r--dom/security/test/sec-fetch/file_trustworthy_loopback.html11
-rw-r--r--dom/security/test/sec-fetch/file_websocket_wsh.py6
-rw-r--r--dom/security/test/sec-fetch/mochitest.toml29
-rw-r--r--dom/security/test/sec-fetch/test_iframe_history_manipulation.html85
-rw-r--r--dom/security/test/sec-fetch/test_iframe_src_metaRedirect.html95
-rw-r--r--dom/security/test/sec-fetch/test_iframe_srcdoc_metaRedirect.html95
-rw-r--r--dom/security/test/sec-fetch/test_iframe_window_open_metaRedirect.html98
-rw-r--r--dom/security/test/sec-fetch/test_trustworthy_loopback.html77
-rw-r--r--dom/security/test/sec-fetch/test_websocket.html74
-rw-r--r--dom/security/test/sri/file_bug_1271796.css2
-rw-r--r--dom/security/test/sri/iframe_script_crossdomain.html135
-rw-r--r--dom/security/test/sri/iframe_script_sameorigin.html249
-rw-r--r--dom/security/test/sri/iframe_style_crossdomain.html117
-rw-r--r--dom/security/test/sri/iframe_style_sameorigin.html164
-rw-r--r--dom/security/test/sri/mochitest.toml56
-rw-r--r--dom/security/test/sri/script.js1
-rw-r--r--dom/security/test/sri/script.js^headers^1
-rw-r--r--dom/security/test/sri/script_301.js1
-rw-r--r--dom/security/test/sri/script_301.js^headers^2
-rw-r--r--dom/security/test/sri/script_302.js1
-rw-r--r--dom/security/test/sri/script_302.js^headers^2
-rw-r--r--dom/security/test/sri/script_401.js1
-rw-r--r--dom/security/test/sri/script_401.js^headers^2
-rw-r--r--dom/security/test/sri/script_crossdomain1.js4
-rw-r--r--dom/security/test/sri/script_crossdomain1.js^headers^1
-rw-r--r--dom/security/test/sri/script_crossdomain2.js5
-rw-r--r--dom/security/test/sri/script_crossdomain3.js1
-rw-r--r--dom/security/test/sri/script_crossdomain3.js^headers^1
-rw-r--r--dom/security/test/sri/script_crossdomain4.js1
-rw-r--r--dom/security/test/sri/script_crossdomain4.js^headers^1
-rw-r--r--dom/security/test/sri/script_crossdomain5.js1
-rw-r--r--dom/security/test/sri/script_crossdomain5.js^headers^1
-rw-r--r--dom/security/test/sri/style1.css3
-rw-r--r--dom/security/test/sri/style1.css^headers^1
-rw-r--r--dom/security/test/sri/style2.css1
-rw-r--r--dom/security/test/sri/style3.css3
-rw-r--r--dom/security/test/sri/style4.css4
-rw-r--r--dom/security/test/sri/style4.css^headers^1
-rw-r--r--dom/security/test/sri/style5.css4
-rw-r--r--dom/security/test/sri/style6.css4
-rw-r--r--dom/security/test/sri/style6.css^headers^1
-rw-r--r--dom/security/test/sri/style_301.css3
-rw-r--r--dom/security/test/sri/style_301.css^headers^2
-rw-r--r--dom/security/test/sri/test_bug_1271796.html30
-rw-r--r--dom/security/test/sri/test_bug_1364262.html34
-rw-r--r--dom/security/test/sri/test_script_crossdomain.html15
-rw-r--r--dom/security/test/sri/test_script_sameorigin.html15
-rw-r--r--dom/security/test/sri/test_style_crossdomain.html15
-rw-r--r--dom/security/test/sri/test_style_sameorigin.html15
-rw-r--r--dom/security/test/unit/test_csp_reports.js299
-rw-r--r--dom/security/test/unit/test_csp_upgrade_insecure_request_header.js103
-rw-r--r--dom/security/test/unit/test_deserialization_format_before_100.js244
-rw-r--r--dom/security/test/unit/test_https_only_https_first_default_port.js111
-rw-r--r--dom/security/test/unit/test_https_only_https_first_prefs.js361
-rw-r--r--dom/security/test/unit/test_isOriginPotentiallyTrustworthy.js53
-rw-r--r--dom/security/test/unit/xpcshell.toml16
860 files changed, 67148 insertions, 0 deletions
diff --git a/dom/security/CSPEvalChecker.cpp b/dom/security/CSPEvalChecker.cpp
new file mode 100644
index 0000000000..71fa4397ed
--- /dev/null
+++ b/dom/security/CSPEvalChecker.cpp
@@ -0,0 +1,190 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/CSPEvalChecker.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/WorkerPrivate.h"
+#include "mozilla/dom/WorkerRunnable.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/ErrorResult.h"
+#include "nsIParentChannel.h"
+#include "nsGlobalWindowInner.h"
+#include "nsContentSecurityUtils.h"
+#include "nsContentUtils.h"
+#include "nsCOMPtr.h"
+#include "nsJSUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+namespace {
+
+// We use the subjectPrincipal to assert that eval() is never
+// executed in system privileged context.
+nsresult CheckInternal(nsIContentSecurityPolicy* aCSP,
+ nsICSPEventListener* aCSPEventListener,
+ nsIPrincipal* aSubjectPrincipal,
+ const nsAString& aExpression,
+ const nsAString& aFileNameString, uint32_t aLineNum,
+ uint32_t aColumnNum, bool* aAllowed) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aAllowed);
+
+ // The value is set at any "return", but better to have a default value here.
+ *aAllowed = false;
+
+ // This is the non-CSP check for gating eval() use in the SystemPrincipal
+#if !defined(ANDROID)
+ JSContext* cx = nsContentUtils::GetCurrentJSContext();
+ if (!nsContentSecurityUtils::IsEvalAllowed(
+ cx, aSubjectPrincipal->IsSystemPrincipal(), aExpression)) {
+ *aAllowed = false;
+ return NS_OK;
+ }
+#endif
+
+ if (!aCSP) {
+ *aAllowed = true;
+ return NS_OK;
+ }
+
+ bool reportViolation = false;
+ nsresult rv = aCSP->GetAllowsEval(&reportViolation, aAllowed);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ *aAllowed = false;
+ return rv;
+ }
+
+ if (reportViolation) {
+ aCSP->LogViolationDetails(nsIContentSecurityPolicy::VIOLATION_TYPE_EVAL,
+ nullptr, // triggering element
+ aCSPEventListener, aFileNameString, aExpression,
+ aLineNum, aColumnNum, u""_ns, u""_ns);
+ }
+
+ return NS_OK;
+}
+
+class WorkerCSPCheckRunnable final : public WorkerMainThreadRunnable {
+ public:
+ WorkerCSPCheckRunnable(WorkerPrivate* aWorkerPrivate,
+ const nsAString& aExpression,
+ const nsAString& aFileNameString, uint32_t aLineNum,
+ uint32_t aColumnNum)
+ : WorkerMainThreadRunnable(aWorkerPrivate, "CSP Eval Check"_ns),
+ mExpression(aExpression),
+ mFileNameString(aFileNameString),
+ mLineNum(aLineNum),
+ mColumnNum(aColumnNum),
+ mEvalAllowed(false) {}
+
+ bool MainThreadRun() override {
+ mResult = CheckInternal(
+ mWorkerPrivate->GetCsp(), mWorkerPrivate->CSPEventListener(),
+ mWorkerPrivate->GetLoadingPrincipal(), mExpression, mFileNameString,
+ mLineNum, mColumnNum, &mEvalAllowed);
+ return true;
+ }
+
+ nsresult GetResult(bool* aAllowed) {
+ MOZ_ASSERT(aAllowed);
+ *aAllowed = mEvalAllowed;
+ return mResult;
+ }
+
+ private:
+ const nsString mExpression;
+ const nsString mFileNameString;
+ const uint32_t mLineNum;
+ const uint32_t mColumnNum;
+ bool mEvalAllowed;
+ nsresult mResult;
+};
+
+} // namespace
+
+/* static */
+nsresult CSPEvalChecker::CheckForWindow(JSContext* aCx,
+ nsGlobalWindowInner* aWindow,
+ const nsAString& aExpression,
+ bool* aAllowEval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aWindow);
+ MOZ_ASSERT(aAllowEval);
+
+ // The value is set at any "return", but better to have a default value here.
+ *aAllowEval = false;
+
+ // if CSP is enabled, and setTimeout/setInterval was called with a string,
+ // disable the registration and log an error
+ nsCOMPtr<Document> doc = aWindow->GetExtantDoc();
+ if (!doc) {
+ // if there's no document, we don't have to do anything.
+ *aAllowEval = true;
+ return NS_OK;
+ }
+
+ nsresult rv = NS_OK;
+
+ // Get the calling location.
+ uint32_t lineNum = 0;
+ uint32_t columnNum = 1;
+ nsAutoString fileNameString;
+ if (!nsJSUtils::GetCallingLocation(aCx, fileNameString, &lineNum,
+ &columnNum)) {
+ fileNameString.AssignLiteral("unknown");
+ }
+
+ nsCOMPtr<nsIContentSecurityPolicy> csp = doc->GetCsp();
+ rv = CheckInternal(csp, nullptr /* no CSPEventListener for window */,
+ doc->NodePrincipal(), aExpression, fileNameString, lineNum,
+ columnNum, aAllowEval);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ *aAllowEval = false;
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+/* static */
+nsresult CSPEvalChecker::CheckForWorker(JSContext* aCx,
+ WorkerPrivate* aWorkerPrivate,
+ const nsAString& aExpression,
+ bool* aAllowEval) {
+ MOZ_ASSERT(aWorkerPrivate);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ MOZ_ASSERT(aAllowEval);
+
+ // The value is set at any "return", but better to have a default value here.
+ *aAllowEval = false;
+
+ // Get the calling location.
+ uint32_t lineNum = 0;
+ uint32_t columnNum = 1;
+ nsAutoString fileNameString;
+ if (!nsJSUtils::GetCallingLocation(aCx, fileNameString, &lineNum,
+ &columnNum)) {
+ fileNameString.AssignLiteral("unknown");
+ }
+
+ RefPtr<WorkerCSPCheckRunnable> r = new WorkerCSPCheckRunnable(
+ aWorkerPrivate, aExpression, fileNameString, lineNum, columnNum);
+ ErrorResult error;
+ r->Dispatch(Canceling, error);
+ if (NS_WARN_IF(error.Failed())) {
+ *aAllowEval = false;
+ return error.StealNSResult();
+ }
+
+ nsresult rv = r->GetResult(aAllowEval);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ *aAllowEval = false;
+ return rv;
+ }
+
+ return NS_OK;
+}
diff --git a/dom/security/CSPEvalChecker.h b/dom/security/CSPEvalChecker.h
new file mode 100644
index 0000000000..c2a1f8d774
--- /dev/null
+++ b/dom/security/CSPEvalChecker.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_CSPEvalChecker_h
+#define mozilla_dom_CSPEvalChecker_h
+
+#include "nsString.h"
+
+struct JSContext;
+class nsGlobalWindowInner;
+
+namespace mozilla::dom {
+
+class WorkerPrivate;
+
+class CSPEvalChecker final {
+ public:
+ static nsresult CheckForWindow(JSContext* aCx, nsGlobalWindowInner* aWindow,
+ const nsAString& aExpression,
+ bool* aAllowEval);
+
+ static nsresult CheckForWorker(JSContext* aCx, WorkerPrivate* aWorkerPrivate,
+ const nsAString& aExpression,
+ bool* aAllowEval);
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_CSPEvalChecker_h
diff --git a/dom/security/DOMSecurityMonitor.cpp b/dom/security/DOMSecurityMonitor.cpp
new file mode 100644
index 0000000000..2ec4998b0b
--- /dev/null
+++ b/dom/security/DOMSecurityMonitor.cpp
@@ -0,0 +1,136 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DOMSecurityMonitor.h"
+#include "nsContentUtils.h"
+
+#include "nsIChannel.h"
+#include "nsILoadInfo.h"
+#include "nsIPrincipal.h"
+#include "nsIURI.h"
+#include "nsJSUtils.h"
+#include "xpcpublic.h"
+
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/StaticPrefs_dom.h"
+
+/* static */
+void DOMSecurityMonitor::AuditParsingOfHTMLXMLFragments(
+ nsIPrincipal* aPrincipal, const nsAString& aFragment) {
+ // if the fragment parser (e.g. innerHTML()) is not called in chrome: code
+ // or any of our about: pages, then there is nothing to do here.
+ if (!aPrincipal->IsSystemPrincipal() && !aPrincipal->SchemeIs("about")) {
+ return;
+ }
+
+ // check if the fragment is empty, if so, we can return early.
+ if (aFragment.IsEmpty()) {
+ return;
+ }
+
+ // check if there is a JS caller, if not, then we can can return early here
+ // because we only care about calls to the fragment parser (e.g. innerHTML)
+ // originating from JS code.
+ nsAutoString filename;
+ uint32_t lineNum = 0;
+ uint32_t columnNum = 1;
+ JSContext* cx = nsContentUtils::GetCurrentJSContext();
+ if (!cx ||
+ !nsJSUtils::GetCallingLocation(cx, filename, &lineNum, &columnNum)) {
+ return;
+ }
+
+ // check if we should skip assertion. Please only ever set this pref to
+ // true if really needed for testing purposes.
+ if (mozilla::StaticPrefs::dom_security_skip_html_fragment_assertion()) {
+ return;
+ }
+
+ /*
+ * WARNING: Do not add any new entries to the htmlFragmentAllowlist
+ * without proper review from a dom:security peer!
+ */
+ static nsLiteralCString htmlFragmentAllowlist[] = {
+ "chrome://global/content/elements/marquee.js"_ns,
+ nsLiteralCString(
+ "chrome://pocket/content/panels/js/vendor/jquery-2.1.1.min.js"),
+ nsLiteralCString("chrome://devtools/content/shared/sourceeditor/"
+ "codemirror/codemirror.bundle.js"),
+ nsLiteralCString(
+ "resource://activity-stream/data/content/activity-stream.bundle.js"),
+ nsLiteralCString("resource://devtools/client/debugger/src/components/"
+ "Editor/Breakpoint.js"),
+ nsLiteralCString("resource://devtools/client/debugger/src/components/"
+ "Editor/ColumnBreakpoint.js"),
+ nsLiteralCString(
+ "resource://devtools/client/shared/vendor/fluent-react.js"),
+ "resource://devtools/client/shared/vendor/react-dom.js"_ns,
+ nsLiteralCString(
+ "resource://devtools/client/shared/vendor/react-dom-dev.js"),
+ nsLiteralCString(
+ "resource://devtools/client/shared/widgets/FilterWidget.js"),
+ nsLiteralCString("resource://devtools/client/shared/widgets/tooltip/"
+ "inactive-css-tooltip-helper.js"),
+ "resource://devtools/client/shared/widgets/Spectrum.js"_ns,
+ "resource://gre/modules/narrate/VoiceSelect.sys.mjs"_ns,
+ "resource://normandy-vendor/ReactDOM.js"_ns,
+ // ------------------------------------------------------------------
+ // test pages
+ // ------------------------------------------------------------------
+ "chrome://mochikit/content/browser-harness.xhtml"_ns,
+ "chrome://mochikit/content/harness.xhtml"_ns,
+ "chrome://mochikit/content/tests/"_ns,
+ "chrome://mochitests/content/"_ns,
+ "chrome://reftest/content/"_ns,
+ };
+
+ for (const nsLiteralCString& allowlistEntry : htmlFragmentAllowlist) {
+ if (StringBeginsWith(NS_ConvertUTF16toUTF8(filename), allowlistEntry)) {
+ return;
+ }
+ }
+
+ nsAutoCString uriSpec;
+ aPrincipal->GetAsciiSpec(uriSpec);
+
+ // Ideally we should not call the fragment parser (e.g. innerHTML()) in
+ // chrome: code or any of our about: pages. If you hit that assertion,
+ // please do *not* add your filename to the allowlist above, but rather
+ // refactor your code.
+ fprintf(stderr,
+ "Do not call the fragment parser (e.g innerHTML()) in chrome code "
+ "or in about: pages, (uri: %s), (caller: %s, line: %d, col: %d), "
+ "(fragment: %s)",
+ uriSpec.get(), NS_ConvertUTF16toUTF8(filename).get(), lineNum,
+ columnNum, NS_ConvertUTF16toUTF8(aFragment).get());
+
+ xpc_DumpJSStack(true, true, false);
+ MOZ_ASSERT(false);
+}
+
+/* static */
+void DOMSecurityMonitor::AuditUseOfJavaScriptURI(nsIChannel* aChannel) {
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ nsCOMPtr<nsIPrincipal> loadingPrincipal = loadInfo->GetLoadingPrincipal();
+
+ // We only ever have no loadingPrincipal in case of a new top-level load.
+ // The purpose of this assertion is to make sure we do not allow loading
+ // javascript: URIs in system privileged contexts. Hence there is nothing
+ // to do here in case there is no loadingPrincipal.
+ if (!loadingPrincipal) {
+ return;
+ }
+
+ // if the javascript: URI is not loaded by a system privileged context
+ // or an about: page, there there is nothing to do here.
+ if (!loadingPrincipal->IsSystemPrincipal() &&
+ !loadingPrincipal->SchemeIs("about")) {
+ return;
+ }
+
+ MOZ_ASSERT(false,
+ "Do not use javascript: URIs in chrome code or in about: pages");
+}
diff --git a/dom/security/DOMSecurityMonitor.h b/dom/security/DOMSecurityMonitor.h
new file mode 100644
index 0000000000..457e8a143a
--- /dev/null
+++ b/dom/security/DOMSecurityMonitor.h
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_DOMSecurityMonitor_h
+#define mozilla_dom_DOMSecurityMonitor_h
+
+#include "nsStringFwd.h"
+
+class nsIChannel;
+class nsIPrincipal;
+
+class DOMSecurityMonitor final {
+ public:
+ /* The fragment parser is triggered anytime JS calls innerHTML or similar
+ * JS functions which can generate HTML fragments. This generation of
+ * HTML might be dangerous, hence we should ensure that no new instances
+ * of innerHTML and similar functions are introduced in system privileged
+ * contexts, or also about: pages, in our codebase.
+ *
+ * If the auditor detects a new instance of innerHTML or similar
+ * function it will CRASH using a strong assertion.
+ */
+ static void AuditParsingOfHTMLXMLFragments(nsIPrincipal* aPrincipal,
+ const nsAString& aFragment);
+
+ /* The use of javascript: URIs in system privileged contexts or
+ * also about: pages is considered unsafe and discouraged.
+ *
+ * If the auditor detects a javascript: URI in a privileged
+ * context it will CRASH using a strong assertion.
+ *
+ */
+ static void AuditUseOfJavaScriptURI(nsIChannel* aChannel);
+
+ private:
+ DOMSecurityMonitor() = default;
+ ~DOMSecurityMonitor() = default;
+};
+
+#endif /* mozilla_dom_DOMSecurityMonitor_h */
diff --git a/dom/security/FramingChecker.cpp b/dom/security/FramingChecker.cpp
new file mode 100644
index 0000000000..ecd7a6863e
--- /dev/null
+++ b/dom/security/FramingChecker.cpp
@@ -0,0 +1,237 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FramingChecker.h"
+
+#include <stdint.h> // uint32_t
+
+#include "nsCOMPtr.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsHttpChannel.h"
+#include "nsContentSecurityUtils.h"
+#include "nsGlobalWindowOuter.h"
+#include "nsIContentPolicy.h"
+#include "nsIScriptError.h"
+#include "nsLiteralString.h"
+#include "nsTArray.h"
+#include "nsStringFwd.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/dom/WindowGlobalParent.h"
+#include "mozilla/net/HttpBaseChannel.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Services.h"
+#include "mozilla/Unused.h"
+
+#include "nsIObserverService.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+/* static */
+void FramingChecker::ReportError(const char* aMessageTag,
+ nsIHttpChannel* aChannel, nsIURI* aURI,
+ const nsAString& aPolicy) {
+ MOZ_ASSERT(aChannel);
+ MOZ_ASSERT(aURI);
+
+ nsCOMPtr<net::HttpBaseChannel> httpChannel = do_QueryInterface(aChannel);
+ if (!httpChannel) {
+ return;
+ }
+
+ // Get the URL spec
+ nsAutoCString spec;
+ nsresult rv = aURI->GetAsciiSpec(spec);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ nsTArray<nsString> params;
+ params.AppendElement(aPolicy);
+ params.AppendElement(NS_ConvertUTF8toUTF16(spec));
+
+ httpChannel->AddConsoleReport(nsIScriptError::errorFlag, "X-Frame-Options"_ns,
+ nsContentUtils::eSECURITY_PROPERTIES, spec, 0,
+ 0, nsDependentCString(aMessageTag), params);
+
+ // we are notifying observers for testing purposes because there is no event
+ // to gather that an iframe load was blocked or not.
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ nsAutoString policy(aPolicy);
+ observerService->NotifyObservers(aURI, "xfo-on-violate-policy", policy.get());
+}
+
+// Ignore x-frame-options if CSP with frame-ancestors exists
+static bool ShouldIgnoreFrameOptions(nsIChannel* aChannel,
+ nsIContentSecurityPolicy* aCSP) {
+ NS_ENSURE_TRUE(aChannel, false);
+ if (!aCSP) {
+ return false;
+ }
+
+ bool enforcesFrameAncestors = false;
+ aCSP->GetEnforcesFrameAncestors(&enforcesFrameAncestors);
+ if (!enforcesFrameAncestors) {
+ // if CSP does not contain frame-ancestors, then there
+ // is nothing to do here.
+ return false;
+ }
+
+ return true;
+}
+
+// Check if X-Frame-Options permits this document to be loaded as a
+// subdocument. This will iterate through and check any number of
+// X-Frame-Options policies in the request (comma-separated in a header,
+// multiple headers, etc).
+// This is based on:
+// https://html.spec.whatwg.org/multipage/document-lifecycle.html#the-x-frame-options-header
+/* static */
+bool FramingChecker::CheckFrameOptions(nsIChannel* aChannel,
+ nsIContentSecurityPolicy* aCsp,
+ bool& outIsFrameCheckingSkipped) {
+ // Step 1. If navigable is not a child navigable return true
+ if (!aChannel) {
+ return true;
+ }
+
+ // xfo check only makes sense for subdocument and object loads, if this is
+ // not a load of such type, there is nothing to do here.
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ ExtContentPolicyType contentType = loadInfo->GetExternalContentPolicyType();
+ if (contentType != ExtContentPolicy::TYPE_SUBDOCUMENT &&
+ contentType != ExtContentPolicy::TYPE_OBJECT) {
+ return true;
+ }
+
+ // xfo can only hang off an httpchannel, if this is not an httpChannel
+ // then there is nothing to do here.
+ nsCOMPtr<nsIHttpChannel> httpChannel;
+ nsresult rv = nsContentSecurityUtils::GetHttpChannelFromPotentialMultiPart(
+ aChannel, getter_AddRefs(httpChannel));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return true;
+ }
+ if (!httpChannel) {
+ return true;
+ }
+
+ // ignore XFO checks on channels that will be redirected
+ uint32_t responseStatus;
+ rv = httpChannel->GetResponseStatus(&responseStatus);
+ if (NS_FAILED(rv)) {
+ // GetResponseStatus returning failure is expected in several situations, so
+ // do not warn if it fails.
+ return true;
+ }
+ if (mozilla::net::nsHttpChannel::IsRedirectStatus(responseStatus)) {
+ return true;
+ }
+
+ nsAutoCString xfoHeaderValue;
+ Unused << httpChannel->GetResponseHeader("X-Frame-Options"_ns,
+ xfoHeaderValue);
+
+ // Step 10. (paritally) if the only header we received was empty, then we
+ // process it as if it wasn't sent at all.
+ if (xfoHeaderValue.IsEmpty()) {
+ return true;
+ }
+
+ // Step 2. xfo checks are ignored in the case where CSP frame-ancestors is
+ // present, if so, there is nothing to do here.
+ if (ShouldIgnoreFrameOptions(aChannel, aCsp)) {
+ outIsFrameCheckingSkipped = true;
+ return true;
+ }
+
+ // Step 3-4. reduce the header options to a unique set and count how many
+ // unique values (that we track) are encountered. this avoids using a set to
+ // stop attackers from inheriting arbitrary values in memory and reduce the
+ // complexity of the code.
+ XFOHeader xfoOptions;
+ for (const nsACString& next : xfoHeaderValue.Split(',')) {
+ nsAutoCString option(next);
+ option.StripWhitespace();
+
+ if (option.LowerCaseEqualsLiteral("allowall")) {
+ xfoOptions.ALLOWALL = true;
+ } else if (option.LowerCaseEqualsLiteral("sameorigin")) {
+ xfoOptions.SAMEORIGIN = true;
+ } else if (option.LowerCaseEqualsLiteral("deny")) {
+ xfoOptions.DENY = true;
+ } else {
+ xfoOptions.INVALID = true;
+ }
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ httpChannel->GetURI(getter_AddRefs(uri));
+
+ // Step 6. if header has multiple contradicting directives return early and
+ // prohibit the load. ALLOWALL is considered here for legacy reasons.
+ uint32_t xfoUniqueOptions = xfoOptions.DENY + xfoOptions.ALLOWALL +
+ xfoOptions.SAMEORIGIN + xfoOptions.INVALID;
+ if (xfoUniqueOptions > 1 &&
+ (xfoOptions.DENY || xfoOptions.ALLOWALL || xfoOptions.SAMEORIGIN)) {
+ ReportError("XFrameOptionsInvalid", httpChannel, uri, u"invalid"_ns);
+ return false;
+ }
+
+ // Step 7 (multiple INVALID values) and partially Step 10 (single INVALID
+ // value). if header has any invalid options, but no valid directives (DENY,
+ // ALLOWALL, SAMEORIGIN) then allow the load.
+ if (xfoOptions.INVALID) {
+ ReportError("XFrameOptionsInvalid", httpChannel, uri, u"invalid"_ns);
+ return true;
+ }
+
+ // Step 8. if the value of the header is DENY prohibit the load.
+ if (xfoOptions.DENY) {
+ ReportError("XFrameOptionsDeny", httpChannel, uri, u"deny"_ns);
+ return false;
+ }
+
+ // Step 9. If the X-Frame-Options value is SAMEORIGIN, then the top frame in
+ // the parent chain must be from the same origin as this document.
+ RefPtr<mozilla::dom::BrowsingContext> ctx;
+ loadInfo->GetBrowsingContext(getter_AddRefs(ctx));
+
+ while (ctx && xfoOptions.SAMEORIGIN) {
+ nsCOMPtr<nsIPrincipal> principal;
+ // Generally CheckFrameOptions is consulted from within the
+ // DocumentLoadListener in the parent process. For loads of type object and
+ // embed it's called from the Document in the content process.
+ if (XRE_IsParentProcess()) {
+ WindowGlobalParent* window = ctx->Canonical()->GetCurrentWindowGlobal();
+ if (window) {
+ // Using the URI of the Principal and not the document because
+ // window.open inherits the principal and hence the URI of the opening
+ // context needed for same origin checks.
+ principal = window->DocumentPrincipal();
+ }
+ } else if (nsPIDOMWindowOuter* windowOuter = ctx->GetDOMWindow()) {
+ principal = nsGlobalWindowOuter::Cast(windowOuter)->GetPrincipal();
+ }
+
+ if (principal && principal->IsSystemPrincipal()) {
+ return true;
+ }
+
+ // one of the ancestors is not same origin as this document
+ if (!principal || !principal->IsSameOrigin(uri)) {
+ ReportError("XFrameOptionsDeny", httpChannel, uri, u"sameorigin"_ns);
+ return false;
+ }
+ ctx = ctx->GetParent();
+ }
+
+ // Step 10.
+ return true;
+}
diff --git a/dom/security/FramingChecker.h b/dom/security/FramingChecker.h
new file mode 100644
index 0000000000..a91c353072
--- /dev/null
+++ b/dom/security/FramingChecker.h
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_FramingChecker_h
+#define mozilla_dom_FramingChecker_h
+
+#include "nsStringFwd.h"
+
+class nsIDocShell;
+class nsIChannel;
+class nsIHttpChannel;
+class nsIDocShellTreeItem;
+class nsIURI;
+class nsIContentSecurityPolicy;
+
+namespace mozilla::dom {
+class BrowsingContext;
+} // namespace mozilla::dom
+
+class FramingChecker {
+ public:
+ // Determine if X-Frame-Options allows content to be framed
+ // as a subdocument
+ static bool CheckFrameOptions(nsIChannel* aChannel,
+ nsIContentSecurityPolicy* aCSP,
+ bool& outIsFrameCheckingSkipped);
+
+ protected:
+ struct XFOHeader {
+ bool ALLOWALL = false;
+ bool SAMEORIGIN = false;
+ bool DENY = false;
+ bool INVALID = false;
+ };
+
+ /**
+ * Logs to the window about a X-Frame-Options error.
+ *
+ * @param aMessageTag the error message identifier to log
+ * @param aChannel the HTTP Channel
+ * @param aURI the URI of the frame attempting to load
+ * @param aPolicy the header value string from the frame to the console.
+ */
+ static void ReportError(const char* aMessageTag, nsIHttpChannel* aChannel,
+ nsIURI* aURI, const nsAString& aPolicy);
+};
+
+#endif /* mozilla_dom_FramingChecker_h */
diff --git a/dom/security/PolicyTokenizer.cpp b/dom/security/PolicyTokenizer.cpp
new file mode 100644
index 0000000000..8e28e6ce32
--- /dev/null
+++ b/dom/security/PolicyTokenizer.cpp
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "PolicyTokenizer.h"
+
+#include "mozilla/Logging.h"
+
+static mozilla::LogModule* GetPolicyTokenizerLog() {
+ static mozilla::LazyLogModule gPolicyTokenizerPRLog("PolicyTokenizer");
+ return gPolicyTokenizerPRLog;
+}
+
+#define POLICYTOKENIZERLOG(args) \
+ MOZ_LOG(GetPolicyTokenizerLog(), mozilla::LogLevel::Debug, args)
+
+static const char16_t SEMICOL = ';';
+
+PolicyTokenizer::PolicyTokenizer(const char16_t* aStart, const char16_t* aEnd)
+ : mCurChar(aStart), mEndChar(aEnd) {
+ POLICYTOKENIZERLOG(("PolicyTokenizer::PolicyTokenizer"));
+}
+
+PolicyTokenizer::~PolicyTokenizer() {
+ POLICYTOKENIZERLOG(("PolicyTokenizer::~PolicyTokenizer"));
+}
+
+void PolicyTokenizer::generateNextToken() {
+ skipWhiteSpaceAndSemicolon();
+ MOZ_ASSERT(mCurToken.Length() == 0);
+ const char16_t* const start = mCurChar;
+ while (!atEnd() && !nsContentUtils::IsHTMLWhitespace(*mCurChar) &&
+ *mCurChar != SEMICOL) {
+ mCurChar++;
+ }
+ if (start != mCurChar) {
+ mCurToken.Append(start, mCurChar - start);
+ }
+ POLICYTOKENIZERLOG(("PolicyTokenizer::generateNextToken: %s",
+ NS_ConvertUTF16toUTF8(mCurToken).get()));
+}
+
+void PolicyTokenizer::generateTokens(policyTokens& outTokens) {
+ POLICYTOKENIZERLOG(("PolicyTokenizer::generateTokens"));
+
+ // dirAndSrcs holds one set of [ name, src, src, src, ... ]
+ nsTArray<nsString> dirAndSrcs;
+
+ while (!atEnd()) {
+ generateNextToken();
+ dirAndSrcs.AppendElement(mCurToken);
+ skipWhiteSpace();
+ if (atEnd() || accept(SEMICOL)) {
+ outTokens.AppendElement(std::move(dirAndSrcs));
+ dirAndSrcs.ClearAndRetainStorage();
+ }
+ }
+}
+
+void PolicyTokenizer::tokenizePolicy(const nsAString& aPolicyString,
+ policyTokens& outTokens) {
+ POLICYTOKENIZERLOG(("PolicyTokenizer::tokenizePolicy"));
+
+ PolicyTokenizer tokenizer(aPolicyString.BeginReading(),
+ aPolicyString.EndReading());
+
+ tokenizer.generateTokens(outTokens);
+}
diff --git a/dom/security/PolicyTokenizer.h b/dom/security/PolicyTokenizer.h
new file mode 100644
index 0000000000..1c3c18175d
--- /dev/null
+++ b/dom/security/PolicyTokenizer.h
@@ -0,0 +1,78 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef PolicyTokenizer_h___
+#define PolicyTokenizer_h___
+
+#include "nsContentUtils.h"
+#include "nsString.h"
+
+/**
+ * How does the parsing work?
+ *
+ * We generate tokens by splitting the policy-string by whitespace and
+ * semicolon. Interally the tokens are represented as an array of string-arrays:
+ *
+ * [
+ * [ name, src, src, src, ... ],
+ * [ name, src, src, src, ... ],
+ * [ name, src, src, src, ... ]
+ * ]
+ *
+ * for example:
+ * [
+ * [ img-src, http://www.example.com, http:www.test.com ],
+ * [ default-src, 'self'],
+ * [ script-src, 'unsafe-eval', 'unsafe-inline' ],
+ * ]
+ */
+
+using policyTokens = nsTArray<CopyableTArray<nsString>>;
+
+class PolicyTokenizer {
+ public:
+ static void tokenizePolicy(const nsAString& aPolicyString,
+ policyTokens& outTokens);
+
+ private:
+ PolicyTokenizer(const char16_t* aStart, const char16_t* aEnd);
+ ~PolicyTokenizer();
+
+ inline bool atEnd() { return mCurChar >= mEndChar; }
+
+ inline void skipWhiteSpace() {
+ while (mCurChar < mEndChar && nsContentUtils::IsHTMLWhitespace(*mCurChar)) {
+ mCurChar++;
+ }
+ mCurToken.Truncate();
+ }
+
+ inline void skipWhiteSpaceAndSemicolon() {
+ while (mCurChar < mEndChar &&
+ (*mCurChar == ';' || nsContentUtils::IsHTMLWhitespace(*mCurChar))) {
+ mCurChar++;
+ }
+ mCurToken.Truncate();
+ }
+
+ inline bool accept(char16_t aChar) {
+ NS_ASSERTION(mCurChar < mEndChar, "Trying to dereference mEndChar");
+ if (*mCurChar == aChar) {
+ mCurToken.Append(*mCurChar++);
+ return true;
+ }
+ return false;
+ }
+
+ void generateNextToken();
+ void generateTokens(policyTokens& outTokens);
+
+ const char16_t* mCurChar;
+ const char16_t* mEndChar;
+ nsString mCurToken;
+};
+
+#endif /* PolicyTokenizer_h___ */
diff --git a/dom/security/ReferrerInfo.cpp b/dom/security/ReferrerInfo.cpp
new file mode 100644
index 0000000000..70cdddb8ab
--- /dev/null
+++ b/dom/security/ReferrerInfo.cpp
@@ -0,0 +1,1729 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/ReferrerPolicyBinding.h"
+#include "nsIClassInfoImpl.h"
+#include "nsIEffectiveTLDService.h"
+#include "nsIHttpChannel.h"
+#include "nsIObjectInputStream.h"
+#include "nsIObjectOutputStream.h"
+#include "nsIOService.h"
+#include "nsIPipe.h"
+#include "nsIURL.h"
+
+#include "nsWhitespaceTokenizer.h"
+#include "nsAlgorithm.h"
+#include "nsContentUtils.h"
+#include "nsCharSeparatedTokenizer.h"
+#include "nsScriptSecurityManager.h"
+#include "nsStreamUtils.h"
+#include "ReferrerInfo.h"
+
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/ContentBlockingAllowList.h"
+#include "mozilla/net/CookieJarSettings.h"
+#include "mozilla/net/HttpBaseChannel.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/RequestBinding.h"
+#include "mozilla/StaticPrefs_network.h"
+#include "mozilla/StorageAccess.h"
+#include "mozilla/StyleSheet.h"
+#include "mozilla/Telemetry.h"
+#include "nsIWebProgressListener.h"
+
+static mozilla::LazyLogModule gReferrerInfoLog("ReferrerInfo");
+#define LOG(msg) MOZ_LOG(gReferrerInfoLog, mozilla::LogLevel::Debug, msg)
+#define LOG_ENABLED() MOZ_LOG_TEST(gReferrerInfoLog, mozilla::LogLevel::Debug)
+
+using namespace mozilla::net;
+
+namespace mozilla::dom {
+
+// Implementation of ClassInfo is required to serialize/deserialize
+NS_IMPL_CLASSINFO(ReferrerInfo, nullptr, nsIClassInfo::THREADSAFE,
+ REFERRERINFO_CID)
+
+NS_IMPL_ISUPPORTS_CI(ReferrerInfo, nsIReferrerInfo, nsISerializable)
+
+#define MAX_REFERRER_SENDING_POLICY 2
+#define MAX_CROSS_ORIGIN_SENDING_POLICY 2
+#define MAX_TRIMMING_POLICY 2
+
+#define MIN_REFERRER_SENDING_POLICY 0
+#define MIN_CROSS_ORIGIN_SENDING_POLICY 0
+#define MIN_TRIMMING_POLICY 0
+
+/*
+ * Default referrer policy to use
+ */
+enum DefaultReferrerPolicy : uint32_t {
+ eDefaultPolicyNoReferrer = 0,
+ eDefaultPolicySameOrgin = 1,
+ eDefaultPolicyStrictWhenXorigin = 2,
+ eDefaultPolicyNoReferrerWhenDownGrade = 3,
+};
+
+static uint32_t GetDefaultFirstPartyReferrerPolicyPref(bool aPrivateBrowsing) {
+ return aPrivateBrowsing
+ ? StaticPrefs::network_http_referer_defaultPolicy_pbmode()
+ : StaticPrefs::network_http_referer_defaultPolicy();
+}
+
+static uint32_t GetDefaultThirdPartyReferrerPolicyPref(bool aPrivateBrowsing) {
+ return aPrivateBrowsing
+ ? StaticPrefs::network_http_referer_defaultPolicy_trackers_pbmode()
+ : StaticPrefs::network_http_referer_defaultPolicy_trackers();
+}
+
+static ReferrerPolicy DefaultReferrerPolicyToReferrerPolicy(
+ uint32_t aDefaultToUse) {
+ switch (aDefaultToUse) {
+ case DefaultReferrerPolicy::eDefaultPolicyNoReferrer:
+ return ReferrerPolicy::No_referrer;
+ case DefaultReferrerPolicy::eDefaultPolicySameOrgin:
+ return ReferrerPolicy::Same_origin;
+ case DefaultReferrerPolicy::eDefaultPolicyStrictWhenXorigin:
+ return ReferrerPolicy::Strict_origin_when_cross_origin;
+ }
+
+ return ReferrerPolicy::No_referrer_when_downgrade;
+}
+
+struct LegacyReferrerPolicyTokenMap {
+ const char* mToken;
+ ReferrerPolicy mPolicy;
+};
+
+/*
+ * Parse ReferrerPolicy from token.
+ * The supported tokens are defined in ReferrerPolicy.webidl.
+ * The legacy tokens are "never", "default", "always" and
+ * "origin-when-crossorigin". The legacy tokens are only supported in meta
+ * referrer content
+ *
+ * @param aContent content string to be transformed into
+ * ReferrerPolicyEnum, e.g. "origin".
+ */
+ReferrerPolicy ReferrerPolicyFromToken(const nsAString& aContent,
+ bool allowedLegacyToken) {
+ nsString lowerContent(aContent);
+ ToLowerCase(lowerContent);
+
+ if (allowedLegacyToken) {
+ static const LegacyReferrerPolicyTokenMap sLegacyReferrerPolicyToken[] = {
+ {"never", ReferrerPolicy::No_referrer},
+ {"default", ReferrerPolicy::No_referrer_when_downgrade},
+ {"always", ReferrerPolicy::Unsafe_url},
+ {"origin-when-crossorigin", ReferrerPolicy::Origin_when_cross_origin},
+ };
+
+ uint8_t numStr = (sizeof(sLegacyReferrerPolicyToken) /
+ sizeof(sLegacyReferrerPolicyToken[0]));
+ for (uint8_t i = 0; i < numStr; i++) {
+ if (lowerContent.EqualsASCII(sLegacyReferrerPolicyToken[i].mToken)) {
+ return sLegacyReferrerPolicyToken[i].mPolicy;
+ }
+ }
+ }
+
+ // Supported tokes - ReferrerPolicyValues, are generated from
+ // ReferrerPolicy.webidl
+ for (uint8_t i = 0; ReferrerPolicyValues::strings[i].value; i++) {
+ if (lowerContent.EqualsASCII(ReferrerPolicyValues::strings[i].value)) {
+ return static_cast<enum ReferrerPolicy>(i);
+ }
+ }
+
+ // Return no referrer policy (empty string) if none of the previous match
+ return ReferrerPolicy::_empty;
+}
+
+// static
+ReferrerPolicy ReferrerInfo::ReferrerPolicyFromMetaString(
+ const nsAString& aContent) {
+ // This is implemented as described in
+ // https://html.spec.whatwg.org/multipage/semantics.html#meta-referrer
+ // Meta referrer accepts both supported tokens in ReferrerPolicy.webidl and
+ // legacy tokens.
+ return ReferrerPolicyFromToken(aContent, true);
+}
+
+// static
+ReferrerPolicy ReferrerInfo::ReferrerPolicyAttributeFromString(
+ const nsAString& aContent) {
+ // This is implemented as described in
+ // https://html.spec.whatwg.org/multipage/infrastructure.html#referrer-policy-attribute
+ // referrerpolicy attribute only accepts supported tokens in
+ // ReferrerPolicy.webidl
+ return ReferrerPolicyFromToken(aContent, false);
+}
+
+// static
+ReferrerPolicy ReferrerInfo::ReferrerPolicyFromHeaderString(
+ const nsAString& aContent) {
+ // Multiple headers could be concatenated into one comma-separated
+ // list of policies. Need to tokenize the multiple headers.
+ ReferrerPolicyEnum referrerPolicy = ReferrerPolicy::_empty;
+ for (const auto& token : nsCharSeparatedTokenizer(aContent, ',').ToRange()) {
+ if (token.IsEmpty()) {
+ continue;
+ }
+
+ // Referrer-Policy header only accepts supported tokens in
+ // ReferrerPolicy.webidl
+ ReferrerPolicyEnum policy = ReferrerPolicyFromToken(token, false);
+ // If there are multiple policies available, the last valid policy should be
+ // used.
+ // https://w3c.github.io/webappsec-referrer-policy/#unknown-policy-values
+ if (policy != ReferrerPolicy::_empty) {
+ referrerPolicy = policy;
+ }
+ }
+ return referrerPolicy;
+}
+
+// static
+const char* ReferrerInfo::ReferrerPolicyToString(ReferrerPolicyEnum aPolicy) {
+ uint8_t index = static_cast<uint8_t>(aPolicy);
+ uint8_t referrerPolicyCount = ArrayLength(ReferrerPolicyValues::strings);
+ MOZ_ASSERT(index < referrerPolicyCount);
+ if (index >= referrerPolicyCount) {
+ return "";
+ }
+
+ return ReferrerPolicyValues::strings[index].value;
+}
+
+/* static */
+uint32_t ReferrerInfo::GetUserReferrerSendingPolicy() {
+ return clamped<uint32_t>(
+ StaticPrefs::network_http_sendRefererHeader_DoNotUseDirectly(),
+ MIN_REFERRER_SENDING_POLICY, MAX_REFERRER_SENDING_POLICY);
+}
+
+/* static */
+uint32_t ReferrerInfo::GetUserXOriginSendingPolicy() {
+ return clamped<uint32_t>(
+ StaticPrefs::network_http_referer_XOriginPolicy_DoNotUseDirectly(),
+ MIN_CROSS_ORIGIN_SENDING_POLICY, MAX_CROSS_ORIGIN_SENDING_POLICY);
+}
+
+/* static */
+uint32_t ReferrerInfo::GetUserTrimmingPolicy() {
+ return clamped<uint32_t>(
+ StaticPrefs::network_http_referer_trimmingPolicy_DoNotUseDirectly(),
+ MIN_TRIMMING_POLICY, MAX_TRIMMING_POLICY);
+}
+
+/* static */
+uint32_t ReferrerInfo::GetUserXOriginTrimmingPolicy() {
+ return clamped<uint32_t>(
+ StaticPrefs::
+ network_http_referer_XOriginTrimmingPolicy_DoNotUseDirectly(),
+ MIN_TRIMMING_POLICY, MAX_TRIMMING_POLICY);
+}
+
+/* static */
+ReferrerPolicy ReferrerInfo::GetDefaultReferrerPolicy(nsIHttpChannel* aChannel,
+ nsIURI* aURI,
+ bool aPrivateBrowsing) {
+ bool thirdPartyTrackerIsolated = false;
+ if (aChannel && aURI) {
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ nsCOMPtr<nsICookieJarSettings> cjs;
+ Unused << loadInfo->GetCookieJarSettings(getter_AddRefs(cjs));
+ if (!cjs) {
+ bool shouldResistFingerprinting =
+ nsContentUtils::ShouldResistFingerprinting(
+ aChannel, RFPTarget::IsAlwaysEnabledForPrecompute);
+ cjs = aPrivateBrowsing
+ ? net::CookieJarSettings::Create(CookieJarSettings::ePrivate,
+ shouldResistFingerprinting)
+ : net::CookieJarSettings::Create(CookieJarSettings::eRegular,
+ shouldResistFingerprinting);
+ }
+
+ // We only check if the channel is isolated if it's in the parent process
+ // with the rejection of third party contexts is enabled. We don't need to
+ // check this in content processes since the tracking state of the channel
+ // is unknown here and the referrer policy would be updated when the channel
+ // starts connecting in the parent process.
+ if (XRE_IsParentProcess() && cjs->GetRejectThirdPartyContexts()) {
+ uint32_t rejectedReason = 0;
+ thirdPartyTrackerIsolated =
+ !ShouldAllowAccessFor(aChannel, aURI, &rejectedReason) &&
+ rejectedReason !=
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN);
+ // Here we intentionally do not notify about the rejection reason, if any
+ // in order to avoid this check to have any visible side-effects (e.g. a
+ // web console report.)
+ }
+ }
+
+ // Select the appropriate pref starting with
+ // "network.http.referer.defaultPolicy" to use based on private-browsing
+ // ("pbmode") AND third-party trackers ("trackers").
+ return DefaultReferrerPolicyToReferrerPolicy(
+ thirdPartyTrackerIsolated
+ ? GetDefaultThirdPartyReferrerPolicyPref(aPrivateBrowsing)
+ : GetDefaultFirstPartyReferrerPolicyPref(aPrivateBrowsing));
+}
+
+/* static */
+bool ReferrerInfo::IsReferrerSchemeAllowed(nsIURI* aReferrer) {
+ NS_ENSURE_TRUE(aReferrer, false);
+
+ nsAutoCString scheme;
+ nsresult rv = aReferrer->GetScheme(scheme);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ return scheme.EqualsIgnoreCase("https") || scheme.EqualsIgnoreCase("http");
+}
+
+/* static */
+bool ReferrerInfo::ShouldResponseInheritReferrerInfo(nsIChannel* aChannel) {
+ if (!aChannel) {
+ return false;
+ }
+
+ nsCOMPtr<nsIURI> channelURI;
+ nsresult rv = aChannel->GetURI(getter_AddRefs(channelURI));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ bool isAbout = channelURI->SchemeIs("about");
+ if (!isAbout) {
+ return false;
+ }
+
+ nsAutoCString aboutSpec;
+ rv = channelURI->GetSpec(aboutSpec);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ return aboutSpec.EqualsLiteral("about:srcdoc");
+}
+
+/* static */
+nsresult ReferrerInfo::HandleSecureToInsecureReferral(
+ nsIURI* aOriginalURI, nsIURI* aURI, ReferrerPolicyEnum aPolicy,
+ bool& aAllowed) {
+ NS_ENSURE_ARG(aOriginalURI);
+ NS_ENSURE_ARG(aURI);
+
+ aAllowed = false;
+
+ bool referrerIsHttpsScheme = aOriginalURI->SchemeIs("https");
+ if (!referrerIsHttpsScheme) {
+ aAllowed = true;
+ return NS_OK;
+ }
+
+ // It's ok to send referrer for https-to-http scenarios if the referrer
+ // policy is "unsafe-url", "origin", or "origin-when-cross-origin".
+ // in other referrer policies, https->http is not allowed...
+ bool uriIsHttpsScheme = aURI->SchemeIs("https");
+ if (aPolicy != ReferrerPolicy::Unsafe_url &&
+ aPolicy != ReferrerPolicy::Origin_when_cross_origin &&
+ aPolicy != ReferrerPolicy::Origin && !uriIsHttpsScheme) {
+ return NS_OK;
+ }
+
+ aAllowed = true;
+ return NS_OK;
+}
+
+nsresult ReferrerInfo::HandleUserXOriginSendingPolicy(nsIURI* aURI,
+ nsIURI* aReferrer,
+ bool& aAllowed) const {
+ NS_ENSURE_ARG(aURI);
+ aAllowed = false;
+
+ nsAutoCString uriHost;
+ nsAutoCString referrerHost;
+
+ nsresult rv = aURI->GetAsciiHost(uriHost);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = aReferrer->GetAsciiHost(referrerHost);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Send an empty referrer if xorigin and leaving a .onion domain.
+ if (StaticPrefs::network_http_referer_hideOnionSource() &&
+ !uriHost.Equals(referrerHost) &&
+ StringEndsWith(referrerHost, ".onion"_ns)) {
+ return NS_OK;
+ }
+
+ switch (GetUserXOriginSendingPolicy()) {
+ // Check policy for sending referrer only when hosts match
+ case XOriginSendingPolicy::ePolicySendWhenSameHost: {
+ if (!uriHost.Equals(referrerHost)) {
+ return NS_OK;
+ }
+ break;
+ }
+
+ case XOriginSendingPolicy::ePolicySendWhenSameDomain: {
+ nsCOMPtr<nsIEffectiveTLDService> eTLDService =
+ do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
+ if (!eTLDService) {
+ // check policy for sending only when effective top level domain
+ // matches. this falls back on using host if eTLDService does not work
+ if (!uriHost.Equals(referrerHost)) {
+ return NS_OK;
+ }
+ break;
+ }
+
+ nsAutoCString uriDomain;
+ nsAutoCString referrerDomain;
+ uint32_t extraDomains = 0;
+
+ rv = eTLDService->GetBaseDomain(aURI, extraDomains, uriDomain);
+ if (rv == NS_ERROR_HOST_IS_IP_ADDRESS ||
+ rv == NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
+ // uri is either an IP address, an alias such as 'localhost', an eTLD
+ // such as 'co.uk', or the empty string. Uses the normalized host in
+ // such cases.
+ rv = aURI->GetAsciiHost(uriDomain);
+ }
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = eTLDService->GetBaseDomain(aReferrer, extraDomains, referrerDomain);
+ if (rv == NS_ERROR_HOST_IS_IP_ADDRESS ||
+ rv == NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
+ // referrer is either an IP address, an alias such as 'localhost', an
+ // eTLD such as 'co.uk', or the empty string. Uses the normalized host
+ // in such cases.
+ rv = aReferrer->GetAsciiHost(referrerDomain);
+ }
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (!uriDomain.Equals(referrerDomain)) {
+ return NS_OK;
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ aAllowed = true;
+ return NS_OK;
+}
+
+// This roughly implements Step 3.1. of
+// https://fetch.spec.whatwg.org/#append-a-request-origin-header
+/* static */
+bool ReferrerInfo::ShouldSetNullOriginHeader(net::HttpBaseChannel* aChannel,
+ nsIURI* aOriginURI) {
+ MOZ_ASSERT(aChannel);
+ MOZ_ASSERT(aOriginURI);
+
+ // If request’s mode is not "cors", then switch on request’s referrer policy:
+ RequestMode requestMode = RequestMode::No_cors;
+ MOZ_ALWAYS_SUCCEEDS(aChannel->GetRequestMode(&requestMode));
+ if (requestMode == RequestMode::Cors) {
+ return false;
+ }
+
+ nsCOMPtr<nsIReferrerInfo> referrerInfo;
+ NS_ENSURE_SUCCESS(aChannel->GetReferrerInfo(getter_AddRefs(referrerInfo)),
+ false);
+ if (!referrerInfo) {
+ return false;
+ }
+
+ // "no-referrer":
+ enum ReferrerPolicy policy = referrerInfo->ReferrerPolicy();
+ if (policy == ReferrerPolicy::No_referrer) {
+ // Set serializedOrigin to `null`.
+ // Note: Returning true is the same as setting the serializedOrigin to null
+ // in this method.
+ return true;
+ }
+
+ // "no-referrer-when-downgrade":
+ // "strict-origin":
+ // "strict-origin-when-cross-origin":
+ // If request’s origin is a tuple origin, its scheme is "https", and
+ // request’s current URL’s scheme is not "https", then set serializedOrigin
+ // to `null`.
+ bool allowed = false;
+ nsCOMPtr<nsIURI> uri;
+ NS_ENSURE_SUCCESS(aChannel->GetURI(getter_AddRefs(uri)), false);
+ if (NS_SUCCEEDED(ReferrerInfo::HandleSecureToInsecureReferral(
+ aOriginURI, uri, policy, allowed)) &&
+ !allowed) {
+ return true;
+ }
+
+ // "same-origin":
+ if (policy == ReferrerPolicy::Same_origin) {
+ // If request’s origin is not same origin with request’s current URL’s
+ // origin, then set serializedOrigin to `null`.
+ return ReferrerInfo::IsCrossOriginRequest(aChannel);
+ }
+
+ // Otherwise:
+ // Do Nothing.
+ return false;
+}
+
+nsresult ReferrerInfo::HandleUserReferrerSendingPolicy(nsIHttpChannel* aChannel,
+ bool& aAllowed) const {
+ aAllowed = false;
+ uint32_t referrerSendingPolicy;
+ uint32_t loadFlags;
+ nsresult rv = aChannel->GetLoadFlags(&loadFlags);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (loadFlags & nsIHttpChannel::LOAD_INITIAL_DOCUMENT_URI) {
+ referrerSendingPolicy = ReferrerSendingPolicy::ePolicySendWhenUserTrigger;
+ } else {
+ referrerSendingPolicy = ReferrerSendingPolicy::ePolicySendInlineContent;
+ }
+ if (GetUserReferrerSendingPolicy() < referrerSendingPolicy) {
+ return NS_OK;
+ }
+
+ aAllowed = true;
+ return NS_OK;
+}
+
+/* static */
+bool ReferrerInfo::IsCrossOriginRequest(nsIHttpChannel* aChannel) {
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+
+ if (!loadInfo->TriggeringPrincipal()->GetIsContentPrincipal()) {
+ LOG(("no triggering URI via loadInfo, assuming load is cross-origin"));
+ return true;
+ }
+
+ if (LOG_ENABLED()) {
+ nsAutoCString triggeringURISpec;
+ loadInfo->TriggeringPrincipal()->GetAsciiSpec(triggeringURISpec);
+ LOG(("triggeringURI=%s\n", triggeringURISpec.get()));
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return true;
+ }
+
+ return !loadInfo->TriggeringPrincipal()->IsSameOrigin(uri);
+}
+
+/* static */
+bool ReferrerInfo::IsReferrerCrossOrigin(nsIHttpChannel* aChannel,
+ nsIURI* aReferrer) {
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+
+ if (!loadInfo->TriggeringPrincipal()->GetIsContentPrincipal()) {
+ LOG(("no triggering URI via loadInfo, assuming load is cross-site"));
+ return true;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return true;
+ }
+
+ return !nsScriptSecurityManager::SecurityCompareURIs(uri, aReferrer);
+}
+
+/* static */
+bool ReferrerInfo::IsCrossSiteRequest(nsIHttpChannel* aChannel) {
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+
+ if (!loadInfo->TriggeringPrincipal()->GetIsContentPrincipal()) {
+ LOG(("no triggering URI via loadInfo, assuming load is cross-site"));
+ return true;
+ }
+
+ if (LOG_ENABLED()) {
+ nsAutoCString triggeringURISpec;
+ loadInfo->TriggeringPrincipal()->GetAsciiSpec(triggeringURISpec);
+ LOG(("triggeringURI=%s\n", triggeringURISpec.get()));
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return true;
+ }
+
+ bool isCrossSite = true;
+ rv = loadInfo->TriggeringPrincipal()->IsThirdPartyURI(uri, &isCrossSite);
+ if (NS_FAILED(rv)) {
+ return true;
+ }
+
+ return isCrossSite;
+}
+
+ReferrerInfo::TrimmingPolicy ReferrerInfo::ComputeTrimmingPolicy(
+ nsIHttpChannel* aChannel, nsIURI* aReferrer) const {
+ uint32_t trimmingPolicy = GetUserTrimmingPolicy();
+
+ switch (mPolicy) {
+ case ReferrerPolicy::Origin:
+ case ReferrerPolicy::Strict_origin:
+ trimmingPolicy = TrimmingPolicy::ePolicySchemeHostPort;
+ break;
+
+ case ReferrerPolicy::Origin_when_cross_origin:
+ case ReferrerPolicy::Strict_origin_when_cross_origin:
+ if (trimmingPolicy != TrimmingPolicy::ePolicySchemeHostPort &&
+ IsReferrerCrossOrigin(aChannel, aReferrer)) {
+ // Ignore set trimmingPolicy if it is already the strictest
+ // policy.
+ trimmingPolicy = TrimmingPolicy::ePolicySchemeHostPort;
+ }
+ break;
+
+ // This function is called when a nonempty referrer value is allowed to
+ // send. For the next 3 policies: same-origin, no-referrer-when-downgrade,
+ // unsafe-url, without trimming we should have a full uri. And the trimming
+ // policy only depends on user prefs.
+ case ReferrerPolicy::Same_origin:
+ case ReferrerPolicy::No_referrer_when_downgrade:
+ case ReferrerPolicy::Unsafe_url:
+ if (trimmingPolicy != TrimmingPolicy::ePolicySchemeHostPort) {
+ // Ignore set trimmingPolicy if it is already the strictest
+ // policy. Apply the user cross-origin trimming policy if it's more
+ // restrictive than the general one.
+ if (GetUserXOriginTrimmingPolicy() != TrimmingPolicy::ePolicyFullURI &&
+ IsCrossOriginRequest(aChannel)) {
+ trimmingPolicy =
+ std::max(trimmingPolicy, GetUserXOriginTrimmingPolicy());
+ }
+ }
+ break;
+
+ case ReferrerPolicy::No_referrer:
+ case ReferrerPolicy::_empty:
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unexpected value");
+ break;
+ }
+
+ return static_cast<TrimmingPolicy>(trimmingPolicy);
+}
+
+nsresult ReferrerInfo::LimitReferrerLength(
+ nsIHttpChannel* aChannel, nsIURI* aReferrer, TrimmingPolicy aTrimmingPolicy,
+ nsACString& aInAndOutTrimmedReferrer) const {
+ if (!StaticPrefs::network_http_referer_referrerLengthLimit()) {
+ return NS_OK;
+ }
+
+ if (aInAndOutTrimmedReferrer.Length() <=
+ StaticPrefs::network_http_referer_referrerLengthLimit()) {
+ return NS_OK;
+ }
+
+ nsAutoString referrerLengthLimit;
+ referrerLengthLimit.AppendInt(
+ StaticPrefs::network_http_referer_referrerLengthLimit());
+ if (aTrimmingPolicy == ePolicyFullURI ||
+ aTrimmingPolicy == ePolicySchemeHostPortPath) {
+ // If referrer header is over max Length, down to origin
+ nsresult rv = GetOriginFromReferrerURI(aReferrer, aInAndOutTrimmedReferrer);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Step 6 within https://w3c.github.io/webappsec-referrer-policy/#strip-url
+ // states that the trailing "/" does not need to get stripped. However,
+ // GetOriginFromReferrerURI() also removes any trailing "/" hence we have to
+ // add it back here.
+ aInAndOutTrimmedReferrer.AppendLiteral("/");
+ if (aInAndOutTrimmedReferrer.Length() <=
+ StaticPrefs::network_http_referer_referrerLengthLimit()) {
+ AutoTArray<nsString, 2> params = {
+ referrerLengthLimit, NS_ConvertUTF8toUTF16(aInAndOutTrimmedReferrer)};
+ LogMessageToConsole(aChannel, "ReferrerLengthOverLimitation", params);
+ return NS_OK;
+ }
+ }
+
+ // If we end up here either the trimmingPolicy is equal to
+ // 'ePolicySchemeHostPort' or the 'origin' of any other policy is still over
+ // the length limit. If so, truncate the referrer entirely.
+ AutoTArray<nsString, 2> params = {
+ referrerLengthLimit, NS_ConvertUTF8toUTF16(aInAndOutTrimmedReferrer)};
+ LogMessageToConsole(aChannel, "ReferrerOriginLengthOverLimitation", params);
+ aInAndOutTrimmedReferrer.Truncate();
+
+ return NS_OK;
+}
+
+nsresult ReferrerInfo::GetOriginFromReferrerURI(nsIURI* aReferrer,
+ nsACString& aResult) const {
+ MOZ_ASSERT(aReferrer);
+ aResult.Truncate();
+ // We want the IDN-normalized PrePath. That's not something currently
+ // available and there doesn't yet seem to be justification for adding it to
+ // the interfaces, so just build it up from scheme+AsciiHostPort
+ nsAutoCString scheme, asciiHostPort;
+ nsresult rv = aReferrer->GetScheme(scheme);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ aResult = scheme;
+ aResult.AppendLiteral("://");
+ // Note we explicitly cleared UserPass above, so do not need to build it.
+ rv = aReferrer->GetAsciiHostPort(asciiHostPort);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ aResult.Append(asciiHostPort);
+ return NS_OK;
+}
+
+nsresult ReferrerInfo::TrimReferrerWithPolicy(nsIURI* aReferrer,
+ TrimmingPolicy aTrimmingPolicy,
+ nsACString& aResult) const {
+ MOZ_ASSERT(aReferrer);
+
+ if (aTrimmingPolicy == TrimmingPolicy::ePolicyFullURI) {
+ return aReferrer->GetAsciiSpec(aResult);
+ }
+
+ nsresult rv = GetOriginFromReferrerURI(aReferrer, aResult);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (aTrimmingPolicy == TrimmingPolicy::ePolicySchemeHostPortPath) {
+ nsCOMPtr<nsIURL> url(do_QueryInterface(aReferrer));
+ if (url) {
+ nsAutoCString path;
+ rv = url->GetFilePath(path);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ aResult.Append(path);
+ return NS_OK;
+ }
+ }
+
+ // Step 6 within https://w3c.github.io/webappsec-referrer-policy/#strip-url
+ // states that the trailing "/" does not need to get stripped. However,
+ // GetOriginFromReferrerURI() also removes any trailing "/" hence we have to
+ // add it back here.
+ aResult.AppendLiteral("/");
+ return NS_OK;
+}
+
+bool ReferrerInfo::ShouldIgnoreLessRestrictedPolicies(
+ nsIHttpChannel* aChannel, const ReferrerPolicyEnum aPolicy) const {
+ MOZ_ASSERT(aChannel);
+
+ // We only care about the less restricted policies.
+ if (aPolicy != ReferrerPolicy::Unsafe_url &&
+ aPolicy != ReferrerPolicy::No_referrer_when_downgrade &&
+ aPolicy != ReferrerPolicy::Origin_when_cross_origin) {
+ return false;
+ }
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ bool isPrivate = NS_UsePrivateBrowsing(aChannel);
+
+ // Return early if we don't want to ignore less restricted policies for the
+ // top navigation.
+ if (loadInfo->GetExternalContentPolicyType() ==
+ ExtContentPolicy::TYPE_DOCUMENT) {
+ bool isEnabledForTopNavigation =
+ isPrivate
+ ? StaticPrefs::
+ network_http_referer_disallowCrossSiteRelaxingDefault_pbmode_top_navigation()
+ : StaticPrefs::
+ network_http_referer_disallowCrossSiteRelaxingDefault_top_navigation();
+ if (!isEnabledForTopNavigation) {
+ return false;
+ }
+
+ // We have to get the value of the contentBlockingAllowList earlier because
+ // the channel hasn't been opened yet here. Note that we only need to do
+ // this for first-party navigation. For third-party loads, the value is
+ // inherited from the parent.
+ if (XRE_IsParentProcess()) {
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+ Unused << loadInfo->GetCookieJarSettings(
+ getter_AddRefs(cookieJarSettings));
+
+ net::CookieJarSettings::Cast(cookieJarSettings)
+ ->UpdateIsOnContentBlockingAllowList(aChannel);
+ }
+ }
+
+ // We don't ignore less restricted referrer policies if ETP is toggled off.
+ // This would affect iframe loads and top navigation. For iframes, it will
+ // stop ignoring if the first-party site toggled ETP off. For top navigation,
+ // it depends on the ETP toggle for the destination site.
+ if (ContentBlockingAllowList::Check(aChannel)) {
+ return false;
+ }
+
+ bool isCrossSite = IsCrossSiteRequest(aChannel);
+ bool isEnabled =
+ isPrivate
+ ? StaticPrefs::
+ network_http_referer_disallowCrossSiteRelaxingDefault_pbmode()
+ : StaticPrefs::
+ network_http_referer_disallowCrossSiteRelaxingDefault();
+
+ if (!isEnabled) {
+ // Log the warning message to console to inform that we will ignore
+ // less restricted policies for cross-site requests in the future.
+ if (isCrossSite) {
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ AutoTArray<nsString, 1> params = {
+ NS_ConvertUTF8toUTF16(uri->GetSpecOrDefault())};
+ LogMessageToConsole(aChannel, "ReferrerPolicyDisallowRelaxingWarning",
+ params);
+ }
+ return false;
+ }
+
+ // Check if the channel is triggered by the system or the extension.
+ auto* triggerBasePrincipal =
+ BasePrincipal::Cast(loadInfo->TriggeringPrincipal());
+ if (triggerBasePrincipal->IsSystemPrincipal() ||
+ triggerBasePrincipal->AddonPolicy()) {
+ return false;
+ }
+
+ if (isCrossSite) {
+ // Log the console message to say that the less restricted policy was
+ // ignored.
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, true);
+
+ uint32_t idx = static_cast<uint32_t>(aPolicy);
+
+ AutoTArray<nsString, 2> params = {
+ NS_ConvertUTF8toUTF16(
+ nsDependentCString(ReferrerPolicyValues::strings[idx].value)),
+ NS_ConvertUTF8toUTF16(uri->GetSpecOrDefault())};
+ LogMessageToConsole(aChannel, "ReferrerPolicyDisallowRelaxingMessage",
+ params);
+ }
+
+ return isCrossSite;
+}
+
+void ReferrerInfo::LogMessageToConsole(
+ nsIHttpChannel* aChannel, const char* aMsg,
+ const nsTArray<nsString>& aParams) const {
+ MOZ_ASSERT(aChannel);
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ uint64_t windowID = 0;
+
+ rv = aChannel->GetTopLevelContentWindowId(&windowID);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ if (!windowID) {
+ nsCOMPtr<nsILoadGroup> loadGroup;
+ rv = aChannel->GetLoadGroup(getter_AddRefs(loadGroup));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ if (loadGroup) {
+ windowID = nsContentUtils::GetInnerWindowID(loadGroup);
+ }
+ }
+
+ nsAutoString localizedMsg;
+ rv = nsContentUtils::FormatLocalizedString(
+ nsContentUtils::eSECURITY_PROPERTIES, aMsg, aParams, localizedMsg);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ rv = nsContentUtils::ReportToConsoleByWindowID(
+ localizedMsg, nsIScriptError::infoFlag, "Security"_ns, windowID, uri);
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+}
+
+ReferrerPolicy ReferrerPolicyIDLToReferrerPolicy(
+ nsIReferrerInfo::ReferrerPolicyIDL aReferrerPolicy) {
+ switch (aReferrerPolicy) {
+ case nsIReferrerInfo::EMPTY:
+ return ReferrerPolicy::_empty;
+ break;
+ case nsIReferrerInfo::NO_REFERRER:
+ return ReferrerPolicy::No_referrer;
+ break;
+ case nsIReferrerInfo::NO_REFERRER_WHEN_DOWNGRADE:
+ return ReferrerPolicy::No_referrer_when_downgrade;
+ break;
+ case nsIReferrerInfo::ORIGIN:
+ return ReferrerPolicy::Origin;
+ break;
+ case nsIReferrerInfo::ORIGIN_WHEN_CROSS_ORIGIN:
+ return ReferrerPolicy::Origin_when_cross_origin;
+ break;
+ case nsIReferrerInfo::UNSAFE_URL:
+ return ReferrerPolicy::Unsafe_url;
+ break;
+ case nsIReferrerInfo::SAME_ORIGIN:
+ return ReferrerPolicy::Same_origin;
+ break;
+ case nsIReferrerInfo::STRICT_ORIGIN:
+ return ReferrerPolicy::Strict_origin;
+ break;
+ case nsIReferrerInfo::STRICT_ORIGIN_WHEN_CROSS_ORIGIN:
+ return ReferrerPolicy::Strict_origin_when_cross_origin;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid ReferrerPolicy value");
+ break;
+ }
+
+ return ReferrerPolicy::_empty;
+}
+
+nsIReferrerInfo::ReferrerPolicyIDL ReferrerPolicyToReferrerPolicyIDL(
+ ReferrerPolicy aReferrerPolicy) {
+ switch (aReferrerPolicy) {
+ case ReferrerPolicy::_empty:
+ return nsIReferrerInfo::EMPTY;
+ break;
+ case ReferrerPolicy::No_referrer:
+ return nsIReferrerInfo::NO_REFERRER;
+ break;
+ case ReferrerPolicy::No_referrer_when_downgrade:
+ return nsIReferrerInfo::NO_REFERRER_WHEN_DOWNGRADE;
+ break;
+ case ReferrerPolicy::Origin:
+ return nsIReferrerInfo::ORIGIN;
+ break;
+ case ReferrerPolicy::Origin_when_cross_origin:
+ return nsIReferrerInfo::ORIGIN_WHEN_CROSS_ORIGIN;
+ break;
+ case ReferrerPolicy::Unsafe_url:
+ return nsIReferrerInfo::UNSAFE_URL;
+ break;
+ case ReferrerPolicy::Same_origin:
+ return nsIReferrerInfo::SAME_ORIGIN;
+ break;
+ case ReferrerPolicy::Strict_origin:
+ return nsIReferrerInfo::STRICT_ORIGIN;
+ break;
+ case ReferrerPolicy::Strict_origin_when_cross_origin:
+ return nsIReferrerInfo::STRICT_ORIGIN_WHEN_CROSS_ORIGIN;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid ReferrerPolicy value");
+ break;
+ }
+
+ return nsIReferrerInfo::EMPTY;
+}
+
+ReferrerInfo::ReferrerInfo()
+ : mOriginalReferrer(nullptr),
+ mPolicy(ReferrerPolicy::_empty),
+ mOriginalPolicy(ReferrerPolicy::_empty),
+ mSendReferrer(true),
+ mInitialized(false),
+ mOverridePolicyByDefault(false) {}
+
+ReferrerInfo::ReferrerInfo(const Document& aDoc) : ReferrerInfo() {
+ InitWithDocument(&aDoc);
+}
+
+ReferrerInfo::ReferrerInfo(const Element& aElement) : ReferrerInfo() {
+ InitWithElement(&aElement);
+}
+
+ReferrerInfo::ReferrerInfo(const Element& aElement,
+ ReferrerPolicyEnum aOverridePolicy)
+ : ReferrerInfo(aElement) {
+ // Override referrer policy if not empty
+ if (aOverridePolicy != ReferrerPolicyEnum::_empty) {
+ mPolicy = aOverridePolicy;
+ mOriginalPolicy = aOverridePolicy;
+ }
+}
+
+ReferrerInfo::ReferrerInfo(nsIURI* aOriginalReferrer,
+ ReferrerPolicyEnum aPolicy, bool aSendReferrer,
+ const Maybe<nsCString>& aComputedReferrer)
+ : mOriginalReferrer(aOriginalReferrer),
+ mPolicy(aPolicy),
+ mOriginalPolicy(aPolicy),
+ mSendReferrer(aSendReferrer),
+ mInitialized(true),
+ mOverridePolicyByDefault(false),
+ mComputedReferrer(aComputedReferrer) {}
+
+ReferrerInfo::ReferrerInfo(const ReferrerInfo& rhs)
+ : mOriginalReferrer(rhs.mOriginalReferrer),
+ mPolicy(rhs.mPolicy),
+ mOriginalPolicy(rhs.mOriginalPolicy),
+ mSendReferrer(rhs.mSendReferrer),
+ mInitialized(rhs.mInitialized),
+ mOverridePolicyByDefault(rhs.mOverridePolicyByDefault),
+ mComputedReferrer(rhs.mComputedReferrer) {}
+
+already_AddRefed<ReferrerInfo> ReferrerInfo::Clone() const {
+ RefPtr<ReferrerInfo> copy(new ReferrerInfo(*this));
+ return copy.forget();
+}
+
+already_AddRefed<ReferrerInfo> ReferrerInfo::CloneWithNewPolicy(
+ ReferrerPolicyEnum aPolicy) const {
+ RefPtr<ReferrerInfo> copy(new ReferrerInfo(*this));
+ copy->mPolicy = aPolicy;
+ copy->mOriginalPolicy = aPolicy;
+ return copy.forget();
+}
+
+already_AddRefed<ReferrerInfo> ReferrerInfo::CloneWithNewSendReferrer(
+ bool aSendReferrer) const {
+ RefPtr<ReferrerInfo> copy(new ReferrerInfo(*this));
+ copy->mSendReferrer = aSendReferrer;
+ return copy.forget();
+}
+
+already_AddRefed<ReferrerInfo> ReferrerInfo::CloneWithNewOriginalReferrer(
+ nsIURI* aOriginalReferrer) const {
+ RefPtr<ReferrerInfo> copy(new ReferrerInfo(*this));
+ copy->mOriginalReferrer = aOriginalReferrer;
+ return copy.forget();
+}
+
+NS_IMETHODIMP
+ReferrerInfo::GetOriginalReferrer(nsIURI** aOriginalReferrer) {
+ *aOriginalReferrer = mOriginalReferrer;
+ NS_IF_ADDREF(*aOriginalReferrer);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ReferrerInfo::GetReferrerPolicy(
+ JSContext* aCx, nsIReferrerInfo::ReferrerPolicyIDL* aReferrerPolicy) {
+ *aReferrerPolicy = ReferrerPolicyToReferrerPolicyIDL(mPolicy);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ReferrerInfo::GetReferrerPolicyString(nsACString& aResult) {
+ aResult.AssignASCII(ReferrerPolicyToString(mPolicy));
+ return NS_OK;
+}
+
+ReferrerPolicy ReferrerInfo::ReferrerPolicy() { return mPolicy; }
+
+NS_IMETHODIMP
+ReferrerInfo::GetSendReferrer(bool* aSendReferrer) {
+ *aSendReferrer = mSendReferrer;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ReferrerInfo::Equals(nsIReferrerInfo* aOther, bool* aResult) {
+ NS_ENSURE_TRUE(aOther, NS_ERROR_INVALID_ARG);
+ MOZ_ASSERT(mInitialized);
+ if (aOther == this) {
+ *aResult = true;
+ return NS_OK;
+ }
+
+ *aResult = false;
+ ReferrerInfo* other = static_cast<ReferrerInfo*>(aOther);
+ MOZ_ASSERT(other->mInitialized);
+
+ if (mPolicy != other->mPolicy || mSendReferrer != other->mSendReferrer ||
+ mOverridePolicyByDefault != other->mOverridePolicyByDefault ||
+ mComputedReferrer != other->mComputedReferrer) {
+ return NS_OK;
+ }
+
+ if (!mOriginalReferrer != !other->mOriginalReferrer) {
+ // One or the other has mOriginalReferrer, but not both... not equal
+ return NS_OK;
+ }
+
+ bool originalReferrerEquals;
+ if (mOriginalReferrer &&
+ (NS_FAILED(mOriginalReferrer->Equals(other->mOriginalReferrer,
+ &originalReferrerEquals)) ||
+ !originalReferrerEquals)) {
+ return NS_OK;
+ }
+
+ *aResult = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ReferrerInfo::GetComputedReferrerSpec(nsAString& aComputedReferrerSpec) {
+ aComputedReferrerSpec.Assign(
+ mComputedReferrer.isSome()
+ ? NS_ConvertUTF8toUTF16(mComputedReferrer.value())
+ : EmptyString());
+ return NS_OK;
+}
+
+already_AddRefed<nsIURI> ReferrerInfo::GetComputedReferrer() {
+ if (!mComputedReferrer.isSome() || mComputedReferrer.value().IsEmpty()) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIURI> result;
+ nsresult rv = NS_NewURI(getter_AddRefs(result), mComputedReferrer.value());
+ if (NS_FAILED(rv)) {
+ return nullptr;
+ }
+
+ return result.forget();
+}
+
+HashNumber ReferrerInfo::Hash() const {
+ MOZ_ASSERT(mInitialized);
+ nsAutoCString originalReferrerSpec;
+ if (mOriginalReferrer) {
+ Unused << mOriginalReferrer->GetSpec(originalReferrerSpec);
+ }
+
+ return mozilla::AddToHash(
+ static_cast<uint32_t>(mPolicy), mSendReferrer, mOverridePolicyByDefault,
+ mozilla::HashString(originalReferrerSpec),
+ mozilla::HashString(mComputedReferrer.isSome() ? mComputedReferrer.value()
+ : ""_ns));
+}
+
+NS_IMETHODIMP
+ReferrerInfo::Init(nsIReferrerInfo::ReferrerPolicyIDL aReferrerPolicy,
+ bool aSendReferrer, nsIURI* aOriginalReferrer) {
+ MOZ_ASSERT(!mInitialized);
+ if (mInitialized) {
+ return NS_ERROR_ALREADY_INITIALIZED;
+ };
+
+ mPolicy = ReferrerPolicyIDLToReferrerPolicy(aReferrerPolicy);
+ mOriginalPolicy = mPolicy;
+ mSendReferrer = aSendReferrer;
+ mOriginalReferrer = aOriginalReferrer;
+ mInitialized = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ReferrerInfo::InitWithDocument(const Document* aDocument) {
+ MOZ_ASSERT(!mInitialized);
+ if (mInitialized) {
+ return NS_ERROR_ALREADY_INITIALIZED;
+ };
+
+ mPolicy = aDocument->GetReferrerPolicy();
+ mOriginalPolicy = mPolicy;
+ mSendReferrer = true;
+ mOriginalReferrer = aDocument->GetDocumentURIAsReferrer();
+ mInitialized = true;
+ return NS_OK;
+}
+
+/**
+ * Check whether the given node has referrerpolicy attribute and parse
+ * referrer policy from the attribute.
+ * Currently, referrerpolicy attribute is supported in a, area, img, iframe,
+ * script, or link element.
+ */
+static ReferrerPolicy ReferrerPolicyFromAttribute(const Element& aElement) {
+ if (!aElement.IsAnyOfHTMLElements(nsGkAtoms::a, nsGkAtoms::area,
+ nsGkAtoms::script, nsGkAtoms::iframe,
+ nsGkAtoms::link, nsGkAtoms::img)) {
+ return ReferrerPolicy::_empty;
+ }
+ return aElement.GetReferrerPolicyAsEnum();
+}
+
+static bool HasRelNoReferrer(const Element& aElement) {
+ // rel=noreferrer is only supported in <a>, <area>, and <form>
+ if (!aElement.IsAnyOfHTMLElements(nsGkAtoms::a, nsGkAtoms::area,
+ nsGkAtoms::form) &&
+ !aElement.IsSVGElement(nsGkAtoms::a)) {
+ return false;
+ }
+
+ nsAutoString rel;
+ aElement.GetAttr(nsGkAtoms::rel, rel);
+ nsWhitespaceTokenizerTemplate<nsContentUtils::IsHTMLWhitespace> tok(rel);
+
+ while (tok.hasMoreTokens()) {
+ const nsAString& token = tok.nextToken();
+ if (token.LowerCaseEqualsLiteral("noreferrer")) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+NS_IMETHODIMP
+ReferrerInfo::InitWithElement(const Element* aElement) {
+ MOZ_ASSERT(!mInitialized);
+ if (mInitialized) {
+ return NS_ERROR_ALREADY_INITIALIZED;
+ };
+
+ // Referrer policy from referrerpolicy attribute will have a higher priority
+ // than referrer policy from <meta> tag and Referrer-Policy header.
+ mPolicy = ReferrerPolicyFromAttribute(*aElement);
+ if (mPolicy == ReferrerPolicy::_empty) {
+ // Fallback to use document's referrer poicy if we don't have referrer
+ // policy from attribute.
+ mPolicy = aElement->OwnerDoc()->GetReferrerPolicy();
+ }
+
+ mOriginalPolicy = mPolicy;
+ mSendReferrer = !HasRelNoReferrer(*aElement);
+ mOriginalReferrer = aElement->OwnerDoc()->GetDocumentURIAsReferrer();
+
+ mInitialized = true;
+ return NS_OK;
+}
+
+/* static */
+already_AddRefed<nsIReferrerInfo>
+ReferrerInfo::CreateFromDocumentAndPolicyOverride(
+ Document* aDoc, ReferrerPolicyEnum aPolicyOverride) {
+ MOZ_ASSERT(aDoc);
+ ReferrerPolicyEnum policy = aPolicyOverride != ReferrerPolicy::_empty
+ ? aPolicyOverride
+ : aDoc->GetReferrerPolicy();
+ nsCOMPtr<nsIReferrerInfo> referrerInfo =
+ new ReferrerInfo(aDoc->GetDocumentURIAsReferrer(), policy);
+ return referrerInfo.forget();
+}
+
+/* static */
+already_AddRefed<nsIReferrerInfo> ReferrerInfo::CreateForFetch(
+ nsIPrincipal* aPrincipal, Document* aDoc) {
+ MOZ_ASSERT(aPrincipal);
+
+ nsCOMPtr<nsIReferrerInfo> referrerInfo;
+ if (!aPrincipal || aPrincipal->IsSystemPrincipal()) {
+ referrerInfo = new ReferrerInfo(nullptr);
+ return referrerInfo.forget();
+ }
+
+ if (!aDoc) {
+ aPrincipal->CreateReferrerInfo(ReferrerPolicy::_empty,
+ getter_AddRefs(referrerInfo));
+ return referrerInfo.forget();
+ }
+
+ // If it weren't for history.push/replaceState, we could just use the
+ // principal's URI here. But since we want changes to the URI effected
+ // by push/replaceState to be reflected in the XHR referrer, we have to
+ // be more clever.
+ //
+ // If the document's original URI (before any push/replaceStates) matches
+ // our principal, then we use the document's current URI (after
+ // push/replaceStates). Otherwise (if the document is, say, a data:
+ // URI), we just use the principal's URI.
+ nsCOMPtr<nsIURI> docCurURI = aDoc->GetDocumentURI();
+ nsCOMPtr<nsIURI> docOrigURI = aDoc->GetOriginalURI();
+
+ if (docCurURI && docOrigURI) {
+ bool equal = false;
+ aPrincipal->EqualsURI(docOrigURI, &equal);
+ if (equal) {
+ referrerInfo = new ReferrerInfo(docCurURI, aDoc->GetReferrerPolicy());
+ return referrerInfo.forget();
+ }
+ }
+ aPrincipal->CreateReferrerInfo(aDoc->GetReferrerPolicy(),
+ getter_AddRefs(referrerInfo));
+ return referrerInfo.forget();
+}
+
+/* static */
+already_AddRefed<nsIReferrerInfo> ReferrerInfo::CreateForExternalCSSResources(
+ mozilla::StyleSheet* aExternalSheet, ReferrerPolicyEnum aPolicy) {
+ MOZ_ASSERT(aExternalSheet && !aExternalSheet->IsInline());
+ nsCOMPtr<nsIReferrerInfo> referrerInfo;
+
+ // Step 2
+ // https://w3c.github.io/webappsec-referrer-policy/#integration-with-css
+ // Use empty policy at the beginning and update it later from Referrer-Policy
+ // header.
+ referrerInfo = new ReferrerInfo(aExternalSheet->GetSheetURI(), aPolicy);
+ return referrerInfo.forget();
+}
+
+/* static */
+already_AddRefed<nsIReferrerInfo>
+ReferrerInfo::CreateForInternalCSSAndSVGResources(Document* aDocument) {
+ MOZ_ASSERT(aDocument);
+ return do_AddRef(new ReferrerInfo(aDocument->GetDocumentURI(),
+ aDocument->GetReferrerPolicy()));
+}
+
+nsresult ReferrerInfo::ComputeReferrer(nsIHttpChannel* aChannel) {
+ NS_ENSURE_ARG(aChannel);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // If the referrerInfo is passed around when redirect, just use the last
+ // computedReferrer to recompute
+ nsCOMPtr<nsIURI> referrer;
+ nsresult rv = NS_OK;
+ mOverridePolicyByDefault = false;
+
+ if (mComputedReferrer.isSome()) {
+ if (mComputedReferrer.value().IsEmpty()) {
+ return NS_OK;
+ }
+
+ rv = NS_NewURI(getter_AddRefs(referrer), mComputedReferrer.value());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ mComputedReferrer.reset();
+ // Emplace mComputedReferrer with an empty string, which means we have
+ // computed the referrer and the result referrer value is empty (not send
+ // referrer). So any early return later than this line will use that empty
+ // referrer.
+ mComputedReferrer.emplace(""_ns);
+
+ if (!mSendReferrer || !mOriginalReferrer ||
+ mPolicy == ReferrerPolicy::No_referrer) {
+ return NS_OK;
+ }
+
+ if (mPolicy == ReferrerPolicy::_empty ||
+ ShouldIgnoreLessRestrictedPolicies(aChannel, mOriginalPolicy)) {
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ OriginAttributes attrs = loadInfo->GetOriginAttributes();
+ bool isPrivate = attrs.mPrivateBrowsingId > 0;
+
+ nsCOMPtr<nsIURI> uri;
+ rv = aChannel->GetURI(getter_AddRefs(uri));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mPolicy = GetDefaultReferrerPolicy(aChannel, uri, isPrivate);
+ mOverridePolicyByDefault = true;
+ }
+
+ // This is for the case where the ETP toggle is off. In this case, we need to
+ // reset the referrer and the policy if the original policy is different from
+ // the current policy in order to recompute the referrer policy with the
+ // original policy.
+ if (!mOverridePolicyByDefault && mOriginalPolicy != ReferrerPolicy::_empty &&
+ mPolicy != mOriginalPolicy) {
+ referrer = nullptr;
+ mPolicy = mOriginalPolicy;
+ }
+
+ if (mPolicy == ReferrerPolicy::No_referrer) {
+ return NS_OK;
+ }
+
+ bool isUserReferrerSendingAllowed = false;
+ rv = HandleUserReferrerSendingPolicy(aChannel, isUserReferrerSendingAllowed);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (!isUserReferrerSendingAllowed) {
+ return NS_OK;
+ }
+
+ // Enforce Referrer allowlist, only http, https scheme are allowed
+ if (!IsReferrerSchemeAllowed(mOriginalReferrer)) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ rv = aChannel->GetURI(getter_AddRefs(uri));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ bool isSecureToInsecureAllowed = false;
+ rv = HandleSecureToInsecureReferral(mOriginalReferrer, uri, mPolicy,
+ isSecureToInsecureAllowed);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (!isSecureToInsecureAllowed) {
+ return NS_OK;
+ }
+
+ // Strip away any fragment per RFC 2616 section 14.36
+ // and Referrer Policy section 6.3.5.
+ if (!referrer) {
+ rv = NS_GetURIWithoutRef(mOriginalReferrer, getter_AddRefs(referrer));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ bool isUserXOriginAllowed = false;
+ rv = HandleUserXOriginSendingPolicy(uri, referrer, isUserXOriginAllowed);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (!isUserXOriginAllowed) {
+ return NS_OK;
+ }
+
+ // Handle user pref network.http.referer.spoofSource, send spoofed referrer if
+ // desired
+ if (StaticPrefs::network_http_referer_spoofSource()) {
+ nsCOMPtr<nsIURI> userSpoofReferrer;
+ rv = NS_GetURIWithoutRef(uri, getter_AddRefs(userSpoofReferrer));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ referrer = userSpoofReferrer;
+ }
+
+ // strip away any userpass; we don't want to be giving out passwords ;-)
+ // This is required by Referrer Policy stripping algorithm.
+ nsCOMPtr<nsIURI> exposableURI = nsIOService::CreateExposableURI(referrer);
+ referrer = exposableURI;
+
+ // Don't send referrer when the request is cross-origin and policy is
+ // "same-origin".
+ if (mPolicy == ReferrerPolicy::Same_origin &&
+ IsReferrerCrossOrigin(aChannel, referrer)) {
+ return NS_OK;
+ }
+
+ TrimmingPolicy trimmingPolicy = ComputeTrimmingPolicy(aChannel, referrer);
+
+ nsAutoCString trimmedReferrer;
+ // We first trim the referrer according to the policy by calling
+ // 'TrimReferrerWithPolicy' and right after we have to call
+ // 'LimitReferrerLength' (using the same arguments) because the trimmed
+ // referrer might exceed the allowed max referrer length.
+ rv = TrimReferrerWithPolicy(referrer, trimmingPolicy, trimmedReferrer);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = LimitReferrerLength(aChannel, referrer, trimmingPolicy, trimmedReferrer);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // finally, remember the referrer spec.
+ mComputedReferrer.reset();
+ mComputedReferrer.emplace(trimmedReferrer);
+
+ return NS_OK;
+}
+
+/* ===== nsISerializable implementation ====== */
+
+nsresult ReferrerInfo::ReadTailDataBeforeGecko100(
+ const uint32_t& aData, nsIObjectInputStream* aInputStream) {
+ MOZ_ASSERT(aInputStream);
+
+ nsCOMPtr<nsIInputStream> reader;
+ nsCOMPtr<nsIOutputStream> writer;
+
+ // We need to create a new pipe in order to read the aData and the rest of
+ // the input stream together in the old format. This would also help us with
+ // handling big endian correctly.
+ NS_NewPipe(getter_AddRefs(reader), getter_AddRefs(writer));
+
+ nsCOMPtr<nsIBinaryOutputStream> binaryPipeWriter =
+ NS_NewObjectOutputStream(writer);
+
+ // Write back the aData so that we can read bytes from it and handle big
+ // endian correctly.
+ nsresult rv = binaryPipeWriter->Write32(aData);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIBinaryInputStream> binaryPipeReader =
+ NS_NewObjectInputStream(reader);
+
+ rv = binaryPipeReader->ReadBoolean(&mSendReferrer);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ bool isComputed;
+ rv = binaryPipeReader->ReadBoolean(&isComputed);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // We need to handle the following string if isComputed is true.
+ if (isComputed) {
+ // Comsume the following 2 bytes from the input stream. They are the half
+ // part of the length prefix of the following string.
+ uint16_t data;
+ rv = aInputStream->Read16(&data);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Write the bytes to the pipe so that we can read the length of the string.
+ rv = binaryPipeWriter->Write16(data);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ uint32_t length;
+ rv = binaryPipeReader->Read32(&length);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Consume the string body from the input stream.
+ nsAutoCString computedReferrer;
+ rv = NS_ConsumeStream(aInputStream, length, computedReferrer);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ mComputedReferrer.emplace(computedReferrer);
+
+ // Read the remaining two bytes and write to the pipe.
+ uint16_t remain;
+ rv = aInputStream->Read16(&remain);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = binaryPipeWriter->Write16(remain);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ rv = binaryPipeReader->ReadBoolean(&mInitialized);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = binaryPipeReader->ReadBoolean(&mOverridePolicyByDefault);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ReferrerInfo::Read(nsIObjectInputStream* aStream) {
+ bool nonNull;
+ nsresult rv = aStream->ReadBoolean(&nonNull);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (nonNull) {
+ nsAutoCString spec;
+ nsresult rv = aStream->ReadCString(spec);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = NS_NewURI(getter_AddRefs(mOriginalReferrer), spec);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ mOriginalReferrer = nullptr;
+ }
+
+ // ReferrerPolicy.webidl has different order with ReferrerPolicyIDL. We store
+ // to disk using the order of ReferrerPolicyIDL, so we convert to
+ // ReferrerPolicyIDL to make it be compatible to the old format.
+ uint32_t policy;
+ rv = aStream->Read32(&policy);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ mPolicy = ReferrerPolicyIDLToReferrerPolicy(
+ static_cast<nsIReferrerInfo::ReferrerPolicyIDL>(policy));
+
+ uint32_t originalPolicy;
+ rv = aStream->Read32(&originalPolicy);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1784045#c6 for more
+ // details.
+ //
+ // We need to differentiate the old format and the new format here in order
+ // to be able to read both formats. The check here helps us with verifying
+ // which format it is.
+ if (MOZ_UNLIKELY(originalPolicy > 0xFF)) {
+ mOriginalPolicy = mPolicy;
+
+ return ReadTailDataBeforeGecko100(originalPolicy, aStream);
+ }
+
+ mOriginalPolicy = ReferrerPolicyIDLToReferrerPolicy(
+ static_cast<nsIReferrerInfo::ReferrerPolicyIDL>(originalPolicy));
+
+ rv = aStream->ReadBoolean(&mSendReferrer);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ bool isComputed;
+ rv = aStream->ReadBoolean(&isComputed);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (isComputed) {
+ nsAutoCString computedReferrer;
+ rv = aStream->ReadCString(computedReferrer);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ mComputedReferrer.emplace(computedReferrer);
+ }
+
+ rv = aStream->ReadBoolean(&mInitialized);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = aStream->ReadBoolean(&mOverridePolicyByDefault);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ReferrerInfo::Write(nsIObjectOutputStream* aStream) {
+ bool nonNull = (mOriginalReferrer != nullptr);
+ nsresult rv = aStream->WriteBoolean(nonNull);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (nonNull) {
+ nsAutoCString spec;
+ nsresult rv = mOriginalReferrer->GetSpec(spec);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = aStream->WriteStringZ(spec.get());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ rv = aStream->Write32(ReferrerPolicyToReferrerPolicyIDL(mPolicy));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = aStream->Write32(ReferrerPolicyToReferrerPolicyIDL(mOriginalPolicy));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = aStream->WriteBoolean(mSendReferrer);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ bool isComputed = mComputedReferrer.isSome();
+ rv = aStream->WriteBoolean(isComputed);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (isComputed) {
+ rv = aStream->WriteStringZ(mComputedReferrer.value().get());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ rv = aStream->WriteBoolean(mInitialized);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = aStream->WriteBoolean(mOverridePolicyByDefault);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ return NS_OK;
+}
+
+void ReferrerInfo::RecordTelemetry(nsIHttpChannel* aChannel) {
+#ifdef DEBUG
+ MOZ_ASSERT(!mTelemetryRecorded);
+ mTelemetryRecorded = true;
+#endif // DEBUG
+
+ // The telemetry probe has 18 buckets. The first 9 buckets are for same-site
+ // requests and the rest 9 buckets are for cross-site requests.
+ uint32_t telemetryOffset =
+ IsCrossSiteRequest(aChannel)
+ ? static_cast<uint32_t>(ReferrerPolicy::EndGuard_)
+ : 0;
+
+ Telemetry::Accumulate(Telemetry::REFERRER_POLICY_COUNT,
+ static_cast<uint32_t>(mPolicy) + telemetryOffset);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/security/ReferrerInfo.h b/dom/security/ReferrerInfo.h
new file mode 100644
index 0000000000..78440a5a70
--- /dev/null
+++ b/dom/security/ReferrerInfo.h
@@ -0,0 +1,470 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ReferrerInfo_h
+#define mozilla_dom_ReferrerInfo_h
+
+#include "nsCOMPtr.h"
+#include "nsIReferrerInfo.h"
+#include "nsReadableUtils.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/HashFunctions.h"
+#include "mozilla/dom/ReferrerPolicyBinding.h"
+
+#define REFERRERINFOF_CONTRACTID "@mozilla.org/referrer-info;1"
+// 041a129f-10ce-4bda-a60d-e027a26d5ed0
+#define REFERRERINFO_CID \
+ { \
+ 0x041a129f, 0x10ce, 0x4bda, { \
+ 0xa6, 0x0d, 0xe0, 0x27, 0xa2, 0x6d, 0x5e, 0xd0 \
+ } \
+ }
+
+class nsIHttpChannel;
+class nsIURI;
+class nsIChannel;
+class nsILoadInfo;
+class nsINode;
+class nsIPrincipal;
+
+namespace mozilla {
+class StyleSheet;
+class URLAndReferrerInfo;
+
+namespace net {
+class HttpBaseChannel;
+class nsHttpChannel;
+} // namespace net
+} // namespace mozilla
+
+namespace mozilla::dom {
+
+/**
+ * The ReferrerInfo class holds the raw referrer and potentially a referrer
+ * policy which allows to query the computed referrer which should be applied to
+ * a channel as the actual referrer value.
+ *
+ * The ReferrerInfo class solely contains readonly fields and represents a 1:1
+ * sync to the referrer header of the corresponding channel. In turn that means
+ * the class is immutable - so any modifications require to clone the current
+ * ReferrerInfo.
+ *
+ * For example if a request undergoes a redirect, the new channel
+ * will need a new ReferrerInfo clone with members being updated accordingly.
+ */
+
+class ReferrerInfo : public nsIReferrerInfo {
+ public:
+ typedef enum ReferrerPolicy ReferrerPolicyEnum;
+ ReferrerInfo();
+
+ explicit ReferrerInfo(
+ nsIURI* aOriginalReferrer,
+ ReferrerPolicyEnum aPolicy = ReferrerPolicy::_empty,
+ bool aSendReferrer = true,
+ const Maybe<nsCString>& aComputedReferrer = Maybe<nsCString>());
+
+ // Creates already initialized ReferrerInfo from an element or a document.
+ explicit ReferrerInfo(const Element&);
+ explicit ReferrerInfo(const Document&);
+
+ // Creates already initialized ReferrerInfo from an element or a document with
+ // a specific referrer policy.
+ ReferrerInfo(const Element&, ReferrerPolicyEnum);
+
+ // create an exact copy of the ReferrerInfo
+ already_AddRefed<ReferrerInfo> Clone() const;
+
+ // create an copy of the ReferrerInfo with new referrer policy
+ already_AddRefed<ReferrerInfo> CloneWithNewPolicy(
+ ReferrerPolicyEnum aPolicy) const;
+
+ // create an copy of the ReferrerInfo with new send referrer
+ already_AddRefed<ReferrerInfo> CloneWithNewSendReferrer(
+ bool aSendReferrer) const;
+
+ // create an copy of the ReferrerInfo with new original referrer
+ already_AddRefed<ReferrerInfo> CloneWithNewOriginalReferrer(
+ nsIURI* aOriginalReferrer) const;
+
+ // Record the telemetry for the referrer policy.
+ void RecordTelemetry(nsIHttpChannel* aChannel);
+
+ /*
+ * Helper function to create a new ReferrerInfo object from a given document
+ * and override referrer policy if needed (for example, when parsing link
+ * header or speculative loading).
+ *
+ * @param aDocument the document to init referrerInfo object.
+ * @param aPolicyOverride referrer policy to override if necessary.
+ */
+ static already_AddRefed<nsIReferrerInfo> CreateFromDocumentAndPolicyOverride(
+ Document* aDoc, ReferrerPolicyEnum aPolicyOverride);
+
+ /*
+ * Implements step 3.1 and 3.3 of the Determine request's Referrer algorithm
+ * from the Referrer Policy specification.
+ *
+ * https://w3c.github.io/webappsec/specs/referrer-policy/#determine-requests-referrer
+ */
+ static already_AddRefed<nsIReferrerInfo> CreateForFetch(
+ nsIPrincipal* aPrincipal, Document* aDoc);
+
+ /**
+ * Helper function to create new ReferrerInfo object from a given external
+ * stylesheet. The returned nsIReferrerInfo object will be used for any
+ * requests or resources referenced by the sheet.
+ *
+ * @param aSheet the stylesheet to init referrerInfo.
+ * @param aPolicy referrer policy from header if there's any.
+ */
+ static already_AddRefed<nsIReferrerInfo> CreateForExternalCSSResources(
+ StyleSheet* aExternalSheet,
+ ReferrerPolicyEnum aPolicy = ReferrerPolicy::_empty);
+
+ /**
+ * Helper function to create new ReferrerInfo object from a given document.
+ * The returned nsIReferrerInfo object will be used for any requests or
+ * resources referenced by internal stylesheet (for example style="" or
+ * wrapped by <style> tag), as well as SVG resources.
+ *
+ * @param aDocument the document to init referrerInfo object.
+ */
+ static already_AddRefed<nsIReferrerInfo> CreateForInternalCSSAndSVGResources(
+ Document* aDocument);
+
+ /**
+ * Check whether the given referrer's scheme is allowed to be computed and
+ * sent. The allowlist schemes are: http, https.
+ */
+ static bool IsReferrerSchemeAllowed(nsIURI* aReferrer);
+
+ /*
+ * The Referrer Policy should be inherited for nested browsing contexts that
+ * are not created from responses. Such as: srcdoc, data, blob.
+ */
+ static bool ShouldResponseInheritReferrerInfo(nsIChannel* aChannel);
+
+ /*
+ * Check whether referrer is allowed to send in secure to insecure scenario.
+ */
+ static nsresult HandleSecureToInsecureReferral(nsIURI* aOriginalURI,
+ nsIURI* aURI,
+ ReferrerPolicyEnum aPolicy,
+ bool& aAllowed);
+
+ /**
+ * Returns true if the given channel is cross-origin request
+ *
+ * Computing whether the request is cross-origin may be expensive, so please
+ * do that in cases where we're going to use this information later on.
+ */
+ static bool IsCrossOriginRequest(nsIHttpChannel* aChannel);
+
+ /**
+ * Returns true if aReferrer's origin and aChannel's URI are cross-origin.
+ */
+ static bool IsReferrerCrossOrigin(nsIHttpChannel* aChannel,
+ nsIURI* aReferrer);
+
+ /**
+ * Returns true if the given channel is cross-site request.
+ */
+ static bool IsCrossSiteRequest(nsIHttpChannel* aChannel);
+
+ /**
+ * Returns true if the given channel is suppressed by Referrer-Policy header
+ * and should set "null" to Origin header.
+ */
+ static bool ShouldSetNullOriginHeader(net::HttpBaseChannel* aChannel,
+ nsIURI* aOriginURI);
+
+ /**
+ * Getter for network.http.sendRefererHeader.
+ */
+ static uint32_t GetUserReferrerSendingPolicy();
+
+ /**
+ * Getter for network.http.referer.XOriginPolicy.
+ */
+ static uint32_t GetUserXOriginSendingPolicy();
+
+ /**
+ * Getter for network.http.referer.trimmingPolicy.
+ */
+ static uint32_t GetUserTrimmingPolicy();
+
+ /**
+ * Getter for network.http.referer.XOriginTrimmingPolicy.
+ */
+ static uint32_t GetUserXOriginTrimmingPolicy();
+
+ /**
+ * Return default referrer policy which is controlled by user
+ * prefs:
+ * network.http.referer.defaultPolicy for regular mode
+ * network.http.referer.defaultPolicy.trackers for third-party trackers
+ * in regular mode
+ * network.http.referer.defaultPolicy.pbmode for private mode
+ * network.http.referer.defaultPolicy.trackers.pbmode for third-party trackers
+ * in private mode
+ */
+ static ReferrerPolicyEnum GetDefaultReferrerPolicy(
+ nsIHttpChannel* aChannel = nullptr, nsIURI* aURI = nullptr,
+ bool aPrivateBrowsing = false);
+
+ /**
+ * Return default referrer policy for third party which is controlled by user
+ * prefs:
+ * network.http.referer.defaultPolicy.trackers for regular mode
+ * network.http.referer.defaultPolicy.trackers.pbmode for private mode
+ */
+ static ReferrerPolicyEnum GetDefaultThirdPartyReferrerPolicy(
+ bool aPrivateBrowsing = false);
+
+ /*
+ * Helper function to parse ReferrerPolicy from meta tag referrer content.
+ * For example: <meta name="referrer" content="origin">
+ *
+ * @param aContent content string to be transformed into ReferrerPolicyEnum,
+ * e.g. "origin".
+ */
+ static ReferrerPolicyEnum ReferrerPolicyFromMetaString(
+ const nsAString& aContent);
+
+ /*
+ * Helper function to parse ReferrerPolicy from string content of
+ * referrerpolicy attribute.
+ * For example: <a href="http://example.com" referrerpolicy="no-referrer">
+ *
+ * @param aContent content string to be transformed into ReferrerPolicyEnum,
+ * e.g. "no-referrer".
+ */
+ static ReferrerPolicyEnum ReferrerPolicyAttributeFromString(
+ const nsAString& aContent);
+
+ /*
+ * Helper function to parse ReferrerPolicy from string content of
+ * Referrer-Policy header.
+ * For example: Referrer-Policy: origin no-referrer
+ * https://www.w3.org/tr/referrer-policy/#parse-referrer-policy-from-header
+ *
+ * @param aContent content string to be transformed into ReferrerPolicyEnum.
+ * e.g. "origin no-referrer"
+ */
+ static ReferrerPolicyEnum ReferrerPolicyFromHeaderString(
+ const nsAString& aContent);
+
+ /*
+ * Helper function to convert ReferrerPolicy enum to string
+ *
+ * @param aPolicy referrer policy to convert.
+ */
+ static const char* ReferrerPolicyToString(ReferrerPolicyEnum aPolicy);
+
+ /**
+ * Hash function for this object
+ */
+ HashNumber Hash() const;
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIREFERRERINFO
+ NS_DECL_NSISERIALIZABLE
+
+ private:
+ virtual ~ReferrerInfo() = default;
+
+ ReferrerInfo(const ReferrerInfo& rhs);
+
+ /*
+ * Trimming policy when compute referrer, indicate how much information in the
+ * referrer will be sent. Order matters here.
+ */
+ enum TrimmingPolicy : uint32_t {
+ ePolicyFullURI = 0,
+ ePolicySchemeHostPortPath = 1,
+ ePolicySchemeHostPort = 2,
+ };
+
+ /*
+ * Referrer sending policy, indicates type of action could trigger to send
+ * referrer header, not send at all, send only with user's action (click on a
+ * link) or send even with inline content request (image request).
+ * Order matters here.
+ */
+ enum ReferrerSendingPolicy : uint32_t {
+ ePolicyNotSend = 0,
+ ePolicySendWhenUserTrigger = 1,
+ ePolicySendInlineContent = 2,
+ };
+
+ /*
+ * Sending referrer when cross origin policy, indicates when referrer should
+ * be send when compare 2 origins. Order matters here.
+ */
+ enum XOriginSendingPolicy : uint32_t {
+ ePolicyAlwaysSend = 0,
+ ePolicySendWhenSameDomain = 1,
+ ePolicySendWhenSameHost = 2,
+ };
+
+ /*
+ * Handle user controlled pref network.http.referer.XOriginPolicy
+ */
+ nsresult HandleUserXOriginSendingPolicy(nsIURI* aURI, nsIURI* aReferrer,
+ bool& aAllowed) const;
+
+ /*
+ * Handle user controlled pref network.http.sendRefererHeader
+ */
+ nsresult HandleUserReferrerSendingPolicy(nsIHttpChannel* aChannel,
+ bool& aAllowed) const;
+
+ /*
+ * Compute trimming policy from user controlled prefs.
+ * This function is called when we already made sure a nonempty referrer is
+ * allowed to send.
+ */
+ TrimmingPolicy ComputeTrimmingPolicy(nsIHttpChannel* aChannel,
+ nsIURI* aReferrer) const;
+
+ // HttpBaseChannel could access IsInitialized() and ComputeReferrer();
+ friend class mozilla::net::HttpBaseChannel;
+
+ /*
+ * Compute referrer for a given channel. The computation result then will be
+ * stored in this class and then used to set the actual referrer header of
+ * the channel. The computation could be controlled by several user prefs
+ * which are defined in StaticPrefList.yaml (see StaticPrefList.yaml for more
+ * details):
+ * network.http.sendRefererHeader
+ * network.http.referer.spoofSource
+ * network.http.referer.hideOnionSource
+ * network.http.referer.XOriginPolicy
+ * network.http.referer.trimmingPolicy
+ * network.http.referer.XOriginTrimmingPolicy
+ */
+ nsresult ComputeReferrer(nsIHttpChannel* aChannel);
+
+ /*
+ * Check whether the ReferrerInfo has been initialized or not.
+ */
+ bool IsInitialized() { return mInitialized; }
+
+ // nsHttpChannel, Document could access IsPolicyOverrided();
+ friend class mozilla::net::nsHttpChannel;
+ friend class mozilla::dom::Document;
+ /*
+ * Check whether if unset referrer policy is overrided by default or not
+ */
+ bool IsPolicyOverrided() { return mOverridePolicyByDefault; }
+
+ /*
+ * Get origin string from a given valid referrer URI (http, https)
+ *
+ * @aReferrer - the full referrer URI
+ * @aResult - the resulting aReferrer in string format.
+ */
+ nsresult GetOriginFromReferrerURI(nsIURI* aReferrer,
+ nsACString& aResult) const;
+
+ /*
+ * Trim a given referrer with a given a trimming policy,
+ */
+ nsresult TrimReferrerWithPolicy(nsIURI* aReferrer,
+ TrimmingPolicy aTrimmingPolicy,
+ nsACString& aResult) const;
+
+ /**
+ * Returns true if we should ignore less restricted referrer policies,
+ * including 'unsafe_url', 'no_referrer_when_downgrade' and
+ * 'origin_when_cross_origin', for the given channel. We only apply this
+ * restriction for cross-site requests. For the same-site request, we will
+ * still allow overriding the default referrer policy with less restricted
+ * one.
+ *
+ * Note that the channel triggered by the system and the extension will be
+ * exempt from this restriction.
+ */
+ bool ShouldIgnoreLessRestrictedPolicies(
+ nsIHttpChannel* aChannel, const ReferrerPolicyEnum aPolicy) const;
+
+ /*
+ * Limit referrer length using the following ruleset:
+ * - If the length of referrer URL is over max length, strip down to origin.
+ * - If the origin is still over max length, remove the referrer entirely.
+ *
+ * This function comlements TrimReferrerPolicy and needs to be called right
+ * after TrimReferrerPolicy.
+ *
+ * @aChannel - used to query information needed for logging to the console.
+ * @aReferrer - the full referrer URI; needs to be identical to aReferrer
+ * passed to TrimReferrerPolicy.
+ * @aTrimmingPolicy - represents the trimming policy which was applied to the
+ * referrer; needs to be identical to aTrimmingPolicy
+ * passed to TrimReferrerPolicy.
+ * @aInAndOutTrimmedReferrer - an in and outgoing argument representing the
+ * referrer value. Please pass the result of
+ * TrimReferrerWithPolicy as
+ * aInAndOutTrimmedReferrer which will then be
+ * reduced to the origin or completely truncated
+ * in case the referrer value exceeds the length
+ * limitation.
+ */
+ nsresult LimitReferrerLength(nsIHttpChannel* aChannel, nsIURI* aReferrer,
+ TrimmingPolicy aTrimmingPolicy,
+ nsACString& aInAndOutTrimmedReferrer) const;
+
+ /**
+ * The helper function to read the old data format before gecko 100 for
+ * deserialization.
+ */
+ nsresult ReadTailDataBeforeGecko100(const uint32_t& aData,
+ nsIObjectInputStream* aInputStream);
+
+ /*
+ * Write message to the error console
+ */
+ void LogMessageToConsole(nsIHttpChannel* aChannel, const char* aMsg,
+ const nsTArray<nsString>& aParams) const;
+
+ friend class mozilla::URLAndReferrerInfo;
+
+ nsCOMPtr<nsIURI> mOriginalReferrer;
+
+ ReferrerPolicyEnum mPolicy;
+
+ // The referrer policy that has been set originally for the channel. Note that
+ // the policy may have been overridden by the default referrer policy, so we
+ // need to keep track of this if we need to recover the original referrer
+ // policy.
+ ReferrerPolicyEnum mOriginalPolicy;
+
+ // Indicates if the referrer should be sent or not even when it's available
+ // (default is true).
+ bool mSendReferrer;
+
+ // Since the ReferrerInfo is immutable, we use this member as a helper to
+ // ensure no one can call e.g. init() twice to modify state of the
+ // ReferrerInfo.
+ bool mInitialized;
+
+ // Indicates if unset referrer policy is overrided by default
+ bool mOverridePolicyByDefault;
+
+ // Store a computed referrer for a given channel
+ Maybe<nsCString> mComputedReferrer;
+
+#ifdef DEBUG
+ // Indicates if the telemetry has been recorded. This is used to make sure the
+ // telemetry will be only recored once.
+ bool mTelemetryRecorded = false;
+#endif // DEBUG
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_ReferrerInfo_h
diff --git a/dom/security/SRICheck.cpp b/dom/security/SRICheck.cpp
new file mode 100644
index 0000000000..9eadcd04fc
--- /dev/null
+++ b/dom/security/SRICheck.cpp
@@ -0,0 +1,511 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SRICheck.h"
+
+#include "mozilla/Base64.h"
+#include "mozilla/LoadTainting.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/dom/SRILogHelper.h"
+#include "mozilla/dom/SRIMetadata.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentUtils.h"
+#include "nsIChannel.h"
+#include "nsIConsoleReportCollector.h"
+#include "nsIScriptError.h"
+#include "nsIURI.h"
+#include "nsNetUtil.h"
+#include "nsWhitespaceTokenizer.h"
+
+#define SRIVERBOSE(args) \
+ MOZ_LOG(SRILogHelper::GetSriLog(), mozilla::LogLevel::Verbose, args)
+#define SRILOG(args) \
+ MOZ_LOG(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug, args)
+#define SRIERROR(args) \
+ MOZ_LOG(SRILogHelper::GetSriLog(), mozilla::LogLevel::Error, args)
+
+namespace mozilla::dom {
+
+/**
+ * Returns whether or not the sub-resource about to be loaded is eligible
+ * for integrity checks. If it's not, the checks will be skipped and the
+ * sub-resource will be loaded.
+ */
+static nsresult IsEligible(nsIChannel* aChannel,
+ mozilla::LoadTainting aTainting,
+ const nsACString& aSourceFileURI,
+ nsIConsoleReportCollector* aReporter) {
+ NS_ENSURE_ARG_POINTER(aReporter);
+
+ if (!aChannel) {
+ SRILOG(("SRICheck::IsEligible, null channel"));
+ return NS_ERROR_SRI_NOT_ELIGIBLE;
+ }
+
+ // Was the sub-resource loaded via CORS?
+ if (aTainting == LoadTainting::CORS) {
+ SRILOG(("SRICheck::IsEligible, CORS mode"));
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIURI> finalURI;
+ nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(finalURI));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIURI> originalURI;
+ rv = aChannel->GetOriginalURI(getter_AddRefs(originalURI));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoCString requestSpec;
+ rv = originalURI->GetSpec(requestSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (MOZ_LOG_TEST(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug)) {
+ SRILOG(("SRICheck::IsEligible, requestURI=%s; finalURI=%s",
+ requestSpec.get(),
+ finalURI ? finalURI->GetSpecOrDefault().get() : ""));
+ }
+
+ // Is the sub-resource same-origin?
+ if (aTainting == LoadTainting::Basic) {
+ SRILOG(("SRICheck::IsEligible, same-origin"));
+ return NS_OK;
+ }
+ SRILOG(("SRICheck::IsEligible, NOT same-origin"));
+
+ NS_ConvertUTF8toUTF16 requestSpecUTF16(requestSpec);
+ nsTArray<nsString> params;
+ params.AppendElement(requestSpecUTF16);
+ aReporter->AddConsoleReport(
+ nsIScriptError::errorFlag, "Sub-resource Integrity"_ns,
+ nsContentUtils::eSECURITY_PROPERTIES, aSourceFileURI, 0, 0,
+ "IneligibleResource"_ns, const_cast<const nsTArray<nsString>&>(params));
+ return NS_ERROR_SRI_NOT_ELIGIBLE;
+}
+
+/* static */
+nsresult SRICheck::IntegrityMetadata(const nsAString& aMetadataList,
+ const nsACString& aSourceFileURI,
+ nsIConsoleReportCollector* aReporter,
+ SRIMetadata* outMetadata) {
+ NS_ENSURE_ARG_POINTER(outMetadata);
+ NS_ENSURE_ARG_POINTER(aReporter);
+ MOZ_ASSERT(outMetadata->IsEmpty()); // caller must pass empty metadata
+
+ NS_ConvertUTF16toUTF8 metadataList(aMetadataList);
+ SRILOG(("SRICheck::IntegrityMetadata, metadataList=%s", metadataList.get()));
+
+ // the integrity attribute is a list of whitespace-separated hashes
+ // and options so we need to look at them one by one and pick the
+ // strongest (valid) one
+ nsCWhitespaceTokenizer tokenizer(metadataList);
+ nsAutoCString token;
+ while (tokenizer.hasMoreTokens()) {
+ token = tokenizer.nextToken();
+
+ SRIMetadata metadata(token);
+ if (metadata.IsMalformed()) {
+ NS_ConvertUTF8toUTF16 tokenUTF16(token);
+ nsTArray<nsString> params;
+ params.AppendElement(tokenUTF16);
+ aReporter->AddConsoleReport(
+ nsIScriptError::warningFlag, "Sub-resource Integrity"_ns,
+ nsContentUtils::eSECURITY_PROPERTIES, aSourceFileURI, 0, 0,
+ "MalformedIntegrityHash"_ns,
+ const_cast<const nsTArray<nsString>&>(params));
+ } else if (!metadata.IsAlgorithmSupported()) {
+ nsAutoCString alg;
+ metadata.GetAlgorithm(&alg);
+ NS_ConvertUTF8toUTF16 algUTF16(alg);
+ nsTArray<nsString> params;
+ params.AppendElement(algUTF16);
+ aReporter->AddConsoleReport(
+ nsIScriptError::warningFlag, "Sub-resource Integrity"_ns,
+ nsContentUtils::eSECURITY_PROPERTIES, aSourceFileURI, 0, 0,
+ "UnsupportedHashAlg"_ns,
+ const_cast<const nsTArray<nsString>&>(params));
+ }
+
+ nsAutoCString alg1, alg2;
+ if (MOZ_LOG_TEST(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug)) {
+ outMetadata->GetAlgorithm(&alg1);
+ metadata.GetAlgorithm(&alg2);
+ }
+ if (*outMetadata == metadata) {
+ SRILOG(("SRICheck::IntegrityMetadata, alg '%s' is the same as '%s'",
+ alg1.get(), alg2.get()));
+ *outMetadata += metadata; // add new hash to strongest metadata
+ } else if (*outMetadata < metadata) {
+ SRILOG(("SRICheck::IntegrityMetadata, alg '%s' is weaker than '%s'",
+ alg1.get(), alg2.get()));
+ *outMetadata = metadata; // replace strongest metadata with current
+ }
+ }
+
+ outMetadata->mIntegrityString = aMetadataList;
+
+ if (MOZ_LOG_TEST(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug)) {
+ if (outMetadata->IsValid()) {
+ nsAutoCString alg;
+ outMetadata->GetAlgorithm(&alg);
+ SRILOG(("SRICheck::IntegrityMetadata, using a '%s' hash", alg.get()));
+ } else if (outMetadata->IsEmpty()) {
+ SRILOG(("SRICheck::IntegrityMetadata, no metadata"));
+ } else {
+ SRILOG(("SRICheck::IntegrityMetadata, no valid metadata found"));
+ }
+ }
+ return NS_OK;
+}
+
+//////////////////////////////////////////////////////////////
+//
+//////////////////////////////////////////////////////////////
+SRICheckDataVerifier::SRICheckDataVerifier(const SRIMetadata& aMetadata,
+ const nsACString& aSourceFileURI,
+ nsIConsoleReportCollector* aReporter)
+ : mCryptoHash(nullptr),
+ mBytesHashed(0),
+ mHashLength(0),
+ mHashType('\0'),
+ mInvalidMetadata(false),
+ mComplete(false) {
+ MOZ_ASSERT(!aMetadata.IsEmpty()); // should be checked by caller
+ MOZ_ASSERT(aReporter);
+
+ if (!aMetadata.IsValid()) {
+ nsTArray<nsString> params;
+ aReporter->AddConsoleReport(
+ nsIScriptError::warningFlag, "Sub-resource Integrity"_ns,
+ nsContentUtils::eSECURITY_PROPERTIES, aSourceFileURI, 0, 0,
+ "NoValidMetadata"_ns, const_cast<const nsTArray<nsString>&>(params));
+ mInvalidMetadata = true;
+ return; // ignore invalid metadata for forward-compatibility
+ }
+
+ aMetadata.GetHashType(&mHashType, &mHashLength);
+}
+
+nsresult SRICheckDataVerifier::EnsureCryptoHash() {
+ MOZ_ASSERT(!mInvalidMetadata);
+
+ if (mCryptoHash) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsICryptoHash> cryptoHash;
+ nsresult rv = NS_NewCryptoHash(mHashType, getter_AddRefs(cryptoHash));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mCryptoHash = cryptoHash;
+ return NS_OK;
+}
+
+nsresult SRICheckDataVerifier::Update(uint32_t aStringLen,
+ const uint8_t* aString) {
+ NS_ENSURE_ARG_POINTER(aString);
+ if (mInvalidMetadata) {
+ return NS_OK; // ignoring any data updates, see mInvalidMetadata usage
+ }
+
+ nsresult rv;
+ rv = EnsureCryptoHash();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mBytesHashed += aStringLen;
+
+ return mCryptoHash->Update(aString, aStringLen);
+}
+
+nsresult SRICheckDataVerifier::Finish() {
+ if (mInvalidMetadata || mComplete) {
+ return NS_OK; // already finished or invalid metadata
+ }
+
+ nsresult rv;
+ rv = EnsureCryptoHash(); // we need computed hash even for 0-length data
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mCryptoHash->Finish(false, mComputedHash);
+ mCryptoHash = nullptr;
+ mComplete = true;
+ return rv;
+}
+
+nsresult SRICheckDataVerifier::VerifyHash(
+ const SRIMetadata& aMetadata, uint32_t aHashIndex,
+ const nsACString& aSourceFileURI, nsIConsoleReportCollector* aReporter) {
+ NS_ENSURE_ARG_POINTER(aReporter);
+
+ nsAutoCString base64Hash;
+ aMetadata.GetHash(aHashIndex, &base64Hash);
+ SRILOG(("SRICheckDataVerifier::VerifyHash, hash[%u]=%s", aHashIndex,
+ base64Hash.get()));
+
+ nsAutoCString binaryHash;
+
+ // We're decoding the supplied hash twice. Trying base64 first.
+ nsresult rv = Base64Decode(base64Hash, binaryHash);
+
+ if (NS_FAILED(rv)) {
+ SRILOG(
+ ("SRICheckDataVerifier::VerifyHash, base64 decoding failed. Trying "
+ "base64url next."));
+ FallibleTArray<uint8_t> decoded;
+ rv = Base64URLDecode(base64Hash, Base64URLDecodePaddingPolicy::Ignore,
+ decoded);
+ if (NS_FAILED(rv)) {
+ SRILOG(
+ ("SRICheckDataVerifier::VerifyHash, base64url decoding failed too. "
+ "Bailing out."));
+ // if neither succeeded, we can bail out and warn
+ nsTArray<nsString> params;
+ aReporter->AddConsoleReport(
+ nsIScriptError::errorFlag, "Sub-resource Integrity"_ns,
+ nsContentUtils::eSECURITY_PROPERTIES, aSourceFileURI, 0, 0,
+ "InvalidIntegrityBase64"_ns,
+ const_cast<const nsTArray<nsString>&>(params));
+ return NS_ERROR_SRI_CORRUPT;
+ }
+ binaryHash.Assign(reinterpret_cast<const char*>(decoded.Elements()),
+ decoded.Length());
+ SRILOG(
+ ("SRICheckDataVerifier::VerifyHash, decoded supplied base64url hash "
+ "successfully."));
+ } else {
+ SRILOG(
+ ("SRICheckDataVerifier::VerifyHash, decoded supplied base64 hash "
+ "successfully."));
+ }
+
+ uint32_t hashLength;
+ int8_t hashType;
+ aMetadata.GetHashType(&hashType, &hashLength);
+ if (binaryHash.Length() != hashLength) {
+ SRILOG(
+ ("SRICheckDataVerifier::VerifyHash, supplied base64(url) hash was "
+ "incorrect length after decoding."));
+ nsTArray<nsString> params;
+ aReporter->AddConsoleReport(
+ nsIScriptError::errorFlag, "Sub-resource Integrity"_ns,
+ nsContentUtils::eSECURITY_PROPERTIES, aSourceFileURI, 0, 0,
+ "InvalidIntegrityLength"_ns,
+ const_cast<const nsTArray<nsString>&>(params));
+ return NS_ERROR_SRI_CORRUPT;
+ }
+
+ // the decoded supplied hash should match our computed binary hash.
+ if (!binaryHash.Equals(mComputedHash)) {
+ SRILOG(("SRICheckDataVerifier::VerifyHash, hash[%u] did not match",
+ aHashIndex));
+ return NS_ERROR_SRI_CORRUPT;
+ }
+
+ SRILOG(("SRICheckDataVerifier::VerifyHash, hash[%u] verified successfully",
+ aHashIndex));
+ return NS_OK;
+}
+
+nsresult SRICheckDataVerifier::Verify(const SRIMetadata& aMetadata,
+ nsIChannel* aChannel,
+ const nsACString& aSourceFileURI,
+ nsIConsoleReportCollector* aReporter) {
+ NS_ENSURE_ARG_POINTER(aReporter);
+
+ if (MOZ_LOG_TEST(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug)) {
+ nsAutoCString requestURL;
+ nsCOMPtr<nsIRequest> request = aChannel;
+ request->GetName(requestURL);
+ SRILOG(("SRICheckDataVerifier::Verify, url=%s (length=%zu)",
+ requestURL.get(), mBytesHashed));
+ }
+
+ nsresult rv = Finish();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ LoadTainting tainting = loadInfo->GetTainting();
+
+ if (NS_FAILED(IsEligible(aChannel, tainting, aSourceFileURI, aReporter))) {
+ return NS_ERROR_SRI_NOT_ELIGIBLE;
+ }
+
+ if (mInvalidMetadata) {
+ return NS_OK; // ignore invalid metadata for forward-compatibility
+ }
+
+ for (uint32_t i = 0; i < aMetadata.HashCount(); i++) {
+ if (NS_SUCCEEDED(VerifyHash(aMetadata, i, aSourceFileURI, aReporter))) {
+ return NS_OK; // stop at the first valid hash
+ }
+ }
+
+ nsAutoCString alg;
+ aMetadata.GetAlgorithm(&alg);
+ NS_ConvertUTF8toUTF16 algUTF16(alg);
+ nsAutoCString encodedHash;
+ rv = Base64Encode(mComputedHash, encodedHash);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ConvertUTF8toUTF16 encodedHashUTF16(encodedHash);
+
+ nsTArray<nsString> params;
+ params.AppendElement(algUTF16);
+ params.AppendElement(encodedHashUTF16);
+ aReporter->AddConsoleReport(
+ nsIScriptError::errorFlag, "Sub-resource Integrity"_ns,
+ nsContentUtils::eSECURITY_PROPERTIES, aSourceFileURI, 0, 0,
+ "IntegrityMismatch2"_ns, const_cast<const nsTArray<nsString>&>(params));
+
+ return NS_ERROR_SRI_CORRUPT;
+}
+
+uint32_t SRICheckDataVerifier::DataSummaryLength() {
+ MOZ_ASSERT(!mInvalidMetadata);
+ return sizeof(mHashType) + sizeof(mHashLength) + mHashLength;
+}
+
+uint32_t SRICheckDataVerifier::EmptyDataSummaryLength() {
+ return sizeof(int8_t) + sizeof(uint32_t);
+}
+
+nsresult SRICheckDataVerifier::DataSummaryLength(uint32_t aDataLen,
+ const uint8_t* aData,
+ uint32_t* length) {
+ *length = 0;
+ NS_ENSURE_ARG_POINTER(aData);
+
+ // we expect to always encode an SRI, even if it is empty or incomplete
+ if (aDataLen < EmptyDataSummaryLength()) {
+ SRILOG(
+ ("SRICheckDataVerifier::DataSummaryLength, encoded length[%u] is too "
+ "small",
+ aDataLen));
+ return NS_ERROR_SRI_IMPORT;
+ }
+
+ // decode the content of the buffer
+ size_t offset = sizeof(mHashType);
+ decltype(mHashLength) len = 0;
+ memcpy(&len, &aData[offset], sizeof(mHashLength));
+ offset += sizeof(mHashLength);
+
+ SRIVERBOSE(
+ ("SRICheckDataVerifier::DataSummaryLength, header {%x, %x, %x, %x, %x, "
+ "...}",
+ aData[0], aData[1], aData[2], aData[3], aData[4]));
+
+ if (offset + len > aDataLen) {
+ SRILOG(
+ ("SRICheckDataVerifier::DataSummaryLength, encoded length[%u] overflow "
+ "the buffer size",
+ aDataLen));
+ SRIVERBOSE(("SRICheckDataVerifier::DataSummaryLength, offset[%u], len[%u]",
+ uint32_t(offset), uint32_t(len)));
+ return NS_ERROR_SRI_IMPORT;
+ }
+ *length = uint32_t(offset + len);
+ return NS_OK;
+}
+
+nsresult SRICheckDataVerifier::ImportDataSummary(uint32_t aDataLen,
+ const uint8_t* aData) {
+ MOZ_ASSERT(!mInvalidMetadata); // mHashType and mHashLength should be valid
+ MOZ_ASSERT(!mCryptoHash); // EnsureCryptoHash should not have been called
+ NS_ENSURE_ARG_POINTER(aData);
+ if (mInvalidMetadata) {
+ return NS_OK; // ignoring any data updates, see mInvalidMetadata usage
+ }
+
+ // we expect to always encode an SRI, even if it is empty or incomplete
+ if (aDataLen < DataSummaryLength()) {
+ SRILOG(
+ ("SRICheckDataVerifier::ImportDataSummary, encoded length[%u] is too "
+ "small",
+ aDataLen));
+ return NS_ERROR_SRI_IMPORT;
+ }
+
+ SRIVERBOSE(
+ ("SRICheckDataVerifier::ImportDataSummary, header {%x, %x, %x, %x, %x, "
+ "...}",
+ aData[0], aData[1], aData[2], aData[3], aData[4]));
+
+ // decode the content of the buffer
+ size_t offset = 0;
+ decltype(mHashType) hashType;
+ memcpy(&hashType, &aData[offset], sizeof(mHashType));
+ if (hashType != mHashType) {
+ SRILOG(
+ ("SRICheckDataVerifier::ImportDataSummary, hash type[%d] does not "
+ "match[%d]",
+ hashType, mHashType));
+ return NS_ERROR_SRI_UNEXPECTED_HASH_TYPE;
+ }
+ offset += sizeof(mHashType);
+
+ decltype(mHashLength) hashLength;
+ memcpy(&hashLength, &aData[offset], sizeof(mHashLength));
+ if (hashLength != mHashLength) {
+ SRILOG(
+ ("SRICheckDataVerifier::ImportDataSummary, hash length[%d] does not "
+ "match[%d]",
+ hashLength, mHashLength));
+ return NS_ERROR_SRI_UNEXPECTED_HASH_TYPE;
+ }
+ offset += sizeof(mHashLength);
+
+ // copy the hash to mComputedHash, as-if we had finished streaming the bytes
+ mComputedHash.Assign(reinterpret_cast<const char*>(&aData[offset]),
+ mHashLength);
+ mCryptoHash = nullptr;
+ mComplete = true;
+ return NS_OK;
+}
+
+nsresult SRICheckDataVerifier::ExportDataSummary(uint32_t aDataLen,
+ uint8_t* aData) {
+ MOZ_ASSERT(!mInvalidMetadata); // mHashType and mHashLength should be valid
+ MOZ_ASSERT(mComplete); // finished streaming
+ NS_ENSURE_ARG_POINTER(aData);
+ NS_ENSURE_TRUE(aDataLen >= DataSummaryLength(), NS_ERROR_INVALID_ARG);
+
+ // serialize the hash in the buffer
+ size_t offset = 0;
+ memcpy(&aData[offset], &mHashType, sizeof(mHashType));
+ offset += sizeof(mHashType);
+ memcpy(&aData[offset], &mHashLength, sizeof(mHashLength));
+ offset += sizeof(mHashLength);
+
+ SRIVERBOSE(
+ ("SRICheckDataVerifier::ExportDataSummary, header {%x, %x, %x, %x, %x, "
+ "...}",
+ aData[0], aData[1], aData[2], aData[3], aData[4]));
+
+ // copy the hash to mComputedHash, as-if we had finished streaming the bytes
+ nsCharTraits<char>::copy(reinterpret_cast<char*>(&aData[offset]),
+ mComputedHash.get(), mHashLength);
+ return NS_OK;
+}
+
+nsresult SRICheckDataVerifier::ExportEmptyDataSummary(uint32_t aDataLen,
+ uint8_t* aData) {
+ NS_ENSURE_ARG_POINTER(aData);
+ NS_ENSURE_TRUE(aDataLen >= EmptyDataSummaryLength(), NS_ERROR_INVALID_ARG);
+
+ // serialize an unknown hash in the buffer, to be able to skip it later
+ size_t offset = 0;
+ memset(&aData[offset], 0, sizeof(mHashType));
+ offset += sizeof(mHashType);
+ memset(&aData[offset], 0, sizeof(mHashLength));
+
+ SRIVERBOSE(
+ ("SRICheckDataVerifier::ExportEmptyDataSummary, header {%x, %x, %x, %x, "
+ "%x, ...}",
+ aData[0], aData[1], aData[2], aData[3], aData[4]));
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/security/SRICheck.h b/dom/security/SRICheck.h
new file mode 100644
index 0000000000..3efacf41a1
--- /dev/null
+++ b/dom/security/SRICheck.h
@@ -0,0 +1,102 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SRICheck_h
+#define mozilla_dom_SRICheck_h
+
+#include "nsTString.h"
+#include "nsStringFwd.h"
+#include "nsCOMPtr.h"
+#include "nsICryptoHash.h"
+
+class nsIChannel;
+class nsIConsoleReportCollector;
+
+namespace mozilla::dom {
+
+class SRIMetadata;
+
+class SRICheck final {
+ public:
+ /**
+ * Parse the multiple hashes specified in the integrity attribute and
+ * return the strongest supported hash.
+ */
+ static nsresult IntegrityMetadata(const nsAString& aMetadataList,
+ const nsACString& aSourceFileURI,
+ nsIConsoleReportCollector* aReporter,
+ SRIMetadata* outMetadata);
+};
+
+// The SRICheckDataVerifier can be used in 2 different mode:
+//
+// 1. The streaming mode involves reading bytes from an input, and to use
+// the |Update| function to stream new bytes, and to use the |Verify|
+// function to check the hash of the content with the hash provided by
+// the metadata.
+//
+// Optionally, one can serialize the verified hash with |ExportDataSummary|,
+// in a buffer in order to rely on the second mode the next time.
+//
+// 2. The pre-computed mode, involves reading a hash with |ImportDataSummary|,
+// which got exported by the SRICheckDataVerifier and potentially cached, and
+// then use the |Verify| function to check against the hash provided by the
+// metadata.
+class SRICheckDataVerifier final {
+ public:
+ SRICheckDataVerifier(const SRIMetadata& aMetadata,
+ const nsACString& aSourceFileURI,
+ nsIConsoleReportCollector* aReporter);
+
+ // Append the following bytes to the content used to compute the hash. Once
+ // all bytes are streamed, use the Verify function to check the integrity.
+ nsresult Update(uint32_t aStringLen, const uint8_t* aString);
+
+ // Verify that the computed hash corresponds to the metadata.
+ nsresult Verify(const SRIMetadata& aMetadata, nsIChannel* aChannel,
+ const nsACString& aSourceFileURI,
+ nsIConsoleReportCollector* aReporter);
+
+ bool IsComplete() const { return mComplete; }
+
+ // Report the length of the computed hash and its type, such that we can
+ // reserve the space for encoding it in a vector.
+ uint32_t DataSummaryLength();
+ static uint32_t EmptyDataSummaryLength();
+
+ // Write the computed hash and its type in a pre-allocated buffer.
+ nsresult ExportDataSummary(uint32_t aDataLen, uint8_t* aData);
+ static nsresult ExportEmptyDataSummary(uint32_t aDataLen, uint8_t* aData);
+
+ // Report the length of the computed hash and its type, such that we can
+ // skip these data while reading a buffer.
+ static nsresult DataSummaryLength(uint32_t aDataLen, const uint8_t* aData,
+ uint32_t* length);
+
+ // Extract the computed hash and its type, such that we can |Verify| if it
+ // matches the metadata. The buffer should be at least the same size or
+ // larger than the value returned by |DataSummaryLength|.
+ nsresult ImportDataSummary(uint32_t aDataLen, const uint8_t* aData);
+
+ private:
+ nsCOMPtr<nsICryptoHash> mCryptoHash;
+ nsAutoCString mComputedHash;
+ size_t mBytesHashed;
+ uint32_t mHashLength;
+ int8_t mHashType;
+ bool mInvalidMetadata;
+ bool mComplete;
+
+ nsresult EnsureCryptoHash();
+ nsresult Finish();
+ nsresult VerifyHash(const SRIMetadata& aMetadata, uint32_t aHashIndex,
+ const nsACString& aSourceFileURI,
+ nsIConsoleReportCollector* aReporter);
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_SRICheck_h
diff --git a/dom/security/SRILogHelper.h b/dom/security/SRILogHelper.h
new file mode 100644
index 0000000000..e453f30842
--- /dev/null
+++ b/dom/security/SRILogHelper.h
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SRILogHelper_h
+#define mozilla_dom_SRILogHelper_h
+
+#include "mozilla/Logging.h"
+
+namespace mozilla::dom {
+
+class SRILogHelper final {
+ public:
+ static LogModule* GetSriLog() {
+ static LazyLogModule gSriPRLog("SRI");
+ return gSriPRLog;
+ }
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_SRILogHelper_h
diff --git a/dom/security/SRIMetadata.cpp b/dom/security/SRIMetadata.cpp
new file mode 100644
index 0000000000..02144f0f13
--- /dev/null
+++ b/dom/security/SRIMetadata.cpp
@@ -0,0 +1,187 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SRIMetadata.h"
+
+#include "hasht.h"
+#include "mozilla/Logging.h"
+#include "nsICryptoHash.h"
+
+static mozilla::LogModule* GetSriMetadataLog() {
+ static mozilla::LazyLogModule gSriMetadataPRLog("SRIMetadata");
+ return gSriMetadataPRLog;
+}
+
+#define SRIMETADATALOG(args) \
+ MOZ_LOG(GetSriMetadataLog(), mozilla::LogLevel::Debug, args)
+#define SRIMETADATAERROR(args) \
+ MOZ_LOG(GetSriMetadataLog(), mozilla::LogLevel::Error, args)
+
+namespace mozilla::dom {
+
+SRIMetadata::SRIMetadata(const nsACString& aToken)
+ : mAlgorithmType(SRIMetadata::UNKNOWN_ALGORITHM), mEmpty(false) {
+ MOZ_ASSERT(!aToken.IsEmpty()); // callers should check this first
+
+ SRIMETADATALOG(("SRIMetadata::SRIMetadata, aToken='%s'",
+ PromiseFlatCString(aToken).get()));
+
+ int32_t hyphen = aToken.FindChar('-');
+ if (hyphen == -1) {
+ SRIMETADATAERROR(("SRIMetadata::SRIMetadata, invalid (no hyphen)"));
+ return; // invalid metadata
+ }
+
+ // split the token into its components
+ mAlgorithm = Substring(aToken, 0, hyphen);
+ uint32_t hashStart = hyphen + 1;
+ if (hashStart >= aToken.Length()) {
+ SRIMETADATAERROR(("SRIMetadata::SRIMetadata, invalid (missing digest)"));
+ return; // invalid metadata
+ }
+ int32_t question = aToken.FindChar('?');
+ if (question == -1) {
+ mHashes.AppendElement(
+ Substring(aToken, hashStart, aToken.Length() - hashStart));
+ } else {
+ MOZ_ASSERT(question > 0);
+ if (static_cast<uint32_t>(question) <= hashStart) {
+ SRIMETADATAERROR(
+ ("SRIMetadata::SRIMetadata, invalid (options w/o digest)"));
+ return; // invalid metadata
+ }
+ mHashes.AppendElement(Substring(aToken, hashStart, question - hashStart));
+ }
+
+ if (mAlgorithm.EqualsLiteral("sha256")) {
+ mAlgorithmType = nsICryptoHash::SHA256;
+ } else if (mAlgorithm.EqualsLiteral("sha384")) {
+ mAlgorithmType = nsICryptoHash::SHA384;
+ } else if (mAlgorithm.EqualsLiteral("sha512")) {
+ mAlgorithmType = nsICryptoHash::SHA512;
+ }
+
+ SRIMETADATALOG(("SRIMetadata::SRIMetadata, hash='%s'; alg='%s'",
+ mHashes[0].get(), mAlgorithm.get()));
+}
+
+bool SRIMetadata::operator<(const SRIMetadata& aOther) const {
+ static_assert(nsICryptoHash::SHA256 < nsICryptoHash::SHA384,
+ "We rely on the order indicating relative alg strength");
+ static_assert(nsICryptoHash::SHA384 < nsICryptoHash::SHA512,
+ "We rely on the order indicating relative alg strength");
+ MOZ_ASSERT(mAlgorithmType == SRIMetadata::UNKNOWN_ALGORITHM ||
+ mAlgorithmType == nsICryptoHash::SHA256 ||
+ mAlgorithmType == nsICryptoHash::SHA384 ||
+ mAlgorithmType == nsICryptoHash::SHA512);
+ MOZ_ASSERT(aOther.mAlgorithmType == SRIMetadata::UNKNOWN_ALGORITHM ||
+ aOther.mAlgorithmType == nsICryptoHash::SHA256 ||
+ aOther.mAlgorithmType == nsICryptoHash::SHA384 ||
+ aOther.mAlgorithmType == nsICryptoHash::SHA512);
+
+ if (mEmpty) {
+ SRIMETADATALOG(("SRIMetadata::operator<, first metadata is empty"));
+ return true; // anything beats the empty metadata (incl. invalid ones)
+ }
+
+ SRIMETADATALOG(("SRIMetadata::operator<, alg1='%d'; alg2='%d'",
+ mAlgorithmType, aOther.mAlgorithmType));
+ return (mAlgorithmType < aOther.mAlgorithmType);
+}
+
+bool SRIMetadata::operator>(const SRIMetadata& aOther) const {
+ MOZ_ASSERT(false);
+ return false;
+}
+
+SRIMetadata& SRIMetadata::operator+=(const SRIMetadata& aOther) {
+ MOZ_ASSERT(!aOther.IsEmpty() && !IsEmpty());
+ MOZ_ASSERT(aOther.IsValid() && IsValid());
+ MOZ_ASSERT(mAlgorithmType == aOther.mAlgorithmType);
+
+ // We only pull in the first element of the other metadata
+ MOZ_ASSERT(aOther.mHashes.Length() == 1);
+ if (mHashes.Length() < SRIMetadata::MAX_ALTERNATE_HASHES) {
+ SRIMETADATALOG((
+ "SRIMetadata::operator+=, appending another '%s' hash (new length=%zu)",
+ mAlgorithm.get(), mHashes.Length()));
+ mHashes.AppendElement(aOther.mHashes[0]);
+ }
+
+ MOZ_ASSERT(mHashes.Length() > 1);
+ MOZ_ASSERT(mHashes.Length() <= SRIMetadata::MAX_ALTERNATE_HASHES);
+ return *this;
+}
+
+bool SRIMetadata::operator==(const SRIMetadata& aOther) const {
+ if (IsEmpty() || !IsValid()) {
+ return false;
+ }
+ return mAlgorithmType == aOther.mAlgorithmType;
+}
+
+void SRIMetadata::GetHash(uint32_t aIndex, nsCString* outHash) const {
+ MOZ_ASSERT(aIndex < SRIMetadata::MAX_ALTERNATE_HASHES);
+ if (NS_WARN_IF(aIndex >= mHashes.Length())) {
+ *outHash = nullptr;
+ return;
+ }
+ *outHash = mHashes[aIndex];
+}
+
+void SRIMetadata::GetHashType(int8_t* outType, uint32_t* outLength) const {
+ // these constants are defined in security/nss/lib/util/hasht.h and
+ // netwerk/base/public/nsICryptoHash.idl
+ switch (mAlgorithmType) {
+ case nsICryptoHash::SHA256:
+ *outLength = SHA256_LENGTH;
+ break;
+ case nsICryptoHash::SHA384:
+ *outLength = SHA384_LENGTH;
+ break;
+ case nsICryptoHash::SHA512:
+ *outLength = SHA512_LENGTH;
+ break;
+ default:
+ *outLength = 0;
+ }
+ *outType = mAlgorithmType;
+}
+
+bool SRIMetadata::CanTrustBeDelegatedTo(const SRIMetadata& aOther) const {
+ if (IsEmpty()) {
+ // No integrity requirements enforced, just let go.
+ return true;
+ }
+
+ if (aOther.IsEmpty()) {
+ // This metadata requires a check and the other has none, can't delegate.
+ return false;
+ }
+
+ if (mAlgorithmType != aOther.mAlgorithmType) {
+ // They must use the same hash algorithm.
+ return false;
+ }
+
+ // They must be completely identical, except for the order of hashes.
+ // We don't know which hash is the one passing eventually the check, so only
+ // option is to require this metadata to contain the same set of hashes as the
+ // one we want to delegate the trust to.
+ if (mHashes.Length() != aOther.mHashes.Length()) {
+ return false;
+ }
+
+ for (const auto& hash : mHashes) {
+ if (!aOther.mHashes.Contains(hash)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/security/SRIMetadata.h b/dom/security/SRIMetadata.h
new file mode 100644
index 0000000000..caa3ba25f3
--- /dev/null
+++ b/dom/security/SRIMetadata.h
@@ -0,0 +1,90 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SRIMetadata_h
+#define mozilla_dom_SRIMetadata_h
+
+#include "nsTArray.h"
+#include "nsString.h"
+#include "SRICheck.h"
+
+namespace mozilla::dom {
+
+class SRIMetadata final {
+ friend class SRICheck;
+
+ public:
+ static const uint32_t MAX_ALTERNATE_HASHES = 256;
+ static const int8_t UNKNOWN_ALGORITHM = -1;
+
+ /**
+ * Create an empty metadata object.
+ */
+ SRIMetadata() : mAlgorithmType(UNKNOWN_ALGORITHM), mEmpty(true) {}
+
+ /**
+ * Split a string token into the components of an SRI metadata
+ * attribute.
+ */
+ explicit SRIMetadata(const nsACString& aToken);
+
+ /**
+ * Returns true when this object's hash algorithm is weaker than the
+ * other object's hash algorithm.
+ */
+ bool operator<(const SRIMetadata& aOther) const;
+
+ /**
+ * Not implemented. Should not be used.
+ */
+ bool operator>(const SRIMetadata& aOther) const;
+
+ /**
+ * Add another metadata's hash to this one.
+ */
+ SRIMetadata& operator+=(const SRIMetadata& aOther);
+
+ /**
+ * Returns true when the two metadata use the same hash algorithm.
+ */
+ bool operator==(const SRIMetadata& aOther) const;
+
+ bool IsEmpty() const { return mEmpty; }
+ bool IsMalformed() const { return mHashes.IsEmpty() || mAlgorithm.IsEmpty(); }
+ bool IsAlgorithmSupported() const {
+ return mAlgorithmType != UNKNOWN_ALGORITHM;
+ }
+ bool IsValid() const { return !IsMalformed() && IsAlgorithmSupported(); }
+
+ uint32_t HashCount() const { return mHashes.Length(); }
+ void GetHash(uint32_t aIndex, nsCString* outHash) const;
+ void GetAlgorithm(nsCString* outAlg) const { *outAlg = mAlgorithm; }
+ void GetHashType(int8_t* outType, uint32_t* outLength) const;
+
+ const nsString& GetIntegrityString() const { return mIntegrityString; }
+
+ // Return true if:
+ // - this integrity metadata is empty, or
+ // - the other integrity metadata has the same hash algorithm and also the
+ // same set of values otherwise, return false.
+ //
+ // This method simply checks if the other integrity metadata is identical to
+ // this one (if it exists), so that a load that has been checked against that
+ // other integrity metadata implies that the current integrity metadata is
+ // also satisfied.
+ bool CanTrustBeDelegatedTo(const SRIMetadata& aOther) const;
+
+ private:
+ CopyableTArray<nsCString> mHashes;
+ nsString mIntegrityString;
+ nsCString mAlgorithm;
+ int8_t mAlgorithmType;
+ bool mEmpty;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_SRIMetadata_h
diff --git a/dom/security/SecFetch.cpp b/dom/security/SecFetch.cpp
new file mode 100644
index 0000000000..17f4a23e0e
--- /dev/null
+++ b/dom/security/SecFetch.cpp
@@ -0,0 +1,411 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SecFetch.h"
+#include "nsIHttpChannel.h"
+#include "nsContentUtils.h"
+#include "nsIRedirectHistoryEntry.h"
+#include "nsIReferrerInfo.h"
+#include "mozIThirdPartyUtil.h"
+#include "nsMixedContentBlocker.h"
+#include "nsNetUtil.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/StaticPrefs_dom.h"
+
+// Helper function which maps an internal content policy type
+// to the corresponding destination for the context of SecFetch.
+nsCString MapInternalContentPolicyTypeToDest(nsContentPolicyType aType) {
+ switch (aType) {
+ case nsIContentPolicy::TYPE_OTHER:
+ return "empty"_ns;
+ case nsIContentPolicy::TYPE_INTERNAL_SCRIPT:
+ case nsIContentPolicy::TYPE_INTERNAL_SCRIPT_PRELOAD:
+ case nsIContentPolicy::TYPE_INTERNAL_MODULE:
+ case nsIContentPolicy::TYPE_INTERNAL_MODULE_PRELOAD:
+ case nsIContentPolicy::TYPE_INTERNAL_WORKER_IMPORT_SCRIPTS:
+ case nsIContentPolicy::TYPE_INTERNAL_CHROMEUTILS_COMPILED_SCRIPT:
+ case nsIContentPolicy::TYPE_INTERNAL_FRAME_MESSAGEMANAGER_SCRIPT:
+ case nsIContentPolicy::TYPE_SCRIPT:
+ return "script"_ns;
+ case nsIContentPolicy::TYPE_INTERNAL_WORKER:
+ case nsIContentPolicy::TYPE_INTERNAL_WORKER_STATIC_MODULE:
+ return "worker"_ns;
+ case nsIContentPolicy::TYPE_INTERNAL_SHARED_WORKER:
+ return "sharedworker"_ns;
+ case nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER:
+ return "serviceworker"_ns;
+ case nsIContentPolicy::TYPE_INTERNAL_AUDIOWORKLET:
+ return "audioworklet"_ns;
+ case nsIContentPolicy::TYPE_INTERNAL_PAINTWORKLET:
+ return "paintworklet"_ns;
+ case nsIContentPolicy::TYPE_IMAGESET:
+ case nsIContentPolicy::TYPE_INTERNAL_IMAGE:
+ case nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD:
+ case nsIContentPolicy::TYPE_INTERNAL_IMAGE_FAVICON:
+ case nsIContentPolicy::TYPE_IMAGE:
+ return "image"_ns;
+ case nsIContentPolicy::TYPE_STYLESHEET:
+ case nsIContentPolicy::TYPE_INTERNAL_STYLESHEET:
+ case nsIContentPolicy::TYPE_INTERNAL_STYLESHEET_PRELOAD:
+ return "style"_ns;
+ case nsIContentPolicy::TYPE_OBJECT:
+ case nsIContentPolicy::TYPE_INTERNAL_OBJECT:
+ return "object"_ns;
+ case nsIContentPolicy::TYPE_INTERNAL_EMBED:
+ return "embed"_ns;
+ case nsIContentPolicy::TYPE_DOCUMENT:
+ return "document"_ns;
+ case nsIContentPolicy::TYPE_SUBDOCUMENT:
+ case nsIContentPolicy::TYPE_INTERNAL_IFRAME:
+ return "iframe"_ns;
+ case nsIContentPolicy::TYPE_INTERNAL_FRAME:
+ return "frame"_ns;
+ case nsIContentPolicy::TYPE_PING:
+ return "empty"_ns;
+ case nsIContentPolicy::TYPE_XMLHTTPREQUEST:
+ case nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST:
+ return "empty"_ns;
+ case nsIContentPolicy::TYPE_INTERNAL_EVENTSOURCE:
+ return "empty"_ns;
+ case nsIContentPolicy::TYPE_OBJECT_SUBREQUEST:
+ return "empty"_ns;
+ case nsIContentPolicy::TYPE_DTD:
+ case nsIContentPolicy::TYPE_INTERNAL_DTD:
+ case nsIContentPolicy::TYPE_INTERNAL_FORCE_ALLOWED_DTD:
+ return "empty"_ns;
+ case nsIContentPolicy::TYPE_FONT:
+ case nsIContentPolicy::TYPE_INTERNAL_FONT_PRELOAD:
+ case nsIContentPolicy::TYPE_UA_FONT:
+ return "font"_ns;
+ case nsIContentPolicy::TYPE_MEDIA:
+ return "empty"_ns;
+ case nsIContentPolicy::TYPE_INTERNAL_AUDIO:
+ return "audio"_ns;
+ case nsIContentPolicy::TYPE_INTERNAL_VIDEO:
+ return "video"_ns;
+ case nsIContentPolicy::TYPE_INTERNAL_TRACK:
+ return "track"_ns;
+ case nsIContentPolicy::TYPE_WEBSOCKET:
+ return "empty"_ns;
+ case nsIContentPolicy::TYPE_CSP_REPORT:
+ return "report"_ns;
+ case nsIContentPolicy::TYPE_XSLT:
+ return "xslt"_ns;
+ case nsIContentPolicy::TYPE_BEACON:
+ return "empty"_ns;
+ case nsIContentPolicy::TYPE_FETCH:
+ case nsIContentPolicy::TYPE_INTERNAL_FETCH_PRELOAD:
+ return "empty"_ns;
+ case nsIContentPolicy::TYPE_WEB_MANIFEST:
+ return "manifest"_ns;
+ case nsIContentPolicy::TYPE_SAVEAS_DOWNLOAD:
+ return "empty"_ns;
+ case nsIContentPolicy::TYPE_SPECULATIVE:
+ return "empty"_ns;
+ case nsIContentPolicy::TYPE_PROXIED_WEBRTC_MEDIA:
+ return "empty"_ns;
+ case nsIContentPolicy::TYPE_WEB_IDENTITY:
+ return "webidentity"_ns;
+ case nsIContentPolicy::TYPE_WEB_TRANSPORT:
+ return "webtransport"_ns;
+ case nsIContentPolicy::TYPE_END:
+ case nsIContentPolicy::TYPE_INVALID:
+ break;
+ // Do not add default: so that compilers can catch the missing case.
+ }
+
+ MOZ_CRASH("Unhandled nsContentPolicyType value");
+}
+
+// Helper function to determine if a ExpandedPrincipal is of the same-origin as
+// a URI in the sec-fetch context.
+void IsExpandedPrincipalSameOrigin(
+ nsCOMPtr<nsIExpandedPrincipal> aExpandedPrincipal, nsIURI* aURI,
+ bool* aRes) {
+ *aRes = false;
+ for (const auto& principal : aExpandedPrincipal->AllowList()) {
+ // Ignore extension principals to continue treating
+ // "moz-extension:"-requests as not "same-origin".
+ if (!mozilla::BasePrincipal::Cast(principal)->AddonPolicy()) {
+ // A ExpandedPrincipal usually has at most one ContentPrincipal, so we can
+ // check IsSameOrigin on it here and return early.
+ mozilla::BasePrincipal::Cast(principal)->IsSameOrigin(aURI, aRes);
+ return;
+ }
+ }
+}
+
+// Helper function to determine whether a request (including involved
+// redirects) is same-origin in the context of SecFetch.
+bool IsSameOrigin(nsIHttpChannel* aHTTPChannel) {
+ nsCOMPtr<nsIURI> channelURI;
+ NS_GetFinalChannelURI(aHTTPChannel, getter_AddRefs(channelURI));
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo();
+
+ if (mozilla::BasePrincipal::Cast(loadInfo->TriggeringPrincipal())
+ ->AddonPolicy()) {
+ // If an extension triggered the load that has access to the URI then the
+ // load is considered as same-origin.
+ return mozilla::BasePrincipal::Cast(loadInfo->TriggeringPrincipal())
+ ->AddonAllowsLoad(channelURI);
+ }
+
+ bool isSameOrigin = false;
+ if (nsContentUtils::IsExpandedPrincipal(loadInfo->TriggeringPrincipal())) {
+ nsCOMPtr<nsIExpandedPrincipal> ep =
+ do_QueryInterface(loadInfo->TriggeringPrincipal());
+ IsExpandedPrincipalSameOrigin(ep, channelURI, &isSameOrigin);
+ } else {
+ isSameOrigin = loadInfo->TriggeringPrincipal()->IsSameOrigin(channelURI);
+ }
+
+ // if the initial request is not same-origin, we can return here
+ // because we already know it's not a same-origin request
+ if (!isSameOrigin) {
+ return false;
+ }
+
+ // let's further check all the hoops in the redirectChain to
+ // ensure all involved redirects are same-origin
+ nsCOMPtr<nsIPrincipal> redirectPrincipal;
+ for (nsIRedirectHistoryEntry* entry : loadInfo->RedirectChain()) {
+ entry->GetPrincipal(getter_AddRefs(redirectPrincipal));
+ if (redirectPrincipal && !redirectPrincipal->IsSameOrigin(channelURI)) {
+ return false;
+ }
+ }
+
+ // must be a same-origin request
+ return true;
+}
+
+// Helper function to determine whether a request (including involved
+// redirects) is same-site in the context of SecFetch.
+bool IsSameSite(nsIChannel* aHTTPChannel) {
+ nsCOMPtr<mozIThirdPartyUtil> thirdPartyUtil =
+ do_GetService(THIRDPARTYUTIL_CONTRACTID);
+ if (!thirdPartyUtil) {
+ return false;
+ }
+
+ nsAutoCString hostDomain;
+ nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo();
+ nsresult rv = loadInfo->TriggeringPrincipal()->GetBaseDomain(hostDomain);
+ mozilla::Unused << NS_WARN_IF(NS_FAILED(rv));
+
+ nsAutoCString channelDomain;
+ nsCOMPtr<nsIURI> channelURI;
+ NS_GetFinalChannelURI(aHTTPChannel, getter_AddRefs(channelURI));
+ rv = thirdPartyUtil->GetBaseDomain(channelURI, channelDomain);
+ mozilla::Unused << NS_WARN_IF(NS_FAILED(rv));
+
+ // if the initial request is not same-site, or not https, we can
+ // return here because we already know it's not a same-site request
+ if (!hostDomain.Equals(channelDomain) ||
+ (!loadInfo->TriggeringPrincipal()->SchemeIs("https") &&
+ !nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackHost(
+ hostDomain))) {
+ return false;
+ }
+
+ // let's further check all the hoops in the redirectChain to
+ // ensure all involved redirects are same-site and https
+ nsCOMPtr<nsIPrincipal> redirectPrincipal;
+ for (nsIRedirectHistoryEntry* entry : loadInfo->RedirectChain()) {
+ entry->GetPrincipal(getter_AddRefs(redirectPrincipal));
+ if (redirectPrincipal) {
+ redirectPrincipal->GetBaseDomain(hostDomain);
+ if (!hostDomain.Equals(channelDomain) ||
+ !redirectPrincipal->SchemeIs("https")) {
+ return false;
+ }
+ }
+ }
+
+ // must be a same-site request
+ return true;
+}
+
+// Helper function to determine whether a request was triggered
+// by the end user in the context of SecFetch.
+bool IsUserTriggeredForSecFetchSite(nsIHttpChannel* aHTTPChannel) {
+ /*
+ * The goal is to distinguish between "webby" navigations that are controlled
+ * by a given website (e.g. links, the window.location setter,form
+ * submissions, etc.), and those that are not (e.g. user interaction with a
+ * user agent’s address bar, bookmarks, etc).
+ */
+ nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo();
+ ExtContentPolicyType contentType = loadInfo->GetExternalContentPolicyType();
+
+ // A request issued by the browser is always user initiated.
+ if (loadInfo->TriggeringPrincipal()->IsSystemPrincipal() &&
+ contentType == ExtContentPolicy::TYPE_OTHER) {
+ return true;
+ }
+
+ // only requests wich result in type "document" are subject to
+ // user initiated actions in the context of SecFetch.
+ if (contentType != ExtContentPolicy::TYPE_DOCUMENT &&
+ contentType != ExtContentPolicy::TYPE_SUBDOCUMENT) {
+ return false;
+ }
+
+ // The load is considered user triggered if it was triggered by an external
+ // application.
+ if (loadInfo->GetLoadTriggeredFromExternal()) {
+ return true;
+ }
+
+ // sec-fetch-site can only be user triggered if the load was user triggered.
+ if (!loadInfo->GetHasValidUserGestureActivation()) {
+ return false;
+ }
+
+ // We can assert that the navigation must be "webby" if the load was triggered
+ // by a meta refresh. See also Bug 1647128.
+ if (loadInfo->GetIsMetaRefresh()) {
+ return false;
+ }
+
+ // All web requests have a valid "original" referrer set in the
+ // ReferrerInfo which we can use to determine whether a request
+ // was triggered by a user or not.
+ nsCOMPtr<nsIReferrerInfo> referrerInfo = aHTTPChannel->GetReferrerInfo();
+ if (referrerInfo) {
+ nsCOMPtr<nsIURI> originalReferrer;
+ referrerInfo->GetOriginalReferrer(getter_AddRefs(originalReferrer));
+ if (originalReferrer) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void mozilla::dom::SecFetch::AddSecFetchDest(nsIHttpChannel* aHTTPChannel) {
+ nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo();
+ nsContentPolicyType contentType = loadInfo->InternalContentPolicyType();
+ nsCString dest = MapInternalContentPolicyTypeToDest(contentType);
+
+ nsresult rv =
+ aHTTPChannel->SetRequestHeader("Sec-Fetch-Dest"_ns, dest, false);
+ mozilla::Unused << NS_WARN_IF(NS_FAILED(rv));
+}
+
+void mozilla::dom::SecFetch::AddSecFetchMode(nsIHttpChannel* aHTTPChannel) {
+ nsAutoCString mode("no-cors");
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo();
+ uint32_t securityMode = loadInfo->GetSecurityMode();
+ ExtContentPolicyType externalType = loadInfo->GetExternalContentPolicyType();
+
+ if (securityMode ==
+ nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT ||
+ securityMode == nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED) {
+ mode = "same-origin"_ns;
+ } else if (securityMode ==
+ nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT) {
+ mode = "cors"_ns;
+ } else {
+ // If it's not one of the security modes above, then we ensure it's
+ // at least one of the others defined in nsILoadInfo
+ MOZ_ASSERT(
+ securityMode ==
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT ||
+ securityMode ==
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ "unhandled security mode");
+ }
+
+ if (externalType == ExtContentPolicy::TYPE_DOCUMENT ||
+ externalType == ExtContentPolicy::TYPE_SUBDOCUMENT ||
+ externalType == ExtContentPolicy::TYPE_OBJECT) {
+ mode = "navigate"_ns;
+ } else if (externalType == ExtContentPolicy::TYPE_WEBSOCKET) {
+ mode = "websocket"_ns;
+ }
+
+ nsresult rv =
+ aHTTPChannel->SetRequestHeader("Sec-Fetch-Mode"_ns, mode, false);
+ mozilla::Unused << NS_WARN_IF(NS_FAILED(rv));
+}
+
+void mozilla::dom::SecFetch::AddSecFetchSite(nsIHttpChannel* aHTTPChannel) {
+ nsAutoCString site("same-origin");
+
+ bool isSameOrigin = IsSameOrigin(aHTTPChannel);
+ if (!isSameOrigin) {
+ bool isSameSite = IsSameSite(aHTTPChannel);
+ if (isSameSite) {
+ site = "same-site"_ns;
+ } else {
+ site = "cross-site"_ns;
+ }
+ }
+
+ if (IsUserTriggeredForSecFetchSite(aHTTPChannel)) {
+ site = "none"_ns;
+ }
+
+ nsresult rv =
+ aHTTPChannel->SetRequestHeader("Sec-Fetch-Site"_ns, site, false);
+ mozilla::Unused << NS_WARN_IF(NS_FAILED(rv));
+}
+
+void mozilla::dom::SecFetch::AddSecFetchUser(nsIHttpChannel* aHTTPChannel) {
+ nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo();
+ ExtContentPolicyType externalType = loadInfo->GetExternalContentPolicyType();
+
+ // sec-fetch-user only applies to loads of type document or subdocument
+ if (externalType != ExtContentPolicy::TYPE_DOCUMENT &&
+ externalType != ExtContentPolicy::TYPE_SUBDOCUMENT) {
+ return;
+ }
+
+ // sec-fetch-user only applies if the request is user triggered.
+ // requests triggered by an external application are considerd user triggered.
+ if (!loadInfo->GetLoadTriggeredFromExternal() &&
+ !loadInfo->GetHasValidUserGestureActivation()) {
+ return;
+ }
+
+ nsAutoCString user("?1");
+ nsresult rv =
+ aHTTPChannel->SetRequestHeader("Sec-Fetch-User"_ns, user, false);
+ mozilla::Unused << NS_WARN_IF(NS_FAILED(rv));
+}
+
+void mozilla::dom::SecFetch::AddSecFetchHeader(nsIHttpChannel* aHTTPChannel) {
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = aHTTPChannel->GetURI(getter_AddRefs(uri));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ // if we are not dealing with a potentially trustworthy URL, then
+ // there is nothing to do here
+ if (!nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(uri)) {
+ return;
+ }
+
+ // If we're dealing with a system XMLHttpRequest or fetch, don't add
+ // Sec- headers.
+ nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo();
+ if (loadInfo->TriggeringPrincipal()->IsSystemPrincipal()) {
+ ExtContentPolicy extType = loadInfo->GetExternalContentPolicyType();
+ if (extType == ExtContentPolicy::TYPE_FETCH ||
+ extType == ExtContentPolicy::TYPE_XMLHTTPREQUEST) {
+ return;
+ }
+ }
+
+ AddSecFetchDest(aHTTPChannel);
+ AddSecFetchMode(aHTTPChannel);
+ AddSecFetchSite(aHTTPChannel);
+ AddSecFetchUser(aHTTPChannel);
+}
diff --git a/dom/security/SecFetch.h b/dom/security/SecFetch.h
new file mode 100644
index 0000000000..b4b1495f4d
--- /dev/null
+++ b/dom/security/SecFetch.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SecFetch_h
+#define mozilla_dom_SecFetch_h
+
+class nsIHttpChannel;
+
+namespace mozilla::dom {
+
+class SecFetch final {
+ public:
+ static void AddSecFetchHeader(nsIHttpChannel* aHTTPChannel);
+
+ private:
+ static void AddSecFetchDest(nsIHttpChannel* aHTTPChannel);
+ static void AddSecFetchMode(nsIHttpChannel* aHTTPChannel);
+ static void AddSecFetchSite(nsIHttpChannel* aHTTPChannel);
+ static void AddSecFetchUser(nsIHttpChannel* aHTTPChannel);
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_SecFetch_h
diff --git a/dom/security/featurepolicy/Feature.cpp b/dom/security/featurepolicy/Feature.cpp
new file mode 100644
index 0000000000..92a92369da
--- /dev/null
+++ b/dom/security/featurepolicy/Feature.cpp
@@ -0,0 +1,76 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Feature.h"
+#include "mozilla/BasePrincipal.h"
+
+namespace mozilla::dom {
+
+void Feature::GetAllowList(nsTArray<nsCOMPtr<nsIPrincipal>>& aList) const {
+ MOZ_ASSERT(mPolicy == eAllowList);
+ aList.AppendElements(mAllowList);
+}
+
+bool Feature::Allows(nsIPrincipal* aPrincipal) const {
+ if (mPolicy == eNone) {
+ return false;
+ }
+
+ if (mPolicy == eAll) {
+ return true;
+ }
+
+ return AllowListContains(aPrincipal);
+}
+
+Feature::Feature(const nsAString& aFeatureName)
+ : mFeatureName(aFeatureName), mPolicy(eAllowList) {}
+
+Feature::~Feature() = default;
+
+const nsAString& Feature::Name() const { return mFeatureName; }
+
+void Feature::SetAllowsNone() {
+ mPolicy = eNone;
+ mAllowList.Clear();
+}
+
+bool Feature::AllowsNone() const { return mPolicy == eNone; }
+
+void Feature::SetAllowsAll() {
+ mPolicy = eAll;
+ mAllowList.Clear();
+}
+
+bool Feature::AllowsAll() const { return mPolicy == eAll; }
+
+void Feature::AppendToAllowList(nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(aPrincipal);
+
+ mPolicy = eAllowList;
+ mAllowList.AppendElement(aPrincipal);
+}
+
+bool Feature::AllowListContains(nsIPrincipal* aPrincipal) const {
+ MOZ_ASSERT(aPrincipal);
+
+ if (!HasAllowList()) {
+ return false;
+ }
+
+ for (nsIPrincipal* principal : mAllowList) {
+ if (BasePrincipal::Cast(principal)->Subsumes(
+ aPrincipal, BasePrincipal::ConsiderDocumentDomain)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool Feature::HasAllowList() const { return mPolicy == eAllowList; }
+
+} // namespace mozilla::dom
diff --git a/dom/security/featurepolicy/Feature.h b/dom/security/featurepolicy/Feature.h
new file mode 100644
index 0000000000..e8a3d89c81
--- /dev/null
+++ b/dom/security/featurepolicy/Feature.h
@@ -0,0 +1,65 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_Feature_h
+#define mozilla_dom_Feature_h
+
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsCOMPtr.h"
+
+class nsIPrincipal;
+
+namespace mozilla::dom {
+
+class Feature final {
+ public:
+ explicit Feature(const nsAString& aFeatureName);
+
+ ~Feature();
+
+ const nsAString& Name() const;
+
+ void SetAllowsNone();
+
+ bool AllowsNone() const;
+
+ void SetAllowsAll();
+
+ bool AllowsAll() const;
+
+ void AppendToAllowList(nsIPrincipal* aPrincipal);
+
+ void GetAllowList(nsTArray<nsCOMPtr<nsIPrincipal>>& aList) const;
+
+ bool AllowListContains(nsIPrincipal* aPrincipal) const;
+
+ bool HasAllowList() const;
+
+ bool Allows(nsIPrincipal* aPrincipal) const;
+
+ private:
+ nsString mFeatureName;
+
+ enum Policy {
+ // denotes a policy of "feature 'none'"
+ eNone,
+
+ // denotes a policy of "feature *"
+ eAll,
+
+ // denotes a policy of "feature bar.com foo.com"
+ eAllowList,
+ };
+
+ Policy mPolicy;
+
+ CopyableTArray<nsCOMPtr<nsIPrincipal>> mAllowList;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_Feature_h
diff --git a/dom/security/featurepolicy/FeaturePolicy.cpp b/dom/security/featurepolicy/FeaturePolicy.cpp
new file mode 100644
index 0000000000..cb5c1ea44a
--- /dev/null
+++ b/dom/security/featurepolicy/FeaturePolicy.cpp
@@ -0,0 +1,334 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FeaturePolicy.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/dom/Feature.h"
+#include "mozilla/dom/FeaturePolicyBinding.h"
+#include "mozilla/dom/FeaturePolicyParser.h"
+#include "mozilla/dom/FeaturePolicyUtils.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "nsContentUtils.h"
+#include "nsNetUtil.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FeaturePolicy)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(FeaturePolicy)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(FeaturePolicy)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FeaturePolicy)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+FeaturePolicy::FeaturePolicy(nsINode* aNode) : mParentNode(aNode) {}
+
+void FeaturePolicy::InheritPolicy(FeaturePolicy* aParentPolicy) {
+ MOZ_ASSERT(aParentPolicy);
+
+ mInheritedDeniedFeatureNames.Clear();
+
+ RefPtr<FeaturePolicy> dest = this;
+ RefPtr<FeaturePolicy> src = aParentPolicy;
+
+ // Inherit origins which explicitly declared policy in chain
+ for (const Feature& featureInChain :
+ aParentPolicy->mDeclaredFeaturesInAncestorChain) {
+ dest->AppendToDeclaredAllowInAncestorChain(featureInChain);
+ }
+
+ FeaturePolicyUtils::ForEachFeature([dest, src](const char* aFeatureName) {
+ nsString featureName;
+ featureName.AppendASCII(aFeatureName);
+ // Store unsafe allows all (allow=*)
+ if (src->HasFeatureUnsafeAllowsAll(featureName)) {
+ dest->mParentAllowedAllFeatures.AppendElement(featureName);
+ }
+
+ // If the destination has a declared feature (via the HTTP header or 'allow'
+ // attribute) we allow the feature if the destination allows it and the
+ // parent allows its origin or the destinations' one.
+ if (dest->HasDeclaredFeature(featureName) &&
+ dest->AllowsFeatureInternal(featureName, dest->mDefaultOrigin)) {
+ if (!src->AllowsFeatureInternal(featureName, src->mDefaultOrigin) &&
+ !src->AllowsFeatureInternal(featureName, dest->mDefaultOrigin)) {
+ dest->SetInheritedDeniedFeature(featureName);
+ }
+ return;
+ }
+
+ // If there was not a declared feature, we allow the feature if the parent
+ // FeaturePolicy allows the current origin.
+ if (!src->AllowsFeatureInternal(featureName, dest->mDefaultOrigin)) {
+ dest->SetInheritedDeniedFeature(featureName);
+ }
+ });
+}
+
+void FeaturePolicy::SetInheritedDeniedFeature(const nsAString& aFeatureName) {
+ MOZ_ASSERT(!HasInheritedDeniedFeature(aFeatureName));
+ mInheritedDeniedFeatureNames.AppendElement(aFeatureName);
+}
+
+bool FeaturePolicy::HasInheritedDeniedFeature(
+ const nsAString& aFeatureName) const {
+ return mInheritedDeniedFeatureNames.Contains(aFeatureName);
+}
+
+bool FeaturePolicy::HasDeclaredFeature(const nsAString& aFeatureName) const {
+ for (const Feature& feature : mFeatures) {
+ if (feature.Name().Equals(aFeatureName)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool FeaturePolicy::HasFeatureUnsafeAllowsAll(
+ const nsAString& aFeatureName) const {
+ for (const Feature& feature : mFeatures) {
+ if (feature.AllowsAll() && feature.Name().Equals(aFeatureName)) {
+ return true;
+ }
+ }
+
+ // We should look into parent too (for example, document of iframe which
+ // allows all, would be unsafe)
+ return mParentAllowedAllFeatures.Contains(aFeatureName);
+}
+
+void FeaturePolicy::AppendToDeclaredAllowInAncestorChain(
+ const Feature& aFeature) {
+ for (Feature& featureInChain : mDeclaredFeaturesInAncestorChain) {
+ if (featureInChain.Name().Equals(aFeature.Name())) {
+ MOZ_ASSERT(featureInChain.HasAllowList());
+
+ nsTArray<nsCOMPtr<nsIPrincipal>> list;
+ aFeature.GetAllowList(list);
+
+ for (nsIPrincipal* principal : list) {
+ featureInChain.AppendToAllowList(principal);
+ }
+ continue;
+ }
+ }
+
+ mDeclaredFeaturesInAncestorChain.AppendElement(aFeature);
+}
+
+bool FeaturePolicy::IsSameOriginAsSrc(nsIPrincipal* aPrincipal) const {
+ MOZ_ASSERT(aPrincipal);
+
+ if (!mSrcOrigin) {
+ return false;
+ }
+
+ return BasePrincipal::Cast(mSrcOrigin)
+ ->Subsumes(aPrincipal, BasePrincipal::ConsiderDocumentDomain);
+}
+
+void FeaturePolicy::SetDeclaredPolicy(Document* aDocument,
+ const nsAString& aPolicyString,
+ nsIPrincipal* aSelfOrigin,
+ nsIPrincipal* aSrcOrigin) {
+ ResetDeclaredPolicy();
+
+ mDeclaredString = aPolicyString;
+ mSelfOrigin = aSelfOrigin;
+ mSrcOrigin = aSrcOrigin;
+
+ Unused << NS_WARN_IF(!FeaturePolicyParser::ParseString(
+ aPolicyString, aDocument, aSelfOrigin, aSrcOrigin, mFeatures));
+
+ // Only store explicitly declared allowlist
+ for (const Feature& feature : mFeatures) {
+ if (feature.HasAllowList()) {
+ AppendToDeclaredAllowInAncestorChain(feature);
+ }
+ }
+}
+
+void FeaturePolicy::ResetDeclaredPolicy() {
+ mFeatures.Clear();
+ mDeclaredString.Truncate();
+ mSelfOrigin = nullptr;
+ mSrcOrigin = nullptr;
+ mDeclaredFeaturesInAncestorChain.Clear();
+ mAttributeEnabledFeatureNames.Clear();
+}
+
+JSObject* FeaturePolicy::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return FeaturePolicy_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+bool FeaturePolicy::AllowsFeature(const nsAString& aFeatureName,
+ const Optional<nsAString>& aOrigin) const {
+ nsCOMPtr<nsIPrincipal> origin;
+ if (aOrigin.WasPassed()) {
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), aOrigin.Value());
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+ origin = BasePrincipal::CreateContentPrincipal(
+ uri, BasePrincipal::Cast(mDefaultOrigin)->OriginAttributesRef());
+ } else {
+ origin = mDefaultOrigin;
+ }
+
+ if (NS_WARN_IF(!origin)) {
+ return false;
+ }
+
+ return AllowsFeatureInternal(aFeatureName, origin);
+}
+
+bool FeaturePolicy::AllowsFeatureExplicitlyInAncestorChain(
+ const nsAString& aFeatureName, nsIPrincipal* aOrigin) const {
+ MOZ_ASSERT(aOrigin);
+
+ for (const Feature& feature : mDeclaredFeaturesInAncestorChain) {
+ if (feature.Name().Equals(aFeatureName)) {
+ return feature.AllowListContains(aOrigin);
+ }
+ }
+
+ return false;
+}
+
+bool FeaturePolicy::AllowsFeatureInternal(const nsAString& aFeatureName,
+ nsIPrincipal* aOrigin) const {
+ MOZ_ASSERT(aOrigin);
+
+ // Let's see if have to disable this feature because inherited policy.
+ if (HasInheritedDeniedFeature(aFeatureName)) {
+ return false;
+ }
+
+ for (const Feature& feature : mFeatures) {
+ if (feature.Name().Equals(aFeatureName)) {
+ return feature.Allows(aOrigin);
+ }
+ }
+
+ switch (FeaturePolicyUtils::DefaultAllowListFeature(aFeatureName)) {
+ case FeaturePolicyUtils::FeaturePolicyValue::eAll:
+ return true;
+
+ case FeaturePolicyUtils::FeaturePolicyValue::eSelf:
+ return BasePrincipal::Cast(mDefaultOrigin)
+ ->Subsumes(aOrigin, BasePrincipal::ConsiderDocumentDomain);
+
+ case FeaturePolicyUtils::FeaturePolicyValue::eNone:
+ return false;
+
+ default:
+ MOZ_CRASH("Unknown default value");
+ }
+
+ return false;
+}
+
+void FeaturePolicy::Features(nsTArray<nsString>& aFeatures) {
+ RefPtr<FeaturePolicy> self = this;
+ FeaturePolicyUtils::ForEachFeature(
+ [self, &aFeatures](const char* aFeatureName) {
+ nsString featureName;
+ featureName.AppendASCII(aFeatureName);
+ aFeatures.AppendElement(featureName);
+ });
+}
+
+void FeaturePolicy::AllowedFeatures(nsTArray<nsString>& aAllowedFeatures) {
+ RefPtr<FeaturePolicy> self = this;
+ FeaturePolicyUtils::ForEachFeature(
+ [self, &aAllowedFeatures](const char* aFeatureName) {
+ nsString featureName;
+ featureName.AppendASCII(aFeatureName);
+
+ if (self->AllowsFeatureInternal(featureName, self->mDefaultOrigin)) {
+ aAllowedFeatures.AppendElement(featureName);
+ }
+ });
+}
+
+void FeaturePolicy::GetAllowlistForFeature(const nsAString& aFeatureName,
+ nsTArray<nsString>& aList) const {
+ if (!AllowsFeatureInternal(aFeatureName, mDefaultOrigin)) {
+ return;
+ }
+
+ for (const Feature& feature : mFeatures) {
+ if (feature.Name().Equals(aFeatureName)) {
+ if (feature.AllowsAll()) {
+ aList.AppendElement(u"*"_ns);
+ return;
+ }
+
+ nsTArray<nsCOMPtr<nsIPrincipal>> list;
+ feature.GetAllowList(list);
+
+ for (nsIPrincipal* principal : list) {
+ nsAutoCString originNoSuffix;
+ nsresult rv = principal->GetOriginNoSuffix(originNoSuffix);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ aList.AppendElement(NS_ConvertUTF8toUTF16(originNoSuffix));
+ }
+ return;
+ }
+ }
+
+ switch (FeaturePolicyUtils::DefaultAllowListFeature(aFeatureName)) {
+ case FeaturePolicyUtils::FeaturePolicyValue::eAll:
+ aList.AppendElement(u"*"_ns);
+ return;
+
+ case FeaturePolicyUtils::FeaturePolicyValue::eSelf: {
+ nsAutoCString originNoSuffix;
+ nsresult rv = mDefaultOrigin->GetOriginNoSuffix(originNoSuffix);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ aList.AppendElement(NS_ConvertUTF8toUTF16(originNoSuffix));
+ return;
+ }
+
+ case FeaturePolicyUtils::FeaturePolicyValue::eNone:
+ return;
+
+ default:
+ MOZ_CRASH("Unknown default value");
+ }
+}
+
+void FeaturePolicy::MaybeSetAllowedPolicy(const nsAString& aFeatureName) {
+ MOZ_ASSERT(FeaturePolicyUtils::IsSupportedFeature(aFeatureName) ||
+ FeaturePolicyUtils::IsExperimentalFeature(aFeatureName));
+ // Skip if feature is in experimental phase
+ if (!StaticPrefs::dom_security_featurePolicy_experimental_enabled() &&
+ FeaturePolicyUtils::IsExperimentalFeature(aFeatureName)) {
+ return;
+ }
+
+ if (HasDeclaredFeature(aFeatureName)) {
+ return;
+ }
+
+ Feature feature(aFeatureName);
+ feature.SetAllowsAll();
+
+ mFeatures.AppendElement(feature);
+ mAttributeEnabledFeatureNames.AppendElement(aFeatureName);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/security/featurepolicy/FeaturePolicy.h b/dom/security/featurepolicy/FeaturePolicy.h
new file mode 100644
index 0000000000..65f5259749
--- /dev/null
+++ b/dom/security/featurepolicy/FeaturePolicy.h
@@ -0,0 +1,204 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_FeaturePolicy_h
+#define mozilla_dom_FeaturePolicy_h
+
+#include "nsCycleCollectionParticipant.h"
+#include "nsIPrincipal.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+#include "nsWrapperCache.h"
+
+/**
+ * FeaturePolicy
+ * ~~~~~~~~~~~~~
+ *
+ * Each document and each HTMLIFrameElement have a FeaturePolicy object which is
+ * used to allow or deny features in their contexts.
+ *
+ * FeaturePolicy is composed by a set of directives configured by the
+ * 'Feature-Policy' HTTP Header and the 'allow' attribute in HTMLIFrameElements.
+ * Both header and attribute are parsed by FeaturePolicyParser which returns an
+ * array of Feature objects. Each Feature object has a feature name and one of
+ * these policies:
+ * - eNone - the feature is fully disabled.
+ * - eAll - the feature is allowed.
+ * - eAllowList - the feature is allowed for a list of origins.
+ *
+ * An interesting element of FeaturePolicy is the inheritance: each context
+ * inherits the feature-policy directives from the parent context, if it exists.
+ * When a context inherits a policy for feature X, it only knows if that feature
+ * is allowed or denied (it ignores the list of allowed origins for instance).
+ * This information is stored in an array of inherited feature strings because
+ * we care only to know when they are denied.
+ *
+ * FeaturePolicy can be reset if the 'allow' or 'src' attributes change in
+ * HTMLIFrameElements. 'src' attribute is important to compute correcly
+ * the features via FeaturePolicy 'src' keyword.
+ *
+ * When FeaturePolicy must decide if feature X is allowed or denied for the
+ * current origin, it checks if the parent context denied that feature.
+ * If not, it checks if there is a Feature object for that
+ * feature named X and if the origin is allowed or not.
+ *
+ * From a C++ point of view, use FeaturePolicyUtils to obtain the list of
+ * features and to check if they are allowed in the current context.
+ *
+ * dom.security.featurePolicy.header.enabled pref can be used to disable the
+ * HTTP header support.
+ **/
+
+class nsINode;
+
+namespace mozilla::dom {
+class Document;
+class Feature;
+template <typename T>
+class Optional;
+
+class FeaturePolicyUtils;
+
+class FeaturePolicy final : public nsISupports, public nsWrapperCache {
+ friend class FeaturePolicyUtils;
+
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(FeaturePolicy)
+
+ explicit FeaturePolicy(nsINode* aNode);
+
+ // A FeaturePolicy must have a default origin.
+ // This method must be called before any other exposed WebIDL method or before
+ // checking if a feature is allowed.
+ void SetDefaultOrigin(nsIPrincipal* aPrincipal) {
+ mDefaultOrigin = aPrincipal;
+ }
+
+ void SetSrcOrigin(nsIPrincipal* aPrincipal) { mSrcOrigin = aPrincipal; }
+
+ nsIPrincipal* DefaultOrigin() const { return mDefaultOrigin; }
+
+ // Inherits the policy from the 'parent' context if it exists.
+ void InheritPolicy(FeaturePolicy* aParentFeaturePolicy);
+
+ // Sets the declarative part of the policy. This can be from the HTTP header
+ // or for the 'allow' HTML attribute.
+ void SetDeclaredPolicy(mozilla::dom::Document* aDocument,
+ const nsAString& aPolicyString,
+ nsIPrincipal* aSelfOrigin, nsIPrincipal* aSrcOrigin);
+
+ // This method creates a policy for aFeatureName allowing it to '*' if it
+ // doesn't exist yet. It's used by HTMLIFrameElement to enable features by
+ // attributes.
+ void MaybeSetAllowedPolicy(const nsAString& aFeatureName);
+
+ // Clears all the declarative policy directives. This is needed when the
+ // 'allow' attribute or the 'src' attribute change for HTMLIFrameElement's
+ // policy.
+ void ResetDeclaredPolicy();
+
+ // This method appends a feature to in-chain declared allowlist. If the name's
+ // feature existed in the list, we only need to append the allowlist of new
+ // feature to the existed one.
+ void AppendToDeclaredAllowInAncestorChain(const Feature& aFeature);
+
+ // This method returns true if aFeatureName is declared as "*" (allow all)
+ // in parent.
+ bool HasFeatureUnsafeAllowsAll(const nsAString& aFeatureName) const;
+
+ // This method returns true if the aFeatureName is allowed for aOrigin
+ // explicitly in ancestor chain,
+ bool AllowsFeatureExplicitlyInAncestorChain(const nsAString& aFeatureName,
+ nsIPrincipal* aOrigin) const;
+
+ bool IsSameOriginAsSrc(nsIPrincipal* aPrincipal) const;
+
+ // WebIDL internal methods.
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ nsINode* GetParentObject() const { return mParentNode; }
+
+ // WebIDL explosed methods.
+
+ bool AllowsFeature(const nsAString& aFeatureName,
+ const Optional<nsAString>& aOrigin) const;
+
+ void Features(nsTArray<nsString>& aFeatures);
+
+ void AllowedFeatures(nsTArray<nsString>& aAllowedFeatures);
+
+ void GetAllowlistForFeature(const nsAString& aFeatureName,
+ nsTArray<nsString>& aList) const;
+
+ const nsTArray<nsString>& InheritedDeniedFeatureNames() const {
+ return mInheritedDeniedFeatureNames;
+ }
+
+ const nsTArray<nsString>& AttributeEnabledFeatureNames() const {
+ return mAttributeEnabledFeatureNames;
+ }
+
+ void SetInheritedDeniedFeatureNames(
+ const nsTArray<nsString>& aInheritedDeniedFeatureNames) {
+ mInheritedDeniedFeatureNames = aInheritedDeniedFeatureNames.Clone();
+ }
+
+ const nsAString& DeclaredString() const { return mDeclaredString; }
+
+ nsIPrincipal* GetSelfOrigin() const { return mSelfOrigin; }
+ nsIPrincipal* GetSrcOrigin() const { return mSrcOrigin; }
+
+ private:
+ ~FeaturePolicy() = default;
+
+ // This method returns true if the aFeatureName is allowed for aOrigin,
+ // following the feature-policy directives. See the comment at the top of this
+ // file.
+ bool AllowsFeatureInternal(const nsAString& aFeatureName,
+ nsIPrincipal* aOrigin) const;
+
+ // Inherits a single denied feature from the parent context.
+ void SetInheritedDeniedFeature(const nsAString& aFeatureName);
+
+ bool HasInheritedDeniedFeature(const nsAString& aFeatureName) const;
+
+ // This returns true if we have a declared feature policy for aFeatureName.
+ bool HasDeclaredFeature(const nsAString& aFeatureName) const;
+
+ nsINode* mParentNode;
+
+ // This is set in sub-contexts when the parent blocks some feature for the
+ // current context.
+ nsTArray<nsString> mInheritedDeniedFeatureNames;
+
+ // The list of features that have been enabled via MaybeSetAllowedPolicy.
+ nsTArray<nsString> mAttributeEnabledFeatureNames;
+
+ // This is set of feature names when the parent allows all for that feature.
+ nsTArray<nsString> mParentAllowedAllFeatures;
+
+ // The explicitly declared policy contains allowlist as a set of origins
+ // except 'none' and '*'. This set contains all explicitly declared policies
+ // in ancestor chain
+ nsTArray<Feature> mDeclaredFeaturesInAncestorChain;
+
+ // Feature policy for the current context.
+ nsTArray<Feature> mFeatures;
+
+ // Declared string represents Feature policy.
+ nsString mDeclaredString;
+
+ nsCOMPtr<nsIPrincipal> mDefaultOrigin;
+ nsCOMPtr<nsIPrincipal> mSelfOrigin;
+ nsCOMPtr<nsIPrincipal> mSrcOrigin;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_FeaturePolicy_h
diff --git a/dom/security/featurepolicy/FeaturePolicyParser.cpp b/dom/security/featurepolicy/FeaturePolicyParser.cpp
new file mode 100644
index 0000000000..8ab95420aa
--- /dev/null
+++ b/dom/security/featurepolicy/FeaturePolicyParser.cpp
@@ -0,0 +1,157 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FeaturePolicyParser.h"
+
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/dom/Feature.h"
+#include "mozilla/dom/FeaturePolicyUtils.h"
+#include "mozilla/dom/PolicyTokenizer.h"
+#include "nsIScriptError.h"
+#include "nsIURI.h"
+#include "nsNetUtil.h"
+
+namespace mozilla::dom {
+
+namespace {
+
+void ReportToConsoleUnsupportedFeature(Document* aDocument,
+ const nsString& aFeatureName) {
+ if (!aDocument) {
+ return;
+ }
+
+ AutoTArray<nsString, 1> params = {aFeatureName};
+
+ nsContentUtils::ReportToConsole(
+ nsIScriptError::warningFlag, "Feature Policy"_ns, aDocument,
+ nsContentUtils::eSECURITY_PROPERTIES,
+ "FeaturePolicyUnsupportedFeatureName", params);
+}
+
+void ReportToConsoleInvalidEmptyAllowValue(Document* aDocument,
+ const nsString& aFeatureName) {
+ if (!aDocument) {
+ return;
+ }
+
+ AutoTArray<nsString, 1> params = {aFeatureName};
+
+ nsContentUtils::ReportToConsole(
+ nsIScriptError::warningFlag, "Feature Policy"_ns, aDocument,
+ nsContentUtils::eSECURITY_PROPERTIES,
+ "FeaturePolicyInvalidEmptyAllowValue", params);
+}
+
+void ReportToConsoleInvalidAllowValue(Document* aDocument,
+ const nsString& aValue) {
+ if (!aDocument) {
+ return;
+ }
+
+ AutoTArray<nsString, 1> params = {aValue};
+
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
+ "Feature Policy"_ns, aDocument,
+ nsContentUtils::eSECURITY_PROPERTIES,
+ "FeaturePolicyInvalidAllowValue", params);
+}
+
+} // namespace
+
+/* static */
+bool FeaturePolicyParser::ParseString(const nsAString& aPolicy,
+ Document* aDocument,
+ nsIPrincipal* aSelfOrigin,
+ nsIPrincipal* aSrcOrigin,
+ nsTArray<Feature>& aParsedFeatures) {
+ MOZ_ASSERT(aSelfOrigin);
+
+ nsTArray<CopyableTArray<nsString>> tokens;
+ PolicyTokenizer::tokenizePolicy(aPolicy, tokens);
+
+ nsTArray<Feature> parsedFeatures;
+
+ for (const nsTArray<nsString>& featureTokens : tokens) {
+ if (featureTokens.IsEmpty()) {
+ continue;
+ }
+
+ if (!FeaturePolicyUtils::IsSupportedFeature(featureTokens[0])) {
+ ReportToConsoleUnsupportedFeature(aDocument, featureTokens[0]);
+ continue;
+ }
+
+ Feature feature(featureTokens[0]);
+
+ if (featureTokens.Length() == 1) {
+ if (aSrcOrigin) {
+ feature.AppendToAllowList(aSrcOrigin);
+ } else {
+ ReportToConsoleInvalidEmptyAllowValue(aDocument, featureTokens[0]);
+ continue;
+ }
+ } else {
+ // we gotta start at 1 here
+ for (uint32_t i = 1; i < featureTokens.Length(); ++i) {
+ const nsString& curVal = featureTokens[i];
+ if (curVal.LowerCaseEqualsASCII("'none'")) {
+ feature.SetAllowsNone();
+ break;
+ }
+
+ if (curVal.EqualsLiteral("*")) {
+ feature.SetAllowsAll();
+ break;
+ }
+
+ if (curVal.LowerCaseEqualsASCII("'self'")) {
+ feature.AppendToAllowList(aSelfOrigin);
+ continue;
+ }
+
+ if (aSrcOrigin && curVal.LowerCaseEqualsASCII("'src'")) {
+ feature.AppendToAllowList(aSrcOrigin);
+ continue;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), curVal);
+ if (NS_FAILED(rv)) {
+ ReportToConsoleInvalidAllowValue(aDocument, curVal);
+ continue;
+ }
+
+ nsCOMPtr<nsIPrincipal> origin = BasePrincipal::CreateContentPrincipal(
+ uri, BasePrincipal::Cast(aSelfOrigin)->OriginAttributesRef());
+ if (NS_WARN_IF(!origin)) {
+ ReportToConsoleInvalidAllowValue(aDocument, curVal);
+ continue;
+ }
+
+ feature.AppendToAllowList(origin);
+ }
+ }
+
+ // No duplicate!
+ bool found = false;
+ for (const Feature& parsedFeature : parsedFeatures) {
+ if (parsedFeature.Name() == feature.Name()) {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ parsedFeatures.AppendElement(feature);
+ }
+ }
+
+ aParsedFeatures = std::move(parsedFeatures);
+ return true;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/security/featurepolicy/FeaturePolicyParser.h b/dom/security/featurepolicy/FeaturePolicyParser.h
new file mode 100644
index 0000000000..a60a391a78
--- /dev/null
+++ b/dom/security/featurepolicy/FeaturePolicyParser.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_FeaturePolicyParser_h
+#define mozilla_dom_FeaturePolicyParser_h
+
+#include "nsString.h"
+
+class nsIPrincipal;
+
+namespace mozilla::dom {
+
+class Document;
+class Feature;
+
+class FeaturePolicyParser final {
+ public:
+ // aSelfOrigin must not be null. if aSrcOrigin is null, the parsing will not
+ // support 'src' as valid allow directive value.
+ static bool ParseString(const nsAString& aPolicy, Document* aDocument,
+ nsIPrincipal* aSelfOrigin, nsIPrincipal* aSrcOrigin,
+ nsTArray<Feature>& aParsedFeatures);
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_FeaturePolicyParser_h
diff --git a/dom/security/featurepolicy/FeaturePolicyUtils.cpp b/dom/security/featurepolicy/FeaturePolicyUtils.cpp
new file mode 100644
index 0000000000..4e0bb92e0a
--- /dev/null
+++ b/dom/security/featurepolicy/FeaturePolicyUtils.cpp
@@ -0,0 +1,315 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FeaturePolicyUtils.h"
+#include "nsIOService.h"
+
+#include "mozilla/dom/DOMTypes.h"
+#include "mozilla/ipc/IPDLParamTraits.h"
+#include "mozilla/dom/FeaturePolicyViolationReportBody.h"
+#include "mozilla/dom/ReportingUtils.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/dom/Document.h"
+#include "nsContentUtils.h"
+#include "nsJSUtils.h"
+
+namespace mozilla {
+namespace dom {
+
+struct FeatureMap {
+ const char* mFeatureName;
+ FeaturePolicyUtils::FeaturePolicyValue mDefaultAllowList;
+};
+
+/*
+ * IMPORTANT: Do not change this list without review from a DOM peer _AND_ a
+ * DOM Security peer!
+ */
+static FeatureMap sSupportedFeatures[] = {
+ {"camera", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
+ {"geolocation", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
+ {"microphone", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
+ {"display-capture", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
+ {"fullscreen", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
+ {"web-share", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
+ {"gamepad", FeaturePolicyUtils::FeaturePolicyValue::eAll},
+ {"publickey-credentials-create",
+ FeaturePolicyUtils::FeaturePolicyValue::eSelf},
+ {"publickey-credentials-get",
+ FeaturePolicyUtils::FeaturePolicyValue::eSelf},
+ {"speaker-selection", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
+ {"storage-access", FeaturePolicyUtils::FeaturePolicyValue::eAll},
+ {"screen-wake-lock", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
+};
+
+/*
+ * This is experimental features list, which is disabled by default by pref
+ * dom.security.featurePolicy.experimental.enabled.
+ */
+static FeatureMap sExperimentalFeatures[] = {
+ // We don't support 'autoplay' for now, because it would be overwrote by
+ // 'user-gesture-activation' policy. However, we can still keep it in the
+ // list as we might start supporting it after we use different autoplay
+ // policy.
+ {"autoplay", FeaturePolicyUtils::FeaturePolicyValue::eAll},
+ {"encrypted-media", FeaturePolicyUtils::FeaturePolicyValue::eAll},
+ {"midi", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
+ {"payment", FeaturePolicyUtils::FeaturePolicyValue::eAll},
+ {"document-domain", FeaturePolicyUtils::FeaturePolicyValue::eAll},
+ {"vr", FeaturePolicyUtils::FeaturePolicyValue::eAll},
+ // https://immersive-web.github.io/webxr/#feature-policy
+ {"xr-spatial-tracking", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
+};
+
+/* static */
+bool FeaturePolicyUtils::IsExperimentalFeature(const nsAString& aFeatureName) {
+ uint32_t numFeatures =
+ (sizeof(sExperimentalFeatures) / sizeof(sExperimentalFeatures[0]));
+ for (uint32_t i = 0; i < numFeatures; ++i) {
+ if (aFeatureName.LowerCaseEqualsASCII(
+ sExperimentalFeatures[i].mFeatureName)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/* static */
+bool FeaturePolicyUtils::IsSupportedFeature(const nsAString& aFeatureName) {
+ uint32_t numFeatures =
+ (sizeof(sSupportedFeatures) / sizeof(sSupportedFeatures[0]));
+ for (uint32_t i = 0; i < numFeatures; ++i) {
+ if (aFeatureName.LowerCaseEqualsASCII(sSupportedFeatures[i].mFeatureName)) {
+ return true;
+ }
+ }
+
+ return StaticPrefs::dom_security_featurePolicy_experimental_enabled() &&
+ IsExperimentalFeature(aFeatureName);
+}
+
+/* static */
+void FeaturePolicyUtils::ForEachFeature(
+ const std::function<void(const char*)>& aCallback) {
+ uint32_t numFeatures =
+ (sizeof(sSupportedFeatures) / sizeof(sSupportedFeatures[0]));
+ for (uint32_t i = 0; i < numFeatures; ++i) {
+ aCallback(sSupportedFeatures[i].mFeatureName);
+ }
+
+ if (StaticPrefs::dom_security_featurePolicy_experimental_enabled()) {
+ numFeatures =
+ (sizeof(sExperimentalFeatures) / sizeof(sExperimentalFeatures[0]));
+ for (uint32_t i = 0; i < numFeatures; ++i) {
+ aCallback(sExperimentalFeatures[i].mFeatureName);
+ }
+ }
+}
+
+/* static */ FeaturePolicyUtils::FeaturePolicyValue
+FeaturePolicyUtils::DefaultAllowListFeature(const nsAString& aFeatureName) {
+ uint32_t numFeatures =
+ (sizeof(sSupportedFeatures) / sizeof(sSupportedFeatures[0]));
+ for (uint32_t i = 0; i < numFeatures; ++i) {
+ if (aFeatureName.LowerCaseEqualsASCII(sSupportedFeatures[i].mFeatureName)) {
+ return sSupportedFeatures[i].mDefaultAllowList;
+ }
+ }
+
+ if (StaticPrefs::dom_security_featurePolicy_experimental_enabled()) {
+ numFeatures =
+ (sizeof(sExperimentalFeatures) / sizeof(sExperimentalFeatures[0]));
+ for (uint32_t i = 0; i < numFeatures; ++i) {
+ if (aFeatureName.LowerCaseEqualsASCII(
+ sExperimentalFeatures[i].mFeatureName)) {
+ return sExperimentalFeatures[i].mDefaultAllowList;
+ }
+ }
+ }
+
+ return FeaturePolicyValue::eNone;
+}
+
+static bool IsSameOriginAsTop(Document* aDocument) {
+ MOZ_ASSERT(aDocument);
+
+ BrowsingContext* browsingContext = aDocument->GetBrowsingContext();
+ if (!browsingContext) {
+ return false;
+ }
+
+ nsPIDOMWindowOuter* topWindow = browsingContext->Top()->GetDOMWindow();
+ if (!topWindow) {
+ // If we don't have a DOMWindow, We are not in same origin.
+ return false;
+ }
+
+ Document* topLevelDocument = topWindow->GetExtantDoc();
+ if (!topLevelDocument) {
+ return false;
+ }
+
+ return NS_SUCCEEDED(
+ nsContentUtils::CheckSameOrigin(topLevelDocument, aDocument));
+}
+
+/* static */
+bool FeaturePolicyUtils::IsFeatureUnsafeAllowedAll(
+ Document* aDocument, const nsAString& aFeatureName) {
+ MOZ_ASSERT(aDocument);
+
+ if (!aDocument->IsHTMLDocument()) {
+ return false;
+ }
+
+ FeaturePolicy* policy = aDocument->FeaturePolicy();
+ MOZ_ASSERT(policy);
+
+ return policy->HasFeatureUnsafeAllowsAll(aFeatureName) &&
+ !policy->IsSameOriginAsSrc(aDocument->NodePrincipal()) &&
+ !policy->AllowsFeatureExplicitlyInAncestorChain(
+ aFeatureName, policy->DefaultOrigin()) &&
+ !IsSameOriginAsTop(aDocument);
+}
+
+/* static */
+bool FeaturePolicyUtils::IsFeatureAllowed(Document* aDocument,
+ const nsAString& aFeatureName) {
+ MOZ_ASSERT(aDocument);
+
+ // Skip apply features in experimental phase
+ if (!StaticPrefs::dom_security_featurePolicy_experimental_enabled() &&
+ IsExperimentalFeature(aFeatureName)) {
+ return true;
+ }
+
+ FeaturePolicy* policy = aDocument->FeaturePolicy();
+ MOZ_ASSERT(policy);
+
+ if (policy->AllowsFeatureInternal(aFeatureName, policy->DefaultOrigin())) {
+ return true;
+ }
+
+ ReportViolation(aDocument, aFeatureName);
+ return false;
+}
+
+/* static */
+void FeaturePolicyUtils::ReportViolation(Document* aDocument,
+ const nsAString& aFeatureName) {
+ MOZ_ASSERT(aDocument);
+
+ nsCOMPtr<nsIURI> uri = aDocument->GetDocumentURI();
+ if (NS_WARN_IF(!uri)) {
+ return;
+ }
+
+ // Strip the URL of any possible username/password and make it ready to be
+ // presented in the UI.
+ nsCOMPtr<nsIURI> exposableURI = net::nsIOService::CreateExposableURI(uri);
+ nsAutoCString spec;
+ nsresult rv = exposableURI->GetSpec(spec);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ JSContext* cx = nsContentUtils::GetCurrentJSContext();
+ if (NS_WARN_IF(!cx)) {
+ return;
+ }
+
+ nsAutoString fileName;
+ Nullable<int32_t> lineNumber;
+ Nullable<int32_t> columnNumber;
+ uint32_t line = 0;
+ uint32_t column = 0;
+ if (nsJSUtils::GetCallingLocation(cx, fileName, &line, &column)) {
+ lineNumber.SetValue(static_cast<int32_t>(line));
+ columnNumber.SetValue(static_cast<int32_t>(column));
+ }
+
+ nsPIDOMWindowInner* window = aDocument->GetInnerWindow();
+ if (NS_WARN_IF(!window)) {
+ return;
+ }
+
+ RefPtr<FeaturePolicyViolationReportBody> body =
+ new FeaturePolicyViolationReportBody(window->AsGlobal(), aFeatureName,
+ fileName, lineNumber, columnNumber,
+ u"enforce"_ns);
+
+ ReportingUtils::Report(window->AsGlobal(), nsGkAtoms::featurePolicyViolation,
+ u"default"_ns, NS_ConvertUTF8toUTF16(spec), body);
+}
+
+} // namespace dom
+
+namespace ipc {
+void IPDLParamTraits<dom::FeaturePolicy*>::Write(IPC::MessageWriter* aWriter,
+ IProtocol* aActor,
+ dom::FeaturePolicy* aParam) {
+ if (!aParam) {
+ WriteIPDLParam(aWriter, aActor, false);
+ return;
+ }
+
+ WriteIPDLParam(aWriter, aActor, true);
+
+ dom::FeaturePolicyInfo info;
+ info.defaultOrigin() = aParam->DefaultOrigin();
+ info.selfOrigin() = aParam->GetSelfOrigin();
+ info.srcOrigin() = aParam->GetSrcOrigin();
+
+ info.declaredString() = aParam->DeclaredString();
+ info.inheritedDeniedFeatureNames() =
+ aParam->InheritedDeniedFeatureNames().Clone();
+ info.attributeEnabledFeatureNames() =
+ aParam->AttributeEnabledFeatureNames().Clone();
+
+ WriteIPDLParam(aWriter, aActor, info);
+}
+
+bool IPDLParamTraits<dom::FeaturePolicy*>::Read(
+ IPC::MessageReader* aReader, IProtocol* aActor,
+ RefPtr<dom::FeaturePolicy>* aResult) {
+ *aResult = nullptr;
+ bool notnull = false;
+ if (!ReadIPDLParam(aReader, aActor, &notnull)) {
+ return false;
+ }
+
+ if (!notnull) {
+ return true;
+ }
+
+ dom::FeaturePolicyInfo info;
+ if (!ReadIPDLParam(aReader, aActor, &info)) {
+ return false;
+ }
+
+ // Note that we only do IPC for feature policy to inherit policy from parent
+ // to child document. That does not need to bind feature policy with a node.
+ RefPtr<dom::FeaturePolicy> featurePolicy = new dom::FeaturePolicy(nullptr);
+ featurePolicy->SetDefaultOrigin(info.defaultOrigin());
+ featurePolicy->SetInheritedDeniedFeatureNames(
+ info.inheritedDeniedFeatureNames());
+
+ const auto& declaredString = info.declaredString();
+ if (info.selfOrigin() && !declaredString.IsEmpty()) {
+ featurePolicy->SetDeclaredPolicy(nullptr, declaredString, info.selfOrigin(),
+ info.srcOrigin());
+ }
+
+ for (auto& featureName : info.attributeEnabledFeatureNames()) {
+ featurePolicy->MaybeSetAllowedPolicy(featureName);
+ }
+
+ *aResult = std::move(featurePolicy);
+ return true;
+}
+} // namespace ipc
+
+} // namespace mozilla
diff --git a/dom/security/featurepolicy/FeaturePolicyUtils.h b/dom/security/featurepolicy/FeaturePolicyUtils.h
new file mode 100644
index 0000000000..380806433d
--- /dev/null
+++ b/dom/security/featurepolicy/FeaturePolicyUtils.h
@@ -0,0 +1,91 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_FeaturePolicyUtils_h
+#define mozilla_dom_FeaturePolicyUtils_h
+
+#include "nsString.h"
+#include <functional>
+
+#include "mozilla/dom/FeaturePolicy.h"
+
+class PickleIterator;
+
+namespace IPC {
+class Message;
+class MessageReader;
+class MessageWriter;
+} // namespace IPC
+
+namespace mozilla {
+namespace dom {
+
+class Document;
+
+class FeaturePolicyUtils final {
+ public:
+ enum FeaturePolicyValue {
+ // Feature always allowed.
+ eAll,
+
+ // Feature allowed for documents that are same-origin with this one.
+ eSelf,
+
+ // Feature denied.
+ eNone,
+ };
+
+ // This method returns true if aFeatureName is allowed for aDocument.
+ // Use this method everywhere you need to check feature-policy directives.
+ static bool IsFeatureAllowed(Document* aDocument,
+ const nsAString& aFeatureName);
+
+ // Returns true if aFeatureName is a known feature policy name.
+ static bool IsSupportedFeature(const nsAString& aFeatureName);
+
+ // Returns true if aFeatureName is a experimental feature policy name.
+ static bool IsExperimentalFeature(const nsAString& aFeatureName);
+
+ // Runs aCallback for each known feature policy, with the feature name as
+ // argument.
+ static void ForEachFeature(const std::function<void(const char*)>& aCallback);
+
+ // Returns the default policy value for aFeatureName.
+ static FeaturePolicyValue DefaultAllowListFeature(
+ const nsAString& aFeatureName);
+
+ // This method returns true if aFeatureName is in unsafe allowed "*" case.
+ // We are in "unsafe" case when there is 'allow "*"' presents for an origin
+ // that's not presented in the ancestor feature policy chain, via src, via
+ // explicitly listed in allow, and not being the top-level origin.
+ static bool IsFeatureUnsafeAllowedAll(Document* aDocument,
+ const nsAString& aFeatureName);
+
+ private:
+ static void ReportViolation(Document* aDocument,
+ const nsAString& aFeatureName);
+};
+
+} // namespace dom
+
+namespace ipc {
+
+class IProtocol;
+
+template <typename T>
+struct IPDLParamTraits;
+
+template <>
+struct IPDLParamTraits<mozilla::dom::FeaturePolicy*> {
+ static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor,
+ mozilla::dom::FeaturePolicy* aParam);
+ static bool Read(IPC::MessageReader* aReader, IProtocol* aActor,
+ RefPtr<mozilla::dom::FeaturePolicy>* aResult);
+};
+} // namespace ipc
+} // namespace mozilla
+
+#endif // mozilla_dom_FeaturePolicyUtils_h
diff --git a/dom/security/featurepolicy/fuzztest/fp_fuzzer.cpp b/dom/security/featurepolicy/fuzztest/fp_fuzzer.cpp
new file mode 100644
index 0000000000..25f7dc8d41
--- /dev/null
+++ b/dom/security/featurepolicy/fuzztest/fp_fuzzer.cpp
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "FuzzingInterface.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/dom/Feature.h"
+#include "mozilla/dom/FeaturePolicyParser.h"
+#include "nsNetUtil.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+static nsCOMPtr<nsIPrincipal> selfURIPrincipal;
+static nsCOMPtr<nsIURI> selfURI;
+
+static int LVVMFuzzerInitTest(int* argc, char*** argv) {
+ nsresult ret;
+ ret = NS_NewURI(getter_AddRefs(selfURI), "http://selfuri.com");
+ if (ret != NS_OK) {
+ MOZ_CRASH("NS_NewURI failed.");
+ }
+
+ mozilla::OriginAttributes attrs;
+ selfURIPrincipal =
+ mozilla::BasePrincipal::CreateContentPrincipal(selfURI, attrs);
+ if (!selfURIPrincipal) {
+ MOZ_CRASH("CreateContentPrincipal failed.");
+ }
+ return 0;
+}
+
+static int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+ if (!size) {
+ return 0;
+ }
+ nsTArray<Feature> parsedFeatures;
+
+ NS_ConvertASCIItoUTF16 policy(reinterpret_cast<const char*>(data), size);
+ if (!policy.get()) return 0;
+
+ FeaturePolicyParser::ParseString(policy, nullptr, selfURIPrincipal,
+ selfURIPrincipal, parsedFeatures);
+
+ for (const Feature& feature : parsedFeatures) {
+ nsTArray<nsCOMPtr<nsIPrincipal>> list;
+ feature.GetAllowList(list);
+
+ for (nsIPrincipal* principal : list) {
+ nsAutoCString originNoSuffix;
+ nsresult rv = principal->GetOriginNoSuffix(originNoSuffix);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return 0;
+ }
+ printf("%s - %s\n", NS_ConvertUTF16toUTF8(feature.Name()).get(),
+ originNoSuffix.get());
+ }
+ }
+ return 0;
+}
+
+MOZ_FUZZING_INTERFACE_RAW(LVVMFuzzerInitTest, LLVMFuzzerTestOneInput,
+ FeaturePolicyParser);
diff --git a/dom/security/featurepolicy/fuzztest/fp_fuzzer.dict b/dom/security/featurepolicy/fuzztest/fp_fuzzer.dict
new file mode 100644
index 0000000000..e95508bf8e
--- /dev/null
+++ b/dom/security/featurepolicy/fuzztest/fp_fuzzer.dict
@@ -0,0 +1,54 @@
+# tokens
+"'"
+";"
+
+### https://www.w3.org/TR/{CSP,CSP2,CSP3}/
+# directive names
+"accelerometer"
+"ambient-light-sensor"
+"autoplay"
+"battery"
+"camera"
+"display-capture"
+"document-domain"
+"encrypted-media"
+"execution-while-not-rendered"
+"execution-while-out-of-viewport"
+"fullscreen
+"geolocation
+"gyroscope"
+"layout-animations"
+"legacy-image-formats"
+"magnetometer"
+"microphone"
+"midi"
+"navigation-override"
+"oversized-images"
+"payment"
+"picture-in-picture"
+"publickey-credentials"
+"sync-xhr"
+"usb"
+"vr"
+"wake-lock"
+"xr-spatial-tracking"
+
+# directive values
+"'self'"
+"'none'"
+"'src''"
+*
+
+
+# URI components
+"https:"
+"ws:"
+"blob:"
+"data:"
+"filesystem:"
+"javascript:"
+"http://"
+"selfuri.com"
+"127.0.0.1"
+"::1"
+https://example.com \ No newline at end of file
diff --git a/dom/security/featurepolicy/fuzztest/moz.build b/dom/security/featurepolicy/fuzztest/moz.build
new file mode 100644
index 0000000000..ea577e8339
--- /dev/null
+++ b/dom/security/featurepolicy/fuzztest/moz.build
@@ -0,0 +1,18 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Library("FuzzingFeaturePolicy")
+
+LOCAL_INCLUDES += [
+ "/dom/security/featurepolicy",
+ "/netwerk/base",
+]
+
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
+
+SOURCES += ["fp_fuzzer.cpp"]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/dom/security/featurepolicy/moz.build b/dom/security/featurepolicy/moz.build
new file mode 100644
index 0000000000..b39cdd9c7f
--- /dev/null
+++ b/dom/security/featurepolicy/moz.build
@@ -0,0 +1,36 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: Security")
+
+TEST_DIRS += ["test/gtest"]
+MOCHITEST_MANIFESTS += ["test/mochitest/mochitest.toml"]
+
+EXPORTS.mozilla.dom += [
+ "Feature.h",
+ "FeaturePolicy.h",
+ "FeaturePolicyParser.h",
+ "FeaturePolicyUtils.h",
+]
+
+UNIFIED_SOURCES += [
+ "Feature.cpp",
+ "FeaturePolicy.cpp",
+ "FeaturePolicyParser.cpp",
+ "FeaturePolicyUtils.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/netwerk/base",
+]
+include("/ipc/chromium/chromium-config.mozbuild")
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+if CONFIG["FUZZING_INTERFACES"]:
+ TEST_DIRS += ["fuzztest"]
diff --git a/dom/security/featurepolicy/test/gtest/TestFeaturePolicyParser.cpp b/dom/security/featurepolicy/test/gtest/TestFeaturePolicyParser.cpp
new file mode 100644
index 0000000000..3e58971c9b
--- /dev/null
+++ b/dom/security/featurepolicy/test/gtest/TestFeaturePolicyParser.cpp
@@ -0,0 +1,162 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/dom/Feature.h"
+#include "mozilla/dom/FeaturePolicyParser.h"
+#include "nsNetUtil.h"
+#include "nsTArray.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+#define URL_SELF "https://example.com"_ns
+#define URL_EXAMPLE_COM "http://example.com"_ns
+#define URL_EXAMPLE_NET "http://example.net"_ns
+
+void CheckParser(const nsAString& aInput, bool aExpectedResults,
+ uint32_t aExpectedFeatures,
+ nsTArray<Feature>& aParsedFeatures) {
+ nsCOMPtr<nsIPrincipal> principal =
+ mozilla::BasePrincipal::CreateContentPrincipal(URL_SELF);
+ nsTArray<Feature> parsedFeatures;
+ ASSERT_TRUE(FeaturePolicyParser::ParseString(aInput, nullptr, principal,
+ principal, parsedFeatures) ==
+ aExpectedResults);
+ ASSERT_TRUE(parsedFeatures.Length() == aExpectedFeatures);
+
+ aParsedFeatures = std::move(parsedFeatures);
+}
+
+TEST(FeaturePolicyParser, Basic)
+{
+ nsCOMPtr<nsIPrincipal> selfPrincipal =
+ mozilla::BasePrincipal::CreateContentPrincipal(URL_SELF);
+ nsCOMPtr<nsIPrincipal> exampleComPrincipal =
+ mozilla::BasePrincipal::CreateContentPrincipal(URL_EXAMPLE_COM);
+ nsCOMPtr<nsIPrincipal> exampleNetPrincipal =
+ mozilla::BasePrincipal::CreateContentPrincipal(URL_EXAMPLE_NET);
+
+ nsTArray<Feature> parsedFeatures;
+
+ // Empty string is a valid policy.
+ CheckParser(u""_ns, true, 0, parsedFeatures);
+
+ // Empty string with spaces is still valid.
+ CheckParser(u" "_ns, true, 0, parsedFeatures);
+
+ // Non-Existing features with no allowed values
+ CheckParser(u"non-existing-feature"_ns, true, 0, parsedFeatures);
+ CheckParser(u"non-existing-feature;another-feature"_ns, true, 0,
+ parsedFeatures);
+
+ // Existing feature with no allowed values
+ CheckParser(u"camera"_ns, true, 1, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].HasAllowList());
+
+ // Some spaces.
+ CheckParser(u" camera "_ns, true, 1, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].HasAllowList());
+
+ // A random ;
+ CheckParser(u"camera;"_ns, true, 1, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].HasAllowList());
+
+ // Another random ;
+ CheckParser(u";camera;"_ns, true, 1, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].HasAllowList());
+
+ // 2 features
+ CheckParser(u"camera;microphone"_ns, true, 2, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].HasAllowList());
+ ASSERT_TRUE(parsedFeatures[1].Name().Equals(u"microphone"_ns));
+ ASSERT_TRUE(parsedFeatures[1].HasAllowList());
+
+ // 2 features with spaces
+ CheckParser(u" camera ; microphone "_ns, true, 2, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].HasAllowList());
+ ASSERT_TRUE(parsedFeatures[1].Name().Equals(u"microphone"_ns));
+ ASSERT_TRUE(parsedFeatures[1].HasAllowList());
+
+ // 3 features, but only 2 exist.
+ CheckParser(u"camera;microphone;foobar"_ns, true, 2, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].HasAllowList());
+ ASSERT_TRUE(parsedFeatures[1].Name().Equals(u"microphone"_ns));
+ ASSERT_TRUE(parsedFeatures[1].HasAllowList());
+
+ // Multiple spaces around the value
+ CheckParser(u"camera 'self'"_ns, true, 1, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].AllowListContains(selfPrincipal));
+
+ // Multiple spaces around the value
+ CheckParser(u"camera 'self' "_ns, true, 1, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].AllowListContains(selfPrincipal));
+
+ // No final '
+ CheckParser(u"camera 'self"_ns, true, 1, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].HasAllowList());
+ ASSERT_TRUE(!parsedFeatures[0].AllowListContains(selfPrincipal));
+
+ // Lowercase/Uppercase
+ CheckParser(u"camera 'selF'"_ns, true, 1, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].AllowListContains(selfPrincipal));
+
+ // Lowercase/Uppercase
+ CheckParser(u"camera * 'self' none' a.com 123"_ns, true, 1, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].AllowsAll());
+
+ // After a 'none' we don't continue the parsing.
+ CheckParser(u"camera 'none' a.com b.org c.net d.co.uk"_ns, true, 1,
+ parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].AllowsNone());
+
+ // After a * we don't continue the parsing.
+ CheckParser(u"camera * a.com b.org c.net d.co.uk"_ns, true, 1,
+ parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].AllowsAll());
+
+ // 'self'
+ CheckParser(u"camera 'self'"_ns, true, 1, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].AllowListContains(selfPrincipal));
+
+ // A couple of URLs
+ CheckParser(u"camera http://example.com http://example.net"_ns, true, 1,
+ parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(!parsedFeatures[0].AllowListContains(selfPrincipal));
+ ASSERT_TRUE(parsedFeatures[0].AllowListContains(exampleComPrincipal));
+ ASSERT_TRUE(parsedFeatures[0].AllowListContains(exampleNetPrincipal));
+
+ // A couple of URLs + self
+ CheckParser(u"camera http://example.com 'self' http://example.net"_ns, true,
+ 1, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].AllowListContains(selfPrincipal));
+ ASSERT_TRUE(parsedFeatures[0].AllowListContains(exampleComPrincipal));
+ ASSERT_TRUE(parsedFeatures[0].AllowListContains(exampleNetPrincipal));
+
+ // A couple of URLs but then *
+ CheckParser(u"camera http://example.com 'self' http://example.net *"_ns, true,
+ 1, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].AllowsAll());
+}
diff --git a/dom/security/featurepolicy/test/gtest/moz.build b/dom/security/featurepolicy/test/gtest/moz.build
new file mode 100644
index 0000000000..e307810ff2
--- /dev/null
+++ b/dom/security/featurepolicy/test/gtest/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, you can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES = [
+ "TestFeaturePolicyParser.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/dom/security/featurepolicy/test/mochitest/empty.html b/dom/security/featurepolicy/test/mochitest/empty.html
new file mode 100644
index 0000000000..64355e7d19
--- /dev/null
+++ b/dom/security/featurepolicy/test/mochitest/empty.html
@@ -0,0 +1 @@
+Nothing here
diff --git a/dom/security/featurepolicy/test/mochitest/mochitest.toml b/dom/security/featurepolicy/test/mochitest/mochitest.toml
new file mode 100644
index 0000000000..c621331596
--- /dev/null
+++ b/dom/security/featurepolicy/test/mochitest/mochitest.toml
@@ -0,0 +1,14 @@
+[DEFAULT]
+prefs = [
+ "dom.security.featurePolicy.header.enabled=true",
+ "dom.security.featurePolicy.webidl.enabled=true",
+]
+support-files = [
+ "empty.html",
+ "test_parser.html^headers^",
+]
+
+["test_featureList.html"]
+
+["test_parser.html"]
+fail-if = ["xorigin"]
diff --git a/dom/security/featurepolicy/test/mochitest/test_featureList.html b/dom/security/featurepolicy/test/mochitest/test_featureList.html
new file mode 100644
index 0000000000..8a518da653
--- /dev/null
+++ b/dom/security/featurepolicy/test/mochitest/test_featureList.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test feature policy - list</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe src="empty.html" id="ifr"></iframe>
+<script type="text/javascript">
+
+let supportedFeatures = [
+ "autoplay",
+ "camera",
+ "encrypted-media",
+ "fullscreen",
+ "gamepad",
+ "geolocation",
+ "microphone",
+ "midi",
+ "payment",
+ "publickey-credentials-create",
+ "publickey-credentials-get",
+ "storage-access",
+ "display-capture",
+ "document-domain",
+ "speaker-selection",
+ "vr",
+ "web-share",
+ "screen-wake-lock",
+];
+
+function checkFeatures(features) {
+ features.forEach(feature => {
+ ok(supportedFeatures.includes(feature), "Feature: " + feature);
+ });
+}
+
+ok("featurePolicy" in document, "We have document.featurePolicy");
+checkFeatures(document.featurePolicy.features());
+
+let ifr = document.getElementById("ifr");
+ok("featurePolicy" in ifr, "We have HTMLIFrameElement.featurePolicy");
+checkFeatures(ifr.featurePolicy.features());
+
+</script>
+</body>
+</html>
diff --git a/dom/security/featurepolicy/test/mochitest/test_parser.html b/dom/security/featurepolicy/test/mochitest/test_parser.html
new file mode 100644
index 0000000000..a8322f6e7d
--- /dev/null
+++ b/dom/security/featurepolicy/test/mochitest/test_parser.html
@@ -0,0 +1,418 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test feature policy - parsing</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe src="empty.html" id="ifr"></iframe>
+<iframe src="https://example.org/tests/dom/security/featurePolicy/test/mochitest/empty.html" id="cross_ifr"></iframe>
+<script type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+const CROSS_ORIGIN = "https://example.org";
+
+function test_document() {
+ info("Checking document.featurePolicy");
+ ok("featurePolicy" in document, "We have document.featurePolicy");
+
+ ok(!document.featurePolicy.allowsFeature("foobar"), "Random feature");
+ ok(!document.featurePolicy.allowsFeature("foobar", "https://www.something.net"), "Random feature");
+
+ ok(document.featurePolicy.allowsFeature("camera"), "Camera is allowed for self");
+ ok(document.featurePolicy.allowsFeature("camera", "https://foo.bar"), "Camera is always allowed");
+ let allowed = document.featurePolicy.getAllowlistForFeature("camera");
+ is(allowed.length, 1, "Only 1 entry in allowlist for camera");
+ is(allowed[0], "*", "allowlist is *");
+
+ ok(document.featurePolicy.allowsFeature("geolocation"), "Geolocation is allowed for self");
+ ok(document.featurePolicy.allowsFeature("geolocation", location.origin), "Geolocation is allowed for self");
+ ok(!document.featurePolicy.allowsFeature("geolocation", "https://foo.bar"), "Geolocation is not allowed for any random URL");
+ allowed = document.featurePolicy.getAllowlistForFeature("geolocation");
+ is(allowed.length, 1, "Only 1 entry in allowlist for geolocation");
+ is(allowed[0], location.origin, "allowlist is self");
+
+ ok(!document.featurePolicy.allowsFeature("microphone"), "Microphone is disabled for self");
+ ok(!document.featurePolicy.allowsFeature("microphone", location.origin), "Microphone is disabled for self");
+ ok(!document.featurePolicy.allowsFeature("microphone", "https://foo.bar"), "Microphone is disabled for foo.bar");
+ ok(document.featurePolicy.allowsFeature("microphone", "https://example.com"), "Microphone is allowed for example.com");
+ ok(document.featurePolicy.allowsFeature("microphone", "https://example.org"), "Microphone is allowed for example.org");
+ allowed = document.featurePolicy.getAllowlistForFeature("microphone");
+ is(allowed.length, 0, "No allowlist for microphone");
+
+ ok(!document.featurePolicy.allowsFeature("vr"), "Vibrate is disabled for self");
+ ok(!document.featurePolicy.allowsFeature("vr", location.origin), "Vibrate is disabled for self");
+ ok(!document.featurePolicy.allowsFeature("vr", "https://foo.bar"), "Vibrate is disabled for foo.bar");
+ allowed = document.featurePolicy.getAllowlistForFeature("vr");
+ is(allowed.length, 0, "No allowlist for vr");
+
+ allowed = document.featurePolicy.allowedFeatures();
+ // microphone is disabled for this origin, vr is disabled everywhere.
+ let camera = false;
+ let geolocation = false;
+ allowed.forEach(a => {
+ if (a == "camera") camera = true;
+ if (a == "geolocation") geolocation = true;
+ });
+
+ ok(camera, "Camera is always allowed");
+ ok(geolocation, "Geolocation is allowed only for self");
+
+ next();
+}
+
+function test_iframe_without_allow() {
+ info("Checking HTMLIFrameElement.featurePolicy");
+ let ifr = document.getElementById("ifr");
+ ok("featurePolicy" in ifr, "HTMLIFrameElement.featurePolicy exists");
+
+ ok(!ifr.featurePolicy.allowsFeature("foobar"), "Random feature");
+ ok(!ifr.featurePolicy.allowsFeature("foobar", "https://www.something.net"), "Random feature");
+
+ ok(ifr.featurePolicy.allowsFeature("camera"), "Camera is allowed for self");
+ ok(ifr.featurePolicy.allowsFeature("camera", location.origin), "Camera is allowed for self");
+ ok(!ifr.featurePolicy.allowsFeature("camera", "https://foo.bar"), "Camera is not allowed for a random URL");
+ let allowed = ifr.featurePolicy.getAllowlistForFeature("camera");
+ is(allowed.length, 1, "Only 1 entry in allowlist for camera");
+ is(allowed[0], location.origin, "allowlist is 'self'");
+
+ ok(ifr.featurePolicy.allowsFeature("geolocation"), "Geolocation is allowed for self");
+ ok(ifr.featurePolicy.allowsFeature("geolocation", location.origin), "Geolocation is allowed for self");
+ ok(!ifr.featurePolicy.allowsFeature("geolocation", "https://foo.bar"), "Geolocation is not allowed for any random URL");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("geolocation");
+ is(allowed.length, 1, "Only 1 entry in allowlist for geolocation");
+ is(allowed[0], location.origin, "allowlist is '*'");
+
+ ok(!ifr.featurePolicy.allowsFeature("microphone"), "Microphone is disabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("microphone", location.origin), "Microphone is disabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("microphone", "https://foo.bar"), "Microphone is disabled for foo.bar");
+ ok(!ifr.featurePolicy.allowsFeature("microphone", "https://example.com"), "Microphone is disabled for example.com");
+ ok(!ifr.featurePolicy.allowsFeature("microphone", "https://example.org"), "Microphone is disabled for example.org");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("microphone");
+ is(allowed.length, 0, "No allowlist for microphone");
+
+ ok(!ifr.featurePolicy.allowsFeature("vr"), "Vibrate is disabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("vr", location.origin), "Vibrate is disabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("vr", "https://foo.bar"), "Vibrate is disabled for foo.bar");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("vr");
+ is(allowed.length, 0, "No allowlist for vr");
+
+ ok(ifr.featurePolicy.allowedFeatures().includes("camera"), "Camera is allowed");
+ ok(ifr.featurePolicy.allowedFeatures().includes("geolocation"), "Geolocation is allowed");
+ // microphone is disabled for this origin
+ ok(!ifr.featurePolicy.allowedFeatures().includes("microphone"), "microphone is not allowed");
+ // vr is disabled everywhere.
+ ok(!ifr.featurePolicy.allowedFeatures().includes("vr"), "VR is not allowed");
+
+ next();
+}
+
+function test_iframe_with_allow() {
+ info("Checking HTMLIFrameElement.featurePolicy");
+ let ifr = document.getElementById("ifr");
+ ok("featurePolicy" in ifr, "HTMLIFrameElement.featurePolicy exists");
+
+ ifr.setAttribute("allow", "camera 'none'");
+
+ ok(!ifr.featurePolicy.allowsFeature("foobar"), "Random feature");
+ ok(!ifr.featurePolicy.allowsFeature("foobar", "https://www.something.net"), "Random feature");
+
+ ok(!ifr.featurePolicy.allowsFeature("camera"), "Camera is not allowed");
+ let allowed = ifr.featurePolicy.getAllowlistForFeature("camera");
+ is(allowed.length, 0, "Camera has an empty allowlist");
+
+ ok(ifr.featurePolicy.allowsFeature("geolocation"), "Geolocation is allowed for self");
+ ok(ifr.featurePolicy.allowsFeature("geolocation", location.origin), "Geolocation is allowed for self");
+ ok(!ifr.featurePolicy.allowsFeature("geolocation", "https://foo.bar"), "Geolocation is not allowed for any random URL");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("geolocation");
+ is(allowed.length, 1, "Only 1 entry in allowlist for geolocation");
+ is(allowed[0], location.origin, "allowlist is '*'");
+
+ ok(!ifr.featurePolicy.allowsFeature("microphone"), "Microphone is disabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("microphone", location.origin), "Microphone is disabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("microphone", "https://foo.bar"), "Microphone is disabled for foo.bar");
+ ok(!ifr.featurePolicy.allowsFeature("microphone", "https://example.com"), "Microphone is disabled for example.com");
+ ok(!ifr.featurePolicy.allowsFeature("microphone", "https://example.org"), "Microphone is disabled for example.org");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("microphone");
+ is(allowed.length, 0, "No allowlist for microphone");
+
+ ok(!ifr.featurePolicy.allowsFeature("vr"), "Vibrate is disabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("vr", location.origin), "Vibrate is disabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("vr", "https://foo.bar"), "Vibrate is disabled for foo.bar");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("vr");
+ is(allowed.length, 0, "No allowlist for vr");
+
+ ok(ifr.featurePolicy.allowedFeatures().includes("geolocation"), "Geolocation is allowed only for self");
+
+ next();
+}
+
+function test_iframe_contentDocument() {
+ info("Checking iframe document.featurePolicy");
+
+ let ifr = document.createElement("iframe");
+ ifr.setAttribute("src", "empty.html");
+ ifr.onload = function() {
+ ok("featurePolicy" in ifr.contentDocument, "We have ifr.contentDocument.featurePolicy");
+
+ ok(!ifr.contentDocument.featurePolicy.allowsFeature("foobar"), "Random feature");
+ ok(!ifr.contentDocument.featurePolicy.allowsFeature("foobar", "https://www.something.net"), "Random feature");
+
+ ok(ifr.contentDocument.featurePolicy.allowsFeature("camera"), "Camera is allowed for self");
+ ok(ifr.contentDocument.featurePolicy.allowsFeature("camera", location.origin), "Camera is allowed for self");
+ ok(!ifr.contentDocument.featurePolicy.allowsFeature("camera", "https://foo.bar"), "Camera is allowed for self");
+ let allowed = ifr.contentDocument.featurePolicy.getAllowlistForFeature("camera");
+ is(allowed.length, 1, "Only 1 entry in allowlist for camera");
+ is(allowed[0], location.origin, "allowlist is 'self'");
+
+ ok(ifr.featurePolicy.allowsFeature("geolocation"), "Geolocation is allowed for self");
+ ok(ifr.featurePolicy.allowsFeature("geolocation", location.origin), "Geolocation is allowed for self");
+ ok(!ifr.featurePolicy.allowsFeature("geolocation", "https://foo.bar"), "Geolocation is not allowed for any random URL");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("geolocation");
+ is(allowed.length, 1, "Only 1 entry in allowlist for geolocation");
+ is(allowed[0], location.origin, "allowlist is '*'");
+
+ ok(!ifr.contentDocument.featurePolicy.allowsFeature("microphone"), "Microphone is disabled for self");
+ ok(!ifr.contentDocument.featurePolicy.allowsFeature("microphone", location.origin), "Microphone is disabled for self");
+ ok(!ifr.contentDocument.featurePolicy.allowsFeature("microphone", "https://foo.bar"), "Microphone is disabled for foo.bar");
+ ok(!ifr.contentDocument.featurePolicy.allowsFeature("microphone", "https://example.com"), "Microphone is allowed for example.com");
+ ok(!ifr.contentDocument.featurePolicy.allowsFeature("microphone", "https://example.org"), "Microphone is allowed for example.org");
+ allowed = ifr.contentDocument.featurePolicy.getAllowlistForFeature("microphone");
+ is(allowed.length, 0, "No allowlist for microphone");
+
+ ok(!ifr.contentDocument.featurePolicy.allowsFeature("vr"), "Vibrate is disabled for self");
+ ok(!ifr.contentDocument.featurePolicy.allowsFeature("vr", location.origin), "Vibrate is disabled for self");
+ ok(!ifr.contentDocument.featurePolicy.allowsFeature("vr", "https://foo.bar"), "Vibrate is disabled for foo.bar");
+ allowed = ifr.contentDocument.featurePolicy.getAllowlistForFeature("vr");
+ is(allowed.length, 0, "No allowlist for vr");
+
+ ok(ifr.contentDocument.featurePolicy.allowedFeatures().includes("camera"), "Camera is allowed");
+ ok(ifr.contentDocument.featurePolicy.allowedFeatures().includes("geolocation"), "Geolocation is allowed");
+ // microphone is disabled for this origin
+ ok(!ifr.contentDocument.featurePolicy.allowedFeatures().includes("microphone"), "Microphone is not allowed");
+ // vr is disabled everywhere.
+ ok(!ifr.contentDocument.featurePolicy.allowedFeatures().includes("vr"), "VR is not allowed");
+
+ next();
+ };
+ document.body.appendChild(ifr);
+}
+
+function test_cross_iframe_without_allow() {
+ info("Checking cross HTMLIFrameElement.featurePolicy no allow");
+ let ifr = document.getElementById("cross_ifr");
+ ok("featurePolicy" in ifr, "HTMLIFrameElement.featurePolicy exists");
+
+ ok(!ifr.featurePolicy.allowsFeature("foobar"), "Random feature");
+ ok(!ifr.featurePolicy.allowsFeature("foobar", "https://www.something.net"), "Random feature");
+
+ ok(ifr.featurePolicy.allowsFeature("camera"), "Camera is allowed for self");
+ ok(ifr.featurePolicy.allowsFeature("camera", CROSS_ORIGIN), "Camera is allowed for self");
+ ok(!ifr.featurePolicy.allowsFeature("camera", "https://foo.bar"), "Camera is not allowed for a random URL");
+ let allowed = ifr.featurePolicy.getAllowlistForFeature("camera");
+ is(allowed.length, 1, "Only 1 entry in allowlist for camera");
+ is(allowed[0], CROSS_ORIGIN, "allowlist is 'self'");
+
+ ok(!ifr.featurePolicy.allowsFeature("geolocation"), "Geolocation is not allowed for self");
+ ok(!ifr.featurePolicy.allowsFeature("geolocation", CROSS_ORIGIN),
+ "Geolocation is not allowed for self");
+ ok(!ifr.featurePolicy.allowsFeature("geolocation", "https://foo.bar"), "Geolocation is not allowed for any random URL");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("geolocation");
+ is(allowed.length, 0, "No allowlist for geolocation");
+
+ ok(ifr.featurePolicy.allowsFeature("microphone"), "Microphone is enabled for self");
+ ok(ifr.featurePolicy.allowsFeature("microphone", CROSS_ORIGIN), "Microphone is enabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("microphone", "https://foo.bar"), "Microphone is disabled for foo.bar");
+ ok(!ifr.featurePolicy.allowsFeature("microphone", "https://example.com"), "Microphone is disabled for example.com");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("microphone");
+ is(allowed.length, 1, "Only 1 entry in allowlist for microphone");
+ is(allowed[0], CROSS_ORIGIN, "allowlist is self");
+
+ ok(!ifr.featurePolicy.allowsFeature("vr"), "Vibrate is disabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("vr", CROSS_ORIGIN), "Vibrate is disabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("vr", "https://foo.bar"), "Vibrate is disabled for foo.bar");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("vr");
+ is(allowed.length, 0, "No allowlist for vr");
+
+ ok(ifr.featurePolicy.allowedFeatures().includes("camera"), "Camera is allowed");
+ ok(!ifr.featurePolicy.allowedFeatures().includes("geolocation"), "Geolocation is not allowed");
+ // microphone is enabled for this origin
+ ok(ifr.featurePolicy.allowedFeatures().includes("microphone"), "microphone is allowed");
+ // vr is disabled everywhere.
+ ok(!ifr.featurePolicy.allowedFeatures().includes("vr"), "VR is not allowed");
+
+ next();
+}
+
+function test_cross_iframe_with_allow() {
+ info("Checking cross HTMLIFrameElement.featurePolicy with allow");
+ let ifr = document.getElementById("cross_ifr");
+ ok("featurePolicy" in ifr, "HTMLIFrameElement.featurePolicy exists");
+
+ ifr.setAttribute("allow", "geolocation; camera 'none'");
+
+ ok(!ifr.featurePolicy.allowsFeature("foobar"), "Random feature");
+ ok(!ifr.featurePolicy.allowsFeature("foobar", "https://www.something.net"), "Random feature");
+
+ ok(!ifr.featurePolicy.allowsFeature("camera"), "Camera is not allowed");
+ let allowed = ifr.featurePolicy.getAllowlistForFeature("camera");
+ is(allowed.length, 0, "Camera has an empty allowlist");
+
+ ok(ifr.featurePolicy.allowsFeature("geolocation"), "Geolocation is allowed for self");
+ ok(ifr.featurePolicy.allowsFeature("geolocation", CROSS_ORIGIN), "Geolocation is allowed for self");
+ ok(!ifr.featurePolicy.allowsFeature("geolocation", "https://foo.bar"), "Geolocation is not allowed for any random URL");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("geolocation");
+ is(allowed.length, 1, "Only 1 entry in allowlist for geolocation");
+ is(allowed[0], CROSS_ORIGIN, "allowlist is '*'");
+
+ ok(ifr.featurePolicy.allowsFeature("microphone"), "Microphone is enabled for self");
+ ok(ifr.featurePolicy.allowsFeature("microphone", CROSS_ORIGIN), "Microphone is enabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("microphone", "https://foo.bar"), "Microphone is disabled for foo.bar");
+ ok(!ifr.featurePolicy.allowsFeature("microphone", "https://example.com"), "Microphone is disabled for example.com");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("microphone");
+ is(allowed.length, 1, "Only 1 entry in allowlist for microphone");
+ is(allowed[0], CROSS_ORIGIN, "allowlist is self");
+
+ ok(!ifr.featurePolicy.allowsFeature("vr"), "Vibrate is disabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("vr", CROSS_ORIGIN), "Vibrate is disabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("vr", "https://foo.bar"), "Vibrate is disabled for foo.bar");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("vr");
+ is(allowed.length, 0, "No allowlist for vr");
+
+ ok(ifr.featurePolicy.allowedFeatures().includes("geolocation"), "Geolocation is allowed only for self");
+ // microphone is enabled for this origin
+ ok(ifr.featurePolicy.allowedFeatures().includes("microphone"), "microphone is allowed");
+
+ next();
+}
+
+function test_cross_iframe_contentDocument_no_allow() {
+ info("Checking cross iframe document.featurePolicy no allow");
+
+ let ifr = document.createElement("iframe");
+ ifr.setAttribute("src", "https://example.org/tests/dom/security/featurePolicy/test/mochitest/empty.html");
+ ifr.onload = async function() {
+ await SpecialPowers.spawn(ifr, [], () => {
+ Assert.ok("featurePolicy" in this.content.document, "We have this.content.document.featurePolicy");
+
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("foobar"), "Random feature");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("foobar", "https://www.something.net"), "Random feature");
+
+ Assert.ok(this.content.document.featurePolicy.allowsFeature("camera"), "Camera is allowed for self");
+ Assert.ok(this.content.document.featurePolicy.allowsFeature("camera", "https://example.org"), "Camera is allowed for self");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("camera", "https://foo.bar"), "Camera is not allowed for a random URL");
+ let allowed = this.content.document.featurePolicy.getAllowlistForFeature("camera");
+ Assert.equal(allowed.length, 1, "Only 1 entry in allowlist for camera");
+ Assert.equal(allowed[0], "https://example.org", "allowlist is 'self'");
+
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("geolocation"), "Geolocation is not allowed for self");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("geolocation", "https://example.org"),
+ "Geolocation is not allowed for self");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("geolocation", "https://foo.bar"), "Geolocation is not allowed for any random URL");
+ allowed = this.content.document.featurePolicy.getAllowlistForFeature("geolocation");
+ Assert.equal(allowed.length, 0, "No allowlist for geolocation");
+
+ Assert.ok(this.content.document.featurePolicy.allowsFeature("microphone"), "Microphone is enabled for self");
+ Assert.ok(this.content.document.featurePolicy.allowsFeature("microphone", "https://example.org"), "Microphone is enabled for self");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("microphone", "https://foo.bar"), "Microphone is disabled for foo.bar");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("microphone", "https://example.com"), "Microphone is disabled for example.com");
+ allowed = this.content.document.featurePolicy.getAllowlistForFeature("microphone");
+ Assert.equal(allowed.length, 1, "Only 1 entry in allowlist for microphone");
+ Assert.equal(allowed[0], "https://example.org", "allowlist is self");
+
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("vr"), "Vibrate is disabled for self");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("vr", "https://example.org"), "Vibrate is disabled for self");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("vr", "https://foo.bar"), "Vibrate is disabled for foo.bar");
+ allowed = this.content.document.featurePolicy.getAllowlistForFeature("vr");
+ Assert.equal(allowed.length, 0, "No allowlist for vr");
+
+ Assert.ok(this.content.document.featurePolicy.allowedFeatures().includes("camera"), "Camera is allowed");
+ Assert.ok(!this.content.document.featurePolicy.allowedFeatures().includes("geolocation"), "Geolocation is not allowed");
+ // microphone is enabled for this origin
+ Assert.ok(this.content.document.featurePolicy.allowedFeatures().includes("microphone"), "microphone is allowed");
+ // vr is disabled everywhere.
+ Assert.ok(!this.content.document.featurePolicy.allowedFeatures().includes("vr"), "VR is not allowed");
+ });
+
+ next();
+ };
+ document.body.appendChild(ifr);
+}
+
+function test_cross_iframe_contentDocument_allow() {
+ info("Checking cross iframe document.featurePolicy with allow");
+
+ let ifr = document.createElement("iframe");
+ ifr.setAttribute("src", "https://example.org/tests/dom/security/featurePolicy/test/mochitest/empty.html");
+ ifr.setAttribute("allow", "geolocation; camera 'none'");
+ ifr.onload = async function() {
+ await SpecialPowers.spawn(ifr, [], () => {
+ Assert.ok("featurePolicy" in this.content.document, "We have this.content.document.featurePolicy");
+
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("foobar"), "Random feature");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("foobar", "https://www.something.net"), "Random feature");
+
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("camera"), "Camera is not allowed");
+ let allowed = this.content.document.featurePolicy.getAllowlistForFeature("camera");
+ Assert.equal(allowed.length, 0, "Camera has an empty allowlist");
+
+ Assert.ok(this.content.document.featurePolicy.allowsFeature("geolocation"), "Geolocation is allowed for self");
+ Assert.ok(this.content.document.featurePolicy.allowsFeature("geolocation", "https://example.org"), "Geolocation is allowed for self");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("geolocation", "https://foo.bar"), "Geolocation is not allowed for any random URL");
+ allowed = this.content.document.featurePolicy.getAllowlistForFeature("geolocation");
+ Assert.equal(allowed.length, 1, "Only 1 entry in allowlist for geolocation");
+ Assert.equal(allowed[0], "https://example.org", "allowlist is '*'");
+
+ Assert.ok(this.content.document.featurePolicy.allowsFeature("microphone"), "Microphone is enabled for self");
+ Assert.ok(this.content.document.featurePolicy.allowsFeature("microphone", "https://example.org"), "Microphone is enabled for self");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("microphone", "https://foo.bar"), "Microphone is disabled for foo.bar");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("microphone", "https://example.com"), "Microphone is disabled for example.com");
+ allowed = this.content.document.featurePolicy.getAllowlistForFeature("microphone");
+ Assert.equal(allowed.length, 1, "Only 1 entry in allowlist for microphone");
+ Assert.equal(allowed[0], "https://example.org", "allowlist is self");
+
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("vr"), "Vibrate is disabled for self");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("vr", "https://example.org"), "Vibrate is disabled for self");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("vr", "https://foo.bar"), "Vibrate is disabled for foo.bar");
+ allowed = this.content.document.featurePolicy.getAllowlistForFeature("vr");
+ Assert.equal(allowed.length, 0, "No allowlist for vr");
+
+ Assert.ok(this.content.document.featurePolicy.allowedFeatures().includes("geolocation"), "Geolocation is allowed only for self");
+ // microphone is enabled for this origin
+ Assert.ok(this.content.document.featurePolicy.allowedFeatures().includes("microphone"), "microphone is allowed");
+ });
+
+ next();
+ };
+ document.body.appendChild(ifr);
+}
+
+
+var tests = [
+ test_document,
+ test_iframe_without_allow,
+ test_iframe_with_allow,
+ test_iframe_contentDocument,
+ test_cross_iframe_without_allow,
+ test_cross_iframe_with_allow,
+ test_cross_iframe_contentDocument_no_allow,
+ test_cross_iframe_contentDocument_allow
+];
+
+function next() {
+ if (!tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ var test = tests.shift();
+ test();
+}
+
+next();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/featurepolicy/test/mochitest/test_parser.html^headers^ b/dom/security/featurepolicy/test/mochitest/test_parser.html^headers^
new file mode 100644
index 0000000000..949de013d3
--- /dev/null
+++ b/dom/security/featurepolicy/test/mochitest/test_parser.html^headers^
@@ -0,0 +1 @@
+Feature-Policy: camera *; geolocation 'self'; microphone https://example.com https://example.org; vr 'none'
diff --git a/dom/security/fuzztest/csp_fuzzer.cpp b/dom/security/fuzztest/csp_fuzzer.cpp
new file mode 100644
index 0000000000..24f938cb1f
--- /dev/null
+++ b/dom/security/fuzztest/csp_fuzzer.cpp
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "FuzzingInterface.h"
+#include "mozilla/BasePrincipal.h"
+#include "nsComponentManagerUtils.h"
+#include "nsCSPContext.h"
+#include "nsNetUtil.h"
+#include "nsStringFwd.h"
+
+static int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+ nsresult ret;
+ nsCOMPtr<nsIURI> selfURI;
+ ret = NS_NewURI(getter_AddRefs(selfURI), "http://selfuri.com");
+ if (ret != NS_OK) return 0;
+
+ mozilla::OriginAttributes attrs;
+ nsCOMPtr<nsIPrincipal> selfURIPrincipal =
+ mozilla::BasePrincipal::CreateContentPrincipal(selfURI, attrs);
+ if (!selfURIPrincipal) return 0;
+
+ nsCOMPtr<nsIContentSecurityPolicy> csp =
+ do_CreateInstance(NS_CSPCONTEXT_CONTRACTID, &ret);
+ if (ret != NS_OK) return 0;
+
+ ret =
+ csp->SetRequestContextWithPrincipal(selfURIPrincipal, selfURI, u""_ns, 0);
+ if (ret != NS_OK) return 0;
+
+ NS_ConvertASCIItoUTF16 policy(reinterpret_cast<const char*>(data), size);
+ if (!policy.get()) return 0;
+ csp->AppendPolicy(policy, false, false);
+
+ return 0;
+}
+
+MOZ_FUZZING_INTERFACE_RAW(nullptr, LLVMFuzzerTestOneInput,
+ ContentSecurityPolicyParser);
diff --git a/dom/security/fuzztest/csp_fuzzer.dict b/dom/security/fuzztest/csp_fuzzer.dict
new file mode 100644
index 0000000000..480165d929
--- /dev/null
+++ b/dom/security/fuzztest/csp_fuzzer.dict
@@ -0,0 +1,95 @@
+### dom/security/nsCSPParser.cpp
+# tokens
+":"
+";"
+"/"
+"+"
+"-"
+"."
+"_"
+"~"
+"*"
+"'"
+"#"
+"?"
+"%"
+"!"
+"$"
+"&"
+"("
+")"
+"="
+"@"
+
+### https://www.w3.org/TR/{CSP,CSP2,CSP3}/
+# directive names
+"default-src"
+"script-src"
+"object-src"
+"style-src"
+"img-src"
+"media-src"
+"frame-src"
+"font-src"
+"connect-src"
+"report-uri"
+"frame-ancestors"
+"reflected-xss"
+"base-uri"
+"form-action"
+"manifest-src"
+"upgrade-insecure-requests"
+"child-src"
+"block-all-mixed-content"
+"sandbox"
+"worker-src"
+"plugin-types"
+"disown-opener"
+"report-to"
+
+# directive values
+"'self'"
+"'unsafe-inline'"
+"'unsafe-eval'"
+"'none'"
+"'strict-dynamic'"
+"'unsafe-hashed-attributes'"
+"'nonce-AA=='"
+"'sha256-fw=='"
+"'sha384-/w=='"
+"'sha512-//8='"
+
+# subresources
+"a"
+"audio"
+"embed"
+"iframe"
+"img"
+"link"
+"object"
+"script"
+"source"
+"style"
+"track"
+"video"
+
+# sandboxing flags
+"allow-forms"
+"allow-pointer-lock"
+"allow-popups"
+"allow-same-origin"
+"allow-scripts"
+"allow-top-navigation"
+"allow-top-navigation-by-user-activation"
+
+# URI components
+"https:"
+"ws:"
+"blob:"
+"data:"
+"filesystem:"
+"javascript:"
+"http://"
+"selfuri.com"
+"127.0.0.1"
+"::1"
diff --git a/dom/security/fuzztest/moz.build b/dom/security/fuzztest/moz.build
new file mode 100644
index 0000000000..3a1f3f4396
--- /dev/null
+++ b/dom/security/fuzztest/moz.build
@@ -0,0 +1,18 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Library("FuzzingDOMSecurity")
+
+LOCAL_INCLUDES += [
+ "/dom/security",
+ "/netwerk/base",
+]
+
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
+
+SOURCES += ["csp_fuzzer.cpp"]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/dom/security/moz.build b/dom/security/moz.build
new file mode 100644
index 0000000000..0f2aed6a1f
--- /dev/null
+++ b/dom/security/moz.build
@@ -0,0 +1,82 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("*"):
+ BUG_COMPONENT = ("Core", "DOM: Security")
+
+TEST_DIRS += ["test"]
+
+DIRS += ["featurepolicy", "sanitizer"]
+
+EXPORTS.mozilla.dom += [
+ "CSPEvalChecker.h",
+ "DOMSecurityMonitor.h",
+ "FramingChecker.h",
+ "nsContentSecurityManager.h",
+ "nsContentSecurityUtils.h",
+ "nsCSPContext.h",
+ "nsCSPService.h",
+ "nsCSPUtils.h",
+ "nsHTTPSOnlyStreamListener.h",
+ "nsHTTPSOnlyUtils.h",
+ "nsMixedContentBlocker.h",
+ "PolicyTokenizer.h",
+ "ReferrerInfo.h",
+ "SecFetch.h",
+ "SRICheck.h",
+ "SRILogHelper.h",
+ "SRIMetadata.h",
+]
+
+EXPORTS += [
+ "nsContentSecurityManager.h",
+ "nsContentSecurityUtils.h",
+ "nsMixedContentBlocker.h",
+ "ReferrerInfo.h",
+]
+
+UNIFIED_SOURCES += [
+ "CSPEvalChecker.cpp",
+ "DOMSecurityMonitor.cpp",
+ "FramingChecker.cpp",
+ "nsContentSecurityManager.cpp",
+ "nsContentSecurityUtils.cpp",
+ "nsCSPContext.cpp",
+ "nsCSPParser.cpp",
+ "nsCSPService.cpp",
+ "nsCSPUtils.cpp",
+ "nsHTTPSOnlyStreamListener.cpp",
+ "nsHTTPSOnlyUtils.cpp",
+ "nsMixedContentBlocker.cpp",
+ "PolicyTokenizer.cpp",
+ "ReferrerInfo.cpp",
+ "SecFetch.cpp",
+ "SRICheck.cpp",
+ "SRIMetadata.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+LOCAL_INCLUDES += [
+ "/caps",
+ "/docshell/base", # for nsDocShell.h
+ "/netwerk/base",
+ "/netwerk/protocol/data", # for nsDataHandler.h
+ "/netwerk/protocol/http", # for HttpBaseChannel.h
+]
+
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
+
+if CONFIG["FUZZING_INTERFACES"]:
+ TEST_DIRS += ["fuzztest"]
+
+
+XPIDL_SOURCES += [
+ "nsIHttpsOnlyModePermission.idl",
+]
+
+XPIDL_MODULE = "dom_security"
diff --git a/dom/security/nsCSPContext.cpp b/dom/security/nsCSPContext.cpp
new file mode 100644
index 0000000000..aafd2b64f2
--- /dev/null
+++ b/dom/security/nsCSPContext.cpp
@@ -0,0 +1,1974 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <string>
+#include <unordered_set>
+
+#include "nsCOMPtr.h"
+#include "nsContentPolicyUtils.h"
+#include "nsContentSecurityUtils.h"
+#include "nsContentUtils.h"
+#include "nsCSPContext.h"
+#include "nsCSPParser.h"
+#include "nsCSPService.h"
+#include "nsGlobalWindowOuter.h"
+#include "nsError.h"
+#include "nsIAsyncVerifyRedirectCallback.h"
+#include "nsIClassInfoImpl.h"
+#include "mozilla/dom/Document.h"
+#include "nsIHttpChannel.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIObjectInputStream.h"
+#include "nsIObjectOutputStream.h"
+#include "nsIObserver.h"
+#include "nsIObserverService.h"
+#include "nsIStringStream.h"
+#include "nsISupportsPrimitives.h"
+#include "nsIUploadChannel.h"
+#include "nsIURIMutator.h"
+#include "nsIScriptError.h"
+#include "nsMimeTypes.h"
+#include "nsNetUtil.h"
+#include "nsIContentPolicy.h"
+#include "nsSupportsPrimitives.h"
+#include "nsThreadUtils.h"
+#include "nsString.h"
+#include "nsScriptSecurityManager.h"
+#include "nsStringStream.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_security.h"
+#include "mozilla/dom/CSPReportBinding.h"
+#include "mozilla/dom/CSPDictionariesBinding.h"
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
+#include "mozilla/dom/WindowGlobalParent.h"
+#include "nsINetworkInterceptController.h"
+#include "nsSandboxFlags.h"
+#include "nsIScriptElement.h"
+#include "nsIEventTarget.h"
+#include "mozilla/dom/DocGroup.h"
+#include "mozilla/dom/Element.h"
+#include "nsXULAppAPI.h"
+#include "nsJSUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::ipc;
+
+static LogModule* GetCspContextLog() {
+ static LazyLogModule gCspContextPRLog("CSPContext");
+ return gCspContextPRLog;
+}
+
+#define CSPCONTEXTLOG(args) \
+ MOZ_LOG(GetCspContextLog(), mozilla::LogLevel::Debug, args)
+#define CSPCONTEXTLOGENABLED() \
+ MOZ_LOG_TEST(GetCspContextLog(), mozilla::LogLevel::Debug)
+
+static LogModule* GetCspOriginLogLog() {
+ static LazyLogModule gCspOriginPRLog("CSPOrigin");
+ return gCspOriginPRLog;
+}
+
+#define CSPORIGINLOG(args) \
+ MOZ_LOG(GetCspOriginLogLog(), mozilla::LogLevel::Debug, args)
+#define CSPORIGINLOGENABLED() \
+ MOZ_LOG_TEST(GetCspOriginLogLog(), mozilla::LogLevel::Debug)
+
+#ifdef DEBUG
+/**
+ * This function is only used for verification purposes within
+ * GatherSecurityPolicyViolationEventData.
+ */
+static bool ValidateDirectiveName(const nsAString& aDirective) {
+ static const auto directives = []() {
+ std::unordered_set<std::string> directives;
+ constexpr size_t dirLen =
+ sizeof(CSPStrDirectives) / sizeof(CSPStrDirectives[0]);
+ for (size_t i = 0; i < dirLen; ++i) {
+ directives.insert(CSPStrDirectives[i]);
+ }
+ return directives;
+ }();
+
+ nsAutoString directive(aDirective);
+ auto itr = directives.find(NS_ConvertUTF16toUTF8(directive).get());
+ return itr != directives.end();
+}
+#endif // DEBUG
+
+static void BlockedContentSourceToString(
+ nsCSPContext::BlockedContentSource aSource, nsACString& aString) {
+ switch (aSource) {
+ case nsCSPContext::BlockedContentSource::eUnknown:
+ aString.Truncate();
+ break;
+
+ case nsCSPContext::BlockedContentSource::eInline:
+ aString.AssignLiteral("inline");
+ break;
+
+ case nsCSPContext::BlockedContentSource::eEval:
+ aString.AssignLiteral("eval");
+ break;
+
+ case nsCSPContext::BlockedContentSource::eSelf:
+ aString.AssignLiteral("self");
+ break;
+
+ case nsCSPContext::BlockedContentSource::eWasmEval:
+ aString.AssignLiteral("wasm-eval");
+ break;
+ }
+}
+
+/* ===== nsIContentSecurityPolicy impl ====== */
+
+NS_IMETHODIMP
+nsCSPContext::ShouldLoad(nsContentPolicyType aContentType,
+ nsICSPEventListener* aCSPEventListener,
+ nsILoadInfo* aLoadInfo, nsIURI* aContentLocation,
+ nsIURI* aOriginalURIIfRedirect,
+ bool aSendViolationReports, int16_t* outDecision) {
+ if (CSPCONTEXTLOGENABLED()) {
+ CSPCONTEXTLOG(("nsCSPContext::ShouldLoad, aContentLocation: %s",
+ aContentLocation->GetSpecOrDefault().get()));
+ CSPCONTEXTLOG((">>>> aContentType: %s",
+ NS_CP_ContentTypeName(aContentType)));
+ }
+
+ // This ShouldLoad function is called from nsCSPService::ShouldLoad,
+ // which already checked a number of things, including:
+ // * aContentLocation is not null; we can consume this without further checks
+ // * scheme is not a allowlisted scheme (about: chrome:, etc).
+ // * CSP is enabled
+ // * Content Type is not allowlisted (CSP Reports, TYPE_DOCUMENT, etc).
+ // * Fast Path for Apps
+
+ // Default decision, CSP can revise it if there's a policy to enforce
+ *outDecision = nsIContentPolicy::ACCEPT;
+
+ // If the content type doesn't map to a CSP directive, there's nothing for
+ // CSP to do.
+ CSPDirective dir = CSP_ContentTypeToDirective(aContentType);
+ if (dir == nsIContentSecurityPolicy::NO_DIRECTIVE) {
+ return NS_OK;
+ }
+
+ bool permitted = permitsInternal(
+ dir,
+ nullptr, // aTriggeringElement
+ aCSPEventListener, aLoadInfo, aContentLocation, aOriginalURIIfRedirect,
+ false, // allow fallback to default-src
+ aSendViolationReports,
+ true); // send blocked URI in violation reports
+
+ *outDecision =
+ permitted ? nsIContentPolicy::ACCEPT : nsIContentPolicy::REJECT_SERVER;
+
+ if (CSPCONTEXTLOGENABLED()) {
+ CSPCONTEXTLOG(
+ ("nsCSPContext::ShouldLoad, decision: %s, "
+ "aContentLocation: %s",
+ *outDecision > 0 ? "load" : "deny",
+ aContentLocation->GetSpecOrDefault().get()));
+ }
+ return NS_OK;
+}
+
+bool nsCSPContext::permitsInternal(
+ CSPDirective aDir, Element* aTriggeringElement,
+ nsICSPEventListener* aCSPEventListener, nsILoadInfo* aLoadInfo,
+ nsIURI* aContentLocation, nsIURI* aOriginalURIIfRedirect, bool aSpecific,
+ bool aSendViolationReports, bool aSendContentLocationInViolationReports) {
+ EnsureIPCPoliciesRead();
+ bool permits = true;
+
+ nsAutoString violatedDirective;
+ for (uint32_t p = 0; p < mPolicies.Length(); p++) {
+ if (!mPolicies[p]->permits(aDir, aLoadInfo, aContentLocation,
+ !!aOriginalURIIfRedirect, aSpecific,
+ violatedDirective)) {
+ // If the policy is violated and not report-only, reject the load and
+ // report to the console
+ if (!mPolicies[p]->getReportOnlyFlag()) {
+ CSPCONTEXTLOG(("nsCSPContext::permitsInternal, false"));
+ permits = false;
+ }
+
+ // In CSP 3.0 the effective directive doesn't become the actually used
+ // directive in case of a fallback from e.g. script-src-elem to
+ // script-src or default-src.
+ nsAutoString effectiveDirective;
+ effectiveDirective.AssignASCII(CSP_CSPDirectiveToString(aDir));
+
+ // Callers should set |aSendViolationReports| to false if this is a
+ // preload - the decision may be wrong due to the inability to get the
+ // nonce, and will incorrectly fail the unit tests.
+ if (aSendViolationReports) {
+ uint32_t lineNumber = 0;
+ uint32_t columnNumber = 1;
+ nsAutoString spec;
+ JSContext* cx = nsContentUtils::GetCurrentJSContext();
+ if (cx) {
+ nsJSUtils::GetCallingLocation(cx, spec, &lineNumber, &columnNumber);
+ // If GetCallingLocation fails linenumber & columnNumber are set to
+ // (0, 1) anyway so we can skip checking if that is the case.
+ }
+ AsyncReportViolation(
+ aTriggeringElement, aCSPEventListener,
+ (aSendContentLocationInViolationReports ? aContentLocation
+ : nullptr),
+ BlockedContentSource::eUnknown, /* a BlockedContentSource */
+ aOriginalURIIfRedirect, /* in case of redirect originalURI is not
+ null */
+ violatedDirective, effectiveDirective, p, /* policy index */
+ u""_ns, /* no observer subject */
+ spec, /* source file */
+ false, // aReportSample (no sample)
+ u""_ns, /* no script sample */
+ lineNumber, /* line number */
+ columnNumber); /* column number */
+ }
+ }
+ }
+
+ return permits;
+}
+
+/* ===== nsISupports implementation ========== */
+
+NS_IMPL_CLASSINFO(nsCSPContext, nullptr, 0, NS_CSPCONTEXT_CID)
+
+NS_IMPL_ISUPPORTS_CI(nsCSPContext, nsIContentSecurityPolicy, nsISerializable)
+
+nsCSPContext::nsCSPContext()
+ : mInnerWindowID(0),
+ mSkipAllowInlineStyleCheck(false),
+ mLoadingContext(nullptr),
+ mLoadingPrincipal(nullptr),
+ mQueueUpMessages(true) {
+ CSPCONTEXTLOG(("nsCSPContext::nsCSPContext"));
+}
+
+nsCSPContext::~nsCSPContext() {
+ CSPCONTEXTLOG(("nsCSPContext::~nsCSPContext"));
+ for (uint32_t i = 0; i < mPolicies.Length(); i++) {
+ delete mPolicies[i];
+ }
+}
+
+/* static */
+bool nsCSPContext::Equals(nsIContentSecurityPolicy* aCSP,
+ nsIContentSecurityPolicy* aOtherCSP) {
+ if (aCSP == aOtherCSP) {
+ // fast path for pointer equality
+ return true;
+ }
+
+ uint32_t policyCount = 0;
+ if (aCSP) {
+ aCSP->GetPolicyCount(&policyCount);
+ }
+
+ uint32_t otherPolicyCount = 0;
+ if (aOtherCSP) {
+ aOtherCSP->GetPolicyCount(&otherPolicyCount);
+ }
+
+ if (policyCount != otherPolicyCount) {
+ return false;
+ }
+
+ nsAutoString policyStr, otherPolicyStr;
+ for (uint32_t i = 0; i < policyCount; ++i) {
+ aCSP->GetPolicyString(i, policyStr);
+ aOtherCSP->GetPolicyString(i, otherPolicyStr);
+ if (!policyStr.Equals(otherPolicyStr)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+nsresult nsCSPContext::InitFromOther(nsCSPContext* aOtherContext) {
+ NS_ENSURE_ARG(aOtherContext);
+
+ nsresult rv = NS_OK;
+ nsCOMPtr<Document> doc = do_QueryReferent(aOtherContext->mLoadingContext);
+ if (doc) {
+ rv = SetRequestContextWithDocument(doc);
+ } else {
+ rv = SetRequestContextWithPrincipal(
+ aOtherContext->mLoadingPrincipal, aOtherContext->mSelfURI,
+ aOtherContext->mReferrer, aOtherContext->mInnerWindowID);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mSkipAllowInlineStyleCheck = aOtherContext->mSkipAllowInlineStyleCheck;
+
+ // This policy was already parsed somewhere else, don't emit parsing errors.
+ mSuppressParserLogMessages = true;
+ for (auto policy : aOtherContext->mPolicies) {
+ nsAutoString policyStr;
+ policy->toString(policyStr);
+ AppendPolicy(policyStr, policy->getReportOnlyFlag(),
+ policy->getDeliveredViaMetaTagFlag());
+ }
+
+ mSuppressParserLogMessages = aOtherContext->mSuppressParserLogMessages;
+
+ mIPCPolicies = aOtherContext->mIPCPolicies.Clone();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCSPContext::EnsureIPCPoliciesRead() {
+ // Most likely the parser errors already happened before serializing
+ // the policy for IPC.
+ bool previous = mSuppressParserLogMessages;
+ mSuppressParserLogMessages = true;
+
+ if (mIPCPolicies.Length() > 0) {
+ nsresult rv;
+ for (auto& policy : mIPCPolicies) {
+ rv = AppendPolicy(policy.policy(), policy.reportOnlyFlag(),
+ policy.deliveredViaMetaTagFlag());
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+ }
+ mIPCPolicies.Clear();
+ }
+
+ mSuppressParserLogMessages = previous;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCSPContext::GetPolicyString(uint32_t aIndex, nsAString& outStr) {
+ outStr.Truncate();
+ EnsureIPCPoliciesRead();
+ if (aIndex >= mPolicies.Length()) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+ mPolicies[aIndex]->toString(outStr);
+ return NS_OK;
+}
+
+const nsCSPPolicy* nsCSPContext::GetPolicy(uint32_t aIndex) {
+ EnsureIPCPoliciesRead();
+ if (aIndex >= mPolicies.Length()) {
+ return nullptr;
+ }
+ return mPolicies[aIndex];
+}
+
+NS_IMETHODIMP
+nsCSPContext::GetPolicyCount(uint32_t* outPolicyCount) {
+ EnsureIPCPoliciesRead();
+ *outPolicyCount = mPolicies.Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCSPContext::GetUpgradeInsecureRequests(bool* outUpgradeRequest) {
+ EnsureIPCPoliciesRead();
+ *outUpgradeRequest = false;
+ for (uint32_t i = 0; i < mPolicies.Length(); i++) {
+ if (mPolicies[i]->hasDirective(
+ nsIContentSecurityPolicy::UPGRADE_IF_INSECURE_DIRECTIVE) &&
+ !mPolicies[i]->getReportOnlyFlag()) {
+ *outUpgradeRequest = true;
+ return NS_OK;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCSPContext::GetBlockAllMixedContent(bool* outBlockAllMixedContent) {
+ EnsureIPCPoliciesRead();
+ *outBlockAllMixedContent = false;
+ for (uint32_t i = 0; i < mPolicies.Length(); i++) {
+ if (!mPolicies[i]->getReportOnlyFlag() &&
+ mPolicies[i]->hasDirective(
+ nsIContentSecurityPolicy::BLOCK_ALL_MIXED_CONTENT)) {
+ *outBlockAllMixedContent = true;
+ return NS_OK;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCSPContext::GetEnforcesFrameAncestors(bool* outEnforcesFrameAncestors) {
+ EnsureIPCPoliciesRead();
+ *outEnforcesFrameAncestors = false;
+ for (uint32_t i = 0; i < mPolicies.Length(); i++) {
+ if (!mPolicies[i]->getReportOnlyFlag() &&
+ mPolicies[i]->hasDirective(
+ nsIContentSecurityPolicy::FRAME_ANCESTORS_DIRECTIVE)) {
+ *outEnforcesFrameAncestors = true;
+ return NS_OK;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCSPContext::AppendPolicy(const nsAString& aPolicyString, bool aReportOnly,
+ bool aDeliveredViaMetaTag) {
+ CSPCONTEXTLOG(("nsCSPContext::AppendPolicy: %s",
+ NS_ConvertUTF16toUTF8(aPolicyString).get()));
+
+ // Use mSelfURI from setRequestContextWith{Document,Principal} (bug 991474)
+ MOZ_ASSERT(
+ mLoadingPrincipal,
+ "did you forget to call setRequestContextWith{Document,Principal}?");
+ MOZ_ASSERT(
+ mSelfURI,
+ "did you forget to call setRequestContextWith{Document,Principal}?");
+ NS_ENSURE_TRUE(mLoadingPrincipal, NS_ERROR_UNEXPECTED);
+ NS_ENSURE_TRUE(mSelfURI, NS_ERROR_UNEXPECTED);
+
+ if (CSPORIGINLOGENABLED()) {
+ nsAutoCString selfURISpec;
+ mSelfURI->GetSpec(selfURISpec);
+ CSPORIGINLOG(("CSP - AppendPolicy"));
+ CSPORIGINLOG((" * selfURI: %s", selfURISpec.get()));
+ CSPORIGINLOG((" * reportOnly: %s", aReportOnly ? "yes" : "no"));
+ CSPORIGINLOG(
+ (" * deliveredViaMetaTag: %s", aDeliveredViaMetaTag ? "yes" : "no"));
+ CSPORIGINLOG(
+ (" * policy: %s\n", NS_ConvertUTF16toUTF8(aPolicyString).get()));
+ }
+
+ nsCSPPolicy* policy = nsCSPParser::parseContentSecurityPolicy(
+ aPolicyString, mSelfURI, aReportOnly, this, aDeliveredViaMetaTag,
+ mSuppressParserLogMessages);
+ if (policy) {
+ if (policy->hasDirective(
+ nsIContentSecurityPolicy::UPGRADE_IF_INSECURE_DIRECTIVE)) {
+ nsAutoCString selfURIspec, referrer;
+ if (mSelfURI) {
+ mSelfURI->GetAsciiSpec(selfURIspec);
+ }
+ CopyUTF16toUTF8(mReferrer, referrer);
+ CSPCONTEXTLOG(
+ ("nsCSPContext::AppendPolicy added UPGRADE_IF_INSECURE_DIRECTIVE "
+ "self-uri=%s referrer=%s",
+ selfURIspec.get(), referrer.get()));
+ }
+
+ mPolicies.AppendElement(policy);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCSPContext::GetAllowsEval(bool* outShouldReportViolation,
+ bool* outAllowsEval) {
+ EnsureIPCPoliciesRead();
+ *outShouldReportViolation = false;
+ *outAllowsEval = true;
+
+ for (uint32_t i = 0; i < mPolicies.Length(); i++) {
+ if (!mPolicies[i]->allows(SCRIPT_SRC_DIRECTIVE, CSP_UNSAFE_EVAL, u""_ns)) {
+ // policy is violated: must report the violation and allow the inline
+ // script if the policy is report-only.
+ *outShouldReportViolation = true;
+ if (!mPolicies[i]->getReportOnlyFlag()) {
+ *outAllowsEval = false;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCSPContext::GetAllowsWasmEval(bool* outShouldReportViolation,
+ bool* outAllowsWasmEval) {
+ EnsureIPCPoliciesRead();
+ *outShouldReportViolation = false;
+ *outAllowsWasmEval = true;
+
+ for (uint32_t i = 0; i < mPolicies.Length(); i++) {
+ // Either 'unsafe-eval' or 'wasm-unsafe-eval' can allow this
+ if (!mPolicies[i]->allows(SCRIPT_SRC_DIRECTIVE, CSP_WASM_UNSAFE_EVAL,
+ u""_ns) &&
+ !mPolicies[i]->allows(SCRIPT_SRC_DIRECTIVE, CSP_UNSAFE_EVAL, u""_ns)) {
+ // policy is violated: must report the violation and allow the inline
+ // script if the policy is report-only.
+ *outShouldReportViolation = true;
+ if (!mPolicies[i]->getReportOnlyFlag()) {
+ *outAllowsWasmEval = false;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+// Helper function to report inline violations
+void nsCSPContext::reportInlineViolation(
+ CSPDirective aDirective, Element* aTriggeringElement,
+ nsICSPEventListener* aCSPEventListener, const nsAString& aNonce,
+ bool aReportSample, const nsAString& aSample,
+ const nsAString& aViolatedDirective, const nsAString& aEffectiveDirective,
+ uint32_t aViolatedPolicyIndex, // TODO, use report only flag for that
+ uint32_t aLineNumber, uint32_t aColumnNumber) {
+ nsString observerSubject;
+ // if the nonce is non empty, then we report the nonce error, otherwise
+ // let's report the hash error; no need to report the unsafe-inline error
+ // anymore.
+ if (!aNonce.IsEmpty()) {
+ observerSubject = (aDirective == SCRIPT_SRC_ELEM_DIRECTIVE ||
+ aDirective == SCRIPT_SRC_ATTR_DIRECTIVE)
+ ? NS_LITERAL_STRING_FROM_CSTRING(
+ SCRIPT_NONCE_VIOLATION_OBSERVER_TOPIC)
+ : NS_LITERAL_STRING_FROM_CSTRING(
+ STYLE_NONCE_VIOLATION_OBSERVER_TOPIC);
+ } else {
+ observerSubject = (aDirective == SCRIPT_SRC_ELEM_DIRECTIVE ||
+ aDirective == SCRIPT_SRC_ATTR_DIRECTIVE)
+ ? NS_LITERAL_STRING_FROM_CSTRING(
+ SCRIPT_HASH_VIOLATION_OBSERVER_TOPIC)
+ : NS_LITERAL_STRING_FROM_CSTRING(
+ STYLE_HASH_VIOLATION_OBSERVER_TOPIC);
+ }
+
+ nsAutoString sourceFile;
+ uint32_t lineNumber;
+ uint32_t columnNumber;
+
+ JSContext* cx = nsContentUtils::GetCurrentJSContext();
+ if (!cx || !nsJSUtils::GetCallingLocation(cx, sourceFile, &lineNumber,
+ &columnNumber)) {
+ // use selfURI as the sourceFile
+ if (mSelfURI) {
+ nsAutoCString cSourceFile;
+ mSelfURI->GetSpec(cSourceFile);
+ sourceFile.Assign(NS_ConvertUTF8toUTF16(cSourceFile));
+ }
+ lineNumber = aLineNumber;
+ columnNumber = aColumnNumber;
+ }
+
+ AsyncReportViolation(aTriggeringElement, aCSPEventListener,
+ nullptr, // aBlockedURI
+ BlockedContentSource::eInline, // aBlockedSource
+ mSelfURI, // aOriginalURI
+ aViolatedDirective, // aViolatedDirective
+ aEffectiveDirective, // aEffectiveDirective
+ aViolatedPolicyIndex, // aViolatedPolicyIndex
+ observerSubject, // aObserverSubject
+ sourceFile, // aSourceFile
+ aReportSample, // aReportSample
+ aSample, // aScriptSample
+ lineNumber, // aLineNum
+ columnNumber); // aColumnNum
+}
+
+NS_IMETHODIMP
+nsCSPContext::GetAllowsInline(CSPDirective aDirective, bool aHasUnsafeHash,
+ const nsAString& aNonce, bool aParserCreated,
+ Element* aTriggeringElement,
+ nsICSPEventListener* aCSPEventListener,
+ const nsAString& aContentOfPseudoScript,
+ uint32_t aLineNumber, uint32_t aColumnNumber,
+ bool* outAllowsInline) {
+ *outAllowsInline = true;
+
+ if (aDirective != SCRIPT_SRC_ELEM_DIRECTIVE &&
+ aDirective != SCRIPT_SRC_ATTR_DIRECTIVE &&
+ aDirective != STYLE_SRC_ELEM_DIRECTIVE &&
+ aDirective != STYLE_SRC_ATTR_DIRECTIVE) {
+ MOZ_ASSERT(false,
+ "can only allow inline for (script/style)-src-(attr/elem)");
+ return NS_OK;
+ }
+
+ EnsureIPCPoliciesRead();
+ nsAutoString content;
+
+ // always iterate all policies, otherwise we might not send out all reports
+ for (uint32_t i = 0; i < mPolicies.Length(); i++) {
+ // https://w3c.github.io/webappsec-csp/#match-element-to-source-list
+
+ // Step 1. If §6.7.3.2 Does a source list allow all inline behavior for
+ // type? returns "Allows" given list and type, return "Matches".
+ if (mPolicies[i]->allowsAllInlineBehavior(aDirective)) {
+ continue;
+ }
+
+ // Step 2. If type is "script" or "style", and §6.7.3.1 Is element
+ // nonceable? returns "Nonceable" when executed upon element:
+ if ((aDirective == SCRIPT_SRC_ELEM_DIRECTIVE ||
+ aDirective == STYLE_SRC_ELEM_DIRECTIVE) &&
+ aTriggeringElement && !aNonce.IsEmpty()) {
+#ifdef DEBUG
+ // NOTE: Folllowing Chrome "Is element nonceable?" doesn't apply to
+ // <style>.
+ if (aDirective == SCRIPT_SRC_ELEM_DIRECTIVE) {
+ // Our callers should have checked this.
+ MOZ_ASSERT(nsContentSecurityUtils::GetIsElementNonceableNonce(
+ *aTriggeringElement) == aNonce);
+ }
+#endif
+
+ // Step 2.1. For each expression of list: [...]
+ if (mPolicies[i]->allows(aDirective, CSP_NONCE, aNonce)) {
+ continue;
+ }
+ }
+
+ // Check the content length to ensure the content is not allocated more than
+ // once. Even though we are in a for loop, it is probable that there is only
+ // one policy, so this check may be unnecessary.
+ if (content.IsEmpty() && aTriggeringElement) {
+ nsCOMPtr<nsIScriptElement> element =
+ do_QueryInterface(aTriggeringElement);
+ if (element) {
+ element->GetScriptText(content);
+ }
+ }
+ if (content.IsEmpty()) {
+ content = aContentOfPseudoScript;
+ }
+
+ // Step 3. Let unsafe-hashes flag be false.
+ // Step 4. For each expression of list: [...]
+ bool unsafeHashesFlag =
+ mPolicies[i]->allows(aDirective, CSP_UNSAFE_HASHES, u""_ns);
+
+ // Step 5. If type is "script" or "style", or unsafe-hashes flag is true:
+ //
+ // aHasUnsafeHash is true for event handlers (type "script attribute"),
+ // style= attributes (type "style attribute") and the javascript: protocol.
+ if (!aHasUnsafeHash || unsafeHashesFlag) {
+ if (mPolicies[i]->allows(aDirective, CSP_HASH, content)) {
+ continue;
+ }
+ }
+
+ // TODO(Bug 1844290): Figure out how/if strict-dynamic for inline scripts is
+ // specified
+ bool allowed = false;
+ if ((aDirective == SCRIPT_SRC_ELEM_DIRECTIVE ||
+ aDirective == SCRIPT_SRC_ATTR_DIRECTIVE) &&
+ mPolicies[i]->allows(aDirective, CSP_STRICT_DYNAMIC, u""_ns)) {
+ allowed = !aParserCreated;
+ }
+
+ if (!allowed) {
+ // policy is violoated: deny the load unless policy is report only and
+ // report the violation.
+ if (!mPolicies[i]->getReportOnlyFlag()) {
+ *outAllowsInline = false;
+ }
+ nsAutoString violatedDirective;
+ bool reportSample = false;
+ mPolicies[i]->getDirectiveStringAndReportSampleForContentType(
+ aDirective, violatedDirective, &reportSample);
+
+ // In CSP 3.0 the effective directive doesn't become the actually used
+ // directive in case of a fallback from e.g. script-src-elem to
+ // script-src or default-src.
+ nsAutoString effectiveDirective;
+ effectiveDirective.AssignASCII(CSP_CSPDirectiveToString(aDirective));
+
+ reportInlineViolation(aDirective, aTriggeringElement, aCSPEventListener,
+ aNonce, reportSample, content, violatedDirective,
+ effectiveDirective, i, aLineNumber, aColumnNumber);
+ }
+ }
+
+ return NS_OK;
+}
+
+/**
+ * For each policy, log any violation on the Error Console and send a report
+ * if a report-uri is present in the policy
+ *
+ * @param aViolationType
+ * one of the VIOLATION_TYPE_* constants, e.g. inline-script or eval
+ * @param aSourceFile
+ * name of the source file containing the violation (if available)
+ * @param aContentSample
+ * sample of the violating content (to aid debugging)
+ * @param aLineNum
+ * source line number of the violation (if available)
+ * @param aColumnNum
+ * source column number of the violation (if available)
+ * @param aNonce
+ * (optional) If this is a nonce violation, include the nonce so we can
+ * recheck to determine which policies were violated and send the
+ * appropriate reports.
+ * @param aContent
+ * (optional) If this is a hash violation, include contents of the inline
+ * resource in the question so we can recheck the hash in order to
+ * determine which policies were violated and send the appropriate
+ * reports.
+ */
+NS_IMETHODIMP
+nsCSPContext::LogViolationDetails(
+ uint16_t aViolationType, Element* aTriggeringElement,
+ nsICSPEventListener* aCSPEventListener, const nsAString& aSourceFile,
+ const nsAString& aScriptSample, int32_t aLineNum, int32_t aColumnNum,
+ const nsAString& aNonce, const nsAString& aContent) {
+ EnsureIPCPoliciesRead();
+
+ BlockedContentSource blockedContentSource;
+ enum CSPKeyword keyword;
+ nsAutoString observerSubject;
+ if (aViolationType == nsIContentSecurityPolicy::VIOLATION_TYPE_EVAL) {
+ blockedContentSource = BlockedContentSource::eEval;
+ keyword = CSP_UNSAFE_EVAL;
+ observerSubject.AssignLiteral(EVAL_VIOLATION_OBSERVER_TOPIC);
+ } else {
+ NS_ASSERTION(
+ aViolationType == nsIContentSecurityPolicy::VIOLATION_TYPE_WASM_EVAL,
+ "unexpected aViolationType");
+ blockedContentSource = BlockedContentSource::eWasmEval;
+ keyword = CSP_WASM_UNSAFE_EVAL;
+ observerSubject.AssignLiteral(WASM_EVAL_VIOLATION_OBSERVER_TOPIC);
+ }
+
+ for (uint32_t p = 0; p < mPolicies.Length(); p++) {
+ NS_ASSERTION(mPolicies[p], "null pointer in nsTArray<nsCSPPolicy>");
+
+ if (mPolicies[p]->allows(SCRIPT_SRC_DIRECTIVE, keyword, u""_ns)) {
+ continue;
+ }
+
+ nsAutoString violatedDirective;
+ bool reportSample = false;
+ mPolicies[p]->getDirectiveStringAndReportSampleForContentType(
+ SCRIPT_SRC_DIRECTIVE, violatedDirective, &reportSample);
+
+ AsyncReportViolation(aTriggeringElement, aCSPEventListener, nullptr,
+ blockedContentSource, nullptr, violatedDirective,
+ u"script-src"_ns /* aEffectiveDirective */, p,
+ observerSubject, aSourceFile, reportSample,
+ aScriptSample, aLineNum, aColumnNum);
+ }
+ return NS_OK;
+}
+
+#undef CASE_CHECK_AND_REPORT
+
+NS_IMETHODIMP
+nsCSPContext::SetRequestContextWithDocument(Document* aDocument) {
+ MOZ_ASSERT(aDocument, "Can't set context without doc");
+ NS_ENSURE_ARG(aDocument);
+
+ mLoadingContext = do_GetWeakReference(aDocument);
+ mSelfURI = aDocument->GetDocumentURI();
+ mLoadingPrincipal = aDocument->NodePrincipal();
+ aDocument->GetReferrer(mReferrer);
+ mInnerWindowID = aDocument->InnerWindowID();
+ // the innerWindowID is not available for CSPs delivered through the
+ // header at the time setReqeustContext is called - let's queue up
+ // console messages until it becomes available, see flushConsoleMessages
+ mQueueUpMessages = !mInnerWindowID;
+ mCallingChannelLoadGroup = aDocument->GetDocumentLoadGroup();
+ // set the flag on the document for CSP telemetry
+ mEventTarget = GetMainThreadSerialEventTarget();
+
+ MOZ_ASSERT(mLoadingPrincipal, "need a valid requestPrincipal");
+ MOZ_ASSERT(mSelfURI, "need mSelfURI to translate 'self' into actual URI");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCSPContext::SetRequestContextWithPrincipal(nsIPrincipal* aRequestPrincipal,
+ nsIURI* aSelfURI,
+ const nsAString& aReferrer,
+ uint64_t aInnerWindowId) {
+ NS_ENSURE_ARG(aRequestPrincipal);
+
+ mLoadingPrincipal = aRequestPrincipal;
+ mSelfURI = aSelfURI;
+ mReferrer = aReferrer;
+ mInnerWindowID = aInnerWindowId;
+ // if no document is available, then it also does not make sense to queue
+ // console messages sending messages to the browser console instead of the web
+ // console in that case.
+ mQueueUpMessages = false;
+ mCallingChannelLoadGroup = nullptr;
+ mEventTarget = nullptr;
+
+ MOZ_ASSERT(mLoadingPrincipal, "need a valid requestPrincipal");
+ MOZ_ASSERT(mSelfURI, "need mSelfURI to translate 'self' into actual URI");
+ return NS_OK;
+}
+
+nsIPrincipal* nsCSPContext::GetRequestPrincipal() { return mLoadingPrincipal; }
+
+nsIURI* nsCSPContext::GetSelfURI() { return mSelfURI; }
+
+NS_IMETHODIMP
+nsCSPContext::GetReferrer(nsAString& outReferrer) {
+ outReferrer.Truncate();
+ outReferrer.Append(mReferrer);
+ return NS_OK;
+}
+
+uint64_t nsCSPContext::GetInnerWindowID() { return mInnerWindowID; }
+
+bool nsCSPContext::GetSkipAllowInlineStyleCheck() {
+ return mSkipAllowInlineStyleCheck;
+}
+
+void nsCSPContext::SetSkipAllowInlineStyleCheck(
+ bool aSkipAllowInlineStyleCheck) {
+ mSkipAllowInlineStyleCheck = aSkipAllowInlineStyleCheck;
+}
+
+NS_IMETHODIMP
+nsCSPContext::EnsureEventTarget(nsIEventTarget* aEventTarget) {
+ NS_ENSURE_ARG(aEventTarget);
+ // Don't bother if we did have a valid event target (if the csp object is
+ // tied to a document in SetRequestContextWithDocument)
+ if (mEventTarget) {
+ return NS_OK;
+ }
+
+ mEventTarget = aEventTarget;
+ return NS_OK;
+}
+
+struct ConsoleMsgQueueElem {
+ nsString mMsg;
+ nsString mSourceName;
+ nsString mSourceLine;
+ uint32_t mLineNumber;
+ uint32_t mColumnNumber;
+ uint32_t mSeverityFlag;
+ nsCString mCategory;
+};
+
+void nsCSPContext::flushConsoleMessages() {
+ bool privateWindow = false;
+
+ // should flush messages even if doc is not available
+ nsCOMPtr<Document> doc = do_QueryReferent(mLoadingContext);
+ if (doc) {
+ mInnerWindowID = doc->InnerWindowID();
+ privateWindow =
+ !!doc->NodePrincipal()->OriginAttributesRef().mPrivateBrowsingId;
+ }
+
+ mQueueUpMessages = false;
+
+ for (uint32_t i = 0; i < mConsoleMsgQueue.Length(); i++) {
+ ConsoleMsgQueueElem& elem = mConsoleMsgQueue[i];
+ CSP_LogMessage(elem.mMsg, elem.mSourceName, elem.mSourceLine,
+ elem.mLineNumber, elem.mColumnNumber, elem.mSeverityFlag,
+ elem.mCategory, mInnerWindowID, privateWindow);
+ }
+ mConsoleMsgQueue.Clear();
+}
+
+void nsCSPContext::logToConsole(const char* aName,
+ const nsTArray<nsString>& aParams,
+ const nsAString& aSourceName,
+ const nsAString& aSourceLine,
+ uint32_t aLineNumber, uint32_t aColumnNumber,
+ uint32_t aSeverityFlag) {
+ // we are passing aName as the category so we can link to the
+ // appropriate MDN docs depending on the specific error.
+ nsDependentCString category(aName);
+
+ // Fallback
+ nsAutoString sourceName(aSourceName);
+ if (sourceName.IsEmpty() && mSelfURI) {
+ nsAutoCString spec;
+ mSelfURI->GetSpec(spec);
+ CopyUTF8toUTF16(spec, sourceName);
+ }
+
+ // let's check if we have to queue up console messages
+ if (mQueueUpMessages) {
+ nsAutoString msg;
+ CSP_GetLocalizedStr(aName, aParams, msg);
+ ConsoleMsgQueueElem& elem = *mConsoleMsgQueue.AppendElement();
+ elem.mMsg = msg;
+ elem.mSourceName = PromiseFlatString(sourceName);
+ elem.mSourceLine = PromiseFlatString(aSourceLine);
+ elem.mLineNumber = aLineNumber;
+ elem.mColumnNumber = aColumnNumber;
+ elem.mSeverityFlag = aSeverityFlag;
+ elem.mCategory = category;
+ return;
+ }
+
+ bool privateWindow = false;
+ nsCOMPtr<Document> doc = do_QueryReferent(mLoadingContext);
+ if (doc) {
+ privateWindow =
+ !!doc->NodePrincipal()->OriginAttributesRef().mPrivateBrowsingId;
+ }
+
+ CSP_LogLocalizedStr(aName, aParams, sourceName, aSourceLine, aLineNumber,
+ aColumnNumber, aSeverityFlag, category, mInnerWindowID,
+ privateWindow);
+}
+
+/**
+ * Strip URI for reporting according to:
+ * https://w3c.github.io/webappsec-csp/#security-violation-reports
+ *
+ * @param aSelfURI
+ * The URI of the CSP policy. Used for cross-origin checks.
+ * @param aURI
+ * The URI of the blocked resource. In case of a redirect, this it the
+ * initial URI the request started out with, not the redirected URI.
+ * @param aEffectiveDirective
+ * The effective directive that triggered this report
+ * @return The ASCII serialization of the uri to be reported ignoring
+ * the ref part of the URI.
+ */
+void StripURIForReporting(nsIURI* aSelfURI, nsIURI* aURI,
+ const nsAString& aEffectiveDirective,
+ nsACString& outStrippedURI) {
+ // If the origin of aURI is a globally unique identifier (for example,
+ // aURI has a scheme of data, blob, or filesystem), then
+ // return the ASCII serialization of uri’s scheme.
+ bool isHttpOrWs = (aURI->SchemeIs("http") || aURI->SchemeIs("https") ||
+ aURI->SchemeIs("ws") || aURI->SchemeIs("wss"));
+
+ if (!isHttpOrWs) {
+ // not strictly spec compliant, but what we really care about is
+ // http/https. If it's not http/https, then treat aURI
+ // as if it's a globally unique identifier and just return the scheme.
+ aURI->GetScheme(outStrippedURI);
+ return;
+ }
+
+ // For cross-origin URIs in frame-src also strip the path.
+ // This prevents detailed tracking of pages loaded into an iframe
+ // by the embedding page using a report-only policy.
+ if (aEffectiveDirective.EqualsLiteral("frame-src") ||
+ aEffectiveDirective.EqualsLiteral("object-src")) {
+ nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager();
+ if (NS_FAILED(ssm->CheckSameOriginURI(aSelfURI, aURI, false, false))) {
+ aURI->GetPrePath(outStrippedURI);
+ return;
+ }
+ }
+
+ // Return aURI, with any fragment component removed.
+ aURI->GetSpecIgnoringRef(outStrippedURI);
+}
+
+nsresult nsCSPContext::GatherSecurityPolicyViolationEventData(
+ nsIURI* aBlockedURI, const nsACString& aBlockedString, nsIURI* aOriginalURI,
+ const nsAString& aEffectiveDirective, uint32_t aViolatedPolicyIndex,
+ const nsAString& aSourceFile, const nsAString& aScriptSample,
+ uint32_t aLineNum, uint32_t aColumnNum,
+ mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit) {
+ EnsureIPCPoliciesRead();
+ NS_ENSURE_ARG_MAX(aViolatedPolicyIndex, mPolicies.Length() - 1);
+
+ MOZ_ASSERT(ValidateDirectiveName(aEffectiveDirective),
+ "Invalid directive name");
+
+ nsresult rv;
+
+ // document-uri
+ nsAutoCString reportDocumentURI;
+ StripURIForReporting(mSelfURI, mSelfURI, aEffectiveDirective,
+ reportDocumentURI);
+ CopyUTF8toUTF16(reportDocumentURI, aViolationEventInit.mDocumentURI);
+
+ // referrer
+ aViolationEventInit.mReferrer = mReferrer;
+
+ // blocked-uri
+ if (aBlockedURI) {
+ nsAutoCString reportBlockedURI;
+ StripURIForReporting(mSelfURI, aOriginalURI ? aOriginalURI : aBlockedURI,
+ aEffectiveDirective, reportBlockedURI);
+ CopyUTF8toUTF16(reportBlockedURI, aViolationEventInit.mBlockedURI);
+ } else {
+ CopyUTF8toUTF16(aBlockedString, aViolationEventInit.mBlockedURI);
+ }
+
+ // effective-directive
+ // The name of the policy directive that was violated.
+ aViolationEventInit.mEffectiveDirective = aEffectiveDirective;
+
+ // violated-directive
+ // In CSP2, the policy directive that was violated, as it appears in the
+ // policy. In CSP3, the same as effective-directive.
+ aViolationEventInit.mViolatedDirective = aEffectiveDirective;
+
+ // original-policy
+ nsAutoString originalPolicy;
+ rv = this->GetPolicyString(aViolatedPolicyIndex, originalPolicy);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aViolationEventInit.mOriginalPolicy = originalPolicy;
+
+ // source-file
+ if (!aSourceFile.IsEmpty()) {
+ // if aSourceFile is a URI, we have to make sure to strip fragments
+ nsCOMPtr<nsIURI> sourceURI;
+ NS_NewURI(getter_AddRefs(sourceURI), aSourceFile);
+ if (sourceURI) {
+ nsAutoCString spec;
+ StripURIForReporting(mSelfURI, sourceURI, aEffectiveDirective, spec);
+ CopyUTF8toUTF16(spec, aViolationEventInit.mSourceFile);
+ } else {
+ aViolationEventInit.mSourceFile = aSourceFile;
+ }
+ }
+
+ // sample (already truncated)
+ aViolationEventInit.mSample = aScriptSample;
+
+ // disposition
+ aViolationEventInit.mDisposition =
+ mPolicies[aViolatedPolicyIndex]->getReportOnlyFlag()
+ ? mozilla::dom::SecurityPolicyViolationEventDisposition::Report
+ : mozilla::dom::SecurityPolicyViolationEventDisposition::Enforce;
+
+ // status-code
+ uint16_t statusCode = 0;
+ {
+ nsCOMPtr<Document> doc = do_QueryReferent(mLoadingContext);
+ if (doc) {
+ nsCOMPtr<nsIHttpChannel> channel = do_QueryInterface(doc->GetChannel());
+ if (channel) {
+ uint32_t responseStatus = 0;
+ nsresult rv = channel->GetResponseStatus(&responseStatus);
+ if (NS_SUCCEEDED(rv) && (responseStatus <= UINT16_MAX)) {
+ statusCode = static_cast<uint16_t>(responseStatus);
+ }
+ }
+ }
+ }
+ aViolationEventInit.mStatusCode = statusCode;
+
+ // line-number
+ aViolationEventInit.mLineNumber = aLineNum;
+
+ // column-number
+ aViolationEventInit.mColumnNumber = aColumnNum;
+
+ aViolationEventInit.mBubbles = true;
+ aViolationEventInit.mComposed = true;
+
+ return NS_OK;
+}
+
+bool nsCSPContext::ShouldThrottleReport(
+ const mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit) {
+ // Fetch rate limiting preferences
+ const uint32_t kLimitCount =
+ StaticPrefs::security_csp_reporting_limit_count();
+ const uint32_t kTimeSpanSeconds =
+ StaticPrefs::security_csp_reporting_limit_timespan();
+
+ // Disable throttling if either of the preferences is set to 0.
+ if (kLimitCount == 0 || kTimeSpanSeconds == 0) {
+ return false;
+ }
+
+ TimeDuration throttleSpan = TimeDuration::FromSeconds(kTimeSpanSeconds);
+ if (mSendReportLimitSpanStart.IsNull() ||
+ ((TimeStamp::Now() - mSendReportLimitSpanStart) > throttleSpan)) {
+ // Initial call or timespan exceeded, reset counter and timespan.
+ mSendReportLimitSpanStart = TimeStamp::Now();
+ mSendReportLimitCount = 1;
+ // Also make sure we warn about omitted messages. (XXX or only do this once
+ // per context?)
+ mWarnedAboutTooManyReports = false;
+ return false;
+ }
+
+ if (mSendReportLimitCount < kLimitCount) {
+ mSendReportLimitCount++;
+ return false;
+ }
+
+ // Rate limit reached
+ if (!mWarnedAboutTooManyReports) {
+ logToConsole("tooManyReports", {}, aViolationEventInit.mSourceFile,
+ aViolationEventInit.mSample, aViolationEventInit.mLineNumber,
+ aViolationEventInit.mColumnNumber, nsIScriptError::errorFlag);
+ mWarnedAboutTooManyReports = true;
+ }
+ return true;
+}
+
+nsresult nsCSPContext::SendReports(
+ const mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit,
+ uint32_t aViolatedPolicyIndex) {
+ EnsureIPCPoliciesRead();
+ NS_ENSURE_ARG_MAX(aViolatedPolicyIndex, mPolicies.Length() - 1);
+
+ nsTArray<nsString> reportURIs;
+ mPolicies[aViolatedPolicyIndex]->getReportURIs(reportURIs);
+ // There is nowhere to send reports to.
+ if (reportURIs.IsEmpty()) {
+ return NS_OK;
+ }
+
+ if (ShouldThrottleReport(aViolationEventInit)) {
+ return NS_OK;
+ }
+
+ dom::CSPReport report;
+
+ // blocked-uri
+ report.mCsp_report.mBlocked_uri = aViolationEventInit.mBlockedURI;
+
+ // document-uri
+ report.mCsp_report.mDocument_uri = aViolationEventInit.mDocumentURI;
+
+ // original-policy
+ report.mCsp_report.mOriginal_policy = aViolationEventInit.mOriginalPolicy;
+
+ // referrer
+ report.mCsp_report.mReferrer = aViolationEventInit.mReferrer;
+
+ // effective-directive
+ report.mCsp_report.mEffective_directive =
+ aViolationEventInit.mEffectiveDirective;
+
+ // violated-directive
+ report.mCsp_report.mViolated_directive =
+ aViolationEventInit.mEffectiveDirective;
+
+ // disposition
+ report.mCsp_report.mDisposition = aViolationEventInit.mDisposition;
+
+ // status-code
+ report.mCsp_report.mStatus_code = aViolationEventInit.mStatusCode;
+
+ // source-file
+ if (!aViolationEventInit.mSourceFile.IsEmpty()) {
+ report.mCsp_report.mSource_file.Construct();
+ report.mCsp_report.mSource_file.Value() = aViolationEventInit.mSourceFile;
+ }
+
+ // script-sample
+ if (!aViolationEventInit.mSample.IsEmpty()) {
+ report.mCsp_report.mScript_sample.Construct();
+ report.mCsp_report.mScript_sample.Value() = aViolationEventInit.mSample;
+ }
+
+ // line-number
+ if (aViolationEventInit.mLineNumber != 0) {
+ report.mCsp_report.mLine_number.Construct();
+ report.mCsp_report.mLine_number.Value() = aViolationEventInit.mLineNumber;
+ }
+
+ if (aViolationEventInit.mColumnNumber != 0) {
+ report.mCsp_report.mColumn_number.Construct();
+ report.mCsp_report.mColumn_number.Value() =
+ aViolationEventInit.mColumnNumber;
+ }
+
+ nsString csp_report;
+ if (!report.ToJSON(csp_report)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // ---------- Assembled, now send it to all the report URIs ----------- //
+ nsCOMPtr<Document> doc = do_QueryReferent(mLoadingContext);
+ nsCOMPtr<nsIURI> reportURI;
+ nsCOMPtr<nsIChannel> reportChannel;
+
+ nsresult rv;
+ for (uint32_t r = 0; r < reportURIs.Length(); r++) {
+ nsAutoCString reportURICstring = NS_ConvertUTF16toUTF8(reportURIs[r]);
+ // try to create a new uri from every report-uri string
+ rv = NS_NewURI(getter_AddRefs(reportURI), reportURIs[r]);
+ if (NS_FAILED(rv)) {
+ AutoTArray<nsString, 1> params = {reportURIs[r]};
+ CSPCONTEXTLOG(("Could not create nsIURI for report URI %s",
+ reportURICstring.get()));
+ logToConsole("triedToSendReport", params, aViolationEventInit.mSourceFile,
+ aViolationEventInit.mSample, aViolationEventInit.mLineNumber,
+ aViolationEventInit.mColumnNumber,
+ nsIScriptError::errorFlag);
+ continue; // don't return yet, there may be more URIs
+ }
+
+ // try to create a new channel for every report-uri
+ if (doc) {
+ rv =
+ NS_NewChannel(getter_AddRefs(reportChannel), reportURI, doc,
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_CSP_REPORT);
+ } else {
+ rv = NS_NewChannel(
+ getter_AddRefs(reportChannel), reportURI, mLoadingPrincipal,
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_CSP_REPORT);
+ }
+
+ if (NS_FAILED(rv)) {
+ CSPCONTEXTLOG(("Could not create new channel for report URI %s",
+ reportURICstring.get()));
+ continue; // don't return yet, there may be more URIs
+ }
+
+ // log a warning to console if scheme is not http or https
+ bool isHttpScheme =
+ reportURI->SchemeIs("http") || reportURI->SchemeIs("https");
+
+ if (!isHttpScheme) {
+ AutoTArray<nsString, 1> params = {reportURIs[r]};
+ logToConsole(
+ "reportURInotHttpsOrHttp2", params, aViolationEventInit.mSourceFile,
+ aViolationEventInit.mSample, aViolationEventInit.mLineNumber,
+ aViolationEventInit.mColumnNumber, nsIScriptError::errorFlag);
+ continue;
+ }
+
+ // make sure this is an anonymous request (no cookies) so in case the
+ // policy URI is injected, it can't be abused for CSRF.
+ nsLoadFlags flags;
+ rv = reportChannel->GetLoadFlags(&flags);
+ NS_ENSURE_SUCCESS(rv, rv);
+ flags |= nsIRequest::LOAD_ANONYMOUS;
+ rv = reportChannel->SetLoadFlags(flags);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // we need to set an nsIChannelEventSink on the channel object
+ // so we can tell it to not follow redirects when posting the reports
+ RefPtr<CSPReportRedirectSink> reportSink = new CSPReportRedirectSink();
+ if (doc && doc->GetDocShell()) {
+ nsCOMPtr<nsINetworkInterceptController> interceptController =
+ do_QueryInterface(doc->GetDocShell());
+ reportSink->SetInterceptController(interceptController);
+ }
+ reportChannel->SetNotificationCallbacks(reportSink);
+
+ // apply the loadgroup taken by setRequestContextWithDocument. If there's
+ // no loadgroup, AsyncOpen will fail on process-split necko (since the
+ // channel cannot query the iBrowserChild).
+ rv = reportChannel->SetLoadGroup(mCallingChannelLoadGroup);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // wire in the string input stream to send the report
+ nsCOMPtr<nsIStringInputStream> sis(
+ do_CreateInstance(NS_STRINGINPUTSTREAM_CONTRACTID));
+ NS_ASSERTION(sis,
+ "nsIStringInputStream is needed but not available to send CSP "
+ "violation reports");
+ nsAutoCString utf8CSPReport = NS_ConvertUTF16toUTF8(csp_report);
+ rv = sis->SetData(utf8CSPReport.get(), utf8CSPReport.Length());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIUploadChannel> uploadChannel(do_QueryInterface(reportChannel));
+ if (!uploadChannel) {
+ // It's possible the URI provided can't be uploaded to, in which case
+ // we skip this one. We'll already have warned about a non-HTTP URI
+ // earlier.
+ continue;
+ }
+
+ rv = uploadChannel->SetUploadStream(sis, "application/csp-report"_ns, -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // if this is an HTTP channel, set the request method to post
+ nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(reportChannel));
+ if (httpChannel) {
+ rv = httpChannel->SetRequestMethod("POST"_ns);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+
+ RefPtr<CSPViolationReportListener> listener =
+ new CSPViolationReportListener();
+ rv = reportChannel->AsyncOpen(listener);
+
+ // AsyncOpen should not fail, but could if there's no load group (like if
+ // SetRequestContextWith{Document,Principal} is not given a channel). This
+ // should fail quietly and not return an error since it's really ok if
+ // reports don't go out, but it's good to log the error locally.
+
+ if (NS_FAILED(rv)) {
+ AutoTArray<nsString, 1> params = {reportURIs[r]};
+ CSPCONTEXTLOG(("AsyncOpen failed for report URI %s",
+ NS_ConvertUTF16toUTF8(params[0]).get()));
+ logToConsole("triedToSendReport", params, aViolationEventInit.mSourceFile,
+ aViolationEventInit.mSample, aViolationEventInit.mLineNumber,
+ aViolationEventInit.mColumnNumber,
+ nsIScriptError::errorFlag);
+ } else {
+ CSPCONTEXTLOG(
+ ("Sent violation report to URI %s", reportURICstring.get()));
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsCSPContext::FireViolationEvent(
+ Element* aTriggeringElement, nsICSPEventListener* aCSPEventListener,
+ const mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit) {
+ if (aCSPEventListener) {
+ nsAutoString json;
+ if (aViolationEventInit.ToJSON(json)) {
+ aCSPEventListener->OnCSPViolationEvent(json);
+ }
+ }
+
+ // 1. If target is not null, and global is a Window, and target’s
+ // shadow-including root is not global’s associated Document, set target to
+ // null.
+ RefPtr<EventTarget> eventTarget = aTriggeringElement;
+
+ nsCOMPtr<Document> doc = do_QueryReferent(mLoadingContext);
+ if (doc && aTriggeringElement &&
+ aTriggeringElement->GetComposedDoc() != doc) {
+ eventTarget = nullptr;
+ }
+
+ if (!eventTarget) {
+ // If target is a Window, set target to target’s associated Document.
+ eventTarget = doc;
+ }
+
+ if (!eventTarget && mInnerWindowID && XRE_IsParentProcess()) {
+ if (RefPtr<WindowGlobalParent> parent =
+ WindowGlobalParent::GetByInnerWindowId(mInnerWindowID)) {
+ nsAutoString json;
+ if (aViolationEventInit.ToJSON(json)) {
+ Unused << parent->SendDispatchSecurityPolicyViolation(json);
+ }
+ }
+ return NS_OK;
+ }
+
+ if (!eventTarget) {
+ // If we are here, we are probably dealing with workers. Those are handled
+ // via nsICSPEventListener. Nothing to do here.
+ return NS_OK;
+ }
+
+ RefPtr<mozilla::dom::Event> event =
+ mozilla::dom::SecurityPolicyViolationEvent::Constructor(
+ eventTarget, u"securitypolicyviolation"_ns, aViolationEventInit);
+ event->SetTrusted(true);
+
+ ErrorResult rv;
+ eventTarget->DispatchEvent(*event, rv);
+ return rv.StealNSResult();
+}
+
+/**
+ * Dispatched from the main thread to send reports for one CSP violation.
+ */
+class CSPReportSenderRunnable final : public Runnable {
+ public:
+ CSPReportSenderRunnable(
+ Element* aTriggeringElement, nsICSPEventListener* aCSPEventListener,
+ nsIURI* aBlockedURI,
+ nsCSPContext::BlockedContentSource aBlockedContentSource,
+ nsIURI* aOriginalURI, uint32_t aViolatedPolicyIndex, bool aReportOnlyFlag,
+ const nsAString& aViolatedDirective, const nsAString& aEffectiveDirective,
+ const nsAString& aObserverSubject, const nsAString& aSourceFile,
+ bool aReportSample, const nsAString& aScriptSample, uint32_t aLineNum,
+ uint32_t aColumnNum, nsCSPContext* aCSPContext)
+ : mozilla::Runnable("CSPReportSenderRunnable"),
+ mTriggeringElement(aTriggeringElement),
+ mCSPEventListener(aCSPEventListener),
+ mBlockedURI(aBlockedURI),
+ mBlockedContentSource(aBlockedContentSource),
+ mOriginalURI(aOriginalURI),
+ mViolatedPolicyIndex(aViolatedPolicyIndex),
+ mReportOnlyFlag(aReportOnlyFlag),
+ mReportSample(aReportSample),
+ mViolatedDirective(aViolatedDirective),
+ mEffectiveDirective(aEffectiveDirective),
+ mSourceFile(aSourceFile),
+ mScriptSample(aScriptSample),
+ mLineNum(aLineNum),
+ mColumnNum(aColumnNum),
+ mCSPContext(aCSPContext) {
+ NS_ASSERTION(!aViolatedDirective.IsEmpty(),
+ "Can not send reports without a violated directive");
+ // the observer subject is an nsISupports: either an nsISupportsCString
+ // from the arg passed in directly, or if that's empty, it's the blocked
+ // source.
+ if (aObserverSubject.IsEmpty() && mBlockedURI) {
+ mObserverSubject = aBlockedURI;
+ return;
+ }
+
+ nsAutoCString subject;
+ if (aObserverSubject.IsEmpty()) {
+ BlockedContentSourceToString(aBlockedContentSource, subject);
+ } else {
+ CopyUTF16toUTF8(aObserverSubject, subject);
+ }
+
+ nsCOMPtr<nsISupportsCString> supportscstr =
+ do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID);
+ if (supportscstr) {
+ supportscstr->SetData(subject);
+ mObserverSubject = do_QueryInterface(supportscstr);
+ }
+
+ // Truncate sample string.
+ uint32_t length = mScriptSample.Length();
+ if (length > nsCSPContext::ScriptSampleMaxLength()) {
+ uint32_t desiredLength = nsCSPContext::ScriptSampleMaxLength();
+ // Don't cut off right before a low surrogate. Just include it.
+ if (NS_IS_LOW_SURROGATE(mScriptSample[desiredLength])) {
+ desiredLength++;
+ }
+ mScriptSample.Replace(nsCSPContext::ScriptSampleMaxLength(),
+ length - desiredLength,
+ nsContentUtils::GetLocalizedEllipsis());
+ }
+ }
+
+ NS_IMETHOD Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsresult rv;
+
+ // 0) prepare violation data
+ mozilla::dom::SecurityPolicyViolationEventInit init;
+
+ nsAutoCString blockedContentSource;
+ BlockedContentSourceToString(mBlockedContentSource, blockedContentSource);
+
+ rv = mCSPContext->GatherSecurityPolicyViolationEventData(
+ mBlockedURI, blockedContentSource, mOriginalURI, mEffectiveDirective,
+ mViolatedPolicyIndex, mSourceFile,
+ mReportSample ? mScriptSample : EmptyString(), mLineNum, mColumnNum,
+ init);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // 1) notify observers
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (mObserverSubject && observerService) {
+ rv = observerService->NotifyObservers(
+ mObserverSubject, CSP_VIOLATION_TOPIC, mViolatedDirective.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // 2) send reports for the policy that was violated
+ mCSPContext->SendReports(init, mViolatedPolicyIndex);
+
+ // 3) log to console (one per policy violation)
+
+ if (mBlockedURI) {
+ mBlockedURI->GetSpec(blockedContentSource);
+ if (blockedContentSource.Length() >
+ nsCSPContext::ScriptSampleMaxLength()) {
+ bool isData = mBlockedURI->SchemeIs("data");
+ if (NS_SUCCEEDED(rv) && isData &&
+ blockedContentSource.Length() >
+ nsCSPContext::ScriptSampleMaxLength()) {
+ blockedContentSource.Truncate(nsCSPContext::ScriptSampleMaxLength());
+ blockedContentSource.Append(
+ NS_ConvertUTF16toUTF8(nsContentUtils::GetLocalizedEllipsis()));
+ }
+ }
+ }
+
+ if (blockedContentSource.Length() > 0) {
+ nsString blockedContentSource16 =
+ NS_ConvertUTF8toUTF16(blockedContentSource);
+ AutoTArray<nsString, 2> params = {mViolatedDirective,
+ blockedContentSource16};
+ mCSPContext->logToConsole(
+ mReportOnlyFlag ? "CSPROViolationWithURI" : "CSPViolationWithURI",
+ params, mSourceFile, mScriptSample, mLineNum, mColumnNum,
+ nsIScriptError::errorFlag);
+ }
+
+ // 4) fire violation event
+ // A frame-ancestors violation has occurred, but we should not dispatch
+ // the violation event to a potentially cross-origin ancestor.
+ if (!mViolatedDirective.EqualsLiteral("frame-ancestors")) {
+ mCSPContext->FireViolationEvent(mTriggeringElement, mCSPEventListener,
+ init);
+ }
+
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<Element> mTriggeringElement;
+ nsCOMPtr<nsICSPEventListener> mCSPEventListener;
+ nsCOMPtr<nsIURI> mBlockedURI;
+ nsCSPContext::BlockedContentSource mBlockedContentSource;
+ nsCOMPtr<nsIURI> mOriginalURI;
+ uint32_t mViolatedPolicyIndex;
+ bool mReportOnlyFlag;
+ bool mReportSample;
+ nsString mViolatedDirective;
+ nsString mEffectiveDirective;
+ nsCOMPtr<nsISupports> mObserverSubject;
+ nsString mSourceFile;
+ nsString mScriptSample;
+ uint32_t mLineNum;
+ uint32_t mColumnNum;
+ RefPtr<nsCSPContext> mCSPContext;
+};
+
+/**
+ * Asynchronously notifies any nsIObservers listening to the CSP violation
+ * topic that a violation occurred. Also triggers report sending and console
+ * logging. All asynchronous on the main thread.
+ *
+ * @param aTriggeringElement
+ * The element that triggered this report violation. It can be null.
+ * @param aBlockedContentSource
+ * Either a CSP Source (like 'self', as string) or nsIURI: the source
+ * of the violation.
+ * @param aOriginalUri
+ * The original URI if the blocked content is a redirect, else null
+ * @param aViolatedDirective
+ * the directive that was violated (string).
+ * @param aViolatedPolicyIndex
+ * the index of the policy that was violated (so we know where to send
+ * the reports).
+ * @param aObserverSubject
+ * optional, subject sent to the nsIObservers listening to the CSP
+ * violation topic.
+ * @param aSourceFile
+ * name of the file containing the inline script violation
+ * @param aScriptSample
+ * a sample of the violating inline script
+ * @param aLineNum
+ * source line number of the violation (if available)
+ * @param aColumnNum
+ * source column number of the violation (if available)
+ */
+nsresult nsCSPContext::AsyncReportViolation(
+ Element* aTriggeringElement, nsICSPEventListener* aCSPEventListener,
+ nsIURI* aBlockedURI, BlockedContentSource aBlockedContentSource,
+ nsIURI* aOriginalURI, const nsAString& aViolatedDirective,
+ const nsAString& aEffectiveDirective, uint32_t aViolatedPolicyIndex,
+ const nsAString& aObserverSubject, const nsAString& aSourceFile,
+ bool aReportSample, const nsAString& aScriptSample, uint32_t aLineNum,
+ uint32_t aColumnNum) {
+ EnsureIPCPoliciesRead();
+ NS_ENSURE_ARG_MAX(aViolatedPolicyIndex, mPolicies.Length() - 1);
+
+ nsCOMPtr<nsIRunnable> task = new CSPReportSenderRunnable(
+ aTriggeringElement, aCSPEventListener, aBlockedURI, aBlockedContentSource,
+ aOriginalURI, aViolatedPolicyIndex,
+ mPolicies[aViolatedPolicyIndex]->getReportOnlyFlag(), aViolatedDirective,
+ aEffectiveDirective, aObserverSubject, aSourceFile, aReportSample,
+ aScriptSample, aLineNum, aColumnNum, this);
+
+ if (XRE_IsContentProcess()) {
+ if (mEventTarget) {
+ mEventTarget->Dispatch(task.forget(), NS_DISPATCH_NORMAL);
+ return NS_OK;
+ }
+ }
+
+ NS_DispatchToMainThread(task.forget());
+ return NS_OK;
+}
+
+/**
+ * Based on the given loadinfo, determines if this CSP context allows the
+ * ancestry.
+ *
+ * In order to determine the URI of the parent document (one causing the load
+ * of this protected document), this function traverses all Browsing Contexts
+ * until it reaches the top level browsing context.
+ */
+NS_IMETHODIMP
+nsCSPContext::PermitsAncestry(nsILoadInfo* aLoadInfo,
+ bool* outPermitsAncestry) {
+ nsresult rv;
+
+ *outPermitsAncestry = true;
+
+ RefPtr<mozilla::dom::BrowsingContext> ctx;
+ aLoadInfo->GetBrowsingContext(getter_AddRefs(ctx));
+
+ // extract the ancestry as an array
+ nsCOMArray<nsIURI> ancestorsArray;
+ nsCOMPtr<nsIURI> uriClone;
+
+ while (ctx) {
+ nsCOMPtr<nsIPrincipal> currentPrincipal;
+ // Generally permitsAncestry is consulted from within the
+ // DocumentLoadListener in the parent process. For loads of type object
+ // and embed it's called from the Document in the content process.
+ // After Bug 1646899 we should be able to remove that branching code for
+ // querying the currentURI.
+ if (XRE_IsParentProcess()) {
+ WindowGlobalParent* window = ctx->Canonical()->GetCurrentWindowGlobal();
+ if (window) {
+ // Using the URI of the Principal and not the document because e.g.
+ // about:blank inherits the principal and hence the URI of the
+ // document does not reflect the security context of the document.
+ currentPrincipal = window->DocumentPrincipal();
+ }
+ } else if (nsPIDOMWindowOuter* windowOuter = ctx->GetDOMWindow()) {
+ currentPrincipal = nsGlobalWindowOuter::Cast(windowOuter)->GetPrincipal();
+ }
+
+ if (currentPrincipal) {
+ nsCOMPtr<nsIURI> currentURI;
+ auto* currentBasePrincipal = BasePrincipal::Cast(currentPrincipal);
+ currentBasePrincipal->GetURI(getter_AddRefs(currentURI));
+
+ if (currentURI) {
+ nsAutoCString spec;
+ currentURI->GetSpec(spec);
+ // delete the userpass from the URI.
+ rv = NS_MutateURI(currentURI)
+ .SetRef(""_ns)
+ .SetUserPass(""_ns)
+ .Finalize(uriClone);
+
+ // If setUserPass fails for some reason, just return a clone of the
+ // current URI
+ if (NS_FAILED(rv)) {
+ rv = NS_GetURIWithoutRef(currentURI, getter_AddRefs(uriClone));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ ancestorsArray.AppendElement(uriClone);
+ }
+ }
+ ctx = ctx->GetParent();
+ }
+
+ nsAutoString violatedDirective;
+
+ // Now that we've got the ancestry chain in ancestorsArray, time to check
+ // them against any CSP.
+ // NOTE: the ancestors are not allowed to be sent cross origin; this is a
+ // restriction not placed on subresource loads.
+
+ for (uint32_t a = 0; a < ancestorsArray.Length(); a++) {
+ if (CSPCONTEXTLOGENABLED()) {
+ CSPCONTEXTLOG(("nsCSPContext::PermitsAncestry, checking ancestor: %s",
+ ancestorsArray[a]->GetSpecOrDefault().get()));
+ }
+ // omit the ancestor URI in violation reports if cross-origin as per spec
+ // (it is a violation of the same-origin policy).
+ bool okToSendAncestor =
+ NS_SecurityCompareURIs(ancestorsArray[a], mSelfURI, true);
+
+ bool permits =
+ permitsInternal(nsIContentSecurityPolicy::FRAME_ANCESTORS_DIRECTIVE,
+ nullptr, // triggering element
+ nullptr, // nsICSPEventListener
+ nullptr, // nsILoadInfo
+ ancestorsArray[a],
+ nullptr, // no redirect here.
+ true, // specific, do not use default-src
+ true, // send violation reports
+ okToSendAncestor);
+ if (!permits) {
+ *outPermitsAncestry = false;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCSPContext::Permits(Element* aTriggeringElement,
+ nsICSPEventListener* aCSPEventListener, nsIURI* aURI,
+ CSPDirective aDir, bool aSpecific,
+ bool aSendViolationReports, bool* outPermits) {
+ // Can't perform check without aURI
+ if (aURI == nullptr) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aURI->SchemeIs("resource")) {
+ // XXX Ideally we would call SubjectToCSP() here but that would also
+ // allowlist e.g. javascript: URIs which should not be allowlisted here.
+ // As a hotfix we just allowlist pdf.js internals here explicitly.
+ nsAutoCString uriSpec;
+ aURI->GetSpec(uriSpec);
+ if (StringBeginsWith(uriSpec, "resource://pdf.js/"_ns)) {
+ *outPermits = true;
+ return NS_OK;
+ }
+ }
+
+ *outPermits = permitsInternal(aDir, aTriggeringElement, aCSPEventListener,
+ nullptr, // no nsILoadInfo
+ aURI,
+ nullptr, // no original (pre-redirect) URI
+ aSpecific, aSendViolationReports,
+ true); // send blocked URI in violation reports
+
+ if (CSPCONTEXTLOGENABLED()) {
+ CSPCONTEXTLOG(("nsCSPContext::Permits, aUri: %s, aDir: %s, isAllowed: %s",
+ aURI->GetSpecOrDefault().get(),
+ CSP_CSPDirectiveToString(aDir),
+ *outPermits ? "allow" : "deny"));
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCSPContext::ToJSON(nsAString& outCSPinJSON) {
+ outCSPinJSON.Truncate();
+ dom::CSPPolicies jsonPolicies;
+ jsonPolicies.mCsp_policies.Construct();
+ EnsureIPCPoliciesRead();
+
+ for (uint32_t p = 0; p < mPolicies.Length(); p++) {
+ dom::CSP jsonCSP;
+ mPolicies[p]->toDomCSPStruct(jsonCSP);
+ if (!jsonPolicies.mCsp_policies.Value().AppendElement(jsonCSP, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+
+ // convert the gathered information to JSON
+ if (!jsonPolicies.ToJSON(outCSPinJSON)) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCSPContext::GetCSPSandboxFlags(uint32_t* aOutSandboxFlags) {
+ if (!aOutSandboxFlags) {
+ return NS_ERROR_FAILURE;
+ }
+ *aOutSandboxFlags = SANDBOXED_NONE;
+
+ EnsureIPCPoliciesRead();
+ for (uint32_t i = 0; i < mPolicies.Length(); i++) {
+ uint32_t flags = mPolicies[i]->getSandboxFlags();
+
+ // current policy doesn't have sandbox flag, check next policy
+ if (!flags) {
+ continue;
+ }
+
+ // current policy has sandbox flags, if the policy is in enforcement-mode
+ // (i.e. not report-only) set these flags and check for policies with more
+ // restrictions
+ if (!mPolicies[i]->getReportOnlyFlag()) {
+ *aOutSandboxFlags |= flags;
+ } else {
+ // sandbox directive is ignored in report-only mode, warn about it and
+ // continue the loop checking for an enforcement policy.
+ nsAutoString policy;
+ mPolicies[i]->toString(policy);
+
+ CSPCONTEXTLOG(
+ ("nsCSPContext::GetCSPSandboxFlags, report only policy, ignoring "
+ "sandbox in: %s",
+ NS_ConvertUTF16toUTF8(policy).get()));
+
+ AutoTArray<nsString, 1> params = {policy};
+ logToConsole("ignoringReportOnlyDirective", params, u""_ns, u""_ns, 0, 1,
+ nsIScriptError::warningFlag);
+ }
+ }
+
+ return NS_OK;
+}
+
+/* ========== CSPViolationReportListener implementation ========== */
+
+NS_IMPL_ISUPPORTS(CSPViolationReportListener, nsIStreamListener,
+ nsIRequestObserver, nsISupports);
+
+CSPViolationReportListener::CSPViolationReportListener() = default;
+
+CSPViolationReportListener::~CSPViolationReportListener() = default;
+
+nsresult AppendSegmentToString(nsIInputStream* aInputStream, void* aClosure,
+ const char* aRawSegment, uint32_t aToOffset,
+ uint32_t aCount, uint32_t* outWrittenCount) {
+ nsCString* decodedData = static_cast<nsCString*>(aClosure);
+ decodedData->Append(aRawSegment, aCount);
+ *outWrittenCount = aCount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CSPViolationReportListener::OnDataAvailable(nsIRequest* aRequest,
+ nsIInputStream* aInputStream,
+ uint64_t aOffset, uint32_t aCount) {
+ uint32_t read;
+ nsCString decodedData;
+ return aInputStream->ReadSegments(AppendSegmentToString, &decodedData, aCount,
+ &read);
+}
+
+NS_IMETHODIMP
+CSPViolationReportListener::OnStopRequest(nsIRequest* aRequest,
+ nsresult aStatus) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CSPViolationReportListener::OnStartRequest(nsIRequest* aRequest) {
+ return NS_OK;
+}
+
+/* ========== CSPReportRedirectSink implementation ========== */
+
+NS_IMPL_ISUPPORTS(CSPReportRedirectSink, nsIChannelEventSink,
+ nsIInterfaceRequestor);
+
+CSPReportRedirectSink::CSPReportRedirectSink() = default;
+
+CSPReportRedirectSink::~CSPReportRedirectSink() = default;
+
+NS_IMETHODIMP
+CSPReportRedirectSink::AsyncOnChannelRedirect(
+ nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aRedirFlags,
+ nsIAsyncVerifyRedirectCallback* aCallback) {
+ if (aRedirFlags & nsIChannelEventSink::REDIRECT_INTERNAL) {
+ aCallback->OnRedirectVerifyCallback(NS_OK);
+ return NS_OK;
+ }
+
+ // cancel the old channel so XHR failure callback happens
+ nsresult rv = aOldChannel->Cancel(NS_ERROR_ABORT);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // notify an observer that we have blocked the report POST due to a
+ // redirect, used in testing, do this async since we're in an async call now
+ // to begin with
+ nsCOMPtr<nsIURI> uri;
+ rv = aOldChannel->GetURI(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ NS_ASSERTION(observerService,
+ "Observer service required to log CSP violations");
+ observerService->NotifyObservers(
+ uri, CSP_VIOLATION_TOPIC,
+ u"denied redirect while sending violation report");
+
+ return NS_BINDING_REDIRECTED;
+}
+
+NS_IMETHODIMP
+CSPReportRedirectSink::GetInterface(const nsIID& aIID, void** aResult) {
+ if (aIID.Equals(NS_GET_IID(nsINetworkInterceptController)) &&
+ mInterceptController) {
+ nsCOMPtr<nsINetworkInterceptController> copy(mInterceptController);
+ *aResult = copy.forget().take();
+
+ return NS_OK;
+ }
+
+ return QueryInterface(aIID, aResult);
+}
+
+void CSPReportRedirectSink::SetInterceptController(
+ nsINetworkInterceptController* aInterceptController) {
+ mInterceptController = aInterceptController;
+}
+
+/* ===== nsISerializable implementation ====== */
+
+NS_IMETHODIMP
+nsCSPContext::Read(nsIObjectInputStream* aStream) {
+ nsresult rv;
+ nsCOMPtr<nsISupports> supports;
+
+ rv = NS_ReadOptionalObject(aStream, true, getter_AddRefs(supports));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mSelfURI = do_QueryInterface(supports);
+ MOZ_ASSERT(mSelfURI, "need a self URI to de-serialize");
+
+ nsAutoCString JSON;
+ rv = aStream->ReadCString(JSON);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrincipal> principal = BasePrincipal::FromJSON(JSON);
+ mLoadingPrincipal = principal;
+ MOZ_ASSERT(mLoadingPrincipal, "need a loadingPrincipal to de-serialize");
+
+ uint32_t numPolicies;
+ rv = aStream->Read32(&numPolicies);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString policyString;
+
+ while (numPolicies > 0) {
+ numPolicies--;
+
+ rv = aStream->ReadString(policyString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool reportOnly = false;
+ rv = aStream->ReadBoolean(&reportOnly);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool deliveredViaMetaTag = false;
+ rv = aStream->ReadBoolean(&deliveredViaMetaTag);
+ NS_ENSURE_SUCCESS(rv, rv);
+ AddIPCPolicy(mozilla::ipc::ContentSecurityPolicy(policyString, reportOnly,
+ deliveredViaMetaTag));
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCSPContext::Write(nsIObjectOutputStream* aStream) {
+ nsresult rv = NS_WriteOptionalCompoundObject(aStream, mSelfURI,
+ NS_GET_IID(nsIURI), true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString JSON;
+ BasePrincipal::Cast(mLoadingPrincipal)->ToJSON(JSON);
+ rv = aStream->WriteStringZ(JSON.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Serialize all the policies.
+ aStream->Write32(mPolicies.Length() + mIPCPolicies.Length());
+
+ nsAutoString polStr;
+ for (uint32_t p = 0; p < mPolicies.Length(); p++) {
+ polStr.Truncate();
+ mPolicies[p]->toString(polStr);
+ aStream->WriteWStringZ(polStr.get());
+ aStream->WriteBoolean(mPolicies[p]->getReportOnlyFlag());
+ aStream->WriteBoolean(mPolicies[p]->getDeliveredViaMetaTagFlag());
+ }
+ for (auto& policy : mIPCPolicies) {
+ aStream->WriteWStringZ(policy.policy().get());
+ aStream->WriteBoolean(policy.reportOnlyFlag());
+ aStream->WriteBoolean(policy.deliveredViaMetaTagFlag());
+ }
+ return NS_OK;
+}
+
+void nsCSPContext::AddIPCPolicy(const ContentSecurityPolicy& aPolicy) {
+ mIPCPolicies.AppendElement(aPolicy);
+}
+
+void nsCSPContext::SerializePolicies(
+ nsTArray<ContentSecurityPolicy>& aPolicies) {
+ for (auto* policy : mPolicies) {
+ nsAutoString policyString;
+ policy->toString(policyString);
+ aPolicies.AppendElement(
+ ContentSecurityPolicy(policyString, policy->getReportOnlyFlag(),
+ policy->getDeliveredViaMetaTagFlag()));
+ }
+
+ aPolicies.AppendElements(mIPCPolicies);
+}
diff --git a/dom/security/nsCSPContext.h b/dom/security/nsCSPContext.h
new file mode 100644
index 0000000000..0c1438e573
--- /dev/null
+++ b/dom/security/nsCSPContext.h
@@ -0,0 +1,240 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsCSPContext_h___
+#define nsCSPContext_h___
+
+#include "mozilla/dom/nsCSPUtils.h"
+#include "mozilla/dom/SecurityPolicyViolationEvent.h"
+#include "mozilla/StaticPrefs_security.h"
+#include "nsIChannel.h"
+#include "nsIChannelEventSink.h"
+#include "nsIContentSecurityPolicy.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIStreamListener.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsXPCOM.h"
+
+#define NS_CSPCONTEXT_CONTRACTID "@mozilla.org/cspcontext;1"
+// 09d9ed1a-e5d4-4004-bfe0-27ceb923d9ac
+#define NS_CSPCONTEXT_CID \
+ { \
+ 0x09d9ed1a, 0xe5d4, 0x4004, { \
+ 0xbf, 0xe0, 0x27, 0xce, 0xb9, 0x23, 0xd9, 0xac \
+ } \
+ }
+
+class nsINetworkInterceptController;
+class nsIEventTarget;
+struct ConsoleMsgQueueElem;
+
+namespace mozilla {
+namespace dom {
+class Element;
+}
+namespace ipc {
+class ContentSecurityPolicy;
+}
+} // namespace mozilla
+
+class nsCSPContext : public nsIContentSecurityPolicy {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICONTENTSECURITYPOLICY
+ NS_DECL_NSISERIALIZABLE
+
+ protected:
+ virtual ~nsCSPContext();
+
+ public:
+ nsCSPContext();
+
+ static bool Equals(nsIContentSecurityPolicy* aCSP,
+ nsIContentSecurityPolicy* aOtherCSP);
+
+ // Init a CSP from a different CSP
+ nsresult InitFromOther(nsCSPContext* otherContext);
+
+ // Used to suppress errors and warnings produced by the parser.
+ // Use this when doing an one-off parsing of the CSP.
+ void SuppressParserLogMessages() { mSuppressParserLogMessages = true; }
+
+ /**
+ * SetRequestContextWithDocument() needs to be called before the
+ * innerWindowID is initialized on the document. Use this function
+ * to call back to flush queued up console messages and initialize
+ * the innerWindowID. Node, If SetRequestContextWithPrincipal() was
+ * called then we do not have a innerWindowID anyway and hence
+ * we can not flush messages to the correct console.
+ */
+ void flushConsoleMessages();
+
+ void logToConsole(const char* aName, const nsTArray<nsString>& aParams,
+ const nsAString& aSourceName, const nsAString& aSourceLine,
+ uint32_t aLineNumber, uint32_t aColumnNumber,
+ uint32_t aSeverityFlag);
+
+ /**
+ * Construct SecurityPolicyViolationEventInit structure.
+ *
+ * @param aBlockedURI
+ * A nsIURI: the source of the violation.
+ * @param aOriginalUri
+ * The original URI if the blocked content is a redirect, else null
+ * @param aViolatedDirective
+ * the directive that was violated (string).
+ * @param aSourceFile
+ * name of the file containing the inline script violation
+ * @param aScriptSample
+ * a sample of the violating inline script
+ * @param aLineNum
+ * source line number of the violation (if available)
+ * @param aColumnNum
+ * source column number of the violation (if available)
+ * @param aViolationEventInit
+ * The output
+ */
+ nsresult GatherSecurityPolicyViolationEventData(
+ nsIURI* aBlockedURI, const nsACString& aBlockedString,
+ nsIURI* aOriginalURI, const nsAString& aViolatedDirective,
+ uint32_t aViolatedPolicyIndex, const nsAString& aSourceFile,
+ const nsAString& aScriptSample, uint32_t aLineNum, uint32_t aColumnNum,
+ mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit);
+
+ nsresult SendReports(
+ const mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit,
+ uint32_t aViolatedPolicyIndex);
+
+ nsresult FireViolationEvent(
+ mozilla::dom::Element* aTriggeringElement,
+ nsICSPEventListener* aCSPEventListener,
+ const mozilla::dom::SecurityPolicyViolationEventInit&
+ aViolationEventInit);
+
+ enum BlockedContentSource {
+ eUnknown,
+ eInline,
+ eEval,
+ eSelf,
+ eWasmEval,
+ };
+
+ nsresult AsyncReportViolation(
+ mozilla::dom::Element* aTriggeringElement,
+ nsICSPEventListener* aCSPEventListener, nsIURI* aBlockedURI,
+ BlockedContentSource aBlockedContentSource, nsIURI* aOriginalURI,
+ const nsAString& aViolatedDirective, const nsAString& aEffectiveDirective,
+ uint32_t aViolatedPolicyIndex, const nsAString& aObserverSubject,
+ const nsAString& aSourceFile, bool aReportSample,
+ const nsAString& aScriptSample, uint32_t aLineNum, uint32_t aColumnNum);
+
+ // Hands off! Don't call this method unless you know what you
+ // are doing. It's only supposed to be called from within
+ // the principal destructor to avoid a tangling pointer.
+ void clearLoadingPrincipal() { mLoadingPrincipal = nullptr; }
+
+ nsWeakPtr GetLoadingContext() { return mLoadingContext; }
+
+ static uint32_t ScriptSampleMaxLength() {
+ return std::max(
+ mozilla::StaticPrefs::security_csp_reporting_script_sample_max_length(),
+ 0);
+ }
+
+ void AddIPCPolicy(const mozilla::ipc::ContentSecurityPolicy& aPolicy);
+ void SerializePolicies(
+ nsTArray<mozilla::ipc::ContentSecurityPolicy>& aPolicies);
+
+ private:
+ bool ShouldThrottleReport(
+ const mozilla::dom::SecurityPolicyViolationEventInit&
+ aViolationEventInit);
+
+ bool permitsInternal(CSPDirective aDir,
+ mozilla::dom::Element* aTriggeringElement,
+ nsICSPEventListener* aCSPEventListener,
+ nsILoadInfo* aLoadInfo, nsIURI* aContentLocation,
+ nsIURI* aOriginalURIIfRedirect, bool aSpecific,
+ bool aSendViolationReports,
+ bool aSendContentLocationInViolationReports);
+
+ // helper to report inline script/style violations
+ void reportInlineViolation(CSPDirective aDirective,
+ mozilla::dom::Element* aTriggeringElement,
+ nsICSPEventListener* aCSPEventListener,
+ const nsAString& aNonce, bool aReportSample,
+ const nsAString& aSample,
+ const nsAString& aViolatedDirective,
+ const nsAString& aEffectiveDirective,
+ uint32_t aViolatedPolicyIndex,
+ uint32_t aLineNumber, uint32_t aColumnNumber);
+
+ nsString mReferrer;
+ uint64_t mInnerWindowID; // used for web console logging
+ bool mSkipAllowInlineStyleCheck; // used to allow Devtools to edit styles
+ // When deserializing an nsCSPContext instance, we initially just keep the
+ // policies unparsed. We will only reconstruct actual CSP policy instances
+ // when there's an attempt to use the CSP. Given a better way to serialize/
+ // deserialize individual nsCSPPolicy objects, this performance
+ // optimization could go away.
+ nsTArray<mozilla::ipc::ContentSecurityPolicy> mIPCPolicies;
+ nsTArray<nsCSPPolicy*> mPolicies;
+ nsCOMPtr<nsIURI> mSelfURI;
+ nsCOMPtr<nsILoadGroup> mCallingChannelLoadGroup;
+ nsWeakPtr mLoadingContext;
+ nsCOMPtr<nsIPrincipal> mLoadingPrincipal;
+
+ bool mSuppressParserLogMessages = false;
+
+ // helper members used to queue up web console messages till
+ // the windowID becomes available. see flushConsoleMessages()
+ nsTArray<ConsoleMsgQueueElem> mConsoleMsgQueue;
+ bool mQueueUpMessages;
+ nsCOMPtr<nsIEventTarget> mEventTarget;
+
+ mozilla::TimeStamp mSendReportLimitSpanStart;
+ uint32_t mSendReportLimitCount = 1;
+ bool mWarnedAboutTooManyReports = false;
+};
+
+// Class that listens to violation report transmission and logs errors.
+class CSPViolationReportListener : public nsIStreamListener {
+ public:
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_ISUPPORTS
+
+ public:
+ CSPViolationReportListener();
+
+ protected:
+ virtual ~CSPViolationReportListener();
+};
+
+// The POST of the violation report (if it happens) should not follow
+// redirects, per the spec. hence, we implement an nsIChannelEventSink
+// with an object so we can tell XHR to abort if a redirect happens.
+class CSPReportRedirectSink final : public nsIChannelEventSink,
+ public nsIInterfaceRequestor {
+ public:
+ NS_DECL_NSICHANNELEVENTSINK
+ NS_DECL_NSIINTERFACEREQUESTOR
+ NS_DECL_ISUPPORTS
+
+ public:
+ CSPReportRedirectSink();
+
+ void SetInterceptController(
+ nsINetworkInterceptController* aInterceptController);
+
+ protected:
+ virtual ~CSPReportRedirectSink();
+
+ private:
+ nsCOMPtr<nsINetworkInterceptController> mInterceptController;
+};
+
+#endif /* nsCSPContext_h___ */
diff --git a/dom/security/nsCSPParser.cpp b/dom/security/nsCSPParser.cpp
new file mode 100644
index 0000000000..2559367831
--- /dev/null
+++ b/dom/security/nsCSPParser.cpp
@@ -0,0 +1,1239 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/TextUtils.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_security.h"
+#include "nsCOMPtr.h"
+#include "nsContentUtils.h"
+#include "nsCSPParser.h"
+#include "nsCSPUtils.h"
+#include "nsIScriptError.h"
+#include "nsNetUtil.h"
+#include "nsReadableUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsUnicharUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+static LogModule* GetCspParserLog() {
+ static LazyLogModule gCspParserPRLog("CSPParser");
+ return gCspParserPRLog;
+}
+
+#define CSPPARSERLOG(args) \
+ MOZ_LOG(GetCspParserLog(), mozilla::LogLevel::Debug, args)
+#define CSPPARSERLOGENABLED() \
+ MOZ_LOG_TEST(GetCspParserLog(), mozilla::LogLevel::Debug)
+
+static const uint32_t kSubHostPathCharacterCutoff = 512;
+
+static const char* const kHashSourceValidFns[] = {"sha256", "sha384", "sha512"};
+static const uint32_t kHashSourceValidFnsLen = 3;
+
+/* ===== nsCSPParser ==================== */
+
+nsCSPParser::nsCSPParser(policyTokens& aTokens, nsIURI* aSelfURI,
+ nsCSPContext* aCSPContext, bool aDeliveredViaMetaTag,
+ bool aSuppressLogMessages)
+ : mCurChar(nullptr),
+ mEndChar(nullptr),
+ mHasHashOrNonce(false),
+ mHasAnyUnsafeEval(false),
+ mStrictDynamic(false),
+ mUnsafeInlineKeywordSrc(nullptr),
+ mChildSrc(nullptr),
+ mFrameSrc(nullptr),
+ mWorkerSrc(nullptr),
+ mScriptSrc(nullptr),
+ mStyleSrc(nullptr),
+ mParsingFrameAncestorsDir(false),
+ mTokens(aTokens.Clone()),
+ mSelfURI(aSelfURI),
+ mPolicy(nullptr),
+ mCSPContext(aCSPContext),
+ mDeliveredViaMetaTag(aDeliveredViaMetaTag),
+ mSuppressLogMessages(aSuppressLogMessages) {
+ CSPPARSERLOG(("nsCSPParser::nsCSPParser"));
+}
+
+nsCSPParser::~nsCSPParser() { CSPPARSERLOG(("nsCSPParser::~nsCSPParser")); }
+
+static bool isCharacterToken(char16_t aSymbol) {
+ return (aSymbol >= 'a' && aSymbol <= 'z') ||
+ (aSymbol >= 'A' && aSymbol <= 'Z');
+}
+
+bool isNumberToken(char16_t aSymbol) {
+ return (aSymbol >= '0' && aSymbol <= '9');
+}
+
+bool isValidHexDig(char16_t aHexDig) {
+ return (isNumberToken(aHexDig) || (aHexDig >= 'A' && aHexDig <= 'F') ||
+ (aHexDig >= 'a' && aHexDig <= 'f'));
+}
+
+static bool isValidBase64Value(const char16_t* cur, const char16_t* end) {
+ // Using grammar at
+ // https://w3c.github.io/webappsec-csp/#grammardef-nonce-source
+
+ // May end with one or two =
+ if (end > cur && *(end - 1) == EQUALS) end--;
+ if (end > cur && *(end - 1) == EQUALS) end--;
+
+ // Must have at least one character aside from any =
+ if (end == cur) {
+ return false;
+ }
+
+ // Rest must all be A-Za-z0-9+/-_
+ for (; cur < end; ++cur) {
+ if (!(isCharacterToken(*cur) || isNumberToken(*cur) || *cur == PLUS ||
+ *cur == SLASH || *cur == DASH || *cur == UNDERLINE)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void nsCSPParser::resetCurChar(const nsAString& aToken) {
+ mCurChar = aToken.BeginReading();
+ mEndChar = aToken.EndReading();
+ resetCurValue();
+}
+
+// The path is terminated by the first question mark ("?") or
+// number sign ("#") character, or by the end of the URI.
+// http://tools.ietf.org/html/rfc3986#section-3.3
+bool nsCSPParser::atEndOfPath() {
+ return (atEnd() || peek(QUESTIONMARK) || peek(NUMBER_SIGN));
+}
+
+// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
+bool nsCSPParser::atValidUnreservedChar() {
+ return (peek(isCharacterToken) || peek(isNumberToken) || peek(DASH) ||
+ peek(DOT) || peek(UNDERLINE) || peek(TILDE));
+}
+
+// sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
+// / "*" / "+" / "," / ";" / "="
+// Please note that even though ',' and ';' appear to be
+// valid sub-delims according to the RFC production of paths,
+// both can not appear here by itself, they would need to be
+// pct-encoded in order to be part of the path.
+bool nsCSPParser::atValidSubDelimChar() {
+ return (peek(EXCLAMATION) || peek(DOLLAR) || peek(AMPERSAND) ||
+ peek(SINGLEQUOTE) || peek(OPENBRACE) || peek(CLOSINGBRACE) ||
+ peek(WILDCARD) || peek(PLUS) || peek(EQUALS));
+}
+
+// pct-encoded = "%" HEXDIG HEXDIG
+bool nsCSPParser::atValidPctEncodedChar() {
+ const char16_t* pctCurChar = mCurChar;
+
+ if ((pctCurChar + 2) >= mEndChar) {
+ // string too short, can't be a valid pct-encoded char.
+ return false;
+ }
+
+ // Any valid pct-encoding must follow the following format:
+ // "% HEXDIG HEXDIG"
+ if (PERCENT_SIGN != *pctCurChar || !isValidHexDig(*(pctCurChar + 1)) ||
+ !isValidHexDig(*(pctCurChar + 2))) {
+ return false;
+ }
+ return true;
+}
+
+// pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
+// http://tools.ietf.org/html/rfc3986#section-3.3
+bool nsCSPParser::atValidPathChar() {
+ return (atValidUnreservedChar() || atValidSubDelimChar() ||
+ atValidPctEncodedChar() || peek(COLON) || peek(ATSYMBOL));
+}
+
+void nsCSPParser::logWarningErrorToConsole(uint32_t aSeverityFlag,
+ const char* aProperty,
+ const nsTArray<nsString>& aParams) {
+ CSPPARSERLOG(("nsCSPParser::logWarningErrorToConsole: %s", aProperty));
+
+ if (mSuppressLogMessages) {
+ return;
+ }
+
+ // send console messages off to the context and let the context
+ // deal with it (potentially messages need to be queued up)
+ mCSPContext->logToConsole(aProperty, aParams,
+ u""_ns, // aSourceName
+ u""_ns, // aSourceLine
+ 0, // aLineNumber
+ 1, // aColumnNumber
+ aSeverityFlag); // aFlags
+}
+
+bool nsCSPParser::hostChar() {
+ if (atEnd()) {
+ return false;
+ }
+ return accept(isCharacterToken) || accept(isNumberToken) || accept(DASH);
+}
+
+// (ALPHA / DIGIT / "+" / "-" / "." )
+bool nsCSPParser::schemeChar() {
+ if (atEnd()) {
+ return false;
+ }
+ return accept(isCharacterToken) || accept(isNumberToken) || accept(PLUS) ||
+ accept(DASH) || accept(DOT);
+}
+
+// port = ":" ( 1*DIGIT / "*" )
+bool nsCSPParser::port() {
+ CSPPARSERLOG(("nsCSPParser::port, mCurToken: %s, mCurValue: %s",
+ NS_ConvertUTF16toUTF8(mCurToken).get(),
+ NS_ConvertUTF16toUTF8(mCurValue).get()));
+
+ // Consume the COLON we just peeked at in houstSource
+ accept(COLON);
+
+ // Resetting current value since we start to parse a port now.
+ // e.g; "http://www.example.com:8888" then we have already parsed
+ // everything up to (including) ":";
+ resetCurValue();
+
+ // Port might be "*"
+ if (accept(WILDCARD)) {
+ return true;
+ }
+
+ // Port must start with a number
+ if (!accept(isNumberToken)) {
+ AutoTArray<nsString, 1> params = {mCurToken};
+ logWarningErrorToConsole(nsIScriptError::warningFlag, "couldntParsePort",
+ params);
+ return false;
+ }
+ // Consume more numbers and set parsed port to the nsCSPHost
+ while (accept(isNumberToken)) { /* consume */
+ }
+ return true;
+}
+
+bool nsCSPParser::subPath(nsCSPHostSrc* aCspHost) {
+ CSPPARSERLOG(("nsCSPParser::subPath, mCurToken: %s, mCurValue: %s",
+ NS_ConvertUTF16toUTF8(mCurToken).get(),
+ NS_ConvertUTF16toUTF8(mCurValue).get()));
+
+ // Emergency exit to avoid endless loops in case a path in a CSP policy
+ // is longer than 512 characters, or also to avoid endless loops
+ // in case we are parsing unrecognized characters in the following loop.
+ uint32_t charCounter = 0;
+ nsString pctDecodedSubPath;
+
+ while (!atEndOfPath()) {
+ if (peek(SLASH)) {
+ // before appendig any additional portion of a subpath we have to
+ // pct-decode that portion of the subpath. atValidPathChar() already
+ // verified a correct pct-encoding, now we can safely decode and append
+ // the decoded-sub path.
+ CSP_PercentDecodeStr(mCurValue, pctDecodedSubPath);
+ aCspHost->appendPath(pctDecodedSubPath);
+ // Resetting current value since we are appending parts of the path
+ // to aCspHost, e.g; "http://www.example.com/path1/path2" then the
+ // first part is "/path1", second part "/path2"
+ resetCurValue();
+ } else if (!atValidPathChar()) {
+ AutoTArray<nsString, 1> params = {mCurToken};
+ logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "couldntParseInvalidSource", params);
+ return false;
+ }
+ // potentially we have encountred a valid pct-encoded character in
+ // atValidPathChar(); if so, we have to account for "% HEXDIG HEXDIG" and
+ // advance the pointer past the pct-encoded char.
+ if (peek(PERCENT_SIGN)) {
+ advance();
+ advance();
+ }
+ advance();
+ if (++charCounter > kSubHostPathCharacterCutoff) {
+ return false;
+ }
+ }
+ // before appendig any additional portion of a subpath we have to pct-decode
+ // that portion of the subpath. atValidPathChar() already verified a correct
+ // pct-encoding, now we can safely decode and append the decoded-sub path.
+ CSP_PercentDecodeStr(mCurValue, pctDecodedSubPath);
+ aCspHost->appendPath(pctDecodedSubPath);
+ resetCurValue();
+ return true;
+}
+
+bool nsCSPParser::path(nsCSPHostSrc* aCspHost) {
+ CSPPARSERLOG(("nsCSPParser::path, mCurToken: %s, mCurValue: %s",
+ NS_ConvertUTF16toUTF8(mCurToken).get(),
+ NS_ConvertUTF16toUTF8(mCurValue).get()));
+
+ // Resetting current value and forgetting everything we have parsed so far
+ // e.g. parsing "http://www.example.com/path1/path2", then
+ // "http://www.example.com" has already been parsed so far
+ // forget about it.
+ resetCurValue();
+
+ if (!accept(SLASH)) {
+ AutoTArray<nsString, 1> params = {mCurToken};
+ logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "couldntParseInvalidSource", params);
+ return false;
+ }
+ if (atEndOfPath()) {
+ // one slash right after host [port] is also considered a path, e.g.
+ // www.example.com/ should result in www.example.com/
+ // please note that we do not have to perform any pct-decoding here
+ // because we are just appending a '/' and not any actual chars.
+ aCspHost->appendPath(u"/"_ns);
+ return true;
+ }
+ // path can begin with "/" but not "//"
+ // see http://tools.ietf.org/html/rfc3986#section-3.3
+ if (peek(SLASH)) {
+ AutoTArray<nsString, 1> params = {mCurToken};
+ logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "couldntParseInvalidSource", params);
+ return false;
+ }
+ return subPath(aCspHost);
+}
+
+bool nsCSPParser::subHost() {
+ CSPPARSERLOG(("nsCSPParser::subHost, mCurToken: %s, mCurValue: %s",
+ NS_ConvertUTF16toUTF8(mCurToken).get(),
+ NS_ConvertUTF16toUTF8(mCurValue).get()));
+
+ // Emergency exit to avoid endless loops in case a host in a CSP policy
+ // is longer than 512 characters, or also to avoid endless loops
+ // in case we are parsing unrecognized characters in the following loop.
+ uint32_t charCounter = 0;
+
+ while (!atEndOfPath() && !peek(COLON) && !peek(SLASH)) {
+ ++charCounter;
+ while (hostChar()) {
+ /* consume */
+ ++charCounter;
+ }
+ if (accept(DOT) && !hostChar()) {
+ return false;
+ }
+ if (charCounter > kSubHostPathCharacterCutoff) {
+ return false;
+ }
+ }
+ return true;
+}
+
+// host = "*" / [ "*." ] 1*host-char *( "." 1*host-char )
+nsCSPHostSrc* nsCSPParser::host() {
+ CSPPARSERLOG(("nsCSPParser::host, mCurToken: %s, mCurValue: %s",
+ NS_ConvertUTF16toUTF8(mCurToken).get(),
+ NS_ConvertUTF16toUTF8(mCurValue).get()));
+
+ // Check if the token starts with "*"; please remember that we handle
+ // a single "*" as host in sourceExpression, but we still have to handle
+ // the case where a scheme was defined, e.g., as:
+ // "https://*", "*.example.com", "*:*", etc.
+ if (accept(WILDCARD)) {
+ // Might solely be the wildcard
+ if (atEnd() || peek(COLON)) {
+ return new nsCSPHostSrc(mCurValue);
+ }
+ // If the token is not only the "*", a "." must follow right after
+ if (!accept(DOT)) {
+ AutoTArray<nsString, 1> params = {mCurToken};
+ logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "couldntParseInvalidHost", params);
+ return nullptr;
+ }
+ }
+
+ // Expecting at least one host-char
+ if (!hostChar()) {
+ AutoTArray<nsString, 1> params = {mCurToken};
+ logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "couldntParseInvalidHost", params);
+ return nullptr;
+ }
+
+ // There might be several sub hosts defined.
+ if (!subHost()) {
+ AutoTArray<nsString, 1> params = {mCurToken};
+ logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "couldntParseInvalidHost", params);
+ return nullptr;
+ }
+
+ // HostName might match a keyword, log to the console.
+ if (CSP_IsQuotelessKeyword(mCurValue)) {
+ nsString keyword = mCurValue;
+ ToLowerCase(keyword);
+ AutoTArray<nsString, 2> params = {mCurToken, keyword};
+ logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "hostNameMightBeKeyword", params);
+ }
+
+ // Create a new nsCSPHostSrc with the parsed host.
+ return new nsCSPHostSrc(mCurValue);
+}
+
+// keyword-source = "'self'" / "'unsafe-inline'" / "'unsafe-eval'" /
+// "'wasm-unsafe-eval'"
+nsCSPBaseSrc* nsCSPParser::keywordSource() {
+ CSPPARSERLOG(("nsCSPParser::keywordSource, mCurToken: %s, mCurValue: %s",
+ NS_ConvertUTF16toUTF8(mCurToken).get(),
+ NS_ConvertUTF16toUTF8(mCurValue).get()));
+
+ // Special case handling for 'self' which is not stored internally as a
+ // keyword, but rather creates a nsCSPHostSrc using the selfURI
+ if (CSP_IsKeyword(mCurToken, CSP_SELF)) {
+ return CSP_CreateHostSrcFromSelfURI(mSelfURI);
+ }
+
+ if (CSP_IsKeyword(mCurToken, CSP_REPORT_SAMPLE)) {
+ return new nsCSPKeywordSrc(CSP_UTF16KeywordToEnum(mCurToken));
+ }
+
+ if (CSP_IsKeyword(mCurToken, CSP_STRICT_DYNAMIC)) {
+ if (!CSP_IsDirective(mCurDir[0],
+ nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE) &&
+ !CSP_IsDirective(mCurDir[0],
+ nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE) &&
+ !CSP_IsDirective(mCurDir[0],
+ nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE) &&
+ !CSP_IsDirective(mCurDir[0],
+ nsIContentSecurityPolicy::DEFAULT_SRC_DIRECTIVE)) {
+ AutoTArray<nsString, 1> params = {u"strict-dynamic"_ns};
+ logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "ignoringStrictDynamic", params);
+ }
+
+ mStrictDynamic = true;
+ return new nsCSPKeywordSrc(CSP_UTF16KeywordToEnum(mCurToken));
+ }
+
+ if (CSP_IsKeyword(mCurToken, CSP_UNSAFE_INLINE)) {
+ // make sure script-src only contains 'unsafe-inline' once;
+ // ignore duplicates and log warning
+ if (mUnsafeInlineKeywordSrc) {
+ AutoTArray<nsString, 1> params = {mCurToken};
+ logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "ignoringDuplicateSrc", params);
+ return nullptr;
+ }
+ // cache if we encounter 'unsafe-inline' so we can invalidate (ignore) it in
+ // case that script-src directive also contains hash- or nonce-.
+ mUnsafeInlineKeywordSrc =
+ new nsCSPKeywordSrc(CSP_UTF16KeywordToEnum(mCurToken));
+ return mUnsafeInlineKeywordSrc;
+ }
+
+ if (CSP_IsKeyword(mCurToken, CSP_UNSAFE_EVAL)) {
+ mHasAnyUnsafeEval = true;
+ return new nsCSPKeywordSrc(CSP_UTF16KeywordToEnum(mCurToken));
+ }
+
+ if (CSP_IsKeyword(mCurToken, CSP_WASM_UNSAFE_EVAL)) {
+ mHasAnyUnsafeEval = true;
+ return new nsCSPKeywordSrc(CSP_UTF16KeywordToEnum(mCurToken));
+ }
+
+ if (CSP_IsKeyword(mCurToken, CSP_UNSAFE_HASHES)) {
+ return new nsCSPKeywordSrc(CSP_UTF16KeywordToEnum(mCurToken));
+ }
+
+ return nullptr;
+}
+
+// host-source = [ scheme "://" ] host [ port ] [ path ]
+nsCSPHostSrc* nsCSPParser::hostSource() {
+ CSPPARSERLOG(("nsCSPParser::hostSource, mCurToken: %s, mCurValue: %s",
+ NS_ConvertUTF16toUTF8(mCurToken).get(),
+ NS_ConvertUTF16toUTF8(mCurValue).get()));
+
+ nsCSPHostSrc* cspHost = host();
+ if (!cspHost) {
+ // Error was reported in host()
+ return nullptr;
+ }
+
+ // Calling port() to see if there is a port to parse, if an error
+ // occurs, port() reports the error, if port() returns true;
+ // we have a valid port, so we add it to cspHost.
+ if (peek(COLON)) {
+ if (!port()) {
+ delete cspHost;
+ return nullptr;
+ }
+ cspHost->setPort(mCurValue);
+ }
+
+ if (atEndOfPath()) {
+ return cspHost;
+ }
+
+ // Calling path() to see if there is a path to parse, if an error
+ // occurs, path() reports the error; handing cspHost as an argument
+ // which simplifies parsing of several paths.
+ if (!path(cspHost)) {
+ // If the host [port] is followed by a path, it has to be a valid path,
+ // otherwise we pass the nullptr, indicating an error, up the callstack.
+ // see also http://www.w3.org/TR/CSP11/#source-list
+ delete cspHost;
+ return nullptr;
+ }
+ return cspHost;
+}
+
+// scheme-source = scheme ":"
+nsCSPSchemeSrc* nsCSPParser::schemeSource() {
+ CSPPARSERLOG(("nsCSPParser::schemeSource, mCurToken: %s, mCurValue: %s",
+ NS_ConvertUTF16toUTF8(mCurToken).get(),
+ NS_ConvertUTF16toUTF8(mCurValue).get()));
+
+ if (!accept(isCharacterToken)) {
+ return nullptr;
+ }
+ while (schemeChar()) { /* consume */
+ }
+ nsString scheme = mCurValue;
+
+ // If the potential scheme is not followed by ":" - it's not a scheme
+ if (!accept(COLON)) {
+ return nullptr;
+ }
+
+ // If the chraracter following the ":" is a number or the "*"
+ // then we are not parsing a scheme; but rather a host;
+ if (peek(isNumberToken) || peek(WILDCARD)) {
+ return nullptr;
+ }
+
+ return new nsCSPSchemeSrc(scheme);
+}
+
+// nonce-source = "'nonce-" nonce-value "'"
+nsCSPNonceSrc* nsCSPParser::nonceSource() {
+ CSPPARSERLOG(("nsCSPParser::nonceSource, mCurToken: %s, mCurValue: %s",
+ NS_ConvertUTF16toUTF8(mCurToken).get(),
+ NS_ConvertUTF16toUTF8(mCurValue).get()));
+
+ // Check if mCurToken begins with "'nonce-" and ends with "'"
+ if (!StringBeginsWith(mCurToken,
+ nsDependentString(CSP_EnumToUTF16Keyword(CSP_NONCE)),
+ nsASCIICaseInsensitiveStringComparator) ||
+ mCurToken.Last() != SINGLEQUOTE) {
+ return nullptr;
+ }
+
+ // Trim surrounding single quotes
+ const nsAString& expr = Substring(mCurToken, 1, mCurToken.Length() - 2);
+
+ int32_t dashIndex = expr.FindChar(DASH);
+ if (dashIndex < 0) {
+ return nullptr;
+ }
+ if (!isValidBase64Value(expr.BeginReading() + dashIndex + 1,
+ expr.EndReading())) {
+ return nullptr;
+ }
+
+ // cache if encountering hash or nonce to invalidate unsafe-inline
+ mHasHashOrNonce = true;
+ return new nsCSPNonceSrc(
+ Substring(expr, dashIndex + 1, expr.Length() - dashIndex + 1));
+}
+
+// hash-source = "'" hash-algo "-" base64-value "'"
+nsCSPHashSrc* nsCSPParser::hashSource() {
+ CSPPARSERLOG(("nsCSPParser::hashSource, mCurToken: %s, mCurValue: %s",
+ NS_ConvertUTF16toUTF8(mCurToken).get(),
+ NS_ConvertUTF16toUTF8(mCurValue).get()));
+
+ // Check if mCurToken starts and ends with "'"
+ if (mCurToken.First() != SINGLEQUOTE || mCurToken.Last() != SINGLEQUOTE) {
+ return nullptr;
+ }
+
+ // Trim surrounding single quotes
+ const nsAString& expr = Substring(mCurToken, 1, mCurToken.Length() - 2);
+
+ int32_t dashIndex = expr.FindChar(DASH);
+ if (dashIndex < 0) {
+ return nullptr;
+ }
+
+ if (!isValidBase64Value(expr.BeginReading() + dashIndex + 1,
+ expr.EndReading())) {
+ return nullptr;
+ }
+
+ nsAutoString algo(Substring(expr, 0, dashIndex));
+ nsAutoString hash(
+ Substring(expr, dashIndex + 1, expr.Length() - dashIndex + 1));
+
+ for (uint32_t i = 0; i < kHashSourceValidFnsLen; i++) {
+ if (algo.LowerCaseEqualsASCII(kHashSourceValidFns[i])) {
+ // cache if encountering hash or nonce to invalidate unsafe-inline
+ mHasHashOrNonce = true;
+ return new nsCSPHashSrc(algo, hash);
+ }
+ }
+ return nullptr;
+}
+
+// source-expression = scheme-source / host-source / keyword-source
+// / nonce-source / hash-source
+nsCSPBaseSrc* nsCSPParser::sourceExpression() {
+ CSPPARSERLOG(("nsCSPParser::sourceExpression, mCurToken: %s, mCurValue: %s",
+ NS_ConvertUTF16toUTF8(mCurToken).get(),
+ NS_ConvertUTF16toUTF8(mCurValue).get()));
+
+ // Check if it is a keyword
+ if (nsCSPBaseSrc* cspKeyword = keywordSource()) {
+ return cspKeyword;
+ }
+
+ // Check if it is a nonce-source
+ if (nsCSPNonceSrc* cspNonce = nonceSource()) {
+ return cspNonce;
+ }
+
+ // Check if it is a hash-source
+ if (nsCSPHashSrc* cspHash = hashSource()) {
+ return cspHash;
+ }
+
+ // We handle a single "*" as host here, to avoid any confusion when applying
+ // the default scheme. However, we still would need to apply the default
+ // scheme in case we would parse "*:80".
+ if (mCurToken.EqualsASCII("*")) {
+ return new nsCSPHostSrc(u"*"_ns);
+ }
+
+ // Calling resetCurChar allows us to use mCurChar and mEndChar
+ // to parse mCurToken; e.g. mCurToken = "http://www.example.com", then
+ // mCurChar = 'h'
+ // mEndChar = points just after the last 'm'
+ // mCurValue = ""
+ resetCurChar(mCurToken);
+
+ // Check if mCurToken starts with a scheme
+ nsAutoString parsedScheme;
+ if (nsCSPSchemeSrc* cspScheme = schemeSource()) {
+ // mCurToken might only enforce a specific scheme
+ if (atEnd()) {
+ return cspScheme;
+ }
+ // If something follows the scheme, we do not create
+ // a nsCSPSchemeSrc, but rather a nsCSPHostSrc, which
+ // needs to know the scheme to enforce; remember the
+ // scheme and delete cspScheme;
+ cspScheme->toString(parsedScheme);
+ parsedScheme.Trim(":", false, true);
+ delete cspScheme;
+
+ // If mCurToken provides not only a scheme, but also a host, we have to
+ // check if two slashes follow the scheme.
+ if (!accept(SLASH) || !accept(SLASH)) {
+ AutoTArray<nsString, 1> params = {mCurToken};
+ logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "failedToParseUnrecognizedSource", params);
+ return nullptr;
+ }
+ }
+
+ // Calling resetCurValue allows us to keep pointers for mCurChar and mEndChar
+ // alive, but resets mCurValue; e.g. mCurToken = "http://www.example.com",
+ // then mCurChar = 'w' mEndChar = 'm' mCurValue = ""
+ resetCurValue();
+
+ // If mCurToken does not provide a scheme (scheme-less source), we apply the
+ // scheme from selfURI
+ if (parsedScheme.IsEmpty()) {
+ // Resetting internal helpers, because we might already have parsed some of
+ // the host when trying to parse a scheme.
+ resetCurChar(mCurToken);
+ nsAutoCString selfScheme;
+ mSelfURI->GetScheme(selfScheme);
+ parsedScheme.AssignASCII(selfScheme.get());
+ }
+
+ // At this point we are expecting a host to be parsed.
+ // Trying to create a new nsCSPHost.
+ if (nsCSPHostSrc* cspHost = hostSource()) {
+ // Do not forget to set the parsed scheme.
+ cspHost->setScheme(parsedScheme);
+ cspHost->setWithinFrameAncestorsDir(mParsingFrameAncestorsDir);
+ return cspHost;
+ }
+ // Error was reported in hostSource()
+ return nullptr;
+}
+
+// source-list = *WSP [ source-expression *( 1*WSP source-expression ) *WSP ]
+// / *WSP "'none'" *WSP
+void nsCSPParser::sourceList(nsTArray<nsCSPBaseSrc*>& outSrcs) {
+ bool isNone = false;
+
+ // remember, srcs start at index 1
+ for (uint32_t i = 1; i < mCurDir.Length(); i++) {
+ // mCurToken is only set here and remains the current token
+ // to be processed, which avoid passing arguments between functions.
+ mCurToken = mCurDir[i];
+ resetCurValue();
+
+ CSPPARSERLOG(("nsCSPParser::sourceList, mCurToken: %s, mCurValue: %s",
+ NS_ConvertUTF16toUTF8(mCurToken).get(),
+ NS_ConvertUTF16toUTF8(mCurValue).get()));
+
+ // Special case handling for none:
+ // Ignore 'none' if any other src is available.
+ // (See http://www.w3.org/TR/CSP11/#parsing)
+ if (CSP_IsKeyword(mCurToken, CSP_NONE)) {
+ isNone = true;
+ continue;
+ }
+ // Must be a regular source expression
+ nsCSPBaseSrc* src = sourceExpression();
+ if (src) {
+ outSrcs.AppendElement(src);
+ }
+ }
+
+ // Check if the directive contains a 'none'
+ if (isNone) {
+ // If the directive contains no other srcs, then we set the 'none'
+ if (outSrcs.IsEmpty() ||
+ (outSrcs.Length() == 1 && outSrcs[0]->isReportSample())) {
+ nsCSPKeywordSrc* keyword = new nsCSPKeywordSrc(CSP_NONE);
+ outSrcs.InsertElementAt(0, keyword);
+ }
+ // Otherwise, we ignore 'none' and report a warning
+ else {
+ AutoTArray<nsString, 1> params;
+ params.AppendElement(CSP_EnumToUTF16Keyword(CSP_NONE));
+ logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "ignoringUnknownOption", params);
+ }
+ }
+}
+
+void nsCSPParser::reportURIList(nsCSPDirective* aDir) {
+ CSPPARSERLOG(("nsCSPParser::reportURIList"));
+
+ nsTArray<nsCSPBaseSrc*> srcs;
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv;
+
+ // remember, srcs start at index 1
+ for (uint32_t i = 1; i < mCurDir.Length(); i++) {
+ mCurToken = mCurDir[i];
+
+ CSPPARSERLOG(("nsCSPParser::reportURIList, mCurToken: %s, mCurValue: %s",
+ NS_ConvertUTF16toUTF8(mCurToken).get(),
+ NS_ConvertUTF16toUTF8(mCurValue).get()));
+
+ rv = NS_NewURI(getter_AddRefs(uri), mCurToken, "", mSelfURI);
+
+ // If creating the URI casued an error, skip this URI
+ if (NS_FAILED(rv)) {
+ AutoTArray<nsString, 1> params = {mCurToken};
+ logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "couldNotParseReportURI", params);
+ continue;
+ }
+
+ // Create new nsCSPReportURI and append to the list.
+ nsCSPReportURI* reportURI = new nsCSPReportURI(uri);
+ srcs.AppendElement(reportURI);
+ }
+
+ if (srcs.Length() == 0) {
+ AutoTArray<nsString, 1> directiveName = {mCurToken};
+ logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "ignoringDirectiveWithNoValues", directiveName);
+ delete aDir;
+ return;
+ }
+
+ aDir->addSrcs(srcs);
+ mPolicy->addDirective(aDir);
+}
+
+/* Helper function for parsing sandbox flags. This function solely concatenates
+ * all the source list tokens (the sandbox flags) so the attribute parser
+ * (nsContentUtils::ParseSandboxAttributeToFlags) can parse them.
+ */
+void nsCSPParser::sandboxFlagList(nsCSPDirective* aDir) {
+ CSPPARSERLOG(("nsCSPParser::sandboxFlagList"));
+
+ nsAutoString flags;
+
+ // remember, srcs start at index 1
+ for (uint32_t i = 1; i < mCurDir.Length(); i++) {
+ mCurToken = mCurDir[i];
+
+ CSPPARSERLOG(("nsCSPParser::sandboxFlagList, mCurToken: %s, mCurValue: %s",
+ NS_ConvertUTF16toUTF8(mCurToken).get(),
+ NS_ConvertUTF16toUTF8(mCurValue).get()));
+
+ if (!nsContentUtils::IsValidSandboxFlag(mCurToken)) {
+ AutoTArray<nsString, 1> params = {mCurToken};
+ logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "couldntParseInvalidSandboxFlag", params);
+ continue;
+ }
+
+ flags.Append(mCurToken);
+ if (i != mCurDir.Length() - 1) {
+ flags.AppendLiteral(" ");
+ }
+ }
+
+ // Please note that the sandbox directive can exist
+ // by itself (not containing any flags).
+ nsTArray<nsCSPBaseSrc*> srcs;
+ srcs.AppendElement(new nsCSPSandboxFlags(flags));
+ aDir->addSrcs(srcs);
+ mPolicy->addDirective(aDir);
+}
+
+// directive-value = *( WSP / <VCHAR except ";" and ","> )
+void nsCSPParser::directiveValue(nsTArray<nsCSPBaseSrc*>& outSrcs) {
+ CSPPARSERLOG(("nsCSPParser::directiveValue"));
+
+ // Just forward to sourceList
+ sourceList(outSrcs);
+}
+
+// directive-name = 1*( ALPHA / DIGIT / "-" )
+nsCSPDirective* nsCSPParser::directiveName() {
+ CSPPARSERLOG(("nsCSPParser::directiveName, mCurToken: %s, mCurValue: %s",
+ NS_ConvertUTF16toUTF8(mCurToken).get(),
+ NS_ConvertUTF16toUTF8(mCurValue).get()));
+
+ // Check if it is a valid directive
+ CSPDirective directive = CSP_StringToCSPDirective(mCurToken);
+ if (directive == nsIContentSecurityPolicy::NO_DIRECTIVE) {
+ AutoTArray<nsString, 1> params = {mCurToken};
+ logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "couldNotProcessUnknownDirective", params);
+ return nullptr;
+ }
+
+ // The directive 'reflected-xss' is part of CSP 1.1, see:
+ // http://www.w3.org/TR/2014/WD-CSP11-20140211/#reflected-xss
+ // Currently we are not supporting that directive, hence we log a
+ // warning to the console and ignore the directive including its values.
+ if (directive == nsIContentSecurityPolicy::REFLECTED_XSS_DIRECTIVE) {
+ AutoTArray<nsString, 1> params = {mCurToken};
+ logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "notSupportingDirective", params);
+ return nullptr;
+ }
+
+ // Make sure the directive does not already exist
+ // (see http://www.w3.org/TR/CSP11/#parsing)
+ if (mPolicy->hasDirective(directive)) {
+ AutoTArray<nsString, 1> params = {mCurToken};
+ logWarningErrorToConsole(nsIScriptError::warningFlag, "duplicateDirective",
+ params);
+ return nullptr;
+ }
+
+ // CSP delivered via meta tag should ignore the following directives:
+ // report-uri, frame-ancestors, and sandbox, see:
+ // http://www.w3.org/TR/CSP11/#delivery-html-meta-element
+ if (mDeliveredViaMetaTag &&
+ ((directive == nsIContentSecurityPolicy::REPORT_URI_DIRECTIVE) ||
+ (directive == nsIContentSecurityPolicy::FRAME_ANCESTORS_DIRECTIVE) ||
+ (directive == nsIContentSecurityPolicy::SANDBOX_DIRECTIVE))) {
+ // log to the console to indicate that meta CSP is ignoring the directive
+ AutoTArray<nsString, 1> params = {mCurToken};
+ logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "ignoringSrcFromMetaCSP", params);
+ return nullptr;
+ }
+
+ // special case handling for block-all-mixed-content
+ if (directive == nsIContentSecurityPolicy::BLOCK_ALL_MIXED_CONTENT) {
+ // If mixed content upgrade is enabled for all types block-all-mixed-content
+ // is obsolete
+ if (mozilla::StaticPrefs::
+ security_mixed_content_upgrade_display_content() &&
+ mozilla::StaticPrefs::
+ security_mixed_content_upgrade_display_content_image() &&
+ mozilla::StaticPrefs::
+ security_mixed_content_upgrade_display_content_audio() &&
+ mozilla::StaticPrefs::
+ security_mixed_content_upgrade_display_content_video()) {
+ // log to the console that if mixed content display upgrading is enabled
+ // block-all-mixed-content is obsolete.
+ AutoTArray<nsString, 1> params = {mCurToken};
+ logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "obsoleteBlockAllMixedContent", params);
+ }
+ return new nsBlockAllMixedContentDirective(directive);
+ }
+
+ // special case handling for upgrade-insecure-requests
+ if (directive == nsIContentSecurityPolicy::UPGRADE_IF_INSECURE_DIRECTIVE) {
+ return new nsUpgradeInsecureDirective(directive);
+ }
+
+ // if we have a child-src, cache it as a fallback for
+ // * workers (if worker-src is not explicitly specified)
+ // * frames (if frame-src is not explicitly specified)
+ if (directive == nsIContentSecurityPolicy::CHILD_SRC_DIRECTIVE) {
+ mChildSrc = new nsCSPChildSrcDirective(directive);
+ return mChildSrc;
+ }
+
+ // if we have a frame-src, cache it so we can discard child-src for frames
+ if (directive == nsIContentSecurityPolicy::FRAME_SRC_DIRECTIVE) {
+ mFrameSrc = new nsCSPDirective(directive);
+ return mFrameSrc;
+ }
+
+ // if we have a worker-src, cache it so we can discard child-src for workers
+ if (directive == nsIContentSecurityPolicy::WORKER_SRC_DIRECTIVE) {
+ mWorkerSrc = new nsCSPDirective(directive);
+ return mWorkerSrc;
+ }
+
+ // if we have a script-src, cache it as a fallback for worker-src
+ // in case child-src is not present. It is also used as a fallback for
+ // script-src-elem and script-src-attr.
+ if (directive == nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE) {
+ mScriptSrc = new nsCSPScriptSrcDirective(directive);
+ return mScriptSrc;
+ }
+
+ // If we have a style-src, cache it as a fallback for style-src-elem and
+ // style-src-attr.
+ if (directive == nsIContentSecurityPolicy::STYLE_SRC_DIRECTIVE) {
+ mStyleSrc = new nsCSPStyleSrcDirective(directive);
+ return mStyleSrc;
+ }
+
+ return new nsCSPDirective(directive);
+}
+
+// directive = *WSP [ directive-name [ WSP directive-value ] ]
+void nsCSPParser::directive() {
+ // Set the directiveName to mCurToken
+ // Remember, the directive name is stored at index 0
+ mCurToken = mCurDir[0];
+
+ CSPPARSERLOG(("nsCSPParser::directive, mCurToken: %s, mCurValue: %s",
+ NS_ConvertUTF16toUTF8(mCurToken).get(),
+ NS_ConvertUTF16toUTF8(mCurValue).get()));
+
+ // Make sure that the directive-srcs-array contains at least
+ // one directive.
+ if (mCurDir.Length() == 0) {
+ AutoTArray<nsString, 1> params = {u"directive missing"_ns};
+ logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "failedToParseUnrecognizedSource", params);
+ return;
+ }
+
+ if (CSP_IsEmptyDirective(mCurValue, mCurToken)) {
+ return;
+ }
+
+ // Try to create a new CSPDirective
+ nsCSPDirective* cspDir = directiveName();
+ if (!cspDir) {
+ // if we can not create a CSPDirective, we can skip parsing the srcs for
+ // that array
+ return;
+ }
+
+ // special case handling for block-all-mixed-content, which is only specified
+ // by a directive name but does not include any srcs.
+ if (cspDir->equals(nsIContentSecurityPolicy::BLOCK_ALL_MIXED_CONTENT)) {
+ if (mCurDir.Length() > 1) {
+ AutoTArray<nsString, 1> params = {u"block-all-mixed-content"_ns};
+ logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "ignoreSrcForDirective", params);
+ }
+ // add the directive and return
+ mPolicy->addDirective(cspDir);
+ return;
+ }
+
+ // special case handling for upgrade-insecure-requests, which is only
+ // specified by a directive name but does not include any srcs.
+ if (cspDir->equals(nsIContentSecurityPolicy::UPGRADE_IF_INSECURE_DIRECTIVE)) {
+ if (mCurDir.Length() > 1) {
+ AutoTArray<nsString, 1> params = {u"upgrade-insecure-requests"_ns};
+ logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "ignoreSrcForDirective", params);
+ }
+ // add the directive and return
+ mPolicy->addUpgradeInsecDir(
+ static_cast<nsUpgradeInsecureDirective*>(cspDir));
+ return;
+ }
+
+ // special case handling for report-uri directive (since it doesn't contain
+ // a valid source list but rather actual URIs)
+ if (CSP_IsDirective(mCurDir[0],
+ nsIContentSecurityPolicy::REPORT_URI_DIRECTIVE)) {
+ reportURIList(cspDir);
+ return;
+ }
+
+ // special case handling for sandbox directive (since it doe4sn't contain
+ // a valid source list but rather special sandbox flags)
+ if (CSP_IsDirective(mCurDir[0],
+ nsIContentSecurityPolicy::SANDBOX_DIRECTIVE)) {
+ sandboxFlagList(cspDir);
+ return;
+ }
+
+ // make sure to reset cache variables when trying to invalidate unsafe-inline;
+ // unsafe-inline might not only appear in script-src, but also in default-src
+ mHasHashOrNonce = false;
+ mHasAnyUnsafeEval = false;
+ mStrictDynamic = false;
+ mUnsafeInlineKeywordSrc = nullptr;
+
+ mParsingFrameAncestorsDir = CSP_IsDirective(
+ mCurDir[0], nsIContentSecurityPolicy::FRAME_ANCESTORS_DIRECTIVE);
+
+ // Try to parse all the srcs by handing the array off to directiveValue
+ nsTArray<nsCSPBaseSrc*> srcs;
+ directiveValue(srcs);
+
+ // If we can not parse any srcs; we let the source expression be the empty set
+ // ('none') see, http://www.w3.org/TR/CSP11/#source-list-parsing
+ if (srcs.IsEmpty() || (srcs.Length() == 1 && srcs[0]->isReportSample())) {
+ nsCSPKeywordSrc* keyword = new nsCSPKeywordSrc(CSP_NONE);
+ srcs.InsertElementAt(0, keyword);
+ }
+
+ // If policy contains 'strict-dynamic' warn about ignored sources.
+ if (mStrictDynamic &&
+ !CSP_IsDirective(mCurDir[0],
+ nsIContentSecurityPolicy::DEFAULT_SRC_DIRECTIVE)) {
+ for (uint32_t i = 0; i < srcs.Length(); i++) {
+ nsAutoString srcStr;
+ srcs[i]->toString(srcStr);
+ // Hashes and nonces continue to apply with 'strict-dynamic', as well as
+ // 'unsafe-eval', 'wasm-unsafe-eval' and 'unsafe-hashes'.
+ if (!srcs[i]->isKeyword(CSP_STRICT_DYNAMIC) &&
+ !srcs[i]->isKeyword(CSP_UNSAFE_EVAL) &&
+ !srcs[i]->isKeyword(CSP_WASM_UNSAFE_EVAL) &&
+ !srcs[i]->isKeyword(CSP_UNSAFE_HASHES) && !srcs[i]->isNonce() &&
+ !srcs[i]->isHash()) {
+ AutoTArray<nsString, 2> params = {srcStr, mCurDir[0]};
+ logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "ignoringScriptSrcForStrictDynamic", params);
+ }
+ }
+
+ // Log a warning that all scripts might be blocked because the policy
+ // contains 'strict-dynamic' but no valid nonce or hash.
+ if (!mHasHashOrNonce) {
+ AutoTArray<nsString, 1> params = {mCurDir[0]};
+ logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "strictDynamicButNoHashOrNonce", params);
+ }
+ }
+
+ // From https://w3c.github.io/webappsec-csp/#allow-all-inline
+ // follows that when either a hash or nonce is specified, 'unsafe-inline'
+ // should not apply.
+ if (mHasHashOrNonce && mUnsafeInlineKeywordSrc &&
+ (cspDir->isDefaultDirective() ||
+ cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE) ||
+ cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE) ||
+ cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE) ||
+ cspDir->equals(nsIContentSecurityPolicy::STYLE_SRC_DIRECTIVE) ||
+ cspDir->equals(nsIContentSecurityPolicy::STYLE_SRC_ELEM_DIRECTIVE) ||
+ cspDir->equals(nsIContentSecurityPolicy::STYLE_SRC_ATTR_DIRECTIVE))) {
+ // Log to the console that unsafe-inline will be ignored.
+ AutoTArray<nsString, 2> params = {u"'unsafe-inline'"_ns, mCurDir[0]};
+ logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "ignoringSrcWithinNonceOrHashDirective", params);
+ }
+
+ if (mHasAnyUnsafeEval &&
+ (cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE) ||
+ cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE))) {
+ // Log to the console that (wasm-)unsafe-eval will be ignored.
+ AutoTArray<nsString, 1> params = {mCurDir[0]};
+ logWarningErrorToConsole(nsIScriptError::warningFlag, "ignoringUnsafeEval",
+ params);
+ }
+
+ // Add the newly created srcs to the directive and add the directive to the
+ // policy
+ cspDir->addSrcs(srcs);
+ mPolicy->addDirective(cspDir);
+}
+
+// policy = [ directive *( ";" [ directive ] ) ]
+nsCSPPolicy* nsCSPParser::policy() {
+ CSPPARSERLOG(("nsCSPParser::policy"));
+
+ mPolicy = new nsCSPPolicy();
+ for (uint32_t i = 0; i < mTokens.Length(); i++) {
+ // https://w3c.github.io/webappsec-csp/#parse-serialized-policy
+ // Step 2.2. ..., or if token is not an ASCII string, continue.
+ //
+ // Note: In the spec the token isn't split by whitespace yet.
+ bool isAscii = true;
+ for (const auto& token : mTokens[i]) {
+ if (!IsAscii(token)) {
+ AutoTArray<nsString, 1> params = {mTokens[i][0], token};
+ logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "ignoringNonAsciiToken", params);
+ isAscii = false;
+ break;
+ }
+ }
+ if (!isAscii) {
+ continue;
+ }
+
+ // All input is already tokenized; set one tokenized array in the form of
+ // [ name, src, src, ... ]
+ // to mCurDir and call directive which processes the current directive.
+ mCurDir = mTokens[i].Clone();
+ directive();
+ }
+
+ if (mChildSrc) {
+ if (!mFrameSrc) {
+ // if frame-src is specified explicitly for that policy than child-src
+ // should not restrict frames; if not, than child-src needs to restrict
+ // frames.
+ mChildSrc->setRestrictFrames();
+ }
+ if (!mWorkerSrc) {
+ // if worker-src is specified explicitly for that policy than child-src
+ // should not restrict workers; if not, than child-src needs to restrict
+ // workers.
+ mChildSrc->setRestrictWorkers();
+ }
+ }
+
+ // if script-src is specified, but not worker-src and also no child-src, then
+ // script-src has to govern workers.
+ if (mScriptSrc && !mWorkerSrc && !mChildSrc) {
+ mScriptSrc->setRestrictWorkers();
+ }
+
+ // If script-src is specified and script-src-elem is not specified, then
+ // script-src has to govern script requests and script blocks.
+ if (mScriptSrc && !mPolicy->hasDirective(
+ nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE)) {
+ mScriptSrc->setRestrictScriptElem();
+ }
+
+ // If script-src is specified and script-src-attr is not specified, then
+ // script-src has to govern script attr (event handlers).
+ if (mScriptSrc && !mPolicy->hasDirective(
+ nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE)) {
+ mScriptSrc->setRestrictScriptAttr();
+ }
+
+ // If style-src is specified and style-src-elem is not specified, then
+ // style-src serves as a fallback.
+ if (mStyleSrc && !mPolicy->hasDirective(
+ nsIContentSecurityPolicy::STYLE_SRC_ELEM_DIRECTIVE)) {
+ mStyleSrc->setRestrictStyleElem();
+ }
+
+ // If style-src is specified and style-attr-elem is not specified, then
+ // style-src serves as a fallback.
+ if (mStyleSrc && !mPolicy->hasDirective(
+ nsIContentSecurityPolicy::STYLE_SRC_ATTR_DIRECTIVE)) {
+ mStyleSrc->setRestrictStyleAttr();
+ }
+
+ return mPolicy;
+}
+
+nsCSPPolicy* nsCSPParser::parseContentSecurityPolicy(
+ const nsAString& aPolicyString, nsIURI* aSelfURI, bool aReportOnly,
+ nsCSPContext* aCSPContext, bool aDeliveredViaMetaTag,
+ bool aSuppressLogMessages) {
+ if (CSPPARSERLOGENABLED()) {
+ CSPPARSERLOG(("nsCSPParser::parseContentSecurityPolicy, policy: %s",
+ NS_ConvertUTF16toUTF8(aPolicyString).get()));
+ CSPPARSERLOG(("nsCSPParser::parseContentSecurityPolicy, selfURI: %s",
+ aSelfURI->GetSpecOrDefault().get()));
+ CSPPARSERLOG(("nsCSPParser::parseContentSecurityPolicy, reportOnly: %s",
+ (aReportOnly ? "true" : "false")));
+ CSPPARSERLOG(
+ ("nsCSPParser::parseContentSecurityPolicy, deliveredViaMetaTag: %s",
+ (aDeliveredViaMetaTag ? "true" : "false")));
+ }
+
+ NS_ASSERTION(aSelfURI, "Can not parseContentSecurityPolicy without aSelfURI");
+
+ // Separate all input into tokens and store them in the form of:
+ // [ [ name, src, src, ... ], [ name, src, src, ... ], ... ]
+ // The tokenizer itself can not fail; all eventual errors
+ // are detected in the parser itself.
+
+ nsTArray<CopyableTArray<nsString> > tokens;
+ PolicyTokenizer::tokenizePolicy(aPolicyString, tokens);
+
+ nsCSPParser parser(tokens, aSelfURI, aCSPContext, aDeliveredViaMetaTag,
+ aSuppressLogMessages);
+
+ // Start the parser to generate a new CSPPolicy using the generated tokens.
+ nsCSPPolicy* policy = parser.policy();
+
+ // Check that report-only policies define a report-uri, otherwise log warning.
+ if (aReportOnly) {
+ policy->setReportOnlyFlag(true);
+ if (!policy->hasDirective(nsIContentSecurityPolicy::REPORT_URI_DIRECTIVE)) {
+ nsAutoCString prePath;
+ nsresult rv = aSelfURI->GetPrePath(prePath);
+ NS_ENSURE_SUCCESS(rv, policy);
+ AutoTArray<nsString, 1> params;
+ CopyUTF8toUTF16(prePath, *params.AppendElement());
+ parser.logWarningErrorToConsole(nsIScriptError::warningFlag,
+ "reportURInotInReportOnlyHeader", params);
+ }
+ }
+
+ policy->setDeliveredViaMetaTagFlag(aDeliveredViaMetaTag);
+
+ if (policy->getNumDirectives() == 0) {
+ // Individual errors were already reported in the parser, but if
+ // we do not have an enforcable directive at all, we return null.
+ delete policy;
+ return nullptr;
+ }
+
+ if (CSPPARSERLOGENABLED()) {
+ nsString parsedPolicy;
+ policy->toString(parsedPolicy);
+ CSPPARSERLOG(("nsCSPParser::parseContentSecurityPolicy, parsedPolicy: %s",
+ NS_ConvertUTF16toUTF8(parsedPolicy).get()));
+ }
+
+ return policy;
+}
diff --git a/dom/security/nsCSPParser.h b/dom/security/nsCSPParser.h
new file mode 100644
index 0000000000..21679d86a0
--- /dev/null
+++ b/dom/security/nsCSPParser.h
@@ -0,0 +1,217 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsCSPParser_h___
+#define nsCSPParser_h___
+
+#include "nsCSPUtils.h"
+#include "nsCSPContext.h"
+#include "nsIURI.h"
+#include "PolicyTokenizer.h"
+
+bool isNumberToken(char16_t aSymbol);
+bool isValidHexDig(char16_t aHexDig);
+
+// clang-format off
+const char16_t COLON = ':';
+const char16_t SEMICOLON = ';';
+const char16_t SLASH = '/';
+const char16_t PLUS = '+';
+const char16_t DASH = '-';
+const char16_t DOT = '.';
+const char16_t UNDERLINE = '_';
+const char16_t TILDE = '~';
+const char16_t WILDCARD = '*';
+const char16_t SINGLEQUOTE = '\'';
+const char16_t NUMBER_SIGN = '#';
+const char16_t QUESTIONMARK = '?';
+const char16_t PERCENT_SIGN = '%';
+const char16_t EXCLAMATION = '!';
+const char16_t DOLLAR = '$';
+const char16_t AMPERSAND = '&';
+const char16_t OPENBRACE = '(';
+const char16_t CLOSINGBRACE = ')';
+const char16_t EQUALS = '=';
+const char16_t ATSYMBOL = '@';
+// clang-format on
+
+class nsCSPParser {
+ public:
+ /**
+ * The CSP parser only has one publicly accessible function, which is
+ * parseContentSecurityPolicy. Internally the input string is separated into
+ * string tokens and policy() is called, which starts parsing the policy. The
+ * parser calls one function after the other according the the source-list
+ * from http://www.w3.org/TR/CSP11/#source-list. E.g., the parser can only
+ * call port() after the parser has already processed any possible host in
+ * host(), similar to a finite state machine.
+ */
+ static nsCSPPolicy* parseContentSecurityPolicy(const nsAString& aPolicyString,
+ nsIURI* aSelfURI,
+ bool aReportOnly,
+ nsCSPContext* aCSPContext,
+ bool aDeliveredViaMetaTag,
+ bool aSuppressLogMessages);
+
+ private:
+ nsCSPParser(policyTokens& aTokens, nsIURI* aSelfURI,
+ nsCSPContext* aCSPContext, bool aDeliveredViaMetaTag,
+ bool aSuppressLogMessages);
+
+ ~nsCSPParser();
+
+ // Parsing the CSP using the source-list from
+ // http://www.w3.org/TR/CSP11/#source-list
+ nsCSPPolicy* policy();
+ void directive();
+ nsCSPDirective* directiveName();
+ void directiveValue(nsTArray<nsCSPBaseSrc*>& outSrcs);
+ void referrerDirectiveValue(nsCSPDirective* aDir);
+ void reportURIList(nsCSPDirective* aDir);
+ void sandboxFlagList(nsCSPDirective* aDir);
+ void sourceList(nsTArray<nsCSPBaseSrc*>& outSrcs);
+ nsCSPBaseSrc* sourceExpression();
+ nsCSPSchemeSrc* schemeSource();
+ nsCSPHostSrc* hostSource();
+ nsCSPBaseSrc* keywordSource();
+ nsCSPNonceSrc* nonceSource();
+ nsCSPHashSrc* hashSource();
+ nsCSPHostSrc* host();
+ bool hostChar();
+ bool schemeChar();
+ bool port();
+ bool path(nsCSPHostSrc* aCspHost);
+
+ bool subHost(); // helper function to parse subDomains
+ bool atValidUnreservedChar(); // helper function to parse unreserved
+ bool atValidSubDelimChar(); // helper function to parse sub-delims
+ bool atValidPctEncodedChar(); // helper function to parse pct-encoded
+ bool subPath(nsCSPHostSrc* aCspHost); // helper function to parse paths
+
+ inline bool atEnd() { return mCurChar >= mEndChar; }
+
+ inline bool accept(char16_t aSymbol) {
+ if (atEnd()) {
+ return false;
+ }
+ return (*mCurChar == aSymbol) && advance();
+ }
+
+ inline bool accept(bool (*aClassifier)(char16_t)) {
+ if (atEnd()) {
+ return false;
+ }
+ return (aClassifier(*mCurChar)) && advance();
+ }
+
+ inline bool peek(char16_t aSymbol) {
+ if (atEnd()) {
+ return false;
+ }
+ return *mCurChar == aSymbol;
+ }
+
+ inline bool peek(bool (*aClassifier)(char16_t)) {
+ if (atEnd()) {
+ return false;
+ }
+ return aClassifier(*mCurChar);
+ }
+
+ inline bool advance() {
+ if (atEnd()) {
+ return false;
+ }
+ mCurValue.Append(*mCurChar++);
+ return true;
+ }
+
+ inline void resetCurValue() { mCurValue.Truncate(); }
+
+ bool atEndOfPath();
+ bool atValidPathChar();
+
+ void resetCurChar(const nsAString& aToken);
+
+ void logWarningErrorToConsole(uint32_t aSeverityFlag, const char* aProperty,
+ const nsTArray<nsString>& aParams);
+
+ /**
+ * When parsing the policy, the parser internally uses the following helper
+ * variables/members which are used/reset during parsing. The following
+ * example explains how they are used.
+ * The tokenizer separats all input into arrays of arrays of strings, which
+ * are stored in mTokens, for example:
+ * mTokens = [ [ script-src, http://www.example.com, 'self' ], ... ]
+ *
+ * When parsing starts, mCurdir always holds the currently processed array of
+ * strings.
+ * In our example:
+ * mCurDir = [ script-src, http://www.example.com, 'self' ]
+ *
+ * During parsing, we process/consume one string at a time of that array.
+ * We set mCurToken to the string we are currently processing; in the first
+ * case that would be: mCurToken = script-src which allows to do simple string
+ * comparisons to see if mCurToken is a valid directive.
+ *
+ * Continuing parsing, the parser consumes the next string of that array,
+ * resetting:
+ * mCurToken = "http://www.example.com"
+ * ^ ^
+ * mCurChar mEndChar (points *after* the 'm')
+ * mCurValue = ""
+ *
+ * After calling advance() the first time, helpers would hold the following
+ * values:
+ * mCurToken = "http://www.example.com"
+ * ^ ^
+ * mCurChar mEndChar (points *after* the 'm')
+ * mCurValue = "h"
+ *
+ * We continue parsing till all strings of one directive are consumed, then we
+ * reset mCurDir to hold the next array of strings and start the process all
+ * over.
+ */
+
+ const char16_t* mCurChar;
+ const char16_t* mEndChar;
+ nsString mCurValue;
+ nsString mCurToken;
+ nsTArray<nsString> mCurDir;
+
+ // helpers to allow invalidation of srcs within script-src and style-src
+ // if either 'strict-dynamic' or at least a hash or nonce is present.
+ bool mHasHashOrNonce; // false, if no hash or nonce is defined
+ bool mHasAnyUnsafeEval; // false, if no (wasm-)unsafe-eval keyword is used.
+ bool mStrictDynamic; // false, if 'strict-dynamic' is not defined
+ nsCSPKeywordSrc* mUnsafeInlineKeywordSrc; // null, otherwise invlidate()
+
+ // cache variables for child-src, frame-src and worker-src handling;
+ // in CSP 3 child-src is deprecated. For backwards compatibility
+ // child-src needs to restrict:
+ // (*) frames, in case frame-src is not expicitly specified
+ // (*) workers, in case worker-src is not expicitly specified
+ // If neither worker-src, nor child-src is present, then script-src
+ // needs to govern workers.
+ nsCSPChildSrcDirective* mChildSrc;
+ nsCSPDirective* mFrameSrc;
+ nsCSPDirective* mWorkerSrc;
+ nsCSPScriptSrcDirective* mScriptSrc;
+ nsCSPStyleSrcDirective* mStyleSrc;
+
+ // cache variable to let nsCSPHostSrc know that it's within
+ // the frame-ancestors directive.
+ bool mParsingFrameAncestorsDir;
+
+ policyTokens mTokens;
+ nsIURI* mSelfURI;
+ nsCSPPolicy* mPolicy;
+ nsCSPContext* mCSPContext; // used for console logging
+ bool mDeliveredViaMetaTag;
+ bool mSuppressLogMessages;
+};
+
+#endif /* nsCSPParser_h___ */
diff --git a/dom/security/nsCSPService.cpp b/dom/security/nsCSPService.cpp
new file mode 100644
index 0000000000..75e8ec1933
--- /dev/null
+++ b/dom/security/nsCSPService.cpp
@@ -0,0 +1,374 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/Logging.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_security.h"
+#include "nsString.h"
+#include "nsCOMPtr.h"
+#include "nsIURI.h"
+#include "nsIContent.h"
+#include "nsCSPService.h"
+#include "nsIContentSecurityPolicy.h"
+#include "nsError.h"
+#include "nsIAsyncVerifyRedirectCallback.h"
+#include "nsAsyncRedirectVerifyHelper.h"
+#include "nsContentUtils.h"
+#include "nsContentPolicyUtils.h"
+#include "nsNetUtil.h"
+#include "nsIProtocolHandler.h"
+#include "nsQueryObject.h"
+#include "mozilla/net/DocumentLoadListener.h"
+#include "mozilla/net/DocumentChannel.h"
+
+using namespace mozilla;
+
+static LazyLogModule gCspPRLog("CSP");
+
+CSPService::CSPService() = default;
+
+CSPService::~CSPService() = default;
+
+NS_IMPL_ISUPPORTS(CSPService, nsIContentPolicy, nsIChannelEventSink)
+
+// Helper function to identify protocols and content types not subject to CSP.
+bool subjectToCSP(nsIURI* aURI, nsContentPolicyType aContentType) {
+ ExtContentPolicyType contentType =
+ nsContentUtils::InternalContentPolicyTypeToExternal(aContentType);
+
+ // These content types are not subject to CSP content policy checks:
+ // TYPE_CSP_REPORT -- csp can't block csp reports
+ // TYPE_DOCUMENT -- used for frame-ancestors
+ if (contentType == ExtContentPolicy::TYPE_CSP_REPORT ||
+ contentType == ExtContentPolicy::TYPE_DOCUMENT) {
+ return false;
+ }
+
+ // The three protocols: data:, blob: and filesystem: share the same
+ // protocol flag (URI_IS_LOCAL_RESOURCE) with other protocols,
+ // but those three protocols get special attention in CSP and
+ // are subject to CSP, hence we have to make sure those
+ // protocols are subject to CSP, see:
+ // http://www.w3.org/TR/CSP2/#source-list-guid-matching
+ if (aURI->SchemeIs("data") || aURI->SchemeIs("blob") ||
+ aURI->SchemeIs("filesystem")) {
+ return true;
+ }
+
+ // Finally we have to allowlist "about:" which does not fall into
+ // the category underneath and also "javascript:" which is not
+ // subject to CSP content loading rules.
+ if (aURI->SchemeIs("about") || aURI->SchemeIs("javascript")) {
+ return false;
+ }
+
+ // Please note that it should be possible for websites to
+ // allowlist their own protocol handlers with respect to CSP,
+ // hence we use protocol flags to accomplish that, but we also
+ // want resource:, chrome: and moz-icon to be subject to CSP
+ // (which also use URI_IS_LOCAL_RESOURCE).
+ // Exception to the rule are images, styles, and localization
+ // DTDs using a scheme of resource: or chrome:
+ bool isImgOrStyleOrDTD = contentType == ExtContentPolicy::TYPE_IMAGE ||
+ contentType == ExtContentPolicy::TYPE_STYLESHEET ||
+ contentType == ExtContentPolicy::TYPE_DTD;
+ if (aURI->SchemeIs("resource")) {
+ nsAutoCString uriSpec;
+ aURI->GetSpec(uriSpec);
+ // Exempt pdf.js from being subject to a page's CSP.
+ if (StringBeginsWith(uriSpec, "resource://pdf.js/"_ns)) {
+ return false;
+ }
+ if (!isImgOrStyleOrDTD) {
+ return true;
+ }
+ }
+ if (aURI->SchemeIs("chrome") && !isImgOrStyleOrDTD) {
+ return true;
+ }
+ if (aURI->SchemeIs("moz-icon")) {
+ return true;
+ }
+ bool match;
+ nsresult rv = NS_URIChainHasFlags(
+ aURI, nsIProtocolHandler::URI_IS_LOCAL_RESOURCE, &match);
+ if (NS_SUCCEEDED(rv) && match) {
+ return false;
+ }
+ // all other protocols are subject To CSP.
+ return true;
+}
+
+/* static */ nsresult CSPService::ConsultCSP(nsIURI* aContentLocation,
+ nsILoadInfo* aLoadInfo,
+ int16_t* aDecision) {
+ if (!aContentLocation) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsContentPolicyType contentType = aLoadInfo->InternalContentPolicyType();
+
+ nsCOMPtr<nsICSPEventListener> cspEventListener;
+ nsresult rv =
+ aLoadInfo->GetCspEventListener(getter_AddRefs(cspEventListener));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (MOZ_LOG_TEST(gCspPRLog, LogLevel::Debug)) {
+ MOZ_LOG(gCspPRLog, LogLevel::Debug,
+ ("CSPService::ShouldLoad called for %s",
+ aContentLocation->GetSpecOrDefault().get()));
+ }
+
+ // default decision, CSP can revise it if there's a policy to enforce
+ *aDecision = nsIContentPolicy::ACCEPT;
+
+ // No need to continue processing if CSP is disabled or if the protocol
+ // or type is *not* subject to CSP.
+ // Please note, the correct way to opt-out of CSP using a custom
+ // protocolHandler is to set one of the nsIProtocolHandler flags
+ // that are allowlistet in subjectToCSP()
+ if (!subjectToCSP(aContentLocation, contentType)) {
+ return NS_OK;
+ }
+
+ // 1) Apply speculate CSP for preloads
+ bool isPreload = nsContentUtils::IsPreloadType(contentType);
+
+ if (isPreload) {
+ nsCOMPtr<nsIContentSecurityPolicy> preloadCsp = aLoadInfo->GetPreloadCsp();
+ if (preloadCsp) {
+ // obtain the enforcement decision
+ rv = preloadCsp->ShouldLoad(
+ contentType, cspEventListener, aLoadInfo, aContentLocation,
+ nullptr, // no redirect, aOriginal URL is null.
+ false, aDecision);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // if the preload policy already denied the load, then there
+ // is no point in checking the real policy
+ if (NS_CP_REJECTED(*aDecision)) {
+ NS_SetRequestBlockingReason(
+ aLoadInfo, nsILoadInfo::BLOCKING_REASON_CONTENT_POLICY_PRELOAD);
+
+ return NS_OK;
+ }
+ }
+ }
+
+ // 2) Apply actual CSP to all loads. Please note that in case
+ // the csp should be overruled (e.g. by an ExpandedPrincipal)
+ // then loadinfo->GetCsp() returns that CSP instead of the
+ // document's CSP.
+ nsCOMPtr<nsIContentSecurityPolicy> csp = aLoadInfo->GetCsp();
+
+ if (csp) {
+ // Generally aOriginalURI denotes the URI before a redirect and hence
+ // will always be a nullptr here. Only exception are frame navigations
+ // which we want to treat as a redirect for the purpose of CSP reporting
+ // and in particular the `blocked-uri` in the CSP report where we want
+ // to report the prePath information.
+ nsCOMPtr<nsIURI> originalURI = nullptr;
+ ExtContentPolicyType extType =
+ nsContentUtils::InternalContentPolicyTypeToExternal(contentType);
+ if (extType == ExtContentPolicy::TYPE_SUBDOCUMENT &&
+ !aLoadInfo->GetOriginalFrameSrcLoad() &&
+ mozilla::StaticPrefs::
+ security_csp_truncate_blocked_uri_for_frame_navigations()) {
+ nsAutoCString prePathStr;
+ nsresult rv = aContentLocation->GetPrePath(prePathStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = NS_NewURI(getter_AddRefs(originalURI), prePathStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // obtain the enforcement decision
+ rv = csp->ShouldLoad(
+ contentType, cspEventListener, aLoadInfo, aContentLocation,
+ originalURI, // no redirect, unless it's a frame navigation.
+ !isPreload && aLoadInfo->GetSendCSPViolationEvents(), aDecision);
+
+ if (NS_CP_REJECTED(*aDecision)) {
+ NS_SetRequestBlockingReason(
+ aLoadInfo, nsILoadInfo::BLOCKING_REASON_CONTENT_POLICY_GENERAL);
+ }
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+/* nsIContentPolicy implementation */
+NS_IMETHODIMP
+CSPService::ShouldLoad(nsIURI* aContentLocation, nsILoadInfo* aLoadInfo,
+ int16_t* aDecision) {
+ return ConsultCSP(aContentLocation, aLoadInfo, aDecision);
+}
+
+NS_IMETHODIMP
+CSPService::ShouldProcess(nsIURI* aContentLocation, nsILoadInfo* aLoadInfo,
+ int16_t* aDecision) {
+ if (!aContentLocation) {
+ return NS_ERROR_FAILURE;
+ }
+ nsContentPolicyType contentType = aLoadInfo->InternalContentPolicyType();
+
+ if (MOZ_LOG_TEST(gCspPRLog, LogLevel::Debug)) {
+ MOZ_LOG(gCspPRLog, LogLevel::Debug,
+ ("CSPService::ShouldProcess called for %s",
+ aContentLocation->GetSpecOrDefault().get()));
+ }
+
+ // ShouldProcess is only relevant to TYPE_OBJECT, so let's convert the
+ // internal contentPolicyType to the mapping external one.
+ // If it is not TYPE_OBJECT, we can return at this point.
+ // Note that we should still pass the internal contentPolicyType
+ // (contentType) to ShouldLoad().
+ ExtContentPolicyType policyType =
+ nsContentUtils::InternalContentPolicyTypeToExternal(contentType);
+
+ if (policyType != ExtContentPolicy::TYPE_OBJECT) {
+ *aDecision = nsIContentPolicy::ACCEPT;
+ return NS_OK;
+ }
+
+ return ShouldLoad(aContentLocation, aLoadInfo, aDecision);
+}
+
+/* nsIChannelEventSink implementation */
+NS_IMETHODIMP
+CSPService::AsyncOnChannelRedirect(nsIChannel* oldChannel,
+ nsIChannel* newChannel, uint32_t flags,
+ nsIAsyncVerifyRedirectCallback* callback) {
+ net::nsAsyncRedirectAutoCallback autoCallback(callback);
+
+ if (XRE_IsE10sParentProcess()) {
+ nsCOMPtr<nsIParentChannel> parentChannel;
+ NS_QueryNotificationCallbacks(oldChannel, parentChannel);
+ RefPtr<net::DocumentLoadListener> docListener =
+ do_QueryObject(parentChannel);
+ // Since this is an IPC'd channel we do not have access to the request
+ // context. In turn, we do not have an event target for policy violations.
+ // Enforce the CSP check in the content process where we have that info.
+ // We allow redirect checks to run for document loads via
+ // DocumentLoadListener, since these are fully supported and we don't
+ // expose the redirects to the content process. We can't do this for all
+ // request types yet because we don't serialize nsICSPEventListener.
+ if (parentChannel && !docListener) {
+ return NS_OK;
+ }
+ }
+
+ // Don't do these checks if we're switching from DocumentChannel
+ // to a real channel. In that case, we should already have done
+ // the checks in the parent process. AsyncOnChannelRedirect
+ // isn't called in the content process if we switch process,
+ // so checking here would just hide bugs in the process switch
+ // cases.
+ if (RefPtr<net::DocumentChannel> docChannel = do_QueryObject(oldChannel)) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIURI> newUri;
+ nsresult rv = newChannel->GetURI(getter_AddRefs(newUri));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsILoadInfo> loadInfo = oldChannel->LoadInfo();
+
+ /* Since redirecting channels don't call into nsIContentPolicy, we call our
+ * Content Policy implementation directly when redirects occur using the
+ * information set in the LoadInfo when channels are created.
+ *
+ * We check if the CSP permits this host for this type of load, if not,
+ * we cancel the load now.
+ */
+ nsCOMPtr<nsIURI> originalUri;
+ rv = oldChannel->GetOriginalURI(getter_AddRefs(originalUri));
+ if (NS_FAILED(rv)) {
+ autoCallback.DontCallback();
+ oldChannel->Cancel(NS_ERROR_DOM_BAD_URI);
+ return rv;
+ }
+
+ Maybe<nsresult> cancelCode;
+ rv = ConsultCSPForRedirect(originalUri, newUri, loadInfo, cancelCode);
+ if (cancelCode) {
+ oldChannel->Cancel(*cancelCode);
+ }
+ if (NS_FAILED(rv)) {
+ autoCallback.DontCallback();
+ }
+
+ return rv;
+}
+
+nsresult CSPService::ConsultCSPForRedirect(nsIURI* aOriginalURI,
+ nsIURI* aNewURI,
+ nsILoadInfo* aLoadInfo,
+ Maybe<nsresult>& aCancelCode) {
+ // No need to continue processing if CSP is disabled or if the protocol
+ // is *not* subject to CSP.
+ // Please note, the correct way to opt-out of CSP using a custom
+ // protocolHandler is to set one of the nsIProtocolHandler flags
+ // that are allowlistet in subjectToCSP()
+ nsContentPolicyType policyType = aLoadInfo->InternalContentPolicyType();
+ if (!subjectToCSP(aNewURI, policyType)) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsICSPEventListener> cspEventListener;
+ nsresult rv =
+ aLoadInfo->GetCspEventListener(getter_AddRefs(cspEventListener));
+ MOZ_ALWAYS_SUCCEEDS(rv);
+
+ bool isPreload = nsContentUtils::IsPreloadType(policyType);
+
+ /* On redirect, if the content policy is a preload type, rejecting the
+ * preload results in the load silently failing, so we pass true to
+ * the aSendViolationReports parameter. See Bug 1219453.
+ */
+
+ int16_t decision = nsIContentPolicy::ACCEPT;
+
+ // 1) Apply speculative CSP for preloads
+ if (isPreload) {
+ nsCOMPtr<nsIContentSecurityPolicy> preloadCsp = aLoadInfo->GetPreloadCsp();
+ if (preloadCsp) {
+ // Pass originalURI to indicate the redirect
+ preloadCsp->ShouldLoad(
+ policyType, // load type per nsIContentPolicy (uint32_t)
+ cspEventListener, aLoadInfo,
+ aNewURI, // nsIURI
+ aOriginalURI, // Original nsIURI
+ true, // aSendViolationReports
+ &decision);
+
+ // if the preload policy already denied the load, then there
+ // is no point in checking the real policy
+ if (NS_CP_REJECTED(decision)) {
+ aCancelCode = Some(NS_ERROR_DOM_BAD_URI);
+ return NS_BINDING_FAILED;
+ }
+ }
+ }
+
+ // 2) Apply actual CSP to all loads
+ nsCOMPtr<nsIContentSecurityPolicy> csp = aLoadInfo->GetCsp();
+ if (csp) {
+ // Pass originalURI to indicate the redirect
+ csp->ShouldLoad(policyType, // load type per nsIContentPolicy (uint32_t)
+ cspEventListener, aLoadInfo,
+ aNewURI, // nsIURI
+ aOriginalURI, // Original nsIURI
+ true, // aSendViolationReports
+ &decision);
+ if (NS_CP_REJECTED(decision)) {
+ aCancelCode = Some(NS_ERROR_DOM_BAD_URI);
+ return NS_BINDING_FAILED;
+ }
+ }
+
+ return NS_OK;
+}
diff --git a/dom/security/nsCSPService.h b/dom/security/nsCSPService.h
new file mode 100644
index 0000000000..260fe6a21e
--- /dev/null
+++ b/dom/security/nsCSPService.h
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsCSPService_h___
+#define nsCSPService_h___
+
+#include "nsXPCOM.h"
+#include "nsIContentPolicy.h"
+#include "nsIChannel.h"
+#include "nsIChannelEventSink.h"
+
+#define CSPSERVICE_CONTRACTID "@mozilla.org/cspservice;1"
+#define CSPSERVICE_CID \
+ { \
+ 0x8d2f40b2, 0x4875, 0x4c95, { \
+ 0x97, 0xd9, 0x3f, 0x7d, 0xca, 0x2c, 0xb4, 0x60 \
+ } \
+ }
+class CSPService : public nsIContentPolicy, public nsIChannelEventSink {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICONTENTPOLICY
+ NS_DECL_NSICHANNELEVENTSINK
+
+ CSPService();
+
+ // helper function to avoid creating a new instance of the
+ // cspservice everytime we call content policies.
+ static nsresult ConsultCSP(nsIURI* aContentLocation, nsILoadInfo* aLoadInfo,
+ int16_t* aDecision);
+
+ // Static helper to check CSP when doing a channel redirect.
+ // Returns the results to returns from
+ // AsyncOnChannelRedirect/nsIAsyncVerifyRedirectCallback. Optionally returns
+ // an nsresult to Cancel the old channel with.
+ static nsresult ConsultCSPForRedirect(nsIURI* aOriginalURI, nsIURI* aNewURI,
+ nsILoadInfo* aLoadInfo,
+ mozilla::Maybe<nsresult>& aCancelCode);
+
+ protected:
+ virtual ~CSPService();
+};
+#endif /* nsCSPService_h___ */
diff --git a/dom/security/nsCSPUtils.cpp b/dom/security/nsCSPUtils.cpp
new file mode 100644
index 0000000000..50730b691b
--- /dev/null
+++ b/dom/security/nsCSPUtils.cpp
@@ -0,0 +1,1777 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsAttrValue.h"
+#include "nsCharSeparatedTokenizer.h"
+#include "nsContentUtils.h"
+#include "nsCSPUtils.h"
+#include "nsDebug.h"
+#include "nsCSPParser.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIConsoleService.h"
+#include "nsIChannel.h"
+#include "nsICryptoHash.h"
+#include "nsIScriptError.h"
+#include "nsIStringBundle.h"
+#include "nsIURL.h"
+#include "nsNetUtil.h"
+#include "nsReadableUtils.h"
+#include "nsSandboxFlags.h"
+#include "nsServiceManagerUtils.h"
+#include "nsWhitespaceTokenizer.h"
+
+#include "mozilla/Components.h"
+#include "mozilla/dom/CSPDictionariesBinding.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/SRIMetadata.h"
+#include "mozilla/StaticPrefs_security.h"
+
+using namespace mozilla;
+using mozilla::dom::SRIMetadata;
+
+#define DEFAULT_PORT -1
+
+static mozilla::LogModule* GetCspUtilsLog() {
+ static mozilla::LazyLogModule gCspUtilsPRLog("CSPUtils");
+ return gCspUtilsPRLog;
+}
+
+#define CSPUTILSLOG(args) \
+ MOZ_LOG(GetCspUtilsLog(), mozilla::LogLevel::Debug, args)
+#define CSPUTILSLOGENABLED() \
+ MOZ_LOG_TEST(GetCspUtilsLog(), mozilla::LogLevel::Debug)
+
+void CSP_PercentDecodeStr(const nsAString& aEncStr, nsAString& outDecStr) {
+ outDecStr.Truncate();
+
+ // helper function that should not be visible outside this methods scope
+ struct local {
+ static inline char16_t convertHexDig(char16_t aHexDig) {
+ if (isNumberToken(aHexDig)) {
+ return aHexDig - '0';
+ }
+ if (aHexDig >= 'A' && aHexDig <= 'F') {
+ return aHexDig - 'A' + 10;
+ }
+ // must be a lower case character
+ // (aHexDig >= 'a' && aHexDig <= 'f')
+ return aHexDig - 'a' + 10;
+ }
+ };
+
+ const char16_t *cur, *end, *hexDig1, *hexDig2;
+ cur = aEncStr.BeginReading();
+ end = aEncStr.EndReading();
+
+ while (cur != end) {
+ // if it's not a percent sign then there is
+ // nothing to do for that character
+ if (*cur != PERCENT_SIGN) {
+ outDecStr.Append(*cur);
+ cur++;
+ continue;
+ }
+
+ // get the two hexDigs following the '%'-sign
+ hexDig1 = cur + 1;
+ hexDig2 = cur + 2;
+
+ // if there are no hexdigs after the '%' then
+ // there is nothing to do for us.
+ if (hexDig1 == end || hexDig2 == end || !isValidHexDig(*hexDig1) ||
+ !isValidHexDig(*hexDig2)) {
+ outDecStr.Append(PERCENT_SIGN);
+ cur++;
+ continue;
+ }
+
+ // decode "% hexDig1 hexDig2" into a character.
+ char16_t decChar =
+ (local::convertHexDig(*hexDig1) << 4) + local::convertHexDig(*hexDig2);
+ outDecStr.Append(decChar);
+
+ // increment 'cur' to after the second hexDig
+ cur = ++hexDig2;
+ }
+}
+
+// The Content Security Policy should be inherited for
+// local schemes like: "about", "blob", "data", or "filesystem".
+// see: https://w3c.github.io/webappsec-csp/#initialize-document-csp
+bool CSP_ShouldResponseInheritCSP(nsIChannel* aChannel) {
+ if (!aChannel) {
+ return false;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ bool isAbout = uri->SchemeIs("about");
+ if (isAbout) {
+ nsAutoCString aboutSpec;
+ rv = uri->GetSpec(aboutSpec);
+ NS_ENSURE_SUCCESS(rv, false);
+ // also allow about:blank#foo
+ if (StringBeginsWith(aboutSpec, "about:blank"_ns) ||
+ StringBeginsWith(aboutSpec, "about:srcdoc"_ns)) {
+ return true;
+ }
+ }
+
+ return uri->SchemeIs("blob") || uri->SchemeIs("data") ||
+ uri->SchemeIs("filesystem") || uri->SchemeIs("javascript");
+}
+
+void CSP_ApplyMetaCSPToDoc(mozilla::dom::Document& aDoc,
+ const nsAString& aPolicyStr) {
+ if (aDoc.IsLoadedAsData()) {
+ return;
+ }
+
+ nsAutoString policyStr(
+ nsContentUtils::TrimWhitespace<nsContentUtils::IsHTMLWhitespace>(
+ aPolicyStr));
+
+ if (policyStr.IsEmpty()) {
+ return;
+ }
+
+ nsCOMPtr<nsIContentSecurityPolicy> csp = aDoc.GetCsp();
+ if (!csp) {
+ MOZ_ASSERT(false, "how come there is no CSP");
+ return;
+ }
+
+ // Multiple CSPs (delivered through either header of meta tag) need to
+ // be joined together, see:
+ // https://w3c.github.io/webappsec/specs/content-security-policy/#delivery-html-meta-element
+ nsresult rv =
+ csp->AppendPolicy(policyStr,
+ false, // csp via meta tag can not be report only
+ true); // delivered through the meta tag
+ NS_ENSURE_SUCCESS_VOID(rv);
+ if (nsPIDOMWindowInner* inner = aDoc.GetInnerWindow()) {
+ inner->SetCsp(csp);
+ }
+ aDoc.ApplySettingsFromCSP(false);
+}
+
+void CSP_GetLocalizedStr(const char* aName, const nsTArray<nsString>& aParams,
+ nsAString& outResult) {
+ nsCOMPtr<nsIStringBundle> keyStringBundle;
+ nsCOMPtr<nsIStringBundleService> stringBundleService =
+ mozilla::components::StringBundle::Service();
+
+ NS_ASSERTION(stringBundleService, "String bundle service must be present!");
+ stringBundleService->CreateBundle(
+ "chrome://global/locale/security/csp.properties",
+ getter_AddRefs(keyStringBundle));
+
+ NS_ASSERTION(keyStringBundle, "Key string bundle must be available!");
+
+ if (!keyStringBundle) {
+ return;
+ }
+ keyStringBundle->FormatStringFromName(aName, aParams, outResult);
+}
+
+void CSP_LogStrMessage(const nsAString& aMsg) {
+ nsCOMPtr<nsIConsoleService> console(
+ do_GetService("@mozilla.org/consoleservice;1"));
+
+ if (!console) {
+ return;
+ }
+ nsString msg(aMsg);
+ console->LogStringMessage(msg.get());
+}
+
+void CSP_LogMessage(const nsAString& aMessage, const nsAString& aSourceName,
+ const nsAString& aSourceLine, uint32_t aLineNumber,
+ uint32_t aColumnNumber, uint32_t aFlags,
+ const nsACString& aCategory, uint64_t aInnerWindowID,
+ bool aFromPrivateWindow) {
+ nsCOMPtr<nsIConsoleService> console(
+ do_GetService(NS_CONSOLESERVICE_CONTRACTID));
+
+ nsCOMPtr<nsIScriptError> error(do_CreateInstance(NS_SCRIPTERROR_CONTRACTID));
+
+ if (!console || !error) {
+ return;
+ }
+
+ // Prepending CSP to the outgoing console message
+ nsString cspMsg;
+ CSP_GetLocalizedStr("CSPMessagePrefix",
+ AutoTArray<nsString, 1>{nsString(aMessage)}, cspMsg);
+
+ // Currently 'aSourceLine' is not logged to the console, because similar
+ // information is already included within the source link of the message.
+ // For inline violations however, the line and column number are 0 and
+ // information contained within 'aSourceLine' can be really useful for devs.
+ // E.g. 'aSourceLine' might be: 'onclick attribute on DIV element'.
+ // In such cases we append 'aSourceLine' directly to the error message.
+ if (!aSourceLine.IsEmpty() && aLineNumber == 0) {
+ cspMsg.AppendLiteral(u"\nSource: ");
+ cspMsg.Append(aSourceLine);
+ }
+
+ // Since we are leveraging csp errors as the category names which
+ // we pass to devtools, we should prepend them with "CSP_" to
+ // allow easy distincution in devtools code. e.g.
+ // upgradeInsecureRequest -> CSP_upgradeInsecureRequest
+ nsCString category("CSP_");
+ category.Append(aCategory);
+
+ nsresult rv;
+ if (aInnerWindowID > 0) {
+ rv = error->InitWithWindowID(cspMsg, aSourceName, aSourceLine, aLineNumber,
+ aColumnNumber, aFlags, category,
+ aInnerWindowID);
+ } else {
+ rv = error->Init(cspMsg, aSourceName, aSourceLine, aLineNumber,
+ aColumnNumber, aFlags, category, aFromPrivateWindow,
+ true /* from chrome context */);
+ }
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ console->LogMessage(error);
+}
+
+/**
+ * Combines CSP_LogMessage and CSP_GetLocalizedStr into one call.
+ */
+void CSP_LogLocalizedStr(const char* aName, const nsTArray<nsString>& aParams,
+ const nsAString& aSourceName,
+ const nsAString& aSourceLine, uint32_t aLineNumber,
+ uint32_t aColumnNumber, uint32_t aFlags,
+ const nsACString& aCategory, uint64_t aInnerWindowID,
+ bool aFromPrivateWindow) {
+ nsAutoString logMsg;
+ CSP_GetLocalizedStr(aName, aParams, logMsg);
+ CSP_LogMessage(logMsg, aSourceName, aSourceLine, aLineNumber, aColumnNumber,
+ aFlags, aCategory, aInnerWindowID, aFromPrivateWindow);
+}
+
+/* ===== Helpers ============================ */
+// This implements
+// https://w3c.github.io/webappsec-csp/#effective-directive-for-a-request.
+// However the spec doesn't currently cover all request destinations, which
+// we roughly represent using nsContentPolicyType.
+CSPDirective CSP_ContentTypeToDirective(nsContentPolicyType aType) {
+ switch (aType) {
+ case nsIContentPolicy::TYPE_IMAGE:
+ case nsIContentPolicy::TYPE_IMAGESET:
+ case nsIContentPolicy::TYPE_INTERNAL_IMAGE:
+ case nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD:
+ case nsIContentPolicy::TYPE_INTERNAL_IMAGE_FAVICON:
+ return nsIContentSecurityPolicy::IMG_SRC_DIRECTIVE;
+
+ // BLock XSLT as script, see bug 910139
+ case nsIContentPolicy::TYPE_XSLT:
+ case nsIContentPolicy::TYPE_SCRIPT:
+ case nsIContentPolicy::TYPE_INTERNAL_SCRIPT:
+ case nsIContentPolicy::TYPE_INTERNAL_SCRIPT_PRELOAD:
+ case nsIContentPolicy::TYPE_INTERNAL_MODULE:
+ case nsIContentPolicy::TYPE_INTERNAL_MODULE_PRELOAD:
+ case nsIContentPolicy::TYPE_INTERNAL_WORKER_IMPORT_SCRIPTS:
+ case nsIContentPolicy::TYPE_INTERNAL_AUDIOWORKLET:
+ case nsIContentPolicy::TYPE_INTERNAL_PAINTWORKLET:
+ case nsIContentPolicy::TYPE_INTERNAL_CHROMEUTILS_COMPILED_SCRIPT:
+ case nsIContentPolicy::TYPE_INTERNAL_FRAME_MESSAGEMANAGER_SCRIPT:
+ // (https://github.com/w3c/webappsec-csp/issues/554)
+ // Some of these types are not explicitly defined in the spec.
+ //
+ // Chrome seems to use script-src-elem for worklet!
+ return nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE;
+
+ case nsIContentPolicy::TYPE_STYLESHEET:
+ case nsIContentPolicy::TYPE_INTERNAL_STYLESHEET:
+ case nsIContentPolicy::TYPE_INTERNAL_STYLESHEET_PRELOAD:
+ return nsIContentSecurityPolicy::STYLE_SRC_ELEM_DIRECTIVE;
+
+ case nsIContentPolicy::TYPE_FONT:
+ case nsIContentPolicy::TYPE_INTERNAL_FONT_PRELOAD:
+ return nsIContentSecurityPolicy::FONT_SRC_DIRECTIVE;
+
+ case nsIContentPolicy::TYPE_MEDIA:
+ case nsIContentPolicy::TYPE_INTERNAL_AUDIO:
+ case nsIContentPolicy::TYPE_INTERNAL_VIDEO:
+ case nsIContentPolicy::TYPE_INTERNAL_TRACK:
+ return nsIContentSecurityPolicy::MEDIA_SRC_DIRECTIVE;
+
+ case nsIContentPolicy::TYPE_WEB_MANIFEST:
+ return nsIContentSecurityPolicy::WEB_MANIFEST_SRC_DIRECTIVE;
+
+ case nsIContentPolicy::TYPE_INTERNAL_WORKER:
+ case nsIContentPolicy::TYPE_INTERNAL_WORKER_STATIC_MODULE:
+ case nsIContentPolicy::TYPE_INTERNAL_SHARED_WORKER:
+ case nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER:
+ return nsIContentSecurityPolicy::WORKER_SRC_DIRECTIVE;
+
+ case nsIContentPolicy::TYPE_SUBDOCUMENT:
+ case nsIContentPolicy::TYPE_INTERNAL_FRAME:
+ case nsIContentPolicy::TYPE_INTERNAL_IFRAME:
+ return nsIContentSecurityPolicy::FRAME_SRC_DIRECTIVE;
+
+ case nsIContentPolicy::TYPE_WEBSOCKET:
+ case nsIContentPolicy::TYPE_XMLHTTPREQUEST:
+ case nsIContentPolicy::TYPE_BEACON:
+ case nsIContentPolicy::TYPE_PING:
+ case nsIContentPolicy::TYPE_FETCH:
+ case nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST:
+ case nsIContentPolicy::TYPE_INTERNAL_EVENTSOURCE:
+ case nsIContentPolicy::TYPE_INTERNAL_FETCH_PRELOAD:
+ case nsIContentPolicy::TYPE_WEB_IDENTITY:
+ case nsIContentPolicy::TYPE_WEB_TRANSPORT:
+ return nsIContentSecurityPolicy::CONNECT_SRC_DIRECTIVE;
+
+ case nsIContentPolicy::TYPE_OBJECT:
+ case nsIContentPolicy::TYPE_OBJECT_SUBREQUEST:
+ case nsIContentPolicy::TYPE_INTERNAL_EMBED:
+ case nsIContentPolicy::TYPE_INTERNAL_OBJECT:
+ return nsIContentSecurityPolicy::OBJECT_SRC_DIRECTIVE;
+
+ case nsIContentPolicy::TYPE_DTD:
+ case nsIContentPolicy::TYPE_OTHER:
+ case nsIContentPolicy::TYPE_SPECULATIVE:
+ case nsIContentPolicy::TYPE_INTERNAL_DTD:
+ case nsIContentPolicy::TYPE_INTERNAL_FORCE_ALLOWED_DTD:
+ return nsIContentSecurityPolicy::DEFAULT_SRC_DIRECTIVE;
+
+ // CSP does not apply to webrtc connections
+ case nsIContentPolicy::TYPE_PROXIED_WEBRTC_MEDIA:
+ // csp shold not block top level loads, e.g. in case
+ // of a redirect.
+ case nsIContentPolicy::TYPE_DOCUMENT:
+ // CSP can not block csp reports
+ case nsIContentPolicy::TYPE_CSP_REPORT:
+ return nsIContentSecurityPolicy::NO_DIRECTIVE;
+
+ case nsIContentPolicy::TYPE_SAVEAS_DOWNLOAD:
+ case nsIContentPolicy::TYPE_UA_FONT:
+ return nsIContentSecurityPolicy::NO_DIRECTIVE;
+
+ // Fall through to error for all other directives
+ case nsIContentPolicy::TYPE_INVALID:
+ case nsIContentPolicy::TYPE_END:
+ MOZ_ASSERT(false, "Can not map nsContentPolicyType to CSPDirective");
+ // Do not add default: so that compilers can catch the missing case.
+ }
+ return nsIContentSecurityPolicy::DEFAULT_SRC_DIRECTIVE;
+}
+
+nsCSPHostSrc* CSP_CreateHostSrcFromSelfURI(nsIURI* aSelfURI) {
+ // Create the host first
+ nsCString host;
+ aSelfURI->GetAsciiHost(host);
+ nsCSPHostSrc* hostsrc = new nsCSPHostSrc(NS_ConvertUTF8toUTF16(host));
+ hostsrc->setGeneratedFromSelfKeyword();
+
+ // Add the scheme.
+ nsCString scheme;
+ aSelfURI->GetScheme(scheme);
+ hostsrc->setScheme(NS_ConvertUTF8toUTF16(scheme));
+
+ // An empty host (e.g. for data:) indicates it's effectively a unique origin.
+ // Please note that we still need to set the scheme on hostsrc (see above),
+ // because it's used for reporting.
+ if (host.EqualsLiteral("")) {
+ hostsrc->setIsUniqueOrigin();
+ // no need to query the port in that case.
+ return hostsrc;
+ }
+
+ int32_t port;
+ aSelfURI->GetPort(&port);
+ // Only add port if it's not default port.
+ if (port > 0) {
+ nsAutoString portStr;
+ portStr.AppendInt(port);
+ hostsrc->setPort(portStr);
+ }
+ return hostsrc;
+}
+
+bool CSP_IsEmptyDirective(const nsAString& aValue, const nsAString& aDir) {
+ return (aDir.Length() == 0 && aValue.Length() == 0);
+}
+bool CSP_IsDirective(const nsAString& aValue, CSPDirective aDir) {
+ return aValue.LowerCaseEqualsASCII(CSP_CSPDirectiveToString(aDir));
+}
+
+bool CSP_IsKeyword(const nsAString& aValue, enum CSPKeyword aKey) {
+ return aValue.LowerCaseEqualsASCII(CSP_EnumToUTF8Keyword(aKey));
+}
+
+bool CSP_IsQuotelessKeyword(const nsAString& aKey) {
+ nsString lowerKey;
+ ToLowerCase(aKey, lowerKey);
+
+ nsAutoString keyword;
+ for (uint32_t i = 0; i < CSP_LAST_KEYWORD_VALUE; i++) {
+ // skipping the leading ' and trimming the trailing '
+ keyword.AssignASCII(gCSPUTF8Keywords[i] + 1);
+ keyword.Trim("'", false, true);
+ if (lowerKey.Equals(keyword)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/*
+ * Checks whether the current directive permits a specific
+ * scheme. This function is called from nsCSPSchemeSrc() and
+ * also nsCSPHostSrc.
+ * @param aEnforcementScheme
+ * The scheme that this directive allows
+ * @param aUri
+ * The uri of the subresource load.
+ * @param aReportOnly
+ * Whether the enforced policy is report only or not.
+ * @param aUpgradeInsecure
+ * Whether the policy makes use of the directive
+ * 'upgrade-insecure-requests'.
+ * @param aFromSelfURI
+ * Whether a scheme was generated from the keyword 'self'
+ * which then allows schemeless sources to match ws and wss.
+ */
+
+bool permitsScheme(const nsAString& aEnforcementScheme, nsIURI* aUri,
+ bool aReportOnly, bool aUpgradeInsecure, bool aFromSelfURI) {
+ nsAutoCString scheme;
+ nsresult rv = aUri->GetScheme(scheme);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ // no scheme to enforce, let's allow the load (e.g. script-src *)
+ if (aEnforcementScheme.IsEmpty()) {
+ return true;
+ }
+
+ // if the scheme matches, all good - allow the load
+ if (aEnforcementScheme.EqualsASCII(scheme.get())) {
+ return true;
+ }
+
+ // allow scheme-less sources where the protected resource is http
+ // and the load is https, see:
+ // http://www.w3.org/TR/CSP2/#match-source-expression
+ if (aEnforcementScheme.EqualsASCII("http")) {
+ if (scheme.EqualsASCII("https")) {
+ return true;
+ }
+ if ((scheme.EqualsASCII("ws") || scheme.EqualsASCII("wss")) &&
+ aFromSelfURI) {
+ return true;
+ }
+ }
+ if (aEnforcementScheme.EqualsASCII("https")) {
+ if (scheme.EqualsLiteral("wss") && aFromSelfURI) {
+ return true;
+ }
+ }
+ if (aEnforcementScheme.EqualsASCII("ws") && scheme.EqualsASCII("wss")) {
+ return true;
+ }
+
+ // Allow the load when enforcing upgrade-insecure-requests with the
+ // promise the request gets upgraded from http to https and ws to wss.
+ // See nsHttpChannel::Connect() and also WebSocket.cpp. Please note,
+ // the report only policies should not allow the load and report
+ // the error back to the page.
+ return (
+ (aUpgradeInsecure && !aReportOnly) &&
+ ((scheme.EqualsASCII("http") &&
+ aEnforcementScheme.EqualsASCII("https")) ||
+ (scheme.EqualsASCII("ws") && aEnforcementScheme.EqualsASCII("wss"))));
+}
+
+/*
+ * A helper function for appending a CSP header to an existing CSP
+ * policy.
+ *
+ * @param aCsp the CSP policy
+ * @param aHeaderValue the header
+ * @param aReportOnly is this a report-only header?
+ */
+
+nsresult CSP_AppendCSPFromHeader(nsIContentSecurityPolicy* aCsp,
+ const nsAString& aHeaderValue,
+ bool aReportOnly) {
+ NS_ENSURE_ARG(aCsp);
+
+ // Need to tokenize the header value since multiple headers could be
+ // concatenated into one comma-separated list of policies.
+ // See RFC2616 section 4.2 (last paragraph)
+ nsresult rv = NS_OK;
+ for (const nsAString& policy :
+ nsCharSeparatedTokenizer(aHeaderValue, ',').ToRange()) {
+ rv = aCsp->AppendPolicy(policy, aReportOnly, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ {
+ CSPUTILSLOG(("CSP refined with policy: \"%s\"",
+ NS_ConvertUTF16toUTF8(policy).get()));
+ }
+ }
+ return NS_OK;
+}
+
+/* ===== nsCSPSrc ============================ */
+
+nsCSPBaseSrc::nsCSPBaseSrc() {}
+
+nsCSPBaseSrc::~nsCSPBaseSrc() = default;
+
+// ::permits is only called for external load requests, therefore:
+// nsCSPKeywordSrc and nsCSPHashSource fall back to this base class
+// implementation which will never allow the load.
+bool nsCSPBaseSrc::permits(nsIURI* aUri, bool aWasRedirected, bool aReportOnly,
+ bool aUpgradeInsecure) const {
+ if (CSPUTILSLOGENABLED()) {
+ CSPUTILSLOG(
+ ("nsCSPBaseSrc::permits, aUri: %s", aUri->GetSpecOrDefault().get()));
+ }
+ return false;
+}
+
+// ::allows is only called for inlined loads, therefore:
+// nsCSPSchemeSrc, nsCSPHostSrc fall back
+// to this base class implementation which will never allow the load.
+bool nsCSPBaseSrc::allows(enum CSPKeyword aKeyword,
+ const nsAString& aHashOrNonce) const {
+ CSPUTILSLOG(("nsCSPBaseSrc::allows, aKeyWord: %s, a HashOrNonce: %s",
+ aKeyword == CSP_HASH ? "hash" : CSP_EnumToUTF8Keyword(aKeyword),
+ NS_ConvertUTF16toUTF8(aHashOrNonce).get()));
+ return false;
+}
+
+/* ====== nsCSPSchemeSrc ===================== */
+
+nsCSPSchemeSrc::nsCSPSchemeSrc(const nsAString& aScheme) : mScheme(aScheme) {
+ ToLowerCase(mScheme);
+}
+
+nsCSPSchemeSrc::~nsCSPSchemeSrc() = default;
+
+bool nsCSPSchemeSrc::permits(nsIURI* aUri, bool aWasRedirected,
+ bool aReportOnly, bool aUpgradeInsecure) const {
+ if (CSPUTILSLOGENABLED()) {
+ CSPUTILSLOG(
+ ("nsCSPSchemeSrc::permits, aUri: %s", aUri->GetSpecOrDefault().get()));
+ }
+ MOZ_ASSERT((!mScheme.EqualsASCII("")), "scheme can not be the empty string");
+ return permitsScheme(mScheme, aUri, aReportOnly, aUpgradeInsecure, false);
+}
+
+bool nsCSPSchemeSrc::visit(nsCSPSrcVisitor* aVisitor) const {
+ return aVisitor->visitSchemeSrc(*this);
+}
+
+void nsCSPSchemeSrc::toString(nsAString& outStr) const {
+ outStr.Append(mScheme);
+ outStr.AppendLiteral(":");
+}
+
+/* ===== nsCSPHostSrc ======================== */
+
+nsCSPHostSrc::nsCSPHostSrc(const nsAString& aHost)
+ : mHost(aHost),
+ mGeneratedFromSelfKeyword(false),
+ mIsUniqueOrigin(false),
+ mWithinFrameAncstorsDir(false) {
+ ToLowerCase(mHost);
+}
+
+nsCSPHostSrc::~nsCSPHostSrc() = default;
+
+/*
+ * Checks whether the current directive permits a specific port.
+ * @param aEnforcementScheme
+ * The scheme that this directive allows
+ * (used to query the default port for that scheme)
+ * @param aEnforcementPort
+ * The port that this directive allows
+ * @param aResourceURI
+ * The uri of the subresource load
+ */
+bool permitsPort(const nsAString& aEnforcementScheme,
+ const nsAString& aEnforcementPort, nsIURI* aResourceURI) {
+ // If enforcement port is the wildcard, don't block the load.
+ if (aEnforcementPort.EqualsASCII("*")) {
+ return true;
+ }
+
+ int32_t resourcePort;
+ nsresult rv = aResourceURI->GetPort(&resourcePort);
+ if (NS_FAILED(rv) && aEnforcementPort.IsEmpty()) {
+ // If we cannot get a Port (e.g. because of an Custom Protocol handler)
+ // We need to check if a default port is associated with the Scheme
+ if (aEnforcementScheme.IsEmpty()) {
+ return false;
+ }
+ int defaultPortforScheme =
+ NS_GetDefaultPort(NS_ConvertUTF16toUTF8(aEnforcementScheme).get());
+
+ // If there is no default port associated with the Scheme (
+ // defaultPortforScheme == -1) or it is an externally handled protocol (
+ // defaultPortforScheme == 0 ) and the csp does not enforce a port - we can
+ // allow not having a port
+ return (defaultPortforScheme == -1 || defaultPortforScheme == -0);
+ }
+ // Avoid unnecessary string creation/manipulation and don't block the
+ // load if the resource to be loaded uses the default port for that
+ // scheme and there is no port to be enforced.
+ // Note, this optimization relies on scheme checks within permitsScheme().
+ if (resourcePort == DEFAULT_PORT && aEnforcementPort.IsEmpty()) {
+ return true;
+ }
+
+ // By now we know at that either the resourcePort does not use the default
+ // port or there is a port restriction to be enforced. A port value of -1
+ // corresponds to the protocol's default port (eg. -1 implies port 80 for
+ // http URIs), in such a case we have to query the default port of the
+ // resource to be loaded.
+ if (resourcePort == DEFAULT_PORT) {
+ nsAutoCString resourceScheme;
+ rv = aResourceURI->GetScheme(resourceScheme);
+ NS_ENSURE_SUCCESS(rv, false);
+ resourcePort = NS_GetDefaultPort(resourceScheme.get());
+ }
+
+ // If there is a port to be enforced and the ports match, then
+ // don't block the load.
+ nsString resourcePortStr;
+ resourcePortStr.AppendInt(resourcePort);
+ if (aEnforcementPort.Equals(resourcePortStr)) {
+ return true;
+ }
+
+ // If there is no port to be enforced, query the default port for the load.
+ nsString enforcementPort(aEnforcementPort);
+ if (enforcementPort.IsEmpty()) {
+ // For scheme less sources, our parser always generates a scheme
+ // which is the scheme of the protected resource.
+ MOZ_ASSERT(!aEnforcementScheme.IsEmpty(),
+ "need a scheme to query default port");
+ int32_t defaultEnforcementPort =
+ NS_GetDefaultPort(NS_ConvertUTF16toUTF8(aEnforcementScheme).get());
+ enforcementPort.Truncate();
+ enforcementPort.AppendInt(defaultEnforcementPort);
+ }
+
+ // If default ports match, don't block the load
+ if (enforcementPort.Equals(resourcePortStr)) {
+ return true;
+ }
+
+ // Additional port matching where the regular URL matching algorithm
+ // treats insecure ports as matching their secure variants.
+ // default port for http is :80
+ // default port for https is :443
+ if (enforcementPort.EqualsLiteral("80") &&
+ resourcePortStr.EqualsLiteral("443")) {
+ return true;
+ }
+
+ // ports do not match, block the load.
+ return false;
+}
+
+bool nsCSPHostSrc::permits(nsIURI* aUri, bool aWasRedirected, bool aReportOnly,
+ bool aUpgradeInsecure) const {
+ if (CSPUTILSLOGENABLED()) {
+ CSPUTILSLOG(
+ ("nsCSPHostSrc::permits, aUri: %s", aUri->GetSpecOrDefault().get()));
+ }
+
+ if (mIsUniqueOrigin) {
+ return false;
+ }
+
+ // we are following the enforcement rules from the spec, see:
+ // http://www.w3.org/TR/CSP11/#match-source-expression
+
+ // 4.3) scheme matching: Check if the scheme matches.
+ if (!permitsScheme(mScheme, aUri, aReportOnly, aUpgradeInsecure,
+ mGeneratedFromSelfKeyword)) {
+ return false;
+ }
+
+ // The host in nsCSpHostSrc should never be empty. In case we are enforcing
+ // just a specific scheme, the parser should generate a nsCSPSchemeSource.
+ NS_ASSERTION((!mHost.IsEmpty()), "host can not be the empty string");
+
+ // Before we can check if the host matches, we have to
+ // extract the host part from aUri.
+ nsAutoCString uriHost;
+ nsresult rv = aUri->GetAsciiHost(uriHost);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ nsString decodedUriHost;
+ CSP_PercentDecodeStr(NS_ConvertUTF8toUTF16(uriHost), decodedUriHost);
+
+ // 2) host matching: Enforce a single *
+ if (mHost.EqualsASCII("*")) {
+ // The single ASTERISK character (*) does not match a URI's scheme of a type
+ // designating a globally unique identifier (such as blob:, data:, or
+ // filesystem:) At the moment firefox does not support filesystem; but for
+ // future compatibility we support it in CSP according to the spec,
+ // see: 4.2.2 Matching Source Expressions Note, that allowlisting any of
+ // these schemes would call nsCSPSchemeSrc::permits().
+ if (aUri->SchemeIs("blob") || aUri->SchemeIs("data") ||
+ aUri->SchemeIs("filesystem")) {
+ return false;
+ }
+
+ // If no scheme is present there also wont be a port and folder to check
+ // which means we can return early
+ if (mScheme.IsEmpty()) {
+ return true;
+ }
+ }
+ // 4.5) host matching: Check if the allowed host starts with a wilcard.
+ else if (mHost.First() == '*') {
+ NS_ASSERTION(
+ mHost[1] == '.',
+ "Second character needs to be '.' whenever host starts with '*'");
+
+ // Eliminate leading "*", but keeping the FULL STOP (.) thereafter before
+ // checking if the remaining characters match
+ nsString wildCardHost = mHost;
+ wildCardHost = Substring(wildCardHost, 1, wildCardHost.Length() - 1);
+ if (!StringEndsWith(decodedUriHost, wildCardHost)) {
+ return false;
+ }
+ }
+ // 4.6) host matching: Check if hosts match.
+ else if (!mHost.Equals(decodedUriHost)) {
+ return false;
+ }
+
+ // Port matching: Check if the ports match.
+ if (!permitsPort(mScheme, mPort, aUri)) {
+ return false;
+ }
+
+ // 4.9) Path matching: If there is a path, we have to enforce
+ // path-level matching, unless the channel got redirected, see:
+ // http://www.w3.org/TR/CSP11/#source-list-paths-and-redirects
+ if (!aWasRedirected && !mPath.IsEmpty()) {
+ // converting aUri into nsIURL so we can strip query and ref
+ // example.com/test#foo -> example.com/test
+ // example.com/test?val=foo -> example.com/test
+ nsCOMPtr<nsIURL> url = do_QueryInterface(aUri);
+ if (!url) {
+ NS_ASSERTION(false, "can't QI into nsIURI");
+ return false;
+ }
+ nsAutoCString uriPath;
+ rv = url->GetFilePath(uriPath);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ if (mWithinFrameAncstorsDir) {
+ // no path matching for frame-ancestors to not leak any path information.
+ return true;
+ }
+
+ nsString decodedUriPath;
+ CSP_PercentDecodeStr(NS_ConvertUTF8toUTF16(uriPath), decodedUriPath);
+
+ // check if the last character of mPath is '/'; if so
+ // we just have to check loading resource is within
+ // the allowed path.
+ if (mPath.Last() == '/') {
+ if (!StringBeginsWith(decodedUriPath, mPath)) {
+ return false;
+ }
+ }
+ // otherwise mPath refers to a specific file, and we have to
+ // check if the loading resource matches the file.
+ else {
+ if (!mPath.Equals(decodedUriPath)) {
+ return false;
+ }
+ }
+ }
+
+ // At the end: scheme, host, port and path match -> allow the load.
+ return true;
+}
+
+bool nsCSPHostSrc::visit(nsCSPSrcVisitor* aVisitor) const {
+ return aVisitor->visitHostSrc(*this);
+}
+
+void nsCSPHostSrc::toString(nsAString& outStr) const {
+ if (mGeneratedFromSelfKeyword) {
+ outStr.AppendLiteral("'self'");
+ return;
+ }
+
+ // If mHost is a single "*", we append the wildcard and return.
+ if (mHost.EqualsASCII("*") && mScheme.IsEmpty() && mPort.IsEmpty()) {
+ outStr.Append(mHost);
+ return;
+ }
+
+ // append scheme
+ outStr.Append(mScheme);
+
+ // append host
+ outStr.AppendLiteral("://");
+ outStr.Append(mHost);
+
+ // append port
+ if (!mPort.IsEmpty()) {
+ outStr.AppendLiteral(":");
+ outStr.Append(mPort);
+ }
+
+ // append path
+ outStr.Append(mPath);
+}
+
+void nsCSPHostSrc::setScheme(const nsAString& aScheme) {
+ mScheme = aScheme;
+ ToLowerCase(mScheme);
+}
+
+void nsCSPHostSrc::setPort(const nsAString& aPort) { mPort = aPort; }
+
+void nsCSPHostSrc::appendPath(const nsAString& aPath) { mPath.Append(aPath); }
+
+/* ===== nsCSPKeywordSrc ===================== */
+
+nsCSPKeywordSrc::nsCSPKeywordSrc(enum CSPKeyword aKeyword)
+ : mKeyword(aKeyword) {
+ NS_ASSERTION((aKeyword != CSP_SELF),
+ "'self' should have been replaced in the parser");
+}
+
+nsCSPKeywordSrc::~nsCSPKeywordSrc() = default;
+
+bool nsCSPKeywordSrc::allows(enum CSPKeyword aKeyword,
+ const nsAString& aHashOrNonce) const {
+ CSPUTILSLOG(("nsCSPKeywordSrc::allows, aKeyWord: %s, aHashOrNonce: %s",
+ CSP_EnumToUTF8Keyword(aKeyword),
+ NS_ConvertUTF16toUTF8(aHashOrNonce).get()));
+ return mKeyword == aKeyword;
+}
+
+bool nsCSPKeywordSrc::visit(nsCSPSrcVisitor* aVisitor) const {
+ return aVisitor->visitKeywordSrc(*this);
+}
+
+void nsCSPKeywordSrc::toString(nsAString& outStr) const {
+ outStr.Append(CSP_EnumToUTF16Keyword(mKeyword));
+}
+
+/* ===== nsCSPNonceSrc ==================== */
+
+nsCSPNonceSrc::nsCSPNonceSrc(const nsAString& aNonce) : mNonce(aNonce) {}
+
+nsCSPNonceSrc::~nsCSPNonceSrc() = default;
+
+bool nsCSPNonceSrc::allows(enum CSPKeyword aKeyword,
+ const nsAString& aHashOrNonce) const {
+ CSPUTILSLOG(("nsCSPNonceSrc::allows, aKeyWord: %s, a HashOrNonce: %s",
+ CSP_EnumToUTF8Keyword(aKeyword),
+ NS_ConvertUTF16toUTF8(aHashOrNonce).get()));
+
+ if (aKeyword != CSP_NONCE) {
+ return false;
+ }
+ // nonces can not be invalidated by strict-dynamic
+ return mNonce.Equals(aHashOrNonce);
+}
+
+bool nsCSPNonceSrc::visit(nsCSPSrcVisitor* aVisitor) const {
+ return aVisitor->visitNonceSrc(*this);
+}
+
+void nsCSPNonceSrc::toString(nsAString& outStr) const {
+ outStr.Append(CSP_EnumToUTF16Keyword(CSP_NONCE));
+ outStr.Append(mNonce);
+ outStr.AppendLiteral("'");
+}
+
+/* ===== nsCSPHashSrc ===================== */
+
+nsCSPHashSrc::nsCSPHashSrc(const nsAString& aAlgo, const nsAString& aHash)
+ : mAlgorithm(aAlgo), mHash(aHash) {
+ // Only the algo should be rewritten to lowercase, the hash must remain the
+ // same.
+ ToLowerCase(mAlgorithm);
+ // Normalize the base64url encoding to base64 encoding:
+ char16_t* cur = mHash.BeginWriting();
+ char16_t* end = mHash.EndWriting();
+
+ for (; cur < end; ++cur) {
+ if (char16_t('-') == *cur) {
+ *cur = char16_t('+');
+ }
+ if (char16_t('_') == *cur) {
+ *cur = char16_t('/');
+ }
+ }
+}
+
+nsCSPHashSrc::~nsCSPHashSrc() = default;
+
+bool nsCSPHashSrc::allows(enum CSPKeyword aKeyword,
+ const nsAString& aHashOrNonce) const {
+ CSPUTILSLOG(("nsCSPHashSrc::allows, aKeyWord: %s, a HashOrNonce: %s",
+ CSP_EnumToUTF8Keyword(aKeyword),
+ NS_ConvertUTF16toUTF8(aHashOrNonce).get()));
+
+ if (aKeyword != CSP_HASH) {
+ return false;
+ }
+
+ // hashes can not be invalidated by strict-dynamic
+
+ // Convert aHashOrNonce to UTF-8
+ NS_ConvertUTF16toUTF8 utf8_hash(aHashOrNonce);
+
+ nsCOMPtr<nsICryptoHash> hasher;
+ nsresult rv = NS_NewCryptoHash(NS_ConvertUTF16toUTF8(mAlgorithm),
+ getter_AddRefs(hasher));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ rv = hasher->Update((uint8_t*)utf8_hash.get(), utf8_hash.Length());
+ NS_ENSURE_SUCCESS(rv, false);
+
+ nsAutoCString hash;
+ rv = hasher->Finish(true, hash);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ return NS_ConvertUTF16toUTF8(mHash).Equals(hash);
+}
+
+bool nsCSPHashSrc::visit(nsCSPSrcVisitor* aVisitor) const {
+ return aVisitor->visitHashSrc(*this);
+}
+
+void nsCSPHashSrc::toString(nsAString& outStr) const {
+ outStr.AppendLiteral("'");
+ outStr.Append(mAlgorithm);
+ outStr.AppendLiteral("-");
+ outStr.Append(mHash);
+ outStr.AppendLiteral("'");
+}
+
+/* ===== nsCSPReportURI ===================== */
+
+nsCSPReportURI::nsCSPReportURI(nsIURI* aURI) : mReportURI(aURI) {}
+
+nsCSPReportURI::~nsCSPReportURI() = default;
+
+bool nsCSPReportURI::visit(nsCSPSrcVisitor* aVisitor) const { return false; }
+
+void nsCSPReportURI::toString(nsAString& outStr) const {
+ nsAutoCString spec;
+ nsresult rv = mReportURI->GetSpec(spec);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ outStr.AppendASCII(spec.get());
+}
+
+/* ===== nsCSPSandboxFlags ===================== */
+
+nsCSPSandboxFlags::nsCSPSandboxFlags(const nsAString& aFlags) : mFlags(aFlags) {
+ ToLowerCase(mFlags);
+}
+
+nsCSPSandboxFlags::~nsCSPSandboxFlags() = default;
+
+bool nsCSPSandboxFlags::visit(nsCSPSrcVisitor* aVisitor) const { return false; }
+
+void nsCSPSandboxFlags::toString(nsAString& outStr) const {
+ outStr.Append(mFlags);
+}
+
+/* ===== nsCSPDirective ====================== */
+
+nsCSPDirective::nsCSPDirective(CSPDirective aDirective) {
+ mDirective = aDirective;
+}
+
+nsCSPDirective::~nsCSPDirective() {
+ for (uint32_t i = 0; i < mSrcs.Length(); i++) {
+ delete mSrcs[i];
+ }
+}
+
+// https://w3c.github.io/webappsec-csp/#match-nonce-to-source-list
+static bool DoesNonceMatchSourceList(nsILoadInfo* aLoadInfo,
+ const nsTArray<nsCSPBaseSrc*>& aSrcs) {
+ // Step 1. Assert: source list is not null. (implicit)
+
+ // Note: For code-reuse we do "request’s cryptographic nonce metadata" here
+ // instead of the caller.
+ nsAutoString nonce;
+ MOZ_ALWAYS_SUCCEEDS(aLoadInfo->GetCspNonce(nonce));
+
+ // Step 2. If nonce is the empty string, return "Does Not Match".
+ if (nonce.IsEmpty()) {
+ return false;
+ }
+
+ // Step 3. For each expression of source list:
+ for (nsCSPBaseSrc* src : aSrcs) {
+ // Step 3.1. If expression matches the nonce-source grammar, and nonce is
+ // identical to expression’s base64-value part, return "Matches".
+ if (src->isNonce()) {
+ nsAutoString srcNonce;
+ static_cast<nsCSPNonceSrc*>(src)->getNonce(srcNonce);
+ if (srcNonce == nonce) {
+ return true;
+ }
+ }
+ }
+
+ // Step 4. Return "Does Not Match".
+ return false;
+}
+
+// https://www.w3.org/TR/SRI/#parse-metadata
+// This function is similar to SRICheck::IntegrityMetadata, but also keeps
+// SRI metadata with weaker hashes.
+// CSP treats "no metadata" and empty results the same way.
+static nsTArray<SRIMetadata> ParseSRIMetadata(const nsAString& aMetadata) {
+ // Step 1. Let result be the empty set.
+ // Step 2. Let empty be equal to true.
+ nsTArray<SRIMetadata> result;
+
+ NS_ConvertUTF16toUTF8 metadataList(aMetadata);
+ nsAutoCString token;
+
+ // Step 3. For each token returned by splitting metadata on spaces:
+ nsCWhitespaceTokenizer tokenizer(metadataList);
+ while (tokenizer.hasMoreTokens()) {
+ token = tokenizer.nextToken();
+ // Step 3.1. Set empty to false.
+ // Step 3.3. Parse token per the grammar in integrity metadata.
+ SRIMetadata metadata(token);
+ // Step 3.2. If token is not a valid metadata, skip the remaining steps, and
+ // proceed to the next token.
+ if (metadata.IsMalformed()) {
+ continue;
+ }
+
+ // Step 3.4. Let algorithm be the alg component of token.
+ // Step 3.5. If algorithm is a hash function recognized by the user agent,
+ // add the
+ // parsed token to result.
+ if (metadata.IsAlgorithmSupported()) {
+ result.AppendElement(metadata);
+ }
+ }
+
+ // Step 4. Return no metadata if empty is true, otherwise return result.
+ return result;
+}
+
+bool nsCSPDirective::permits(CSPDirective aDirective, nsILoadInfo* aLoadInfo,
+ nsIURI* aUri, bool aWasRedirected,
+ bool aReportOnly, bool aUpgradeInsecure) const {
+ MOZ_ASSERT(equals(aDirective) || isDefaultDirective());
+
+ if (CSPUTILSLOGENABLED()) {
+ CSPUTILSLOG(("nsCSPDirective::permits, aUri: %s, aDirective: %s",
+ aUri->GetSpecOrDefault().get(),
+ CSP_CSPDirectiveToString(aDirective)));
+ }
+
+ if (aLoadInfo) {
+ // https://w3c.github.io/webappsec-csp/#style-src-elem-pre-request
+ if (aDirective == CSPDirective::STYLE_SRC_ELEM_DIRECTIVE) {
+ // Step 3. If the result of executing §6.7.2.3 Does nonce match source
+ // list? on request’s cryptographic nonce metadata and this directive’s
+ // value is "Matches", return "Allowed".
+ if (DoesNonceMatchSourceList(aLoadInfo, mSrcs)) {
+ CSPUTILSLOG((" Allowed by matching nonce (style)"));
+ return true;
+ }
+ }
+
+ // https://w3c.github.io/webappsec-csp/#script-pre-request
+ // Step 1. If request’s destination is script-like:
+ else if (aDirective == CSPDirective::SCRIPT_SRC_ELEM_DIRECTIVE ||
+ aDirective == CSPDirective::WORKER_SRC_DIRECTIVE) {
+ // Step 1.1. If the result of executing §6.7.2.3 Does nonce match source
+ // list? on request’s cryptographic nonce metadata and this directive’s
+ // value is "Matches", return "Allowed".
+ if (DoesNonceMatchSourceList(aLoadInfo, mSrcs)) {
+ CSPUTILSLOG((" Allowed by matching nonce (script-like)"));
+ return true;
+ }
+
+ // Step 1.2. Let integrity expressions be the set of source expressions in
+ // directive’s value that match the hash-source grammar.
+ nsTArray<nsCSPHashSrc*> integrityExpressions;
+ bool hasStrictDynamicKeyword =
+ false; // Optimization to reduce number of iterations.
+ for (uint32_t i = 0; i < mSrcs.Length(); i++) {
+ if (mSrcs[i]->isHash()) {
+ integrityExpressions.AppendElement(
+ static_cast<nsCSPHashSrc*>(mSrcs[i]));
+ } else if (mSrcs[i]->isKeyword(CSP_STRICT_DYNAMIC)) {
+ hasStrictDynamicKeyword = true;
+ }
+ }
+
+ // Step 1.3. If integrity expressions is not empty:
+ if (!integrityExpressions.IsEmpty()) {
+ // Step 1.3.1. Let integrity sources be the result of executing the
+ // algorithm defined in [SRI 3.3.3 Parse metadata] on request’s
+ // integrity metadata.
+ nsAutoString integrityMetadata;
+ aLoadInfo->GetIntegrityMetadata(integrityMetadata);
+
+ nsTArray<SRIMetadata> integritySources =
+ ParseSRIMetadata(integrityMetadata);
+ MOZ_ASSERT(
+ integritySources.IsEmpty() == integrityMetadata.IsEmpty(),
+ "The integrity metadata should be only be empty, "
+ "when the parsed string was completely empty, otherwise it should "
+ "include at least one valid hash");
+
+ // Step 1.3.2. If integrity sources is "no metadata" or an empty set,
+ // skip the remaining substeps.
+ if (!integritySources.IsEmpty()) {
+ // Step 1.3.3. Let bypass due to integrity match be true.
+ bool bypass = true;
+
+ nsAutoCString sourceAlgorithmUTF8;
+ nsAutoCString sourceHashUTF8;
+ nsAutoString sourceAlgorithm;
+ nsAutoString sourceHash;
+ nsAutoString algorithm;
+ nsAutoString hash;
+
+ // Step 1.3.4. For each source of integrity sources:
+ for (const SRIMetadata& source : integritySources) {
+ source.GetAlgorithm(&sourceAlgorithmUTF8);
+ sourceAlgorithm = NS_ConvertUTF8toUTF16(sourceAlgorithmUTF8);
+ source.GetHash(0, &sourceHashUTF8);
+ sourceHash = NS_ConvertUTF8toUTF16(sourceHashUTF8);
+
+ // Step 1.3.4.1 If directive’s value does not contain a source
+ // expression whose hash-algorithm is an ASCII case-insensitive
+ // match for source’s hash-algorithm, and whose base64-value is
+ // identical to source’s base64-value, then set bypass due to
+ // integrity match to false.
+ bool found = false;
+ for (const nsCSPHashSrc* hashSrc : integrityExpressions) {
+ hashSrc->getAlgorithm(algorithm);
+ hashSrc->getHash(hash);
+
+ // The nsCSPHashSrc constructor lowercases algorithm, so this
+ // is case-insensitive.
+ if (sourceAlgorithm == algorithm && sourceHash == hash) {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ bypass = false;
+ break;
+ }
+ }
+
+ // Step 1.3.5. If bypass due to integrity match is true, return
+ // "Allowed".
+ if (bypass) {
+ CSPUTILSLOG(
+ (" Allowed by matching integrity metadata (script-like)"));
+ return true;
+ }
+ }
+ }
+
+ // Step 1.4. If directive’s value contains a source expression that is an
+ // ASCII case-insensitive match for the "'strict-dynamic'" keyword-source:
+
+ // XXX I don't think we should apply strict-dynamic to XSLT.
+ if (hasStrictDynamicKeyword && aLoadInfo->InternalContentPolicyType() !=
+ nsIContentPolicy::TYPE_XSLT) {
+ // Step 1.4.1 If the request’s parser metadata is "parser-inserted",
+ // return "Blocked". Otherwise, return "Allowed".
+ if (aLoadInfo->GetParserCreatedScript()) {
+ CSPUTILSLOG(
+ (" Blocked by 'strict-dynamic' because parser-inserted"));
+ return false;
+ }
+
+ CSPUTILSLOG(
+ (" Allowed by 'strict-dynamic' because not-parser-inserted"));
+ return true;
+ }
+ }
+ }
+
+ for (uint32_t i = 0; i < mSrcs.Length(); i++) {
+ if (mSrcs[i]->permits(aUri, aWasRedirected, aReportOnly,
+ aUpgradeInsecure)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool nsCSPDirective::allows(enum CSPKeyword aKeyword,
+ const nsAString& aHashOrNonce) const {
+ CSPUTILSLOG(("nsCSPDirective::allows, aKeyWord: %s, aHashOrNonce: %s",
+ CSP_EnumToUTF8Keyword(aKeyword),
+ NS_ConvertUTF16toUTF8(aHashOrNonce).get()));
+
+ for (uint32_t i = 0; i < mSrcs.Length(); i++) {
+ if (mSrcs[i]->allows(aKeyword, aHashOrNonce)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+// https://w3c.github.io/webappsec-csp/#allow-all-inline
+bool nsCSPDirective::allowsAllInlineBehavior(CSPDirective aDir) const {
+ // Step 1. Let allow all inline be false.
+ bool allowAll = false;
+
+ // Step 2. For each expression of list:
+ for (nsCSPBaseSrc* src : mSrcs) {
+ // Step 2.1. If expression matches the nonce-source or hash-source grammar,
+ // return "Does Not Allow".
+ if (src->isNonce() || src->isHash()) {
+ return false;
+ }
+
+ // Step 2.2. If type is "script", "script attribute" or "navigation" and
+ // expression matches the keyword-source "'strict-dynamic'", return "Does
+ // Not Allow".
+ if ((aDir == nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE ||
+ aDir == nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE) &&
+ src->isKeyword(CSP_STRICT_DYNAMIC)) {
+ return false;
+ }
+
+ // Step 2.3. If expression is an ASCII case-insensitive match for the
+ // keyword-source "'unsafe-inline'", set allow all inline to true.
+ if (src->isKeyword(CSP_UNSAFE_INLINE)) {
+ allowAll = true;
+ }
+ }
+
+ // Step 3. If allow all inline is true, return "Allows". Otherwise, return
+ // "Does Not Allow".
+ return allowAll;
+}
+
+void nsCSPDirective::toString(nsAString& outStr) const {
+ // Append directive name
+ outStr.AppendASCII(CSP_CSPDirectiveToString(mDirective));
+ outStr.AppendLiteral(" ");
+
+ // Append srcs
+ StringJoinAppend(outStr, u" "_ns, mSrcs,
+ [](nsAString& dest, nsCSPBaseSrc* cspBaseSrc) {
+ cspBaseSrc->toString(dest);
+ });
+}
+
+void nsCSPDirective::toDomCSPStruct(mozilla::dom::CSP& outCSP) const {
+ mozilla::dom::Sequence<nsString> srcs;
+ nsString src;
+ if (NS_WARN_IF(!srcs.SetCapacity(mSrcs.Length(), mozilla::fallible))) {
+ MOZ_ASSERT(false,
+ "Not enough memory for 'sources' sequence in "
+ "nsCSPDirective::toDomCSPStruct().");
+ return;
+ }
+ for (uint32_t i = 0; i < mSrcs.Length(); i++) {
+ src.Truncate();
+ mSrcs[i]->toString(src);
+ if (!srcs.AppendElement(src, mozilla::fallible)) {
+ MOZ_ASSERT(false,
+ "Failed to append to 'sources' sequence in "
+ "nsCSPDirective::toDomCSPStruct().");
+ }
+ }
+
+ switch (mDirective) {
+ case nsIContentSecurityPolicy::DEFAULT_SRC_DIRECTIVE:
+ outCSP.mDefault_src.Construct();
+ outCSP.mDefault_src.Value() = std::move(srcs);
+ return;
+
+ case nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE:
+ outCSP.mScript_src.Construct();
+ outCSP.mScript_src.Value() = std::move(srcs);
+ return;
+
+ case nsIContentSecurityPolicy::OBJECT_SRC_DIRECTIVE:
+ outCSP.mObject_src.Construct();
+ outCSP.mObject_src.Value() = std::move(srcs);
+ return;
+
+ case nsIContentSecurityPolicy::STYLE_SRC_DIRECTIVE:
+ outCSP.mStyle_src.Construct();
+ outCSP.mStyle_src.Value() = std::move(srcs);
+ return;
+
+ case nsIContentSecurityPolicy::IMG_SRC_DIRECTIVE:
+ outCSP.mImg_src.Construct();
+ outCSP.mImg_src.Value() = std::move(srcs);
+ return;
+
+ case nsIContentSecurityPolicy::MEDIA_SRC_DIRECTIVE:
+ outCSP.mMedia_src.Construct();
+ outCSP.mMedia_src.Value() = std::move(srcs);
+ return;
+
+ case nsIContentSecurityPolicy::FRAME_SRC_DIRECTIVE:
+ outCSP.mFrame_src.Construct();
+ outCSP.mFrame_src.Value() = std::move(srcs);
+ return;
+
+ case nsIContentSecurityPolicy::FONT_SRC_DIRECTIVE:
+ outCSP.mFont_src.Construct();
+ outCSP.mFont_src.Value() = std::move(srcs);
+ return;
+
+ case nsIContentSecurityPolicy::CONNECT_SRC_DIRECTIVE:
+ outCSP.mConnect_src.Construct();
+ outCSP.mConnect_src.Value() = std::move(srcs);
+ return;
+
+ case nsIContentSecurityPolicy::REPORT_URI_DIRECTIVE:
+ outCSP.mReport_uri.Construct();
+ outCSP.mReport_uri.Value() = std::move(srcs);
+ return;
+
+ case nsIContentSecurityPolicy::FRAME_ANCESTORS_DIRECTIVE:
+ outCSP.mFrame_ancestors.Construct();
+ outCSP.mFrame_ancestors.Value() = std::move(srcs);
+ return;
+
+ case nsIContentSecurityPolicy::WEB_MANIFEST_SRC_DIRECTIVE:
+ outCSP.mManifest_src.Construct();
+ outCSP.mManifest_src.Value() = std::move(srcs);
+ return;
+ // not supporting REFLECTED_XSS_DIRECTIVE
+
+ case nsIContentSecurityPolicy::BASE_URI_DIRECTIVE:
+ outCSP.mBase_uri.Construct();
+ outCSP.mBase_uri.Value() = std::move(srcs);
+ return;
+
+ case nsIContentSecurityPolicy::FORM_ACTION_DIRECTIVE:
+ outCSP.mForm_action.Construct();
+ outCSP.mForm_action.Value() = std::move(srcs);
+ return;
+
+ case nsIContentSecurityPolicy::BLOCK_ALL_MIXED_CONTENT:
+ outCSP.mBlock_all_mixed_content.Construct();
+ // does not have any srcs
+ return;
+
+ case nsIContentSecurityPolicy::UPGRADE_IF_INSECURE_DIRECTIVE:
+ outCSP.mUpgrade_insecure_requests.Construct();
+ // does not have any srcs
+ return;
+
+ case nsIContentSecurityPolicy::CHILD_SRC_DIRECTIVE:
+ outCSP.mChild_src.Construct();
+ outCSP.mChild_src.Value() = std::move(srcs);
+ return;
+
+ case nsIContentSecurityPolicy::SANDBOX_DIRECTIVE:
+ outCSP.mSandbox.Construct();
+ outCSP.mSandbox.Value() = std::move(srcs);
+ return;
+
+ case nsIContentSecurityPolicy::WORKER_SRC_DIRECTIVE:
+ outCSP.mWorker_src.Construct();
+ outCSP.mWorker_src.Value() = std::move(srcs);
+ return;
+
+ case nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE:
+ outCSP.mScript_src_elem.Construct();
+ outCSP.mScript_src_elem.Value() = std::move(srcs);
+ return;
+
+ case nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE:
+ outCSP.mScript_src_attr.Construct();
+ outCSP.mScript_src_attr.Value() = std::move(srcs);
+ return;
+
+ default:
+ NS_ASSERTION(false, "cannot find directive to convert CSP to JSON");
+ }
+}
+
+void nsCSPDirective::getReportURIs(nsTArray<nsString>& outReportURIs) const {
+ NS_ASSERTION((mDirective == nsIContentSecurityPolicy::REPORT_URI_DIRECTIVE),
+ "not a report-uri directive");
+
+ // append uris
+ nsString tmpReportURI;
+ for (uint32_t i = 0; i < mSrcs.Length(); i++) {
+ tmpReportURI.Truncate();
+ mSrcs[i]->toString(tmpReportURI);
+ outReportURIs.AppendElement(tmpReportURI);
+ }
+}
+
+bool nsCSPDirective::visitSrcs(nsCSPSrcVisitor* aVisitor) const {
+ for (uint32_t i = 0; i < mSrcs.Length(); i++) {
+ if (!mSrcs[i]->visit(aVisitor)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool nsCSPDirective::equals(CSPDirective aDirective) const {
+ return (mDirective == aDirective);
+}
+
+void nsCSPDirective::getDirName(nsAString& outStr) const {
+ outStr.AppendASCII(CSP_CSPDirectiveToString(mDirective));
+}
+
+bool nsCSPDirective::hasReportSampleKeyword() const {
+ for (nsCSPBaseSrc* src : mSrcs) {
+ if (src->isReportSample()) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/* =============== nsCSPChildSrcDirective ============= */
+
+nsCSPChildSrcDirective::nsCSPChildSrcDirective(CSPDirective aDirective)
+ : nsCSPDirective(aDirective),
+ mRestrictFrames(false),
+ mRestrictWorkers(false) {}
+
+nsCSPChildSrcDirective::~nsCSPChildSrcDirective() = default;
+
+bool nsCSPChildSrcDirective::equals(CSPDirective aDirective) const {
+ if (aDirective == nsIContentSecurityPolicy::FRAME_SRC_DIRECTIVE) {
+ return mRestrictFrames;
+ }
+ if (aDirective == nsIContentSecurityPolicy::WORKER_SRC_DIRECTIVE) {
+ return mRestrictWorkers;
+ }
+ return (mDirective == aDirective);
+}
+
+/* =============== nsCSPScriptSrcDirective ============= */
+
+nsCSPScriptSrcDirective::nsCSPScriptSrcDirective(CSPDirective aDirective)
+ : nsCSPDirective(aDirective) {}
+
+nsCSPScriptSrcDirective::~nsCSPScriptSrcDirective() = default;
+
+bool nsCSPScriptSrcDirective::equals(CSPDirective aDirective) const {
+ if (aDirective == nsIContentSecurityPolicy::WORKER_SRC_DIRECTIVE) {
+ return mRestrictWorkers;
+ }
+ if (aDirective == nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE) {
+ return mRestrictScriptElem;
+ }
+ if (aDirective == nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE) {
+ return mRestrictScriptAttr;
+ }
+ return mDirective == aDirective;
+}
+
+/* =============== nsCSPStyleSrcDirective ============= */
+
+nsCSPStyleSrcDirective::nsCSPStyleSrcDirective(CSPDirective aDirective)
+ : nsCSPDirective(aDirective) {}
+
+nsCSPStyleSrcDirective::~nsCSPStyleSrcDirective() = default;
+
+bool nsCSPStyleSrcDirective::equals(CSPDirective aDirective) const {
+ if (aDirective == nsIContentSecurityPolicy::STYLE_SRC_ELEM_DIRECTIVE) {
+ return mRestrictStyleElem;
+ }
+ if (aDirective == nsIContentSecurityPolicy::STYLE_SRC_ATTR_DIRECTIVE) {
+ return mRestrictStyleAttr;
+ }
+ return mDirective == aDirective;
+}
+
+/* =============== nsBlockAllMixedContentDirective ============= */
+
+nsBlockAllMixedContentDirective::nsBlockAllMixedContentDirective(
+ CSPDirective aDirective)
+ : nsCSPDirective(aDirective) {}
+
+nsBlockAllMixedContentDirective::~nsBlockAllMixedContentDirective() = default;
+
+void nsBlockAllMixedContentDirective::toString(nsAString& outStr) const {
+ outStr.AppendASCII(CSP_CSPDirectiveToString(
+ nsIContentSecurityPolicy::BLOCK_ALL_MIXED_CONTENT));
+}
+
+void nsBlockAllMixedContentDirective::getDirName(nsAString& outStr) const {
+ outStr.AppendASCII(CSP_CSPDirectiveToString(
+ nsIContentSecurityPolicy::BLOCK_ALL_MIXED_CONTENT));
+}
+
+/* =============== nsUpgradeInsecureDirective ============= */
+
+nsUpgradeInsecureDirective::nsUpgradeInsecureDirective(CSPDirective aDirective)
+ : nsCSPDirective(aDirective) {}
+
+nsUpgradeInsecureDirective::~nsUpgradeInsecureDirective() = default;
+
+void nsUpgradeInsecureDirective::toString(nsAString& outStr) const {
+ outStr.AppendASCII(CSP_CSPDirectiveToString(
+ nsIContentSecurityPolicy::UPGRADE_IF_INSECURE_DIRECTIVE));
+}
+
+void nsUpgradeInsecureDirective::getDirName(nsAString& outStr) const {
+ outStr.AppendASCII(CSP_CSPDirectiveToString(
+ nsIContentSecurityPolicy::UPGRADE_IF_INSECURE_DIRECTIVE));
+}
+
+/* ===== nsCSPPolicy ========================= */
+
+nsCSPPolicy::nsCSPPolicy()
+ : mUpgradeInsecDir(nullptr),
+ mReportOnly(false),
+ mDeliveredViaMetaTag(false) {
+ CSPUTILSLOG(("nsCSPPolicy::nsCSPPolicy"));
+}
+
+nsCSPPolicy::~nsCSPPolicy() {
+ CSPUTILSLOG(("nsCSPPolicy::~nsCSPPolicy"));
+
+ for (uint32_t i = 0; i < mDirectives.Length(); i++) {
+ delete mDirectives[i];
+ }
+}
+
+bool nsCSPPolicy::permits(CSPDirective aDir, nsILoadInfo* aLoadInfo,
+ nsIURI* aUri, bool aWasRedirected, bool aSpecific,
+ nsAString& outViolatedDirective) const {
+ if (CSPUTILSLOGENABLED()) {
+ CSPUTILSLOG(("nsCSPPolicy::permits, aUri: %s, aDir: %s, aSpecific: %s",
+ aUri->GetSpecOrDefault().get(), CSP_CSPDirectiveToString(aDir),
+ aSpecific ? "true" : "false"));
+ }
+
+ NS_ASSERTION(aUri, "permits needs an uri to perform the check!");
+ outViolatedDirective.Truncate();
+
+ nsCSPDirective* defaultDir = nullptr;
+
+ // Try to find a relevant directive
+ // These directive arrays are short (1-5 elements), not worth using a
+ // hashtable.
+ for (uint32_t i = 0; i < mDirectives.Length(); i++) {
+ if (mDirectives[i]->equals(aDir)) {
+ if (!mDirectives[i]->permits(aDir, aLoadInfo, aUri, aWasRedirected,
+ mReportOnly, mUpgradeInsecDir)) {
+ mDirectives[i]->getDirName(outViolatedDirective);
+ return false;
+ }
+ return true;
+ }
+ if (mDirectives[i]->isDefaultDirective()) {
+ defaultDir = mDirectives[i];
+ }
+ }
+
+ // If the above loop runs through, we haven't found a matching directive.
+ // Avoid relooping, just store the result of default-src while looping.
+ if (!aSpecific && defaultDir) {
+ if (!defaultDir->permits(aDir, aLoadInfo, aUri, aWasRedirected, mReportOnly,
+ mUpgradeInsecDir)) {
+ defaultDir->getDirName(outViolatedDirective);
+ return false;
+ }
+ return true;
+ }
+
+ // Nothing restricts this, so we're allowing the load
+ // See bug 764937
+ return true;
+}
+
+bool nsCSPPolicy::allows(CSPDirective aDirective, enum CSPKeyword aKeyword,
+ const nsAString& aHashOrNonce) const {
+ CSPUTILSLOG(("nsCSPPolicy::allows, aKeyWord: %s, a HashOrNonce: %s",
+ CSP_EnumToUTF8Keyword(aKeyword),
+ NS_ConvertUTF16toUTF8(aHashOrNonce).get()));
+
+ if (nsCSPDirective* directive = matchingOrDefaultDirective(aDirective)) {
+ return directive->allows(aKeyword, aHashOrNonce);
+ }
+
+ // No matching directive or default directive as fallback found, thus
+ // allowing the load; see Bug 885433
+ // a) inline scripts (also unsafe eval) should only be blocked
+ // if there is a [script-src] or [default-src]
+ // b) inline styles should only be blocked
+ // if there is a [style-src] or [default-src]
+ return true;
+}
+
+nsCSPDirective* nsCSPPolicy::matchingOrDefaultDirective(
+ CSPDirective aDirective) const {
+ nsCSPDirective* defaultDir = nullptr;
+
+ // Try to find a matching directive
+ for (uint32_t i = 0; i < mDirectives.Length(); i++) {
+ if (mDirectives[i]->isDefaultDirective()) {
+ defaultDir = mDirectives[i];
+ continue;
+ }
+ if (mDirectives[i]->equals(aDirective)) {
+ return mDirectives[i];
+ }
+ }
+
+ return defaultDir;
+}
+
+void nsCSPPolicy::toString(nsAString& outStr) const {
+ StringJoinAppend(outStr, u"; "_ns, mDirectives,
+ [](nsAString& dest, nsCSPDirective* cspDirective) {
+ cspDirective->toString(dest);
+ });
+}
+
+void nsCSPPolicy::toDomCSPStruct(mozilla::dom::CSP& outCSP) const {
+ outCSP.mReport_only = mReportOnly;
+
+ for (uint32_t i = 0; i < mDirectives.Length(); ++i) {
+ mDirectives[i]->toDomCSPStruct(outCSP);
+ }
+}
+
+bool nsCSPPolicy::hasDirective(CSPDirective aDir) const {
+ for (uint32_t i = 0; i < mDirectives.Length(); i++) {
+ if (mDirectives[i]->equals(aDir)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool nsCSPPolicy::allowsAllInlineBehavior(CSPDirective aDir) const {
+ nsCSPDirective* directive = matchingOrDefaultDirective(aDir);
+ if (!directive) {
+ // No matching or default directive found thus allow the all inline
+ // scripts or styles. (See nsCSPPolicy::allows)
+ return true;
+ }
+
+ return directive->allowsAllInlineBehavior(aDir);
+}
+
+/*
+ * Use this function only after ::allows() returned 'false'. Most and
+ * foremost it's used to get the violated directive before sending reports.
+ * The parameter outDirective is the equivalent of 'outViolatedDirective'
+ * for the ::permits() function family.
+ */
+void nsCSPPolicy::getDirectiveStringAndReportSampleForContentType(
+ CSPDirective aDirective, nsAString& outDirective,
+ bool* aReportSample) const {
+ MOZ_ASSERT(aReportSample);
+ *aReportSample = false;
+
+ nsCSPDirective* defaultDir = nullptr;
+ for (uint32_t i = 0; i < mDirectives.Length(); i++) {
+ if (mDirectives[i]->isDefaultDirective()) {
+ defaultDir = mDirectives[i];
+ continue;
+ }
+ if (mDirectives[i]->equals(aDirective)) {
+ mDirectives[i]->getDirName(outDirective);
+ *aReportSample = mDirectives[i]->hasReportSampleKeyword();
+ return;
+ }
+ }
+ // if we haven't found a matching directive yet,
+ // the contentType must be restricted by the default directive
+ if (defaultDir) {
+ defaultDir->getDirName(outDirective);
+ *aReportSample = defaultDir->hasReportSampleKeyword();
+ return;
+ }
+ NS_ASSERTION(false, "Can not query directive string for contentType!");
+ outDirective.AppendLiteral("couldNotQueryViolatedDirective");
+}
+
+void nsCSPPolicy::getDirectiveAsString(CSPDirective aDir,
+ nsAString& outDirective) const {
+ for (uint32_t i = 0; i < mDirectives.Length(); i++) {
+ if (mDirectives[i]->equals(aDir)) {
+ mDirectives[i]->toString(outDirective);
+ return;
+ }
+ }
+}
+
+/*
+ * Helper function that returns the underlying bit representation of sandbox
+ * flags. The function returns SANDBOXED_NONE if there are no sandbox
+ * directives.
+ */
+uint32_t nsCSPPolicy::getSandboxFlags() const {
+ for (uint32_t i = 0; i < mDirectives.Length(); i++) {
+ if (mDirectives[i]->equals(nsIContentSecurityPolicy::SANDBOX_DIRECTIVE)) {
+ nsAutoString flags;
+ mDirectives[i]->toString(flags);
+
+ if (flags.IsEmpty()) {
+ return SANDBOX_ALL_FLAGS;
+ }
+
+ nsAttrValue attr;
+ attr.ParseAtomArray(flags);
+
+ return nsContentUtils::ParseSandboxAttributeToFlags(&attr);
+ }
+ }
+
+ return SANDBOXED_NONE;
+}
+
+void nsCSPPolicy::getReportURIs(nsTArray<nsString>& outReportURIs) const {
+ for (uint32_t i = 0; i < mDirectives.Length(); i++) {
+ if (mDirectives[i]->equals(
+ nsIContentSecurityPolicy::REPORT_URI_DIRECTIVE)) {
+ mDirectives[i]->getReportURIs(outReportURIs);
+ return;
+ }
+ }
+}
+
+bool nsCSPPolicy::visitDirectiveSrcs(CSPDirective aDir,
+ nsCSPSrcVisitor* aVisitor) const {
+ for (uint32_t i = 0; i < mDirectives.Length(); i++) {
+ if (mDirectives[i]->equals(aDir)) {
+ return mDirectives[i]->visitSrcs(aVisitor);
+ }
+ }
+ return false;
+}
diff --git a/dom/security/nsCSPUtils.h b/dom/security/nsCSPUtils.h
new file mode 100644
index 0000000000..2692681d03
--- /dev/null
+++ b/dom/security/nsCSPUtils.h
@@ -0,0 +1,676 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsCSPUtils_h___
+#define nsCSPUtils_h___
+
+#include "nsCOMPtr.h"
+#include "nsIContentSecurityPolicy.h"
+#include "nsILoadInfo.h"
+#include "nsIURI.h"
+#include "nsLiteralString.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsUnicharUtils.h"
+#include "mozilla/Logging.h"
+
+class nsIChannel;
+
+namespace mozilla::dom {
+struct CSP;
+class Document;
+} // namespace mozilla::dom
+
+/* =============== Logging =================== */
+
+void CSP_LogLocalizedStr(const char* aName, const nsTArray<nsString>& aParams,
+ const nsAString& aSourceName,
+ const nsAString& aSourceLine, uint32_t aLineNumber,
+ uint32_t aColumnNumber, uint32_t aFlags,
+ const nsACString& aCategory, uint64_t aInnerWindowID,
+ bool aFromPrivateWindow);
+
+void CSP_GetLocalizedStr(const char* aName, const nsTArray<nsString>& aParams,
+ nsAString& outResult);
+
+void CSP_LogStrMessage(const nsAString& aMsg);
+
+void CSP_LogMessage(const nsAString& aMessage, const nsAString& aSourceName,
+ const nsAString& aSourceLine, uint32_t aLineNumber,
+ uint32_t aColumnNumber, uint32_t aFlags,
+ const nsACString& aCategory, uint64_t aInnerWindowID,
+ bool aFromPrivateWindow);
+
+/* =============== Constant and Type Definitions ================== */
+
+#define INLINE_STYLE_VIOLATION_OBSERVER_TOPIC \
+ "violated base restriction: Inline Stylesheets will not apply"
+#define INLINE_SCRIPT_VIOLATION_OBSERVER_TOPIC \
+ "violated base restriction: Inline Scripts will not execute"
+#define EVAL_VIOLATION_OBSERVER_TOPIC \
+ "violated base restriction: Code will not be created from strings"
+#define WASM_EVAL_VIOLATION_OBSERVER_TOPIC \
+ "violated base restriction: WebAssembly code will not be created from " \
+ "dynamically"
+#define SCRIPT_NONCE_VIOLATION_OBSERVER_TOPIC "Inline Script had invalid nonce"
+#define STYLE_NONCE_VIOLATION_OBSERVER_TOPIC "Inline Style had invalid nonce"
+#define SCRIPT_HASH_VIOLATION_OBSERVER_TOPIC "Inline Script had invalid hash"
+#define STYLE_HASH_VIOLATION_OBSERVER_TOPIC "Inline Style had invalid hash"
+
+// these strings map to the CSPDirectives in nsIContentSecurityPolicy
+// NOTE: When implementing a new directive, you will need to add it here but
+// also add a corresponding entry to the constants in
+// nsIContentSecurityPolicy.idl and also create an entry for the new directive
+// in nsCSPDirective::toDomCSPStruct() and add it to CSPDictionaries.webidl.
+// Order of elements below important! Make sure it matches the order as in
+// nsIContentSecurityPolicy.idl
+static const char* CSPStrDirectives[] = {
+ "-error-", // NO_DIRECTIVE
+ "default-src", // DEFAULT_SRC_DIRECTIVE
+ "script-src", // SCRIPT_SRC_DIRECTIVE
+ "object-src", // OBJECT_SRC_DIRECTIVE
+ "style-src", // STYLE_SRC_DIRECTIVE
+ "img-src", // IMG_SRC_DIRECTIVE
+ "media-src", // MEDIA_SRC_DIRECTIVE
+ "frame-src", // FRAME_SRC_DIRECTIVE
+ "font-src", // FONT_SRC_DIRECTIVE
+ "connect-src", // CONNECT_SRC_DIRECTIVE
+ "report-uri", // REPORT_URI_DIRECTIVE
+ "frame-ancestors", // FRAME_ANCESTORS_DIRECTIVE
+ "reflected-xss", // REFLECTED_XSS_DIRECTIVE
+ "base-uri", // BASE_URI_DIRECTIVE
+ "form-action", // FORM_ACTION_DIRECTIVE
+ "manifest-src", // MANIFEST_SRC_DIRECTIVE
+ "upgrade-insecure-requests", // UPGRADE_IF_INSECURE_DIRECTIVE
+ "child-src", // CHILD_SRC_DIRECTIVE
+ "block-all-mixed-content", // BLOCK_ALL_MIXED_CONTENT
+ "sandbox", // SANDBOX_DIRECTIVE
+ "worker-src", // WORKER_SRC_DIRECTIVE
+ "script-src-elem", // SCRIPT_SRC_ELEM_DIRECTIVE
+ "script-src-attr", // SCRIPT_SRC_ATTR_DIRECTIVE
+ "style-src-elem", // STYLE_SRC_ELEM_DIRECTIVE
+ "style-src-attr", // STYLE_SRC_ATTR_DIRECTIVE
+};
+
+inline const char* CSP_CSPDirectiveToString(CSPDirective aDir) {
+ return CSPStrDirectives[static_cast<uint32_t>(aDir)];
+}
+
+inline CSPDirective CSP_StringToCSPDirective(const nsAString& aDir) {
+ nsString lowerDir = PromiseFlatString(aDir);
+ ToLowerCase(lowerDir);
+
+ uint32_t numDirs = (sizeof(CSPStrDirectives) / sizeof(CSPStrDirectives[0]));
+ for (uint32_t i = 1; i < numDirs; i++) {
+ if (lowerDir.EqualsASCII(CSPStrDirectives[i])) {
+ return static_cast<CSPDirective>(i);
+ }
+ }
+ return nsIContentSecurityPolicy::NO_DIRECTIVE;
+}
+
+#define FOR_EACH_CSP_KEYWORD(MACRO) \
+ MACRO(CSP_SELF, "'self'") \
+ MACRO(CSP_UNSAFE_INLINE, "'unsafe-inline'") \
+ MACRO(CSP_UNSAFE_EVAL, "'unsafe-eval'") \
+ MACRO(CSP_UNSAFE_HASHES, "'unsafe-hashes'") \
+ MACRO(CSP_NONE, "'none'") \
+ MACRO(CSP_NONCE, "'nonce-") \
+ MACRO(CSP_REPORT_SAMPLE, "'report-sample'") \
+ MACRO(CSP_STRICT_DYNAMIC, "'strict-dynamic'") \
+ MACRO(CSP_WASM_UNSAFE_EVAL, "'wasm-unsafe-eval'")
+
+enum CSPKeyword {
+#define KEYWORD_ENUM(id_, string_) id_,
+ FOR_EACH_CSP_KEYWORD(KEYWORD_ENUM)
+#undef KEYWORD_ENUM
+
+ // CSP_LAST_KEYWORD_VALUE always needs to be the last element in the enum
+ // because we use it to calculate the size for the char* array.
+ CSP_LAST_KEYWORD_VALUE,
+
+ // Putting CSP_HASH after the delimitor, because CSP_HASH is not a valid
+ // keyword (hash uses e.g. sha256, sha512) but we use CSP_HASH internally
+ // to identify allowed hashes in ::allows.
+ CSP_HASH
+};
+
+// The keywords, in UTF-8 form.
+static const char* gCSPUTF8Keywords[] = {
+#define KEYWORD_UTF8_LITERAL(id_, string_) string_,
+ FOR_EACH_CSP_KEYWORD(KEYWORD_UTF8_LITERAL)
+#undef KEYWORD_UTF8_LITERAL
+};
+
+// The keywords, in UTF-16 form.
+static const char16_t* gCSPUTF16Keywords[] = {
+#define KEYWORD_UTF16_LITERAL(id_, string_) u"" string_,
+ FOR_EACH_CSP_KEYWORD(KEYWORD_UTF16_LITERAL)
+#undef KEYWORD_UTF16_LITERAL
+};
+
+#undef FOR_EACH_CSP_KEYWORD
+
+inline const char* CSP_EnumToUTF8Keyword(enum CSPKeyword aKey) {
+ // Make sure all elements in enum CSPKeyword got added to gCSPUTF8Keywords.
+ static_assert((sizeof(gCSPUTF8Keywords) / sizeof(gCSPUTF8Keywords[0]) ==
+ CSP_LAST_KEYWORD_VALUE),
+ "CSP_LAST_KEYWORD_VALUE != length(gCSPUTF8Keywords)");
+
+ if (static_cast<uint32_t>(aKey) <
+ static_cast<uint32_t>(CSP_LAST_KEYWORD_VALUE)) {
+ return gCSPUTF8Keywords[static_cast<uint32_t>(aKey)];
+ }
+ return "error: invalid keyword in CSP_EnumToUTF8Keyword";
+}
+
+inline const char16_t* CSP_EnumToUTF16Keyword(enum CSPKeyword aKey) {
+ // Make sure all elements in enum CSPKeyword got added to gCSPUTF16Keywords.
+ static_assert((sizeof(gCSPUTF16Keywords) / sizeof(gCSPUTF16Keywords[0]) ==
+ CSP_LAST_KEYWORD_VALUE),
+ "CSP_LAST_KEYWORD_VALUE != length(gCSPUTF16Keywords)");
+
+ if (static_cast<uint32_t>(aKey) <
+ static_cast<uint32_t>(CSP_LAST_KEYWORD_VALUE)) {
+ return gCSPUTF16Keywords[static_cast<uint32_t>(aKey)];
+ }
+ return u"error: invalid keyword in CSP_EnumToUTF16Keyword";
+}
+
+inline CSPKeyword CSP_UTF16KeywordToEnum(const nsAString& aKey) {
+ nsString lowerKey = PromiseFlatString(aKey);
+ ToLowerCase(lowerKey);
+
+ for (uint32_t i = 0; i < CSP_LAST_KEYWORD_VALUE; i++) {
+ if (lowerKey.Equals(gCSPUTF16Keywords[i])) {
+ return static_cast<CSPKeyword>(i);
+ }
+ }
+ NS_ASSERTION(false, "Can not convert unknown Keyword to Enum");
+ return CSP_LAST_KEYWORD_VALUE;
+}
+
+nsresult CSP_AppendCSPFromHeader(nsIContentSecurityPolicy* aCsp,
+ const nsAString& aHeaderValue,
+ bool aReportOnly);
+
+/* =============== Helpers ================== */
+
+class nsCSPHostSrc;
+
+nsCSPHostSrc* CSP_CreateHostSrcFromSelfURI(nsIURI* aSelfURI);
+bool CSP_IsEmptyDirective(const nsAString& aValue, const nsAString& aDir);
+bool CSP_IsDirective(const nsAString& aValue, CSPDirective aDir);
+bool CSP_IsKeyword(const nsAString& aValue, enum CSPKeyword aKey);
+bool CSP_IsQuotelessKeyword(const nsAString& aKey);
+CSPDirective CSP_ContentTypeToDirective(nsContentPolicyType aType);
+
+class nsCSPSrcVisitor;
+
+void CSP_PercentDecodeStr(const nsAString& aEncStr, nsAString& outDecStr);
+bool CSP_ShouldResponseInheritCSP(nsIChannel* aChannel);
+
+void CSP_ApplyMetaCSPToDoc(mozilla::dom::Document& aDoc,
+ const nsAString& aPolicyStr);
+
+/* =============== nsCSPSrc ================== */
+
+class nsCSPBaseSrc {
+ public:
+ nsCSPBaseSrc();
+ virtual ~nsCSPBaseSrc();
+
+ virtual bool permits(nsIURI* aUri, bool aWasRedirected, bool aReportOnly,
+ bool aUpgradeInsecure) const;
+ virtual bool allows(enum CSPKeyword aKeyword,
+ const nsAString& aHashOrNonce) const;
+ virtual bool visit(nsCSPSrcVisitor* aVisitor) const = 0;
+ virtual void toString(nsAString& outStr) const = 0;
+
+ virtual bool isReportSample() const { return false; }
+
+ virtual bool isHash() const { return false; }
+ virtual bool isNonce() const { return false; }
+ virtual bool isKeyword(CSPKeyword aKeyword) const { return false; }
+};
+
+/* =============== nsCSPSchemeSrc ============ */
+
+class nsCSPSchemeSrc : public nsCSPBaseSrc {
+ public:
+ explicit nsCSPSchemeSrc(const nsAString& aScheme);
+ virtual ~nsCSPSchemeSrc();
+
+ bool permits(nsIURI* aUri, bool aWasRedirected, bool aReportOnly,
+ bool aUpgradeInsecure) const override;
+ bool visit(nsCSPSrcVisitor* aVisitor) const override;
+ void toString(nsAString& outStr) const override;
+
+ inline void getScheme(nsAString& outStr) const { outStr.Assign(mScheme); };
+
+ private:
+ nsString mScheme;
+};
+
+/* =============== nsCSPHostSrc ============== */
+
+class nsCSPHostSrc : public nsCSPBaseSrc {
+ public:
+ explicit nsCSPHostSrc(const nsAString& aHost);
+ virtual ~nsCSPHostSrc();
+
+ bool permits(nsIURI* aUri, bool aWasRedirected, bool aReportOnly,
+ bool aUpgradeInsecure) const override;
+ bool visit(nsCSPSrcVisitor* aVisitor) const override;
+ void toString(nsAString& outStr) const override;
+
+ void setScheme(const nsAString& aScheme);
+ void setPort(const nsAString& aPort);
+ void appendPath(const nsAString& aPath);
+
+ inline void setGeneratedFromSelfKeyword() const {
+ mGeneratedFromSelfKeyword = true;
+ }
+
+ inline void setIsUniqueOrigin() const { mIsUniqueOrigin = true; }
+
+ inline void setWithinFrameAncestorsDir(bool aValue) const {
+ mWithinFrameAncstorsDir = aValue;
+ }
+
+ inline void getScheme(nsAString& outStr) const { outStr.Assign(mScheme); };
+
+ inline void getHost(nsAString& outStr) const { outStr.Assign(mHost); };
+
+ inline void getPort(nsAString& outStr) const { outStr.Assign(mPort); };
+
+ inline void getPath(nsAString& outStr) const { outStr.Assign(mPath); };
+
+ private:
+ nsString mScheme;
+ nsString mHost;
+ nsString mPort;
+ nsString mPath;
+ mutable bool mGeneratedFromSelfKeyword;
+ mutable bool mIsUniqueOrigin;
+ mutable bool mWithinFrameAncstorsDir;
+};
+
+/* =============== nsCSPKeywordSrc ============ */
+
+class nsCSPKeywordSrc : public nsCSPBaseSrc {
+ public:
+ explicit nsCSPKeywordSrc(CSPKeyword aKeyword);
+ virtual ~nsCSPKeywordSrc();
+
+ bool allows(enum CSPKeyword aKeyword,
+ const nsAString& aHashOrNonce) const override;
+ bool visit(nsCSPSrcVisitor* aVisitor) const override;
+ void toString(nsAString& outStr) const override;
+
+ inline CSPKeyword getKeyword() const { return mKeyword; };
+
+ bool isReportSample() const override { return mKeyword == CSP_REPORT_SAMPLE; }
+
+ bool isKeyword(CSPKeyword aKeyword) const final {
+ return mKeyword == aKeyword;
+ }
+
+ private:
+ CSPKeyword mKeyword;
+};
+
+/* =============== nsCSPNonceSource =========== */
+
+class nsCSPNonceSrc : public nsCSPBaseSrc {
+ public:
+ explicit nsCSPNonceSrc(const nsAString& aNonce);
+ virtual ~nsCSPNonceSrc();
+
+ bool allows(enum CSPKeyword aKeyword,
+ const nsAString& aHashOrNonce) const override;
+ bool visit(nsCSPSrcVisitor* aVisitor) const override;
+ void toString(nsAString& outStr) const override;
+
+ inline void getNonce(nsAString& outStr) const { outStr.Assign(mNonce); };
+
+ bool isNonce() const final { return true; }
+
+ private:
+ nsString mNonce;
+};
+
+/* =============== nsCSPHashSource ============ */
+
+class nsCSPHashSrc : public nsCSPBaseSrc {
+ public:
+ nsCSPHashSrc(const nsAString& algo, const nsAString& hash);
+ virtual ~nsCSPHashSrc();
+
+ bool allows(enum CSPKeyword aKeyword,
+ const nsAString& aHashOrNonce) const override;
+ void toString(nsAString& outStr) const override;
+ bool visit(nsCSPSrcVisitor* aVisitor) const override;
+
+ inline void getAlgorithm(nsAString& outStr) const {
+ outStr.Assign(mAlgorithm);
+ };
+
+ inline void getHash(nsAString& outStr) const { outStr.Assign(mHash); };
+
+ bool isHash() const final { return true; }
+
+ private:
+ nsString mAlgorithm;
+ nsString mHash;
+};
+
+/* =============== nsCSPReportURI ============ */
+
+class nsCSPReportURI : public nsCSPBaseSrc {
+ public:
+ explicit nsCSPReportURI(nsIURI* aURI);
+ virtual ~nsCSPReportURI();
+
+ bool visit(nsCSPSrcVisitor* aVisitor) const override;
+ void toString(nsAString& outStr) const override;
+
+ private:
+ nsCOMPtr<nsIURI> mReportURI;
+};
+
+/* =============== nsCSPSandboxFlags ================== */
+
+class nsCSPSandboxFlags : public nsCSPBaseSrc {
+ public:
+ explicit nsCSPSandboxFlags(const nsAString& aFlags);
+ virtual ~nsCSPSandboxFlags();
+
+ bool visit(nsCSPSrcVisitor* aVisitor) const override;
+ void toString(nsAString& outStr) const override;
+
+ private:
+ nsString mFlags;
+};
+
+/* =============== nsCSPSrcVisitor ================== */
+
+class nsCSPSrcVisitor {
+ public:
+ virtual bool visitSchemeSrc(const nsCSPSchemeSrc& src) = 0;
+
+ virtual bool visitHostSrc(const nsCSPHostSrc& src) = 0;
+
+ virtual bool visitKeywordSrc(const nsCSPKeywordSrc& src) = 0;
+
+ virtual bool visitNonceSrc(const nsCSPNonceSrc& src) = 0;
+
+ virtual bool visitHashSrc(const nsCSPHashSrc& src) = 0;
+
+ protected:
+ explicit nsCSPSrcVisitor() = default;
+ virtual ~nsCSPSrcVisitor() = default;
+};
+
+/* =============== nsCSPDirective ============= */
+
+class nsCSPDirective {
+ public:
+ explicit nsCSPDirective(CSPDirective aDirective);
+ virtual ~nsCSPDirective();
+
+ virtual bool permits(CSPDirective aDirective, nsILoadInfo* aLoadInfo,
+ nsIURI* aUri, bool aWasRedirected, bool aReportOnly,
+ bool aUpgradeInsecure) const;
+ virtual bool allows(enum CSPKeyword aKeyword,
+ const nsAString& aHashOrNonce) const;
+ bool allowsAllInlineBehavior(CSPDirective aDir) const;
+ virtual void toString(nsAString& outStr) const;
+ void toDomCSPStruct(mozilla::dom::CSP& outCSP) const;
+
+ virtual void addSrcs(const nsTArray<nsCSPBaseSrc*>& aSrcs) {
+ mSrcs = aSrcs.Clone();
+ }
+
+ inline bool isDefaultDirective() const {
+ return mDirective == nsIContentSecurityPolicy::DEFAULT_SRC_DIRECTIVE;
+ }
+
+ virtual bool equals(CSPDirective aDirective) const;
+
+ void getReportURIs(nsTArray<nsString>& outReportURIs) const;
+
+ bool visitSrcs(nsCSPSrcVisitor* aVisitor) const;
+
+ virtual void getDirName(nsAString& outStr) const;
+
+ bool hasReportSampleKeyword() const;
+
+ protected:
+ CSPDirective mDirective;
+ nsTArray<nsCSPBaseSrc*> mSrcs;
+};
+
+/* =============== nsCSPChildSrcDirective ============= */
+
+/*
+ * In CSP 3 child-src is deprecated. For backwards compatibility
+ * child-src needs to restrict:
+ * (*) frames, in case frame-src is not expicitly specified
+ * (*) workers, in case worker-src is not expicitly specified
+ */
+class nsCSPChildSrcDirective : public nsCSPDirective {
+ public:
+ explicit nsCSPChildSrcDirective(CSPDirective aDirective);
+ virtual ~nsCSPChildSrcDirective();
+
+ void setRestrictFrames() { mRestrictFrames = true; }
+
+ void setRestrictWorkers() { mRestrictWorkers = true; }
+
+ virtual bool equals(CSPDirective aDirective) const override;
+
+ private:
+ bool mRestrictFrames;
+ bool mRestrictWorkers;
+};
+
+/* =============== nsCSPScriptSrcDirective ============= */
+
+/*
+ * In CSP 3 worker-src restricts workers, for backwards compatibily
+ * script-src has to restrict workers as the ultimate fallback if
+ * neither worker-src nor child-src is present in a CSP.
+ */
+class nsCSPScriptSrcDirective : public nsCSPDirective {
+ public:
+ explicit nsCSPScriptSrcDirective(CSPDirective aDirective);
+ virtual ~nsCSPScriptSrcDirective();
+
+ void setRestrictWorkers() { mRestrictWorkers = true; }
+ void setRestrictScriptElem() { mRestrictScriptElem = true; }
+ void setRestrictScriptAttr() { mRestrictScriptAttr = true; }
+
+ bool equals(CSPDirective aDirective) const override;
+
+ private:
+ bool mRestrictWorkers = false;
+ bool mRestrictScriptElem = false;
+ bool mRestrictScriptAttr = false;
+};
+
+/* =============== nsCSPStyleSrcDirective ============= */
+
+/*
+ * In CSP 3 style-src is use as a fallback for style-src-elem and
+ * style-src-attr.
+ */
+class nsCSPStyleSrcDirective : public nsCSPDirective {
+ public:
+ explicit nsCSPStyleSrcDirective(CSPDirective aDirective);
+ virtual ~nsCSPStyleSrcDirective();
+
+ void setRestrictStyleElem() { mRestrictStyleElem = true; }
+ void setRestrictStyleAttr() { mRestrictStyleAttr = true; }
+
+ bool equals(CSPDirective aDirective) const override;
+
+ private:
+ bool mRestrictStyleElem = false;
+ bool mRestrictStyleAttr = false;
+};
+
+/* =============== nsBlockAllMixedContentDirective === */
+
+class nsBlockAllMixedContentDirective : public nsCSPDirective {
+ public:
+ explicit nsBlockAllMixedContentDirective(CSPDirective aDirective);
+ ~nsBlockAllMixedContentDirective();
+
+ bool permits(CSPDirective aDirective, nsILoadInfo* aLoadInfo, nsIURI* aUri,
+ bool aWasRedirected, bool aReportOnly,
+ bool aUpgradeInsecure) const override {
+ return false;
+ }
+
+ bool permits(nsIURI* aUri) const { return false; }
+
+ bool allows(enum CSPKeyword aKeyword,
+ const nsAString& aHashOrNonce) const override {
+ return false;
+ }
+
+ void toString(nsAString& outStr) const override;
+
+ void addSrcs(const nsTArray<nsCSPBaseSrc*>& aSrcs) override {
+ MOZ_ASSERT(false, "block-all-mixed-content does not hold any srcs");
+ }
+
+ void getDirName(nsAString& outStr) const override;
+};
+
+/* =============== nsUpgradeInsecureDirective === */
+
+/*
+ * Upgrading insecure requests includes the following actors:
+ * (1) CSP:
+ * The CSP implementation allowlists the http-request
+ * in case the policy is executed in enforcement mode.
+ * The CSP implementation however does not allow http
+ * requests to succeed if executed in report-only mode.
+ * In such a case the CSP implementation reports the
+ * error back to the page.
+ *
+ * (2) MixedContent:
+ * The evalution of MixedContent allowlists all http
+ * requests with the promise that the http requests
+ * gets upgraded to https before any data is fetched
+ * from the network.
+ *
+ * (3) CORS:
+ * Does not consider the http request to be of a
+ * different origin in case the scheme is the only
+ * difference in otherwise matching URIs.
+ *
+ * (4) nsHttpChannel:
+ * Before connecting, the channel gets redirected
+ * to use https.
+ *
+ * (5) WebSocketChannel:
+ * Similar to the httpChannel, the websocketchannel
+ * gets upgraded from ws to wss.
+ */
+class nsUpgradeInsecureDirective : public nsCSPDirective {
+ public:
+ explicit nsUpgradeInsecureDirective(CSPDirective aDirective);
+ ~nsUpgradeInsecureDirective();
+
+ bool permits(CSPDirective aDirective, nsILoadInfo* aLoadInfo, nsIURI* aUri,
+ bool aWasRedirected, bool aReportOnly,
+ bool aUpgradeInsecure) const override {
+ return false;
+ }
+
+ bool permits(nsIURI* aUri) const { return false; }
+
+ bool allows(enum CSPKeyword aKeyword,
+ const nsAString& aHashOrNonce) const override {
+ return false;
+ }
+
+ void toString(nsAString& outStr) const override;
+
+ void addSrcs(const nsTArray<nsCSPBaseSrc*>& aSrcs) override {
+ MOZ_ASSERT(false, "upgrade-insecure-requests does not hold any srcs");
+ }
+
+ void getDirName(nsAString& outStr) const override;
+};
+
+/* =============== nsCSPPolicy ================== */
+
+class nsCSPPolicy {
+ public:
+ nsCSPPolicy();
+ virtual ~nsCSPPolicy();
+
+ bool permits(CSPDirective aDirective, nsILoadInfo* aLoadInfo, nsIURI* aUri,
+ bool aWasRedirected, bool aSpecific,
+ nsAString& outViolatedDirective) const;
+ bool allows(CSPDirective aDirective, enum CSPKeyword aKeyword,
+ const nsAString& aHashOrNonce) const;
+ void toString(nsAString& outStr) const;
+ void toDomCSPStruct(mozilla::dom::CSP& outCSP) const;
+
+ inline void addDirective(nsCSPDirective* aDir) {
+ mDirectives.AppendElement(aDir);
+ }
+
+ inline void addUpgradeInsecDir(nsUpgradeInsecureDirective* aDir) {
+ mUpgradeInsecDir = aDir;
+ addDirective(aDir);
+ }
+
+ bool hasDirective(CSPDirective aDir) const;
+
+ inline void setDeliveredViaMetaTagFlag(bool aFlag) {
+ mDeliveredViaMetaTag = aFlag;
+ }
+
+ inline bool getDeliveredViaMetaTagFlag() const {
+ return mDeliveredViaMetaTag;
+ }
+
+ inline void setReportOnlyFlag(bool aFlag) { mReportOnly = aFlag; }
+
+ inline bool getReportOnlyFlag() const { return mReportOnly; }
+
+ void getReportURIs(nsTArray<nsString>& outReportURIs) const;
+
+ void getDirectiveStringAndReportSampleForContentType(
+ CSPDirective aDirective, nsAString& outDirective,
+ bool* aReportSample) const;
+
+ void getDirectiveAsString(CSPDirective aDir, nsAString& outDirective) const;
+
+ uint32_t getSandboxFlags() const;
+
+ inline uint32_t getNumDirectives() const { return mDirectives.Length(); }
+
+ bool visitDirectiveSrcs(CSPDirective aDir, nsCSPSrcVisitor* aVisitor) const;
+
+ bool allowsAllInlineBehavior(CSPDirective aDir) const;
+
+ private:
+ nsCSPDirective* matchingOrDefaultDirective(CSPDirective aDirective) const;
+
+ nsUpgradeInsecureDirective* mUpgradeInsecDir;
+ nsTArray<nsCSPDirective*> mDirectives;
+ bool mReportOnly;
+ bool mDeliveredViaMetaTag;
+};
+
+#endif /* nsCSPUtils_h___ */
diff --git a/dom/security/nsContentSecurityManager.cpp b/dom/security/nsContentSecurityManager.cpp
new file mode 100644
index 0000000000..b36f8ac2b7
--- /dev/null
+++ b/dom/security/nsContentSecurityManager.cpp
@@ -0,0 +1,1715 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsAboutProtocolUtils.h"
+#include "nsArray.h"
+#include "nsContentSecurityManager.h"
+#include "nsContentSecurityUtils.h"
+#include "nsContentPolicyUtils.h"
+#include "nsEscape.h"
+#include "nsDataHandler.h"
+#include "nsIChannel.h"
+#include "nsIContentPolicy.h"
+#include "nsIHttpChannelInternal.h"
+#include "nsINode.h"
+#include "nsIStreamListener.h"
+#include "nsILoadInfo.h"
+#include "nsIMIMEService.h"
+#include "nsIOService.h"
+#include "nsContentUtils.h"
+#include "nsCORSListenerProxy.h"
+#include "nsIParentChannel.h"
+#include "nsIRedirectHistoryEntry.h"
+#include "nsIXULRuntime.h"
+#include "nsNetUtil.h"
+#include "nsReadableUtils.h"
+#include "nsSandboxFlags.h"
+#include "nsIXPConnect.h"
+
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/CmdLineAndEnvUtils.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/nsMixedContentBlocker.h"
+#include "mozilla/dom/BrowserChild.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/extensions/WebExtensionPolicy.h"
+#include "mozilla/Components.h"
+#include "mozilla/ExtensionPolicyService.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPrefs_security.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TelemetryComms.h"
+#include "xpcpublic.h"
+#include "nsMimeTypes.h"
+
+#include "jsapi.h"
+#include "js/RegExp.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::Telemetry;
+
+NS_IMPL_ISUPPORTS(nsContentSecurityManager, nsIContentSecurityManager,
+ nsIChannelEventSink)
+
+mozilla::LazyLogModule sCSMLog("CSMLog");
+
+// These first two are used for off-the-main-thread checks of
+// general.config.filename
+// (which can't be checked off-main-thread).
+Atomic<bool, mozilla::Relaxed> sJSHacksChecked(false);
+Atomic<bool, mozilla::Relaxed> sJSHacksPresent(false);
+Atomic<bool, mozilla::Relaxed> sCSSHacksChecked(false);
+Atomic<bool, mozilla::Relaxed> sCSSHacksPresent(false);
+Atomic<bool, mozilla::Relaxed> sTelemetryEventEnabled(false);
+
+/* static */
+bool nsContentSecurityManager::AllowTopLevelNavigationToDataURI(
+ nsIChannel* aChannel) {
+ // Let's block all toplevel document navigations to a data: URI.
+ // In all cases where the toplevel document is navigated to a
+ // data: URI the triggeringPrincipal is a contentPrincipal, or
+ // a NullPrincipal. In other cases, e.g. typing a data: URL into
+ // the URL-Bar, the triggeringPrincipal is a SystemPrincipal;
+ // we don't want to block those loads. Only exception, loads coming
+ // from an external applicaton (e.g. Thunderbird) don't load
+ // using a contentPrincipal, but we want to block those loads.
+ if (!StaticPrefs::security_data_uri_block_toplevel_data_uri_navigations()) {
+ return true;
+ }
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ if (loadInfo->GetExternalContentPolicyType() !=
+ ExtContentPolicy::TYPE_DOCUMENT) {
+ return true;
+ }
+ if (loadInfo->GetForceAllowDataURI()) {
+ // if the loadinfo explicitly allows the data URI navigation, let's allow it
+ // now
+ return true;
+ }
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, true);
+ bool isDataURI = uri->SchemeIs("data");
+ if (!isDataURI) {
+ return true;
+ }
+
+ nsAutoCString spec;
+ rv = uri->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, true);
+ nsAutoCString contentType;
+ bool base64;
+ rv = nsDataHandler::ParseURI(spec, contentType, nullptr, base64, nullptr);
+ NS_ENSURE_SUCCESS(rv, true);
+
+ // Allow data: images as long as they are not SVGs
+ if (StringBeginsWith(contentType, "image/"_ns) &&
+ !contentType.EqualsLiteral("image/svg+xml")) {
+ return true;
+ }
+ // Allow all data: PDFs. or JSON documents
+ if (contentType.EqualsLiteral(APPLICATION_JSON) ||
+ contentType.EqualsLiteral(TEXT_JSON) ||
+ contentType.EqualsLiteral(APPLICATION_PDF)) {
+ return true;
+ }
+ // Redirecting to a toplevel data: URI is not allowed, hence we make
+ // sure the RedirectChain is empty.
+ if (!loadInfo->GetLoadTriggeredFromExternal() &&
+ loadInfo->TriggeringPrincipal()->IsSystemPrincipal() &&
+ loadInfo->RedirectChain().IsEmpty()) {
+ return true;
+ }
+
+ ReportBlockedDataURI(uri, loadInfo);
+
+ return false;
+}
+
+void nsContentSecurityManager::ReportBlockedDataURI(nsIURI* aURI,
+ nsILoadInfo* aLoadInfo,
+ bool aIsRedirect) {
+ // We're going to block the request, construct the localized error message to
+ // report to the console.
+ nsAutoCString dataSpec;
+ aURI->GetSpec(dataSpec);
+ if (dataSpec.Length() > 50) {
+ dataSpec.Truncate(50);
+ dataSpec.AppendLiteral("...");
+ }
+ AutoTArray<nsString, 1> params;
+ CopyUTF8toUTF16(NS_UnescapeURL(dataSpec), *params.AppendElement());
+ nsAutoString errorText;
+ const char* stringID =
+ aIsRedirect ? "BlockRedirectToDataURI" : "BlockTopLevelDataURINavigation";
+ nsresult rv = nsContentUtils::FormatLocalizedString(
+ nsContentUtils::eSECURITY_PROPERTIES, stringID, params, errorText);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ // Report the localized error message to the console for the loading
+ // BrowsingContext's current inner window.
+ RefPtr<BrowsingContext> target = aLoadInfo->GetBrowsingContext();
+ nsContentUtils::ReportToConsoleByWindowID(
+ errorText, nsIScriptError::warningFlag, "DATA_URI_BLOCKED"_ns,
+ target ? target->GetCurrentInnerWindowId() : 0);
+}
+
+/* static */
+bool nsContentSecurityManager::AllowInsecureRedirectToDataURI(
+ nsIChannel* aNewChannel) {
+ nsCOMPtr<nsILoadInfo> loadInfo = aNewChannel->LoadInfo();
+ if (loadInfo->GetExternalContentPolicyType() !=
+ ExtContentPolicy::TYPE_SCRIPT) {
+ return true;
+ }
+ nsCOMPtr<nsIURI> newURI;
+ nsresult rv = NS_GetFinalChannelURI(aNewChannel, getter_AddRefs(newURI));
+ if (NS_FAILED(rv) || !newURI) {
+ return true;
+ }
+ bool isDataURI = newURI->SchemeIs("data");
+ if (!isDataURI) {
+ return true;
+ }
+
+ // Web Extensions are exempt from that restriction and are allowed to redirect
+ // a channel to a data: URI. When a web extension redirects a channel, we set
+ // a flag on the loadInfo which allows us to identify such redirects here.
+ if (loadInfo->GetAllowInsecureRedirectToDataURI()) {
+ return true;
+ }
+
+ ReportBlockedDataURI(newURI, loadInfo, true);
+
+ return false;
+}
+
+static nsresult ValidateSecurityFlags(nsILoadInfo* aLoadInfo) {
+ nsSecurityFlags securityMode = aLoadInfo->GetSecurityMode();
+
+ // We should never perform a security check on a loadInfo that uses the flag
+ // SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, because that is only used for
+ // temporary loadInfos used for explicit nsIContentPolicy checks, but never be
+ // set as a security flag on an actual channel.
+ if (securityMode !=
+ nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT &&
+ securityMode != nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED &&
+ securityMode !=
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT &&
+ securityMode != nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL &&
+ securityMode != nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT) {
+ MOZ_ASSERT(
+ false,
+ "need one securityflag from nsILoadInfo to perform security checks");
+ return NS_ERROR_FAILURE;
+ }
+
+ // all good, found the right security flags
+ return NS_OK;
+}
+
+static already_AddRefed<nsIPrincipal> GetExtensionSandboxPrincipal(
+ nsILoadInfo* aLoadInfo) {
+ // An extension is allowed to load resources from itself when its pages are
+ // loaded into a sandboxed frame. Extension resources in a sandbox have
+ // a null principal and no access to extension APIs. See "sandbox" in
+ // MDN extension docs for more information.
+ if (!aLoadInfo->TriggeringPrincipal()->GetIsNullPrincipal()) {
+ return nullptr;
+ }
+ RefPtr<Document> doc;
+ aLoadInfo->GetLoadingDocument(getter_AddRefs(doc));
+ if (!doc || !(doc->GetSandboxFlags() & SANDBOXED_ORIGIN)) {
+ return nullptr;
+ }
+
+ // node principal is also a null principal here, so we need to
+ // create a principal using documentURI, which is the moz-extension
+ // uri for the page if this is an extension sandboxed page.
+ nsCOMPtr<nsIPrincipal> docPrincipal = BasePrincipal::CreateContentPrincipal(
+ doc->GetDocumentURI(), doc->NodePrincipal()->OriginAttributesRef());
+
+ if (!BasePrincipal::Cast(docPrincipal)->AddonPolicy()) {
+ return nullptr;
+ }
+ return docPrincipal.forget();
+}
+
+static bool IsImageLoadInEditorAppType(nsILoadInfo* aLoadInfo) {
+ // Editor apps get special treatment here, editors can load images
+ // from anywhere. This allows editor to insert images from file://
+ // into documents that are being edited.
+ nsContentPolicyType type = aLoadInfo->InternalContentPolicyType();
+ if (type != nsIContentPolicy::TYPE_INTERNAL_IMAGE &&
+ type != nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD &&
+ type != nsIContentPolicy::TYPE_INTERNAL_IMAGE_FAVICON &&
+ type != nsIContentPolicy::TYPE_IMAGESET) {
+ return false;
+ }
+
+ auto appType = nsIDocShell::APP_TYPE_UNKNOWN;
+ nsINode* node = aLoadInfo->LoadingNode();
+ if (!node) {
+ return false;
+ }
+ Document* doc = node->OwnerDoc();
+ if (!doc) {
+ return false;
+ }
+
+ nsCOMPtr<nsIDocShellTreeItem> docShellTreeItem = doc->GetDocShell();
+ if (!docShellTreeItem) {
+ return false;
+ }
+
+ nsCOMPtr<nsIDocShellTreeItem> root;
+ docShellTreeItem->GetInProcessRootTreeItem(getter_AddRefs(root));
+ nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(root));
+ if (docShell) {
+ appType = docShell->GetAppType();
+ }
+
+ return appType == nsIDocShell::APP_TYPE_EDITOR;
+}
+
+static nsresult DoCheckLoadURIChecks(nsIURI* aURI, nsILoadInfo* aLoadInfo) {
+ // In practice, these DTDs are just used for localization, so applying the
+ // same principal check as Fluent.
+ if (aLoadInfo->InternalContentPolicyType() ==
+ nsIContentPolicy::TYPE_INTERNAL_DTD) {
+ RefPtr<Document> doc;
+ aLoadInfo->GetLoadingDocument(getter_AddRefs(doc));
+ bool allowed = false;
+ aLoadInfo->TriggeringPrincipal()->IsL10nAllowed(
+ doc ? doc->GetDocumentURI() : nullptr, &allowed);
+
+ return allowed ? NS_OK : NS_ERROR_DOM_BAD_URI;
+ }
+
+ // This is used in order to allow a privileged DOMParser to parse documents
+ // that need to access localization DTDs. We just allow through
+ // TYPE_INTERNAL_FORCE_ALLOWED_DTD no matter what the triggering principal is.
+ if (aLoadInfo->InternalContentPolicyType() ==
+ nsIContentPolicy::TYPE_INTERNAL_FORCE_ALLOWED_DTD) {
+ return NS_OK;
+ }
+
+ if (IsImageLoadInEditorAppType(aLoadInfo)) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIPrincipal> triggeringPrincipal = aLoadInfo->TriggeringPrincipal();
+ nsCOMPtr<nsIPrincipal> addonPrincipal =
+ GetExtensionSandboxPrincipal(aLoadInfo);
+ if (addonPrincipal) {
+ // call CheckLoadURIWithPrincipal() as below to continue other checks, but
+ // with the addon principal.
+ triggeringPrincipal = addonPrincipal;
+ }
+
+ // Only call CheckLoadURIWithPrincipal() using the TriggeringPrincipal and not
+ // the LoadingPrincipal when SEC_ALLOW_CROSS_ORIGIN_* security flags are set,
+ // to allow, e.g. user stylesheets to load chrome:// URIs.
+ return nsContentUtils::GetSecurityManager()->CheckLoadURIWithPrincipal(
+ triggeringPrincipal, aURI, aLoadInfo->CheckLoadURIFlags(),
+ aLoadInfo->GetInnerWindowID());
+}
+
+static bool URIHasFlags(nsIURI* aURI, uint32_t aURIFlags) {
+ bool hasFlags;
+ nsresult rv = NS_URIChainHasFlags(aURI, aURIFlags, &hasFlags);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ return hasFlags;
+}
+
+static nsresult DoSOPChecks(nsIURI* aURI, nsILoadInfo* aLoadInfo,
+ nsIChannel* aChannel) {
+ if (aLoadInfo->GetAllowChrome() &&
+ (URIHasFlags(aURI, nsIProtocolHandler::URI_IS_UI_RESOURCE) ||
+ nsContentUtils::SchemeIs(aURI, "moz-safe-about"))) {
+ // UI resources are allowed.
+ return DoCheckLoadURIChecks(aURI, aLoadInfo);
+ }
+
+ if (NS_HasBeenCrossOrigin(aChannel, true)) {
+ NS_SetRequestBlockingReason(aLoadInfo,
+ nsILoadInfo::BLOCKING_REASON_NOT_SAME_ORIGIN);
+ return NS_ERROR_DOM_BAD_URI;
+ }
+
+ return NS_OK;
+}
+
+static nsresult DoCORSChecks(nsIChannel* aChannel, nsILoadInfo* aLoadInfo,
+ nsCOMPtr<nsIStreamListener>& aInAndOutListener) {
+ MOZ_RELEASE_ASSERT(aInAndOutListener,
+ "can not perform CORS checks without a listener");
+
+ // No need to set up CORS if TriggeringPrincipal is the SystemPrincipal.
+ if (aLoadInfo->TriggeringPrincipal()->IsSystemPrincipal()) {
+ return NS_OK;
+ }
+
+ // We use the triggering principal here, rather than the loading principal
+ // to ensure that anonymous CORS content in the browser resources and in
+ // WebExtensions is allowed to load.
+ nsIPrincipal* principal = aLoadInfo->TriggeringPrincipal();
+ RefPtr<nsCORSListenerProxy> corsListener = new nsCORSListenerProxy(
+ aInAndOutListener, principal,
+ aLoadInfo->GetCookiePolicy() == nsILoadInfo::SEC_COOKIES_INCLUDE);
+ // XXX: @arg: DataURIHandling::Allow
+ // lets use DataURIHandling::Allow for now and then decide on callsite basis.
+ // see also:
+ // http://mxr.mozilla.org/mozilla-central/source/dom/security/nsCORSListenerProxy.h#33
+ nsresult rv = corsListener->Init(aChannel, DataURIHandling::Allow);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aInAndOutListener = corsListener;
+ return NS_OK;
+}
+
+static nsresult DoContentSecurityChecks(nsIChannel* aChannel,
+ nsILoadInfo* aLoadInfo) {
+ ExtContentPolicyType contentPolicyType =
+ aLoadInfo->GetExternalContentPolicyType();
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ switch (contentPolicyType) {
+ case ExtContentPolicy::TYPE_XMLHTTPREQUEST: {
+#ifdef DEBUG
+ {
+ nsCOMPtr<nsINode> node = aLoadInfo->LoadingNode();
+ MOZ_ASSERT(!node || node->NodeType() == nsINode::DOCUMENT_NODE,
+ "type_xml requires requestingContext of type Document");
+ }
+#endif
+ break;
+ }
+
+ case ExtContentPolicy::TYPE_OBJECT_SUBREQUEST: {
+#ifdef DEBUG
+ {
+ nsCOMPtr<nsINode> node = aLoadInfo->LoadingNode();
+ MOZ_ASSERT(
+ !node || node->NodeType() == nsINode::ELEMENT_NODE,
+ "type_subrequest requires requestingContext of type Element");
+ }
+#endif
+ break;
+ }
+
+ case ExtContentPolicy::TYPE_DTD: {
+#ifdef DEBUG
+ {
+ nsCOMPtr<nsINode> node = aLoadInfo->LoadingNode();
+ MOZ_ASSERT(!node || node->NodeType() == nsINode::DOCUMENT_NODE,
+ "type_dtd requires requestingContext of type Document");
+ }
+#endif
+ break;
+ }
+
+ case ExtContentPolicy::TYPE_MEDIA: {
+#ifdef DEBUG
+ {
+ nsCOMPtr<nsINode> node = aLoadInfo->LoadingNode();
+ MOZ_ASSERT(!node || node->NodeType() == nsINode::ELEMENT_NODE,
+ "type_media requires requestingContext of type Element");
+ }
+#endif
+ break;
+ }
+
+ case ExtContentPolicy::TYPE_WEBSOCKET: {
+ // Websockets have to use the proxied URI:
+ // ws:// instead of http:// for CSP checks
+ nsCOMPtr<nsIHttpChannelInternal> httpChannelInternal =
+ do_QueryInterface(aChannel);
+ MOZ_ASSERT(httpChannelInternal);
+ if (httpChannelInternal) {
+ rv = httpChannelInternal->GetProxyURI(getter_AddRefs(uri));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+ break;
+ }
+
+ case ExtContentPolicy::TYPE_XSLT: {
+#ifdef DEBUG
+ {
+ nsCOMPtr<nsINode> node = aLoadInfo->LoadingNode();
+ MOZ_ASSERT(!node || node->NodeType() == nsINode::DOCUMENT_NODE,
+ "type_xslt requires requestingContext of type Document");
+ }
+#endif
+ break;
+ }
+
+ case ExtContentPolicy::TYPE_BEACON: {
+#ifdef DEBUG
+ {
+ nsCOMPtr<nsINode> node = aLoadInfo->LoadingNode();
+ MOZ_ASSERT(!node || node->NodeType() == nsINode::DOCUMENT_NODE,
+ "type_beacon requires requestingContext of type Document");
+ }
+#endif
+ break;
+ }
+
+ case ExtContentPolicy::TYPE_OTHER:
+ case ExtContentPolicy::TYPE_SCRIPT:
+ case ExtContentPolicy::TYPE_IMAGE:
+ case ExtContentPolicy::TYPE_STYLESHEET:
+ case ExtContentPolicy::TYPE_OBJECT:
+ case ExtContentPolicy::TYPE_DOCUMENT:
+ case ExtContentPolicy::TYPE_SUBDOCUMENT:
+ case ExtContentPolicy::TYPE_PING:
+ case ExtContentPolicy::TYPE_FONT:
+ case ExtContentPolicy::TYPE_UA_FONT:
+ case ExtContentPolicy::TYPE_CSP_REPORT:
+ case ExtContentPolicy::TYPE_WEB_MANIFEST:
+ case ExtContentPolicy::TYPE_FETCH:
+ case ExtContentPolicy::TYPE_IMAGESET:
+ case ExtContentPolicy::TYPE_SAVEAS_DOWNLOAD:
+ case ExtContentPolicy::TYPE_SPECULATIVE:
+ case ExtContentPolicy::TYPE_PROXIED_WEBRTC_MEDIA:
+ case ExtContentPolicy::TYPE_WEB_TRANSPORT:
+ case ExtContentPolicy::TYPE_WEB_IDENTITY:
+ break;
+
+ case ExtContentPolicy::TYPE_INVALID:
+ MOZ_ASSERT(false,
+ "can not perform security check without a valid contentType");
+ // Do not add default: so that compilers can catch the missing case.
+ }
+
+ int16_t shouldLoad = nsIContentPolicy::ACCEPT;
+ rv = NS_CheckContentLoadPolicy(uri, aLoadInfo, &shouldLoad,
+ nsContentUtils::GetContentPolicy());
+
+ if (NS_FAILED(rv) || NS_CP_REJECTED(shouldLoad)) {
+ NS_SetRequestBlockingReasonIfNull(
+ aLoadInfo, nsILoadInfo::BLOCKING_REASON_CONTENT_POLICY_GENERAL);
+
+ if (NS_SUCCEEDED(rv) &&
+ (contentPolicyType == ExtContentPolicy::TYPE_DOCUMENT ||
+ contentPolicyType == ExtContentPolicy::TYPE_SUBDOCUMENT)) {
+ if (shouldLoad == nsIContentPolicy::REJECT_TYPE) {
+ // for docshell loads we might have to return SHOW_ALT.
+ return NS_ERROR_CONTENT_BLOCKED_SHOW_ALT;
+ }
+ if (shouldLoad == nsIContentPolicy::REJECT_POLICY) {
+ return NS_ERROR_BLOCKED_BY_POLICY;
+ }
+ }
+ return NS_ERROR_CONTENT_BLOCKED;
+ }
+
+ return NS_OK;
+}
+
+static void LogHTTPSOnlyInfo(nsILoadInfo* aLoadInfo) {
+ MOZ_LOG(sCSMLog, LogLevel::Verbose, (" httpsOnlyFirstStatus:"));
+ uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus();
+
+ if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_UNINITIALIZED) {
+ MOZ_LOG(sCSMLog, LogLevel::Verbose, (" - HTTPS_ONLY_UNINITIALIZED"));
+ }
+ if (httpsOnlyStatus &
+ nsILoadInfo::HTTPS_ONLY_UPGRADED_LISTENER_NOT_REGISTERED) {
+ MOZ_LOG(sCSMLog, LogLevel::Verbose,
+ (" - HTTPS_ONLY_UPGRADED_LISTENER_NOT_REGISTERED"));
+ }
+ if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_UPGRADED_LISTENER_REGISTERED) {
+ MOZ_LOG(sCSMLog, LogLevel::Verbose,
+ (" - HTTPS_ONLY_UPGRADED_LISTENER_REGISTERED"));
+ }
+ if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) {
+ MOZ_LOG(sCSMLog, LogLevel::Verbose, (" - HTTPS_ONLY_EXEMPT"));
+ }
+ if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_TOP_LEVEL_LOAD_IN_PROGRESS) {
+ MOZ_LOG(sCSMLog, LogLevel::Verbose,
+ (" - HTTPS_ONLY_TOP_LEVEL_LOAD_IN_PROGRESS"));
+ }
+ if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_DOWNLOAD_IN_PROGRESS) {
+ MOZ_LOG(sCSMLog, LogLevel::Verbose,
+ (" - HTTPS_ONLY_DOWNLOAD_IN_PROGRESS"));
+ }
+ if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_DO_NOT_LOG_TO_CONSOLE) {
+ MOZ_LOG(sCSMLog, LogLevel::Verbose,
+ (" - HTTPS_ONLY_DO_NOT_LOG_TO_CONSOLE"));
+ }
+ if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_UPGRADED_HTTPS_FIRST) {
+ MOZ_LOG(sCSMLog, LogLevel::Verbose,
+ (" - HTTPS_ONLY_UPGRADED_HTTPS_FIRST"));
+ }
+}
+
+static void LogPrincipal(nsIPrincipal* aPrincipal,
+ const nsAString& aPrincipalName,
+ const uint8_t& aNestingLevel) {
+ nsPrintfCString aIndentationString("%*s", aNestingLevel * 2, "");
+
+ if (aPrincipal && aPrincipal->IsSystemPrincipal()) {
+ MOZ_LOG(sCSMLog, LogLevel::Debug,
+ ("%s%s: SystemPrincipal\n", aIndentationString.get(),
+ NS_ConvertUTF16toUTF8(aPrincipalName).get()));
+ return;
+ }
+ if (aPrincipal) {
+ if (aPrincipal->GetIsNullPrincipal()) {
+ MOZ_LOG(sCSMLog, LogLevel::Debug,
+ ("%s%s: NullPrincipal\n", aIndentationString.get(),
+ NS_ConvertUTF16toUTF8(aPrincipalName).get()));
+ return;
+ }
+ if (aPrincipal->GetIsExpandedPrincipal()) {
+ nsCOMPtr<nsIExpandedPrincipal> expanded(do_QueryInterface(aPrincipal));
+ nsAutoCString origin;
+ origin.AssignLiteral("[Expanded Principal [");
+
+ StringJoinAppend(origin, ", "_ns, expanded->AllowList(),
+ [](nsACString& dest, nsIPrincipal* principal) {
+ nsAutoCString subOrigin;
+ DebugOnly<nsresult> rv =
+ principal->GetOrigin(subOrigin);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ dest.Append(subOrigin);
+ });
+
+ origin.AppendLiteral("]]");
+
+ MOZ_LOG(sCSMLog, LogLevel::Debug,
+ ("%s%s: %s\n", aIndentationString.get(),
+ NS_ConvertUTF16toUTF8(aPrincipalName).get(), origin.get()));
+ return;
+ }
+ nsAutoCString principalSpec;
+ aPrincipal->GetAsciiSpec(principalSpec);
+ if (aPrincipalName.IsEmpty()) {
+ MOZ_LOG(sCSMLog, LogLevel::Debug,
+ ("%s - \"%s\"\n", aIndentationString.get(), principalSpec.get()));
+ } else {
+ MOZ_LOG(
+ sCSMLog, LogLevel::Debug,
+ ("%s%s: \"%s\"\n", aIndentationString.get(),
+ NS_ConvertUTF16toUTF8(aPrincipalName).get(), principalSpec.get()));
+ }
+ return;
+ }
+ MOZ_LOG(sCSMLog, LogLevel::Debug,
+ ("%s%s: nullptr\n", aIndentationString.get(),
+ NS_ConvertUTF16toUTF8(aPrincipalName).get()));
+}
+
+static void LogSecurityFlags(nsSecurityFlags securityFlags) {
+ struct DebugSecFlagType {
+ unsigned long secFlag;
+ char secTypeStr[128];
+ };
+ static const DebugSecFlagType secTypes[] = {
+ {nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK,
+ "SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK"},
+ {nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT,
+ "SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT"},
+ {nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED,
+ "SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED"},
+ {nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT,
+ "SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT"},
+ {nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ "SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL"},
+ {nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT,
+ "SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT"},
+ {nsILoadInfo::SEC_COOKIES_DEFAULT, "SEC_COOKIES_DEFAULT"},
+ {nsILoadInfo::SEC_COOKIES_INCLUDE, "SEC_COOKIES_INCLUDE"},
+ {nsILoadInfo::SEC_COOKIES_SAME_ORIGIN, "SEC_COOKIES_SAME_ORIGIN"},
+ {nsILoadInfo::SEC_COOKIES_OMIT, "SEC_COOKIES_OMIT"},
+ {nsILoadInfo::SEC_FORCE_INHERIT_PRINCIPAL, "SEC_FORCE_INHERIT_PRINCIPAL"},
+ {nsILoadInfo::SEC_ABOUT_BLANK_INHERITS, "SEC_ABOUT_BLANK_INHERITS"},
+ {nsILoadInfo::SEC_ALLOW_CHROME, "SEC_ALLOW_CHROME"},
+ {nsILoadInfo::SEC_DISALLOW_SCRIPT, "SEC_DISALLOW_SCRIPT"},
+ {nsILoadInfo::SEC_DONT_FOLLOW_REDIRECTS, "SEC_DONT_FOLLOW_REDIRECTS"},
+ {nsILoadInfo::SEC_LOAD_ERROR_PAGE, "SEC_LOAD_ERROR_PAGE"},
+ {nsILoadInfo::SEC_FORCE_INHERIT_PRINCIPAL_OVERRULE_OWNER,
+ "SEC_FORCE_INHERIT_PRINCIPAL_OVERRULE_OWNER"}};
+
+ for (const DebugSecFlagType& flag : secTypes) {
+ if (securityFlags & flag.secFlag) {
+ // the logging level should be in sync with the logging level in
+ // DebugDoContentSecurityCheck()
+ MOZ_LOG(sCSMLog, LogLevel::Verbose, (" - %s\n", flag.secTypeStr));
+ }
+ }
+}
+static void DebugDoContentSecurityCheck(nsIChannel* aChannel,
+ nsILoadInfo* aLoadInfo) {
+ nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(aChannel));
+
+ MOZ_LOG(sCSMLog, LogLevel::Debug, ("\n#DebugDoContentSecurityCheck Begin\n"));
+
+ // we only log http channels, unless loglevel is 5.
+ if (httpChannel || MOZ_LOG_TEST(sCSMLog, LogLevel::Verbose)) {
+ MOZ_LOG(sCSMLog, LogLevel::Verbose, ("doContentSecurityCheck:\n"));
+
+ nsAutoCString remoteType;
+ if (XRE_IsParentProcess()) {
+ nsCOMPtr<nsIParentChannel> parentChannel;
+ NS_QueryNotificationCallbacks(aChannel, parentChannel);
+ if (parentChannel) {
+ parentChannel->GetRemoteType(remoteType);
+ }
+ } else {
+ remoteType.Assign(
+ mozilla::dom::ContentChild::GetSingleton()->GetRemoteType());
+ }
+ MOZ_LOG(sCSMLog, LogLevel::Verbose,
+ (" processType: \"%s\"\n", remoteType.get()));
+
+ nsCOMPtr<nsIURI> channelURI;
+ nsAutoCString channelSpec;
+ nsAutoCString channelMethod;
+ NS_GetFinalChannelURI(aChannel, getter_AddRefs(channelURI));
+ if (channelURI) {
+ channelURI->GetSpec(channelSpec);
+ }
+ MOZ_LOG(sCSMLog, LogLevel::Verbose,
+ (" channelURI: \"%s\"\n", channelSpec.get()));
+
+ // Log HTTP-specific things
+ if (httpChannel) {
+ nsresult rv;
+ rv = httpChannel->GetRequestMethod(channelMethod);
+ if (!NS_FAILED(rv)) {
+ MOZ_LOG(sCSMLog, LogLevel::Verbose,
+ (" httpMethod: %s\n", channelMethod.get()));
+ }
+ }
+
+ // Log Principals
+ nsCOMPtr<nsIPrincipal> requestPrincipal = aLoadInfo->TriggeringPrincipal();
+ LogPrincipal(aLoadInfo->GetLoadingPrincipal(), u"loadingPrincipal"_ns, 1);
+ LogPrincipal(requestPrincipal, u"triggeringPrincipal"_ns, 1);
+ LogPrincipal(aLoadInfo->PrincipalToInherit(), u"principalToInherit"_ns, 1);
+
+ // Log Redirect Chain
+ MOZ_LOG(sCSMLog, LogLevel::Verbose, (" redirectChain:\n"));
+ for (nsIRedirectHistoryEntry* redirectHistoryEntry :
+ aLoadInfo->RedirectChain()) {
+ nsCOMPtr<nsIPrincipal> principal;
+ redirectHistoryEntry->GetPrincipal(getter_AddRefs(principal));
+ LogPrincipal(principal, u""_ns, 2);
+ }
+
+ MOZ_LOG(sCSMLog, LogLevel::Verbose,
+ (" internalContentPolicyType: %s\n",
+ NS_CP_ContentTypeName(aLoadInfo->InternalContentPolicyType())));
+ MOZ_LOG(sCSMLog, LogLevel::Verbose,
+ (" externalContentPolicyType: %s\n",
+ NS_CP_ContentTypeName(aLoadInfo->GetExternalContentPolicyType())));
+ MOZ_LOG(sCSMLog, LogLevel::Verbose,
+ (" upgradeInsecureRequests: %s\n",
+ aLoadInfo->GetUpgradeInsecureRequests() ? "true" : "false"));
+ MOZ_LOG(sCSMLog, LogLevel::Verbose,
+ (" initialSecurityChecksDone: %s\n",
+ aLoadInfo->GetInitialSecurityCheckDone() ? "true" : "false"));
+ MOZ_LOG(sCSMLog, LogLevel::Verbose,
+ (" allowDeprecatedSystemRequests: %s\n",
+ aLoadInfo->GetAllowDeprecatedSystemRequests() ? "true" : "false"));
+ MOZ_LOG(sCSMLog, LogLevel::Verbose,
+ (" wasSchemeless: %s\n",
+ aLoadInfo->GetWasSchemelessInput() ? "true" : "false"));
+
+ // Log CSPrequestPrincipal
+ nsCOMPtr<nsIContentSecurityPolicy> csp = aLoadInfo->GetCsp();
+ MOZ_LOG(sCSMLog, LogLevel::Debug, (" CSP:"));
+ if (csp) {
+ nsAutoString parsedPolicyStr;
+ uint32_t count = 0;
+ csp->GetPolicyCount(&count);
+ for (uint32_t i = 0; i < count; ++i) {
+ csp->GetPolicyString(i, parsedPolicyStr);
+ // we need to add quotation marks, as otherwise yaml parsers may fail
+ // with CSP directives
+ // no need to escape quote marks in the parsed policy string, as URLs in
+ // there are already encoded
+ MOZ_LOG(sCSMLog, LogLevel::Debug,
+ (" - \"%s\"\n", NS_ConvertUTF16toUTF8(parsedPolicyStr).get()));
+ }
+ }
+
+ // Security Flags
+ MOZ_LOG(sCSMLog, LogLevel::Verbose, (" securityFlags:"));
+ LogSecurityFlags(aLoadInfo->GetSecurityFlags());
+ // HTTPS-Only
+ LogHTTPSOnlyInfo(aLoadInfo);
+
+ MOZ_LOG(sCSMLog, LogLevel::Debug, ("\n#DebugDoContentSecurityCheck End\n"));
+ }
+}
+
+/* static */
+void nsContentSecurityManager::MeasureUnexpectedPrivilegedLoads(
+ nsILoadInfo* aLoadInfo, nsIURI* aFinalURI, const nsACString& aRemoteType) {
+ if (!StaticPrefs::dom_security_unexpected_system_load_telemetry_enabled()) {
+ return;
+ }
+ nsContentSecurityUtils::DetectJsHacks();
+ nsContentSecurityUtils::DetectCssHacks();
+ // The detection only work on the main-thread.
+ // To avoid races and early reports, we need to ensure the checks actually
+ // happened.
+ if (MOZ_UNLIKELY(sJSHacksPresent || !sJSHacksChecked || sCSSHacksPresent ||
+ !sCSSHacksChecked)) {
+ return;
+ }
+
+ ExtContentPolicyType contentPolicyType =
+ aLoadInfo->GetExternalContentPolicyType();
+ // restricting reported types to script, styles and documents
+ // to be continued in follow-ups of bug 1697163.
+ if (contentPolicyType != ExtContentPolicyType::TYPE_SCRIPT &&
+ contentPolicyType != ExtContentPolicyType::TYPE_STYLESHEET &&
+ contentPolicyType != ExtContentPolicyType::TYPE_DOCUMENT) {
+ return;
+ }
+
+ // Gather redirected schemes in string
+ nsAutoCString loggedRedirects;
+ const nsTArray<nsCOMPtr<nsIRedirectHistoryEntry>>& redirects =
+ aLoadInfo->RedirectChain();
+ if (!redirects.IsEmpty()) {
+ nsCOMPtr<nsIRedirectHistoryEntry> end = redirects.LastElement();
+ for (nsIRedirectHistoryEntry* entry : redirects) {
+ nsCOMPtr<nsIPrincipal> principal;
+ entry->GetPrincipal(getter_AddRefs(principal));
+ if (principal) {
+ nsAutoCString scheme;
+ principal->GetScheme(scheme);
+ loggedRedirects.Append(scheme);
+ if (entry != end) {
+ loggedRedirects.AppendLiteral(", ");
+ }
+ }
+ }
+ }
+
+ nsAutoCString uriString;
+ if (aFinalURI) {
+ aFinalURI->GetAsciiSpec(uriString);
+ } else {
+ uriString.AssignLiteral("");
+ }
+ FilenameTypeAndDetails fileNameTypeAndDetails =
+ nsContentSecurityUtils::FilenameToFilenameType(
+ NS_ConvertUTF8toUTF16(uriString), true);
+
+ nsCString loggedFileDetails = "unknown"_ns;
+ if (fileNameTypeAndDetails.second.isSome()) {
+ loggedFileDetails.Assign(
+ NS_ConvertUTF16toUTF8(fileNameTypeAndDetails.second.value()));
+ }
+ // sanitize remoteType because it may contain sensitive
+ // info, like URLs. e.g. `webIsolated=https://example.com`
+ nsAutoCString loggedRemoteType(dom::RemoteTypePrefix(aRemoteType));
+ nsAutoCString loggedContentType(NS_CP_ContentTypeName(contentPolicyType));
+
+ MOZ_LOG(sCSMLog, LogLevel::Debug, ("UnexpectedPrivilegedLoadTelemetry:\n"));
+ MOZ_LOG(sCSMLog, LogLevel::Debug,
+ ("- contentType: %s\n", loggedContentType.get()));
+ MOZ_LOG(sCSMLog, LogLevel::Debug,
+ ("- URL (not to be reported): %s\n", uriString.get()));
+ MOZ_LOG(sCSMLog, LogLevel::Debug,
+ ("- remoteType: %s\n", loggedRemoteType.get()));
+ MOZ_LOG(sCSMLog, LogLevel::Debug,
+ ("- fileInfo: %s\n", fileNameTypeAndDetails.first.get()));
+ MOZ_LOG(sCSMLog, LogLevel::Debug,
+ ("- fileDetails: %s\n", loggedFileDetails.get()));
+ MOZ_LOG(sCSMLog, LogLevel::Debug,
+ ("- redirects: %s\n\n", loggedRedirects.get()));
+
+ // Send Telemetry
+ auto extra = Some<nsTArray<EventExtraEntry>>(
+ {EventExtraEntry{"contenttype"_ns, loggedContentType},
+ EventExtraEntry{"remotetype"_ns, loggedRemoteType},
+ EventExtraEntry{"filedetails"_ns, loggedFileDetails},
+ EventExtraEntry{"redirects"_ns, loggedRedirects}});
+
+ if (!sTelemetryEventEnabled.exchange(true)) {
+ Telemetry::SetEventRecordingEnabled("security"_ns, true);
+ }
+
+ Telemetry::EventID eventType =
+ Telemetry::EventID::Security_Unexpectedload_Systemprincipal;
+ Telemetry::RecordEvent(eventType, mozilla::Some(fileNameTypeAndDetails.first),
+ extra);
+}
+
+/* static */
+nsSecurityFlags nsContentSecurityManager::ComputeSecurityFlags(
+ mozilla::CORSMode aCORSMode, CORSSecurityMapping aCORSSecurityMapping) {
+ if (aCORSSecurityMapping == CORSSecurityMapping::DISABLE_CORS_CHECKS) {
+ return nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL;
+ }
+
+ switch (aCORSMode) {
+ case CORS_NONE:
+ if (aCORSSecurityMapping == CORSSecurityMapping::REQUIRE_CORS_CHECKS) {
+ // CORS_NONE gets treated like CORS_ANONYMOUS in this mode
+ return nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT |
+ nsILoadInfo::SEC_COOKIES_SAME_ORIGIN;
+ } else if (aCORSSecurityMapping ==
+ CORSSecurityMapping::CORS_NONE_MAPS_TO_INHERITED_CONTEXT) {
+ // CORS_NONE inherits
+ return nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT;
+ } else {
+ // CORS_NONE_MAPS_TO_DISABLED_CORS_CHECKS, the only remaining enum
+ // variant. CORSSecurityMapping::DISABLE_CORS_CHECKS returned early.
+ MOZ_ASSERT(aCORSSecurityMapping ==
+ CORSSecurityMapping::CORS_NONE_MAPS_TO_DISABLED_CORS_CHECKS);
+ return nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL;
+ }
+ case CORS_ANONYMOUS:
+ return nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT |
+ nsILoadInfo::SEC_COOKIES_SAME_ORIGIN;
+ case CORS_USE_CREDENTIALS:
+ return nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT |
+ nsILoadInfo::SEC_COOKIES_INCLUDE;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid aCORSMode enum value");
+ return nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT |
+ nsILoadInfo::SEC_COOKIES_SAME_ORIGIN;
+ }
+}
+
+/* static */
+nsresult nsContentSecurityManager::CheckAllowLoadInSystemPrivilegedContext(
+ nsIChannel* aChannel) {
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ nsCOMPtr<nsIPrincipal> inspectedPrincipal = loadInfo->GetLoadingPrincipal();
+ if (!inspectedPrincipal) {
+ return NS_OK;
+ }
+ // Check if we are actually dealing with a privileged request
+ if (!inspectedPrincipal->IsSystemPrincipal()) {
+ return NS_OK;
+ }
+ // loads with the allow flag are waived through
+ // until refactored (e.g., Shavar, OCSP)
+ if (loadInfo->GetAllowDeprecatedSystemRequests()) {
+ return NS_OK;
+ }
+ ExtContentPolicyType contentPolicyType =
+ loadInfo->GetExternalContentPolicyType();
+ // For now, let's not inspect top-level document loads
+ if (contentPolicyType == ExtContentPolicy::TYPE_DOCUMENT) {
+ return NS_OK;
+ }
+
+ // allowing some fetches due to their lowered risk
+ // i.e., data & downloads fetches do limited parsing, no rendering
+ // remote images are too widely used (favicons, about:addons etc.)
+ if ((contentPolicyType == ExtContentPolicy::TYPE_FETCH) ||
+ (contentPolicyType == ExtContentPolicy::TYPE_XMLHTTPREQUEST) ||
+ (contentPolicyType == ExtContentPolicy::TYPE_WEBSOCKET) ||
+ (contentPolicyType == ExtContentPolicy::TYPE_SAVEAS_DOWNLOAD) ||
+ (contentPolicyType == ExtContentPolicy::TYPE_IMAGE)) {
+ return NS_OK;
+ }
+
+ // Allow the user interface (e.g., schemes like chrome, resource)
+ nsCOMPtr<nsIURI> finalURI;
+ NS_GetFinalChannelURI(aChannel, getter_AddRefs(finalURI));
+ bool isUiResource = false;
+ if (NS_SUCCEEDED(NS_URIChainHasFlags(
+ finalURI, nsIProtocolHandler::URI_IS_UI_RESOURCE, &isUiResource)) &&
+ isUiResource) {
+ return NS_OK;
+ }
+ // For about: and extension-based URIs, which don't get
+ // URI_IS_UI_RESOURCE, first remove layers of view-source:, if present.
+ nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(finalURI);
+
+ nsAutoCString remoteType;
+ if (XRE_IsParentProcess()) {
+ nsCOMPtr<nsIParentChannel> parentChannel;
+ NS_QueryNotificationCallbacks(aChannel, parentChannel);
+ if (parentChannel) {
+ parentChannel->GetRemoteType(remoteType);
+ }
+ } else {
+ remoteType.Assign(
+ mozilla::dom::ContentChild::GetSingleton()->GetRemoteType());
+ }
+
+ // GetInnerURI can return null for malformed nested URIs like moz-icon:trash
+ if (!innerURI) {
+ MeasureUnexpectedPrivilegedLoads(loadInfo, innerURI, remoteType);
+ if (StaticPrefs::security_disallow_privileged_no_finaluri_loads()) {
+ aChannel->Cancel(NS_ERROR_CONTENT_BLOCKED);
+ return NS_ERROR_CONTENT_BLOCKED;
+ }
+ return NS_OK;
+ }
+ // loads of userContent.css during startup and tests that show up as file:
+ if (innerURI->SchemeIs("file")) {
+ if ((contentPolicyType == ExtContentPolicy::TYPE_STYLESHEET) ||
+ (contentPolicyType == ExtContentPolicy::TYPE_OTHER)) {
+ return NS_OK;
+ }
+ }
+ // (1) loads from within omni.ja and system add-ons use jar:
+ // this is safe to allow, because we do not support remote jar.
+ // (2) about: resources are always allowed: they are part of the build.
+ // (3) extensions are signed or the user has made bad decisions.
+ if (innerURI->SchemeIs("jar") || innerURI->SchemeIs("about") ||
+ innerURI->SchemeIs("moz-extension")) {
+ return NS_OK;
+ }
+
+ nsAutoCString requestedURL;
+ innerURI->GetAsciiSpec(requestedURL);
+ MOZ_LOG(sCSMLog, LogLevel::Warning,
+ ("SystemPrincipal should not load remote resources. URL: %s, type %d",
+ requestedURL.get(), int(contentPolicyType)));
+
+ // The load types that we want to disallow, will extend over time and
+ // prioritized by risk. The most risky/dangerous are load-types are documents,
+ // subdocuments, scripts and styles in that order. The most dangerous URL
+ // schemes to cover are HTTP, HTTPS, data, blob in that order. Meta bug
+ // 1725112 will track upcoming restrictions
+
+ // Telemetry for unexpected privileged loads.
+ // pref check & data sanitization happens in the called function
+ MeasureUnexpectedPrivilegedLoads(loadInfo, innerURI, remoteType);
+
+ // Relaxing restrictions for our test suites:
+ // (1) AreNonLocalConnectionsDisabled() disables network, so
+ // http://mochitest is actually local and allowed. (2) The marionette test
+ // framework uses injections and data URLs to execute scripts, checking for
+ // the environment variable breaks the attack but not the tests.
+ if (xpc::AreNonLocalConnectionsDisabled() ||
+ mozilla::EnvHasValue("MOZ_MARIONETTE")) {
+ bool disallowSystemPrincipalRemoteDocuments = Preferences::GetBool(
+ "security.disallow_non_local_systemprincipal_in_tests");
+ if (disallowSystemPrincipalRemoteDocuments) {
+ // our own mochitest needs NS_ASSERTION instead of MOZ_ASSERT
+ NS_ASSERTION(false, "SystemPrincipal must not load remote documents.");
+ aChannel->Cancel(NS_ERROR_CONTENT_BLOCKED);
+ return NS_ERROR_CONTENT_BLOCKED;
+ }
+ // but other mochitest are exempt from this
+ return NS_OK;
+ }
+
+ if (contentPolicyType == ExtContentPolicy::TYPE_SUBDOCUMENT) {
+ if (StaticPrefs::security_disallow_privileged_https_subdocuments_loads() &&
+ (innerURI->SchemeIs("http") || innerURI->SchemeIs("https"))) {
+ MOZ_ASSERT(
+ false,
+ "Disallowing SystemPrincipal load of subdocuments on HTTP(S).");
+ aChannel->Cancel(NS_ERROR_CONTENT_BLOCKED);
+ return NS_ERROR_CONTENT_BLOCKED;
+ }
+ if ((StaticPrefs::security_disallow_privileged_data_subdocuments_loads()) &&
+ (innerURI->SchemeIs("data"))) {
+ MOZ_ASSERT(
+ false,
+ "Disallowing SystemPrincipal load of subdocuments on data URL.");
+ aChannel->Cancel(NS_ERROR_CONTENT_BLOCKED);
+ return NS_ERROR_CONTENT_BLOCKED;
+ }
+ }
+ if (contentPolicyType == ExtContentPolicy::TYPE_SCRIPT) {
+ if ((StaticPrefs::security_disallow_privileged_https_script_loads()) &&
+ (innerURI->SchemeIs("http") || innerURI->SchemeIs("https"))) {
+ MOZ_ASSERT(false,
+ "Disallowing SystemPrincipal load of scripts on HTTP(S).");
+ aChannel->Cancel(NS_ERROR_CONTENT_BLOCKED);
+ return NS_ERROR_CONTENT_BLOCKED;
+ }
+ }
+ if (contentPolicyType == ExtContentPolicy::TYPE_STYLESHEET) {
+ if (StaticPrefs::security_disallow_privileged_https_stylesheet_loads() &&
+ (innerURI->SchemeIs("http") || innerURI->SchemeIs("https"))) {
+ MOZ_ASSERT(false,
+ "Disallowing SystemPrincipal load of stylesheets on HTTP(S).");
+ aChannel->Cancel(NS_ERROR_CONTENT_BLOCKED);
+ return NS_ERROR_CONTENT_BLOCKED;
+ }
+ }
+ return NS_OK;
+}
+
+/*
+ * Disallow about pages in the privilegedaboutcontext (e.g., password manager,
+ * newtab etc.) to load remote scripts. Regardless of whether this is coming
+ * from the contentprincipal or the systemprincipal.
+ */
+/* static */
+nsresult nsContentSecurityManager::CheckAllowLoadInPrivilegedAboutContext(
+ nsIChannel* aChannel) {
+ // bail out if check is disabled
+ if (StaticPrefs::security_disallow_privilegedabout_remote_script_loads()) {
+ return NS_OK;
+ }
+
+ nsAutoCString remoteType;
+ if (XRE_IsParentProcess()) {
+ nsCOMPtr<nsIParentChannel> parentChannel;
+ NS_QueryNotificationCallbacks(aChannel, parentChannel);
+ if (parentChannel) {
+ parentChannel->GetRemoteType(remoteType);
+ }
+ } else {
+ remoteType.Assign(
+ mozilla::dom::ContentChild::GetSingleton()->GetRemoteType());
+ }
+
+ // only perform check for privileged about process
+ if (!remoteType.Equals(PRIVILEGEDABOUT_REMOTE_TYPE)) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ ExtContentPolicyType contentPolicyType =
+ loadInfo->GetExternalContentPolicyType();
+ // only check for script loads
+ if (contentPolicyType != ExtContentPolicy::TYPE_SCRIPT) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIURI> finalURI;
+ NS_GetFinalChannelURI(aChannel, getter_AddRefs(finalURI));
+ nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(finalURI);
+
+ bool isLocal;
+ NS_URIChainHasFlags(innerURI, nsIProtocolHandler::URI_IS_LOCAL_RESOURCE,
+ &isLocal);
+ // We allow URLs that are URI_IS_LOCAL (but that includes `data`
+ // and `blob` which are also undesirable.
+ if ((isLocal) && (!innerURI->SchemeIs("data")) &&
+ (!innerURI->SchemeIs("blob"))) {
+ return NS_OK;
+ }
+ MOZ_ASSERT(
+ false,
+ "Disallowing privileged about process to load scripts on HTTP(S).");
+ aChannel->Cancel(NS_ERROR_CONTENT_BLOCKED);
+ return NS_ERROR_CONTENT_BLOCKED;
+}
+
+/*
+ * Every protocol handler must set one of the six security flags
+ * defined in nsIProtocolHandler - if not - deny the load.
+ */
+nsresult nsContentSecurityManager::CheckChannelHasProtocolSecurityFlag(
+ nsIChannel* aChannel) {
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIIOService> ios = do_GetIOService(&rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t flags;
+ rv = ios->GetDynamicProtocolFlags(uri, &flags);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t securityFlagsSet = 0;
+ if (flags & nsIProtocolHandler::WEBEXT_URI_WEB_ACCESSIBLE) {
+ securityFlagsSet += 1;
+ }
+ if (flags & nsIProtocolHandler::URI_LOADABLE_BY_ANYONE) {
+ securityFlagsSet += 1;
+ }
+ if (flags & nsIProtocolHandler::URI_DANGEROUS_TO_LOAD) {
+ securityFlagsSet += 1;
+ }
+ if (flags & nsIProtocolHandler::URI_IS_UI_RESOURCE) {
+ securityFlagsSet += 1;
+ }
+ if (flags & nsIProtocolHandler::URI_IS_LOCAL_FILE) {
+ securityFlagsSet += 1;
+ }
+ if (flags & nsIProtocolHandler::URI_LOADABLE_BY_SUBSUMERS) {
+ securityFlagsSet += 1;
+ }
+
+ // Ensure that only "1" valid security flags is set.
+ if (securityFlagsSet == 1) {
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(false, "protocol must use one valid security flag");
+ return NS_ERROR_CONTENT_BLOCKED;
+}
+
+// We should not allow loading non-JavaScript files as scripts using
+// a file:// URL.
+static nsresult CheckAllowFileProtocolScriptLoad(nsIChannel* aChannel) {
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ ExtContentPolicyType type = loadInfo->GetExternalContentPolicyType();
+
+ // Only check script loads.
+ if (type != ExtContentPolicy::TYPE_SCRIPT) {
+ return NS_OK;
+ }
+
+ if (!StaticPrefs::security_block_fileuri_script_with_wrong_mime()) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!uri || !uri->SchemeIs("file")) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIMIMEService> mime = do_GetService("@mozilla.org/mime;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // GetTypeFromURI fails for missing or unknown file-extensions.
+ nsAutoCString contentType;
+ rv = mime->GetTypeFromURI(uri, contentType);
+ if (NS_FAILED(rv) || !nsContentUtils::IsJavascriptMIMEType(
+ NS_ConvertUTF8toUTF16(contentType))) {
+ nsCOMPtr<Document> doc;
+ if (nsINode* node = loadInfo->LoadingNode()) {
+ doc = node->OwnerDoc();
+ }
+
+ nsAutoCString spec;
+ uri->GetSpec(spec);
+
+ AutoTArray<nsString, 1> params;
+ CopyUTF8toUTF16(NS_UnescapeURL(spec), *params.AppendElement());
+ CopyUTF8toUTF16(NS_UnescapeURL(contentType), *params.AppendElement());
+
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
+ "FILE_SCRIPT_BLOCKED"_ns, doc,
+ nsContentUtils::eSECURITY_PROPERTIES,
+ "BlockFileScriptWithWrongMimeType", params);
+
+ return NS_ERROR_CONTENT_BLOCKED;
+ }
+
+ return NS_OK;
+}
+
+// We should not allow loading non-JavaScript files as scripts using
+// a moz-extension:// URL.
+static nsresult CheckAllowExtensionProtocolScriptLoad(nsIChannel* aChannel) {
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ ExtContentPolicyType type = loadInfo->GetExternalContentPolicyType();
+
+ // Only check script loads.
+ if (type != ExtContentPolicy::TYPE_SCRIPT) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!uri || !uri->SchemeIs("moz-extension")) {
+ return NS_OK;
+ }
+
+ // We expect this code to never be hit off-the-main-thread (even worker
+ // scripts are currently hitting only on the main thread, see
+ // WorkerScriptLoader::DispatchLoadScript calling NS_DispatchToMainThread
+ // internally), this diagnostic assertion is meant to let us notice if that
+ // isn't the case anymore.
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread(),
+ "Unexpected off-the-main-thread call to "
+ "CheckAllowFileProtocolScriptLoad");
+
+ nsAutoCString host;
+ rv = uri->GetHost(host);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<extensions::WebExtensionPolicyCore> targetPolicy =
+ ExtensionPolicyService::GetCoreByHost(host);
+
+ if (NS_WARN_IF(!targetPolicy) || targetPolicy->ManifestVersion() < 3) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIMIMEService> mime = do_GetService("@mozilla.org/mime;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // GetDefaultTypeFromExtension fails for missing or unknown file-extensions.
+ nsAutoCString contentType;
+ rv = mime->GetDefaultTypeFromURI(uri, contentType);
+ if (NS_FAILED(rv) || !nsContentUtils::IsJavascriptMIMEType(
+ NS_ConvertUTF8toUTF16(contentType))) {
+ nsCOMPtr<Document> doc;
+ if (nsINode* node = loadInfo->LoadingNode()) {
+ doc = node->OwnerDoc();
+ }
+
+ nsAutoCString spec;
+ uri->GetSpec(spec);
+
+ AutoTArray<nsString, 1> params;
+ CopyUTF8toUTF16(NS_UnescapeURL(spec), *params.AppendElement());
+
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
+ "EXTENSION_SCRIPT_BLOCKED"_ns, doc,
+ nsContentUtils::eSECURITY_PROPERTIES,
+ "BlockExtensionScriptWithWrongExt", params);
+
+ return NS_ERROR_CONTENT_BLOCKED;
+ }
+
+ return NS_OK;
+}
+
+// Validate that a load should be allowed based on its remote type. This
+// intentionally prevents some loads from occuring even using the system
+// principal, if they were started in a content process.
+static nsresult CheckAllowLoadByTriggeringRemoteType(nsIChannel* aChannel) {
+ MOZ_ASSERT(aChannel);
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+
+ // For now, only restrict loads for documents. We currently have no
+ // interesting subresource checks for protocols which are are not fully
+ // handled within the content process.
+ ExtContentPolicy contentPolicyType = loadInfo->GetExternalContentPolicyType();
+ if (contentPolicyType != ExtContentPolicy::TYPE_DOCUMENT &&
+ contentPolicyType != ExtContentPolicy::TYPE_SUBDOCUMENT &&
+ contentPolicyType != ExtContentPolicy::TYPE_OBJECT) {
+ return NS_OK;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread(),
+ "Unexpected off-the-main-thread call to "
+ "CheckAllowLoadByTriggeringRemoteType");
+
+ // Due to the way that session history is handled without SHIP, we cannot run
+ // these checks when SHIP is disabled.
+ if (!mozilla::SessionHistoryInParent()) {
+ return NS_OK;
+ }
+
+ nsAutoCString triggeringRemoteType;
+ nsresult rv = loadInfo->GetTriggeringRemoteType(triggeringRemoteType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // For now, only restrict loads coming from web remote types. In the future we
+ // may want to expand this a bit.
+ if (!StringBeginsWith(triggeringRemoteType, WEB_REMOTE_TYPE)) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIURI> finalURI;
+ rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(finalURI));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Don't allow web content processes to load non-remote about pages.
+ // NOTE: URIs with a `moz-safe-about:` inner scheme are safe to link to, so
+ // it's OK we miss them here.
+ nsCOMPtr<nsIURI> innermostURI = NS_GetInnermostURI(finalURI);
+ if (innermostURI->SchemeIs("about")) {
+ nsCOMPtr<nsIAboutModule> aboutModule;
+ rv = NS_GetAboutModule(innermostURI, getter_AddRefs(aboutModule));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t aboutModuleFlags = 0;
+ rv = aboutModule->GetURIFlags(innermostURI, &aboutModuleFlags);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!(aboutModuleFlags & nsIAboutModule::MAKE_LINKABLE) &&
+ !(aboutModuleFlags & nsIAboutModule::URI_CAN_LOAD_IN_CHILD) &&
+ !(aboutModuleFlags & nsIAboutModule::URI_MUST_LOAD_IN_CHILD)) {
+ NS_WARNING(nsPrintfCString("Blocking load of about URI (%s) which cannot "
+ "be linked to in web content process",
+ finalURI->GetSpecOrDefault().get())
+ .get());
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ if (NS_SUCCEEDED(
+ loadInfo->TriggeringPrincipal()->CheckMayLoad(finalURI, true))) {
+ nsAutoCString aboutModuleName;
+ MOZ_ALWAYS_SUCCEEDS(
+ NS_GetAboutModuleName(innermostURI, aboutModuleName));
+ MOZ_CRASH_UNSAFE_PRINTF(
+ "Blocking load of about uri by content process which may have "
+ "otherwise succeeded [aboutModule=%s, isSystemPrincipal=%d]",
+ aboutModuleName.get(),
+ loadInfo->TriggeringPrincipal()->IsSystemPrincipal());
+ }
+#endif
+ return NS_ERROR_CONTENT_BLOCKED;
+ }
+ return NS_OK;
+ }
+
+ // Don't allow web content processes to load file documents. Loads of file
+ // URIs as subresources will be handled by the sandbox, and may be allowed in
+ // some cases.
+ bool localFile = false;
+ rv = NS_URIChainHasFlags(finalURI, nsIProtocolHandler::URI_IS_LOCAL_FILE,
+ &localFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (localFile) {
+ NS_WARNING(
+ nsPrintfCString(
+ "Blocking document load of file URI (%s) from web content process",
+ innermostURI->GetSpecOrDefault().get())
+ .get());
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ if (NS_SUCCEEDED(
+ loadInfo->TriggeringPrincipal()->CheckMayLoad(finalURI, true))) {
+ MOZ_CRASH_UNSAFE_PRINTF(
+ "Blocking document load of file URI by content process which may "
+ "have otherwise succeeded [isSystemPrincipal=%d]",
+ loadInfo->TriggeringPrincipal()->IsSystemPrincipal());
+ }
+#endif
+ return NS_ERROR_CONTENT_BLOCKED;
+ }
+
+ return NS_OK;
+}
+
+/*
+ * Based on the security flags provided in the loadInfo of the channel,
+ * doContentSecurityCheck() performs the following content security checks
+ * before opening the channel:
+ *
+ * (1) Same Origin Policy Check (if applicable)
+ * (2) Allow Cross Origin but perform sanity checks whether a principal
+ * is allowed to access the following URL.
+ * (3) Perform CORS check (if applicable)
+ * (4) ContentPolicy checks (Content-Security-Policy, Mixed Content, ...)
+ *
+ * @param aChannel
+ * The channel to perform the security checks on.
+ * @param aInAndOutListener
+ * The streamListener that is passed to channel->AsyncOpen() that is now
+ * potentially wrappend within nsCORSListenerProxy() and becomes the
+ * corsListener that now needs to be set as new streamListener on the channel.
+ */
+nsresult nsContentSecurityManager::doContentSecurityCheck(
+ nsIChannel* aChannel, nsCOMPtr<nsIStreamListener>& aInAndOutListener) {
+ NS_ENSURE_ARG(aChannel);
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ if (MOZ_UNLIKELY(MOZ_LOG_TEST(sCSMLog, LogLevel::Verbose))) {
+ DebugDoContentSecurityCheck(aChannel, loadInfo);
+ }
+
+ nsresult rv = CheckAllowLoadInSystemPrivilegedContext(aChannel);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = CheckAllowLoadInPrivilegedAboutContext(aChannel);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We want to also check redirected requests to ensure
+ // the target maintains the proper javascript file extensions.
+ rv = CheckAllowExtensionProtocolScriptLoad(aChannel);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = CheckChannelHasProtocolSecurityFlag(aChannel);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = CheckAllowLoadByTriggeringRemoteType(aChannel);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // if dealing with a redirected channel then we have already installed
+ // streamlistener and redirect proxies and so we are done.
+ if (loadInfo->GetInitialSecurityCheckDone()) {
+ return NS_OK;
+ }
+
+ // make sure that only one of the five security flags is set in the loadinfo
+ // e.g. do not require same origin and allow cross origin at the same time
+ rv = ValidateSecurityFlags(loadInfo);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (loadInfo->GetSecurityMode() ==
+ nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT) {
+ rv = DoCORSChecks(aChannel, loadInfo, aInAndOutListener);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = CheckChannel(aChannel);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Perform all ContentPolicy checks (MixedContent, CSP, ...)
+ rv = DoContentSecurityChecks(aChannel, loadInfo);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = CheckAllowFileProtocolScriptLoad(aChannel);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // now lets set the initialSecurityFlag for subsequent calls
+ loadInfo->SetInitialSecurityCheckDone(true);
+
+ // all security checks passed - lets allow the load
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsContentSecurityManager::AsyncOnChannelRedirect(
+ nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aRedirFlags,
+ nsIAsyncVerifyRedirectCallback* aCb) {
+ // Since we compare the principal from the loadInfo to the URI's
+ // princicpal, it's possible that the checks fail when doing an internal
+ // redirect. We can just return early instead, since we should never
+ // need to block an internal redirect.
+ if (aRedirFlags & nsIChannelEventSink::REDIRECT_INTERNAL) {
+ aCb->OnRedirectVerifyCallback(NS_OK);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aOldChannel->LoadInfo();
+ nsresult rv = CheckChannel(aNewChannel);
+ if (NS_FAILED(rv)) {
+ aOldChannel->Cancel(rv);
+ return rv;
+ }
+
+ // Also verify that the redirecting server is allowed to redirect to the
+ // given URI
+ nsCOMPtr<nsIPrincipal> oldPrincipal;
+ nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal(
+ aOldChannel, getter_AddRefs(oldPrincipal));
+
+ nsCOMPtr<nsIURI> newURI;
+ Unused << NS_GetFinalChannelURI(aNewChannel, getter_AddRefs(newURI));
+ NS_ENSURE_STATE(oldPrincipal && newURI);
+
+ // Do not allow insecure redirects to data: URIs
+ if (!AllowInsecureRedirectToDataURI(aNewChannel)) {
+ // cancel the old channel and return an error
+ aOldChannel->Cancel(NS_ERROR_CONTENT_BLOCKED);
+ return NS_ERROR_CONTENT_BLOCKED;
+ }
+
+ const uint32_t flags =
+ nsIScriptSecurityManager::LOAD_IS_AUTOMATIC_DOCUMENT_REPLACEMENT |
+ nsIScriptSecurityManager::DISALLOW_SCRIPT;
+ rv = nsContentUtils::GetSecurityManager()->CheckLoadURIWithPrincipal(
+ oldPrincipal, newURI, flags, loadInfo->GetInnerWindowID());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aCb->OnRedirectVerifyCallback(NS_OK);
+ return NS_OK;
+}
+
+static void AddLoadFlags(nsIRequest* aRequest, nsLoadFlags aNewFlags) {
+ nsLoadFlags flags;
+ aRequest->GetLoadFlags(&flags);
+ flags |= aNewFlags;
+ aRequest->SetLoadFlags(flags);
+}
+
+/*
+ * Check that this channel passes all security checks. Returns an error code
+ * if this requesst should not be permitted.
+ */
+nsresult nsContentSecurityManager::CheckChannel(nsIChannel* aChannel) {
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Handle cookie policies
+ uint32_t cookiePolicy = loadInfo->GetCookiePolicy();
+ if (cookiePolicy == nsILoadInfo::SEC_COOKIES_SAME_ORIGIN) {
+ // We shouldn't have the SEC_COOKIES_SAME_ORIGIN flag for top level loads
+ MOZ_ASSERT(loadInfo->GetExternalContentPolicyType() !=
+ ExtContentPolicy::TYPE_DOCUMENT);
+ nsIPrincipal* loadingPrincipal = loadInfo->GetLoadingPrincipal();
+
+ // It doesn't matter what we pass for the second, data-inherits, argument.
+ // Any protocol which inherits won't pay attention to cookies anyway.
+ rv = loadingPrincipal->CheckMayLoad(uri, false);
+ if (NS_FAILED(rv)) {
+ AddLoadFlags(aChannel, nsIRequest::LOAD_ANONYMOUS);
+ }
+ } else if (cookiePolicy == nsILoadInfo::SEC_COOKIES_OMIT) {
+ AddLoadFlags(aChannel, nsIRequest::LOAD_ANONYMOUS);
+ }
+
+ if (!CrossOriginEmbedderPolicyAllowsCredentials(aChannel)) {
+ AddLoadFlags(aChannel, nsIRequest::LOAD_ANONYMOUS);
+ }
+
+ nsSecurityFlags securityMode = loadInfo->GetSecurityMode();
+
+ // CORS mode is handled by nsCORSListenerProxy
+ if (securityMode == nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT) {
+ if (NS_HasBeenCrossOrigin(aChannel)) {
+ loadInfo->MaybeIncreaseTainting(LoadTainting::CORS);
+ }
+ return NS_OK;
+ }
+
+ // Allow subresource loads if TriggeringPrincipal is the SystemPrincipal.
+ if (loadInfo->TriggeringPrincipal()->IsSystemPrincipal() &&
+ loadInfo->GetExternalContentPolicyType() !=
+ ExtContentPolicy::TYPE_DOCUMENT &&
+ loadInfo->GetExternalContentPolicyType() !=
+ ExtContentPolicy::TYPE_SUBDOCUMENT) {
+ return NS_OK;
+ }
+
+ // if none of the REQUIRE_SAME_ORIGIN flags are set, then SOP does not apply
+ if ((securityMode ==
+ nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT) ||
+ (securityMode == nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED)) {
+ rv = DoSOPChecks(uri, loadInfo, aChannel);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if ((securityMode ==
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT) ||
+ (securityMode ==
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL)) {
+ if (NS_HasBeenCrossOrigin(aChannel)) {
+ NS_ENSURE_FALSE(loadInfo->GetDontFollowRedirects(), NS_ERROR_DOM_BAD_URI);
+ loadInfo->MaybeIncreaseTainting(LoadTainting::Opaque);
+ }
+ // Please note that DoCheckLoadURIChecks should only be enforced for
+ // cross origin requests. If the flag SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT
+ // is set within the loadInfo, then CheckLoadURIWithPrincipal is performed
+ // within nsCorsListenerProxy
+ rv = DoCheckLoadURIChecks(uri, loadInfo);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // TODO: Bug 1371237
+ // consider calling SetBlockedRequest in
+ // nsContentSecurityManager::CheckChannel
+ }
+
+ return NS_OK;
+}
+
+// https://fetch.spec.whatwg.org/#ref-for-cross-origin-embedder-policy-allows-credentials
+bool nsContentSecurityManager::CrossOriginEmbedderPolicyAllowsCredentials(
+ nsIChannel* aChannel) {
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+
+ // 1. If request’s mode is not "no-cors", then return true.
+ //
+ // `no-cors` check applies to document navigation such that if it is
+ // an document navigation, this check should return true to allow
+ // credentials.
+ if (loadInfo->GetExternalContentPolicyType() ==
+ ExtContentPolicy::TYPE_DOCUMENT ||
+ loadInfo->GetExternalContentPolicyType() ==
+ ExtContentPolicy::TYPE_SUBDOCUMENT ||
+ loadInfo->GetExternalContentPolicyType() ==
+ ExtContentPolicy::TYPE_WEBSOCKET) {
+ return true;
+ }
+
+ if (loadInfo->GetSecurityMode() !=
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL &&
+ loadInfo->GetSecurityMode() !=
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT) {
+ return true;
+ }
+
+ // If request’s client’s policy container’s embedder policy’s value is not
+ // "credentialless", then return true.
+ if (loadInfo->GetLoadingEmbedderPolicy() !=
+ nsILoadInfo::EMBEDDER_POLICY_CREDENTIALLESS) {
+ return true;
+ }
+
+ // If request’s origin is same origin with request’s current URL’s origin and
+ // request does not have a redirect-tainted origin, then return true.
+ nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager();
+ nsCOMPtr<nsIPrincipal> resourcePrincipal;
+ ssm->GetChannelURIPrincipal(aChannel, getter_AddRefs(resourcePrincipal));
+
+ bool sameOrigin = resourcePrincipal->Equals(loadInfo->TriggeringPrincipal());
+ nsAutoCString serializedOrigin;
+ GetSerializedOrigin(loadInfo->TriggeringPrincipal(), resourcePrincipal,
+ serializedOrigin, loadInfo);
+ if (sameOrigin && !serializedOrigin.IsEmpty()) {
+ return true;
+ }
+
+ return false;
+}
+
+// https://fetch.spec.whatwg.org/#serializing-a-request-origin
+void nsContentSecurityManager::GetSerializedOrigin(
+ nsIPrincipal* aOrigin, nsIPrincipal* aResourceOrigin,
+ nsACString& aSerializedOrigin, nsILoadInfo* aLoadInfo) {
+ // The following for loop performs the
+ // https://fetch.spec.whatwg.org/#ref-for-concept-request-tainted-origin
+ nsCOMPtr<nsIPrincipal> lastOrigin;
+ for (nsIRedirectHistoryEntry* entry : aLoadInfo->RedirectChain()) {
+ if (!lastOrigin) {
+ entry->GetPrincipal(getter_AddRefs(lastOrigin));
+ continue;
+ }
+
+ nsCOMPtr<nsIPrincipal> currentOrigin;
+ entry->GetPrincipal(getter_AddRefs(currentOrigin));
+
+ if (!currentOrigin->Equals(lastOrigin) && !lastOrigin->Equals(aOrigin)) {
+ aSerializedOrigin.AssignLiteral("null");
+ return;
+ }
+ lastOrigin = currentOrigin;
+ }
+
+ // When the redirectChain is empty, it means this is the first redirect.
+ // So according to the #serializing-a-request-origin spec, we don't
+ // have a redirect-tainted origin, so we return the origin of the request
+ // here.
+ if (!lastOrigin) {
+ aOrigin->GetWebExposedOriginSerialization(aSerializedOrigin);
+ return;
+ }
+
+ // Same as above, redirectChain doesn't contain the current redirect,
+ // so we have to do the check one last time here.
+ if (!lastOrigin->Equals(aResourceOrigin) && !lastOrigin->Equals(aOrigin)) {
+ aSerializedOrigin.AssignLiteral("null");
+ return;
+ }
+
+ aOrigin->GetWebExposedOriginSerialization(aSerializedOrigin);
+}
+
+// https://html.spec.whatwg.org/multipage/browsers.html#compatible-with-cross-origin-isolation
+bool nsContentSecurityManager::IsCompatibleWithCrossOriginIsolation(
+ nsILoadInfo::CrossOriginEmbedderPolicy aPolicy) {
+ return aPolicy == nsILoadInfo::EMBEDDER_POLICY_CREDENTIALLESS ||
+ aPolicy == nsILoadInfo::EMBEDDER_POLICY_REQUIRE_CORP;
+}
+
+// ==== nsIContentSecurityManager implementation =====
+
+NS_IMETHODIMP
+nsContentSecurityManager::PerformSecurityCheck(
+ nsIChannel* aChannel, nsIStreamListener* aStreamListener,
+ nsIStreamListener** outStreamListener) {
+ nsCOMPtr<nsIStreamListener> inAndOutListener = aStreamListener;
+ nsresult rv = doContentSecurityCheck(aChannel, inAndOutListener);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ inAndOutListener.forget(outStreamListener);
+ return NS_OK;
+}
diff --git a/dom/security/nsContentSecurityManager.h b/dom/security/nsContentSecurityManager.h
new file mode 100644
index 0000000000..17d42e9676
--- /dev/null
+++ b/dom/security/nsContentSecurityManager.h
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsContentSecurityManager_h___
+#define nsContentSecurityManager_h___
+
+#include "mozilla/CORSMode.h"
+#include "nsIContentSecurityManager.h"
+#include "nsIChannel.h"
+#include "nsIChannelEventSink.h"
+#include "nsILoadInfo.h"
+
+class nsILoadInfo;
+class nsIStreamListener;
+
+#define NS_CONTENTSECURITYMANAGER_CONTRACTID \
+ "@mozilla.org/contentsecuritymanager;1"
+// cdcc1ab8-3cea-4e6c-a294-a651fa35227f
+#define NS_CONTENTSECURITYMANAGER_CID \
+ { \
+ 0xcdcc1ab8, 0x3cea, 0x4e6c, { \
+ 0xa2, 0x94, 0xa6, 0x51, 0xfa, 0x35, 0x22, 0x7f \
+ } \
+ }
+
+class nsContentSecurityManager : public nsIContentSecurityManager,
+ public nsIChannelEventSink {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICONTENTSECURITYMANAGER
+ NS_DECL_NSICHANNELEVENTSINK
+
+ nsContentSecurityManager() = default;
+
+ static nsresult doContentSecurityCheck(
+ nsIChannel* aChannel, nsCOMPtr<nsIStreamListener>& aInAndOutListener);
+
+ static bool AllowTopLevelNavigationToDataURI(nsIChannel* aChannel);
+ static void ReportBlockedDataURI(nsIURI* aURI, nsILoadInfo* aLoadInfo,
+ bool aIsRedirect = false);
+ static bool AllowInsecureRedirectToDataURI(nsIChannel* aNewChannel);
+ static void MeasureUnexpectedPrivilegedLoads(nsILoadInfo* aLoadInfo,
+ nsIURI* aFinalURI,
+ const nsACString& aRemoteType);
+
+ enum CORSSecurityMapping {
+ // Disables all CORS checking overriding the value of aCORSMode. All checks
+ // are disabled even when CORSMode::CORS_ANONYMOUS or
+ // CORSMode::CORS_USE_CREDENTIALS is passed. This is mostly used for chrome
+ // code, where we don't need security checks. See
+ // SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL for the detailed explanation
+ // of the security mode.
+ DISABLE_CORS_CHECKS,
+ // Disables all CORS checking on CORSMode::CORS_NONE. The other two CORS
+ // modes CORSMode::CORS_ANONYMOUS and CORSMode::CORS_USE_CREDENTIALS are
+ // respected.
+ CORS_NONE_MAPS_TO_DISABLED_CORS_CHECKS,
+ // Allow load from any origin, but cross-origin requests require CORS. See
+ // SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT. Like above the other two
+ // CORS modes are unaffected and get parsed.
+ CORS_NONE_MAPS_TO_INHERITED_CONTEXT,
+ // Always require the server to acknowledge the request via CORS.
+ // CORSMode::CORS_NONE is parsed as if CORSMode::CORS_ANONYMOUS is passed.
+ REQUIRE_CORS_CHECKS,
+ };
+
+ // computes the security flags for the requested CORS mode
+ // @param aCORSSecurityMapping: See CORSSecurityMapping for variant
+ // descriptions
+ static nsSecurityFlags ComputeSecurityFlags(
+ mozilla::CORSMode aCORSMode, CORSSecurityMapping aCORSSecurityMapping);
+
+ static void GetSerializedOrigin(nsIPrincipal* aOrigin,
+ nsIPrincipal* aResourceOrigin,
+ nsACString& aResult, nsILoadInfo* aLoadInfo);
+
+ // https://html.spec.whatwg.org/multipage/browsers.html#compatible-with-cross-origin-isolation
+ static bool IsCompatibleWithCrossOriginIsolation(
+ nsILoadInfo::CrossOriginEmbedderPolicy aPolicy);
+
+ private:
+ static nsresult CheckChannel(nsIChannel* aChannel);
+ static nsresult CheckAllowLoadInSystemPrivilegedContext(nsIChannel* aChannel);
+ static nsresult CheckAllowLoadInPrivilegedAboutContext(nsIChannel* aChannel);
+ static nsresult CheckChannelHasProtocolSecurityFlag(nsIChannel* aChannel);
+ static bool CrossOriginEmbedderPolicyAllowsCredentials(nsIChannel* aChannel);
+
+ virtual ~nsContentSecurityManager() = default;
+};
+
+#endif /* nsContentSecurityManager_h___ */
diff --git a/dom/security/nsContentSecurityUtils.cpp b/dom/security/nsContentSecurityUtils.cpp
new file mode 100644
index 0000000000..a483522499
--- /dev/null
+++ b/dom/security/nsContentSecurityUtils.cpp
@@ -0,0 +1,1719 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* A namespace class for static content security utilities. */
+
+#include "nsContentSecurityUtils.h"
+
+#include "mozilla/Components.h"
+#include "mozilla/dom/nsMixedContentBlocker.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/WorkerCommon.h"
+#include "mozilla/dom/WorkerPrivate.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIContentSecurityPolicy.h"
+#include "nsIChannel.h"
+#include "nsIHttpChannel.h"
+#include "nsIMultiPartChannel.h"
+#include "nsIURI.h"
+#include "nsITransfer.h"
+#include "nsNetUtil.h"
+#include "nsSandboxFlags.h"
+#if defined(XP_WIN)
+# include "mozilla/WinHeaderOnlyUtils.h"
+# include "WinUtils.h"
+# include <wininet.h>
+#endif
+
+#include "FramingChecker.h"
+#include "js/Array.h" // JS::GetArrayLength
+#include "js/ContextOptions.h"
+#include "js/PropertyAndElement.h" // JS_GetElement
+#include "js/RegExp.h"
+#include "js/RegExpFlags.h" // JS::RegExpFlags
+#include "js/friend/ErrorMessages.h" // JSMSG_UNSAFE_FILENAME
+#include "mozilla/ExtensionPolicyService.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/nsCSPContext.h"
+#include "mozilla/StaticPrefs_security.h"
+#include "LoadInfo.h"
+#include "mozilla/StaticPrefs_extensions.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TelemetryComms.h"
+#include "mozilla/TelemetryEventEnums.h"
+#include "nsIConsoleService.h"
+#include "nsIStringBundle.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::Telemetry;
+
+extern mozilla::LazyLogModule sCSMLog;
+extern Atomic<bool, mozilla::Relaxed> sJSHacksChecked;
+extern Atomic<bool, mozilla::Relaxed> sJSHacksPresent;
+extern Atomic<bool, mozilla::Relaxed> sCSSHacksChecked;
+extern Atomic<bool, mozilla::Relaxed> sCSSHacksPresent;
+extern Atomic<bool, mozilla::Relaxed> sTelemetryEventEnabled;
+
+// Helper function for IsConsideredSameOriginForUIR which makes
+// Principals of scheme 'http' return Principals of scheme 'https'.
+static already_AddRefed<nsIPrincipal> MakeHTTPPrincipalHTTPS(
+ nsIPrincipal* aPrincipal) {
+ nsCOMPtr<nsIPrincipal> principal = aPrincipal;
+ // if the principal is not http, then it can also not be upgraded
+ // to https.
+ if (!principal->SchemeIs("http")) {
+ return principal.forget();
+ }
+
+ nsAutoCString spec;
+ aPrincipal->GetAsciiSpec(spec);
+ // replace http with https
+ spec.ReplaceLiteral(0, 4, "https");
+
+ nsCOMPtr<nsIURI> newURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(newURI), spec);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ mozilla::OriginAttributes OA =
+ BasePrincipal::Cast(aPrincipal)->OriginAttributesRef();
+
+ principal = BasePrincipal::CreateContentPrincipal(newURI, OA);
+ return principal.forget();
+}
+
+/* static */
+bool nsContentSecurityUtils::IsConsideredSameOriginForUIR(
+ nsIPrincipal* aTriggeringPrincipal, nsIPrincipal* aResultPrincipal) {
+ MOZ_ASSERT(aTriggeringPrincipal);
+ MOZ_ASSERT(aResultPrincipal);
+ // we only have to make sure that the following truth table holds:
+ // aTriggeringPrincipal | aResultPrincipal | Result
+ // ----------------------------------------------------------------
+ // http://example.com/foo.html | http://example.com/bar.html | true
+ // http://example.com/foo.html | https://example.com/bar.html | true
+ // https://example.com/foo.html | https://example.com/bar.html | true
+ // https://example.com/foo.html | http://example.com/bar.html | true
+
+ // fast path if both principals are same-origin
+ if (aTriggeringPrincipal->Equals(aResultPrincipal)) {
+ return true;
+ }
+
+ // in case a principal uses a scheme of 'http' then we just upgrade to
+ // 'https' and use the principal equals comparison operator to check
+ // for same-origin.
+ nsCOMPtr<nsIPrincipal> compareTriggeringPrincipal =
+ MakeHTTPPrincipalHTTPS(aTriggeringPrincipal);
+
+ nsCOMPtr<nsIPrincipal> compareResultPrincipal =
+ MakeHTTPPrincipalHTTPS(aResultPrincipal);
+
+ return compareTriggeringPrincipal->Equals(compareResultPrincipal);
+}
+
+/*
+ * Performs a Regular Expression match, optionally returning the results.
+ * This function is not safe to use OMT.
+ *
+ * @param aPattern The regex pattern
+ * @param aString The string to compare against
+ * @param aOnlyMatch Whether we want match results or only a true/false for
+ * the match
+ * @param aMatchResult Out param for whether or not the pattern matched
+ * @param aRegexResults Out param for the matches of the regex, if requested
+ * @returns nsresult indicating correct function operation or error
+ */
+nsresult RegexEval(const nsAString& aPattern, const nsAString& aString,
+ bool aOnlyMatch, bool& aMatchResult,
+ nsTArray<nsString>* aRegexResults = nullptr) {
+ MOZ_ASSERT(NS_IsMainThread());
+ aMatchResult = false;
+
+ mozilla::dom::AutoJSAPI jsapi;
+ jsapi.Init();
+
+ JSContext* cx = jsapi.cx();
+ mozilla::AutoDisableJSInterruptCallback disabler(cx);
+
+ // We can use the junk scope here, because we're just using it for regexp
+ // evaluation, not actual script execution, and we disable statics so that the
+ // evaluation does not interact with the execution global.
+ JSAutoRealm ar(cx, xpc::PrivilegedJunkScope());
+
+ JS::Rooted<JSObject*> regexp(
+ cx, JS::NewUCRegExpObject(cx, aPattern.BeginReading(), aPattern.Length(),
+ JS::RegExpFlag::Unicode));
+ if (!regexp) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ JS::Rooted<JS::Value> regexResult(cx, JS::NullValue());
+
+ size_t index = 0;
+ if (!JS::ExecuteRegExpNoStatics(cx, regexp, aString.BeginReading(),
+ aString.Length(), &index, aOnlyMatch,
+ &regexResult)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (regexResult.isNull()) {
+ // On no match, ExecuteRegExpNoStatics returns Null
+ return NS_OK;
+ }
+ if (aOnlyMatch) {
+ // On match, with aOnlyMatch = true, ExecuteRegExpNoStatics returns boolean
+ // true.
+ MOZ_ASSERT(regexResult.isBoolean() && regexResult.toBoolean());
+ aMatchResult = true;
+ return NS_OK;
+ }
+ if (aRegexResults == nullptr) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // Now we know we have a result, and we need to extract it so we can read it.
+ uint32_t length;
+ JS::Rooted<JSObject*> regexResultObj(cx, &regexResult.toObject());
+ if (!JS::GetArrayLength(cx, regexResultObj, &length)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ MOZ_LOG(sCSMLog, LogLevel::Verbose, ("Regex Matched %i strings", length));
+
+ for (uint32_t i = 0; i < length; i++) {
+ JS::Rooted<JS::Value> element(cx);
+ if (!JS_GetElement(cx, regexResultObj, i, &element)) {
+ return NS_ERROR_NO_CONTENT;
+ }
+
+ nsAutoJSString value;
+ if (!value.init(cx, element)) {
+ return NS_ERROR_NO_CONTENT;
+ }
+
+ MOZ_LOG(sCSMLog, LogLevel::Verbose,
+ ("Regex Matching: %i: %s", i, NS_ConvertUTF16toUTF8(value).get()));
+ aRegexResults->AppendElement(value);
+ }
+
+ aMatchResult = true;
+ return NS_OK;
+}
+
+/*
+ * MOZ_CRASH_UNSAFE_PRINTF has a sPrintfCrashReasonSize-sized buffer. We need
+ * to make sure we don't exceed it. These functions perform this check and
+ * munge things for us.
+ *
+ */
+
+/*
+ * Destructively truncates a string to fit within the limit
+ */
+char* nsContentSecurityUtils::SmartFormatCrashString(const char* str) {
+ return nsContentSecurityUtils::SmartFormatCrashString(strdup(str));
+}
+
+char* nsContentSecurityUtils::SmartFormatCrashString(char* str) {
+ auto str_len = strlen(str);
+
+ if (str_len > sPrintfCrashReasonSize) {
+ str[sPrintfCrashReasonSize - 1] = '\0';
+ str_len = strlen(str);
+ }
+ MOZ_RELEASE_ASSERT(sPrintfCrashReasonSize > str_len);
+
+ return str;
+}
+
+/*
+ * Destructively truncates two strings to fit within the limit.
+ * format_string is a format string containing two %s entries
+ * The second string will be truncated to the _last_ 25 characters
+ * The first string will be truncated to the remaining limit.
+ */
+nsCString nsContentSecurityUtils::SmartFormatCrashString(
+ const char* part1, const char* part2, const char* format_string) {
+ return SmartFormatCrashString(strdup(part1), strdup(part2), format_string);
+}
+
+nsCString nsContentSecurityUtils::SmartFormatCrashString(
+ char* part1, char* part2, const char* format_string) {
+ auto part1_len = strlen(part1);
+ auto part2_len = strlen(part2);
+
+ auto constant_len = strlen(format_string) - 4;
+
+ if (part1_len + part2_len + constant_len > sPrintfCrashReasonSize) {
+ if (part2_len > 25) {
+ part2 += (part2_len - 25);
+ }
+ part2_len = strlen(part2);
+
+ part1[sPrintfCrashReasonSize - (constant_len + part2_len + 1)] = '\0';
+ part1_len = strlen(part1);
+ }
+ MOZ_RELEASE_ASSERT(sPrintfCrashReasonSize >
+ constant_len + part1_len + part2_len);
+
+ auto parts = nsPrintfCString(format_string, part1, part2);
+ return std::move(parts);
+}
+
+/*
+ * Telemetry Events extra data only supports 80 characters, so we optimize the
+ * filename to be smaller and collect more data.
+ */
+nsString OptimizeFileName(const nsAString& aFileName) {
+ nsString optimizedName(aFileName);
+
+ MOZ_LOG(
+ sCSMLog, LogLevel::Verbose,
+ ("Optimizing FileName: %s", NS_ConvertUTF16toUTF8(optimizedName).get()));
+
+ optimizedName.ReplaceSubstring(u".xpi!"_ns, u"!"_ns);
+ optimizedName.ReplaceSubstring(u"shield.mozilla.org!"_ns, u"s!"_ns);
+ optimizedName.ReplaceSubstring(u"mozilla.org!"_ns, u"m!"_ns);
+ if (optimizedName.Length() > 80) {
+ optimizedName.Truncate(80);
+ }
+
+ MOZ_LOG(
+ sCSMLog, LogLevel::Verbose,
+ ("Optimized FileName: %s", NS_ConvertUTF16toUTF8(optimizedName).get()));
+ return optimizedName;
+}
+
+/*
+ * FilenameToFilenameType takes a fileName and returns a Pair of strings.
+ * The First entry is a string indicating the type of fileName
+ * The Second entry is a Maybe<string> that can contain additional details to
+ * report.
+ *
+ * The reason we use strings (instead of an int/enum) is because the Telemetry
+ * Events API only accepts strings.
+ *
+ * Function is a static member of the class to enable gtests.
+ */
+
+/* static */
+FilenameTypeAndDetails nsContentSecurityUtils::FilenameToFilenameType(
+ const nsString& fileName, bool collectAdditionalExtensionData) {
+ // These are strings because the Telemetry Events API only accepts strings
+ static constexpr auto kChromeURI = "chromeuri"_ns;
+ static constexpr auto kResourceURI = "resourceuri"_ns;
+ static constexpr auto kBlobUri = "bloburi"_ns;
+ static constexpr auto kDataUri = "dataurl"_ns;
+ static constexpr auto kAboutUri = "abouturi"_ns;
+ static constexpr auto kDataUriWebExtCStyle =
+ "dataurl-extension-contentstyle"_ns;
+ static constexpr auto kSingleString = "singlestring"_ns;
+ static constexpr auto kMozillaExtensionFile = "mozillaextension_file"_ns;
+ static constexpr auto kOtherExtensionFile = "otherextension_file"_ns;
+ static constexpr auto kExtensionURI = "extension_uri"_ns;
+ static constexpr auto kSuspectedUserChromeJS = "suspectedUserChromeJS"_ns;
+#if defined(XP_WIN)
+ static constexpr auto kSanitizedWindowsURL = "sanitizedWindowsURL"_ns;
+ static constexpr auto kSanitizedWindowsPath = "sanitizedWindowsPath"_ns;
+#endif
+ static constexpr auto kOther = "other"_ns;
+ static constexpr auto kOtherWorker = "other-on-worker"_ns;
+ static constexpr auto kRegexFailure = "regexfailure"_ns;
+
+ static constexpr auto kUCJSRegex = u"(.+).uc.js\\?*[0-9]*$"_ns;
+ static constexpr auto kExtensionRegex = u"extensions/(.+)@(.+)!(.+)$"_ns;
+ static constexpr auto kSingleFileRegex = u"^[a-zA-Z0-9.?]+$"_ns;
+
+ if (fileName.IsEmpty()) {
+ return FilenameTypeAndDetails(kOther, Nothing());
+ }
+
+ // resource:// and chrome://
+ if (StringBeginsWith(fileName, u"chrome://"_ns)) {
+ return FilenameTypeAndDetails(kChromeURI, Some(fileName));
+ }
+ if (StringBeginsWith(fileName, u"resource://"_ns)) {
+ return FilenameTypeAndDetails(kResourceURI, Some(fileName));
+ }
+
+ // blob: and data:
+ if (StringBeginsWith(fileName, u"blob:"_ns)) {
+ return FilenameTypeAndDetails(kBlobUri, Nothing());
+ }
+ if (StringBeginsWith(fileName, u"data:text/css;extension=style;"_ns)) {
+ return FilenameTypeAndDetails(kDataUriWebExtCStyle, Nothing());
+ }
+ if (StringBeginsWith(fileName, u"data:"_ns)) {
+ return FilenameTypeAndDetails(kDataUri, Nothing());
+ }
+
+ // Can't do regex matching off-main-thread
+ if (NS_IsMainThread()) {
+ // Extension as loaded via a file://
+ bool regexMatch;
+ nsTArray<nsString> regexResults;
+ nsresult rv = RegexEval(kExtensionRegex, fileName, /* aOnlyMatch = */ false,
+ regexMatch, &regexResults);
+ if (NS_FAILED(rv)) {
+ return FilenameTypeAndDetails(kRegexFailure, Nothing());
+ }
+ if (regexMatch) {
+ nsCString type = StringEndsWith(regexResults[2], u"mozilla.org.xpi"_ns)
+ ? kMozillaExtensionFile
+ : kOtherExtensionFile;
+ const auto& extensionNameAndPath =
+ Substring(regexResults[0], ArrayLength("extensions/") - 1);
+ return FilenameTypeAndDetails(
+ type, Some(OptimizeFileName(extensionNameAndPath)));
+ }
+
+ // Single File
+ rv = RegexEval(kSingleFileRegex, fileName, /* aOnlyMatch = */ true,
+ regexMatch);
+ if (NS_FAILED(rv)) {
+ return FilenameTypeAndDetails(kRegexFailure, Nothing());
+ }
+ if (regexMatch) {
+ return FilenameTypeAndDetails(kSingleString, Some(fileName));
+ }
+
+ // Suspected userChromeJS script
+ rv = RegexEval(kUCJSRegex, fileName, /* aOnlyMatch = */ true, regexMatch);
+ if (NS_FAILED(rv)) {
+ return FilenameTypeAndDetails(kRegexFailure, Nothing());
+ }
+ if (regexMatch) {
+ return FilenameTypeAndDetails(kSuspectedUserChromeJS, Nothing());
+ }
+ }
+
+ // Something loaded via an about:// URI.
+ if (StringBeginsWith(fileName, u"about:"_ns)) {
+ // Remove any querystrings and such
+ long int desired_length = fileName.Length();
+ long int possible_new_length = 0;
+
+ possible_new_length = fileName.FindChar('?');
+ if (possible_new_length != -1 && possible_new_length < desired_length) {
+ desired_length = possible_new_length;
+ }
+
+ possible_new_length = fileName.FindChar('#');
+ if (possible_new_length != -1 && possible_new_length < desired_length) {
+ desired_length = possible_new_length;
+ }
+
+ auto subFileName = Substring(fileName, 0, desired_length);
+
+ return FilenameTypeAndDetails(kAboutUri, Some(subFileName));
+ }
+
+ // Something loaded via a moz-extension:// URI.
+ if (StringBeginsWith(fileName, u"moz-extension://"_ns)) {
+ if (!collectAdditionalExtensionData) {
+ return FilenameTypeAndDetails(kExtensionURI, Nothing());
+ }
+
+ nsAutoString sanitizedPathAndScheme;
+ sanitizedPathAndScheme.Append(u"moz-extension://["_ns);
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), fileName);
+ if (NS_FAILED(rv)) {
+ // Return after adding ://[ so we know we failed here.
+ return FilenameTypeAndDetails(kExtensionURI,
+ Some(sanitizedPathAndScheme));
+ }
+
+ mozilla::extensions::URLInfo url(uri);
+ if (NS_IsMainThread()) {
+ // EPS is only usable on main thread
+ auto* policy =
+ ExtensionPolicyService::GetSingleton().GetByHost(url.Host());
+ if (policy) {
+ nsString addOnId;
+ policy->GetId(addOnId);
+
+ sanitizedPathAndScheme.Append(addOnId);
+ sanitizedPathAndScheme.Append(u": "_ns);
+ sanitizedPathAndScheme.Append(policy->Name());
+ sanitizedPathAndScheme.Append(u"]"_ns);
+
+ if (policy->IsPrivileged()) {
+ sanitizedPathAndScheme.Append(u"P=1"_ns);
+ } else {
+ sanitizedPathAndScheme.Append(u"P=0"_ns);
+ }
+ } else {
+ sanitizedPathAndScheme.Append(u"failed finding addon by host]"_ns);
+ }
+ } else {
+ sanitizedPathAndScheme.Append(u"can't get addon off main thread]"_ns);
+ }
+
+ AppendUTF8toUTF16(url.FilePath(), sanitizedPathAndScheme);
+ return FilenameTypeAndDetails(kExtensionURI, Some(sanitizedPathAndScheme));
+ }
+
+#if defined(XP_WIN)
+ auto flags = mozilla::widget::WinUtils::PathTransformFlags::Default |
+ mozilla::widget::WinUtils::PathTransformFlags::RequireFilePath;
+ nsAutoString strSanitizedPath(fileName);
+ if (widget::WinUtils::PreparePathForTelemetry(strSanitizedPath, flags)) {
+ DWORD cchDecodedUrl = INTERNET_MAX_URL_LENGTH;
+ WCHAR szOut[INTERNET_MAX_URL_LENGTH];
+ HRESULT hr;
+ SAFECALL_URLMON_FUNC(CoInternetParseUrl, fileName.get(), PARSE_SCHEMA, 0,
+ szOut, INTERNET_MAX_URL_LENGTH, &cchDecodedUrl, 0);
+ if (hr == S_OK && cchDecodedUrl) {
+ nsAutoString sanitizedPathAndScheme;
+ sanitizedPathAndScheme.Append(szOut);
+ if (sanitizedPathAndScheme == u"file"_ns) {
+ sanitizedPathAndScheme.Append(u"://.../"_ns);
+ sanitizedPathAndScheme.Append(strSanitizedPath);
+ }
+ return FilenameTypeAndDetails(kSanitizedWindowsURL,
+ Some(sanitizedPathAndScheme));
+ } else {
+ return FilenameTypeAndDetails(kSanitizedWindowsPath,
+ Some(strSanitizedPath));
+ }
+ }
+#endif
+
+ if (!NS_IsMainThread()) {
+ return FilenameTypeAndDetails(kOtherWorker, Nothing());
+ }
+ return FilenameTypeAndDetails(kOther, Nothing());
+}
+
+#if defined(EARLY_BETA_OR_EARLIER)
+// Crash String must be safe from a telemetry point of view.
+// This will be ensured when this function is used.
+void PossiblyCrash(const char* aPrefSuffix, const char* aUnsafeCrashString,
+ const nsCString& aSafeCrashString) {
+ if (MOZ_UNLIKELY(!XRE_IsParentProcess())) {
+ // We only crash in the parent (unfortunately) because it's
+ // the only place we can be sure that our only-crash-once
+ // pref-writing works.
+ return;
+ }
+ if (!NS_IsMainThread()) {
+ // Setting a pref off the main thread causes ContentParent to observe the
+ // pref set, resulting in a Release Assertion when it tries to update the
+ // child off main thread. So don't do any of this off main thread. (Which
+ // is a bit of a blind spot for this purpose...)
+ return;
+ }
+
+ nsCString previous_crashes("security.crash_tracking.");
+ previous_crashes.Append(aPrefSuffix);
+ previous_crashes.Append(".prevCrashes");
+
+ nsCString max_crashes("security.crash_tracking.");
+ max_crashes.Append(aPrefSuffix);
+ max_crashes.Append(".maxCrashes");
+
+ int32_t numberOfPreviousCrashes = 0;
+ numberOfPreviousCrashes = Preferences::GetInt(previous_crashes.get(), 0);
+
+ int32_t maxAllowableCrashes = 0;
+ maxAllowableCrashes = Preferences::GetInt(max_crashes.get(), 0);
+
+ if (numberOfPreviousCrashes >= maxAllowableCrashes) {
+ return;
+ }
+
+ nsresult rv =
+ Preferences::SetInt(previous_crashes.get(), ++numberOfPreviousCrashes);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ nsCOMPtr<nsIPrefService> prefsCom = Preferences::GetService();
+ Preferences* prefs = static_cast<Preferences*>(prefsCom.get());
+
+ if (!prefs->AllowOffMainThreadSave()) {
+ // Do not crash if we can't save prefs off the main thread
+ return;
+ }
+
+ rv = prefs->SavePrefFileBlocking();
+ if (!NS_FAILED(rv)) {
+ // We can only use this in local builds where we don't send stuff up to the
+ // crash reporter because it has user private data.
+ // MOZ_CRASH_UNSAFE_PRINTF("%s",
+ // nsContentSecurityUtils::SmartFormatCrashString(aUnsafeCrashString));
+ MOZ_CRASH_UNSAFE_PRINTF(
+ "%s",
+ nsContentSecurityUtils::SmartFormatCrashString(aSafeCrashString.get()));
+ }
+}
+#endif
+
+class EvalUsageNotificationRunnable final : public Runnable {
+ public:
+ EvalUsageNotificationRunnable(bool aIsSystemPrincipal,
+ NS_ConvertUTF8toUTF16& aFileNameA,
+ uint64_t aWindowID, uint32_t aLineNumber,
+ uint32_t aColumnNumber)
+ : mozilla::Runnable("EvalUsageNotificationRunnable"),
+ mIsSystemPrincipal(aIsSystemPrincipal),
+ mFileNameA(aFileNameA),
+ mWindowID(aWindowID),
+ mLineNumber(aLineNumber),
+ mColumnNumber(aColumnNumber) {}
+
+ NS_IMETHOD Run() override {
+ nsContentSecurityUtils::NotifyEvalUsage(
+ mIsSystemPrincipal, mFileNameA, mWindowID, mLineNumber, mColumnNumber);
+ return NS_OK;
+ }
+
+ void Revoke() {}
+
+ private:
+ bool mIsSystemPrincipal;
+ NS_ConvertUTF8toUTF16 mFileNameA;
+ uint64_t mWindowID;
+ uint32_t mLineNumber;
+ uint32_t mColumnNumber;
+};
+
+/* static */
+bool nsContentSecurityUtils::IsEvalAllowed(JSContext* cx,
+ bool aIsSystemPrincipal,
+ const nsAString& aScript) {
+ // This allowlist contains files that are permanently allowed to use
+ // eval()-like functions. It will ideally be restricted to files that are
+ // exclusively used in testing contexts.
+ static nsLiteralCString evalAllowlist[] = {
+ // Test-only third-party library
+ "resource://testing-common/sinon-7.2.7.js"_ns,
+ // Test-only utility
+ "resource://testing-common/content-task.js"_ns,
+
+ // Tracked by Bug 1584605
+ "resource://gre/modules/translation/cld-worker.js"_ns,
+
+ // require.js implements a script loader for workers. It uses eval
+ // to load the script; but injection is only possible in situations
+ // that you could otherwise control script that gets executed, so
+ // it is okay to allow eval() as it adds no additional attack surface.
+ // Bug 1584564 tracks requiring safe usage of require.js
+ "resource://gre/modules/workers/require.js"_ns,
+
+ // The profiler's symbolication code uses a wasm module to extract symbols
+ // from the binary files result of local builds.
+ // See bug 1777479
+ "resource://devtools/client/performance-new/shared/symbolication.sys.mjs"_ns,
+
+ // The Browser Toolbox/Console
+ "debugger"_ns,
+ };
+
+ // We also permit two specific idioms in eval()-like contexts. We'd like to
+ // elminate these too; but there are in-the-wild Mozilla privileged extensions
+ // that use them.
+ static constexpr auto sAllowedEval1 = u"this"_ns;
+ static constexpr auto sAllowedEval2 =
+ u"function anonymous(\n) {\nreturn this\n}"_ns;
+
+ if (MOZ_LIKELY(!aIsSystemPrincipal && !XRE_IsE10sParentProcess())) {
+ // We restrict eval in the system principal and parent process.
+ // Other uses (like web content and null principal) are allowed.
+ return true;
+ }
+
+ if (JS::ContextOptionsRef(cx).disableEvalSecurityChecks()) {
+ MOZ_LOG(sCSMLog, LogLevel::Debug,
+ ("Allowing eval() because this JSContext was set to allow it"));
+ return true;
+ }
+
+ if (aIsSystemPrincipal &&
+ StaticPrefs::security_allow_eval_with_system_principal()) {
+ MOZ_LOG(sCSMLog, LogLevel::Debug,
+ ("Allowing eval() with System Principal because allowing pref is "
+ "enabled"));
+ return true;
+ }
+
+ if (XRE_IsE10sParentProcess() &&
+ StaticPrefs::security_allow_eval_in_parent_process()) {
+ MOZ_LOG(sCSMLog, LogLevel::Debug,
+ ("Allowing eval() in parent process because allowing pref is "
+ "enabled"));
+ return true;
+ }
+
+ DetectJsHacks();
+ if (MOZ_UNLIKELY(sJSHacksPresent)) {
+ MOZ_LOG(
+ sCSMLog, LogLevel::Debug,
+ ("Allowing eval() %s because some "
+ "JS hacks may be present.",
+ (aIsSystemPrincipal ? "with System Principal" : "in parent process")));
+ return true;
+ }
+
+ if (XRE_IsE10sParentProcess() &&
+ !StaticPrefs::extensions_webextensions_remote()) {
+ MOZ_LOG(sCSMLog, LogLevel::Debug,
+ ("Allowing eval() in parent process because the web extension "
+ "process is disabled"));
+ return true;
+ }
+
+ // We permit these two common idioms to get access to the global JS object
+ if (!aScript.IsEmpty() &&
+ (aScript == sAllowedEval1 || aScript == sAllowedEval2)) {
+ MOZ_LOG(
+ sCSMLog, LogLevel::Debug,
+ ("Allowing eval() %s because a key string is "
+ "provided",
+ (aIsSystemPrincipal ? "with System Principal" : "in parent process")));
+ return true;
+ }
+
+ // Check the allowlist for the provided filename. getFilename is a helper
+ // function
+ nsAutoCString fileName;
+ uint32_t lineNumber = 0, columnNumber = 1;
+ nsJSUtils::GetCallingLocation(cx, fileName, &lineNumber, &columnNumber);
+ if (fileName.IsEmpty()) {
+ fileName = "unknown-file"_ns;
+ }
+
+ NS_ConvertUTF8toUTF16 fileNameA(fileName);
+ for (const nsLiteralCString& allowlistEntry : evalAllowlist) {
+ // checking if current filename begins with entry, because JS Engine
+ // gives us additional stuff for code inside eval or Function ctor
+ // e.g., "require.js > Function"
+ if (StringBeginsWith(fileName, allowlistEntry)) {
+ MOZ_LOG(sCSMLog, LogLevel::Debug,
+ ("Allowing eval() %s because the containing "
+ "file is in the allowlist",
+ (aIsSystemPrincipal ? "with System Principal"
+ : "in parent process")));
+ return true;
+ }
+ }
+
+ // Send Telemetry and Log to the Console
+ uint64_t windowID = nsJSUtils::GetCurrentlyRunningCodeInnerWindowID(cx);
+ if (NS_IsMainThread()) {
+ nsContentSecurityUtils::NotifyEvalUsage(aIsSystemPrincipal, fileNameA,
+ windowID, lineNumber, columnNumber);
+ } else {
+ auto runnable = new EvalUsageNotificationRunnable(
+ aIsSystemPrincipal, fileNameA, windowID, lineNumber, columnNumber);
+ NS_DispatchToMainThread(runnable);
+ }
+
+ // Log to MOZ_LOG
+ MOZ_LOG(sCSMLog, LogLevel::Error,
+ ("Blocking eval() %s from file %s and script "
+ "provided %s",
+ (aIsSystemPrincipal ? "with System Principal" : "in parent process"),
+ fileName.get(), NS_ConvertUTF16toUTF8(aScript).get()));
+
+ // Maybe Crash
+#if defined(DEBUG) || defined(FUZZING)
+ auto crashString = nsContentSecurityUtils::SmartFormatCrashString(
+ NS_ConvertUTF16toUTF8(aScript).get(), fileName.get(),
+ (aIsSystemPrincipal
+ ? "Blocking eval() with System Principal with script %s from file %s"
+ : "Blocking eval() in parent process with script %s from file %s"));
+ MOZ_CRASH_UNSAFE_PRINTF("%s", crashString.get());
+#endif
+
+ return false;
+}
+
+/* static */
+void nsContentSecurityUtils::NotifyEvalUsage(bool aIsSystemPrincipal,
+ NS_ConvertUTF8toUTF16& aFileNameA,
+ uint64_t aWindowID,
+ uint32_t aLineNumber,
+ uint32_t aColumnNumber) {
+ // Send Telemetry
+ Telemetry::EventID eventType =
+ aIsSystemPrincipal ? Telemetry::EventID::Security_Evalusage_Systemcontext
+ : Telemetry::EventID::Security_Evalusage_Parentprocess;
+
+ FilenameTypeAndDetails fileNameTypeAndDetails =
+ FilenameToFilenameType(aFileNameA, false);
+ mozilla::Maybe<nsTArray<EventExtraEntry>> extra;
+ if (fileNameTypeAndDetails.second.isSome()) {
+ extra = Some<nsTArray<EventExtraEntry>>({EventExtraEntry{
+ "fileinfo"_ns,
+ NS_ConvertUTF16toUTF8(fileNameTypeAndDetails.second.value())}});
+ } else {
+ extra = Nothing();
+ }
+ if (!sTelemetryEventEnabled.exchange(true)) {
+ sTelemetryEventEnabled = true;
+ Telemetry::SetEventRecordingEnabled("security"_ns, true);
+ }
+ Telemetry::RecordEvent(eventType, mozilla::Some(fileNameTypeAndDetails.first),
+ extra);
+
+ // Report an error to console
+ nsCOMPtr<nsIConsoleService> console(
+ do_GetService(NS_CONSOLESERVICE_CONTRACTID));
+ if (!console) {
+ return;
+ }
+ nsCOMPtr<nsIScriptError> error(do_CreateInstance(NS_SCRIPTERROR_CONTRACTID));
+ if (!error) {
+ return;
+ }
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsCOMPtr<nsIStringBundleService> stringService =
+ mozilla::components::StringBundle::Service();
+ if (!stringService) {
+ return;
+ }
+ stringService->CreateBundle(
+ "chrome://global/locale/security/security.properties",
+ getter_AddRefs(bundle));
+ if (!bundle) {
+ return;
+ }
+ nsAutoString message;
+ AutoTArray<nsString, 1> formatStrings = {aFileNameA};
+ nsresult rv = bundle->FormatStringFromName("RestrictBrowserEvalUsage",
+ formatStrings, message);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ rv = error->InitWithWindowID(message, aFileNameA, u""_ns, aLineNumber,
+ aColumnNumber, nsIScriptError::errorFlag,
+ "BrowserEvalUsage", aWindowID,
+ true /* From chrome context */);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ console->LogMessage(error);
+}
+
+// If we detect that one of the relevant prefs has been changed, reset
+// sJSHacksChecked to cause us to re-evaluate all the pref values.
+// This will stop us from crashing because a user enabled one of these
+// prefs during a session and then triggered the JavaScript load mitigation
+// (which can cause a crash).
+class JSHackPrefObserver final {
+ public:
+ JSHackPrefObserver() = default;
+ static void PrefChanged(const char* aPref, void* aData);
+
+ protected:
+ ~JSHackPrefObserver() = default;
+};
+
+// static
+void JSHackPrefObserver::PrefChanged(const char* aPref, void* aData) {
+ sJSHacksChecked = false;
+}
+
+static bool sJSHackObserverAdded = false;
+
+/* static */
+void nsContentSecurityUtils::DetectJsHacks() {
+ // We can only perform the check of this preference on the Main Thread
+ // (because a String-based preference check is only safe on Main Thread.)
+ // In theory, it would be possible that a separate thread could get here
+ // before the main thread, resulting in the other thread not being able to
+ // perform this check, but the odds of that are small (and probably zero.)
+ if (!NS_IsMainThread()) {
+ return;
+ }
+
+ // If the pref service isn't available, do nothing and re-do this later.
+ if (!Preferences::IsServiceAvailable()) {
+ return;
+ }
+
+ // No need to check again.
+ if (MOZ_LIKELY(sJSHacksChecked || sJSHacksPresent)) {
+ return;
+ }
+
+ static const char* kObservedPrefs[] = {
+ "xpinstall.signatures.required", "general.config.filename",
+ "autoadmin.global_config_url", "autoadmin.failover_to_cached", nullptr};
+ if (MOZ_UNLIKELY(!sJSHackObserverAdded)) {
+ Preferences::RegisterCallbacks(JSHackPrefObserver::PrefChanged,
+ kObservedPrefs);
+ sJSHackObserverAdded = true;
+ }
+
+ nsresult rv;
+ sJSHacksChecked = true;
+
+ // This preference is required by bootstrapLoader.xpi, which is an
+ // alternate way to load legacy-style extensions. It only works on
+ // DevEdition/Nightly.
+ bool xpinstallSignatures;
+ rv = Preferences::GetBool("xpinstall.signatures.required",
+ &xpinstallSignatures, PrefValueKind::Default);
+ if (!NS_FAILED(rv) && !xpinstallSignatures) {
+ sJSHacksPresent = true;
+ return;
+ }
+ rv = Preferences::GetBool("xpinstall.signatures.required",
+ &xpinstallSignatures, PrefValueKind::User);
+ if (!NS_FAILED(rv) && !xpinstallSignatures) {
+ sJSHacksPresent = true;
+ return;
+ }
+
+ // The content process code is probably safe to use for both, but
+ // this hack detection and related efforts has been very fragile so
+ // I'm being extra conservative.
+ if (XRE_IsParentProcess()) {
+ // This preference is a file used for autoconfiguration of Firefox
+ // by administrators. It has also been (ab)used by the userChromeJS
+ // project to run legacy-style 'extensions', some of which use eval,
+ // all of which run in the System Principal context.
+ nsAutoString jsConfigPref;
+ rv = Preferences::GetString("general.config.filename", jsConfigPref,
+ PrefValueKind::Default);
+ if (!NS_FAILED(rv) && !jsConfigPref.IsEmpty()) {
+ sJSHacksPresent = true;
+ return;
+ }
+ rv = Preferences::GetString("general.config.filename", jsConfigPref,
+ PrefValueKind::User);
+ if (!NS_FAILED(rv) && !jsConfigPref.IsEmpty()) {
+ sJSHacksPresent = true;
+ return;
+ }
+
+ // These preferences are for autoconfiguration of Firefox by admins.
+ // The first will load a file over the network; the second will
+ // fall back to a local file if the network is unavailable
+ nsAutoString configUrlPref;
+ rv = Preferences::GetString("autoadmin.global_config_url", configUrlPref,
+ PrefValueKind::Default);
+ if (!NS_FAILED(rv) && !configUrlPref.IsEmpty()) {
+ sJSHacksPresent = true;
+ return;
+ }
+ rv = Preferences::GetString("autoadmin.global_config_url", configUrlPref,
+ PrefValueKind::User);
+ if (!NS_FAILED(rv) && !configUrlPref.IsEmpty()) {
+ sJSHacksPresent = true;
+ return;
+ }
+
+ } else {
+ if (Preferences::HasDefaultValue("general.config.filename")) {
+ sJSHacksPresent = true;
+ return;
+ }
+ if (Preferences::HasUserValue("general.config.filename")) {
+ sJSHacksPresent = true;
+ return;
+ }
+ if (Preferences::HasDefaultValue("autoadmin.global_config_url")) {
+ sJSHacksPresent = true;
+ return;
+ }
+ if (Preferences::HasUserValue("autoadmin.global_config_url")) {
+ sJSHacksPresent = true;
+ return;
+ }
+ }
+
+ bool failOverToCache;
+ rv = Preferences::GetBool("autoadmin.failover_to_cached", &failOverToCache,
+ PrefValueKind::Default);
+ if (!NS_FAILED(rv) && failOverToCache) {
+ sJSHacksPresent = true;
+ return;
+ }
+ rv = Preferences::GetBool("autoadmin.failover_to_cached", &failOverToCache,
+ PrefValueKind::User);
+ if (!NS_FAILED(rv) && failOverToCache) {
+ sJSHacksPresent = true;
+ }
+}
+
+/* static */
+void nsContentSecurityUtils::DetectCssHacks() {
+ // We can only perform the check of this preference on the Main Thread
+ // It's possible that this function may therefore race and we expect the
+ // caller to ensure that the checks have actually happened.
+ if (!NS_IsMainThread()) {
+ return;
+ }
+
+ // If the pref service isn't available, do nothing and re-do this later.
+ if (!Preferences::IsServiceAvailable()) {
+ return;
+ }
+
+ // No need to check again.
+ if (MOZ_LIKELY(sCSSHacksChecked || sCSSHacksPresent)) {
+ return;
+ }
+
+ // This preference is a bool to see if userChrome css is loaded
+ bool customStylesPresent = Preferences::GetBool(
+ "toolkit.legacyUserProfileCustomizations.stylesheets", false);
+ if (customStylesPresent) {
+ sCSSHacksPresent = true;
+ }
+
+ sCSSHacksChecked = true;
+}
+
+/* static */
+nsresult nsContentSecurityUtils::GetHttpChannelFromPotentialMultiPart(
+ nsIChannel* aChannel, nsIHttpChannel** aHttpChannel) {
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
+ if (httpChannel) {
+ httpChannel.forget(aHttpChannel);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIMultiPartChannel> multipart = do_QueryInterface(aChannel);
+ if (!multipart) {
+ *aHttpChannel = nullptr;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIChannel> baseChannel;
+ nsresult rv = multipart->GetBaseChannel(getter_AddRefs(baseChannel));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ httpChannel = do_QueryInterface(baseChannel);
+ httpChannel.forget(aHttpChannel);
+
+ return NS_OK;
+}
+
+nsresult CheckCSPFrameAncestorPolicy(nsIChannel* aChannel,
+ nsIContentSecurityPolicy** aOutCSP) {
+ MOZ_ASSERT(aChannel);
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ ExtContentPolicyType contentType = loadInfo->GetExternalContentPolicyType();
+ // frame-ancestor check only makes sense for subdocument and object loads,
+ // if this is not a load of such type, there is nothing to do here.
+ if (contentType != ExtContentPolicy::TYPE_SUBDOCUMENT &&
+ contentType != ExtContentPolicy::TYPE_OBJECT) {
+ return NS_OK;
+ }
+
+ // CSP can only hang off an http channel, if this channel is not
+ // an http channel then there is nothing to do here,
+ // except with add-ons, where the CSP is stored in a WebExtensionPolicy.
+ nsCOMPtr<nsIHttpChannel> httpChannel;
+ nsresult rv = nsContentSecurityUtils::GetHttpChannelFromPotentialMultiPart(
+ aChannel, getter_AddRefs(httpChannel));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsAutoCString tCspHeaderValue, tCspROHeaderValue;
+ if (httpChannel) {
+ Unused << httpChannel->GetResponseHeader("content-security-policy"_ns,
+ tCspHeaderValue);
+
+ Unused << httpChannel->GetResponseHeader(
+ "content-security-policy-report-only"_ns, tCspROHeaderValue);
+
+ // if there are no CSP values, then there is nothing to do here.
+ if (tCspHeaderValue.IsEmpty() && tCspROHeaderValue.IsEmpty()) {
+ return NS_OK;
+ }
+ }
+
+ nsCOMPtr<nsIPrincipal> resultPrincipal;
+ rv = nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal(
+ aChannel, getter_AddRefs(resultPrincipal));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<extensions::WebExtensionPolicy> addonPolicy;
+ if (!httpChannel) {
+ addonPolicy = BasePrincipal::Cast(resultPrincipal)->AddonPolicy();
+ if (!addonPolicy) {
+ // Neither a HTTP channel, nor a moz-extension:-resource.
+ // CSP is not supported.
+ return NS_OK;
+ }
+ }
+
+ RefPtr<nsCSPContext> csp = new nsCSPContext();
+ // This CSPContext is only used for checking frame-ancestors, we
+ // will parse the CSP again anyway. (Unless this blocks the load, but
+ // parser warnings aren't really important in that case)
+ csp->SuppressParserLogMessages();
+
+ nsCOMPtr<nsIURI> selfURI;
+ nsAutoString referrerSpec;
+ if (httpChannel) {
+ aChannel->GetURI(getter_AddRefs(selfURI));
+ nsCOMPtr<nsIReferrerInfo> referrerInfo = httpChannel->GetReferrerInfo();
+ if (referrerInfo) {
+ referrerInfo->GetComputedReferrerSpec(referrerSpec);
+ }
+ } else {
+ // aChannel::GetURI would return the jar: or file:-URI for extensions.
+ // Use the "final" URI to get the actual moz-extension:-URL.
+ NS_GetFinalChannelURI(aChannel, getter_AddRefs(selfURI));
+ }
+
+ uint64_t innerWindowID = loadInfo->GetInnerWindowID();
+
+ rv = csp->SetRequestContextWithPrincipal(resultPrincipal, selfURI,
+ referrerSpec, innerWindowID);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (addonPolicy) {
+ csp->AppendPolicy(addonPolicy->BaseCSP(), false, false);
+ csp->AppendPolicy(addonPolicy->ExtensionPageCSP(), false, false);
+ } else {
+ NS_ConvertASCIItoUTF16 cspHeaderValue(tCspHeaderValue);
+ NS_ConvertASCIItoUTF16 cspROHeaderValue(tCspROHeaderValue);
+
+ // ----- if there's a full-strength CSP header, apply it.
+ if (!cspHeaderValue.IsEmpty()) {
+ rv = CSP_AppendCSPFromHeader(csp, cspHeaderValue, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // ----- if there's a report-only CSP header, apply it.
+ if (!cspROHeaderValue.IsEmpty()) {
+ rv = CSP_AppendCSPFromHeader(csp, cspROHeaderValue, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ // ----- Enforce frame-ancestor policy on any applied policies
+ bool safeAncestry = false;
+ // PermitsAncestry sends violation reports when necessary
+ rv = csp->PermitsAncestry(loadInfo, &safeAncestry);
+
+ if (NS_FAILED(rv) || !safeAncestry) {
+ // stop! ERROR page!
+ return NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION;
+ }
+
+ // return the CSP for x-frame-options check
+ csp.forget(aOutCSP);
+
+ return NS_OK;
+}
+
+void EnforceCSPFrameAncestorPolicy(nsIChannel* aChannel,
+ const nsresult& aError) {
+ if (aError == NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION) {
+ aChannel->Cancel(NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION);
+ }
+}
+
+void EnforceXFrameOptionsCheck(nsIChannel* aChannel,
+ nsIContentSecurityPolicy* aCsp) {
+ MOZ_ASSERT(aChannel);
+ bool isFrameOptionsIgnored = false;
+ // check for XFO options
+ // XFO checks can be skipped if there are frame ancestors
+ if (!FramingChecker::CheckFrameOptions(aChannel, aCsp,
+ isFrameOptionsIgnored)) {
+ // stop! ERROR page!
+ aChannel->Cancel(NS_ERROR_XFO_VIOLATION);
+ }
+
+ if (isFrameOptionsIgnored) {
+ // log warning to console that xfo is ignored because of CSP
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ uint64_t innerWindowID = loadInfo->GetInnerWindowID();
+ bool privateWindow = !!loadInfo->GetOriginAttributes().mPrivateBrowsingId;
+ AutoTArray<nsString, 2> params = {u"x-frame-options"_ns,
+ u"frame-ancestors"_ns};
+ CSP_LogLocalizedStr("IgnoringSrcBecauseOfDirective", params,
+ u""_ns, // no sourcefile
+ u""_ns, // no scriptsample
+ 0, // no linenumber
+ 1, // no columnnumber
+ nsIScriptError::warningFlag,
+ "IgnoringSrcBecauseOfDirective"_ns, innerWindowID,
+ privateWindow);
+ }
+}
+
+/* static */
+void nsContentSecurityUtils::PerformCSPFrameAncestorAndXFOCheck(
+ nsIChannel* aChannel) {
+ nsCOMPtr<nsIContentSecurityPolicy> csp;
+ nsresult rv = CheckCSPFrameAncestorPolicy(aChannel, getter_AddRefs(csp));
+
+ if (NS_FAILED(rv)) {
+ EnforceCSPFrameAncestorPolicy(aChannel, rv);
+ return;
+ }
+
+ // X-Frame-Options needs to be enforced after CSP frame-ancestors
+ // checks because if frame-ancestors is present, then x-frame-options
+ // will be discarded
+ EnforceXFrameOptionsCheck(aChannel, csp);
+}
+/* static */
+bool nsContentSecurityUtils::CheckCSPFrameAncestorAndXFO(nsIChannel* aChannel) {
+ nsCOMPtr<nsIContentSecurityPolicy> csp;
+ nsresult rv = CheckCSPFrameAncestorPolicy(aChannel, getter_AddRefs(csp));
+
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ bool isFrameOptionsIgnored = false;
+
+ return FramingChecker::CheckFrameOptions(aChannel, csp,
+ isFrameOptionsIgnored);
+}
+
+// https://w3c.github.io/webappsec-csp/#is-element-nonceable
+/* static */
+nsString nsContentSecurityUtils::GetIsElementNonceableNonce(
+ const Element& aElement) {
+ // Step 1. If element does not have an attribute named "nonce", return "Not
+ // Nonceable".
+ nsString nonce;
+ if (nsString* cspNonce =
+ static_cast<nsString*>(aElement.GetProperty(nsGkAtoms::nonce))) {
+ nonce = *cspNonce;
+ }
+ if (nonce.IsEmpty()) {
+ return nonce;
+ }
+
+ // Step 2. If element is a script element, then for each attribute of
+ // element’s attribute list:
+ if (nsCOMPtr<nsIScriptElement> script =
+ do_QueryInterface(const_cast<Element*>(&aElement))) {
+ auto containsScriptOrStyle = [](const nsAString& aStr) {
+ return aStr.LowerCaseFindASCII("<script") != kNotFound ||
+ aStr.LowerCaseFindASCII("<style") != kNotFound;
+ };
+
+ nsString value;
+ uint32_t i = 0;
+ while (BorrowedAttrInfo info = aElement.GetAttrInfoAt(i++)) {
+ // Step 2.1. If attribute’s name contains an ASCII case-insensitive match
+ // for "<script" or "<style", return "Not Nonceable".
+ const nsAttrName* name = info.mName;
+ if (nsAtom* prefix = name->GetPrefix()) {
+ if (containsScriptOrStyle(nsDependentAtomString(prefix))) {
+ return EmptyString();
+ }
+ }
+ if (containsScriptOrStyle(nsDependentAtomString(name->LocalName()))) {
+ return EmptyString();
+ }
+
+ // Step 2.2. If attribute’s value contains an ASCII case-insensitive match
+ // for "<script" or "<style", return "Not Nonceable".
+ info.mValue->ToString(value);
+ if (containsScriptOrStyle(value)) {
+ return EmptyString();
+ }
+ }
+ }
+
+ // Step 3. If element had a duplicate-attribute parse error during
+ // tokenization, return "Not Nonceable".
+ if (aElement.HasFlag(ELEMENT_PARSER_HAD_DUPLICATE_ATTR_ERROR)) {
+ return EmptyString();
+ }
+
+ // Step 4. Return "Nonceable".
+ return nonce;
+}
+
+#if defined(DEBUG)
+/* static */
+void nsContentSecurityUtils::AssertAboutPageHasCSP(Document* aDocument) {
+ // We want to get to a point where all about: pages ship with a CSP. This
+ // assertion ensures that we can not deploy new about: pages without a CSP.
+ // Please note that any about: page should not use inline JS or inline CSS,
+ // and instead should load JS and CSS from an external file (*.js, *.css)
+ // which allows us to apply a strong CSP omitting 'unsafe-inline'. Ideally,
+ // the CSP allows precisely the resources that need to be loaded; but it
+ // should at least be as strong as:
+ // <meta http-equiv="Content-Security-Policy" content="default-src chrome:;
+ // object-src 'none'"/>
+
+ // This is a data document, created using DOMParser or
+ // document.implementation.createDocument() or such, not an about: page which
+ // is loaded as a web page.
+ if (aDocument->IsLoadedAsData()) {
+ return;
+ }
+
+ // Check if we should skip the assertion
+ if (StaticPrefs::dom_security_skip_about_page_has_csp_assert()) {
+ return;
+ }
+
+ // Check if we are loading an about: URI at all
+ nsCOMPtr<nsIURI> documentURI = aDocument->GetDocumentURI();
+ if (!documentURI->SchemeIs("about")) {
+ return;
+ }
+
+ nsCOMPtr<nsIContentSecurityPolicy> csp = aDocument->GetCsp();
+ bool foundDefaultSrc = false;
+ bool foundObjectSrc = false;
+ bool foundUnsafeEval = false;
+ bool foundUnsafeInline = false;
+ bool foundScriptSrc = false;
+ bool foundWorkerSrc = false;
+ bool foundWebScheme = false;
+ if (csp) {
+ uint32_t policyCount = 0;
+ csp->GetPolicyCount(&policyCount);
+ nsAutoString parsedPolicyStr;
+ for (uint32_t i = 0; i < policyCount; ++i) {
+ csp->GetPolicyString(i, parsedPolicyStr);
+ if (parsedPolicyStr.Find(u"default-src") >= 0) {
+ foundDefaultSrc = true;
+ }
+ if (parsedPolicyStr.Find(u"object-src 'none'") >= 0) {
+ foundObjectSrc = true;
+ }
+ if (parsedPolicyStr.Find(u"'unsafe-eval'") >= 0) {
+ foundUnsafeEval = true;
+ }
+ if (parsedPolicyStr.Find(u"'unsafe-inline'") >= 0) {
+ foundUnsafeInline = true;
+ }
+ if (parsedPolicyStr.Find(u"script-src") >= 0) {
+ foundScriptSrc = true;
+ }
+ if (parsedPolicyStr.Find(u"worker-src") >= 0) {
+ foundWorkerSrc = true;
+ }
+ if (parsedPolicyStr.Find(u"http:") >= 0 ||
+ parsedPolicyStr.Find(u"https:") >= 0) {
+ foundWebScheme = true;
+ }
+ }
+ }
+
+ // Check if we should skip the allowlist and assert right away. Please note
+ // that this pref can and should only be set for automated testing.
+ if (StaticPrefs::dom_security_skip_about_page_csp_allowlist_and_assert()) {
+ NS_ASSERTION(foundDefaultSrc, "about: page must have a CSP");
+ return;
+ }
+
+ nsAutoCString aboutSpec;
+ documentURI->GetSpec(aboutSpec);
+ ToLowerCase(aboutSpec);
+
+ // This allowlist contains about: pages that are permanently allowed to
+ // render without a CSP applied.
+ static nsLiteralCString sAllowedAboutPagesWithNoCSP[] = {
+ // about:blank is a special about page -> no CSP
+ "about:blank"_ns,
+ // about:srcdoc is a special about page -> no CSP
+ "about:srcdoc"_ns,
+ // about:sync-log displays plain text only -> no CSP
+ "about:sync-log"_ns,
+ // about:logo just displays the firefox logo -> no CSP
+ "about:logo"_ns,
+ // about:sync is a special mozilla-signed developer addon with low usage ->
+ // no CSP
+ "about:sync"_ns,
+# if defined(ANDROID)
+ "about:config"_ns,
+# endif
+ };
+
+ for (const nsLiteralCString& allowlistEntry : sAllowedAboutPagesWithNoCSP) {
+ // please note that we perform a substring match here on purpose,
+ // so we don't have to deal and parse out all the query arguments
+ // the various about pages rely on.
+ if (StringBeginsWith(aboutSpec, allowlistEntry)) {
+ return;
+ }
+ }
+
+ MOZ_ASSERT(foundDefaultSrc,
+ "about: page must contain a CSP including default-src");
+ MOZ_ASSERT(foundObjectSrc,
+ "about: page must contain a CSP denying object-src");
+
+ // preferences and downloads allow legacy inline scripts through hash src.
+ MOZ_ASSERT(!foundScriptSrc ||
+ StringBeginsWith(aboutSpec, "about:preferences"_ns) ||
+ StringBeginsWith(aboutSpec, "about:downloads"_ns) ||
+ StringBeginsWith(aboutSpec, "about:asrouter"_ns) ||
+ StringBeginsWith(aboutSpec, "about:newtab"_ns) ||
+ StringBeginsWith(aboutSpec, "about:logins"_ns) ||
+ StringBeginsWith(aboutSpec, "about:compat"_ns) ||
+ StringBeginsWith(aboutSpec, "about:welcome"_ns) ||
+ StringBeginsWith(aboutSpec, "about:profiling"_ns) ||
+ StringBeginsWith(aboutSpec, "about:studies"_ns) ||
+ StringBeginsWith(aboutSpec, "about:home"_ns),
+ "about: page must not contain a CSP including script-src");
+
+ MOZ_ASSERT(!foundWorkerSrc,
+ "about: page must not contain a CSP including worker-src");
+
+ // addons, preferences, debugging, ion, devtools all have to allow some
+ // remote web resources
+ MOZ_ASSERT(!foundWebScheme ||
+ StringBeginsWith(aboutSpec, "about:preferences"_ns) ||
+ StringBeginsWith(aboutSpec, "about:addons"_ns) ||
+ StringBeginsWith(aboutSpec, "about:newtab"_ns) ||
+ StringBeginsWith(aboutSpec, "about:debugging"_ns) ||
+ StringBeginsWith(aboutSpec, "about:ion"_ns) ||
+ StringBeginsWith(aboutSpec, "about:compat"_ns) ||
+ StringBeginsWith(aboutSpec, "about:logins"_ns) ||
+ StringBeginsWith(aboutSpec, "about:home"_ns) ||
+ StringBeginsWith(aboutSpec, "about:welcome"_ns) ||
+ StringBeginsWith(aboutSpec, "about:devtools"_ns) ||
+ StringBeginsWith(aboutSpec, "about:pocket-saved"_ns) ||
+ StringBeginsWith(aboutSpec, "about:pocket-home"_ns),
+ "about: page must not contain a CSP including a web scheme");
+
+ if (aDocument->IsExtensionPage()) {
+ // Extensions have two CSP policies applied where the baseline CSP
+ // includes 'unsafe-eval' and 'unsafe-inline', hence we have to skip
+ // the 'unsafe-eval' and 'unsafe-inline' assertions for extension
+ // pages.
+ return;
+ }
+
+ MOZ_ASSERT(!foundUnsafeEval,
+ "about: page must not contain a CSP including 'unsafe-eval'");
+
+ static nsLiteralCString sLegacyUnsafeInlineAllowList[] = {
+ // Bug 1579160: Remove 'unsafe-inline' from style-src within
+ // about:preferences
+ "about:preferences"_ns,
+ // Bug 1571346: Remove 'unsafe-inline' from style-src within about:addons
+ "about:addons"_ns,
+ // Bug 1584485: Remove 'unsafe-inline' from style-src within:
+ // * about:newtab
+ // * about:welcome
+ // * about:home
+ "about:newtab"_ns,
+ "about:welcome"_ns,
+ "about:home"_ns,
+ };
+
+ for (const nsLiteralCString& aUnsafeInlineEntry :
+ sLegacyUnsafeInlineAllowList) {
+ // please note that we perform a substring match here on purpose,
+ // so we don't have to deal and parse out all the query arguments
+ // the various about pages rely on.
+ if (StringBeginsWith(aboutSpec, aUnsafeInlineEntry)) {
+ return;
+ }
+ }
+
+ MOZ_ASSERT(!foundUnsafeInline,
+ "about: page must not contain a CSP including 'unsafe-inline'");
+}
+#endif
+
+/* static */
+bool nsContentSecurityUtils::ValidateScriptFilename(JSContext* cx,
+ const char* aFilename) {
+ // If the pref is permissive, allow everything
+ if (StaticPrefs::security_allow_parent_unrestricted_js_loads()) {
+ return true;
+ }
+
+ // If we're not in the parent process allow everything (presently)
+ if (!XRE_IsE10sParentProcess()) {
+ return true;
+ }
+
+ // If we have allowed eval (because of a user configuration or more
+ // likely a test has requested it), and the script is an eval, allow it.
+ NS_ConvertUTF8toUTF16 filenameU(aFilename);
+ if (StaticPrefs::security_allow_eval_with_system_principal() ||
+ StaticPrefs::security_allow_eval_in_parent_process()) {
+ if (StringEndsWith(filenameU, u"> eval"_ns)) {
+ return true;
+ }
+ }
+
+ DetectJsHacks();
+
+ if (MOZ_UNLIKELY(!sJSHacksChecked)) {
+ MOZ_LOG(
+ sCSMLog, LogLevel::Debug,
+ ("Allowing a javascript load of %s because "
+ "we have not yet been able to determine if JS hacks may be present",
+ aFilename));
+ return true;
+ }
+
+ if (MOZ_UNLIKELY(sJSHacksPresent)) {
+ MOZ_LOG(sCSMLog, LogLevel::Debug,
+ ("Allowing a javascript load of %s because "
+ "some JS hacks may be present",
+ aFilename));
+ return true;
+ }
+
+ if (XRE_IsE10sParentProcess() &&
+ !StaticPrefs::extensions_webextensions_remote()) {
+ MOZ_LOG(sCSMLog, LogLevel::Debug,
+ ("Allowing a javascript load of %s because the web extension "
+ "process is disabled.",
+ aFilename));
+ return true;
+ }
+
+ if (StringBeginsWith(filenameU, u"chrome://"_ns)) {
+ // If it's a chrome:// url, allow it
+ return true;
+ }
+ if (StringBeginsWith(filenameU, u"resource://"_ns)) {
+ // If it's a resource:// url, allow it
+ return true;
+ }
+ if (StringBeginsWith(filenameU, u"file://"_ns)) {
+ // We will temporarily allow all file:// URIs through for now
+ return true;
+ }
+ if (StringBeginsWith(filenameU, u"jar:file://"_ns)) {
+ // We will temporarily allow all jar URIs through for now
+ return true;
+ }
+ if (filenameU.Equals(u"about:sync-log"_ns)) {
+ // about:sync-log runs in the parent process and displays a directory
+ // listing. The listing has inline javascript that executes on load.
+ return true;
+ }
+
+ if (StringBeginsWith(filenameU, u"moz-extension://"_ns)) {
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), aFilename);
+ if (!NS_FAILED(rv) && NS_IsMainThread()) {
+ mozilla::extensions::URLInfo url(uri);
+ auto* policy =
+ ExtensionPolicyService::GetSingleton().GetByHost(url.Host());
+
+ if (policy && policy->IsPrivileged()) {
+ MOZ_LOG(sCSMLog, LogLevel::Debug,
+ ("Allowing a javascript load of %s because the web extension "
+ "it is associated with is privileged.",
+ aFilename));
+ return true;
+ }
+ }
+ } else if (!NS_IsMainThread()) {
+ WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx);
+ if (workerPrivate && workerPrivate->IsPrivilegedAddonGlobal()) {
+ MOZ_LOG(sCSMLog, LogLevel::Debug,
+ ("Allowing a javascript load of %s because the web extension "
+ "it is associated with is privileged.",
+ aFilename));
+ return true;
+ }
+ }
+
+ auto kAllowedFilenamesExact = {
+ // Allow through the injection provided by about:sync addon
+ u"data:,new function() {\n const { AboutSyncRedirector } = ChromeUtils.import(\"chrome://aboutsync/content/AboutSyncRedirector.js\");\n AboutSyncRedirector.register();\n}"_ns,
+ };
+
+ for (auto allowedFilename : kAllowedFilenamesExact) {
+ if (filenameU == allowedFilename) {
+ return true;
+ }
+ }
+
+ auto kAllowedFilenamesPrefix = {
+ // Until 371900 is fixed, we need to do something about about:downloads
+ // and this is the most reasonable. See 1727770
+ u"about:downloads"_ns,
+ // We think this is the same problem as about:downloads
+ u"about:preferences"_ns,
+ // Browser console will give a filename of 'debugger' See 1763943
+ // Sometimes it's 'debugger eager eval code', other times just 'debugger
+ // eval code'
+ u"debugger"_ns};
+
+ for (auto allowedFilenamePrefix : kAllowedFilenamesPrefix) {
+ if (StringBeginsWith(filenameU, allowedFilenamePrefix)) {
+ return true;
+ }
+ }
+
+ // Log to MOZ_LOG
+ MOZ_LOG(sCSMLog, LogLevel::Error,
+ ("ValidateScriptFilename Failed: %s\n", aFilename));
+
+ // Send Telemetry
+ FilenameTypeAndDetails fileNameTypeAndDetails =
+ FilenameToFilenameType(filenameU, true);
+
+ Telemetry::EventID eventType =
+ Telemetry::EventID::Security_Javascriptload_Parentprocess;
+
+ mozilla::Maybe<nsTArray<EventExtraEntry>> extra;
+ if (fileNameTypeAndDetails.second.isSome()) {
+ extra = Some<nsTArray<EventExtraEntry>>({EventExtraEntry{
+ "fileinfo"_ns,
+ NS_ConvertUTF16toUTF8(fileNameTypeAndDetails.second.value())}});
+ } else {
+ extra = Nothing();
+ }
+
+ if (!sTelemetryEventEnabled.exchange(true)) {
+ sTelemetryEventEnabled = true;
+ Telemetry::SetEventRecordingEnabled("security"_ns, true);
+ }
+ Telemetry::RecordEvent(eventType, mozilla::Some(fileNameTypeAndDetails.first),
+ extra);
+
+#if defined(DEBUG) || defined(FUZZING)
+ auto crashString = nsContentSecurityUtils::SmartFormatCrashString(
+ aFilename,
+ fileNameTypeAndDetails.second.isSome()
+ ? NS_ConvertUTF16toUTF8(fileNameTypeAndDetails.second.value()).get()
+ : "(None)",
+ "Blocking a script load %s from file %s");
+ MOZ_CRASH_UNSAFE_PRINTF("%s", crashString.get());
+#elif defined(EARLY_BETA_OR_EARLIER)
+ // Cause a crash (if we've never crashed before and we can ensure we won't do
+ // it again.)
+ // The details in the second arg, passed to UNSAFE_PRINTF, are also included
+ // in Event Telemetry and have received data review.
+ if (fileNameTypeAndDetails.second.isSome()) {
+ PossiblyCrash("js_load_1", aFilename,
+ NS_ConvertUTF16toUTF8(fileNameTypeAndDetails.second.value()));
+ } else {
+ PossiblyCrash("js_load_1", aFilename, "(None)"_ns);
+ }
+#endif
+
+ // Presently we are only enforcing restrictions for the script filename
+ // on Nightly. On all channels we are reporting Telemetry. In the future we
+ // will assert in debug builds and return false to prevent execution in
+ // non-debug builds.
+#ifdef NIGHTLY_BUILD
+ return false;
+#else
+ return true;
+#endif
+}
+
+/* static */
+void nsContentSecurityUtils::LogMessageToConsole(nsIHttpChannel* aChannel,
+ const char* aMsg) {
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ uint64_t windowID = 0;
+ rv = aChannel->GetTopLevelContentWindowId(&windowID);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ if (!windowID) {
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ loadInfo->GetInnerWindowID(&windowID);
+ }
+
+ nsAutoString localizedMsg;
+ nsAutoCString spec;
+ uri->GetSpec(spec);
+ AutoTArray<nsString, 1> params = {NS_ConvertUTF8toUTF16(spec)};
+ rv = nsContentUtils::FormatLocalizedString(
+ nsContentUtils::eSECURITY_PROPERTIES, aMsg, params, localizedMsg);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ nsContentUtils::ReportToConsoleByWindowID(
+ localizedMsg, nsIScriptError::warningFlag, "Security"_ns, windowID, uri);
+}
+
+/* static */
+long nsContentSecurityUtils::ClassifyDownload(
+ nsIChannel* aChannel, const nsAutoCString& aMimeTypeGuess) {
+ MOZ_ASSERT(aChannel, "IsDownloadAllowed without channel?");
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+
+ nsCOMPtr<nsIURI> contentLocation;
+ aChannel->GetURI(getter_AddRefs(contentLocation));
+
+ nsCOMPtr<nsIPrincipal> loadingPrincipal = loadInfo->GetLoadingPrincipal();
+ if (!loadingPrincipal) {
+ loadingPrincipal = loadInfo->TriggeringPrincipal();
+ }
+ // Creating a fake Loadinfo that is just used for the MCB check.
+ nsCOMPtr<nsILoadInfo> secCheckLoadInfo = new mozilla::net::LoadInfo(
+ loadingPrincipal, loadInfo->TriggeringPrincipal(), nullptr,
+ nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK,
+ nsIContentPolicy::TYPE_FETCH);
+ // Disable HTTPS-Only checks for that loadinfo. This is required because
+ // otherwise nsMixedContentBlocker::ShouldLoad would assume that the request
+ // is safe, because HTTPS-Only is handling it.
+ secCheckLoadInfo->SetHttpsOnlyStatus(nsILoadInfo::HTTPS_ONLY_EXEMPT);
+
+ int16_t decission = nsIContentPolicy::ACCEPT;
+ nsMixedContentBlocker::ShouldLoad(false, // aHadInsecureImageRedirect
+ contentLocation, // aContentLocation,
+ secCheckLoadInfo, // aLoadinfo
+ false, // aReportError
+ &decission // aDecision
+ );
+ Telemetry::Accumulate(mozilla::Telemetry::MIXED_CONTENT_DOWNLOADS,
+ decission != nsIContentPolicy::ACCEPT);
+
+ if (StaticPrefs::dom_block_download_insecure() &&
+ decission != nsIContentPolicy::ACCEPT) {
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
+ if (httpChannel) {
+ LogMessageToConsole(httpChannel, "MixedContentBlockedDownload");
+ }
+ return nsITransfer::DOWNLOAD_POTENTIALLY_UNSAFE;
+ }
+
+ if (loadInfo->TriggeringPrincipal()->IsSystemPrincipal()) {
+ return nsITransfer::DOWNLOAD_ACCEPTABLE;
+ }
+
+ uint32_t triggeringFlags = loadInfo->GetTriggeringSandboxFlags();
+ uint32_t currentflags = loadInfo->GetSandboxFlags();
+
+ if ((triggeringFlags & SANDBOXED_ALLOW_DOWNLOADS) ||
+ (currentflags & SANDBOXED_ALLOW_DOWNLOADS)) {
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
+ if (httpChannel) {
+ LogMessageToConsole(httpChannel, "IframeSandboxBlockedDownload");
+ }
+ return nsITransfer::DOWNLOAD_FORBIDDEN;
+ }
+ return nsITransfer::DOWNLOAD_ACCEPTABLE;
+}
diff --git a/dom/security/nsContentSecurityUtils.h b/dom/security/nsContentSecurityUtils.h
new file mode 100644
index 0000000000..807e085797
--- /dev/null
+++ b/dom/security/nsContentSecurityUtils.h
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* A namespace class for static content security utilities. */
+
+#ifndef nsContentSecurityUtils_h___
+#define nsContentSecurityUtils_h___
+
+#include <utility>
+#include "mozilla/Maybe.h"
+#include "nsStringFwd.h"
+
+struct JSContext;
+class nsIChannel;
+class nsIHttpChannel;
+class nsIPrincipal;
+class NS_ConvertUTF8toUTF16;
+
+namespace mozilla::dom {
+class Document;
+class Element;
+} // namespace mozilla::dom
+
+using FilenameTypeAndDetails = std::pair<nsCString, mozilla::Maybe<nsString>>;
+
+class nsContentSecurityUtils {
+ public:
+ // CSPs upgrade-insecure-requests directive applies to same origin top level
+ // navigations. Using the SOP would return false for the case when an https
+ // page triggers and http page to load, even though that http page would be
+ // upgraded to https later. Hence we have to use that custom function instead
+ // of simply calling aTriggeringPrincipal->Equals(aResultPrincipal).
+ static bool IsConsideredSameOriginForUIR(nsIPrincipal* aTriggeringPrincipal,
+ nsIPrincipal* aResultPrincipal);
+
+ static bool IsEvalAllowed(JSContext* cx, bool aIsSystemPrincipal,
+ const nsAString& aScript);
+ static void NotifyEvalUsage(bool aIsSystemPrincipal,
+ NS_ConvertUTF8toUTF16& aFileNameA,
+ uint64_t aWindowID, uint32_t aLineNumber,
+ uint32_t aColumnNumber);
+
+ // Helper function for various checks:
+ // This function detects profiles with userChrome.js or extension signatures
+ // disabled. We can't/won't enforce strong security for people with those
+ // hacks. The function will cache its result.
+ static void DetectJsHacks();
+ // Helper function for detecting custom agent styles
+ static void DetectCssHacks();
+
+ // Helper function to query the HTTP Channel of a potential
+ // multi-part channel. Mostly used for querying response headers
+ static nsresult GetHttpChannelFromPotentialMultiPart(
+ nsIChannel* aChannel, nsIHttpChannel** aHttpChannel);
+
+ // Helper function which performs the following framing checks
+ // * CSP frame-ancestors
+ // * x-frame-options
+ // If any of the two disallows framing, the channel will be cancelled.
+ static void PerformCSPFrameAncestorAndXFOCheck(nsIChannel* aChannel);
+
+ // Helper function which just checks if the channel violates any:
+ // 1. CSP frame-ancestors properties
+ // 2. x-frame-options
+ static bool CheckCSPFrameAncestorAndXFO(nsIChannel* aChannel);
+
+ // Implements https://w3c.github.io/webappsec-csp/#is-element-nonceable.
+ //
+ // Returns an empty nonce for elements without a nonce OR when a potential
+ // dangling markup attack was detected.
+ static nsString GetIsElementNonceableNonce(
+ const mozilla::dom::Element& aElement);
+
+ // Helper function to Check if a Download is allowed;
+ static long ClassifyDownload(nsIChannel* aChannel,
+ const nsAutoCString& aMimeTypeGuess);
+
+ // Public only for testing
+ static FilenameTypeAndDetails FilenameToFilenameType(
+ const nsString& fileName, bool collectAdditionalExtensionData);
+ static char* SmartFormatCrashString(const char* str);
+ static char* SmartFormatCrashString(char* str);
+ static nsCString SmartFormatCrashString(const char* part1, const char* part2,
+ const char* format_string);
+ static nsCString SmartFormatCrashString(char* part1, char* part2,
+ const char* format_string);
+
+#if defined(DEBUG)
+ static void AssertAboutPageHasCSP(mozilla::dom::Document* aDocument);
+#endif
+
+ static bool ValidateScriptFilename(JSContext* cx, const char* aFilename);
+ // Helper Function to Post a message to the corresponding JS-Console
+ static void LogMessageToConsole(nsIHttpChannel* aChannel, const char* aMsg);
+};
+
+#endif /* nsContentSecurityUtils_h___ */
diff --git a/dom/security/nsHTTPSOnlyStreamListener.cpp b/dom/security/nsHTTPSOnlyStreamListener.cpp
new file mode 100644
index 0000000000..e6026e5e90
--- /dev/null
+++ b/dom/security/nsHTTPSOnlyStreamListener.cpp
@@ -0,0 +1,278 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "NSSErrorsService.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/dom/WindowGlobalParent.h"
+#include "mozpkix/pkixnss.h"
+#include "nsCOMPtr.h"
+#include "nsHTTPSOnlyStreamListener.h"
+#include "nsHTTPSOnlyUtils.h"
+#include "nsIChannel.h"
+#include "nsIRequest.h"
+#include "nsITransportSecurityInfo.h"
+#include "nsIURI.h"
+#include "nsIWebProgressListener.h"
+#include "nsPrintfCString.h"
+#include "secerr.h"
+#include "sslerr.h"
+
+NS_IMPL_ISUPPORTS(nsHTTPSOnlyStreamListener, nsIStreamListener,
+ nsIRequestObserver)
+
+nsHTTPSOnlyStreamListener::nsHTTPSOnlyStreamListener(
+ nsIStreamListener* aListener, nsILoadInfo* aLoadInfo)
+ : mListener(aListener), mCreationStart(mozilla::TimeStamp::Now()) {
+ RefPtr<mozilla::dom::WindowGlobalParent> wgp =
+ mozilla::dom::WindowGlobalParent::GetByInnerWindowId(
+ aLoadInfo->GetInnerWindowID());
+ // For Top-level document loads (which don't have a requesting window-context)
+ // we compute these flags once we create the Document in nsSecureBrowserUI.
+ if (wgp) {
+ wgp->TopWindowContext()->AddSecurityState(
+ nsIWebProgressListener::STATE_HTTPS_ONLY_MODE_UPGRADED);
+ }
+}
+
+NS_IMETHODIMP
+nsHTTPSOnlyStreamListener::OnDataAvailable(nsIRequest* aRequest,
+ nsIInputStream* aInputStream,
+ uint64_t aOffset, uint32_t aCount) {
+ return mListener->OnDataAvailable(aRequest, aInputStream, aOffset, aCount);
+}
+
+NS_IMETHODIMP
+nsHTTPSOnlyStreamListener::OnStartRequest(nsIRequest* request) {
+ return mListener->OnStartRequest(request);
+}
+
+NS_IMETHODIMP
+nsHTTPSOnlyStreamListener::OnStopRequest(nsIRequest* request,
+ nsresult aStatus) {
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(request);
+
+ // Note: CouldBeHttpsOnlyError also returns true if there was no error
+ if (nsHTTPSOnlyUtils::CouldBeHttpsOnlyError(channel, aStatus)) {
+ RecordUpgradeTelemetry(request, aStatus);
+ LogUpgradeFailure(request, aStatus);
+
+ // If the request failed and there is a requesting window-context, set
+ // HTTPS-Only state flag to indicate a failed upgrade.
+ // For Top-level document loads (which don't have a requesting
+ // window-context) we simply check in the UI code whether we landed on the
+ // HTTPS-Only error page.
+ if (NS_FAILED(aStatus)) {
+ nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
+ RefPtr<mozilla::dom::WindowGlobalParent> wgp =
+ mozilla::dom::WindowGlobalParent::GetByInnerWindowId(
+ loadInfo->GetInnerWindowID());
+
+ if (wgp) {
+ wgp->TopWindowContext()->AddSecurityState(
+ nsIWebProgressListener::STATE_HTTPS_ONLY_MODE_UPGRADE_FAILED);
+ }
+ }
+ }
+
+ return mListener->OnStopRequest(request, aStatus);
+}
+
+void nsHTTPSOnlyStreamListener::RecordUpgradeTelemetry(nsIRequest* request,
+ nsresult aStatus) {
+ // 1. Get time between now and when the initial upgrade request started
+ int64_t duration =
+ (mozilla::TimeStamp::Now() - mCreationStart).ToMilliseconds();
+
+ // 2. Assemble the category string
+ // [!] All strings have to be present in Histograms.json
+ nsresult rv;
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(request, &rv);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ nsAutoCString category;
+ nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
+ nsContentPolicyType internalType = loadInfo->InternalContentPolicyType();
+
+ if (internalType == nsIContentPolicy::TYPE_DOCUMENT) {
+ category.AppendLiteral("top_");
+ } else {
+ category.AppendLiteral("sub_");
+ }
+
+ if (NS_SUCCEEDED(aStatus)) {
+ category.AppendLiteral("successful");
+ } else {
+ int32_t code = -1 * NS_ERROR_GET_CODE(aStatus);
+
+ if (aStatus == NS_ERROR_REDIRECT_LOOP) {
+ category.AppendLiteral("f_redirectloop");
+ } else if (aStatus == NS_ERROR_NET_TIMEOUT ||
+ aStatus == NS_ERROR_NET_TIMEOUT_EXTERNAL) {
+ category.AppendLiteral("f_timeout");
+ } else if (aStatus == NS_BINDING_ABORTED) {
+ category.AppendLiteral("f_aborted");
+ } else if (aStatus == NS_ERROR_CONNECTION_REFUSED) {
+ category.AppendLiteral("f_cxnrefused");
+ } else if (mozilla::psm::IsNSSErrorCode(code)) {
+ switch (code) {
+ case mozilla::pkix::MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT:
+ category.AppendLiteral("f_ssl_selfsignd");
+ break;
+ case SSL_ERROR_BAD_CERT_DOMAIN:
+ category.AppendLiteral("f_ssl_badcertdm");
+ break;
+ case SEC_ERROR_UNKNOWN_ISSUER:
+ category.AppendLiteral("f_ssl_unkwnissr");
+ break;
+ default:
+ category.AppendLiteral("f_ssl_other");
+ break;
+ }
+ } else {
+ category.AppendLiteral("f_other");
+ }
+ }
+ mozilla::Telemetry::Accumulate(
+ mozilla::Telemetry::HTTPS_ONLY_MODE_UPGRADE_TIME_MS, category, duration);
+
+ bool success = NS_SUCCEEDED(aStatus);
+ ExtContentPolicyType externalType = loadInfo->GetExternalContentPolicyType();
+ auto typeKey = nsAutoCString("unknown");
+
+ if (externalType == ExtContentPolicy::TYPE_MEDIA) {
+ switch (internalType) {
+ case nsIContentPolicy::TYPE_INTERNAL_AUDIO:
+ case nsIContentPolicy::TYPE_INTERNAL_TRACK:
+ typeKey = "audio"_ns;
+ break;
+
+ case nsIContentPolicy::TYPE_INTERNAL_VIDEO:
+ typeKey = "video"_ns;
+ break;
+
+ default:
+ MOZ_ASSERT_UNREACHABLE();
+ break;
+ }
+ } else {
+ switch (externalType) {
+ case ExtContentPolicy::TYPE_SCRIPT:
+ typeKey = "script"_ns;
+ break;
+
+ case ExtContentPolicy::TYPE_OBJECT:
+ case ExtContentPolicy::TYPE_OBJECT_SUBREQUEST:
+ typeKey = "object"_ns;
+ break;
+
+ case ExtContentPolicy::TYPE_DOCUMENT:
+ typeKey = "document"_ns;
+ break;
+
+ case ExtContentPolicy::TYPE_SUBDOCUMENT:
+ typeKey = "subdocument"_ns;
+ break;
+
+ case ExtContentPolicy::TYPE_XMLHTTPREQUEST:
+ typeKey = "xmlhttprequest"_ns;
+ break;
+
+ case ExtContentPolicy::TYPE_IMAGE:
+ case ExtContentPolicy::TYPE_IMAGESET:
+ typeKey = "image"_ns;
+ break;
+
+ case ExtContentPolicy::TYPE_DTD:
+ typeKey = "dtd"_ns;
+ break;
+
+ case ExtContentPolicy::TYPE_FONT:
+ case ExtContentPolicy::TYPE_UA_FONT:
+ typeKey = "font"_ns;
+ break;
+
+ case ExtContentPolicy::TYPE_FETCH:
+ typeKey = "fetch"_ns;
+ break;
+
+ case ExtContentPolicy::TYPE_WEBSOCKET:
+ typeKey = "websocket"_ns;
+ break;
+
+ case ExtContentPolicy::TYPE_STYLESHEET:
+ typeKey = "stylesheet"_ns;
+ break;
+
+ case ExtContentPolicy::TYPE_CSP_REPORT:
+ typeKey = "cspreport"_ns;
+ break;
+
+ case ExtContentPolicy::TYPE_WEB_MANIFEST:
+ typeKey = "webmanifest"_ns;
+ break;
+
+ case ExtContentPolicy::TYPE_PING:
+ typeKey = "ping"_ns;
+ break;
+
+ case ExtContentPolicy::TYPE_XSLT:
+ typeKey = "xslt"_ns;
+ break;
+
+ case ExtContentPolicy::TYPE_PROXIED_WEBRTC_MEDIA:
+ typeKey = "proxied-webrtc"_ns;
+ break;
+
+ case ExtContentPolicy::TYPE_INVALID:
+ case ExtContentPolicy::TYPE_OTHER:
+ case ExtContentPolicy::TYPE_MEDIA: // already handled above
+ case ExtContentPolicy::TYPE_BEACON:
+ case ExtContentPolicy::TYPE_SAVEAS_DOWNLOAD:
+ case ExtContentPolicy::TYPE_SPECULATIVE:
+ case ExtContentPolicy::TYPE_WEB_TRANSPORT:
+ case ExtContentPolicy::TYPE_WEB_IDENTITY:
+ break;
+ // Do not add default: so that compilers can catch the missing case.
+ }
+ }
+
+ mozilla::Telemetry::Accumulate(
+ mozilla::Telemetry::HTTPS_ONLY_MODE_UPGRADE_TYPE, typeKey, success);
+}
+
+void nsHTTPSOnlyStreamListener::LogUpgradeFailure(nsIRequest* request,
+ nsresult aStatus) {
+ // If the request failed we'll log it to the console with the error-code
+ if (NS_SUCCEEDED(aStatus)) {
+ return;
+ }
+ nsresult rv;
+ // Try to query for the channel-object
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(request, &rv);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ rv = channel->GetURI(getter_AddRefs(uri));
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ // Logging URI as well as Module- and Error-Code
+ AutoTArray<nsString, 2> params = {
+ NS_ConvertUTF8toUTF16(uri->GetSpecOrDefault()),
+ NS_ConvertUTF8toUTF16(nsPrintfCString("M%u-C%u",
+ NS_ERROR_GET_MODULE(aStatus),
+ NS_ERROR_GET_CODE(aStatus)))};
+
+ nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
+ nsHTTPSOnlyUtils::LogLocalizedString("HTTPSOnlyFailedRequest", params,
+ nsIScriptError::errorFlag, loadInfo,
+ uri);
+}
diff --git a/dom/security/nsHTTPSOnlyStreamListener.h b/dom/security/nsHTTPSOnlyStreamListener.h
new file mode 100644
index 0000000000..a2ab4711e3
--- /dev/null
+++ b/dom/security/nsHTTPSOnlyStreamListener.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsHTTPSOnlyStreamListener_h___
+#define nsHTTPSOnlyStreamListener_h___
+
+#include "mozilla/TimeStamp.h"
+#include "nsCOMPtr.h"
+#include "nsIStreamListener.h"
+
+class nsILoadInfo;
+
+/**
+ * This event listener gets registered for requests that have been upgraded
+ * using the HTTPS-only mode to log failed upgrades to the console.
+ */
+class nsHTTPSOnlyStreamListener : public nsIStreamListener {
+ public:
+ // nsISupports methods
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+
+ explicit nsHTTPSOnlyStreamListener(nsIStreamListener* aListener,
+ nsILoadInfo* aLoadInfo);
+
+ private:
+ virtual ~nsHTTPSOnlyStreamListener() = default;
+
+ /**
+ * Records telemetry about the upgraded request.
+ * @param aStatus Request object
+ */
+ void RecordUpgradeTelemetry(nsIRequest* request, nsresult aStatus);
+
+ /**
+ * Logs information to the console if the request failed.
+ * @param request Request object
+ * @param aStatus Status of request
+ */
+ void LogUpgradeFailure(nsIRequest* request, nsresult aStatus);
+
+ nsCOMPtr<nsIStreamListener> mListener;
+ mozilla::TimeStamp mCreationStart;
+};
+
+#endif /* nsHTTPSOnlyStreamListener_h___ */
diff --git a/dom/security/nsHTTPSOnlyUtils.cpp b/dom/security/nsHTTPSOnlyUtils.cpp
new file mode 100644
index 0000000000..2a3880ba70
--- /dev/null
+++ b/dom/security/nsHTTPSOnlyUtils.cpp
@@ -0,0 +1,1071 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/Components.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/NullPrincipal.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/net/DNS.h"
+#include "nsContentUtils.h"
+#include "nsHTTPSOnlyUtils.h"
+#include "nsIConsoleService.h"
+#include "nsIHttpChannel.h"
+#include "nsIHttpChannelInternal.h"
+#include "nsIHttpsOnlyModePermission.h"
+#include "nsILoadInfo.h"
+#include "nsIPermissionManager.h"
+#include "nsIPrincipal.h"
+#include "nsIRedirectHistoryEntry.h"
+#include "nsIScriptError.h"
+#include "nsIURIMutator.h"
+#include "nsNetUtil.h"
+#include "prnetdb.h"
+
+/* static */
+bool nsHTTPSOnlyUtils::IsHttpsOnlyModeEnabled(bool aFromPrivateWindow) {
+ // if the general pref is set to true, then we always return
+ if (mozilla::StaticPrefs::dom_security_https_only_mode()) {
+ return true;
+ }
+
+ // otherwise we check if executing in private browsing mode and return true
+ // if the PBM pref for HTTPS-Only is set.
+ if (aFromPrivateWindow &&
+ mozilla::StaticPrefs::dom_security_https_only_mode_pbm()) {
+ return true;
+ }
+ return false;
+}
+
+/* static */
+bool nsHTTPSOnlyUtils::IsHttpsFirstModeEnabled(bool aFromPrivateWindow) {
+ // HTTPS-Only takes priority over HTTPS-First
+ if (IsHttpsOnlyModeEnabled(aFromPrivateWindow)) {
+ return false;
+ }
+
+ // if the general pref is set to true, then we always return
+ if (mozilla::StaticPrefs::dom_security_https_first()) {
+ return true;
+ }
+
+ // otherwise we check if executing in private browsing mode and return true
+ // if the PBM pref for HTTPS-First is set.
+ if (aFromPrivateWindow &&
+ mozilla::StaticPrefs::dom_security_https_first_pbm()) {
+ return true;
+ }
+ return false;
+}
+
+/* static */
+void nsHTTPSOnlyUtils::PotentiallyFireHttpRequestToShortenTimout(
+ mozilla::net::DocumentLoadListener* aDocumentLoadListener) {
+ // only send http background request to counter timeouts if the
+ // pref allows us to do that.
+ if (!mozilla::StaticPrefs::
+ dom_security_https_only_mode_send_http_background_request()) {
+ return;
+ }
+
+ nsCOMPtr<nsIChannel> channel = aDocumentLoadListener->GetChannel();
+ if (!channel) {
+ return;
+ }
+
+ nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
+ bool isPrivateWin = loadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
+
+ // if neither HTTPS-Only nor HTTPS-First mode is enabled, then there is
+ // nothing to do here.
+ if ((!IsHttpsOnlyModeEnabled(isPrivateWin) &&
+ !IsHttpsFirstModeEnabled(isPrivateWin)) &&
+ !(loadInfo->GetWasSchemelessInput() &&
+ mozilla::StaticPrefs::dom_security_https_first_schemeless())) {
+ return;
+ }
+
+ // if we are not dealing with a top-level load, then there is nothing to do
+ // here.
+ if (loadInfo->GetExternalContentPolicyType() !=
+ ExtContentPolicy::TYPE_DOCUMENT) {
+ return;
+ }
+
+ // if the load is exempt, then there is nothing to do here.
+ uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus();
+ if (httpsOnlyStatus & nsILoadInfo::nsILoadInfo::HTTPS_ONLY_EXEMPT) {
+ return;
+ }
+
+ // if it's not an http channel, then there is nothing to do here.
+ nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(channel));
+ if (!httpChannel) {
+ return;
+ }
+
+ // if it's not a GET method, then there is nothing to do here either.
+ nsAutoCString method;
+ mozilla::Unused << httpChannel->GetRequestMethod(method);
+ if (!method.EqualsLiteral("GET")) {
+ return;
+ }
+
+ // if it's already an https channel, then there is nothing to do here.
+ nsCOMPtr<nsIURI> channelURI;
+ channel->GetURI(getter_AddRefs(channelURI));
+ if (!channelURI->SchemeIs("http")) {
+ return;
+ }
+
+ // HTTPS-First only applies to standard ports but HTTPS-Only brute forces
+ // all http connections to be https and overrules HTTPS-First. In case
+ // HTTPS-First is enabled, but HTTPS-Only is not enabled, we might return
+ // early if attempting to send a background request to a non standard port.
+ if ((IsHttpsFirstModeEnabled(isPrivateWin) ||
+ (loadInfo->GetWasSchemelessInput() &&
+ mozilla::StaticPrefs::dom_security_https_first_schemeless()))) {
+ int32_t port = 0;
+ nsresult rv = channelURI->GetPort(&port);
+ int defaultPortforScheme = NS_GetDefaultPort("http");
+ if (NS_SUCCEEDED(rv) && port != defaultPortforScheme && port != -1) {
+ return;
+ }
+ }
+
+ // Check for general exceptions
+ if (OnionException(channelURI) || LoopbackOrLocalException(channelURI)) {
+ return;
+ }
+
+ RefPtr<nsIRunnable> task =
+ new TestHTTPAnswerRunnable(channelURI, aDocumentLoadListener);
+ NS_DispatchToMainThread(task.forget());
+}
+
+/* static */
+bool nsHTTPSOnlyUtils::ShouldUpgradeRequest(nsIURI* aURI,
+ nsILoadInfo* aLoadInfo) {
+ // 1. Check if the HTTPS-Only Mode is even enabled, before we do anything else
+ bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
+ if (!IsHttpsOnlyModeEnabled(isPrivateWin)) {
+ return false;
+ }
+
+ // 2. Check for general exceptions
+ if (OnionException(aURI) || LoopbackOrLocalException(aURI)) {
+ return false;
+ }
+
+ // 3. Check if NoUpgrade-flag is set in LoadInfo
+ uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus();
+ if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) {
+ AutoTArray<nsString, 1> params = {
+ NS_ConvertUTF8toUTF16(aURI->GetSpecOrDefault())};
+ nsHTTPSOnlyUtils::LogLocalizedString("HTTPSOnlyNoUpgradeException", params,
+ nsIScriptError::infoFlag, aLoadInfo,
+ aURI);
+ return false;
+ }
+
+ // All subresources of an exempt triggering principal are also exempt
+ ExtContentPolicyType contentType = aLoadInfo->GetExternalContentPolicyType();
+ if (contentType != ExtContentPolicy::TYPE_DOCUMENT) {
+ if (!aLoadInfo->TriggeringPrincipal()->IsSystemPrincipal() &&
+ TestIfPrincipalIsExempt(aLoadInfo->TriggeringPrincipal())) {
+ return false;
+ }
+ }
+
+ // We can not upgrade "Save-As" downloads, since we have no way of detecting
+ // if the upgrade failed (Bug 1674859). For now we will just allow the
+ // download, since there will still be a visual warning about the download
+ // being insecure.
+ if (contentType == ExtContentPolicyType::TYPE_SAVEAS_DOWNLOAD) {
+ return false;
+ }
+
+ // We can upgrade the request - let's log it to the console
+ // Appending an 's' to the scheme for the logging. (http -> https)
+ nsAutoCString scheme;
+ aURI->GetScheme(scheme);
+ scheme.AppendLiteral("s");
+ NS_ConvertUTF8toUTF16 reportSpec(aURI->GetSpecOrDefault());
+ NS_ConvertUTF8toUTF16 reportScheme(scheme);
+
+ bool isSpeculative = aLoadInfo->GetExternalContentPolicyType() ==
+ ExtContentPolicy::TYPE_SPECULATIVE;
+ AutoTArray<nsString, 2> params = {reportSpec, reportScheme};
+ nsHTTPSOnlyUtils::LogLocalizedString(
+ isSpeculative ? "HTTPSOnlyUpgradeSpeculativeConnection"
+ : "HTTPSOnlyUpgradeRequest",
+ params, nsIScriptError::warningFlag, aLoadInfo, aURI);
+
+ // If the status was not determined before, we now indicate that the request
+ // will get upgraded, but no event-listener has been registered yet.
+ if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_UNINITIALIZED) {
+ httpsOnlyStatus ^= nsILoadInfo::HTTPS_ONLY_UNINITIALIZED;
+ httpsOnlyStatus |= nsILoadInfo::HTTPS_ONLY_UPGRADED_LISTENER_NOT_REGISTERED;
+ aLoadInfo->SetHttpsOnlyStatus(httpsOnlyStatus);
+ }
+ return true;
+}
+
+/* static */
+bool nsHTTPSOnlyUtils::ShouldUpgradeWebSocket(nsIURI* aURI,
+ nsILoadInfo* aLoadInfo) {
+ // 1. Check if the HTTPS-Only Mode is even enabled, before we do anything else
+ bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
+ if (!IsHttpsOnlyModeEnabled(isPrivateWin)) {
+ return false;
+ }
+
+ // 2. Check for general exceptions
+ if (OnionException(aURI) || LoopbackOrLocalException(aURI)) {
+ return false;
+ }
+
+ // 3. Check if NoUpgrade-flag is set in LoadInfo
+ uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus();
+ if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) {
+ // Let's log to the console, that we didn't upgrade this request
+ AutoTArray<nsString, 1> params = {
+ NS_ConvertUTF8toUTF16(aURI->GetSpecOrDefault())};
+ nsHTTPSOnlyUtils::LogLocalizedString("HTTPSOnlyNoUpgradeException", params,
+ nsIScriptError::infoFlag, aLoadInfo,
+ aURI);
+ return false;
+ }
+
+ // All subresources of an exempt triggering principal are also exempt.
+ if (!aLoadInfo->TriggeringPrincipal()->IsSystemPrincipal() &&
+ TestIfPrincipalIsExempt(aLoadInfo->TriggeringPrincipal())) {
+ return false;
+ }
+
+ // We can upgrade the request - let's log it to the console
+ // Appending an 's' to the scheme for the logging. (ws -> wss)
+ nsAutoCString scheme;
+ aURI->GetScheme(scheme);
+ scheme.AppendLiteral("s");
+ NS_ConvertUTF8toUTF16 reportSpec(aURI->GetSpecOrDefault());
+ NS_ConvertUTF8toUTF16 reportScheme(scheme);
+
+ AutoTArray<nsString, 2> params = {reportSpec, reportScheme};
+ nsHTTPSOnlyUtils::LogLocalizedString("HTTPSOnlyUpgradeRequest", params,
+ nsIScriptError::warningFlag, aLoadInfo,
+ aURI);
+ return true;
+}
+
+/* static */
+bool nsHTTPSOnlyUtils::IsUpgradeDowngradeEndlessLoop(
+ nsIURI* aURI, nsILoadInfo* aLoadInfo,
+ const mozilla::EnumSet<UpgradeDowngradeEndlessLoopOptions>& aOptions) {
+ // 1. Check if the HTTPS-Only/HTTPS-First is even enabled, before doing
+ // anything else
+ bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
+ bool enforceForHTTPSOnlyMode =
+ IsHttpsOnlyModeEnabled(isPrivateWin) &&
+ aOptions.contains(
+ UpgradeDowngradeEndlessLoopOptions::EnforceForHTTPSOnlyMode);
+ bool enforceForHTTPSFirstMode =
+ IsHttpsFirstModeEnabled(isPrivateWin) &&
+ aOptions.contains(
+ UpgradeDowngradeEndlessLoopOptions::EnforceForHTTPSFirstMode);
+ bool enforceForHTTPSRR =
+ aOptions.contains(UpgradeDowngradeEndlessLoopOptions::EnforceForHTTPSRR);
+ if (!enforceForHTTPSOnlyMode && !enforceForHTTPSFirstMode &&
+ !enforceForHTTPSRR) {
+ return false;
+ }
+
+ // 2. Check if the upgrade downgrade pref even wants us to try to break the
+ // cycle. In the case that HTTPS RR is presented, we ignore this pref.
+ if (!mozilla::StaticPrefs::
+ dom_security_https_only_mode_break_upgrade_downgrade_endless_loop() &&
+ !enforceForHTTPSRR) {
+ return false;
+ }
+
+ // 3. If it's not a top-level load, then there is nothing to do here either.
+ if (aLoadInfo->GetExternalContentPolicyType() !=
+ ExtContentPolicy::TYPE_DOCUMENT) {
+ return false;
+ }
+
+ // 4. If the load is exempt, then it's defintely not related to https-only
+ uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus();
+ if ((httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) &&
+ !enforceForHTTPSRR) {
+ return false;
+ }
+
+ // 5. If the URI to be loaded is not http, then it's defnitely no endless
+ // loop caused by https-only.
+ if (!aURI->SchemeIs("http")) {
+ return false;
+ }
+
+ nsAutoCString uriHost;
+ aURI->GetAsciiHost(uriHost);
+
+ auto uriAndPrincipalComparator = [&](nsIPrincipal* aPrincipal) {
+ nsAutoCString principalHost;
+ aPrincipal->GetAsciiHost(principalHost);
+ bool checkPath = mozilla::StaticPrefs::
+ dom_security_https_only_check_path_upgrade_downgrade_endless_loop();
+ if (!checkPath) {
+ return uriHost.Equals(principalHost);
+ }
+
+ nsAutoCString uriPath;
+ nsresult rv = aURI->GetFilePath(uriPath);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+ nsAutoCString principalPath;
+ aPrincipal->GetFilePath(principalPath);
+ return uriHost.Equals(principalHost) && uriPath.Equals(principalPath);
+ };
+
+ // 6. Check actual redirects. If the Principal that kicked off the
+ // load/redirect is not https, then it's definitely not a redirect cause by
+ // https-only. If the scheme of the principal however is https and the
+ // asciiHost of the URI to be loaded and the asciiHost of the Principal are
+ // identical, then we are dealing with an upgrade downgrade scenario and we
+ // have to break the cycle.
+ if (!aLoadInfo->RedirectChain().IsEmpty()) {
+ nsCOMPtr<nsIPrincipal> redirectPrincipal;
+ for (nsIRedirectHistoryEntry* entry : aLoadInfo->RedirectChain()) {
+ entry->GetPrincipal(getter_AddRefs(redirectPrincipal));
+ if (redirectPrincipal && redirectPrincipal->SchemeIs("https") &&
+ uriAndPrincipalComparator(redirectPrincipal)) {
+ return true;
+ }
+ }
+ } else {
+ // 6.1 We should only check if this load is triggered by a user gesture
+ // when the redirect chain is empty, since this information is only useful
+ // in our case here. When the redirect chain is not empty, this load is
+ // defnitely triggered by redirection, not a user gesture.
+ if (aLoadInfo->GetHasValidUserGestureActivation()) {
+ return false;
+ }
+ }
+
+ // 7. Meta redirects and JS based redirects (win.location). If the security
+ // context that triggered the load is not https, then it's defnitely no
+ // endless loop caused by https-only. If the scheme is http however and the
+ // asciiHost of the URI to be loaded matches the asciiHost of the Principal,
+ // then we are dealing with an upgrade downgrade scenario and we have to break
+ // the cycle.
+ nsCOMPtr<nsIPrincipal> triggeringPrincipal = aLoadInfo->TriggeringPrincipal();
+ if (!triggeringPrincipal->SchemeIs("https")) {
+ return false;
+ }
+
+ return uriAndPrincipalComparator(triggeringPrincipal);
+}
+
+/* static */
+bool nsHTTPSOnlyUtils::ShouldUpgradeHttpsFirstRequest(nsIURI* aURI,
+ nsILoadInfo* aLoadInfo) {
+ // 1. Check if HTTPS-First Mode is enabled
+ bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
+ if (!IsHttpsFirstModeEnabled(isPrivateWin) &&
+ !(aLoadInfo->GetWasSchemelessInput() &&
+ mozilla::StaticPrefs::dom_security_https_first_schemeless())) {
+ return false;
+ }
+
+ // 2. HTTPS-First only upgrades top-level loads (and speculative connections)
+ ExtContentPolicyType contentType = aLoadInfo->GetExternalContentPolicyType();
+ if (contentType != ExtContentPolicy::TYPE_DOCUMENT &&
+ contentType != ExtContentPolicy::TYPE_SPECULATIVE) {
+ return false;
+ }
+
+ // 3. Check for general exceptions
+ if (OnionException(aURI) || LoopbackOrLocalException(aURI)) {
+ return false;
+ }
+
+ // 4. Don't upgrade if upgraded previously or exempt from upgrades
+ uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus();
+ if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_UPGRADED_HTTPS_FIRST ||
+ httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) {
+ return false;
+ }
+
+ // 5. HTTPS-First Mode only upgrades default ports - do not upgrade the
+ // request to https if port is specified and not the default port of 80.
+ MOZ_ASSERT(aURI->SchemeIs("http"), "how come the request is not 'http'?");
+ int defaultPortforScheme = NS_GetDefaultPort("http");
+ // If no port is specified, then the API returns -1 to indicate the default
+ // port.
+ int32_t port = 0;
+ nsresult rv = aURI->GetPort(&port);
+ NS_ENSURE_SUCCESS(rv, false);
+ if (port != defaultPortforScheme && port != -1) {
+ return false;
+ }
+ // 6. Do not upgrade form submissions (for now), revisit within
+ // Bug 1720500: Revisit upgrading form submissions.
+ if (aLoadInfo->GetIsFormSubmission()) {
+ return false;
+ }
+
+ // https-first needs to account for breaking upgrade-downgrade endless
+ // loops at this point because this function is called before we
+ // check the redirect limit in HttpBaseChannel. If we encounter
+ // a same-origin server side downgrade from e.g https://example.com
+ // to http://example.com then we simply not annotating the loadinfo
+ // and returning false from within this function. Please note that
+ // the handling for https-only mode is different from https-first mode,
+ // because https-only mode results in an exception page in case
+ // we encounter and endless upgrade downgrade loop.
+ bool isUpgradeDowngradeEndlessLoop = IsUpgradeDowngradeEndlessLoop(
+ aURI, aLoadInfo,
+ {UpgradeDowngradeEndlessLoopOptions::EnforceForHTTPSFirstMode});
+ if (isUpgradeDowngradeEndlessLoop) {
+ return false;
+ }
+
+ // We can upgrade the request - let's log to the console and set the status
+ // so we know that we upgraded the request.
+ if (aLoadInfo->GetWasSchemelessInput() &&
+ mozilla::StaticPrefs::dom_security_https_first_schemeless()) {
+ nsAutoCString urlCString;
+ aURI->GetSpec(urlCString);
+ NS_ConvertUTF8toUTF16 urlString(urlCString);
+
+ AutoTArray<nsString, 1> params = {urlString};
+ nsHTTPSOnlyUtils::LogLocalizedString("HTTPSFirstSchemeless", params,
+ nsIScriptError::warningFlag, aLoadInfo,
+ aURI, true);
+ } else {
+ nsAutoCString scheme;
+
+ aURI->GetScheme(scheme);
+ scheme.AppendLiteral("s");
+ NS_ConvertUTF8toUTF16 reportSpec(aURI->GetSpecOrDefault());
+ NS_ConvertUTF8toUTF16 reportScheme(scheme);
+
+ bool isSpeculative = contentType == ExtContentPolicy::TYPE_SPECULATIVE;
+ AutoTArray<nsString, 2> params = {reportSpec, reportScheme};
+ nsHTTPSOnlyUtils::LogLocalizedString(
+ isSpeculative ? "HTTPSOnlyUpgradeSpeculativeConnection"
+ : "HTTPSOnlyUpgradeRequest",
+ params, nsIScriptError::warningFlag, aLoadInfo, aURI, true);
+ }
+ // Set flag so we know that we upgraded the request
+ httpsOnlyStatus |= nsILoadInfo::HTTPS_ONLY_UPGRADED_HTTPS_FIRST;
+ aLoadInfo->SetHttpsOnlyStatus(httpsOnlyStatus);
+ return true;
+}
+
+/* static */
+already_AddRefed<nsIURI>
+nsHTTPSOnlyUtils::PotentiallyDowngradeHttpsFirstRequest(nsIChannel* aChannel,
+ nsresult aStatus) {
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus();
+ // Only downgrade if we this request was upgraded using HTTPS-First Mode
+ if (!(httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_UPGRADED_HTTPS_FIRST)) {
+ return nullptr;
+ }
+ // Once loading is in progress we set that flag so that timeout counter
+ // measures do not kick in.
+ loadInfo->SetHttpsOnlyStatus(
+ httpsOnlyStatus | nsILoadInfo::HTTPS_ONLY_TOP_LEVEL_LOAD_IN_PROGRESS);
+
+ nsresult status = aStatus;
+ // Since 4xx and 5xx errors return NS_OK instead of NS_ERROR_*, we need
+ // to check each NS_OK for those errors.
+ // Only downgrade an NS_OK status if it is an 4xx or 5xx error.
+ if (NS_SUCCEEDED(aStatus)) {
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
+ // If no httpChannel exists we have nothing to do here.
+ if (!httpChannel) {
+ return nullptr;
+ }
+ uint32_t responseStatus = 0;
+ if (NS_FAILED(httpChannel->GetResponseStatus(&responseStatus))) {
+ return nullptr;
+ }
+
+ // In case we found one 4xx or 5xx error we need to log it later on,
+ // for that reason we flip the nsresult 'status' from 'NS_OK' to the
+ // corresponding NS_ERROR_*.
+ // To do so we convert the response status to an nsresult error
+ // Every NS_OK that is NOT an 4xx or 5xx error code won't get downgraded.
+ if (responseStatus >= 400 && responseStatus < 600) {
+ // HttpProxyResponseToErrorCode() maps 400 and 404 on
+ // the same error as a 500 status which would lead to no downgrade
+ // later on. For that reason we explicit filter for 400 and 404 status
+ // codes to log them correctly and to downgrade them if possible.
+ switch (responseStatus) {
+ case 400:
+ status = NS_ERROR_PROXY_BAD_REQUEST;
+ break;
+ case 404:
+ status = NS_ERROR_PROXY_NOT_FOUND;
+ break;
+ default:
+ status = mozilla::net::HttpProxyResponseToErrorCode(responseStatus);
+ break;
+ }
+ }
+ if (NS_SUCCEEDED(status)) {
+ return nullptr;
+ }
+ }
+
+ // We're only downgrading if it's possible that the error was
+ // caused by the upgrade.
+ if (HttpsUpgradeUnrelatedErrorCode(status)) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ nsAutoCString spec;
+ nsCOMPtr<nsIURI> newURI;
+
+ // Only downgrade if the current scheme is (a) https or (b) view-source:https
+ if (uri->SchemeIs("https")) {
+ rv = uri->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ rv = NS_NewURI(getter_AddRefs(newURI), spec);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ rv = NS_MutateURI(newURI).SetScheme("http"_ns).Finalize(
+ getter_AddRefs(newURI));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ } else if (uri->SchemeIs("view-source")) {
+ nsCOMPtr<nsINestedURI> nestedURI = do_QueryInterface(uri);
+ if (!nestedURI) {
+ return nullptr;
+ }
+ nsCOMPtr<nsIURI> innerURI;
+ rv = nestedURI->GetInnerURI(getter_AddRefs(innerURI));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ if (!innerURI || !innerURI->SchemeIs("https")) {
+ return nullptr;
+ }
+ rv = NS_MutateURI(innerURI).SetScheme("http"_ns).Finalize(
+ getter_AddRefs(innerURI));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ nsAutoCString innerSpec;
+ rv = innerURI->GetSpec(innerSpec);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ spec.Append("view-source:");
+ spec.Append(innerSpec);
+
+ rv = NS_NewURI(getter_AddRefs(newURI), spec);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ } else {
+ return nullptr;
+ }
+
+ // Log downgrade to console
+ NS_ConvertUTF8toUTF16 reportSpec(uri->GetSpecOrDefault());
+ AutoTArray<nsString, 1> params = {reportSpec};
+ nsHTTPSOnlyUtils::LogLocalizedString("HTTPSOnlyFailedDowngradeAgain", params,
+ nsIScriptError::warningFlag, loadInfo,
+ uri, true);
+
+ return newURI.forget();
+}
+
+/* static */
+bool nsHTTPSOnlyUtils::CouldBeHttpsOnlyError(nsIChannel* aChannel,
+ nsresult aError) {
+ // If there is no failed channel, then there is nothing to do here.
+ if (!aChannel) {
+ return false;
+ }
+
+ // If HTTPS-Only Mode is not enabled, then there is nothing to do here.
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ bool isPrivateWin = loadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
+ if (!IsHttpsOnlyModeEnabled(isPrivateWin)) {
+ return false;
+ }
+
+ // If the load is exempt or did not get upgraded,
+ // then there is nothing to do here.
+ uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus();
+ if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT ||
+ httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_UNINITIALIZED) {
+ return false;
+ }
+
+ // If it's one of those errors, then most likely it's not a HTTPS-Only error
+ // (This list of errors is largely drawn from nsDocShell::DisplayLoadError())
+ return !HttpsUpgradeUnrelatedErrorCode(aError);
+}
+
+/* static */
+bool nsHTTPSOnlyUtils::TestIfPrincipalIsExempt(nsIPrincipal* aPrincipal) {
+ static nsCOMPtr<nsIPermissionManager> sPermMgr;
+ if (!sPermMgr) {
+ sPermMgr = mozilla::components::PermissionManager::Service();
+ mozilla::ClearOnShutdown(&sPermMgr);
+ }
+ NS_ENSURE_TRUE(sPermMgr, false);
+
+ uint32_t perm;
+ nsresult rv = sPermMgr->TestExactPermissionFromPrincipal(
+ aPrincipal, "https-only-load-insecure"_ns, &perm);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ return perm == nsIHttpsOnlyModePermission::LOAD_INSECURE_ALLOW ||
+ perm == nsIHttpsOnlyModePermission::LOAD_INSECURE_ALLOW_SESSION;
+}
+
+/* static */
+void nsHTTPSOnlyUtils::TestSitePermissionAndPotentiallyAddExemption(
+ nsIChannel* aChannel) {
+ NS_ENSURE_TRUE_VOID(aChannel);
+
+ // If HTTPS-Only or HTTPS-First Mode is not enabled, then there is nothing to
+ // do here.
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ bool isPrivateWin = loadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
+ bool isHttpsOnly = IsHttpsOnlyModeEnabled(isPrivateWin);
+ bool isHttpsFirst = IsHttpsFirstModeEnabled(isPrivateWin);
+ bool isSchemelessHttpsFirst =
+ (loadInfo->GetWasSchemelessInput() &&
+ mozilla::StaticPrefs::dom_security_https_first_schemeless());
+ if (!isHttpsOnly && !isHttpsFirst && !isSchemelessHttpsFirst) {
+ return;
+ }
+
+ // if it's not a top-level load then there is nothing to here.
+ ExtContentPolicyType type = loadInfo->GetExternalContentPolicyType();
+ if (type != ExtContentPolicy::TYPE_DOCUMENT) {
+ return;
+ }
+
+ // it it's not an http channel, then there is nothing to do here.
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
+ if (!httpChannel) {
+ return;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal;
+ nsresult rv = nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal(
+ aChannel, getter_AddRefs(principal));
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus();
+ bool isPrincipalExempt = TestIfPrincipalIsExempt(principal);
+ if (isPrincipalExempt) {
+ httpsOnlyStatus |= nsILoadInfo::HTTPS_ONLY_EXEMPT;
+ } else {
+ // We explicitly remove the exemption flag, because this
+ // function is also consulted after redirects.
+ httpsOnlyStatus &= ~nsILoadInfo::HTTPS_ONLY_EXEMPT;
+ }
+ if (httpsOnlyStatus & nsILoadInfo::HTTPS_FIRST_EXEMPT_NEXT_LOAD &&
+ isHttpsFirst) {
+ httpsOnlyStatus &= ~nsILoadInfo::HTTPS_FIRST_EXEMPT_NEXT_LOAD;
+ httpsOnlyStatus |= nsILoadInfo::HTTPS_ONLY_EXEMPT;
+ }
+ loadInfo->SetHttpsOnlyStatus(httpsOnlyStatus);
+}
+
+/* static */
+bool nsHTTPSOnlyUtils::IsSafeToAcceptCORSOrMixedContent(
+ nsILoadInfo* aLoadInfo) {
+ // Check if the request is exempt from upgrades
+ if ((aLoadInfo->GetHttpsOnlyStatus() & nsILoadInfo::HTTPS_ONLY_EXEMPT)) {
+ return false;
+ }
+ // Check if HTTPS-Only Mode is enabled for this request
+ bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
+ return nsHTTPSOnlyUtils::IsHttpsOnlyModeEnabled(isPrivateWin);
+}
+
+/* static */
+bool nsHTTPSOnlyUtils::HttpsUpgradeUnrelatedErrorCode(nsresult aError) {
+ return NS_ERROR_UNKNOWN_PROTOCOL == aError ||
+ NS_ERROR_FILE_NOT_FOUND == aError ||
+ NS_ERROR_FILE_ACCESS_DENIED == aError ||
+ NS_ERROR_UNKNOWN_HOST == aError || NS_ERROR_PHISHING_URI == aError ||
+ NS_ERROR_MALWARE_URI == aError || NS_ERROR_UNWANTED_URI == aError ||
+ NS_ERROR_HARMFUL_URI == aError || NS_ERROR_CONTENT_CRASHED == aError ||
+ NS_ERROR_FRAME_CRASHED == aError || NS_ERROR_SUPERFLUOS_AUTH == aError;
+}
+
+/* ------ Logging ------ */
+
+/* static */
+void nsHTTPSOnlyUtils::LogLocalizedString(const char* aName,
+ const nsTArray<nsString>& aParams,
+ uint32_t aFlags,
+ nsILoadInfo* aLoadInfo, nsIURI* aURI,
+ bool aUseHttpsFirst) {
+ nsAutoString logMsg;
+ nsContentUtils::FormatLocalizedString(nsContentUtils::eSECURITY_PROPERTIES,
+ aName, aParams, logMsg);
+ LogMessage(logMsg, aFlags, aLoadInfo, aURI, aUseHttpsFirst);
+}
+
+/* static */
+void nsHTTPSOnlyUtils::LogMessage(const nsAString& aMessage, uint32_t aFlags,
+ nsILoadInfo* aLoadInfo, nsIURI* aURI,
+ bool aUseHttpsFirst) {
+ // do not log to the console if the loadinfo says we should not!
+ uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus();
+ if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_DO_NOT_LOG_TO_CONSOLE) {
+ return;
+ }
+
+ // Prepending HTTPS-Only to the outgoing console message
+ nsString message;
+ message.Append(aUseHttpsFirst ? u"HTTPS-First Mode: "_ns
+ : u"HTTPS-Only Mode: "_ns);
+ message.Append(aMessage);
+
+ // Allow for easy distinction in devtools code.
+ auto category = aUseHttpsFirst ? "HTTPSFirst"_ns : "HTTPSOnly"_ns;
+
+ uint64_t windowId = aLoadInfo->GetInnerWindowID();
+ if (!windowId) {
+ windowId = aLoadInfo->GetTriggeringWindowId();
+ }
+ if (windowId) {
+ // Send to content console
+ nsContentUtils::ReportToConsoleByWindowID(message, aFlags, category,
+ windowId, aURI);
+ } else {
+ // Send to browser console
+ bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
+ nsContentUtils::LogSimpleConsoleError(message, category, isPrivateWin,
+ true /* from chrome context */,
+ aFlags);
+ }
+}
+
+/* ------ Exceptions ------ */
+
+/* static */
+bool nsHTTPSOnlyUtils::OnionException(nsIURI* aURI) {
+ // Onion-host exception can get disabled with a pref
+ if (mozilla::StaticPrefs::dom_security_https_only_mode_upgrade_onion()) {
+ return false;
+ }
+ nsAutoCString host;
+ aURI->GetHost(host);
+ return StringEndsWith(host, ".onion"_ns);
+}
+
+/* static */
+bool nsHTTPSOnlyUtils::LoopbackOrLocalException(nsIURI* aURI) {
+ nsAutoCString asciiHost;
+ nsresult rv = aURI->GetAsciiHost(asciiHost);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ // Let's make a quick check if the host matches these loopback strings
+ // before we do anything else
+ if (asciiHost.EqualsLiteral("localhost") || asciiHost.EqualsLiteral("::1")) {
+ return true;
+ }
+
+ mozilla::net::NetAddr addr;
+ if (NS_FAILED(addr.InitFromString(asciiHost))) {
+ return false;
+ }
+ // Loopback IPs are always exempt
+ if (addr.IsLoopbackAddr()) {
+ return true;
+ }
+
+ // Local IP exception can get disabled with a pref
+ bool upgradeLocal =
+ mozilla::StaticPrefs::dom_security_https_only_mode_upgrade_local();
+ return (!upgradeLocal && addr.IsIPAddrLocal());
+}
+
+/* static */
+bool nsHTTPSOnlyUtils::IsEqualURIExceptSchemeAndRef(nsIURI* aHTTPSSchemeURI,
+ nsIURI* aOtherURI,
+ nsILoadInfo* aLoadInfo) {
+ // 1. Check if one of parameters is null then webpage can't be loaded yet
+ // and no further inspections are needed
+ if (!aHTTPSSchemeURI || !aOtherURI || !aLoadInfo) {
+ return false;
+ }
+
+ // 2. If the URI to be loaded is not http, then same origin will be detected
+ // already
+ if (!mozilla::net::SchemeIsHTTP(aOtherURI)) {
+ return false;
+ }
+
+ // 3. Check if the HTTPS-Only Mode is even enabled, before we do anything else
+ bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
+ if (!IsHttpsOnlyModeEnabled(isPrivateWin) &&
+ !IsHttpsFirstModeEnabled(isPrivateWin)) {
+ return false;
+ }
+
+ // 4. If the load is exempt, then it's defintely not related to https-only
+ uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus();
+ if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) {
+ return false;
+ }
+
+ // 5. Create a new target URI with 'https' instead of 'http' and compare it
+ // to the current URI
+ int32_t port = 0;
+ nsresult rv = aOtherURI->GetPort(&port);
+ NS_ENSURE_SUCCESS(rv, false);
+ // a port of -1 indicates the default port, hence we upgrade from port 80 to
+ // port 443
+ // otherwise we keep the port.
+ if (port == -1) {
+ port = NS_GetDefaultPort("https");
+ }
+ nsCOMPtr<nsIURI> newHTTPSchemeURI;
+ rv = NS_MutateURI(aOtherURI)
+ .SetScheme("https"_ns)
+ .SetPort(port)
+ .Finalize(newHTTPSchemeURI);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ bool uriEquals = false;
+ if (NS_FAILED(
+ aHTTPSSchemeURI->EqualsExceptRef(newHTTPSchemeURI, &uriEquals))) {
+ return false;
+ }
+
+ return uriEquals;
+}
+/////////////////////////////////////////////////////////////////////
+// Implementation of TestHTTPAnswerRunnable
+
+NS_IMPL_ISUPPORTS_INHERITED(TestHTTPAnswerRunnable, mozilla::Runnable,
+ nsIStreamListener, nsIInterfaceRequestor,
+ nsITimerCallback)
+
+TestHTTPAnswerRunnable::TestHTTPAnswerRunnable(
+ nsIURI* aURI, mozilla::net::DocumentLoadListener* aDocumentLoadListener)
+ : mozilla::Runnable("TestHTTPAnswerRunnable"),
+ mURI(aURI),
+ mDocumentLoadListener(aDocumentLoadListener) {}
+
+/* static */
+bool TestHTTPAnswerRunnable::IsBackgroundRequestRedirected(
+ nsIHttpChannel* aChannel) {
+ // If there is no background request (aChannel), then there is nothing
+ // to do here.
+ if (!aChannel) {
+ return false;
+ }
+ // If the request was not redirected, then there is nothing to do here.
+ nsCOMPtr<nsILoadInfo> loadinfo = aChannel->LoadInfo();
+ if (loadinfo->RedirectChain().IsEmpty()) {
+ return false;
+ }
+
+ // If the final URI is not targeting an https scheme, then we definitely not
+ // dealing with a 'same-origin' redirect.
+ nsCOMPtr<nsIURI> finalURI;
+ nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(finalURI));
+ NS_ENSURE_SUCCESS(rv, false);
+ if (!finalURI->SchemeIs("https")) {
+ return false;
+ }
+
+ // If the background request was not http, then there is nothing to do here.
+ nsCOMPtr<nsIPrincipal> firstURIPrincipal;
+ loadinfo->RedirectChain()[0]->GetPrincipal(getter_AddRefs(firstURIPrincipal));
+ if (!firstURIPrincipal || !firstURIPrincipal->SchemeIs("http")) {
+ return false;
+ }
+
+ // By now we have verified that the inital background request was http and
+ // that the redirected scheme is https. We want to find the following case
+ // where the background channel redirects to the https version of the
+ // top-level request.
+ // --> background channel: http://example.com
+ // |--> redirects to: https://example.com
+ // Now we have to check that the hosts are 'same-origin'.
+ nsAutoCString redirectHost;
+ nsAutoCString finalHost;
+ firstURIPrincipal->GetAsciiHost(redirectHost);
+ finalURI->GetAsciiHost(finalHost);
+ return finalHost.Equals(redirectHost);
+}
+
+NS_IMETHODIMP
+TestHTTPAnswerRunnable::OnStartRequest(nsIRequest* aRequest) {
+ // If the request status is not OK, it means it encountered some
+ // kind of error in which case we do not want to do anything.
+ nsresult requestStatus;
+ aRequest->GetStatus(&requestStatus);
+ if (requestStatus != NS_OK) {
+ return NS_OK;
+ }
+
+ // Check if the original top-level channel which https-only is trying
+ // to upgrade is already in progress or if the channel is an auth channel.
+ // If it is in progress or Auth is in progress, then all good, if not
+ // then let's cancel that channel so we can dispaly the exception page.
+ nsCOMPtr<nsIChannel> docChannel = mDocumentLoadListener->GetChannel();
+ nsCOMPtr<nsIHttpChannel> httpsOnlyChannel = do_QueryInterface(docChannel);
+ if (httpsOnlyChannel) {
+ nsCOMPtr<nsILoadInfo> loadInfo = httpsOnlyChannel->LoadInfo();
+ uint32_t topLevelLoadInProgress =
+ loadInfo->GetHttpsOnlyStatus() &
+ nsILoadInfo::HTTPS_ONLY_TOP_LEVEL_LOAD_IN_PROGRESS;
+
+ nsCOMPtr<nsIHttpChannelInternal> httpChannelInternal =
+ do_QueryInterface(httpsOnlyChannel);
+ bool isAuthChannel = false;
+ mozilla::Unused << httpChannelInternal->GetIsAuthChannel(&isAuthChannel);
+ // some server configurations need a long time to respond to an https
+ // connection, but also redirect any http connection to the https version of
+ // it. If the top-level load has not started yet, but the http background
+ // request redirects to https, then do not show the error page, but keep
+ // waiting for the https response of the upgraded top-level request.
+ if (!topLevelLoadInProgress) {
+ nsCOMPtr<nsIHttpChannel> backgroundHttpChannel =
+ do_QueryInterface(aRequest);
+ topLevelLoadInProgress =
+ IsBackgroundRequestRedirected(backgroundHttpChannel);
+ }
+ if (!topLevelLoadInProgress && !isAuthChannel) {
+ // Only really cancel the original top-level channel if it's
+ // status is still NS_OK, otherwise it might have already
+ // encountered some other error and was cancelled.
+ nsresult httpsOnlyChannelStatus;
+ httpsOnlyChannel->GetStatus(&httpsOnlyChannelStatus);
+ if (httpsOnlyChannelStatus == NS_OK) {
+ httpsOnlyChannel->Cancel(NS_ERROR_NET_TIMEOUT_EXTERNAL);
+ }
+ }
+ }
+
+ // Cancel this http request because it has reached the end of it's
+ // lifetime at this point.
+ aRequest->Cancel(NS_ERROR_ABORT);
+ return NS_ERROR_ABORT;
+}
+
+NS_IMETHODIMP
+TestHTTPAnswerRunnable::OnDataAvailable(nsIRequest* aRequest,
+ nsIInputStream* aStream,
+ uint64_t aOffset, uint32_t aCount) {
+ // TestHTTPAnswerRunnable only cares about ::OnStartRequest which
+ // will also cancel the request, so we should in fact never even
+ // get here.
+ MOZ_ASSERT(false, "how come we get to ::OnDataAvailable");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TestHTTPAnswerRunnable::OnStopRequest(nsIRequest* aRequest,
+ nsresult aStatusCode) {
+ // TestHTTPAnswerRunnable only cares about ::OnStartRequest
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TestHTTPAnswerRunnable::GetInterface(const nsIID& aIID, void** aResult) {
+ return QueryInterface(aIID, aResult);
+}
+
+NS_IMETHODIMP
+TestHTTPAnswerRunnable::Run() {
+ // Wait N milliseconds to give the original https request a heads start
+ // before firing up this http request in the background. By default the
+ // timer is set to 3 seconds. If the https request has not received
+ // any signal from the server during that time, than it's almost
+ // certain the upgraded request will result in time out.
+ uint32_t background_timer_ms = mozilla::StaticPrefs::
+ dom_security_https_only_fire_http_request_background_timer_ms();
+
+ return NS_NewTimerWithCallback(getter_AddRefs(mTimer), this,
+ background_timer_ms, nsITimer::TYPE_ONE_SHOT);
+}
+
+NS_IMETHODIMP
+TestHTTPAnswerRunnable::Notify(nsITimer* aTimer) {
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+
+ // If the original channel has already started loading at this point
+ // then there is no need to do the dance.
+ nsCOMPtr<nsIChannel> origChannel = mDocumentLoadListener->GetChannel();
+ nsCOMPtr<nsILoadInfo> origLoadInfo = origChannel->LoadInfo();
+ uint32_t origHttpsOnlyStatus = origLoadInfo->GetHttpsOnlyStatus();
+ uint32_t topLevelLoadInProgress =
+ origHttpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_TOP_LEVEL_LOAD_IN_PROGRESS;
+ uint32_t downloadInProgress =
+ origHttpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_DOWNLOAD_IN_PROGRESS;
+ if (topLevelLoadInProgress || downloadInProgress) {
+ return NS_OK;
+ }
+
+ mozilla::OriginAttributes attrs = origLoadInfo->GetOriginAttributes();
+ RefPtr<nsIPrincipal> nullPrincipal = mozilla::NullPrincipal::Create(attrs);
+
+ uint32_t loadFlags =
+ nsIRequest::LOAD_ANONYMOUS | nsIRequest::INHIBIT_CACHING |
+ nsIRequest::INHIBIT_PERSISTENT_CACHING | nsIRequest::LOAD_BYPASS_CACHE |
+ nsIChannel::LOAD_BYPASS_SERVICE_WORKER;
+
+ // No need to connect to the URI including the path because we only care about
+ // the round trip time if a server responds to an http request.
+ nsCOMPtr<nsIURI> backgroundChannelURI;
+ nsAutoCString prePathStr;
+ nsresult rv = mURI->GetPrePath(prePathStr);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ rv = NS_NewURI(getter_AddRefs(backgroundChannelURI), prePathStr);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // we are using TYPE_OTHER because TYPE_DOCUMENT might have side effects
+ nsCOMPtr<nsIChannel> testHTTPChannel;
+ rv = NS_NewChannel(getter_AddRefs(testHTTPChannel), backgroundChannelURI,
+ nullPrincipal,
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER, nullptr, nullptr, nullptr,
+ nullptr, loadFlags);
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // We have exempt that load from HTTPS-Only to avoid getting upgraded
+ // to https as well. Additonally let's not log that request to the console
+ // because it might confuse end users.
+ nsCOMPtr<nsILoadInfo> loadInfo = testHTTPChannel->LoadInfo();
+ uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus();
+ httpsOnlyStatus |= nsILoadInfo::HTTPS_ONLY_EXEMPT |
+ nsILoadInfo::HTTPS_ONLY_DO_NOT_LOG_TO_CONSOLE |
+ nsILoadInfo::HTTPS_ONLY_BYPASS_ORB;
+ loadInfo->SetHttpsOnlyStatus(httpsOnlyStatus);
+
+ testHTTPChannel->SetNotificationCallbacks(this);
+ testHTTPChannel->AsyncOpen(this);
+ return NS_OK;
+}
diff --git a/dom/security/nsHTTPSOnlyUtils.h b/dom/security/nsHTTPSOnlyUtils.h
new file mode 100644
index 0000000000..73a5219082
--- /dev/null
+++ b/dom/security/nsHTTPSOnlyUtils.h
@@ -0,0 +1,247 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsHTTPSOnlyUtils_h___
+#define nsHTTPSOnlyUtils_h___
+
+#include "nsIScriptError.h"
+#include "nsISupports.h"
+#include "mozilla/net/DocumentLoadListener.h"
+
+class nsHTTPSOnlyUtils {
+ public:
+ /**
+ * Returns if HTTPS-Only Mode preference is enabled
+ * @param aFromPrivateWindow true if executing in private browsing mode
+ * @return true if HTTPS-Only Mode is enabled
+ */
+ static bool IsHttpsOnlyModeEnabled(bool aFromPrivateWindow);
+
+ /**
+ * Returns if HTTPS-First Mode preference is enabled
+ * @param aFromPrivateWindow true if executing in private browsing mode
+ * @return true if HTTPS-First Mode is enabled
+ */
+ static bool IsHttpsFirstModeEnabled(bool aFromPrivateWindow);
+
+ /**
+ * Potentially fires an http request for a top-level load (provided by
+ * aDocumentLoadListener) in the background to avoid long timeouts in case
+ * the upgraded https top-level load most likely will result in timeout.
+ * @param aDocumentLoadListener The Document listener associated with
+ * the original top-level channel.
+ */
+ static void PotentiallyFireHttpRequestToShortenTimout(
+ mozilla::net::DocumentLoadListener* aDocumentLoadListener);
+
+ /**
+ * Determines if a request should get upgraded because of the HTTPS-Only mode.
+ * If true, the httpsOnlyStatus flag in LoadInfo gets updated and a message is
+ * logged in the console.
+ * @param aURI nsIURI of request
+ * @param aLoadInfo nsILoadInfo of request
+ * @return true if request should get upgraded
+ */
+ static bool ShouldUpgradeRequest(nsIURI* aURI, nsILoadInfo* aLoadInfo);
+
+ /**
+ * Determines if a request should get upgraded because of the HTTPS-Only mode.
+ * If true, a message is logged in the console.
+ * @param aURI nsIURI of request
+ * @param aLoadInfo nsILoadInfo of request
+ * @return true if request should get upgraded
+ */
+ static bool ShouldUpgradeWebSocket(nsIURI* aURI, nsILoadInfo* aLoadInfo);
+
+ /**
+ * Determines if we might get stuck in an upgrade-downgrade-endless loop
+ * where https-only upgrades the request to https and the website downgrades
+ * the scheme to http again causing an endless upgrade downgrade loop. E.g.
+ * https-only upgrades to https and the website answers with a meta-refresh
+ * to downgrade to same-origin http version. Similarly this method breaks
+ * the endless cycle for JS based redirects and 302 based redirects.
+ * Note this function is also used when we got an HTTPS RR for the website.
+ * @param aURI nsIURI of request
+ * @param aLoadInfo nsILoadInfo of request
+ * @param aOptions an options object indicating if the function
+ * should be consulted for https-only or https-first mode or
+ * the case that an HTTPS RR is presented.
+ * @return true if an endless loop is detected
+ */
+ enum class UpgradeDowngradeEndlessLoopOptions {
+ EnforceForHTTPSOnlyMode,
+ EnforceForHTTPSFirstMode,
+ EnforceForHTTPSRR,
+ };
+ static bool IsUpgradeDowngradeEndlessLoop(
+ nsIURI* aURI, nsILoadInfo* aLoadInfo,
+ const mozilla::EnumSet<UpgradeDowngradeEndlessLoopOptions>& aOptions =
+ {});
+
+ /**
+ * Determines if a request should get upgraded because of the HTTPS-First
+ * mode. If true, the httpsOnlyStatus in LoadInfo gets updated and a message
+ * is logged in the console.
+ * @param aURI nsIURI of request
+ * @param aLoadInfo nsILoadInfo of request
+ * @return true if request should get upgraded
+ */
+ static bool ShouldUpgradeHttpsFirstRequest(nsIURI* aURI,
+ nsILoadInfo* aLoadInfo);
+
+ /**
+ * Determines if the request was previously upgraded with HTTPS-First, creates
+ * a downgraded URI and logs to console.
+ * @param aStatus Status code
+ * @param aChannel Failed channel
+ * @return URI with http-scheme or nullptr
+ */
+ static already_AddRefed<nsIURI> PotentiallyDowngradeHttpsFirstRequest(
+ nsIChannel* aChannel, nsresult aStatus);
+
+ /**
+ * Checks if the error code is on a block-list of codes that are probably
+ * not related to a HTTPS-Only Mode upgrade.
+ * @param aChannel The failed Channel.
+ * @param aError Error Code from Request
+ * @return false if error is not related to upgrade
+ */
+ static bool CouldBeHttpsOnlyError(nsIChannel* aChannel, nsresult aError);
+
+ /**
+ * Logs localized message to either content console or browser console
+ * @param aName Localization key
+ * @param aParams Localization parameters
+ * @param aFlags Logging Flag (see nsIScriptError)
+ * @param aLoadInfo The loadinfo of the request.
+ * @param [aURI] Optional: URI to log
+ * @param [aUseHttpsFirst] Optional: Log using HTTPS-First (vs. HTTPS-Only)
+ */
+ static void LogLocalizedString(const char* aName,
+ const nsTArray<nsString>& aParams,
+ uint32_t aFlags, nsILoadInfo* aLoadInfo,
+ nsIURI* aURI = nullptr,
+ bool aUseHttpsFirst = false);
+
+ /**
+ * Tests if the HTTPS-Only upgrade exception is set for a given principal.
+ * @param aPrincipal The principal for whom the exception should be checked
+ * @return True if exempt
+ */
+ static bool TestIfPrincipalIsExempt(nsIPrincipal* aPrincipal);
+
+ /**
+ * Tests if the HTTPS-Only Mode upgrade exception is set for channel result
+ * principal and sets or removes the httpsOnlyStatus-flag on the loadinfo
+ * accordingly.
+ * Note: This function only adds an exemption for loads of TYPE_DOCUMENT.
+ * @param aChannel The channel to be checked
+ */
+ static void TestSitePermissionAndPotentiallyAddExemption(
+ nsIChannel* aChannel);
+
+ /**
+ * Checks whether CORS or mixed content requests are safe because they'll get
+ * upgraded to HTTPS
+ * @param aLoadInfo nsILoadInfo of request
+ * @return true if it's safe to accept
+ */
+ static bool IsSafeToAcceptCORSOrMixedContent(nsILoadInfo* aLoadInfo);
+
+ /**
+ * Checks if two URIs are same origin modulo the difference that
+ * aHTTPSchemeURI uses an http scheme.
+ * @param aHTTPSSchemeURI nsIURI using scheme of https
+ * @param aOtherURI nsIURI using scheme of http
+ * @param aLoadInfo nsILoadInfo of the request
+ * @return true, if URIs are equal except scheme and ref
+ */
+ static bool IsEqualURIExceptSchemeAndRef(nsIURI* aHTTPSSchemeURI,
+ nsIURI* aOtherURI,
+ nsILoadInfo* aLoadInfo);
+
+ private:
+ /**
+ * Checks if it can be ruled out that the error has something
+ * to do with an HTTPS upgrade.
+ * @param aError error code
+ * @return true if error is unrelated to the upgrade
+ */
+ static bool HttpsUpgradeUnrelatedErrorCode(nsresult aError);
+ /**
+ * Logs localized message to either content console or browser console
+ * @param aMessage Message to log
+ * @param aFlags Logging Flag (see nsIScriptError)
+ * @param aLoadInfo The loadinfo of the request.
+ * @param [aURI] Optional: URI to log
+ * @param [aUseHttpsFirst] Optional: Log using HTTPS-First (vs. HTTPS-Only)
+ */
+ static void LogMessage(const nsAString& aMessage, uint32_t aFlags,
+ nsILoadInfo* aLoadInfo, nsIURI* aURI = nullptr,
+ bool aUseHttpsFirst = false);
+
+ /**
+ * Checks whether the URI ends with .onion
+ * @param aURI URI object
+ * @return true if the URI is an Onion URI
+ */
+ static bool OnionException(nsIURI* aURI);
+
+ /**
+ * Checks whether the URI is a loopback- or local-IP
+ * @param aURI URI object
+ * @return true if the URI is either loopback or local
+ */
+ static bool LoopbackOrLocalException(nsIURI* aURI);
+};
+
+/**
+ * Helper class to perform an http request with a N milliseconds
+ * delay. If that http request is 'receiving data' before the
+ * upgraded https request, then it's a strong indicator that
+ * the https request will result in a timeout and hence we
+ * cancel the https request which will result in displaying
+ * the exception page.
+ */
+class TestHTTPAnswerRunnable final : public mozilla::Runnable,
+ public nsIStreamListener,
+ public nsIInterfaceRequestor,
+ public nsITimerCallback {
+ public:
+ // TestHTTPAnswerRunnable needs to implement all these channel related
+ // interfaces because otherwise our Necko code is not happy, but we
+ // really only care about ::OnStartRequest.
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIRUNNABLE
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSIINTERFACEREQUESTOR
+ NS_DECL_NSITIMERCALLBACK
+
+ explicit TestHTTPAnswerRunnable(
+ nsIURI* aURI, mozilla::net::DocumentLoadListener* aDocumentLoadListener);
+
+ protected:
+ ~TestHTTPAnswerRunnable() = default;
+
+ private:
+ /**
+ * Checks whether the HTTP background request results in a redirect
+ * to the same upgraded top-level HTTPS URL
+ * @param aChannel a nsIHttpChannel object
+ * @return true if the backgroundchannel is redirected
+ */
+ static bool IsBackgroundRequestRedirected(nsIHttpChannel* aChannel);
+
+ RefPtr<nsIURI> mURI;
+ // We're keeping a reference to DocumentLoadListener instead of a specific
+ // channel, because the current top-level channel can change (for example
+ // through redirects)
+ RefPtr<mozilla::net::DocumentLoadListener> mDocumentLoadListener;
+ RefPtr<nsITimer> mTimer;
+};
+
+#endif /* nsHTTPSOnlyUtils_h___ */
diff --git a/dom/security/nsIHttpsOnlyModePermission.idl b/dom/security/nsIHttpsOnlyModePermission.idl
new file mode 100644
index 0000000000..7eabdb6715
--- /dev/null
+++ b/dom/security/nsIHttpsOnlyModePermission.idl
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+/**
+ * An interface to test for cookie permissions
+ */
+[scriptable, uuid(73f4f039-d6ff-41a7-9eb3-00db57b0b7f4)]
+interface nsIHttpsOnlyModePermission : nsISupports
+{
+ /**
+ * nsIPermissionManager permission values
+ */
+ const uint32_t LOAD_INSECURE_DEFAULT = 0;
+ const uint32_t LOAD_INSECURE_ALLOW = 1;
+ const uint32_t LOAD_INSECURE_BLOCK = 2;
+
+ /**
+ * additional values which do not match
+ * nsIPermissionManager. Keep space available to allow nsIPermissionManager to
+ * add values without colliding. ACCESS_SESSION is not directly returned by
+ * any methods on this interface.
+ */
+ const uint32_t LOAD_INSECURE_ALLOW_SESSION = 9;
+};
diff --git a/dom/security/nsMixedContentBlocker.cpp b/dom/security/nsMixedContentBlocker.cpp
new file mode 100644
index 0000000000..5ca8a9743a
--- /dev/null
+++ b/dom/security/nsMixedContentBlocker.cpp
@@ -0,0 +1,1065 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsMixedContentBlocker.h"
+
+#include "nsContentPolicyUtils.h"
+#include "nsCSPContext.h"
+#include "nsThreadUtils.h"
+#include "nsINode.h"
+#include "nsCOMPtr.h"
+#include "nsDocShell.h"
+#include "nsIWebProgressListener.h"
+#include "nsContentUtils.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/WindowContext.h"
+#include "mozilla/dom/Document.h"
+#include "nsIChannel.h"
+#include "nsIParentChannel.h"
+#include "mozilla/Preferences.h"
+#include "nsIScriptObjectPrincipal.h"
+#include "nsIProtocolHandler.h"
+#include "nsCharSeparatedTokenizer.h"
+#include "nsISecureBrowserUI.h"
+#include "nsIWebNavigation.h"
+#include "nsLoadGroup.h"
+#include "nsIScriptError.h"
+#include "nsIURI.h"
+#include "nsIChannelEventSink.h"
+#include "nsNetUtil.h"
+#include "nsAsyncRedirectVerifyHelper.h"
+#include "mozilla/LoadInfo.h"
+#include "nsISiteSecurityService.h"
+#include "prnetdb.h"
+#include "nsQueryObject.h"
+
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/Logging.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPrefs_fission.h"
+#include "mozilla/StaticPrefs_security.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/ipc/URIUtils.h"
+#include "mozilla/net/DNS.h"
+#include "mozilla/net/DocumentLoadListener.h"
+#include "mozilla/net/DocumentChannel.h"
+
+#include "mozilla/dom/nsHTTPSOnlyUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+static mozilla::LazyLogModule sMCBLog("MCBLog");
+
+enum nsMixedContentBlockerMessageType { eBlocked = 0x00, eUserOverride = 0x01 };
+
+// Allowlist of hostnames that should be considered secure contexts even when
+// served over http:// or ws://
+nsCString* nsMixedContentBlocker::sSecurecontextAllowlist = nullptr;
+bool nsMixedContentBlocker::sSecurecontextAllowlistCached = false;
+
+enum MixedContentHSTSState {
+ MCB_HSTS_PASSIVE_NO_HSTS = 0,
+ MCB_HSTS_PASSIVE_WITH_HSTS = 1,
+ MCB_HSTS_ACTIVE_NO_HSTS = 2,
+ MCB_HSTS_ACTIVE_WITH_HSTS = 3
+};
+
+nsMixedContentBlocker::~nsMixedContentBlocker() = default;
+
+NS_IMPL_ISUPPORTS(nsMixedContentBlocker, nsIContentPolicy, nsIChannelEventSink)
+
+static void LogMixedContentMessage(
+ MixedContentTypes aClassification, nsIURI* aContentLocation,
+ uint64_t aInnerWindowID, nsMixedContentBlockerMessageType aMessageType,
+ nsIURI* aRequestingLocation,
+ const nsACString& aOverruleMessageLookUpKeyWithThis = ""_ns) {
+ nsAutoCString messageCategory;
+ uint32_t severityFlag;
+ nsAutoCString messageLookupKey;
+
+ if (aMessageType == eBlocked) {
+ severityFlag = nsIScriptError::errorFlag;
+ messageCategory.AssignLiteral("Mixed Content Blocker");
+ if (aClassification == eMixedDisplay) {
+ messageLookupKey.AssignLiteral("BlockMixedDisplayContent");
+ } else {
+ messageLookupKey.AssignLiteral("BlockMixedActiveContent");
+ }
+ } else {
+ severityFlag = nsIScriptError::warningFlag;
+ messageCategory.AssignLiteral("Mixed Content Message");
+ if (aClassification == eMixedDisplay) {
+ messageLookupKey.AssignLiteral("LoadingMixedDisplayContent2");
+ } else {
+ messageLookupKey.AssignLiteral("LoadingMixedActiveContent2");
+ }
+ }
+
+ // if the callee explicitly wants to use a special message for this
+ // console report, then we allow to overrule the default with the
+ // explicitly provided one here.
+ if (!aOverruleMessageLookUpKeyWithThis.IsEmpty()) {
+ messageLookupKey = aOverruleMessageLookUpKeyWithThis;
+ }
+
+ nsAutoString localizedMsg;
+ AutoTArray<nsString, 1> params;
+ CopyUTF8toUTF16(aContentLocation->GetSpecOrDefault(),
+ *params.AppendElement());
+ nsContentUtils::FormatLocalizedString(nsContentUtils::eSECURITY_PROPERTIES,
+ messageLookupKey.get(), params,
+ localizedMsg);
+
+ nsContentUtils::ReportToConsoleByWindowID(localizedMsg, severityFlag,
+ messageCategory, aInnerWindowID,
+ aRequestingLocation);
+}
+
+/* nsIChannelEventSink implementation
+ * This code is called when a request is redirected.
+ * We check the channel associated with the new uri is allowed to load
+ * in the current context
+ */
+NS_IMETHODIMP
+nsMixedContentBlocker::AsyncOnChannelRedirect(
+ nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags,
+ nsIAsyncVerifyRedirectCallback* aCallback) {
+ mozilla::net::nsAsyncRedirectAutoCallback autoCallback(aCallback);
+
+ if (!aOldChannel) {
+ NS_ERROR("No channel when evaluating mixed content!");
+ return NS_ERROR_FAILURE;
+ }
+
+ // If we are in the parent process in e10s, we don't have access to the
+ // document node, and hence ShouldLoad will fail when we try to get
+ // the docShell. If that's the case, ignore mixed content checks
+ // on redirects in the parent. Let the child check for mixed content.
+ nsCOMPtr<nsIParentChannel> is_ipc_channel;
+ NS_QueryNotificationCallbacks(aNewChannel, is_ipc_channel);
+ RefPtr<net::DocumentLoadListener> docListener =
+ do_QueryObject(is_ipc_channel);
+ if (is_ipc_channel && !docListener) {
+ return NS_OK;
+ }
+
+ // Don't do these checks if we're switching from DocumentChannel
+ // to a real channel. In that case, we should already have done
+ // the checks in the parent process. AsyncOnChannelRedirect
+ // isn't called in the content process if we switch process,
+ // so checking here would just hide bugs in the process switch
+ // cases.
+ if (RefPtr<net::DocumentChannel> docChannel = do_QueryObject(aOldChannel)) {
+ return NS_OK;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIURI> oldUri;
+ rv = aOldChannel->GetURI(getter_AddRefs(oldUri));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> newUri;
+ rv = aNewChannel->GetURI(getter_AddRefs(newUri));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the loading Info from the old channel
+ nsCOMPtr<nsILoadInfo> loadInfo = aOldChannel->LoadInfo();
+ nsCOMPtr<nsIPrincipal> requestingPrincipal = loadInfo->GetLoadingPrincipal();
+
+ // Since we are calling shouldLoad() directly on redirects, we don't go
+ // through the code in nsContentPolicyUtils::NS_CheckContentLoadPolicy().
+ // Hence, we have to duplicate parts of it here.
+ if (requestingPrincipal) {
+ // We check to see if the loadingPrincipal is systemPrincipal and return
+ // early if it is
+ if (requestingPrincipal->IsSystemPrincipal()) {
+ return NS_OK;
+ }
+ }
+
+ int16_t decision = REJECT_REQUEST;
+ rv = ShouldLoad(newUri, loadInfo, &decision);
+ if (NS_FAILED(rv)) {
+ autoCallback.DontCallback();
+ aOldChannel->Cancel(NS_ERROR_DOM_BAD_URI);
+ return NS_BINDING_FAILED;
+ }
+
+ // If the channel is about to load mixed content, abort the channel
+ if (!NS_CP_ACCEPTED(decision)) {
+ autoCallback.DontCallback();
+ aOldChannel->Cancel(NS_ERROR_DOM_BAD_URI);
+ return NS_BINDING_FAILED;
+ }
+
+ return NS_OK;
+}
+
+/* This version of ShouldLoad() is non-static and called by the Content Policy
+ * API and AsyncOnChannelRedirect(). See nsIContentPolicy::ShouldLoad()
+ * for detailed description of the parameters.
+ */
+NS_IMETHODIMP
+nsMixedContentBlocker::ShouldLoad(nsIURI* aContentLocation,
+ nsILoadInfo* aLoadInfo, int16_t* aDecision) {
+ // We pass in false as the first parameter to ShouldLoad(), because the
+ // callers of this method don't know whether the load went through cached
+ // image redirects. This is handled by direct callers of the static
+ // ShouldLoad.
+ nsresult rv = ShouldLoad(false, // aHadInsecureImageRedirect
+ aContentLocation, aLoadInfo, true, aDecision);
+
+ if (*aDecision == nsIContentPolicy::REJECT_REQUEST) {
+ NS_SetRequestBlockingReason(aLoadInfo,
+ nsILoadInfo::BLOCKING_REASON_MIXED_BLOCKED);
+ }
+
+ return rv;
+}
+
+bool nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackHost(
+ const nsACString& aAsciiHost) {
+ if (mozilla::net::IsLoopbackHostname(aAsciiHost)) {
+ return true;
+ }
+
+ using namespace mozilla::net;
+ NetAddr addr;
+ if (NS_FAILED(addr.InitFromString(aAsciiHost))) {
+ return false;
+ }
+
+ // Step 4 of
+ // https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy says
+ // we should only consider [::1]/128 as a potentially trustworthy IPv6
+ // address, whereas for IPv4 127.0.0.1/8 are considered as potentially
+ // trustworthy.
+ return addr.IsLoopBackAddressWithoutIPv6Mapping();
+}
+
+bool nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackURL(nsIURI* aURL) {
+ if (!aURL) {
+ return false;
+ }
+ nsAutoCString asciiHost;
+ nsresult rv = aURL->GetAsciiHost(asciiHost);
+ NS_ENSURE_SUCCESS(rv, false);
+ return IsPotentiallyTrustworthyLoopbackHost(asciiHost);
+}
+
+/* Maybe we have a .onion URL. Treat it as trustworthy as well if
+ * `dom.securecontext.allowlist_onions` is `true`.
+ */
+bool nsMixedContentBlocker::IsPotentiallyTrustworthyOnion(nsIURI* aURL) {
+ if (!StaticPrefs::dom_securecontext_allowlist_onions()) {
+ return false;
+ }
+
+ nsAutoCString host;
+ nsresult rv = aURL->GetHost(host);
+ NS_ENSURE_SUCCESS(rv, false);
+ return StringEndsWith(host, ".onion"_ns);
+}
+
+// static
+void nsMixedContentBlocker::OnPrefChange(const char* aPref, void* aClosure) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!strcmp(aPref, "dom.securecontext.allowlist"));
+ Preferences::GetCString("dom.securecontext.allowlist",
+ *sSecurecontextAllowlist);
+}
+
+// static
+void nsMixedContentBlocker::GetSecureContextAllowList(nsACString& aList) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!sSecurecontextAllowlistCached) {
+ MOZ_ASSERT(!sSecurecontextAllowlist);
+ sSecurecontextAllowlistCached = true;
+ sSecurecontextAllowlist = new nsCString();
+ Preferences::RegisterCallbackAndCall(OnPrefChange,
+ "dom.securecontext.allowlist");
+ }
+ aList = *sSecurecontextAllowlist;
+}
+
+// static
+void nsMixedContentBlocker::Shutdown() {
+ if (sSecurecontextAllowlist) {
+ delete sSecurecontextAllowlist;
+ sSecurecontextAllowlist = nullptr;
+ }
+}
+
+bool nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(nsIURI* aURI) {
+ // The following implements:
+ // https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy
+
+ nsAutoCString scheme;
+ nsresult rv = aURI->GetScheme(scheme);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ // Blobs are expected to inherit their principal so we don't expect to have
+ // a content principal with scheme 'blob' here. We can't assert that though
+ // since someone could mess with a non-blob URI to give it that scheme.
+ NS_WARNING_ASSERTION(!scheme.EqualsLiteral("blob"),
+ "IsOriginPotentiallyTrustworthy ignoring blob scheme");
+
+ // According to the specification, the user agent may choose to extend the
+ // trust to other, vendor-specific URL schemes. We use this for "resource:",
+ // which is technically a substituting protocol handler that is not limited to
+ // local resource mapping, but in practice is never mapped remotely as this
+ // would violate assumptions a lot of code makes.
+ // We use nsIProtocolHandler flags to determine which protocols we consider a
+ // priori authenticated.
+ bool aPrioriAuthenticated = false;
+ if (NS_FAILED(NS_URIChainHasFlags(
+ aURI, nsIProtocolHandler::URI_IS_POTENTIALLY_TRUSTWORTHY,
+ &aPrioriAuthenticated))) {
+ return false;
+ }
+
+ if (aPrioriAuthenticated) {
+ return true;
+ }
+
+ nsAutoCString host;
+ rv = aURI->GetHost(host);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ if (IsPotentiallyTrustworthyLoopbackURL(aURI)) {
+ return true;
+ }
+
+ // If a host is not considered secure according to the default algorithm, then
+ // check to see if it has been allowlisted by the user. We only apply this
+ // allowlist for network resources, i.e., those with scheme "http" or "ws".
+ // The pref should contain a comma-separated list of hostnames.
+
+ if (!scheme.EqualsLiteral("http") && !scheme.EqualsLiteral("ws")) {
+ return false;
+ }
+
+ nsAutoCString allowlist;
+ GetSecureContextAllowList(allowlist);
+ for (const nsACString& allowedHost :
+ nsCCharSeparatedTokenizer(allowlist, ',').ToRange()) {
+ if (host.Equals(allowedHost)) {
+ return true;
+ }
+ }
+
+ // Maybe we have a .onion URL. Treat it as trustworthy as well if
+ // `dom.securecontext.allowlist_onions` is `true`.
+ if (nsMixedContentBlocker::IsPotentiallyTrustworthyOnion(aURI)) {
+ return true;
+ }
+ return false;
+}
+
+/* static */
+bool nsMixedContentBlocker::IsUpgradableContentType(nsContentPolicyType aType,
+ bool aConsiderPrefs) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (aConsiderPrefs &&
+ !StaticPrefs::security_mixed_content_upgrade_display_content()) {
+ return false;
+ }
+
+ switch (aType) {
+ case nsIContentPolicy::TYPE_INTERNAL_IMAGE:
+ case nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD:
+ return !aConsiderPrefs ||
+ StaticPrefs::
+ security_mixed_content_upgrade_display_content_image();
+ case nsIContentPolicy::TYPE_INTERNAL_AUDIO:
+ return !aConsiderPrefs ||
+ StaticPrefs::
+ security_mixed_content_upgrade_display_content_audio();
+ case nsIContentPolicy::TYPE_INTERNAL_VIDEO:
+ return !aConsiderPrefs ||
+ StaticPrefs::
+ security_mixed_content_upgrade_display_content_video();
+ default:
+ return false;
+ }
+}
+
+/*
+ * Return the URI of the precusor principal or the URI of aPrincipal if there is
+ * no precursor URI.
+ */
+static already_AddRefed<nsIURI> GetPrincipalURIOrPrecursorPrincipalURI(
+ nsIPrincipal* aPrincipal) {
+ nsCOMPtr<nsIPrincipal> precursorPrincipal =
+ aPrincipal->GetPrecursorPrincipal();
+
+#ifdef DEBUG
+ if (precursorPrincipal) {
+ MOZ_ASSERT(aPrincipal->GetIsNullPrincipal(),
+ "Only Null Principals should have a Precursor Principal");
+ }
+#endif
+
+ return precursorPrincipal ? precursorPrincipal->GetURI()
+ : aPrincipal->GetURI();
+}
+
+/* Static version of ShouldLoad() that contains all the Mixed Content Blocker
+ * logic. Called from non-static ShouldLoad().
+ */
+nsresult nsMixedContentBlocker::ShouldLoad(bool aHadInsecureImageRedirect,
+ nsIURI* aContentLocation,
+ nsILoadInfo* aLoadInfo,
+ bool aReportError,
+ int16_t* aDecision) {
+ // Asserting that we are on the main thread here and hence do not have to lock
+ // and unlock security.mixed_content.block_active_content and
+ // security.mixed_content.block_display_content before reading/writing to
+ // them.
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (MOZ_UNLIKELY(MOZ_LOG_TEST(sMCBLog, LogLevel::Verbose))) {
+ nsAutoCString asciiUrl;
+ aContentLocation->GetAsciiSpec(asciiUrl);
+ MOZ_LOG(sMCBLog, LogLevel::Verbose, ("shouldLoad:"));
+ MOZ_LOG(sMCBLog, LogLevel::Verbose,
+ (" - contentLocation: %s", asciiUrl.get()));
+ }
+
+ nsContentPolicyType internalContentType =
+ aLoadInfo->InternalContentPolicyType();
+ nsCOMPtr<nsIPrincipal> loadingPrincipal = aLoadInfo->GetLoadingPrincipal();
+ nsCOMPtr<nsIPrincipal> triggeringPrincipal = aLoadInfo->TriggeringPrincipal();
+
+ if (MOZ_UNLIKELY(MOZ_LOG_TEST(sMCBLog, LogLevel::Verbose))) {
+ MOZ_LOG(sMCBLog, LogLevel::Verbose,
+ (" - internalContentPolicyType: %s",
+ NS_CP_ContentTypeName(internalContentType)));
+
+ if (loadingPrincipal != nullptr) {
+ nsAutoCString loadingPrincipalAsciiUrl;
+ loadingPrincipal->GetAsciiSpec(loadingPrincipalAsciiUrl);
+ MOZ_LOG(sMCBLog, LogLevel::Verbose,
+ (" - loadingPrincipal: %s", loadingPrincipalAsciiUrl.get()));
+ } else {
+ MOZ_LOG(sMCBLog, LogLevel::Verbose, (" - loadingPrincipal: (nullptr)"));
+ }
+
+ nsAutoCString triggeringPrincipalAsciiUrl;
+ triggeringPrincipal->GetAsciiSpec(triggeringPrincipalAsciiUrl);
+ MOZ_LOG(sMCBLog, LogLevel::Verbose,
+ (" - triggeringPrincipal: %s", triggeringPrincipalAsciiUrl.get()));
+ }
+
+ RefPtr<WindowContext> requestingWindow =
+ WindowContext::GetById(aLoadInfo->GetInnerWindowID());
+
+ bool isPreload = nsContentUtils::IsPreloadType(internalContentType);
+
+ // The content policy type that we receive may be an internal type for
+ // scripts. Let's remember if we have seen a worker type, and reset it to the
+ // external type in all cases right now.
+ bool isWorkerType =
+ internalContentType == nsIContentPolicy::TYPE_INTERNAL_WORKER ||
+ internalContentType ==
+ nsIContentPolicy::TYPE_INTERNAL_WORKER_STATIC_MODULE ||
+ internalContentType == nsIContentPolicy::TYPE_INTERNAL_SHARED_WORKER ||
+ internalContentType == nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER;
+ ExtContentPolicyType contentType =
+ nsContentUtils::InternalContentPolicyTypeToExternal(internalContentType);
+
+ // Assume active (high risk) content and blocked by default
+ MixedContentTypes classification = eMixedScript;
+ // Make decision to block/reject by default
+ *aDecision = REJECT_REQUEST;
+
+ // Notes on non-obvious decisions:
+ //
+ // TYPE_DTD: A DTD can contain entity definitions that expand to scripts.
+ //
+ // TYPE_FONT: The TrueType hinting mechanism is basically a scripting
+ // language that gets interpreted by the operating system's font rasterizer.
+ // Mixed content web fonts are relatively uncommon, and we can can fall back
+ // to built-in fonts with minimal disruption in almost all cases.
+ //
+ // TYPE_OBJECT_SUBREQUEST could actually be either active content (e.g. a
+ // script that a plugin will execute) or display content (e.g. Flash video
+ // content). Until we have a way to determine active vs passive content
+ // from plugin requests (bug 836352), we will treat this as passive content.
+ // This is to prevent false positives from causing users to become
+ // desensitized to the mixed content blocker.
+ //
+ // TYPE_CSP_REPORT: High-risk because they directly leak information about
+ // the content of the page, and because blocking them does not have any
+ // negative effect on the page loading.
+ //
+ // TYPE_PING: Ping requests are POSTS, not GETs like images and media.
+ // Also, PING requests have no bearing on the rendering or operation of
+ // the page when used as designed, so even though they are lower risk than
+ // scripts, blocking them is basically risk-free as far as compatibility is
+ // concerned.
+ //
+ // TYPE_STYLESHEET: XSLT stylesheets can insert scripts. CSS positioning
+ // and other advanced CSS features can possibly be exploited to cause
+ // spoofing attacks (e.g. make a "grant permission" button look like a
+ // "refuse permission" button).
+ //
+ // TYPE_BEACON: Beacon requests are similar to TYPE_PING, and are blocked by
+ // default.
+ //
+ // TYPE_WEBSOCKET: The Websockets API requires browsers to
+ // reject mixed-content websockets: "If secure is false but the origin of
+ // the entry script has a scheme component that is itself a secure protocol,
+ // e.g. HTTPS, then throw a SecurityError exception." We already block mixed
+ // content websockets within the websockets implementation, so we don't need
+ // to do any blocking here, nor do we need to provide a way to undo or
+ // override the blocking. Websockets without TLS are very flaky anyway in the
+ // face of many HTTP-aware proxies. Compared to passive content, there is
+ // additional risk that the script using WebSockets will disclose sensitive
+ // information from the HTTPS page and/or eval (directly or indirectly)
+ // received data.
+ //
+ // TYPE_XMLHTTPREQUEST: XHR requires either same origin or CORS, so most
+ // mixed-content XHR will already be blocked by that check. This will also
+ // block HTTPS-to-HTTP XHR with CORS. The same security concerns mentioned
+ // above for WebSockets apply to XHR, and XHR should have the same security
+ // properties as WebSockets w.r.t. mixed content. XHR's handling of redirects
+ // amplifies these concerns.
+ //
+ // TYPE_PROXIED_WEBRTC_MEDIA: Ordinarily, webrtc uses low-level sockets for
+ // peer-to-peer media, which bypasses this code entirely. However, when a
+ // web proxy is being used, the TCP and TLS webrtc connections are routed
+ // through the web proxy (using HTTP CONNECT), which causes these connections
+ // to be checked. We just skip mixed content blocking in that case.
+
+ switch (contentType) {
+ // The top-level document cannot be mixed content by definition
+ case ExtContentPolicy::TYPE_DOCUMENT:
+ *aDecision = ACCEPT;
+ return NS_OK;
+ // Creating insecure websocket connections in a secure page is blocked
+ // already in the websocket constructor. We don't need to check the blocking
+ // here and we don't want to un-block
+ case ExtContentPolicy::TYPE_WEBSOCKET:
+ *aDecision = ACCEPT;
+ return NS_OK;
+
+ // TYPE_SAVEAS_DOWNLOAD: Save-link-as feature is used to download a
+ // resource
+ // without involving a docShell. This kind of loading must be
+ // allowed, if not disabled in the preferences.
+ // Creating insecure connections for a save-as link download is
+ // acceptable. This download is completely disconnected from the docShell,
+ // but still using the same loading principal.
+
+ case ExtContentPolicy::TYPE_SAVEAS_DOWNLOAD:
+ *aDecision = ACCEPT;
+ return NS_OK;
+ break;
+
+ // It does not make sense to subject webrtc media connections to mixed
+ // content blocking, since those connections are peer-to-peer and will
+ // therefore almost never match the origin.
+ case ExtContentPolicy::TYPE_PROXIED_WEBRTC_MEDIA:
+ *aDecision = ACCEPT;
+ return NS_OK;
+
+ // Static display content is considered moderate risk for mixed content so
+ // these will be blocked according to the mixed display preference
+ case ExtContentPolicy::TYPE_IMAGE:
+ case ExtContentPolicy::TYPE_MEDIA:
+ classification = eMixedDisplay;
+ break;
+ case ExtContentPolicy::TYPE_OBJECT_SUBREQUEST:
+ if (StaticPrefs::security_mixed_content_block_object_subrequest()) {
+ classification = eMixedScript;
+ } else {
+ classification = eMixedDisplay;
+ }
+ break;
+
+ // Active content (or content with a low value/risk-of-blocking ratio)
+ // that has been explicitly evaluated; listed here for documentation
+ // purposes and to avoid the assertion and warning for the default case.
+ case ExtContentPolicy::TYPE_BEACON:
+ case ExtContentPolicy::TYPE_CSP_REPORT:
+ case ExtContentPolicy::TYPE_DTD:
+ case ExtContentPolicy::TYPE_FETCH:
+ case ExtContentPolicy::TYPE_FONT:
+ case ExtContentPolicy::TYPE_UA_FONT:
+ case ExtContentPolicy::TYPE_IMAGESET:
+ case ExtContentPolicy::TYPE_OBJECT:
+ case ExtContentPolicy::TYPE_SCRIPT:
+ case ExtContentPolicy::TYPE_STYLESHEET:
+ case ExtContentPolicy::TYPE_SUBDOCUMENT:
+ case ExtContentPolicy::TYPE_PING:
+ case ExtContentPolicy::TYPE_WEB_MANIFEST:
+ case ExtContentPolicy::TYPE_XMLHTTPREQUEST:
+ case ExtContentPolicy::TYPE_XSLT:
+ case ExtContentPolicy::TYPE_OTHER:
+ case ExtContentPolicy::TYPE_SPECULATIVE:
+ case ExtContentPolicy::TYPE_WEB_TRANSPORT:
+ case ExtContentPolicy::TYPE_WEB_IDENTITY:
+ break;
+
+ case ExtContentPolicy::TYPE_INVALID:
+ MOZ_ASSERT(false, "Mixed content of unknown type");
+ // Do not add default: so that compilers can catch the missing case.
+ }
+
+ // Make sure to get the URI the load started with. No need to check
+ // outer schemes because all the wrapping pseudo protocols inherit the
+ // security properties of the actual network request represented
+ // by the innerMost URL.
+ nsCOMPtr<nsIURI> innerContentLocation = NS_GetInnermostURI(aContentLocation);
+ if (!innerContentLocation) {
+ NS_ERROR("Can't get innerURI from aContentLocation");
+ *aDecision = REJECT_REQUEST;
+ MOZ_LOG(sMCBLog, LogLevel::Verbose,
+ (" -> decision: Request will be rejected because the innermost "
+ "URI could not be "
+ "retrieved"));
+ return NS_OK;
+ }
+
+ // TYPE_IMAGE redirects are cached based on the original URI, not the final
+ // destination and hence cache hits for images may not have the correct
+ // innerContentLocation. Check if the cached hit went through an http
+ // redirect, and if it did, we can't treat this as a secure subresource.
+ if (!aHadInsecureImageRedirect &&
+ URISafeToBeLoadedInSecureContext(innerContentLocation)) {
+ *aDecision = ACCEPT;
+ return NS_OK;
+ }
+
+ /*
+ * Most likely aLoadingPrincipal reflects the security context of the owning
+ * document for this mixed content check. There are cases where that is not
+ * true, hence we have to we process requests in the following order:
+ * 1) If the load is triggered by the SystemPrincipal, we allow the load.
+ * Content scripts from addon code do provide aTriggeringPrincipal, which
+ * is an ExpandedPrincipal. If encountered, we allow the load.
+ * 2) If aLoadingPrincipal does not yield to a requestingLocation, then we
+ * fall back to querying the requestingLocation from aTriggeringPrincipal.
+ * 3) If we still end up not having a requestingLocation, we reject the load.
+ */
+
+ // 1) Check if the load was triggered by the system (SystemPrincipal) or
+ // a content script from addons code (ExpandedPrincipal) in which case the
+ // load is not subject to mixed content blocking.
+ if (triggeringPrincipal) {
+ if (triggeringPrincipal->IsSystemPrincipal()) {
+ *aDecision = ACCEPT;
+ return NS_OK;
+ }
+ nsCOMPtr<nsIExpandedPrincipal> expanded =
+ do_QueryInterface(triggeringPrincipal);
+ if (expanded) {
+ *aDecision = ACCEPT;
+ return NS_OK;
+ }
+ }
+
+ // 2) If aLoadingPrincipal does not provide a requestingLocation, then
+ // we fall back to to querying the requestingLocation from
+ // aTriggeringPrincipal.
+ nsCOMPtr<nsIURI> requestingLocation =
+ GetPrincipalURIOrPrecursorPrincipalURI(loadingPrincipal);
+ if (!requestingLocation) {
+ requestingLocation =
+ GetPrincipalURIOrPrecursorPrincipalURI(triggeringPrincipal);
+ }
+
+ // 3) Giving up. We still don't have a requesting location, therefore we can't
+ // tell if this is a mixed content load. Deny to be safe.
+ if (!requestingLocation) {
+ *aDecision = REJECT_REQUEST;
+ MOZ_LOG(sMCBLog, LogLevel::Verbose,
+ (" -> decision: Request will be rejected because no requesting "
+ "location could be "
+ "gathered."));
+ return NS_OK;
+ }
+
+ // Check the parent scheme. If it is not an HTTPS page then mixed content
+ // restrictions do not apply.
+ nsCOMPtr<nsIURI> innerRequestingLocation =
+ NS_GetInnermostURI(requestingLocation);
+ if (!innerRequestingLocation) {
+ NS_ERROR("Can't get innerURI from requestingLocation");
+ *aDecision = REJECT_REQUEST;
+ MOZ_LOG(sMCBLog, LogLevel::Verbose,
+ (" -> decision: Request will be rejected because the innermost "
+ "URI of the "
+ "requesting location could be gathered."));
+ return NS_OK;
+ }
+
+ bool parentIsHttps = innerRequestingLocation->SchemeIs("https");
+ if (!parentIsHttps) {
+ *aDecision = ACCEPT;
+ MOZ_LOG(sMCBLog, LogLevel::Verbose,
+ (" -> decision: Request will be allowed because the requesting "
+ "location is not using "
+ "HTTPS."));
+ return NS_OK;
+ }
+
+ // Disallow mixed content loads for workers, shared workers and service
+ // workers.
+ if (isWorkerType) {
+ // For workers, we can assume that we're mixed content at this point, since
+ // the parent is https, and the protocol associated with
+ // innerContentLocation doesn't map to the secure URI flags checked above.
+ // Assert this for sanity's sake
+#ifdef DEBUG
+ bool isHttpsScheme = innerContentLocation->SchemeIs("https");
+ MOZ_ASSERT(!isHttpsScheme);
+#endif
+ *aDecision = REJECT_REQUEST;
+ MOZ_LOG(sMCBLog, LogLevel::Verbose,
+ (" -> decision: Request will be rejected, trying to load a worker "
+ "from an insecure origin."));
+ return NS_OK;
+ }
+
+ bool isHttpScheme = innerContentLocation->SchemeIs("http");
+ if (isHttpScheme && IsPotentiallyTrustworthyOrigin(innerContentLocation)) {
+ *aDecision = ACCEPT;
+ return NS_OK;
+ }
+
+ // Check if https-only mode upgrades this later anyway
+ if (nsHTTPSOnlyUtils::IsSafeToAcceptCORSOrMixedContent(aLoadInfo)) {
+ *aDecision = ACCEPT;
+ return NS_OK;
+ }
+
+ // The page might have set the CSP directive 'upgrade-insecure-requests'. In
+ // such a case allow the http: load to succeed with the promise that the
+ // channel will get upgraded to https before fetching any data from the
+ // netwerk. Please see: nsHttpChannel::Connect()
+ //
+ // Please note that the CSP directive 'upgrade-insecure-requests' only applies
+ // to http: and ws: (for websockets). Websockets are not subject to mixed
+ // content blocking since insecure websockets are not allowed within secure
+ // pages. Hence, we only have to check against http: here. Skip mixed content
+ // blocking if the subresource load uses http: and the CSP directive
+ // 'upgrade-insecure-requests' is present on the page.
+
+ // Carve-out: if we're in the parent and we're loading media, e.g. through
+ // webbrowserpersist, don't reject it if we can't find a docshell.
+ if (XRE_IsParentProcess() && !requestingWindow &&
+ (contentType == ExtContentPolicy::TYPE_IMAGE ||
+ contentType == ExtContentPolicy::TYPE_MEDIA)) {
+ *aDecision = ACCEPT;
+ return NS_OK;
+ }
+ // Otherwise, we must have a window
+ NS_ENSURE_TRUE(requestingWindow, NS_OK);
+
+ if (isHttpScheme && aLoadInfo->GetUpgradeInsecureRequests()) {
+ *aDecision = ACCEPT;
+ return NS_OK;
+ }
+
+ // Allow http: mixed content if we are choosing to upgrade them when the
+ // pref "security.mixed_content.upgrade_display_content" is true.
+ // This behaves like GetUpgradeInsecureRequests above in that the channel will
+ // be upgraded to https before fetching any data from the netwerk.
+ if (isHttpScheme) {
+ bool isUpgradableContentType =
+ IsUpgradableContentType(internalContentType, /* aConsiderPrefs */ true);
+ if (isUpgradableContentType) {
+ *aDecision = ACCEPT;
+ return NS_OK;
+ }
+ }
+
+ // The page might have set the CSP directive 'block-all-mixed-content' which
+ // should block not only active mixed content loads but in fact all mixed
+ // content loads, see https://www.w3.org/TR/mixed-content/#strict-checking
+ // Block all non secure loads in case the CSP directive is present. Please
+ // note that at this point we already know, based on |schemeSecure| that the
+ // load is not secure, so we can bail out early at this point.
+ if (aLoadInfo->GetBlockAllMixedContent()) {
+ // log a message to the console before returning.
+ nsAutoCString spec;
+ nsresult rv = aContentLocation->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AutoTArray<nsString, 1> params;
+ CopyUTF8toUTF16(spec, *params.AppendElement());
+
+ CSP_LogLocalizedStr("blockAllMixedContent", params,
+ u""_ns, // aSourceFile
+ u""_ns, // aScriptSample
+ 0, // aLineNumber
+ 1, // aColumnNumber
+ nsIScriptError::errorFlag, "blockAllMixedContent"_ns,
+ requestingWindow->Id(),
+ !!aLoadInfo->GetOriginAttributes().mPrivateBrowsingId);
+ *aDecision = REJECT_REQUEST;
+ MOZ_LOG(
+ sMCBLog, LogLevel::Verbose,
+ (" -> decision: Request will be rejected because the CSP directive "
+ "'block-all-mixed-content' was set while trying to load data from "
+ "a non-secure origin."));
+ return NS_OK;
+ }
+
+ // Determine if the rootDoc is https and if the user decided to allow Mixed
+ // Content
+ WindowContext* topWC = requestingWindow->TopWindowContext();
+ bool rootHasSecureConnection = topWC->GetIsSecure();
+ bool allowMixedContent = topWC->GetAllowMixedContent();
+
+ // When navigating an iframe, the iframe may be https but its parents may not
+ // be. Check the parents to see if any of them are https. If none of the
+ // parents are https, allow the load.
+ if (contentType == ExtContentPolicyType::TYPE_SUBDOCUMENT &&
+ !rootHasSecureConnection && !parentIsHttps) {
+ bool httpsParentExists = false;
+
+ RefPtr<WindowContext> curWindow = requestingWindow;
+ while (!httpsParentExists && curWindow) {
+ httpsParentExists = curWindow->GetIsSecure();
+ curWindow = curWindow->GetParentWindowContext();
+ }
+
+ if (!httpsParentExists) {
+ *aDecision = nsIContentPolicy::ACCEPT;
+ return NS_OK;
+ }
+ }
+
+ OriginAttributes originAttributes;
+ if (loadingPrincipal) {
+ originAttributes = loadingPrincipal->OriginAttributesRef();
+ } else if (triggeringPrincipal) {
+ originAttributes = triggeringPrincipal->OriginAttributesRef();
+ }
+
+ // At this point we know that the request is mixed content, and the only
+ // question is whether we block it. Record telemetry at this point as to
+ // whether HSTS would have fixed things by making the content location
+ // into an HTTPS URL.
+ //
+ // Note that we count this for redirects as well as primary requests. This
+ // will cause some degree of double-counting, especially when mixed content
+ // is not blocked (e.g., for images). For more detail, see:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1198572#c19
+ //
+ // We do not count requests aHadInsecureImageRedirect=true, since these are
+ // just an artifact of the image caching system.
+ bool active = (classification == eMixedScript);
+ if (!aHadInsecureImageRedirect) {
+ if (XRE_IsParentProcess()) {
+ AccumulateMixedContentHSTS(innerContentLocation, active,
+ originAttributes);
+ } else {
+ // Ask the parent process to do the same call
+ mozilla::dom::ContentChild* cc =
+ mozilla::dom::ContentChild::GetSingleton();
+ if (cc) {
+ cc->SendAccumulateMixedContentHSTS(innerContentLocation, active,
+ originAttributes);
+ }
+ }
+ }
+
+ // set hasMixedContentObjectSubrequest on this object if necessary
+ if (contentType == ExtContentPolicyType::TYPE_OBJECT_SUBREQUEST &&
+ aReportError) {
+ if (!StaticPrefs::security_mixed_content_block_object_subrequest()) {
+ nsAutoCString messageLookUpKey(
+ "LoadingMixedDisplayObjectSubrequestDeprecation");
+
+ LogMixedContentMessage(classification, aContentLocation, topWC->Id(),
+ eUserOverride, requestingLocation,
+ messageLookUpKey);
+ }
+ }
+
+ uint32_t newState = 0;
+ // If the content is display content, and the pref says display content should
+ // be blocked, block it.
+ if (classification == eMixedDisplay) {
+ if (!StaticPrefs::security_mixed_content_block_display_content() ||
+ allowMixedContent) {
+ *aDecision = nsIContentPolicy::ACCEPT;
+ // User has overriden the pref and the root is not https;
+ // mixed display content was allowed on an https subframe.
+ newState |= nsIWebProgressListener::STATE_LOADED_MIXED_DISPLAY_CONTENT;
+ } else {
+ *aDecision = nsIContentPolicy::REJECT_REQUEST;
+ MOZ_LOG(sMCBLog, LogLevel::Verbose,
+ (" -> decision: Request will be rejected because the content is "
+ "display "
+ "content (blocked by pref "
+ "security.mixed_content.block_display_content)."));
+ newState |= nsIWebProgressListener::STATE_BLOCKED_MIXED_DISPLAY_CONTENT;
+ }
+ } else {
+ MOZ_ASSERT(classification == eMixedScript);
+ // If the content is active content, and the pref says active content should
+ // be blocked, block it unless the user has choosen to override the pref
+ if (!StaticPrefs::security_mixed_content_block_active_content() ||
+ allowMixedContent) {
+ *aDecision = nsIContentPolicy::ACCEPT;
+ // User has already overriden the pref and the root is not https;
+ // mixed active content was allowed on an https subframe.
+ newState |= nsIWebProgressListener::STATE_LOADED_MIXED_ACTIVE_CONTENT;
+ } else {
+ // User has not overriden the pref by Disabling protection. Reject the
+ // request and update the security state.
+ *aDecision = nsIContentPolicy::REJECT_REQUEST;
+ MOZ_LOG(sMCBLog, LogLevel::Verbose,
+ (" -> decision: Request will be rejected because the content is "
+ "active "
+ "content (blocked by pref "
+ "security.mixed_content.block_active_content)."));
+ // The user has not overriden the pref, so make sure they still have an
+ // option by calling nativeDocShell which will invoke the doorhanger
+ newState |= nsIWebProgressListener::STATE_BLOCKED_MIXED_ACTIVE_CONTENT;
+ }
+ }
+
+ // To avoid duplicate errors on the console, we do not report blocked
+ // preloads to the console.
+ if (!isPreload && aReportError) {
+ LogMixedContentMessage(classification, aContentLocation, topWC->Id(),
+ (*aDecision == nsIContentPolicy::REJECT_REQUEST)
+ ? eBlocked
+ : eUserOverride,
+ requestingLocation);
+ }
+
+ // Notify the top WindowContext of the flags we've computed, and it
+ // will handle updating any relevant security UI.
+ topWC->AddSecurityState(newState);
+ return NS_OK;
+}
+
+bool nsMixedContentBlocker::URISafeToBeLoadedInSecureContext(nsIURI* aURI) {
+ /* Returns a bool if the URI can be loaded as a sub resource safely.
+ *
+ * Check Protocol Flags to determine if scheme is safe to load:
+ * URI_DOES_NOT_RETURN_DATA - e.g.
+ * "mailto"
+ * URI_IS_LOCAL_RESOURCE - e.g.
+ * "data",
+ * "resource",
+ * "moz-icon"
+ * URI_INHERITS_SECURITY_CONTEXT - e.g.
+ * "javascript"
+ * URI_IS_POTENTIALLY_TRUSTWORTHY - e.g.
+ * "https",
+ * "moz-safe-about"
+ *
+ */
+ bool schemeLocal = false;
+ bool schemeNoReturnData = false;
+ bool schemeInherits = false;
+ bool schemeSecure = false;
+ if (NS_FAILED(NS_URIChainHasFlags(
+ aURI, nsIProtocolHandler::URI_IS_LOCAL_RESOURCE, &schemeLocal)) ||
+ NS_FAILED(NS_URIChainHasFlags(
+ aURI, nsIProtocolHandler::URI_DOES_NOT_RETURN_DATA,
+ &schemeNoReturnData)) ||
+ NS_FAILED(NS_URIChainHasFlags(
+ aURI, nsIProtocolHandler::URI_INHERITS_SECURITY_CONTEXT,
+ &schemeInherits)) ||
+ NS_FAILED(NS_URIChainHasFlags(
+ aURI, nsIProtocolHandler::URI_IS_POTENTIALLY_TRUSTWORTHY,
+ &schemeSecure))) {
+ return false;
+ }
+
+ MOZ_LOG(sMCBLog, LogLevel::Verbose,
+ (" - URISafeToBeLoadedInSecureContext:"));
+ MOZ_LOG(sMCBLog, LogLevel::Verbose, (" - schemeLocal: %i", schemeLocal));
+ MOZ_LOG(sMCBLog, LogLevel::Verbose,
+ (" - schemeNoReturnData: %i", schemeNoReturnData));
+ MOZ_LOG(sMCBLog, LogLevel::Verbose,
+ (" - schemeInherits: %i", schemeInherits));
+ MOZ_LOG(sMCBLog, LogLevel::Verbose, (" - schemeSecure: %i", schemeSecure));
+ return (schemeLocal || schemeNoReturnData || schemeInherits || schemeSecure);
+}
+
+NS_IMETHODIMP
+nsMixedContentBlocker::ShouldProcess(nsIURI* aContentLocation,
+ nsILoadInfo* aLoadInfo,
+ int16_t* aDecision) {
+ if (!aContentLocation) {
+ // aContentLocation may be null when a plugin is loading without an
+ // associated URI resource
+ if (aLoadInfo->GetExternalContentPolicyType() ==
+ ExtContentPolicyType::TYPE_OBJECT) {
+ *aDecision = ACCEPT;
+ return NS_OK;
+ }
+
+ NS_SetRequestBlockingReason(aLoadInfo,
+ nsILoadInfo::BLOCKING_REASON_MIXED_BLOCKED);
+ *aDecision = REJECT_REQUEST;
+ return NS_ERROR_FAILURE;
+ }
+
+ return ShouldLoad(aContentLocation, aLoadInfo, aDecision);
+}
+
+// Record information on when HSTS would have made mixed content not mixed
+// content (regardless of whether it was actually blocked)
+void nsMixedContentBlocker::AccumulateMixedContentHSTS(
+ nsIURI* aURI, bool aActive, const OriginAttributes& aOriginAttributes) {
+ // This method must only be called in the parent, because
+ // nsSiteSecurityService is only available in the parent
+ if (!XRE_IsParentProcess()) {
+ MOZ_ASSERT(false);
+ return;
+ }
+
+ bool hsts;
+ nsresult rv;
+ nsCOMPtr<nsISiteSecurityService> sss =
+ do_GetService(NS_SSSERVICE_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ rv = sss->IsSecureURI(aURI, aOriginAttributes, &hsts);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ // states: would upgrade, would prime, hsts info cached
+ // active, passive
+ //
+ if (!aActive) {
+ if (!hsts) {
+ Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS,
+ MCB_HSTS_PASSIVE_NO_HSTS);
+ } else {
+ Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS,
+ MCB_HSTS_PASSIVE_WITH_HSTS);
+ }
+ } else {
+ if (!hsts) {
+ Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS,
+ MCB_HSTS_ACTIVE_NO_HSTS);
+ } else {
+ Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS,
+ MCB_HSTS_ACTIVE_WITH_HSTS);
+ }
+ }
+}
diff --git a/dom/security/nsMixedContentBlocker.h b/dom/security/nsMixedContentBlocker.h
new file mode 100644
index 0000000000..05038ef087
--- /dev/null
+++ b/dom/security/nsMixedContentBlocker.h
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsMixedContentBlocker_h___
+#define nsMixedContentBlocker_h___
+
+#define NS_MIXEDCONTENTBLOCKER_CONTRACTID "@mozilla.org/mixedcontentblocker;1"
+/* daf1461b-bf29-4f88-8d0e-4bcdf332c862 */
+#define NS_MIXEDCONTENTBLOCKER_CID \
+ { \
+ 0xdaf1461b, 0xbf29, 0x4f88, { \
+ 0x8d, 0x0e, 0x4b, 0xcd, 0xf3, 0x32, 0xc8, 0x62 \
+ } \
+ }
+
+// This enum defines type of content that is detected when an
+// nsMixedContentEvent fires
+enum MixedContentTypes {
+ // "Active" content, such as fonts, plugin content, JavaScript, stylesheets,
+ // iframes, WebSockets, and XHR
+ eMixedScript,
+ // "Display" content, such as images, audio, video, and <a ping>
+ eMixedDisplay
+};
+
+#include "nsIContentPolicy.h"
+#include "nsIChannel.h"
+#include "nsIChannelEventSink.h"
+#include "imgRequest.h"
+
+using mozilla::OriginAttributes;
+
+class nsILoadInfo; // forward declaration
+namespace mozilla::net {
+class nsProtocolProxyService; // forward declaration
+} // namespace mozilla::net
+
+class nsMixedContentBlocker : public nsIContentPolicy,
+ public nsIChannelEventSink {
+ private:
+ virtual ~nsMixedContentBlocker();
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICONTENTPOLICY
+ NS_DECL_NSICHANNELEVENTSINK
+
+ nsMixedContentBlocker() = default;
+
+ // See:
+ // https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy
+ static bool IsPotentiallyTrustworthyLoopbackHost(
+ const nsACString& aAsciiHost);
+ static bool IsPotentiallyTrustworthyLoopbackURL(nsIURI* aURL);
+ static bool IsPotentiallyTrustworthyOnion(nsIURI* aURL);
+ static bool IsPotentiallyTrustworthyOrigin(nsIURI* aURI);
+
+ /**
+ * Returns true if the provided content policy type is subject to the
+ * mixed content level 2 upgrading mechanism (audio, video, image).
+ *
+ * @param aConsiderPrefs A boolean that indicates whether the result of this
+ * functions takes the `security.mixed_content.upgrade_display_content`
+ * preferences into account.
+ */
+ static bool IsUpgradableContentType(nsContentPolicyType aType,
+ bool aConsiderPrefs);
+
+ /* Static version of ShouldLoad() that contains all the Mixed Content Blocker
+ * logic. Called from non-static ShouldLoad().
+ * Called directly from imageLib when an insecure redirect exists in a cached
+ * image load.
+ * @param aHadInsecureImageRedirect
+ * boolean flag indicating that an insecure redirect through http
+ * occured when this image was initially loaded and cached.
+ * @param aReportError
+ * boolean flag indicating if a rejection should automaticly be
+ * logged into the Console.
+ * Remaining parameters are from nsIContentPolicy::ShouldLoad().
+ */
+ static nsresult ShouldLoad(bool aHadInsecureImageRedirect,
+ nsIURI* aContentLocation, nsILoadInfo* aLoadInfo,
+ bool aReportError, int16_t* aDecision);
+ static void AccumulateMixedContentHSTS(
+ nsIURI* aURI, bool aActive, const OriginAttributes& aOriginAttributes);
+
+ static bool URISafeToBeLoadedInSecureContext(nsIURI* aURI);
+
+ static void OnPrefChange(const char* aPref, void* aClosure);
+ static void GetSecureContextAllowList(nsACString& aList);
+ static void Shutdown();
+
+ static bool sSecurecontextAllowlistCached;
+ static nsCString* sSecurecontextAllowlist;
+};
+
+#endif /* nsMixedContentBlocker_h___ */
diff --git a/dom/security/sanitizer/Sanitizer.cpp b/dom/security/sanitizer/Sanitizer.cpp
new file mode 100644
index 0000000000..9d087523ff
--- /dev/null
+++ b/dom/security/sanitizer/Sanitizer.cpp
@@ -0,0 +1,187 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "BindingDeclarations.h"
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/DocumentFragment.h"
+#include "mozilla/dom/SanitizerBinding.h"
+#include "nsContentUtils.h"
+#include "nsGenericHTMLElement.h"
+#include "nsTreeSanitizer.h"
+#include "Sanitizer.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Sanitizer, mGlobal)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(Sanitizer)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(Sanitizer)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Sanitizer)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+JSObject* Sanitizer::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return Sanitizer_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+/* static */
+already_AddRefed<Sanitizer> Sanitizer::New(nsIGlobalObject* aGlobal,
+ const SanitizerConfig& aOptions,
+ ErrorResult& aRv) {
+ nsTreeSanitizer treeSanitizer(nsIParserUtils::SanitizerAllowStyle);
+ treeSanitizer.WithWebSanitizerOptions(aGlobal, aOptions, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ RefPtr<Sanitizer> sanitizer =
+ new Sanitizer(aGlobal, std::move(treeSanitizer));
+ return sanitizer.forget();
+}
+
+/* static */
+already_AddRefed<Sanitizer> Sanitizer::Constructor(
+ const GlobalObject& aGlobal, const SanitizerConfig& aOptions,
+ ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ return New(global, aOptions, aRv);
+}
+
+/* static */
+already_AddRefed<DocumentFragment> Sanitizer::InputToNewFragment(
+ const mozilla::dom::DocumentFragmentOrDocument& aInput, ErrorResult& aRv) {
+ // turns an DocumentFragmentOrDocument into a new DocumentFragment for
+ // internal use with nsTreeSanitizer
+
+ nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(mGlobal);
+ if (!window || !window->GetDoc()) {
+ // FIXME: Should we throw another exception?
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ // We need to create a new docfragment based on the input
+ // and can't use a live document (possibly with mutation observershandlers)
+ nsAutoString innerHTML;
+ if (aInput.IsDocumentFragment()) {
+ RefPtr<DocumentFragment> inFragment = &aInput.GetAsDocumentFragment();
+ inFragment->GetInnerHTML(innerHTML);
+ } else if (aInput.IsDocument()) {
+ RefPtr<Document> doc = &aInput.GetAsDocument();
+ nsCOMPtr<Element> docElement = doc->GetDocumentElement();
+ if (docElement) {
+ docElement->GetInnerHTML(innerHTML, IgnoreErrors());
+ }
+ }
+ if (innerHTML.IsEmpty()) {
+ AutoTArray<nsString, 1> params = {};
+ LogLocalizedString("SanitizerRcvdNoInput", params,
+ nsIScriptError::warningFlag);
+
+ RefPtr<DocumentFragment> emptyFragment =
+ window->GetDoc()->CreateDocumentFragment();
+ return emptyFragment.forget();
+ }
+ // Create an inert HTML document, loaded as data.
+ // this ensures we do not cause any requests.
+ RefPtr<Document> emptyDoc =
+ nsContentUtils::CreateInertHTMLDocument(window->GetDoc());
+ if (!emptyDoc) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+ // We don't have a context element yet. let's create a mock HTML body element
+ RefPtr<mozilla::dom::NodeInfo> info =
+ emptyDoc->NodeInfoManager()->GetNodeInfo(
+ nsGkAtoms::body, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE);
+
+ nsCOMPtr<nsINode> context = NS_NewHTMLBodyElement(
+ info.forget(), mozilla::dom::FromParser::FROM_PARSER_FRAGMENT);
+ RefPtr<DocumentFragment> fragment = nsContentUtils::CreateContextualFragment(
+ context, innerHTML, true /* aPreventScriptExecution */, aRv);
+ if (aRv.Failed()) {
+ aRv.ThrowInvalidStateError("Could not parse input");
+ return nullptr;
+ }
+ return fragment.forget();
+}
+
+already_AddRefed<DocumentFragment> Sanitizer::Sanitize(
+ const mozilla::dom::DocumentFragmentOrDocument& aInput, ErrorResult& aRv) {
+ nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(mGlobal);
+ if (!window || !window->GetDoc()) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+ RefPtr<DocumentFragment> fragment =
+ Sanitizer::InputToNewFragment(aInput, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ mTreeSanitizer.Sanitize(fragment);
+ return fragment.forget();
+}
+
+RefPtr<DocumentFragment> Sanitizer::SanitizeFragment(
+ RefPtr<DocumentFragment> aFragment, ErrorResult& aRv) {
+ nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(mGlobal);
+ if (!window || !window->GetDoc()) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+ // FIXME(freddyb)
+ // (how) can we assert that the supplied doc is indeed inert?
+ mTreeSanitizer.Sanitize(aFragment);
+ return aFragment.forget();
+}
+
+/* ------ Logging ------ */
+
+void Sanitizer::LogLocalizedString(const char* aName,
+ const nsTArray<nsString>& aParams,
+ uint32_t aFlags) {
+ uint64_t innerWindowID = 0;
+ bool isPrivateBrowsing = true;
+ nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(mGlobal);
+ if (window && window->GetDoc()) {
+ auto* doc = window->GetDoc();
+ innerWindowID = doc->InnerWindowID();
+ isPrivateBrowsing = nsContentUtils::IsInPrivateBrowsing(doc);
+ }
+ nsAutoString logMsg;
+ nsContentUtils::FormatLocalizedString(nsContentUtils::eSECURITY_PROPERTIES,
+ aName, aParams, logMsg);
+ LogMessage(logMsg, aFlags, innerWindowID, isPrivateBrowsing);
+}
+
+/* static */
+void Sanitizer::LogMessage(const nsAString& aMessage, uint32_t aFlags,
+ uint64_t aInnerWindowID, bool aFromPrivateWindow) {
+ // Prepending 'Sanitizer' to the outgoing console message
+ nsString message;
+ message.AppendLiteral(u"Sanitizer: ");
+ message.Append(aMessage);
+
+ // Allow for easy distinction in devtools code.
+ constexpr auto category = "Sanitizer"_ns;
+
+ if (aInnerWindowID > 0) {
+ // Send to content console
+ nsContentUtils::ReportToConsoleByWindowID(message, aFlags, category,
+ aInnerWindowID);
+ } else {
+ // Send to browser console
+ nsContentUtils::LogSimpleConsoleError(message, category, aFromPrivateWindow,
+ true /* from chrome context */,
+ aFlags);
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/security/sanitizer/Sanitizer.h b/dom/security/sanitizer/Sanitizer.h
new file mode 100644
index 0000000000..121545b1bf
--- /dev/null
+++ b/dom/security/sanitizer/Sanitizer.h
@@ -0,0 +1,107 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_Sanitizer_h
+#define mozilla_dom_Sanitizer_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/DocumentFragment.h"
+#include "mozilla/dom/SanitizerBinding.h"
+#include "nsString.h"
+#include "nsIGlobalObject.h"
+#include "nsIParserUtils.h"
+#include "nsTreeSanitizer.h"
+
+// XXX(Bug 1673929) This is not really needed here, but the generated
+// SanitizerBinding.cpp needs it and does not include it.
+#include "mozilla/dom/Document.h"
+
+class nsISupports;
+
+namespace mozilla {
+
+class ErrorResult;
+
+namespace dom {
+
+class GlobalObject;
+
+class Sanitizer final : public nsISupports, public nsWrapperCache {
+ explicit Sanitizer(nsIGlobalObject* aGlobal, nsTreeSanitizer&& aTreeSanitizer)
+ : mGlobal(aGlobal), mTreeSanitizer(std::move(aTreeSanitizer)) {
+ MOZ_ASSERT(aGlobal);
+ }
+
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Sanitizer);
+
+ nsIGlobalObject* GetParentObject() const { return mGlobal; }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ static already_AddRefed<Sanitizer> New(nsIGlobalObject* aGlobal,
+ const SanitizerConfig& aOptions,
+ ErrorResult& aRv);
+
+ /**
+ * Sanitizer() WebIDL constructor
+ * @return a new Sanitizer object, with methods as below
+ */
+ static already_AddRefed<Sanitizer> Constructor(
+ const GlobalObject& aGlobal, const SanitizerConfig& aOptions,
+ ErrorResult& aRv);
+
+ /**
+ * sanitize WebIDL method.
+ * @param aInput "bad" HTML that needs to be sanitized
+ * @return DocumentFragment of the sanitized HTML
+ */
+ already_AddRefed<DocumentFragment> Sanitize(
+ const mozilla::dom::DocumentFragmentOrDocument& aInput, ErrorResult& aRv);
+
+ /**
+ * Sanitizes a fragment in place. This assumes that the fragment
+ * belongs but an inert document.
+ *
+ * @param aFragment Fragment to be sanitized in place
+ * @return DocumentFragment
+ */
+
+ RefPtr<DocumentFragment> SanitizeFragment(RefPtr<DocumentFragment> aFragment,
+ ErrorResult& aRv);
+
+ /**
+ * Logs localized message to either content console or browser console
+ * @param aName Localization key
+ * @param aParams Localization parameters
+ * @param aFlags Logging Flag (see nsIScriptError)
+ */
+ void LogLocalizedString(const char* aName, const nsTArray<nsString>& aParams,
+ uint32_t aFlags);
+
+ private:
+ ~Sanitizer() = default;
+ already_AddRefed<DocumentFragment> InputToNewFragment(
+ const mozilla::dom::DocumentFragmentOrDocument& aInput, ErrorResult& aRv);
+ /**
+ * Logs localized message to either content console or browser console
+ * @param aMessage Message to log
+ * @param aFlags Logging Flag (see nsIScriptError)
+ * @param aInnerWindowID Inner Window ID (Logged on browser console if 0)
+ * @param aFromPrivateWindow If from private window
+ */
+ static void LogMessage(const nsAString& aMessage, uint32_t aFlags,
+ uint64_t aInnerWindowID, bool aFromPrivateWindow);
+
+ RefPtr<nsIGlobalObject> mGlobal;
+ nsTreeSanitizer mTreeSanitizer;
+};
+} // namespace dom
+} // namespace mozilla
+
+#endif // ifndef mozilla_dom_Sanitizer_h
diff --git a/dom/security/sanitizer/moz.build b/dom/security/sanitizer/moz.build
new file mode 100644
index 0000000000..6d2f0e0c19
--- /dev/null
+++ b/dom/security/sanitizer/moz.build
@@ -0,0 +1,37 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: Security")
+
+# TEST_DIRS += [ 'tests' ]
+
+MOCHITEST_MANIFESTS += ["tests/mochitest/mochitest.toml"]
+
+
+EXPORTS.mozilla.dom += [
+ "Sanitizer.h",
+]
+
+UNIFIED_SOURCES += [
+ "Sanitizer.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/dom/base",
+ "/dom/bindings",
+ "/dom/html",
+]
+
+# include('/ipc/chromium/chromium-config.mozbuild')
+# include('/tools/fuzzing/libfuzzer-config.mozbuild')
+
+FINAL_LIBRARY = "xul"
+
+# if CONFIG['FUZZING_INTERFACES']:
+# TEST_DIRS += [
+# 'fuzztest'
+# ]
diff --git a/dom/security/sanitizer/tests/mochitest/mochitest.toml b/dom/security/sanitizer/tests/mochitest/mochitest.toml
new file mode 100644
index 0000000000..5cf40f44c3
--- /dev/null
+++ b/dom/security/sanitizer/tests/mochitest/mochitest.toml
@@ -0,0 +1,8 @@
+[DEFAULT]
+prefs = [
+ "dom.security.sanitizer.enabled=true",
+ "dom.security.setHTML.enabled=true",
+]
+scheme = "https"
+
+["test_sanitizer_api.html"]
diff --git a/dom/security/sanitizer/tests/mochitest/test_sanitizer_api.html b/dom/security/sanitizer/tests/mochitest/test_sanitizer_api.html
new file mode 100644
index 0000000000..bfa1fdf6c8
--- /dev/null
+++ b/dom/security/sanitizer/tests/mochitest/test_sanitizer_api.html
@@ -0,0 +1,138 @@
+<!DOCTYPE HTML>
+<title>Test sanitizer api</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+<script type="text/javascript">
+"use strict";
+/* global Sanitizer */
+// we're not done after "onload"
+SimpleTest.waitForExplicitFinish();
+(async function() {
+ // Ensure Sanitizer is not exposed when the pref is false
+ const isEnabled = SpecialPowers.getBoolPref("dom.security.sanitizer.enabled");
+ if (!isEnabled) {
+ ok(false, "This test should only be run with dom.security.sanitizer.enabled set to true");
+ SimpleTest.finish();
+ }
+
+ function* possibleInputTypes(inputStr) {
+ /* This generator function, given a string, yields all possible input objects
+ for our sanitizer API (string, docfragment, document).
+ */
+
+ // 1) as string
+ yield ({testInput: inputStr, testType: "String" });
+ // 2) as DocumentFragment
+ let temp = document.createElement('template');
+ // asking eslint to skip this: innerHTML is safe for template elements.
+ temp.innerHTML = inputStr;
+ yield ({testInput: temp.content, testType: "DocumentFragment" });
+ // 3) as HTMLDocument
+ const parser = new DOMParser;
+ yield ({testInput: parser.parseFromString(inputStr, "text/html"), testType: "Document" });
+ }
+ // basic interface smoke test
+ ok(typeof Sanitizer === "function", "Sanitizer constructor exposed when preffed on");
+ const mySanitizer = new Sanitizer();
+ ok(mySanitizer, "Sanitizer constructor works");
+ ok(mySanitizer.sanitize, "sanitize function exists");
+ ok("setHTML" in Element.prototype, "Element.setHTML exists");
+
+ // testing sanitizer results
+ const testCases = [
+ {
+ testString: "<p>hello</p>",
+ testExpected: "<p>hello</p>",
+ sanitizerOptions: {}
+ },
+ {
+ // script element encoded to not confuse the HTML parser and end execution here
+ testString: "<p>second test</p><script>alert(1)\x3C/script>",
+ testExpected: "<p>second test</p>",
+ sanitizerOptions: {},
+ },
+ {
+ // test for the elements option
+ testString: "<p>hello <i>folks</i></p>",
+ testExpected: "<p>hello folks</p>",
+ sanitizerOptions: { elements: ["p"] },
+ },
+ {
+ // test for the replaceWithChildrenElements option
+ testString: "<p>hello <i>folks</i></p>",
+ testExpected: "<p>hello folks</p>",
+ sanitizerOptions: { replaceWithChildrenElements: ["i"] },
+ },
+ // TODO: Unknown attributes aren't supported yet.
+ // {
+ // // test for the allowAttributes option
+ // testString: `<p haha="lol">hello</p>`,
+ // testExpected: `<p haha="lol">hello</p>`,
+ // sanitizerOptions: { unknownMarkup: true, attributes: ["haha"] },
+ // },
+ {
+ // confirming the inverse
+ testString: `<p haha="lol">hello</p>`,
+ testExpected: `<p>hello</p>`,
+ sanitizerOptions: {},
+ },
+ {
+ // test for the removeAttributes option
+ testString: `<p title="dropme">hello</p>`,
+ testExpected: `<p>hello</p>`,
+ sanitizerOptions: { removeAttributes: ['title'] },
+ },
+ {
+ // confirming the inverse
+ testString: `<p title="dontdropme">hello</p>`,
+ testExpected: `<p title="dontdropme">hello</p>`,
+ sanitizerOptions: {},
+ },
+ {
+ // if an attribute is allowed and removed, the remove will take preference
+ testString: `<p title="lol">hello</p>`,
+ testExpected: `<p>hello</p>`,
+ sanitizerOptions: {
+ attributes: ["title"],
+ removeAttributes: ["title"],
+ },
+ },
+ ];
+
+
+ const div = document.createElement("div");
+ for (let test of testCases) {
+ const {testString, testExpected, sanitizerOptions} = test;
+ const testSanitizer = new Sanitizer(sanitizerOptions);
+
+ for (let testInputAndType of possibleInputTypes(testString)) {
+ const {testInput, testType} = testInputAndType;
+
+ if (testType != "String") {
+ // test sanitize(document/fragment)
+ try {
+ div.innerHTML = "";
+ const docFragment = testSanitizer.sanitize(testInput);
+ div.append(docFragment);
+ is(div.innerHTML, testExpected, `Sanitizer.sanitize() should turn (${testType}) '${testInput}' into '${testExpected}'`);
+ }
+ catch (e) {
+ ok(false, 'Error in sanitize() test: ' + e)
+ }
+ }
+ else {
+ // test setHTML:
+ try {
+ div.setHTML(testString, { sanitizer: sanitizerOptions });
+ is(div.innerHTML, testExpected, `div.setHTML() should turn(${testType}) '${testInput}' into '${testExpected}'`);
+ }
+ catch (e) {
+ ok(false, 'Error in setHTML() test: ' + e)
+ }
+ }
+ }
+ }
+
+ SimpleTest.finish();
+})();
+</script>
diff --git a/dom/security/test/cors/browser.toml b/dom/security/test/cors/browser.toml
new file mode 100644
index 0000000000..4e69201c66
--- /dev/null
+++ b/dom/security/test/cors/browser.toml
@@ -0,0 +1,10 @@
+[DEFAULT]
+support-files = [
+ "file_CrossSiteXHR_server.sjs",
+ "file_CrossSiteXHR_inner.html",
+ "file_cors_logging_test.html",
+ "file_bug1456721.html",
+ "bug1456721.sjs",
+]
+
+["browser_CORS-console-warnings.js"]
diff --git a/dom/security/test/cors/browser_CORS-console-warnings.js b/dom/security/test/cors/browser_CORS-console-warnings.js
new file mode 100644
index 0000000000..aa4a211146
--- /dev/null
+++ b/dom/security/test/cors/browser_CORS-console-warnings.js
@@ -0,0 +1,101 @@
+/*
+ * Description of the test:
+ * Ensure that CORS warnings are printed to the web console.
+ *
+ * This test uses the same tests as the plain mochitest, but needs access to
+ * the console.
+ */
+"use strict";
+
+function console_observer(subject, topic, data) {
+ var message = subject.wrappedJSObject.arguments[0];
+ ok(false, message);
+}
+
+var webconsole = null;
+var messages_seen = 0;
+var expected_messages = 50;
+
+function on_new_message(msgObj) {
+ let text = msgObj.message;
+
+ if (text.match("Cross-Origin Request Blocked:")) {
+ ok(true, "message is: " + text);
+ messages_seen++;
+ }
+}
+
+async function do_cleanup() {
+ Services.console.unregisterListener(on_new_message);
+ await unsetCookiePref();
+}
+
+/**
+ * Set e10s related preferences in the test environment.
+ * @return {Promise} promise that resolves when preferences are set.
+ */
+function setCookiePref() {
+ return new Promise(resolve =>
+ // accept all cookies so that the CORS requests will send the right cookies
+ SpecialPowers.pushPrefEnv(
+ {
+ set: [["network.cookie.cookieBehavior", 0]],
+ },
+ resolve
+ )
+ );
+}
+
+/**
+ * Unset e10s related preferences in the test environment.
+ * @return {Promise} promise that resolves when preferences are unset.
+ */
+function unsetCookiePref() {
+ return new Promise(resolve => {
+ SpecialPowers.popPrefEnv(resolve);
+ });
+}
+
+//jscs:disable
+add_task(async function () {
+ //jscs:enable
+ // A longer timeout is necessary for this test than the plain mochitests
+ // due to opening a new tab with the web console.
+ requestLongerTimeout(4);
+ registerCleanupFunction(do_cleanup);
+ await setCookiePref();
+ Services.console.registerListener(on_new_message);
+
+ let test_uri =
+ "http://mochi.test:8888/browser/dom/security/test/cors/file_cors_logging_test.html";
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ BrowserTestUtils.startLoadingURIString(gBrowser, test_uri);
+
+ await BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ test_uri + "#finished"
+ );
+
+ // Different OS combinations
+ Assert.greater(messages_seen, 0, "Saw " + messages_seen + " messages.");
+
+ messages_seen = 0;
+ let test_two_uri =
+ "http://mochi.test:8888/browser/dom/security/test/cors/file_bug1456721.html";
+ BrowserTestUtils.startLoadingURIString(gBrowser, test_two_uri);
+
+ await BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ test_two_uri + "#finishedTestTwo"
+ );
+ await BrowserTestUtils.waitForCondition(() => messages_seen > 0);
+
+ Assert.greater(messages_seen, 0, "Saw " + messages_seen + " messages.");
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/dom/security/test/cors/bug1456721.sjs b/dom/security/test/cors/bug1456721.sjs
new file mode 100644
index 0000000000..de8bd5a7f4
--- /dev/null
+++ b/dom/security/test/cors/bug1456721.sjs
@@ -0,0 +1,20 @@
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+ let queryStr = request.queryString;
+
+ if (queryStr === "redirect") {
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", "bug1456721.sjs?load", false);
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ return;
+ }
+
+ if (queryStr === "load") {
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.write("foo");
+ return;
+ }
+ // we should never get here - return something unexpected
+ response.write("d'oh");
+}
diff --git a/dom/security/test/cors/file_CrossSiteXHR_cache_server.sjs b/dom/security/test/cors/file_CrossSiteXHR_cache_server.sjs
new file mode 100644
index 0000000000..c8e3243101
--- /dev/null
+++ b/dom/security/test/cors/file_CrossSiteXHR_cache_server.sjs
@@ -0,0 +1,59 @@
+function handleRequest(request, response) {
+ var query = {};
+ request.queryString.split("&").forEach(function (val) {
+ var [name, value] = val.split("=");
+ query[name] = unescape(value);
+ });
+
+ if ("setState" in query) {
+ setState(
+ "test/dom/security/test_CrossSiteXHR_cache:secData",
+ query.setState
+ );
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/plain", false);
+ response.write("hi");
+
+ return;
+ }
+
+ var isPreflight = request.method == "OPTIONS";
+
+ // Send response
+
+ secData = JSON.parse(
+ getState("test/dom/security/test_CrossSiteXHR_cache:secData")
+ );
+
+ if (secData.allowOrigin) {
+ response.setHeader("Access-Control-Allow-Origin", secData.allowOrigin);
+ }
+
+ if (secData.withCred) {
+ response.setHeader("Access-Control-Allow-Credentials", "true");
+ }
+
+ if (isPreflight) {
+ if (secData.allowHeaders) {
+ response.setHeader("Access-Control-Allow-Headers", secData.allowHeaders);
+ }
+
+ if (secData.allowMethods) {
+ response.setHeader("Access-Control-Allow-Methods", secData.allowMethods);
+ }
+
+ if (secData.cacheTime) {
+ response.setHeader(
+ "Access-Control-Max-Age",
+ secData.cacheTime.toString()
+ );
+ }
+
+ return;
+ }
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "application/xml", false);
+ response.write("<res>hello pass</res>\n");
+}
diff --git a/dom/security/test/cors/file_CrossSiteXHR_inner.html b/dom/security/test/cors/file_CrossSiteXHR_inner.html
new file mode 100644
index 0000000000..d3e8421362
--- /dev/null
+++ b/dom/security/test/cors/file_CrossSiteXHR_inner.html
@@ -0,0 +1,121 @@
+<!DOCTYPE HTML>
+<!--
+ NOTE! The content of this file is duplicated in file_CrossSiteXHR_inner.jar
+ and file_CrossSiteXHR_inner_data.sjs
+ Please update those files if you update this one.
+-->
+
+<html>
+<head>
+<script>
+function trimString(stringValue) {
+ return stringValue.replace(/^\s+|\s+$/g, '');
+};
+
+window.addEventListener("message", function(e) {
+
+ sendData = null;
+
+ req = JSON.parse(e.data);
+ var res = {
+ didFail: false,
+ events: [],
+ progressEvents: 0,
+ status: 0,
+ responseText: "",
+ statusText: "",
+ responseXML: null,
+ sendThrew: false
+ };
+
+ var xhr = new XMLHttpRequest();
+ for (type of ["load", "abort", "error", "loadstart", "loadend"]) {
+ xhr.addEventListener(type, function(e) {
+ res.events.push(e.type);
+ });
+ }
+ xhr.addEventListener("readystatechange", function(e) {
+ res.events.push("rs" + xhr.readyState);
+ });
+ xhr.addEventListener("progress", function(e) {
+ res.progressEvents++;
+ });
+ if (req.uploadProgress) {
+ xhr.upload.addEventListener(req.uploadProgress, function(e) {
+ res.progressEvents++;
+ });
+ }
+ xhr.onerror = function(e) {
+ res.didFail = true;
+ };
+ xhr.onloadend = function (event) {
+ res.status = xhr.status;
+ try {
+ res.statusText = xhr.statusText;
+ } catch (e) {
+ delete(res.statusText);
+ }
+ res.responseXML = xhr.responseXML ?
+ (new XMLSerializer()).serializeToString(xhr.responseXML) :
+ null;
+ res.responseText = xhr.responseText;
+
+ res.responseHeaders = {};
+ for (responseHeader in req.responseHeaders) {
+ res.responseHeaders[responseHeader] =
+ xhr.getResponseHeader(responseHeader);
+ }
+ res.allResponseHeaders = {};
+ var splitHeaders = xhr.getAllResponseHeaders().split("\r\n");
+ for (var i = 0; i < splitHeaders.length; i++) {
+ var headerValuePair = splitHeaders[i].split(":");
+ if(headerValuePair[1] != null) {
+ var headerName = trimString(headerValuePair[0]);
+ var headerValue = trimString(headerValuePair[1]);
+ res.allResponseHeaders[headerName] = headerValue;
+ }
+ }
+ post(e, res);
+ }
+
+ if (req.withCred)
+ xhr.withCredentials = true;
+ if (req.body)
+ sendData = req.body;
+
+ res.events.push("opening");
+ // Allow passign in falsy usernames/passwords so we can test them
+ try {
+ xhr.open(req.method, req.url, true,
+ ("username" in req) ? req.username : "",
+ ("password" in req) ? req.password : "");
+ } catch (ex) {
+ res.didFail = true;
+ post(e, res);
+ }
+
+ for (header in req.headers) {
+ xhr.setRequestHeader(header, req.headers[header]);
+ }
+
+ res.events.push("sending");
+ try {
+ xhr.send(sendData);
+ } catch (ex) {
+ res.didFail = true;
+ res.sendThrew = true;
+ post(e, res);
+ }
+
+});
+
+function post(e, res) {
+ e.source.postMessage(JSON.stringify(res), "http://mochi.test:8888");
+}
+
+</script>
+</head>
+<body>
+Inner page
+</body>
+</html>
diff --git a/dom/security/test/cors/file_CrossSiteXHR_inner.jar b/dom/security/test/cors/file_CrossSiteXHR_inner.jar
new file mode 100644
index 0000000000..bdb0eb4408
--- /dev/null
+++ b/dom/security/test/cors/file_CrossSiteXHR_inner.jar
Binary files differ
diff --git a/dom/security/test/cors/file_CrossSiteXHR_inner_data.sjs b/dom/security/test/cors/file_CrossSiteXHR_inner_data.sjs
new file mode 100644
index 0000000000..4a030c4211
--- /dev/null
+++ b/dom/security/test/cors/file_CrossSiteXHR_inner_data.sjs
@@ -0,0 +1,103 @@
+var data =
+ '<!DOCTYPE HTML>\n\
+<html>\n\
+<head>\n\
+<script>\n\
+window.addEventListener("message", function(e) {\n\
+\n\
+ sendData = null;\n\
+\n\
+ req = JSON.parse(e.data);\n\
+ var res = {\n\
+ didFail: false,\n\
+ events: [],\n\
+ progressEvents: 0\n\
+ };\n\
+ \n\
+ var xhr = new XMLHttpRequest();\n\
+ for (type of ["load", "abort", "error", "loadstart", "loadend"]) {\n\
+ xhr.addEventListener(type, function(e) {\n\
+ res.events.push(e.type);\n\
+ }, false);\n\
+ }\n\
+ xhr.addEventListener("readystatechange", function(e) {\n\
+ res.events.push("rs" + xhr.readyState);\n\
+ }, false);\n\
+ xhr.addEventListener("progress", function(e) {\n\
+ res.progressEvents++;\n\
+ }, false);\n\
+ if (req.uploadProgress) {\n\
+ xhr.upload.addEventListener(req.uploadProgress, function(e) {\n\
+ res.progressEvents++;\n\
+ }, false);\n\
+ }\n\
+ xhr.onerror = function(e) {\n\
+ res.didFail = true;\n\
+ };\n\
+ xhr.onloadend = function (event) {\n\
+ res.status = xhr.status;\n\
+ try {\n\
+ res.statusText = xhr.statusText;\n\
+ } catch (e) {\n\
+ delete(res.statusText);\n\
+ }\n\
+ res.responseXML = xhr.responseXML ?\n\
+ (new XMLSerializer()).serializeToString(xhr.responseXML) :\n\
+ null;\n\
+ res.responseText = xhr.responseText;\n\
+\n\
+ res.responseHeaders = {};\n\
+ for (responseHeader in req.responseHeaders) {\n\
+ res.responseHeaders[responseHeader] =\n\
+ xhr.getResponseHeader(responseHeader);\n\
+ }\n\
+ res.allResponseHeaders = {};\n\
+ var splitHeaders = xhr.getAllResponseHeaders().split("\\r\\n");\n\
+ for (var i = 0; i < splitHeaders.length; i++) {\n\
+ var headerValuePair = splitHeaders[i].split(":");\n\
+ if(headerValuePair[1] != null){\n\
+ var headerName = trimString(headerValuePair[0]);\n\
+ var headerValue = trimString(headerValuePair[1]); \n\
+ res.allResponseHeaders[headerName] = headerValue;\n\
+ }\n\
+ }\n\
+ post(e, res);\n\
+ }\n\
+\n\
+ if (req.withCred)\n\
+ xhr.withCredentials = true;\n\
+ if (req.body)\n\
+ sendData = req.body;\n\
+\n\
+ res.events.push("opening");\n\
+ xhr.open(req.method, req.url, true);\n\
+\n\
+ for (header in req.headers) {\n\
+ xhr.setRequestHeader(header, req.headers[header]);\n\
+ }\n\
+\n\
+ res.events.push("sending");\n\
+ xhr.send(sendData);\n\
+\n\
+}, false);\n\
+\n\
+function post(e, res) {\n\
+ e.source.postMessage(JSON.stringify(res), "*");\n\
+}\n\
+function trimString(stringValue) {\n\
+ return stringValue.replace("/^s+|s+$/g","");\n\
+};\n\
+\n\
+</script>\n\
+</head>\n\
+<body>\n\
+Inner page\n\
+</body>\n\
+</html>';
+
+function handleRequest(request, response) {
+ response.setStatusLine(null, 302, "Follow me");
+ response.setHeader("Location", "data:text/html," + escape(data));
+ response.setHeader("Content-Type", "text/plain");
+ response.write("Follow that guy!");
+}
diff --git a/dom/security/test/cors/file_CrossSiteXHR_server.sjs b/dom/security/test/cors/file_CrossSiteXHR_server.sjs
new file mode 100644
index 0000000000..a3129de75f
--- /dev/null
+++ b/dom/security/test/cors/file_CrossSiteXHR_server.sjs
@@ -0,0 +1,230 @@
+const CC = Components.Constructor;
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+
+// eslint-disable-next-line complexity
+function handleRequest(request, response) {
+ var query = {};
+ request.queryString.split("&").forEach(function (val) {
+ var [name, value] = val.split("=");
+ query[name] = unescape(value);
+ });
+
+ var isPreflight = request.method == "OPTIONS";
+
+ var bodyStream = new BinaryInputStream(request.bodyInputStream);
+ var bodyBytes = [];
+ while ((bodyAvail = bodyStream.available()) > 0) {
+ Array.prototype.push.apply(bodyBytes, bodyStream.readByteArray(bodyAvail));
+ }
+
+ var body = decodeURIComponent(
+ escape(String.fromCharCode.apply(null, bodyBytes))
+ );
+
+ if (query.hop) {
+ query.hop = parseInt(query.hop, 10);
+ hops = JSON.parse(query.hops);
+ var curHop = hops[query.hop - 1];
+ query.allowOrigin = curHop.allowOrigin;
+ query.allowHeaders = curHop.allowHeaders;
+ query.allowMethods = curHop.allowMethods;
+ query.allowCred = curHop.allowCred;
+ query.noAllowPreflight = curHop.noAllowPreflight;
+ if (curHop.setCookie) {
+ query.setCookie = unescape(curHop.setCookie);
+ }
+ if (curHop.cookie) {
+ query.cookie = unescape(curHop.cookie);
+ }
+ query.noCookie = curHop.noCookie;
+ }
+
+ // Check that request was correct
+
+ if (!isPreflight && query.body && body != query.body) {
+ sendHttp500(
+ response,
+ "Wrong body. Expected " + query.body + " got " + body
+ );
+ return;
+ }
+
+ if (!isPreflight && "headers" in query) {
+ headers = JSON.parse(query.headers);
+ for (headerName in headers) {
+ // Content-Type is changed if there was a body
+ if (
+ !(headerName == "Content-Type" && body) &&
+ (!request.hasHeader(headerName) ||
+ request.getHeader(headerName) != headers[headerName])
+ ) {
+ var actual = request.hasHeader(headerName)
+ ? request.getHeader(headerName)
+ : "<missing header>";
+ sendHttp500(
+ response,
+ "Header " +
+ headerName +
+ " had wrong value. Expected " +
+ headers[headerName] +
+ " got " +
+ actual
+ );
+ return;
+ }
+ }
+ }
+
+ if (
+ isPreflight &&
+ "requestHeaders" in query &&
+ request.getHeader("Access-Control-Request-Headers") != query.requestHeaders
+ ) {
+ sendHttp500(
+ response,
+ "Access-Control-Request-Headers had wrong value. Expected " +
+ query.requestHeaders +
+ " got " +
+ request.getHeader("Access-Control-Request-Headers")
+ );
+ return;
+ }
+
+ if (
+ isPreflight &&
+ "requestMethod" in query &&
+ request.getHeader("Access-Control-Request-Method") != query.requestMethod
+ ) {
+ sendHttp500(
+ response,
+ "Access-Control-Request-Method had wrong value. Expected " +
+ query.requestMethod +
+ " got " +
+ request.getHeader("Access-Control-Request-Method")
+ );
+ return;
+ }
+
+ if ("origin" in query && request.getHeader("Origin") != query.origin) {
+ sendHttp500(
+ response,
+ "Origin had wrong value. Expected " +
+ query.origin +
+ " got " +
+ request.getHeader("Origin")
+ );
+ return;
+ }
+
+ if ("cookie" in query) {
+ cookies = {};
+ request
+ .getHeader("Cookie")
+ .split(/ *; */)
+ .forEach(function (val) {
+ var [name, value] = val.split("=");
+ cookies[name] = unescape(value);
+ });
+
+ query.cookie.split(",").forEach(function (val) {
+ var [name, value] = val.split("=");
+ if (cookies[name] != value) {
+ sendHttp500(
+ response,
+ "Cookie " +
+ name +
+ " had wrong value. Expected " +
+ value +
+ " got " +
+ cookies[name]
+ );
+ }
+ });
+ }
+
+ if (query.noCookie && request.hasHeader("Cookie")) {
+ sendHttp500(
+ response,
+ "Got cookies when didn't expect to: " + request.getHeader("Cookie")
+ );
+ return;
+ }
+
+ // Send response
+
+ if (!isPreflight && query.status) {
+ response.setStatusLine(null, query.status, query.statusMessage);
+ }
+ if (isPreflight && query.preflightStatus) {
+ response.setStatusLine(null, query.preflightStatus, "preflight status");
+ }
+
+ if (query.allowOrigin && (!isPreflight || !query.noAllowPreflight)) {
+ response.setHeader("Access-Control-Allow-Origin", query.allowOrigin);
+ }
+
+ if (query.allowCred) {
+ response.setHeader("Access-Control-Allow-Credentials", "true");
+ }
+
+ if (query.setCookie) {
+ response.setHeader("Set-Cookie", query.setCookie + "; path=/");
+ }
+
+ if (isPreflight) {
+ if (query.allowHeaders) {
+ response.setHeader("Access-Control-Allow-Headers", query.allowHeaders);
+ }
+
+ if (query.allowMethods) {
+ response.setHeader("Access-Control-Allow-Methods", query.allowMethods);
+ }
+ } else {
+ if (query.responseHeaders) {
+ let responseHeaders = JSON.parse(query.responseHeaders);
+ for (let responseHeader in responseHeaders) {
+ response.setHeader(responseHeader, responseHeaders[responseHeader]);
+ }
+ }
+
+ if (query.exposeHeaders) {
+ response.setHeader("Access-Control-Expose-Headers", query.exposeHeaders);
+ }
+ }
+
+ if (!isPreflight && query.hop && query.hop < hops.length) {
+ newURL =
+ hops[query.hop].server +
+ "/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?" +
+ "hop=" +
+ (query.hop + 1) +
+ "&hops=" +
+ escape(query.hops);
+ if ("headers" in query) {
+ newURL += "&headers=" + escape(query.headers);
+ }
+ response.setStatusLine(null, 307, "redirect");
+ response.setHeader("Location", newURL);
+
+ return;
+ }
+
+ // Send response body
+ if (!isPreflight && request.method != "HEAD") {
+ response.setHeader("Content-Type", "application/xml", false);
+ response.write("<res>hello pass</res>\n");
+ }
+ if (isPreflight && "preflightBody" in query) {
+ response.setHeader("Content-Type", "text/plain", false);
+ response.write(query.preflightBody);
+ }
+}
+
+function sendHttp500(response, text) {
+ response.setStatusLine(null, 500, text);
+}
diff --git a/dom/security/test/cors/file_bug1456721.html b/dom/security/test/cors/file_bug1456721.html
new file mode 100644
index 0000000000..8926b6ffc1
--- /dev/null
+++ b/dom/security/test/cors/file_bug1456721.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8">
+ <title>Test new CORS console messages</title>
+</head>
+<body onload="initTest()">
+<p id="display">
+<iframe id=loader></iframe>
+</p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="application/javascript">
+
+let gen;
+let number_of_tests = 0;
+
+function initTest() {
+ window.addEventListener("message", function(e) {
+ gen.next(e.data);
+ if (number_of_tests == 2) {
+ document.location.href += "#finishedTestTwo";
+ }
+ });
+
+ gen = runTest();
+
+ gen.next();
+}
+
+function* runTest() {
+ let loader = document.getElementById("loader");
+ let loaderWindow = loader.contentWindow;
+ loader.onload = function() { gen.next(); };
+
+ loader.src = "http://example.org/browser/dom/security/test/cors/file_CrossSiteXHR_inner.html";
+ origin = "http://example.org";
+ yield undefined;
+
+ let tests = [
+ // Loading URLs other than http(s) should throw 'CORS request
+ // not http' console message. (Even though we removed ftp support within Bug 1574475
+ // we keep this test since it tests a scheme other than http(s))
+ { baseURL: "ftp://mochi.test:8888/browser/dom/security/test/cors/file_CrossSiteXHR_server.sjs",
+ method: "GET",
+ },
+ // (https://www.w3.org/TR/cors/#cross-origin-request-with-preflight-0)
+ // CORs preflight external redirect should throw 'CORS request
+ // external redirect not allowed' error.
+ // This will also throw 'CORS preflight channel did not succeed'
+ // and 'CORS request did not succeed' console messages.
+ {
+ baseURL: "http://mochi.test:8888/browser/dom/security/test/cors/bug1456721.sjs?redirect",
+ method: "OPTIONS",
+ },
+ ];
+
+ for (let test of tests) {
+ let req = {
+ url: test.baseURL,
+ method: test.method
+ };
+
+ loaderWindow.postMessage(JSON.stringify(req), origin);
+ number_of_tests++;
+ yield;
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/cors/file_cors_logging_test.html b/dom/security/test/cors/file_cors_logging_test.html
new file mode 100644
index 0000000000..d29f93cf9c
--- /dev/null
+++ b/dom/security/test/cors/file_cors_logging_test.html
@@ -0,0 +1,1311 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8">
+ <title>Test for Cross Site XMLHttpRequest</title>
+</head>
+<body onload="initTest()">
+<p id="display">
+<iframe id=loader></iframe>
+</p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="application/javascript">
+
+const runPreflightTests = 1;
+const runCookieTests = 1;
+const runRedirectTests = 1;
+
+var gen;
+
+function initTest() {
+ window.addEventListener("message", function(e) {
+ gen.next(e.data);
+ });
+
+ gen = runTest();
+
+ gen.next()
+}
+
+function initTestCallback() {
+}
+
+function* runTest() {
+ var loader = document.getElementById('loader');
+ var loaderWindow = loader.contentWindow;
+ loader.onload = function () { gen.next() };
+
+ // Test preflight-less requests
+ basePath = "/browser/dom/security/test/cors/file_CrossSiteXHR_server.sjs?"
+ baseURL = "http://mochi.test:8888" + basePath;
+
+ // Test preflighted requests
+ loader.src = "http://example.org/browser/dom/security/test/cors/file_CrossSiteXHR_inner.html";
+ origin = "http://example.org";
+ yield undefined;
+
+ tests = [// Plain request
+ { pass: 1,
+ method: "GET",
+ noAllowPreflight: 1,
+ },
+
+ // undefined username
+ { pass: 1,
+ method: "GET",
+ noAllowPreflight: 1,
+ username: undefined
+ },
+
+ // undefined username and password
+ { pass: 1,
+ method: "GET",
+ noAllowPreflight: 1,
+ username: undefined,
+ password: undefined
+ },
+
+ // nonempty username
+ { pass: 0,
+ method: "GET",
+ noAllowPreflight: 1,
+ username: "user",
+ },
+
+ // nonempty password
+ // XXXbz this passes for now, because we ignore passwords
+ // without usernames in most cases.
+ { pass: 1,
+ method: "GET",
+ noAllowPreflight: 1,
+ password: "password",
+ },
+
+ // Default allowed headers
+ { pass: 1,
+ method: "GET",
+ headers: { "Content-Type": "text/plain",
+ "Accept": "foo/bar",
+ "Accept-Language": "sv-SE" },
+ noAllowPreflight: 1,
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "Content-Type": "foo/bar",
+ "Accept": "foo/bar",
+ "Accept-Language": "sv-SE" },
+ noAllowPreflight: 1,
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "Content-Type": "foo/bar, text/plain" },
+ noAllowPreflight: 1,
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "Content-Type": "foo/bar, text/plain, garbage" },
+ noAllowPreflight: 1,
+ },
+
+ // Custom headers
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "x-my-header",
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "X-My-Header",
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue",
+ "long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header": "secondValue" },
+ allowHeaders: "x-my-header, long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header",
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my%-header": "myValue" },
+ allowHeaders: "x-my%-header",
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "" },
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "",
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "y-my-header",
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "x-my-header y-my-header",
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "x-my-header, y-my-header z",
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "x-my-header, y-my-he(ader",
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "myheader": "" },
+ allowMethods: "myheader",
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "User-Agent": "myValue" },
+ allowHeaders: "User-Agent",
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "User-Agent": "myValue" },
+ },
+
+ // Multiple custom headers
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue",
+ "second-header": "secondValue",
+ "third-header": "thirdValue" },
+ allowHeaders: "x-my-header, second-header, third-header",
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue",
+ "second-header": "secondValue",
+ "third-header": "thirdValue" },
+ allowHeaders: "x-my-header,second-header,third-header",
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue",
+ "second-header": "secondValue",
+ "third-header": "thirdValue" },
+ allowHeaders: "x-my-header ,second-header ,third-header",
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue",
+ "second-header": "secondValue",
+ "third-header": "thirdValue" },
+ allowHeaders: "x-my-header , second-header , third-header",
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue",
+ "second-header": "secondValue" },
+ allowHeaders: ", x-my-header, , ,, second-header, , ",
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue",
+ "second-header": "secondValue" },
+ allowHeaders: "x-my-header, second-header, unused-header",
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "myValue",
+ "y-my-header": "secondValue" },
+ allowHeaders: "x-my-header",
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "",
+ "y-my-header": "" },
+ allowHeaders: "x-my-header",
+ },
+
+ // HEAD requests
+ { pass: 1,
+ method: "HEAD",
+ noAllowPreflight: 1,
+ },
+
+ // HEAD with safe headers
+ { pass: 1,
+ method: "HEAD",
+ headers: { "Content-Type": "text/plain",
+ "Accept": "foo/bar",
+ "Accept-Language": "sv-SE" },
+ noAllowPreflight: 1,
+ },
+ { pass: 0,
+ method: "HEAD",
+ headers: { "Content-Type": "foo/bar",
+ "Accept": "foo/bar",
+ "Accept-Language": "sv-SE" },
+ noAllowPreflight: 1,
+ },
+ { pass: 0,
+ method: "HEAD",
+ headers: { "Content-Type": "foo/bar, text/plain" },
+ noAllowPreflight: 1,
+ },
+ { pass: 0,
+ method: "HEAD",
+ headers: { "Content-Type": "foo/bar, text/plain, garbage" },
+ noAllowPreflight: 1,
+ },
+
+ // HEAD with custom headers
+ { pass: 1,
+ method: "HEAD",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "x-my-header",
+ },
+ { pass: 0,
+ method: "HEAD",
+ headers: { "x-my-header": "myValue" },
+ },
+ { pass: 0,
+ method: "HEAD",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "",
+ },
+ { pass: 0,
+ method: "HEAD",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "y-my-header",
+ },
+ { pass: 0,
+ method: "HEAD",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "x-my-header y-my-header",
+ },
+
+ // POST tests
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ noAllowPreflight: 1,
+ },
+ { pass: 1,
+ method: "POST",
+ },
+ { pass: 1,
+ method: "POST",
+ noAllowPreflight: 1,
+ },
+
+ // POST with standard headers
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain" },
+ noAllowPreflight: 1,
+ },
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "multipart/form-data" },
+ noAllowPreflight: 1,
+ },
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
+ noAllowPreflight: 1,
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "foo/bar" },
+ },
+ { pass: 0,
+ method: "POST",
+ headers: { "Content-Type": "foo/bar" },
+ },
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain",
+ "Accept": "foo/bar",
+ "Accept-Language": "sv-SE" },
+ noAllowPreflight: 1,
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "foo/bar, text/plain" },
+ noAllowPreflight: 1,
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "foo/bar, text/plain, garbage" },
+ noAllowPreflight: 1,
+ },
+
+ // POST with custom headers
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Accept": "foo/bar",
+ "Accept-Language": "sv-SE",
+ "x-my-header": "myValue" },
+ allowHeaders: "x-my-header",
+ },
+ { pass: 1,
+ method: "POST",
+ headers: { "Content-Type": "text/plain",
+ "x-my-header": "myValue" },
+ allowHeaders: "x-my-header",
+ },
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain",
+ "x-my-header": "myValue" },
+ allowHeaders: "x-my-header",
+ },
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "foo/bar",
+ "x-my-header": "myValue" },
+ allowHeaders: "x-my-header, content-type",
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "foo/bar" },
+ noAllowPreflight: 1,
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "foo/bar",
+ "x-my-header": "myValue" },
+ allowHeaders: "x-my-header",
+ },
+ { pass: 1,
+ method: "POST",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "x-my-header",
+ },
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "x-my-header, $_%",
+ },
+
+ // Other methods
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: "DELETE",
+ },
+ { pass: 0,
+ method: "DELETE",
+ allowHeaders: "DELETE",
+ },
+ { pass: 0,
+ method: "DELETE",
+ },
+ { pass: 0,
+ method: "DELETE",
+ allowMethods: "",
+ },
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: "POST, PUT, DELETE",
+ },
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: "POST, DELETE, PUT",
+ },
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: "DELETE, POST, PUT",
+ },
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: "POST ,PUT ,DELETE",
+ },
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: "POST,PUT,DELETE",
+ },
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: "POST , PUT , DELETE",
+ },
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: " ,, PUT ,, , , DELETE , ,",
+ },
+ { pass: 0,
+ method: "DELETE",
+ allowMethods: "PUT",
+ },
+ { pass: 0,
+ method: "DELETE",
+ allowMethods: "DELETEZ",
+ },
+ { pass: 0,
+ method: "DELETE",
+ allowMethods: "DELETE PUT",
+ },
+ { pass: 0,
+ method: "DELETE",
+ allowMethods: "DELETE, PUT Z",
+ },
+ { pass: 0,
+ method: "DELETE",
+ allowMethods: "DELETE, PU(T",
+ },
+ { pass: 0,
+ method: "DELETE",
+ allowMethods: "PUT DELETE",
+ },
+ { pass: 0,
+ method: "DELETE",
+ allowMethods: "PUT Z, DELETE",
+ },
+ { pass: 0,
+ method: "DELETE",
+ allowMethods: "PU(T, DELETE",
+ },
+ { pass: 0,
+ method: "MYMETHOD",
+ allowMethods: "myMethod",
+ },
+ { pass: 0,
+ method: "PUT",
+ allowMethods: "put",
+ },
+
+ // Progress events
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain" },
+ uploadProgress: "progress",
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain" },
+ uploadProgress: "progress",
+ noAllowPreflight: 1,
+ },
+
+ // Status messages
+ { pass: 1,
+ method: "GET",
+ noAllowPreflight: 1,
+ status: 404,
+ statusMessage: "nothin' here",
+ },
+ { pass: 1,
+ method: "GET",
+ noAllowPreflight: 1,
+ status: 401,
+ statusMessage: "no can do",
+ },
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "foo/bar" },
+ allowHeaders: "content-type",
+ status: 500,
+ statusMessage: "server boo",
+ },
+ { pass: 1,
+ method: "GET",
+ noAllowPreflight: 1,
+ status: 200,
+ statusMessage: "Yes!!",
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "header value" },
+ allowHeaders: "x-my-header",
+ preflightStatus: 400
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "header value" },
+ allowHeaders: "x-my-header",
+ preflightStatus: 200
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "header value" },
+ allowHeaders: "x-my-header",
+ preflightStatus: 204
+ },
+
+ // exposed headers
+ { pass: 1,
+ method: "GET",
+ responseHeaders: { "x-my-header": "x header" },
+ exposeHeaders: "x-my-header",
+ expectedResponseHeaders: ["x-my-header"],
+ },
+ { pass: 0,
+ method: "GET",
+ origin: "http://invalid",
+ responseHeaders: { "x-my-header": "x header" },
+ exposeHeaders: "x-my-header",
+ expectedResponseHeaders: [],
+ },
+ { pass: 1,
+ method: "GET",
+ responseHeaders: { "x-my-header": "x header" },
+ expectedResponseHeaders: [],
+ },
+ { pass: 1,
+ method: "GET",
+ responseHeaders: { "x-my-header": "x header" },
+ exposeHeaders: "x-my-header y",
+ expectedResponseHeaders: [],
+ },
+ { pass: 1,
+ method: "GET",
+ responseHeaders: { "x-my-header": "x header" },
+ exposeHeaders: "y x-my-header",
+ expectedResponseHeaders: [],
+ },
+ { pass: 1,
+ method: "GET",
+ responseHeaders: { "x-my-header": "x header" },
+ exposeHeaders: "x-my-header, y-my-header z",
+ expectedResponseHeaders: [],
+ },
+ { pass: 1,
+ method: "GET",
+ responseHeaders: { "x-my-header": "x header" },
+ exposeHeaders: "x-my-header, y-my-hea(er",
+ expectedResponseHeaders: [],
+ },
+ { pass: 1,
+ method: "GET",
+ responseHeaders: { "x-my-header": "x header",
+ "y-my-header": "y header" },
+ exposeHeaders: " , ,,y-my-header,z-my-header, ",
+ expectedResponseHeaders: ["y-my-header"],
+ },
+ { pass: 1,
+ method: "GET",
+ responseHeaders: { "Cache-Control": "cacheControl header",
+ "Content-Language": "contentLanguage header",
+ "Expires":"expires header",
+ "Last-Modified":"lastModified header",
+ "Pragma":"pragma header",
+ "Unexpected":"unexpected header" },
+ expectedResponseHeaders: ["Cache-Control","Content-Language","Content-Type","Expires","Last-Modified","Pragma"],
+ },
+ // Check that sending a body in the OPTIONS response works
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: "DELETE",
+ preflightBody: "I'm a preflight response body",
+ },
+ ];
+
+ if (!runPreflightTests) {
+ tests = [];
+ }
+
+ for (test of tests) {
+ var req = {
+ url: baseURL + "allowOrigin=" + escape(test.origin || origin),
+ method: test.method,
+ headers: test.headers,
+ uploadProgress: test.uploadProgress,
+ body: test.body,
+ responseHeaders: test.responseHeaders,
+ };
+
+ if (test.pass) {
+ req.url += "&origin=" + escape(origin) +
+ "&requestMethod=" + test.method;
+ }
+
+ if ("username" in test) {
+ req.username = test.username;
+ }
+
+ if ("password" in test) {
+ req.password = test.password;
+ }
+
+ if (test.noAllowPreflight)
+ req.url += "&noAllowPreflight";
+
+ if (test.pass && "headers" in test) {
+ function isUnsafeHeader(name) {
+ lName = name.toLowerCase();
+ return lName != "accept" &&
+ lName != "accept-language" &&
+ (lName != "content-type" ||
+ !["text/plain",
+ "multipart/form-data",
+ "application/x-www-form-urlencoded"]
+ .includes(test.headers[name].toLowerCase()));
+ }
+ req.url += "&headers=" + escape(JSON.stringify(test.headers));
+ reqHeaders =
+ escape(Object.keys(test.headers)
+ .filter(isUnsafeHeader)
+ .map(s => s.toLowerCase())
+ .sort()
+ .join(","));
+ req.url += reqHeaders ? "&requestHeaders=" + reqHeaders : "";
+ }
+ if ("allowHeaders" in test)
+ req.url += "&allowHeaders=" + escape(test.allowHeaders);
+ if ("allowMethods" in test)
+ req.url += "&allowMethods=" + escape(test.allowMethods);
+ if (test.body)
+ req.url += "&body=" + escape(test.body);
+ if (test.status) {
+ req.url += "&status=" + test.status;
+ req.url += "&statusMessage=" + escape(test.statusMessage);
+ }
+ if (test.preflightStatus)
+ req.url += "&preflightStatus=" + test.preflightStatus;
+ if (test.responseHeaders)
+ req.url += "&responseHeaders=" + escape(JSON.stringify(test.responseHeaders));
+ if (test.exposeHeaders)
+ req.url += "&exposeHeaders=" + escape(test.exposeHeaders);
+ if (test.preflightBody)
+ req.url += "&preflightBody=" + escape(test.preflightBody);
+
+ loaderWindow.postMessage(JSON.stringify(req), origin);
+ res = JSON.parse(yield);
+ }
+
+ // Test cookie behavior
+ tests = [{ pass: 1,
+ method: "GET",
+ withCred: 1,
+ allowCred: 1,
+ },
+ { pass: 0,
+ method: "GET",
+ withCred: 1,
+ allowCred: 0,
+ },
+ { pass: 0,
+ method: "GET",
+ withCred: 1,
+ allowCred: 1,
+ origin: "*",
+ },
+ { pass: 1,
+ method: "GET",
+ withCred: 0,
+ allowCred: 1,
+ origin: "*",
+ },
+ { pass: 1,
+ method: "GET",
+ setCookie: "a=1",
+ withCred: 1,
+ allowCred: 1,
+ },
+ { pass: 1,
+ method: "GET",
+ cookie: "a=1",
+ withCred: 1,
+ allowCred: 1,
+ },
+ { pass: 1,
+ method: "GET",
+ noCookie: 1,
+ withCred: 0,
+ allowCred: 1,
+ },
+ { pass: 0,
+ method: "GET",
+ noCookie: 1,
+ withCred: 1,
+ allowCred: 1,
+ },
+ { pass: 1,
+ method: "GET",
+ setCookie: "a=2",
+ withCred: 0,
+ allowCred: 1,
+ },
+ { pass: 1,
+ method: "GET",
+ cookie: "a=1",
+ withCred: 1,
+ allowCred: 1,
+ },
+ { pass: 1,
+ method: "GET",
+ setCookie: "a=2",
+ withCred: 1,
+ allowCred: 1,
+ },
+ { pass: 1,
+ method: "GET",
+ cookie: "a=2",
+ withCred: 1,
+ allowCred: 1,
+ },
+ ];
+
+ if (!runCookieTests) {
+ tests = [];
+ }
+
+ for (test of tests) {
+ req = {
+ url: baseURL + "allowOrigin=" + escape(test.origin || origin),
+ method: test.method,
+ headers: test.headers,
+ withCred: test.withCred,
+ };
+
+ if (test.allowCred)
+ req.url += "&allowCred";
+
+ if (test.setCookie)
+ req.url += "&setCookie=" + escape(test.setCookie);
+ if (test.cookie)
+ req.url += "&cookie=" + escape(test.cookie);
+ if (test.noCookie)
+ req.url += "&noCookie";
+
+ if ("allowHeaders" in test)
+ req.url += "&allowHeaders=" + escape(test.allowHeaders);
+ if ("allowMethods" in test)
+ req.url += "&allowMethods=" + escape(test.allowMethods);
+
+ loaderWindow.postMessage(JSON.stringify(req), origin);
+
+ res = JSON.parse(yield);
+ }
+
+ // Make sure to clear cookies to avoid affecting other tests
+ document.cookie = "a=; path=/; expires=Thu, 01-Jan-1970 00:00:01 GMT"
+
+ // Test redirects
+
+ tests = [{ pass: 1,
+ method: "GET",
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin
+ },
+ ],
+ },
+ { pass: 0,
+ method: "GET",
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin
+ },
+ { server: "http://example.org",
+ allowOrigin: origin
+ },
+ ],
+ },
+ { pass: 1,
+ method: "GET",
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin
+ },
+ { server: "http://example.org",
+ allowOrigin: "*"
+ },
+ ],
+ },
+ { pass: 0,
+ method: "GET",
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin
+ },
+ { server: "http://example.org",
+ },
+ ],
+ },
+ { pass: 1,
+ method: "GET",
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin
+ },
+ ],
+ },
+ { pass: 0,
+ method: "GET",
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin
+ },
+ { server: "http://example.org",
+ },
+ ],
+ },
+ { pass: 0,
+ method: "GET",
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin
+ },
+ { server: "http://test2.example.org:8000",
+ allowOrigin: origin
+ },
+ { server: "http://sub2.xn--lt-uia.example.org",
+ allowOrigin: origin
+ },
+ { server: "http://sub1.test1.example.org",
+ allowOrigin: origin
+ },
+ ],
+ },
+ { pass: 0,
+ method: "GET",
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin
+ },
+ { server: "http://test2.example.org:8000",
+ allowOrigin: origin
+ },
+ { server: "http://sub2.xn--lt-uia.example.org",
+ allowOrigin: "*"
+ },
+ { server: "http://sub1.test1.example.org",
+ allowOrigin: "*"
+ },
+ ],
+ },
+ { pass: 1,
+ method: "GET",
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin
+ },
+ { server: "http://test2.example.org:8000",
+ allowOrigin: "*"
+ },
+ { server: "http://sub2.xn--lt-uia.example.org",
+ allowOrigin: "*"
+ },
+ { server: "http://sub1.test1.example.org",
+ allowOrigin: "*"
+ },
+ ],
+ },
+ { pass: 0,
+ method: "GET",
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin
+ },
+ { server: "http://test2.example.org:8000",
+ allowOrigin: origin
+ },
+ { server: "http://sub2.xn--lt-uia.example.org",
+ allowOrigin: "x"
+ },
+ { server: "http://sub1.test1.example.org",
+ allowOrigin: origin
+ },
+ ],
+ },
+ { pass: 0,
+ method: "GET",
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin
+ },
+ { server: "http://test2.example.org:8000",
+ allowOrigin: origin
+ },
+ { server: "http://sub2.xn--lt-uia.example.org",
+ allowOrigin: "*"
+ },
+ { server: "http://sub1.test1.example.org",
+ allowOrigin: origin
+ },
+ ],
+ },
+ { pass: 0,
+ method: "GET",
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin
+ },
+ { server: "http://test2.example.org:8000",
+ allowOrigin: origin
+ },
+ { server: "http://sub2.xn--lt-uia.example.org",
+ allowOrigin: "*"
+ },
+ { server: "http://sub1.test1.example.org",
+ },
+ ],
+ },
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain" },
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ },
+ ],
+ },
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain",
+ "my-header": "myValue",
+ },
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowHeaders: "my-header",
+ },
+ ],
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain",
+ "my-header": "myValue",
+ },
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowHeaders: "my-header",
+ },
+ { server: "http://sub1.test1.example.org",
+ allowOrigin: origin,
+ allowHeaders: "my-header",
+ },
+ ],
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain",
+ "my-header": "myValue",
+ },
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowHeaders: "my-header",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowHeaders: "my-header",
+ },
+ ],
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain",
+ "my-header": "myValue",
+ },
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowHeaders: "my-header",
+ },
+ { server: "http://example.org",
+ allowOrigin: origin,
+ allowHeaders: "my-header",
+ },
+ ],
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain",
+ "my-header": "myValue",
+ },
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ noAllowPreflight: 1,
+ },
+ ],
+ },
+ { pass: 1,
+ method: "DELETE",
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowMethods: "DELETE",
+ },
+ ],
+ },
+ { pass: 0,
+ method: "DELETE",
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowMethods: "DELETE",
+ },
+ { server: "http://sub1.test1.example.org",
+ allowOrigin: origin,
+ allowMethods: "DELETE",
+ },
+ ],
+ },
+ { pass: 0,
+ method: "DELETE",
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowMethods: "DELETE",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowMethods: "DELETE",
+ },
+ ],
+ },
+ { pass: 0,
+ method: "DELETE",
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowMethods: "DELETE",
+ },
+ { server: "http://example.org",
+ allowOrigin: origin,
+ allowMethods: "DELETE",
+ },
+ ],
+ },
+ { pass: 0,
+ method: "DELETE",
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowMethods: "DELETE",
+ noAllowPreflight: 1,
+ },
+ ],
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain",
+ "my-header": "myValue",
+ },
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin,
+ },
+ { server: "http://sub1.test1.example.org",
+ allowOrigin: origin,
+ },
+ ],
+ },
+ { pass: 0,
+ method: "DELETE",
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin,
+ },
+ { server: "http://sub1.test1.example.org",
+ allowOrigin: origin,
+ },
+ ],
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain",
+ "my-header": "myValue",
+ },
+ hops: [{ server: "http://example.com",
+ },
+ { server: "http://sub1.test1.example.org",
+ allowOrigin: origin,
+ allowHeaders: "my-header",
+ },
+ ],
+ },
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain" },
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ },
+ ],
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain",
+ "my-header": "myValue",
+ },
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin,
+ allowHeaders: "my-header",
+ },
+ { server: "http://example.org",
+ allowOrigin: origin,
+ allowHeaders: "my-header",
+ },
+ ],
+ },
+
+ // test redirects with different credentials settings
+ {
+ // Initialize by setting a cookies for same- and cross- origins.
+ pass: 1,
+ method: "GET",
+ hops: [{ server: origin,
+ setCookie: escape("a=1"),
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowCred: 1,
+ setCookie: escape("a=2"),
+ },
+ ],
+ withCred: 1,
+ },
+ { pass: 1,
+ method: "GET",
+ hops: [{ server: origin,
+ cookie: escape("a=1"),
+ },
+ { server: origin,
+ cookie: escape("a=1"),
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ noCookie: 1,
+ },
+ ],
+ withCred: 0,
+ },
+ { pass: 1,
+ method: "GET",
+ hops: [{ server: origin,
+ cookie: escape("a=1"),
+ },
+ { server: origin,
+ cookie: escape("a=1"),
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowCred: 1,
+ cookie: escape("a=2"),
+ },
+ ],
+ withCred: 1,
+ },
+ // expected fail because allow-credentials CORS header is not set
+ { pass: 0,
+ method: "GET",
+ hops: [{ server: origin,
+ cookie: escape("a=1"),
+ },
+ { server: origin,
+ cookie: escape("a=1"),
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ cookie: escape("a=2"),
+ },
+ ],
+ withCred: 1,
+ },
+ { pass: 1,
+ method: "GET",
+ hops: [{ server: origin,
+ cookie: escape("a=1"),
+ },
+ { server: origin,
+ cookie: escape("a=1"),
+ },
+ { server: "http://example.com",
+ allowOrigin: '*',
+ noCookie: 1,
+ },
+ ],
+ withCred: 0,
+ },
+ { pass: 0,
+ method: "GET",
+ hops: [{ server: origin,
+ cookie: escape("a=1"),
+ },
+ { server: origin,
+ cookie: escape("a=1"),
+ },
+ { server: "http://example.com",
+ allowOrigin: '*',
+ allowCred: 1,
+ cookie: escape("a=2"),
+ },
+ ],
+ withCred: 1,
+ },
+ ];
+
+ if (!runRedirectTests) {
+ tests = [];
+ }
+
+ for (test of tests) {
+ req = {
+ url: test.hops[0].server + basePath + "hop=1&hops=" +
+ escape(JSON.stringify(test.hops)),
+ method: test.method,
+ headers: test.headers,
+ body: test.body,
+ withCred: test.withCred,
+ };
+
+ if (test.pass) {
+ if (test.body)
+ req.url += "&body=" + escape(test.body);
+ }
+
+ loaderWindow.postMessage(JSON.stringify(req), origin);
+
+ res = JSON.parse(yield);
+ }
+
+ document.location.href += "#finished";
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/cors/file_cors_logging_test.html.css b/dom/security/test/cors/file_cors_logging_test.html.css
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/dom/security/test/cors/file_cors_logging_test.html.css
diff --git a/dom/security/test/cors/mochitest.toml b/dom/security/test/cors/mochitest.toml
new file mode 100644
index 0000000000..b46def07ea
--- /dev/null
+++ b/dom/security/test/cors/mochitest.toml
@@ -0,0 +1,26 @@
+[DEFAULT]
+support-files = [
+ "file_CrossSiteXHR_cache_server.sjs",
+ "file_CrossSiteXHR_inner.html",
+ "file_CrossSiteXHR_inner_data.sjs",
+ "file_CrossSiteXHR_server.sjs",
+]
+
+["test_CrossSiteXHR.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_CrossSiteXHR_cache.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_CrossSiteXHR_origin.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
diff --git a/dom/security/test/cors/test_CrossSiteXHR.html b/dom/security/test/cors/test_CrossSiteXHR.html
new file mode 100644
index 0000000000..f92571c6f8
--- /dev/null
+++ b/dom/security/test/cors/test_CrossSiteXHR.html
@@ -0,0 +1,1549 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8">
+ <title>Test for Cross Site XMLHttpRequest</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="initTest()">
+<p id="display">
+<iframe id=loader></iframe>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="application/javascript">
+
+const runPreflightTests = 1;
+const runCookieTests = 1;
+const runRedirectTests = 1;
+
+var gen;
+
+function initTest() {
+ SimpleTest.waitForExplicitFinish();
+ // Allow all cookies, then do the actual test initialization
+ SpecialPowers.pushPrefEnv({
+ "set": [
+ ["network.cookie.cookieBehavior", 0],
+ // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default"
+ ["network.cookie.sameSite.laxByDefault", false],
+ ["network.cors_preflight.authorization_covered_by_wildcard", false],
+ ]
+ }, initTestCallback);
+}
+
+function initTestCallback() {
+ window.addEventListener("message", function(e) {
+ gen.next(e.data);
+ });
+
+ gen = runTest();
+
+ gen.next()
+}
+
+// eslint-disable-next-line complexity
+function* runTest() {
+ var loader = document.getElementById('loader');
+ var loaderWindow = loader.contentWindow;
+ loader.onload = function () { gen.next() };
+
+ // Test preflight-less requests
+ basePath = "/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?"
+ baseURL = "http://mochi.test:8888" + basePath;
+
+ // Test preflighted requests
+ loader.src = "http://example.org/tests/dom/security/test/cors/file_CrossSiteXHR_inner.html";
+ origin = "http://example.org";
+ yield undefined;
+
+ tests = [// Plain request
+ { pass: 1,
+ method: "GET",
+ noAllowPreflight: 1,
+ },
+
+ // undefined username
+ { pass: 1,
+ method: "GET",
+ noAllowPreflight: 1,
+ username: undefined
+ },
+
+ // undefined username and password
+ { pass: 1,
+ method: "GET",
+ noAllowPreflight: 1,
+ username: undefined,
+ password: undefined
+ },
+
+ // nonempty username
+ { pass: 1,
+ method: "GET",
+ noAllowPreflight: 1,
+ username: "user",
+ },
+
+ // nonempty password
+ { pass: 1,
+ method: "GET",
+ noAllowPreflight: 1,
+ password: "password",
+ },
+
+ // Default allowed headers
+ { pass: 1,
+ method: "GET",
+ headers: { "Content-Type": "text/plain",
+ "Accept": "foo/bar",
+ "Accept-Language": "sv-SE" },
+ noAllowPreflight: 1,
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "Content-Type": "foo/bar",
+ "Accept": "foo/bar",
+ "Accept-Language": "sv-SE" },
+ noAllowPreflight: 1,
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "Content-Type": "foo/bar, text/plain" },
+ noAllowPreflight: 1,
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "Content-Type": "foo/bar, text/plain, garbage" },
+ noAllowPreflight: 1,
+ },
+
+ // Custom headers
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "x-my-header",
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "X-My-Header",
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue",
+ "long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header": "secondValue" },
+ allowHeaders: "x-my-header, long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header",
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my%-header": "myValue" },
+ allowHeaders: "x-my%-header",
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "" },
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "",
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "y-my-header",
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "x-my-header y-my-header",
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "x-my-header, y-my-header z",
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "x-my-header, y-my-he(ader",
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "myheader": "" },
+ allowMethods: "myheader",
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "User-Agent": "myValue" },
+ allowHeaders: "User-Agent",
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "User-Agent": "myValue" },
+ },
+
+ // Multiple custom headers
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue",
+ "second-header": "secondValue",
+ "third-header": "thirdValue" },
+ allowHeaders: "x-my-header, second-header, third-header",
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue",
+ "second-header": "secondValue",
+ "third-header": "thirdValue" },
+ allowHeaders: "x-my-header,second-header,third-header",
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue",
+ "second-header": "secondValue",
+ "third-header": "thirdValue" },
+ allowHeaders: "x-my-header ,second-header ,third-header",
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue",
+ "second-header": "secondValue",
+ "third-header": "thirdValue" },
+ allowHeaders: "x-my-header , second-header , third-header",
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue",
+ "second-header": "secondValue" },
+ allowHeaders: ", x-my-header, , ,, second-header, , ",
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue",
+ "second-header": "secondValue" },
+ allowHeaders: "x-my-header, second-header, unused-header",
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "myValue",
+ "y-my-header": "secondValue" },
+ allowHeaders: "x-my-header",
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "",
+ "y-my-header": "" },
+ allowHeaders: "x-my-header",
+ },
+
+ // HEAD requests
+ { pass: 1,
+ method: "HEAD",
+ noAllowPreflight: 1,
+ },
+
+ // HEAD with safe headers
+ { pass: 1,
+ method: "HEAD",
+ headers: { "Content-Type": "text/plain",
+ "Accept": "foo/bar",
+ "Accept-Language": "sv-SE" },
+ noAllowPreflight: 1,
+ },
+ { pass: 0,
+ method: "HEAD",
+ headers: { "Content-Type": "foo/bar",
+ "Accept": "foo/bar",
+ "Accept-Language": "sv-SE" },
+ noAllowPreflight: 1,
+ },
+ { pass: 0,
+ method: "HEAD",
+ headers: { "Content-Type": "foo/bar, text/plain" },
+ noAllowPreflight: 1,
+ },
+ { pass: 0,
+ method: "HEAD",
+ headers: { "Content-Type": "foo/bar, text/plain, garbage" },
+ noAllowPreflight: 1,
+ },
+
+ // HEAD with custom headers
+ { pass: 1,
+ method: "HEAD",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "x-my-header",
+ },
+ { pass: 0,
+ method: "HEAD",
+ headers: { "x-my-header": "myValue" },
+ },
+ { pass: 0,
+ method: "HEAD",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "",
+ },
+ { pass: 0,
+ method: "HEAD",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "y-my-header",
+ },
+ { pass: 0,
+ method: "HEAD",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "x-my-header y-my-header",
+ },
+
+ // POST tests
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ noAllowPreflight: 1,
+ },
+ { pass: 1,
+ method: "POST",
+ },
+ { pass: 1,
+ method: "POST",
+ noAllowPreflight: 1,
+ },
+
+ // POST with standard headers
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain" },
+ noAllowPreflight: 1,
+ },
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "multipart/form-data" },
+ noAllowPreflight: 1,
+ },
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
+ noAllowPreflight: 1,
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "foo/bar" },
+ },
+ { pass: 0,
+ method: "POST",
+ headers: { "Content-Type": "foo/bar" },
+ },
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain",
+ "Accept": "foo/bar",
+ "Accept-Language": "sv-SE" },
+ noAllowPreflight: 1,
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "foo/bar, text/plain" },
+ noAllowPreflight: 1,
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "foo/bar, text/plain, garbage" },
+ noAllowPreflight: 1,
+ },
+
+ // POST with custom headers
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Accept": "foo/bar",
+ "Accept-Language": "sv-SE",
+ "x-my-header": "myValue" },
+ allowHeaders: "x-my-header",
+ },
+ { pass: 1,
+ method: "POST",
+ headers: { "Content-Type": "text/plain",
+ "x-my-header": "myValue" },
+ allowHeaders: "x-my-header",
+ },
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain",
+ "x-my-header": "myValue" },
+ allowHeaders: "x-my-header",
+ },
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "foo/bar",
+ "x-my-header": "myValue" },
+ allowHeaders: "x-my-header, content-type",
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "foo/bar" },
+ noAllowPreflight: 1,
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "foo/bar",
+ "x-my-header": "myValue" },
+ allowHeaders: "x-my-header",
+ },
+ { pass: 1,
+ method: "POST",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "x-my-header",
+ },
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "x-my-header, $_%",
+ },
+
+ // Test cases for "Access-Control-Allow-Headers" containing "*".
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "*",
+ },
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "x-my-header": "myValue",
+ "Authorization": "12345" },
+ allowHeaders: "*, Authorization",
+ },
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "x-my-header": "myValue",
+ "Authorization": "12345" },
+ allowHeaders: "Authorization, *",
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "x-my-header": "myValue",
+ "Authorization": "12345" },
+ allowHeaders: "*",
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "x-my-header": "myValue",
+ "Authorization": "12345" },
+ allowHeaders: "x-my-header",
+ },
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "*": "myValue" },
+ allowHeaders: "*",
+ withCred: 1,
+ allowCred: 1,
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "*",
+ withCred: 1,
+ allowCred: 1,
+ },
+
+ // Other methods
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: "DELETE",
+ },
+ { pass: 0,
+ method: "DELETE",
+ allowHeaders: "DELETE",
+ },
+ { pass: 0,
+ method: "DELETE",
+ },
+ { pass: 0,
+ method: "DELETE",
+ allowMethods: "",
+ },
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: "POST, PUT, DELETE",
+ },
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: "POST, DELETE, PUT",
+ },
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: "DELETE, POST, PUT",
+ },
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: "POST ,PUT ,DELETE",
+ },
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: "POST,PUT,DELETE",
+ },
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: "POST , PUT , DELETE",
+ },
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: " ,, PUT ,, , , DELETE , ,",
+ },
+ { pass: 0,
+ method: "DELETE",
+ allowMethods: "PUT",
+ },
+ { pass: 0,
+ method: "DELETE",
+ allowMethods: "DELETEZ",
+ },
+ { pass: 0,
+ method: "DELETE",
+ allowMethods: "DELETE PUT",
+ },
+ { pass: 0,
+ method: "DELETE",
+ allowMethods: "DELETE, PUT Z",
+ },
+ { pass: 0,
+ method: "DELETE",
+ allowMethods: "DELETE, PU(T",
+ },
+ { pass: 0,
+ method: "DELETE",
+ allowMethods: "PUT DELETE",
+ },
+ { pass: 0,
+ method: "DELETE",
+ allowMethods: "PUT Z, DELETE",
+ },
+ { pass: 0,
+ method: "DELETE",
+ allowMethods: "PU(T, DELETE",
+ },
+ { pass: 0,
+ method: "MYMETHOD",
+ allowMethods: "myMethod",
+ },
+ { pass: 0,
+ method: "PUT",
+ allowMethods: "put",
+ },
+
+ // Progress events
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain" },
+ uploadProgress: "progress",
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain" },
+ uploadProgress: "progress",
+ noAllowPreflight: 1,
+ },
+
+ // Status messages
+ { pass: 1,
+ method: "GET",
+ noAllowPreflight: 1,
+ status: 404,
+ statusMessage: "nothin' here",
+ },
+ { pass: 1,
+ method: "GET",
+ noAllowPreflight: 1,
+ status: 401,
+ statusMessage: "no can do",
+ },
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "foo/bar" },
+ allowHeaders: "content-type",
+ status: 500,
+ statusMessage: "server boo",
+ },
+ { pass: 1,
+ method: "GET",
+ noAllowPreflight: 1,
+ status: 200,
+ statusMessage: "Yes!!",
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "header value" },
+ allowHeaders: "x-my-header",
+ preflightStatus: 400
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "header value" },
+ allowHeaders: "x-my-header",
+ preflightStatus: 200
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "header value" },
+ allowHeaders: "x-my-header",
+ preflightStatus: 204
+ },
+
+ // exposed headers
+ { pass: 1,
+ method: "GET",
+ responseHeaders: { "x-my-header": "x header" },
+ exposeHeaders: "x-my-header",
+ expectedResponseHeaders: ["x-my-header"],
+ },
+ { pass: 0,
+ method: "GET",
+ origin: "http://invalid",
+ responseHeaders: { "x-my-header": "x header" },
+ exposeHeaders: "x-my-header",
+ expectedResponseHeaders: [],
+ },
+ { pass: 1,
+ method: "GET",
+ responseHeaders: { "x-my-header": "x header" },
+ expectedResponseHeaders: [],
+ },
+ { pass: 1,
+ method: "GET",
+ responseHeaders: { "x-my-header": "x header" },
+ exposeHeaders: "x-my-header y",
+ expectedResponseHeaders: [],
+ },
+ { pass: 1,
+ method: "GET",
+ responseHeaders: { "x-my-header": "x header" },
+ exposeHeaders: "y x-my-header",
+ expectedResponseHeaders: [],
+ },
+ { pass: 1,
+ method: "GET",
+ responseHeaders: { "x-my-header": "x header" },
+ exposeHeaders: "x-my-header, y-my-header z",
+ expectedResponseHeaders: [],
+ },
+ { pass: 1,
+ method: "GET",
+ responseHeaders: { "x-my-header": "x header" },
+ exposeHeaders: "x-my-header, y-my-hea(er",
+ expectedResponseHeaders: [],
+ },
+ { pass: 1,
+ method: "GET",
+ responseHeaders: { "x-my-header": "x header",
+ "y-my-header": "y header" },
+ exposeHeaders: " , ,,y-my-header,z-my-header, ",
+ expectedResponseHeaders: ["y-my-header"],
+ },
+ { pass: 1,
+ method: "GET",
+ responseHeaders: { "Cache-Control": "cacheControl header",
+ "Content-Language": "contentLanguage header",
+ "Expires":"expires header",
+ "Last-Modified":"lastModified header",
+ "Pragma":"pragma header",
+ "Unexpected":"unexpected header" },
+ expectedResponseHeaders: ["Cache-Control","Content-Language","Content-Type","Expires","Last-Modified","Pragma"],
+ },
+ // Check that sending a body in the OPTIONS response works
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: "DELETE",
+ preflightBody: "I'm a preflight response body",
+ },
+ ];
+
+ if (!runPreflightTests) {
+ tests = [];
+ }
+
+ for (test of tests) {
+ var req = {
+ url: baseURL + "allowOrigin=" + escape(test.origin || origin),
+ method: test.method,
+ headers: test.headers,
+ uploadProgress: test.uploadProgress,
+ body: test.body,
+ responseHeaders: test.responseHeaders,
+ withCred: test.withCred ? test.withCred : 0,
+ };
+
+ if (test.pass) {
+ req.url += "&origin=" + escape(origin) +
+ "&requestMethod=" + test.method;
+ }
+
+ if ("username" in test) {
+ req.username = test.username;
+ }
+
+ if ("password" in test) {
+ req.password = test.password;
+ }
+
+ if (test.noAllowPreflight)
+ req.url += "&noAllowPreflight";
+
+ if (test.allowCred)
+ req.url += "&allowCred";
+
+ if (test.pass && "headers" in test) {
+ function isUnsafeHeader(name) {
+ lName = name.toLowerCase();
+ return lName != "accept" &&
+ lName != "accept-language" &&
+ (lName != "content-type" ||
+ !["text/plain",
+ "multipart/form-data",
+ "application/x-www-form-urlencoded"]
+ .includes(test.headers[name].toLowerCase()));
+ }
+ req.url += "&headers=" + escape(JSON.stringify(test.headers));
+ reqHeaders =
+ escape(Object.keys(test.headers)
+ .filter(isUnsafeHeader)
+ .map(s => s.toLowerCase())
+ .sort()
+ .join(","));
+ req.url += reqHeaders ? "&requestHeaders=" + reqHeaders : "";
+ }
+ if ("allowHeaders" in test)
+ req.url += "&allowHeaders=" + escape(test.allowHeaders);
+ if ("allowMethods" in test)
+ req.url += "&allowMethods=" + escape(test.allowMethods);
+ if (test.body)
+ req.url += "&body=" + escape(test.body);
+ if (test.status) {
+ req.url += "&status=" + test.status;
+ req.url += "&statusMessage=" + escape(test.statusMessage);
+ }
+ if (test.preflightStatus)
+ req.url += "&preflightStatus=" + test.preflightStatus;
+ if (test.responseHeaders)
+ req.url += "&responseHeaders=" + escape(JSON.stringify(test.responseHeaders));
+ if (test.exposeHeaders)
+ req.url += "&exposeHeaders=" + escape(test.exposeHeaders);
+ if (test.preflightBody)
+ req.url += "&preflightBody=" + escape(test.preflightBody);
+
+ loaderWindow.postMessage(JSON.stringify(req), origin);
+ res = JSON.parse(yield);
+
+ if (test.pass) {
+ is(res.didFail, false,
+ "shouldn't have failed in test for " + JSON.stringify(test));
+ if (test.status) {
+ is(res.status, test.status, "wrong status in test for " + JSON.stringify(test));
+ is(res.statusText, test.statusMessage, "wrong status text for " + JSON.stringify(test));
+ }
+ else {
+ is(res.status, 200, "wrong status in test for " + JSON.stringify(test));
+ is(res.statusText, "OK", "wrong status text for " + JSON.stringify(test));
+ }
+ if (test.method !== "HEAD") {
+ is(res.responseXML, "<res>hello pass</res>",
+ "wrong responseXML in test for " + JSON.stringify(test));
+ is(res.responseText, "<res>hello pass</res>\n",
+ "wrong responseText in test for " + JSON.stringify(test));
+ is(res.events.join(","),
+ "opening,rs1,sending,loadstart,rs2,rs3,rs4,load,loadend",
+ "wrong responseText in test for " + JSON.stringify(test));
+ }
+ else {
+ is(res.responseXML, null,
+ "wrong responseXML in test for " + JSON.stringify(test));
+ is(res.responseText, "",
+ "wrong responseText in test for " + JSON.stringify(test));
+ is(res.events.join(","),
+ "opening,rs1,sending,loadstart,rs2,rs4,load,loadend",
+ "wrong responseText in test for " + JSON.stringify(test));
+ }
+ if (test.responseHeaders) {
+ for (header in test.responseHeaders) {
+ if (!test.expectedResponseHeaders.includes(header)) {
+ is(res.responseHeaders[header], null,
+ "|xhr.getResponseHeader()|wrong response header (" + header + ") in test for " +
+ JSON.stringify(test));
+ is(res.allResponseHeaders[header], undefined,
+ "|xhr.getAllResponseHeaderss()|wrong response header (" + header + ") in test for " +
+ JSON.stringify(test));
+ }
+ else {
+ is(res.responseHeaders[header], test.responseHeaders[header],
+ "|xhr.getResponseHeader()|wrong response header (" + header + ") in test for " +
+ JSON.stringify(test));
+ is(res.allResponseHeaders[header.toLowerCase()], test.responseHeaders[header],
+ "|xhr.getAllResponseHeaderss()|wrong response header (" + header + ") in test for " +
+ JSON.stringify(test));
+ }
+ }
+ }
+ }
+ else {
+ is(res.didFail, true,
+ "should have failed in test for " + JSON.stringify(test));
+ is(res.status, 0, "wrong status in test for " + JSON.stringify(test));
+ is(res.statusText, "", "wrong status text for " + JSON.stringify(test));
+ is(res.responseXML, null,
+ "wrong responseXML in test for " + JSON.stringify(test));
+ is(res.responseText, "",
+ "wrong responseText in test for " + JSON.stringify(test));
+ if (!res.sendThrew) {
+ is(res.events.join(","),
+ "opening,rs1,sending,loadstart,rs4,error,loadend",
+ "wrong events in test for " + JSON.stringify(test));
+ }
+ is(res.progressEvents, 0,
+ "wrong events in test for " + JSON.stringify(test));
+ if (test.responseHeaders) {
+ for (header in test.responseHeaders) {
+ is(res.responseHeaders[header], null,
+ "wrong response header (" + header + ") in test for " +
+ JSON.stringify(test));
+ }
+ }
+ }
+ }
+
+ // Test cookie behavior
+ tests = [{ pass: 1,
+ method: "GET",
+ withCred: 1,
+ allowCred: 1,
+ },
+ { pass: 0,
+ method: "GET",
+ withCred: 1,
+ allowCred: 0,
+ },
+ { pass: 0,
+ method: "GET",
+ withCred: 1,
+ allowCred: 1,
+ origin: "*",
+ },
+ { pass: 1,
+ method: "GET",
+ withCred: 0,
+ allowCred: 1,
+ origin: "*",
+ },
+ { pass: 1,
+ method: "GET",
+ setCookie: "a=1",
+ withCred: 1,
+ allowCred: 1,
+ },
+ { pass: 1,
+ method: "GET",
+ cookie: "a=1",
+ withCred: 1,
+ allowCred: 1,
+ },
+ { pass: 1,
+ method: "GET",
+ noCookie: 1,
+ withCred: 0,
+ allowCred: 1,
+ },
+ { pass: 0,
+ method: "GET",
+ noCookie: 1,
+ withCred: 1,
+ allowCred: 1,
+ },
+ { pass: 1,
+ method: "GET",
+ setCookie: "a=2",
+ withCred: 0,
+ allowCred: 1,
+ },
+ { pass: 1,
+ method: "GET",
+ cookie: "a=1",
+ withCred: 1,
+ allowCred: 1,
+ },
+ { pass: 1,
+ method: "GET",
+ setCookie: "a=2",
+ withCred: 1,
+ allowCred: 1,
+ },
+ { pass: 1,
+ method: "GET",
+ cookie: "a=2",
+ withCred: 1,
+ allowCred: 1,
+ },
+ ];
+
+ if (!runCookieTests) {
+ tests = [];
+ }
+
+ for (test of tests) {
+ req = {
+ url: baseURL + "allowOrigin=" + escape(test.origin || origin),
+ method: test.method,
+ headers: test.headers,
+ withCred: test.withCred,
+ };
+
+ if (test.allowCred)
+ req.url += "&allowCred";
+
+ if (test.setCookie)
+ req.url += "&setCookie=" + escape(test.setCookie);
+ if (test.cookie)
+ req.url += "&cookie=" + escape(test.cookie);
+ if (test.noCookie)
+ req.url += "&noCookie";
+
+ if ("allowHeaders" in test)
+ req.url += "&allowHeaders=" + escape(test.allowHeaders);
+ if ("allowMethods" in test)
+ req.url += "&allowMethods=" + escape(test.allowMethods);
+
+ loaderWindow.postMessage(JSON.stringify(req), origin);
+
+ res = JSON.parse(yield);
+ if (test.pass) {
+ is(res.didFail, false,
+ "shouldn't have failed in test for " + JSON.stringify(test));
+ is(res.status, 200, "wrong status in test for " + JSON.stringify(test));
+ is(res.statusText, "OK", "wrong status text for " + JSON.stringify(test));
+ is(res.responseXML, "<res>hello pass</res>",
+ "wrong responseXML in test for " + JSON.stringify(test));
+ is(res.responseText, "<res>hello pass</res>\n",
+ "wrong responseText in test for " + JSON.stringify(test));
+ is(res.events.join(","),
+ "opening,rs1,sending,loadstart,rs2,rs3,rs4,load,loadend",
+ "wrong responseText in test for " + JSON.stringify(test));
+ }
+ else {
+ is(res.didFail, true,
+ "should have failed in test for " + JSON.stringify(test));
+ is(res.status, 0, "wrong status in test for " + JSON.stringify(test));
+ is(res.statusText, "", "wrong status text for " + JSON.stringify(test));
+ is(res.responseXML, null,
+ "wrong responseXML in test for " + JSON.stringify(test));
+ is(res.responseText, "",
+ "wrong responseText in test for " + JSON.stringify(test));
+ is(res.events.join(","),
+ "opening,rs1,sending,loadstart,rs4,error,loadend",
+ "wrong events in test for " + JSON.stringify(test));
+ is(res.progressEvents, 0,
+ "wrong events in test for " + JSON.stringify(test));
+ }
+ }
+
+ // Make sure to clear cookies to avoid affecting other tests
+ document.cookie = "a=; path=/; expires=Thu, 01-Jan-1970 00:00:01 GMT"
+ is(document.cookie, "", "No cookies should be left over");
+
+
+ // Test redirects
+ is(loader.src, "http://example.org/tests/dom/security/test/cors/file_CrossSiteXHR_inner.html");
+ is(origin, "http://example.org");
+
+ tests = [{ pass: 1,
+ method: "GET",
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin
+ },
+ ],
+ },
+ { pass: 0,
+ method: "GET",
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin
+ },
+ { server: "http://example.org",
+ allowOrigin: origin
+ },
+ ],
+ },
+ { pass: 1,
+ method: "GET",
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin
+ },
+ { server: "http://example.org",
+ allowOrigin: "*"
+ },
+ ],
+ },
+ { pass: 0,
+ method: "GET",
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin
+ },
+ { server: "http://example.org",
+ },
+ ],
+ },
+ { pass: 1,
+ method: "GET",
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin
+ },
+ ],
+ },
+ { pass: 0,
+ method: "GET",
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin
+ },
+ { server: "http://example.org",
+ },
+ ],
+ },
+ { pass: 0,
+ method: "GET",
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin
+ },
+ { server: "http://test2.example.org:8000",
+ allowOrigin: origin
+ },
+ { server: "http://sub2.xn--lt-uia.example.org",
+ allowOrigin: origin
+ },
+ { server: "http://sub1.test1.example.org",
+ allowOrigin: origin
+ },
+ ],
+ },
+ { pass: 0,
+ method: "GET",
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin
+ },
+ { server: "http://test2.example.org:8000",
+ allowOrigin: origin
+ },
+ { server: "http://sub2.xn--lt-uia.example.org",
+ allowOrigin: "*"
+ },
+ { server: "http://sub1.test1.example.org",
+ allowOrigin: "*"
+ },
+ ],
+ },
+ { pass: 1,
+ method: "GET",
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin
+ },
+ { server: "http://test2.example.org:8000",
+ allowOrigin: "*"
+ },
+ { server: "http://sub2.xn--lt-uia.example.org",
+ allowOrigin: "*"
+ },
+ { server: "http://sub1.test1.example.org",
+ allowOrigin: "*"
+ },
+ ],
+ },
+ { pass: 0,
+ method: "GET",
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin
+ },
+ { server: "http://test2.example.org:8000",
+ allowOrigin: origin
+ },
+ { server: "http://sub2.xn--lt-uia.example.org",
+ allowOrigin: "x"
+ },
+ { server: "http://sub1.test1.example.org",
+ allowOrigin: origin
+ },
+ ],
+ },
+ { pass: 0,
+ method: "GET",
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin
+ },
+ { server: "http://test2.example.org:8000",
+ allowOrigin: origin
+ },
+ { server: "http://sub2.xn--lt-uia.example.org",
+ allowOrigin: "*"
+ },
+ { server: "http://sub1.test1.example.org",
+ allowOrigin: origin
+ },
+ ],
+ },
+ { pass: 0,
+ method: "GET",
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin
+ },
+ { server: "http://test2.example.org:8000",
+ allowOrigin: origin
+ },
+ { server: "http://sub2.xn--lt-uia.example.org",
+ allowOrigin: "*"
+ },
+ { server: "http://sub1.test1.example.org",
+ },
+ ],
+ },
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain" },
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ },
+ ],
+ },
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain",
+ "my-header": "myValue",
+ },
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowHeaders: "my-header",
+ },
+ ],
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain",
+ "my-header": "myValue",
+ },
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowHeaders: "my-header",
+ },
+ { server: "http://sub1.test1.example.org",
+ allowOrigin: origin,
+ allowHeaders: "my-header",
+ },
+ ],
+ },
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain",
+ "my-header": "myValue",
+ },
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowHeaders: "my-header",
+ },
+ { server: "http://sub1.test1.example.org",
+ allowOrigin: "*",
+ allowHeaders: "my-header",
+ },
+ ],
+ },
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain",
+ "my-header": "myValue",
+ },
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowHeaders: "my-header",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowHeaders: "my-header",
+ },
+ ],
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain",
+ "my-header": "myValue",
+ },
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowHeaders: "my-header",
+ },
+ { server: "http://example.org",
+ allowOrigin: origin,
+ allowHeaders: "my-header",
+ },
+ ],
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain",
+ "my-header": "myValue",
+ },
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ noAllowPreflight: 1,
+ },
+ ],
+ },
+ { pass: 1,
+ method: "DELETE",
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowMethods: "DELETE",
+ },
+ ],
+ },
+ { pass: 0,
+ method: "DELETE",
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowMethods: "DELETE",
+ },
+ { server: "http://sub1.test1.example.org",
+ allowOrigin: origin,
+ allowMethods: "DELETE",
+ },
+ ],
+ },
+ { pass: 1,
+ method: "DELETE",
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowMethods: "DELETE",
+ },
+ { server: "http://sub1.test1.example.org",
+ allowOrigin: "*",
+ allowMethods: "DELETE",
+ },
+ ],
+ },
+ { pass: 1,
+ method: "DELETE",
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowMethods: "DELETE",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowMethods: "DELETE",
+ },
+ ],
+ },
+ { pass: 0,
+ method: "DELETE",
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowMethods: "DELETE",
+ },
+ { server: "http://example.org",
+ allowOrigin: origin,
+ allowMethods: "DELETE",
+ },
+ ],
+ },
+ { pass: 0,
+ method: "DELETE",
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowMethods: "DELETE",
+ noAllowPreflight: 1,
+ },
+ ],
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain",
+ "my-header": "myValue",
+ },
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin,
+ },
+ { server: "http://sub1.test1.example.org",
+ allowOrigin: origin,
+ },
+ ],
+ },
+ { pass: 0,
+ method: "DELETE",
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin,
+ },
+ { server: "http://sub1.test1.example.org",
+ allowOrigin: origin,
+ },
+ ],
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain",
+ "my-header": "myValue",
+ },
+ hops: [{ server: "http://example.com",
+ },
+ { server: "http://sub1.test1.example.org",
+ allowOrigin: origin,
+ allowHeaders: "my-header",
+ },
+ ],
+ },
+ { pass: 1,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain" },
+ hops: [{ server: "http://example.org",
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ },
+ ],
+ },
+ { pass: 0,
+ method: "POST",
+ body: "hi there",
+ headers: { "Content-Type": "text/plain",
+ "my-header": "myValue",
+ },
+ hops: [{ server: "http://example.com",
+ allowOrigin: origin,
+ allowHeaders: "my-header",
+ },
+ { server: "http://example.org",
+ allowOrigin: origin,
+ allowHeaders: "my-header",
+ },
+ ],
+ },
+
+ // test redirects with different credentials settings
+ {
+ // Initialize by setting a cookies for same- and cross- origins.
+ pass: 1,
+ method: "GET",
+ hops: [{ server: origin,
+ setCookie: escape("a=1"),
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowCred: 1,
+ setCookie: escape("a=2"),
+ },
+ ],
+ withCred: 1,
+ },
+ { pass: 1,
+ method: "GET",
+ hops: [{ server: origin,
+ cookie: escape("a=1"),
+ },
+ { server: origin,
+ cookie: escape("a=1"),
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ noCookie: 1,
+ },
+ ],
+ withCred: 0,
+ },
+ { pass: 1,
+ method: "GET",
+ hops: [{ server: origin,
+ cookie: escape("a=1"),
+ },
+ { server: origin,
+ cookie: escape("a=1"),
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ allowCred: 1,
+ cookie: escape("a=2"),
+ },
+ ],
+ withCred: 1,
+ },
+ // expected fail because allow-credentials CORS header is not set
+ { pass: 0,
+ method: "GET",
+ hops: [{ server: origin,
+ cookie: escape("a=1"),
+ },
+ { server: origin,
+ cookie: escape("a=1"),
+ },
+ { server: "http://example.com",
+ allowOrigin: origin,
+ cookie: escape("a=2"),
+ },
+ ],
+ withCred: 1,
+ },
+ { pass: 1,
+ method: "GET",
+ hops: [{ server: origin,
+ cookie: escape("a=1"),
+ },
+ { server: origin,
+ cookie: escape("a=1"),
+ },
+ { server: "http://example.com",
+ allowOrigin: '*',
+ noCookie: 1,
+ },
+ ],
+ withCred: 0,
+ },
+ { pass: 0,
+ method: "GET",
+ hops: [{ server: origin,
+ cookie: escape("a=1"),
+ },
+ { server: origin,
+ cookie: escape("a=1"),
+ },
+ { server: "http://example.com",
+ allowOrigin: '*',
+ allowCred: 1,
+ cookie: escape("a=2"),
+ },
+ ],
+ withCred: 1,
+ },
+ ];
+
+ if (!runRedirectTests) {
+ tests = [];
+ }
+
+ for (test of tests) {
+ req = {
+ url: test.hops[0].server + basePath + "hop=1&hops=" +
+ escape(JSON.stringify(test.hops)),
+ method: test.method,
+ headers: test.headers,
+ body: test.body,
+ withCred: test.withCred,
+ };
+
+ if (test.pass) {
+ if (test.body)
+ req.url += "&body=" + escape(test.body);
+ }
+
+ loaderWindow.postMessage(JSON.stringify(req), origin);
+
+ res = JSON.parse(yield);
+ if (test.pass) {
+ is(res.didFail, false,
+ "shouldn't have failed in test for " + JSON.stringify(test));
+ is(res.status, 200, "wrong status in test for " + JSON.stringify(test));
+ is(res.statusText, "OK", "wrong status text for " + JSON.stringify(test));
+ is(res.responseXML, "<res>hello pass</res>",
+ "wrong responseXML in test for " + JSON.stringify(test));
+ is(res.responseText, "<res>hello pass</res>\n",
+ "wrong responseText in test for " + JSON.stringify(test));
+ is(res.events.join(","),
+ "opening,rs1,sending,loadstart,rs2,rs3,rs4,load,loadend",
+ "wrong responseText in test for " + JSON.stringify(test));
+ }
+ else {
+ is(res.didFail, true,
+ "should have failed in test for " + JSON.stringify(test));
+ is(res.status, 0, "wrong status in test for " + JSON.stringify(test));
+ is(res.statusText, "", "wrong status text for " + JSON.stringify(test));
+ is(res.responseXML, null,
+ "wrong responseXML in test for " + JSON.stringify(test));
+ is(res.responseText, "",
+ "wrong responseText in test for " + JSON.stringify(test));
+ is(res.events.join(","),
+ "opening,rs1,sending,loadstart,rs4,error,loadend",
+ "wrong events in test for " + JSON.stringify(test));
+ is(res.progressEvents, 0,
+ "wrong progressevents in test for " + JSON.stringify(test));
+ }
+ }
+
+
+ SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault");
+ SpecialPowers.clearUserPref("browser.contentblocking.category");
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/cors/test_CrossSiteXHR_cache.html b/dom/security/test/cors/test_CrossSiteXHR_cache.html
new file mode 100644
index 0000000000..77898e38ed
--- /dev/null
+++ b/dom/security/test/cors/test_CrossSiteXHR_cache.html
@@ -0,0 +1,610 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8">
+ <title>Test for Cross Site XMLHttpRequest</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="gen.next()">
+<p id="display">
+<iframe id=loader></iframe>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="application/javascript">
+
+let gen;
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("This test needs to generate artificial pauses, hence it uses timeouts. There is no way around it, unfortunately. :(");
+
+window.addEventListener("message", function(e) {
+ gen.next(e.data);
+});
+
+gen = runTest();
+
+function* runTest() {
+ var loader = document.getElementById('loader');
+ var loaderWindow = loader.contentWindow;
+ loader.onload = function () { gen.next() };
+
+ loader.src = "http://example.org/tests/dom/security/test/cors/file_CrossSiteXHR_inner.html";
+ origin = "http://example.org";
+ yield undefined;
+
+ tests = [{ pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "x-my-header",
+ cacheTime: 3600
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "myValue",
+ "y-my-header": "second" },
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "y-my-header": "hello" },
+ allowHeaders: "y-my-header",
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "y-my-header": "hello" },
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "y-my-header": "hello" },
+ allowHeaders: "y-my-header,x-my-header",
+ cacheTime: 3600,
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "myValue",
+ "y-my-header": "second" },
+ },
+ { newTest: "*******" },
+ { pass: 1,
+ method: "GET",
+ headers: { "y-my-header": "hello" },
+ allowHeaders: "y-my-header,x-my-header",
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "y-my-header": "hello" },
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue",
+ "y-my-header": "second" },
+ },
+ { newTest: "*******" },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "x-my-header",
+ cacheTime: 2
+ },
+ { pause: 2.1 },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ },
+ { newTest: "*******" },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "x-my-header, y-my-header",
+ cacheTime: 3600
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "y-my-header": "myValue" },
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "z-my-header": "myValue" },
+ },
+ { newTest: "*******" },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "x-my-header",
+ cacheTime: "\t 3600 \t ",
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ },
+ { newTest: "*******" },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "x-my-header",
+ cacheTime: "3600 3",
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ },
+ { newTest: "*******" },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "x-my-header",
+ cacheTime: "asdf",
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ },
+ { newTest: "*******" },
+ { pass: 1,
+ method: "GET",
+ headers: { "first-header": "myValue" },
+ allowHeaders: "first-header",
+ cacheTime: 2,
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "second-header": "myValue" },
+ allowHeaders: "second-header",
+ cacheTime: 3600,
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "third-header": "myValue" },
+ allowHeaders: "third-header",
+ cacheTime: 2,
+ },
+ { pause: 2.1 },
+ { pass: 1,
+ method: "GET",
+ headers: { "second-header": "myValue" },
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "first-header": "myValue" },
+ },
+ { newTest: "*******" },
+ { pass: 1,
+ method: "GET",
+ headers: { "first-header": "myValue" },
+ allowHeaders: "first-header",
+ cacheTime: 2,
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "second-header": "myValue" },
+ allowHeaders: "second-header",
+ cacheTime: 3600,
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "third-header": "myValue" },
+ allowHeaders: "third-header",
+ cacheTime: 2,
+ },
+ { pause: 2.1 },
+ { pass: 1,
+ method: "GET",
+ headers: { "second-header": "myValue" },
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "third-header": "myValue" },
+ },
+ { newTest: "*******" },
+ { pass: 0,
+ method: "DELETE",
+ },
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: "DELETE",
+ cacheTime: 3600
+ },
+ { pass: 1,
+ method: "DELETE",
+ },
+ { pass: 1,
+ method: "DELETE",
+ },
+ { pass: 0,
+ method: "PATCH",
+ },
+ { pass: 1,
+ method: "PATCH",
+ allowMethods: "PATCH",
+ },
+ { pass: 1,
+ method: "PATCH",
+ },
+ { pass: 1,
+ method: "PATCH",
+ allowMethods: "PATCH",
+ cacheTime: 3600,
+ },
+ { pass: 1,
+ method: "PATCH",
+ },
+ { pass: 0,
+ method: "DELETE",
+ },
+ { pass: 0,
+ method: "PUT",
+ },
+ { newTest: "*******" },
+ { pass: 1,
+ method: "PATCH",
+ allowMethods: "PATCH",
+ cacheTime: 3600,
+ },
+ { pass: 1,
+ method: "PATCH",
+ },
+ { newTest: "*******" },
+ { pass: 0,
+ method: "DELETE",
+ },
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: "DELETE",
+ cacheTime: 2
+ },
+ { pause: 2.1 },
+ { pass: 0,
+ method: "DELETE",
+ },
+ { newTest: "*******" },
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: "DELETE, PUT",
+ cacheTime: 3600
+ },
+ { pass: 1,
+ method: "DELETE",
+ },
+ { pass: 1,
+ method: "PUT",
+ },
+ { pass: 0,
+ method: "PATCH",
+ },
+ { newTest: "*******" },
+ { pass: 1,
+ method: "FIRST",
+ allowMethods: "FIRST",
+ cacheTime: 2,
+ },
+ { pass: 1,
+ method: "SECOND",
+ allowMethods: "SECOND",
+ cacheTime: 3600,
+ },
+ { pass: 1,
+ method: "THIRD",
+ allowMethods: "THIRD",
+ cacheTime: 2,
+ },
+ { pause: 2.1 },
+ { pass: 1,
+ method: "SECOND",
+ },
+ { pass: 0,
+ method: "FIRST",
+ },
+ { newTest: "*******" },
+ { pass: 1,
+ method: "FIRST",
+ allowMethods: "FIRST",
+ cacheTime: 2,
+ },
+ { pass: 1,
+ method: "SECOND",
+ allowMethods: "SECOND",
+ cacheTime: 3600,
+ },
+ { pass: 1,
+ method: "THIRD",
+ allowMethods: "THIRD",
+ cacheTime: 2,
+ },
+ { pause: 2.1 },
+ { pass: 1,
+ method: "SECOND",
+ },
+ { pass: 0,
+ method: "THIRD",
+ },
+ { newTest: "*******" },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "x-value" },
+ allowHeaders: "x-my-header",
+ cacheTime: 3600,
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "x-value" }
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "y-my-header": "y-value" }
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "x-value" }
+ },
+ { newTest: "*******" },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "x-value" },
+ allowHeaders: "x-my-header",
+ cacheTime: 3600,
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "x-value" },
+ },
+ { pass: 0,
+ method: "PUT",
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "x-value" },
+ },
+ { newTest: "*******" },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "x-value" },
+ allowHeaders: "x-my-header",
+ cacheTime: 3600,
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "x-value" },
+ },
+ { pass: 0,
+ method: "GET",
+ noOrigin: 1,
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "x-value" },
+ },
+ { newTest: "*******" },
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: "DELETE",
+ cacheTime: 3600,
+ },
+ { pass: 1,
+ method: "DELETE"
+ },
+ { pass: 0,
+ method: "PUT"
+ },
+ { pass: 0,
+ method: "DELETE"
+ },
+ { newTest: "*******" },
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: "DELETE",
+ cacheTime: 3600,
+ },
+ { pass: 1,
+ method: "DELETE"
+ },
+ { pass: 0,
+ method: "DELETE",
+ headers: { "my-header": "value" },
+ },
+ { pass: 0,
+ method: "DELETE"
+ },
+ { newTest: "*******" },
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: "DELETE",
+ cacheTime: 3600,
+ },
+ { pass: 1,
+ method: "DELETE"
+ },
+ { pass: 0,
+ method: "GET",
+ noOrigin: 1,
+ },
+ { pass: 0,
+ method: "DELETE"
+ },
+ { newTest: "*******" },
+ { pass: 1,
+ method: "GET",
+ withCred: true,
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "x-my-header",
+ cacheTime: 3600
+ },
+ { pass: 1,
+ method: "GET",
+ withCred: true,
+ headers: { "x-my-header": "myValue" },
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ },
+ { newTest: "*******" },
+ { pass: 1,
+ method: "GET",
+ withCred: true,
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "x-my-header",
+ cacheTime: 3600
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "y-my-header": "myValue" },
+ allowHeaders: "y-my-header",
+ cacheTime: 2
+ },
+ { pass: 1,
+ method: "GET",
+ headers: { "y-my-header": "myValue" },
+ },
+ { pass: 1,
+ method: "GET",
+ withCred: true,
+ headers: { "x-my-header": "myValue" },
+ },
+ { pause: 2.1 },
+ { pass: 1,
+ method: "GET",
+ withCred: true,
+ headers: { "x-my-header": "myValue" },
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "y-my-header": "myValue" },
+ },
+ { pass: 0,
+ method: "GET",
+ withCred: true,
+ headers: { "y-my-header": "myValue" },
+ },
+ { newTest: "*******" },
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: "DELETE",
+ cacheTime: 3600
+ },
+ { pass: 0,
+ method: "GET",
+ headers: { "DELETE": "myvalue" },
+ },
+ { newTest: "*******" },
+ { pass: 1,
+ method: "GET",
+ headers: { "x-my-header": "myValue" },
+ allowHeaders: "x-my-header",
+ cacheTime: 3600
+ },
+ { pass: 0,
+ method: "3600",
+ headers: { "x-my-header": "myvalue" },
+ },
+ ];
+
+ for (let i = 0; i < 110; i++) {
+ tests.push({ newTest: "*******" },
+ { pass: 1,
+ method: "DELETE",
+ allowMethods: "DELETE",
+ cacheTime: 3600,
+ });
+ }
+
+ baseURL = "http://mochi.test:8888/tests/dom/security/test/cors/" +
+ "file_CrossSiteXHR_cache_server.sjs?";
+ setStateURL = baseURL + "setState=";
+
+ var unique = Date.now();
+ for (test of tests) {
+ if (test.newTest) {
+ unique++;
+ continue;
+ }
+ if (test.pause) {
+ setTimeout(function() { gen.next() }, test.pause * 1000);
+ yield undefined;
+ continue;
+ }
+
+ req = {
+ url: baseURL + "c=" + unique,
+ method: test.method,
+ headers: test.headers,
+ withCred: test.withCred,
+ };
+
+ sec = { allowOrigin: test.noOrigin ? "" : origin,
+ allowHeaders: test.allowHeaders,
+ allowMethods: test.allowMethods,
+ cacheTime: test.cacheTime,
+ withCred: test.withCred };
+ xhr = new XMLHttpRequest();
+ xhr.open("POST", setStateURL + escape(JSON.stringify(sec)), true);
+ xhr.onloadend = function() { gen.next(); }
+ xhr.send();
+ yield undefined;
+
+ loaderWindow.postMessage(JSON.stringify(req), origin);
+
+ res = JSON.parse(yield);
+
+ testName = JSON.stringify(test) + " (index " + tests.indexOf(test) + ")";
+
+ if (test.pass) {
+ is(res.didFail, false,
+ "shouldn't have failed in test for " + testName);
+ is(res.status, 200, "wrong status in test for " + testName);
+ is(res.responseXML, "<res>hello pass</res>",
+ "wrong responseXML in test for " + testName);
+ is(res.responseText, "<res>hello pass</res>\n",
+ "wrong responseText in test for " + testName);
+ is(res.events.join(","),
+ "opening,rs1,sending,loadstart,rs2,rs3,rs4,load,loadend",
+ "wrong events in test for " + testName);
+ }
+ else {
+ is(res.didFail, true,
+ "should have failed in test for " + testName);
+ is(res.status, 0, "wrong status in test for " + testName);
+ is(res.responseXML, null,
+ "wrong responseXML in test for " + testName);
+ is(res.responseText, "",
+ "wrong responseText in test for " + testName);
+ is(res.events.join(","),
+ "opening,rs1,sending,loadstart,rs4,error,loadend",
+ "wrong events in test for " + testName);
+ is(res.progressEvents, 0,
+ "wrong events in test for " + testName);
+ }
+ }
+
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/cors/test_CrossSiteXHR_origin.html b/dom/security/test/cors/test_CrossSiteXHR_origin.html
new file mode 100644
index 0000000000..ba4a645965
--- /dev/null
+++ b/dom/security/test/cors/test_CrossSiteXHR_origin.html
@@ -0,0 +1,180 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8">
+ <title>Test for Cross Site XMLHttpRequest</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display">
+<iframe id=loader></iframe>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(2);
+
+var origins =
+ [{ server: 'http://example.org' },
+ { server: 'http://example.org:80',
+ origin: 'http://example.org'
+ },
+ { server: 'http://sub1.test1.example.org' },
+ { server: 'http://test2.example.org:8000' },
+ { server: 'http://sub1.\xe4lt.example.org:8000',
+ origin: 'http://sub1.xn--lt-uia.example.org:8000'
+ },
+ { server: 'http://sub2.\xe4lt.example.org',
+ origin: 'http://sub2.xn--lt-uia.example.org'
+ },
+ { server: 'http://ex\xe4mple.test',
+ origin: 'http://xn--exmple-cua.test'
+ },
+ { server: 'http://xn--exmple-cua.test' },
+ { server: 'http://\u03c0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1.\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae',
+ origin: 'http://xn--hxajbheg2az3al.xn--jxalpdlp'
+ },
+ { origin: 'null',
+ file: 'http://example.org/tests/dom/security/test/cors/file_CrossSiteXHR_inner_data.sjs'
+ },
+ ];
+
+ //['https://example.com:443'],
+ //['https://sub1.test1.example.com:443'],
+
+
+function initTest() {
+ // Allow all cookies, then do the actual test initialization
+ SpecialPowers.pushPrefEnv({
+ "set": [
+ // Some of this test relies on redirecting to data: URLs from http.
+ ["network.allow_redirect_to_data", true],
+ ]
+ }).then(initTestCallback);
+}
+
+function initTestCallback() {
+ window.addEventListener("message", function(e) {
+ gen.next(e.data);
+ });
+
+ gen = runTest();
+ gen.next();
+}
+
+function* runTest() {
+ var loader = document.getElementById('loader');
+ var loaderWindow = loader.contentWindow;
+ loader.onload = function () { gen.next() };
+
+ // Test preflight-less requests
+ basePath = "/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?"
+ baseURL = "http://mochi.test:8888" + basePath;
+
+ for (originEntry of origins) {
+ origin = originEntry.origin || originEntry.server;
+
+ loader.src = originEntry.file ||
+ (originEntry.server + "/tests/dom/security/test/cors/file_CrossSiteXHR_inner.html");
+ yield undefined;
+
+ var isNullOrigin = origin == "null";
+
+ port = /:\d+/;
+ passTests = [
+ origin,
+ "*",
+ " \t " + origin + "\t \t",
+ "\t \t* \t ",
+ ];
+ failTests = [
+ "",
+ " ",
+ port.test(origin) ? origin.replace(port, "")
+ : origin + ":1234",
+ port.test(origin) ? origin.replace(port, ":")
+ : origin + ":",
+ origin + ".",
+ origin + "/",
+ origin + "#",
+ origin + "?",
+ origin + "\\",
+ origin + "%",
+ origin + "@",
+ origin + "/hello",
+ "foo:bar@" + origin,
+ "* " + origin,
+ origin + " " + origin,
+ "allow <" + origin + ">",
+ "<" + origin + ">",
+ "<*>",
+ origin.substr(0, 5) == "https" ? origin.replace("https", "http")
+ : origin.replace("http", "https"),
+ origin.replace("://", "://www."),
+ origin.replace("://", ":// "),
+ origin.replace(/\/[^.]+\./, "/"),
+ ];
+
+ if (isNullOrigin) {
+ passTests = ["*", "\t \t* \t ", "null"];
+ failTests = failTests.filter(function(v) { return v != origin });
+ }
+
+ for (allowOrigin of passTests) {
+ req = {
+ url: baseURL +
+ "allowOrigin=" + escape(allowOrigin) +
+ "&origin=" + escape(origin),
+ method: "GET",
+ };
+ loaderWindow.postMessage(JSON.stringify(req), isNullOrigin ? "*" : origin);
+
+ res = JSON.parse(yield);
+ is(res.didFail, false, "shouldn't have failed for " + allowOrigin);
+ is(res.status, 200, "wrong status for " + allowOrigin);
+ is(res.statusText, "OK", "wrong status text for " + allowOrigin);
+ is(res.responseXML,
+ "<res>hello pass</res>",
+ "wrong responseXML in test for " + allowOrigin);
+ is(res.responseText, "<res>hello pass</res>\n",
+ "wrong responseText in test for " + allowOrigin);
+ is(res.events.join(","),
+ "opening,rs1,sending,loadstart,rs2,rs3,rs4,load,loadend",
+ "wrong responseText in test for " + allowOrigin);
+ }
+
+ for (allowOrigin of failTests) {
+ req = {
+ url: baseURL + "allowOrigin=" + escape(allowOrigin),
+ method: "GET",
+ };
+ loaderWindow.postMessage(JSON.stringify(req), isNullOrigin ? "*" : origin);
+
+ res = JSON.parse(yield);
+ is(res.didFail, true, "should have failed for " + allowOrigin);
+ is(res.responseText, "", "should have no text for " + allowOrigin);
+ is(res.status, 0, "should have no status for " + allowOrigin);
+ is(res.statusText, "", "wrong status text for " + allowOrigin);
+ is(res.responseXML, null, "should have no XML for " + allowOrigin);
+ is(res.events.join(","),
+ "opening,rs1,sending,loadstart,rs4,error,loadend",
+ "wrong events in test for " + allowOrigin);
+ is(res.progressEvents, 0,
+ "wrong events in test for " + allowOrigin);
+ }
+ }
+
+ SimpleTest.finish();
+}
+
+addLoadEvent(initTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/crashtests/1577572.html b/dom/security/test/crashtests/1577572.html
new file mode 100644
index 0000000000..732c7aa5dc
--- /dev/null
+++ b/dom/security/test/crashtests/1577572.html
@@ -0,0 +1,10 @@
+<html>
+<title>Bug 1577572</title>
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Security-Policy" content="&#xFF;">
+</head>
+<body>
+ Bug 1577572
+</body>
+</html>
diff --git a/dom/security/test/crashtests/1583044.html b/dom/security/test/crashtests/1583044.html
new file mode 100644
index 0000000000..aa6d496d64
--- /dev/null
+++ b/dom/security/test/crashtests/1583044.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+<title>Bug 1583044</title>
+<script>
+ function testOpenMozIcon() {
+ window.location.href = "moz-icon://.pdf?size=128";
+ }
+</script>
+</head>
+<body onload="testOpenMozIcon();"></body>
+</html>
diff --git a/dom/security/test/crashtests/crashtests.list b/dom/security/test/crashtests/crashtests.list
new file mode 100644
index 0000000000..fc7986cf3d
--- /dev/null
+++ b/dom/security/test/crashtests/crashtests.list
@@ -0,0 +1,2 @@
+load 1583044.html
+load 1577572.html
diff --git a/dom/security/test/csp/Ahem.ttf b/dom/security/test/csp/Ahem.ttf
new file mode 100644
index 0000000000..ac81cb0316
--- /dev/null
+++ b/dom/security/test/csp/Ahem.ttf
Binary files differ
diff --git a/dom/security/test/csp/File b/dom/security/test/csp/File
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/dom/security/test/csp/File
diff --git a/dom/security/test/csp/browser.toml b/dom/security/test/csp/browser.toml
new file mode 100644
index 0000000000..403be75533
--- /dev/null
+++ b/dom/security/test/csp/browser.toml
@@ -0,0 +1,30 @@
+[DEFAULT]
+support-files = [
+ "!/dom/security/test/csp/file_testserver.sjs",
+ "!/dom/security/test/csp/file_web_manifest.html",
+ "!/dom/security/test/csp/file_web_manifest.json",
+ "!/dom/security/test/csp/file_web_manifest.json^headers^",
+ "!/dom/security/test/csp/file_web_manifest_https.html",
+ "!/dom/security/test/csp/file_web_manifest_https.json",
+ "!/dom/security/test/csp/file_web_manifest_mixed_content.html",
+ "!/dom/security/test/csp/file_web_manifest_remote.html",
+ "file_test_browser_bookmarklets.html",
+ "file_test_browser_bookmarklets.html^headers^",
+]
+
+["browser_manifest-src-override-default-src.js"]
+
+["browser_pdfjs_not_subject_to_csp.js"]
+support-files = [
+ "dummy.pdf",
+ "file_pdfjs_not_subject_to_csp.html",
+]
+
+["browser_test_bookmarklets.js"]
+
+["browser_test_uir_optional_clicks.js"]
+support-files = ["file_csp_meta_uir.html"]
+
+["browser_test_web_manifest.js"]
+
+["browser_test_web_manifest_mixed_content.js"]
diff --git a/dom/security/test/csp/browser_manifest-src-override-default-src.js b/dom/security/test/csp/browser_manifest-src-override-default-src.js
new file mode 100644
index 0000000000..5eef5bcc4e
--- /dev/null
+++ b/dom/security/test/csp/browser_manifest-src-override-default-src.js
@@ -0,0 +1,126 @@
+/*
+ * Description of the tests:
+ * Tests check that default-src can be overridden by manifest-src.
+ */
+/*globals Cu, is, ok*/
+"use strict";
+const { ManifestObtainer } = ChromeUtils.importESModule(
+ "resource://gre/modules/ManifestObtainer.sys.mjs"
+);
+const path = "/tests/dom/security/test/csp/";
+const testFile = `${path}file_web_manifest.html`;
+const mixedContentFile = `${path}file_web_manifest_mixed_content.html`;
+const server = `${path}file_testserver.sjs`;
+const defaultURL = new URL(`https://example.org${server}`);
+const mixedURL = new URL(`http://mochi.test:8888${server}`);
+
+// Enable web manifest processing.
+Services.prefs.setBoolPref("dom.manifest.enabled", true);
+
+const tests = [
+ // Check interaction with default-src and another origin,
+ // CSP allows fetching from example.org, so manifest should load.
+ {
+ expected: `CSP manifest-src overrides default-src of elsewhere.com`,
+ get tabURL() {
+ const url = new URL(defaultURL);
+ url.searchParams.append("file", testFile);
+ url.searchParams.append("cors", "*");
+ url.searchParams.append(
+ "csp",
+ "default-src http://elsewhere.com; manifest-src http://example.org"
+ );
+ return url.href;
+ },
+ run(manifest) {
+ is(manifest.name, "loaded", this.expected);
+ },
+ },
+ // Check interaction with default-src none,
+ // CSP allows fetching manifest from example.org, so manifest should load.
+ {
+ expected: `CSP manifest-src overrides default-src`,
+ get tabURL() {
+ const url = new URL(mixedURL);
+ url.searchParams.append("file", mixedContentFile);
+ url.searchParams.append("cors", "http://test:80");
+ url.searchParams.append(
+ "csp",
+ "default-src 'self'; manifest-src http://test:80"
+ );
+ return url.href;
+ },
+ run(manifest) {
+ is(manifest.name, "loaded", this.expected);
+ },
+ },
+];
+
+//jscs:disable
+add_task(async function () {
+ //jscs:enable
+ const testPromises = tests.map(test => {
+ const tabOptions = {
+ gBrowser,
+ url: test.tabURL,
+ skipAnimation: true,
+ };
+ return BrowserTestUtils.withNewTab(tabOptions, browser =>
+ testObtainingManifest(browser, test)
+ );
+ });
+ await Promise.all(testPromises);
+});
+
+async function testObtainingManifest(aBrowser, aTest) {
+ const expectsBlocked = aTest.expected.includes("block");
+ const observer = expectsBlocked ? createNetObserver(aTest) : null;
+ // Expect an exception (from promise rejection) if there a content policy
+ // that is violated.
+ try {
+ const manifest = await ManifestObtainer.browserObtainManifest(aBrowser);
+ aTest.run(manifest);
+ } catch (e) {
+ const wasBlocked = e.message.includes(
+ "NetworkError when attempting to fetch resource"
+ );
+ ok(
+ wasBlocked,
+ `Expected promise rejection obtaining ${aTest.tabURL}: ${e.message}`
+ );
+ if (observer) {
+ await observer.untilFinished;
+ }
+ }
+}
+
+// Helper object used to observe policy violations. It waits 1 seconds
+// for a response, and then times out causing its associated test to fail.
+function createNetObserver(test) {
+ let finishedTest;
+ let success = false;
+ const finished = new Promise(resolver => {
+ finishedTest = resolver;
+ });
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ const timeoutId = setTimeout(() => {
+ if (!success) {
+ test.run("This test timed out.");
+ finishedTest();
+ }
+ }, 1000);
+ var observer = {
+ get untilFinished() {
+ return finished;
+ },
+ observe(subject, topic) {
+ SpecialPowers.removeObserver(observer, "csp-on-violate-policy");
+ test.run(topic);
+ finishedTest();
+ clearTimeout(timeoutId);
+ success = true;
+ },
+ };
+ SpecialPowers.addObserver(observer, "csp-on-violate-policy");
+ return observer;
+}
diff --git a/dom/security/test/csp/browser_pdfjs_not_subject_to_csp.js b/dom/security/test/csp/browser_pdfjs_not_subject_to_csp.js
new file mode 100644
index 0000000000..2391e955ba
--- /dev/null
+++ b/dom/security/test/csp/browser_pdfjs_not_subject_to_csp.js
@@ -0,0 +1,45 @@
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH + "file_pdfjs_not_subject_to_csp.html",
+ async function (browser) {
+ let pdfPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "documentloaded",
+ false,
+ null,
+ true
+ );
+
+ await ContentTask.spawn(browser, {}, async function () {
+ let pdfButton = content.document.getElementById("pdfButton");
+ pdfButton.click();
+ });
+
+ await pdfPromise;
+
+ await ContentTask.spawn(browser, {}, async function () {
+ let pdfFrame = content.document.getElementById("pdfFrame");
+ // 1) Sanity that we have loaded the PDF using a blob
+ ok(pdfFrame.src.startsWith("blob:"), "it's a blob URL");
+
+ // 2) Ensure that the PDF has actually loaded
+ ok(
+ pdfFrame.contentDocument.querySelector("div#viewer"),
+ "document content has viewer UI"
+ );
+
+ // 3) Ensure we have the correct CSP attached
+ let cspJSON = pdfFrame.contentDocument.cspJSON;
+ ok(cspJSON.includes("script-src"), "found script-src directive");
+ ok(cspJSON.includes("allowPDF"), "found script-src nonce value");
+ });
+ }
+ );
+});
diff --git a/dom/security/test/csp/browser_test_bookmarklets.js b/dom/security/test/csp/browser_test_bookmarklets.js
new file mode 100644
index 0000000000..08b5ab0758
--- /dev/null
+++ b/dom/security/test/csp/browser_test_bookmarklets.js
@@ -0,0 +1,82 @@
+"use strict";
+
+let BASE_URL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+const DUMMY_URL = BASE_URL + "file_test_browser_bookmarklets.html";
+
+function makeBookmarkFor(url, keyword) {
+ return Promise.all([
+ PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmarklet",
+ url,
+ }),
+ PlacesUtils.keywords.insert({ url, keyword }),
+ ]);
+}
+/* Test Description:
+ * 1 - Load a Page with CSP script-src: none
+ * 2 - Create a bookmarklet with javascript:window.open('about:blank')
+ * 3 - Select and enter the bookmarklet
+ * A new tab with about:blank should be opened
+ */
+add_task(async function openKeywordBookmarkWithWindowOpen() {
+ // This is the current default, but let's not assume that...
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.link.open_newwindow", 3],
+ ["dom.disable_open_during_load", true],
+ ],
+ });
+
+ let moztab;
+ let tabOpened = BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ DUMMY_URL
+ ).then(tab => {
+ moztab = tab;
+ });
+ let keywordForBM = "openNewWindowBookmarklet";
+
+ let bookmarkInfo;
+ let bookmarkCreated = makeBookmarkFor(
+ `javascript: window.open("about:blank")`,
+ keywordForBM
+ ).then(values => {
+ bookmarkInfo = values[0];
+ });
+ await Promise.all([tabOpened, bookmarkCreated]);
+
+ registerCleanupFunction(function () {
+ return Promise.all([
+ PlacesUtils.bookmarks.remove(bookmarkInfo),
+ PlacesUtils.keywords.remove(keywordForBM),
+ ]);
+ });
+ gURLBar.value = keywordForBM;
+ gURLBar.focus();
+
+ let tabCreatedPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ EventUtils.synthesizeKey("KEY_Enter");
+ info("Waiting for tab being created");
+ let { target: tab } = await tabCreatedPromise;
+ info("Got tab");
+ let browser = tab.linkedBrowser;
+ if (!browser.currentURI || browser.currentURI.spec != "about:blank") {
+ info("Waiting for browser load");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+ }
+ is(
+ browser.currentURI && browser.currentURI.spec,
+ "about:blank",
+ "Tab with expected URL loaded."
+ );
+ info("Waiting to remove tab");
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(moztab);
+});
diff --git a/dom/security/test/csp/browser_test_uir_optional_clicks.js b/dom/security/test/csp/browser_test_uir_optional_clicks.js
new file mode 100644
index 0000000000..57e1f64f1a
--- /dev/null
+++ b/dom/security/test/csp/browser_test_uir_optional_clicks.js
@@ -0,0 +1,36 @@
+"use strict";
+
+const TEST_PATH_HTTP = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+const TEST_PATH_HTTPS = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_first", false]],
+ });
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH_HTTPS + "file_csp_meta_uir.html",
+ async function (browser) {
+ let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, null, true);
+ BrowserTestUtils.synthesizeMouse(
+ "#mylink",
+ 2,
+ 2,
+ { accelKey: true },
+ browser
+ );
+ let tab = await newTabPromise;
+ is(
+ tab.linkedBrowser.currentURI.scheme,
+ "https",
+ "Should have opened https page."
+ );
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+});
diff --git a/dom/security/test/csp/browser_test_web_manifest.js b/dom/security/test/csp/browser_test_web_manifest.js
new file mode 100644
index 0000000000..bdf62ab397
--- /dev/null
+++ b/dom/security/test/csp/browser_test_web_manifest.js
@@ -0,0 +1,239 @@
+/*
+ * Description of the tests:
+ * These tests check for conformance to the CSP spec as they relate to Web Manifests.
+ *
+ * In particular, the tests check that default-src and manifest-src directives are
+ * are respected by the ManifestObtainer.
+ */
+/*globals Cu, is, ok*/
+"use strict";
+const { ManifestObtainer } = ChromeUtils.importESModule(
+ "resource://gre/modules/ManifestObtainer.sys.mjs"
+);
+const path = "/tests/dom/security/test/csp/";
+const testFile = `${path}file_web_manifest.html`;
+const remoteFile = `${path}file_web_manifest_remote.html`;
+const httpsManifest = `${path}file_web_manifest_https.html`;
+const server = `${path}file_testserver.sjs`;
+const defaultURL = new URL(`http://example.org${server}`);
+const secureURL = new URL(`https://example.com:443${server}`);
+
+// Enable web manifest processing.
+Services.prefs.setBoolPref("dom.manifest.enabled", true);
+
+const tests = [
+ // CSP block everything, so trying to load a manifest
+ // will result in a policy violation.
+ {
+ expected: "default-src 'none' blocks fetching manifest.",
+ get tabURL() {
+ const url = new URL(defaultURL);
+ url.searchParams.append("file", testFile);
+ url.searchParams.append("csp", "default-src 'none'");
+ return url.href;
+ },
+ run(topic) {
+ is(topic, "csp-on-violate-policy", this.expected);
+ },
+ },
+ // CSP allows fetching only from mochi.test:8888,
+ // so trying to load a manifest from same origin
+ // triggers a CSP violation.
+ {
+ expected: "default-src mochi.test:8888 blocks manifest fetching.",
+ get tabURL() {
+ const url = new URL(defaultURL);
+ url.searchParams.append("file", testFile);
+ url.searchParams.append("csp", "default-src mochi.test:8888");
+ return url.href;
+ },
+ run(topic) {
+ is(topic, "csp-on-violate-policy", this.expected);
+ },
+ },
+ // CSP restricts fetching to 'self', so allowing the manifest
+ // to load. The name of the manifest is then checked.
+ {
+ expected: "CSP default-src 'self' allows fetch of manifest.",
+ get tabURL() {
+ const url = new URL(defaultURL);
+ url.searchParams.append("file", testFile);
+ url.searchParams.append("csp", "default-src 'self'");
+ return url.href;
+ },
+ run(manifest) {
+ is(manifest.name, "loaded", this.expected);
+ },
+ },
+ // CSP only allows fetching from mochi.test:8888 and remoteFile
+ // requests a manifest from that origin, so manifest should load.
+ {
+ expected: "CSP default-src mochi.test:8888 allows fetching manifest.",
+ get tabURL() {
+ const url = new URL(defaultURL);
+ url.searchParams.append("file", remoteFile);
+ url.searchParams.append("csp", "default-src http://mochi.test:8888");
+ return url.href;
+ },
+ run(manifest) {
+ is(manifest.name, "loaded", this.expected);
+ },
+ },
+ // default-src blocks everything, so any attempt to
+ // fetch a manifest from another origin will trigger a
+ // policy violation.
+ {
+ expected: "default-src 'none' blocks mochi.test:8888",
+ get tabURL() {
+ const url = new URL(defaultURL);
+ url.searchParams.append("file", remoteFile);
+ url.searchParams.append("csp", "default-src 'none'");
+ return url.href;
+ },
+ run(topic) {
+ is(topic, "csp-on-violate-policy", this.expected);
+ },
+ },
+ // CSP allows fetching from self, so manifest should load.
+ {
+ expected: "CSP manifest-src allows self",
+ get tabURL() {
+ const url = new URL(defaultURL);
+ url.searchParams.append("file", testFile);
+ url.searchParams.append("csp", "manifest-src 'self'");
+ return url.href;
+ },
+ run(manifest) {
+ is(manifest.name, "loaded", this.expected);
+ },
+ },
+ // CSP allows fetching from example.org, so manifest should load.
+ {
+ expected: "CSP manifest-src allows http://example.org",
+ get tabURL() {
+ const url = new URL(defaultURL);
+ url.searchParams.append("file", testFile);
+ url.searchParams.append("csp", "manifest-src http://example.org");
+ return url.href;
+ },
+ run(manifest) {
+ is(manifest.name, "loaded", this.expected);
+ },
+ },
+ {
+ expected: "CSP manifest-src allows mochi.test:8888",
+ get tabURL() {
+ const url = new URL(defaultURL);
+ url.searchParams.append("file", remoteFile);
+ url.searchParams.append("cors", "*");
+ url.searchParams.append(
+ "csp",
+ "default-src *; manifest-src http://mochi.test:8888"
+ );
+ return url.href;
+ },
+ run(manifest) {
+ is(manifest.name, "loaded", this.expected);
+ },
+ },
+ // CSP restricts fetching to mochi.test:8888, but the test
+ // file is at example.org. Hence, a policy violation is
+ // triggered.
+ {
+ expected: "CSP blocks manifest fetching from example.org.",
+ get tabURL() {
+ const url = new URL(defaultURL);
+ url.searchParams.append("file", testFile);
+ url.searchParams.append("csp", "manifest-src mochi.test:8888");
+ return url.href;
+ },
+ run(topic) {
+ is(topic, "csp-on-violate-policy", this.expected);
+ },
+ },
+ // CSP is set to only allow manifest to be loaded from same origin,
+ // but the remote file attempts to load from a different origin. Thus
+ // this causes a CSP violation.
+ {
+ expected: "CSP manifest-src 'self' blocks cross-origin fetch.",
+ get tabURL() {
+ const url = new URL(defaultURL);
+ url.searchParams.append("file", remoteFile);
+ url.searchParams.append("csp", "manifest-src 'self'");
+ return url.href;
+ },
+ run(topic) {
+ is(topic, "csp-on-violate-policy", this.expected);
+ },
+ },
+ // CSP allows fetching over TLS from example.org, so manifest should load.
+ {
+ expected: "CSP manifest-src allows example.com over TLS",
+ get tabURL() {
+ // secureURL loads https://example.com:443
+ // and gets manifest from https://example.org:443
+ const url = new URL(secureURL);
+ url.searchParams.append("file", httpsManifest);
+ url.searchParams.append("cors", "*");
+ url.searchParams.append("csp", "manifest-src https://example.com:443");
+ return url.href;
+ },
+ run(manifest) {
+ is(manifest.name, "loaded", this.expected);
+ },
+ },
+];
+
+//jscs:disable
+add_task(async function () {
+ //jscs:enable
+ const testPromises = tests.map(test => {
+ const tabOptions = {
+ gBrowser,
+ url: test.tabURL,
+ skipAnimation: true,
+ };
+ return BrowserTestUtils.withNewTab(tabOptions, browser =>
+ testObtainingManifest(browser, test)
+ );
+ });
+ await Promise.all(testPromises);
+});
+
+async function testObtainingManifest(aBrowser, aTest) {
+ const waitForObserver = waitForNetObserver(aBrowser, aTest);
+ // Expect an exception (from promise rejection) if there a content policy
+ // that is violated.
+ try {
+ const manifest = await ManifestObtainer.browserObtainManifest(aBrowser);
+ aTest.run(manifest);
+ } catch (e) {
+ const wasBlocked = e.message.includes(
+ "NetworkError when attempting to fetch resource"
+ );
+ ok(
+ wasBlocked,
+ `Expected promise rejection obtaining ${aTest.tabURL}: ${e.message}`
+ );
+ } finally {
+ await waitForObserver;
+ }
+}
+
+// Helper object used to observe policy violations when blocking is expected.
+function waitForNetObserver(aBrowser, aTest) {
+ // We don't need to wait for violation, so just resolve
+ if (!aTest.expected.includes("block")) {
+ return Promise.resolve();
+ }
+
+ return ContentTask.spawn(aBrowser, [], () => {
+ return new Promise(resolve => {
+ function observe(subject, topic) {
+ Services.obs.removeObserver(observe, "csp-on-violate-policy");
+ resolve();
+ }
+ Services.obs.addObserver(observe, "csp-on-violate-policy");
+ });
+ }).then(() => aTest.run("csp-on-violate-policy"));
+}
diff --git a/dom/security/test/csp/browser_test_web_manifest_mixed_content.js b/dom/security/test/csp/browser_test_web_manifest_mixed_content.js
new file mode 100644
index 0000000000..0cf55b80e3
--- /dev/null
+++ b/dom/security/test/csp/browser_test_web_manifest_mixed_content.js
@@ -0,0 +1,57 @@
+/*
+ * Description of the test:
+ * Check that mixed content blocker works prevents fetches of
+ * mixed content manifests.
+ */
+/*globals Cu, ok*/
+"use strict";
+const { ManifestObtainer } = ChromeUtils.importESModule(
+ "resource://gre/modules/ManifestObtainer.sys.mjs"
+);
+const path = "/tests/dom/security/test/csp/";
+const mixedContent = `${path}file_web_manifest_mixed_content.html`;
+const server = `${path}file_testserver.sjs`;
+const secureURL = new URL(`https://example.com${server}`);
+const tests = [
+ // Trying to load mixed content in file_web_manifest_mixed_content.html
+ // needs to result in an error.
+ {
+ expected: "Mixed Content Blocker prevents fetching manifest.",
+ get tabURL() {
+ const url = new URL(secureURL);
+ url.searchParams.append("file", mixedContent);
+ return url.href;
+ },
+ run(error) {
+ // Check reason for error.
+ const check = /NetworkError when attempting to fetch resource/.test(
+ error.message
+ );
+ ok(check, this.expected);
+ },
+ },
+];
+
+//jscs:disable
+add_task(async function () {
+ //jscs:enable
+ const testPromises = tests.map(test => {
+ const tabOptions = {
+ gBrowser,
+ url: test.tabURL,
+ skipAnimation: true,
+ };
+ return BrowserTestUtils.withNewTab(tabOptions, browser =>
+ testObtainingManifest(browser, test)
+ );
+ });
+ await Promise.all(testPromises);
+});
+
+async function testObtainingManifest(aBrowser, aTest) {
+ try {
+ await ManifestObtainer.browserObtainManifest(aBrowser);
+ } catch (e) {
+ aTest.run(e);
+ }
+}
diff --git a/dom/security/test/csp/dummy.pdf b/dom/security/test/csp/dummy.pdf
new file mode 100644
index 0000000000..7ad87e3c2e
--- /dev/null
+++ b/dom/security/test/csp/dummy.pdf
Binary files differ
diff --git a/dom/security/test/csp/file_CSP.css b/dom/security/test/csp/file_CSP.css
new file mode 100644
index 0000000000..6835c4d4ad
--- /dev/null
+++ b/dom/security/test/csp/file_CSP.css
@@ -0,0 +1,20 @@
+/*
+ * Moved this CSS from an inline stylesheet to an external file when we added
+ * inline-style blocking in bug 763879.
+ * This test may hang if the load for this .css file is blocked due to a
+ * malfunction of CSP, but should pass if the style_good test passes.
+ */
+
+/* CSS font embedding tests */
+@font-face {
+ font-family: "arbitrary_good";
+ src: url('file_CSP.sjs?testid=font_good&type=application/octet-stream');
+}
+@font-face {
+ font-family: "arbitrary_bad";
+ src: url('http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=font_bad&type=application/octet-stream');
+}
+
+.div_arbitrary_good { font-family: "arbitrary_good"; }
+.div_arbitrary_bad { font-family: "arbitrary_bad"; }
+
diff --git a/dom/security/test/csp/file_CSP.sjs b/dom/security/test/csp/file_CSP.sjs
new file mode 100644
index 0000000000..ff41690078
--- /dev/null
+++ b/dom/security/test/csp/file_CSP.sjs
@@ -0,0 +1,24 @@
+// SJS file for CSP mochitests
+
+function handleRequest(request, response) {
+ var query = {};
+ request.queryString.split("&").forEach(function (val) {
+ var [name, value] = val.split("=");
+ query[name] = unescape(value);
+ });
+
+ var isPreflight = request.method == "OPTIONS";
+
+ //avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ if ("type" in query) {
+ response.setHeader("Content-Type", unescape(query.type), false);
+ } else {
+ response.setHeader("Content-Type", "text/html", false);
+ }
+
+ if ("content" in query) {
+ response.write(unescape(query.content));
+ }
+}
diff --git a/dom/security/test/csp/file_allow_https_schemes.html b/dom/security/test/csp/file_allow_https_schemes.html
new file mode 100644
index 0000000000..787e683e87
--- /dev/null
+++ b/dom/security/test/csp/file_allow_https_schemes.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Bug 826805 - CSP: Allow http and https for scheme-less sources</title>
+ </head>
+ <body>
+ <div id="testdiv">blocked</div>
+ <!--
+ We resue file_path_matching.js which just updates the contents of 'testdiv' to contain allowed.
+ Note, that we are loading the file_path_matchting.js using a scheme of 'https'.
+ -->
+ <script src="https://example.com/tests/dom/security/test/csp/file_path_matching.js#foo"></script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_base_uri_server.sjs b/dom/security/test/csp/file_base_uri_server.sjs
new file mode 100644
index 0000000000..9056c8bbfd
--- /dev/null
+++ b/dom/security/test/csp/file_base_uri_server.sjs
@@ -0,0 +1,58 @@
+// Custom *.sjs file specifically for the needs of
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1263286
+
+"use strict";
+
+const PRE_BASE = `
+ <!DOCTYPE HTML>
+ <html>
+ <head>
+ <title>Bug 1045897 - Test CSP base-uri directive</title>`;
+
+const REGULAR_POST_BASE = `
+ </head>
+ <body onload='window.parent.postMessage({result: document.baseURI}, "*");'>
+ <!-- just making use of the 'base' tag for this test -->
+ </body>
+ </html>`;
+
+const SCRIPT_POST_BASE = `
+ </head>
+ <body>
+ <script>
+ document.getElementById("base1").removeAttribute("href");
+ window.parent.postMessage({result: document.baseURI}, "*");
+ </script>
+ </body>
+ </html>`;
+
+function handleRequest(request, response) {
+ const query = new URLSearchParams(request.queryString);
+
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ // Deliver the CSP policy encoded in the URL
+ response.setHeader("Content-Security-Policy", query.get("csp"), false);
+
+ // Send HTML to test allowed/blocked behaviors
+ response.setHeader("Content-Type", "text/html", false);
+ response.write(PRE_BASE);
+ var base1 = '<base id="base1" href="' + query.get("base1") + '">';
+ var base2 = '<base id="base2" href="' + query.get("base2") + '">';
+ response.write(base1 + base2);
+
+ if (query.get("action") === "enforce-csp") {
+ response.write(REGULAR_POST_BASE);
+ return;
+ }
+
+ if (query.get("action") === "remove-base1") {
+ response.write(SCRIPT_POST_BASE);
+ return;
+ }
+
+ // we should never get here, but just in case
+ // return something unexpected
+ response.write("do'h");
+}
diff --git a/dom/security/test/csp/file_blob_data_schemes.html b/dom/security/test/csp/file_blob_data_schemes.html
new file mode 100644
index 0000000000..0a4a491606
--- /dev/null
+++ b/dom/security/test/csp/file_blob_data_schemes.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1086999 - Wildcard should not match blob:, data:</title>
+</head>
+<body>
+<script type="text/javascript">
+
+var base64data =
+"iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
+"P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+
+
+// construct an image element using *data:*
+var data_src = "data:image/png;base64," + base64data;
+var data_img = document.createElement('img');
+data_img.onload = function() {
+ window.parent.postMessage({scheme: "data", result: "allowed"}, "*");
+}
+data_img.onerror = function() {
+ window.parent.postMessage({scheme: "data", result: "blocked"}, "*");
+}
+data_img.src = data_src;
+document.body.appendChild(data_img);
+
+
+// construct an image element using *blob:*
+var byteCharacters = atob(base64data);
+var byteNumbers = new Array(byteCharacters.length);
+for (var i = 0; i < byteCharacters.length; i++) {
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
+}
+var byteArray = new Uint8Array(byteNumbers);
+var blob = new Blob([byteArray], {type: "image/png"});
+var imageUrl = URL.createObjectURL( blob );
+
+var blob_img = document.createElement('img');
+blob_img.onload = function() {
+ window.parent.postMessage({scheme: "blob", result: "allowed"}, "*");
+}
+blob_img.onerror = function() {
+ window.parent.postMessage({scheme: "blob", result: "blocked"}, "*");
+}
+blob_img.src = imageUrl;
+document.body.appendChild(blob_img);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_blob_top_nav_block_modals.html b/dom/security/test/csp/file_blob_top_nav_block_modals.html
new file mode 100644
index 0000000000..545f6cffff
--- /dev/null
+++ b/dom/security/test/csp/file_blob_top_nav_block_modals.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+<script>
+ // If the alert box is blocked correctly by the CSP then postMessage will
+ // send the message and test passes.
+ var text = "<script>alert(document.domain);window.opener.postMessage("+
+ "{\"test\": \"block_top_nav_alert_test\", \"msg\": "+
+ "\"blob top nav alert blocked by CSP\"}, \"*\")<\/script>";
+ var blob = new Blob([text], {type : 'text/html'});
+ var url = URL.createObjectURL(blob);
+ location.href=url;
+</script>
+</body>
+</html> \ No newline at end of file
diff --git a/dom/security/test/csp/file_blob_top_nav_block_modals.html^headers^ b/dom/security/test/csp/file_blob_top_nav_block_modals.html^headers^
new file mode 100644
index 0000000000..e2d945d556
--- /dev/null
+++ b/dom/security/test/csp/file_blob_top_nav_block_modals.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: sandbox allow-scripts; \ No newline at end of file
diff --git a/dom/security/test/csp/file_blob_uri_blocks_modals.html b/dom/security/test/csp/file_blob_uri_blocks_modals.html
new file mode 100644
index 0000000000..caf2a5de41
--- /dev/null
+++ b/dom/security/test/csp/file_blob_uri_blocks_modals.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+<!-- iframe loading the blob url with null origin -->
+<iframe id="blobFrame"></iframe>
+<script>
+ // If the alert box is blocked correctly by the CSP then postMessage will
+ // send the message and test passes.
+ var alertScriptText = "data:text/html,<script>location=URL.createObjectURL(" +
+ "new Blob(['<script>alert(document.URL);parent.parent.postMessage(" +
+ "{\"test\": \"block_alert_test\", \"msg\": \"alert blocked by" +
+ " CSP\"}, \"*\");<\\/script>'], {type:\"text/html\"}));<\/script>";
+ document.getElementById("blobFrame").src=alertScriptText;
+ try {
+ var w = window.open("http://www.example.com","newwindow");
+ parent.postMessage({"test": "block_window_open_test",
+ "msg": "new window not blocked by CSP"},"*");
+ } catch(err) {
+ parent.postMessage({"test": "block_window_open_test",
+ "msg": "window blocked by CSP"},"*");
+ }
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_blob_uri_blocks_modals.html^headers^ b/dom/security/test/csp/file_blob_uri_blocks_modals.html^headers^
new file mode 100644
index 0000000000..e2d945d556
--- /dev/null
+++ b/dom/security/test/csp/file_blob_uri_blocks_modals.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: sandbox allow-scripts; \ No newline at end of file
diff --git a/dom/security/test/csp/file_block_all_mcb.sjs b/dom/security/test/csp/file_block_all_mcb.sjs
new file mode 100644
index 0000000000..003c9df57c
--- /dev/null
+++ b/dom/security/test/csp/file_block_all_mcb.sjs
@@ -0,0 +1,78 @@
+// custom *.sjs for Bug 1122236
+// CSP: 'block-all-mixed-content'
+
+const HEAD =
+ "<!DOCTYPE HTML>" +
+ '<html><head><meta charset="utf-8">' +
+ "<title>Bug 1122236 - CSP: Implement block-all-mixed-content</title>" +
+ "</head>";
+
+const CSP_ALLOW =
+ '<meta http-equiv="Content-Security-Policy" content="img-src *">';
+
+const CSP_BLOCK =
+ '<meta http-equiv="Content-Security-Policy" content="block-all-mixed-content">';
+
+const BODY =
+ "<body>" +
+ '<img id="testimage" src="http://mochi.test:8888/tests/image/test/mochitest/blue.png"></img>' +
+ '<script type="application/javascript">' +
+ ' var myImg = document.getElementById("testimage");' +
+ " myImg.onload = function(e) {" +
+ ' window.parent.postMessage({result: "img-loaded"}, "*");' +
+ " };" +
+ " myImg.onerror = function(e) {" +
+ ' window.parent.postMessage({result: "img-blocked"}, "*");' +
+ " };" +
+ "</script>" +
+ "</body>" +
+ "</html>";
+
+// We have to use this special code fragment, in particular '?nocache' to trigger an
+// actual network load rather than loading the image from the cache.
+const BODY_CSPRO =
+ "<body>" +
+ '<img id="testimage" src="http://mochi.test:8888/tests/image/test/mochitest/blue.png?nocache"></img>' +
+ '<script type="application/javascript">' +
+ ' var myImg = document.getElementById("testimage");' +
+ " myImg.onload = function(e) {" +
+ ' window.parent.postMessage({result: "img-loaded"}, "*");' +
+ " };" +
+ " myImg.onerror = function(e) {" +
+ ' window.parent.postMessage({result: "img-blocked"}, "*");' +
+ " };" +
+ "</script>" +
+ "</body>" +
+ "</html>";
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ var queryString = request.queryString;
+
+ if (queryString === "csp-block") {
+ response.write(HEAD + CSP_BLOCK + BODY);
+ return;
+ }
+ if (queryString === "csp-allow") {
+ response.write(HEAD + CSP_ALLOW + BODY);
+ return;
+ }
+ if (queryString === "no-csp") {
+ response.write(HEAD + BODY);
+ return;
+ }
+ if (queryString === "cspro-block") {
+ // CSP RO is not supported in meta tag, let's use the header
+ response.setHeader(
+ "Content-Security-Policy-Report-Only",
+ "block-all-mixed-content",
+ false
+ );
+ response.write(HEAD + BODY_CSPRO);
+ return;
+ }
+ // we should never get here but just in case return something unexpected
+ response.write("do'h");
+}
diff --git a/dom/security/test/csp/file_block_all_mixed_content_frame_navigation1.html b/dom/security/test/csp/file_block_all_mixed_content_frame_navigation1.html
new file mode 100644
index 0000000000..fdc1ae87ac
--- /dev/null
+++ b/dom/security/test/csp/file_block_all_mixed_content_frame_navigation1.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Security-Policy" content="block-all-mixed-content">
+ <title>Bug 1122236 - CSP: Implement block-all-mixed-content</title>
+</head>
+<body>
+<b>user clicks and navigates from https://b.com to http://c.com</b>
+
+<a id="navlink" href="http://example.com/tests/dom/security/test/csp/file_block_all_mixed_content_frame_navigation2.html">foo</a>
+
+<script class="testbody" type="text/javascript">
+ // click the link to start the frame navigation
+ document.getElementById("navlink").click();
+</script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_block_all_mixed_content_frame_navigation2.html b/dom/security/test/csp/file_block_all_mixed_content_frame_navigation2.html
new file mode 100644
index 0000000000..4c4084e9ed
--- /dev/null
+++ b/dom/security/test/csp/file_block_all_mixed_content_frame_navigation2.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1122236 - CSP: Implement block-all-mixed-content</title>
+</head>
+<body>
+<b>http://c.com loaded, let's tell the parent</b>
+
+<script class="testbody" type="text/javascript">
+ window.parent.postMessage({result: "frame-navigated"}, "*");
+</script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_blocked_uri_in_violation_event_after_redirects.html b/dom/security/test/csp/file_blocked_uri_in_violation_event_after_redirects.html
new file mode 100644
index 0000000000..74af0ff767
--- /dev/null
+++ b/dom/security/test/csp/file_blocked_uri_in_violation_event_after_redirects.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1542194 - Check blockedURI in violation reports after redirects</title>
+ <meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' http://example.com">
+</head>
+<body>
+<button id="test1" onclick="createAndNavFrame('?test1a#ref1a')">Test 1: 302 redirect</button>
+<button id="test2" onclick="createAndNavFrame('?test2a#ref2a')">Test 2: JS redirect</button>
+<button id="test3" onclick="createAndNavFrame('?test3a#ref3a')">Test 3: Link navigation</button>
+<div id="div"></div>
+<script>
+ const SERVER_LOCATION =
+ "http://example.com/tests/dom/security/test/csp/file_blocked_uri_in_violation_event_after_redirects.sjs";
+
+ document.addEventListener('securitypolicyviolation', e => {
+ // just forward the blockedURI to the parent
+ window.parent.postMessage({blockedURI: e.blockedURI}, '*');
+ });
+
+ function createAndNavFrame(aTest) {
+ let myFrame = document.createElement('iframe');
+ myFrame.src = SERVER_LOCATION + aTest;
+ div.appendChild(myFrame);
+ }
+
+ window.onload = function() {
+ let button1 = document.getElementById("test1");
+ button1.click();
+
+ let button2 = document.getElementById("test2");
+ button2.click();
+
+ let button3 = document.getElementById("test3");
+ button3.click();
+ }
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_blocked_uri_in_violation_event_after_redirects.sjs b/dom/security/test/csp/file_blocked_uri_in_violation_event_after_redirects.sjs
new file mode 100644
index 0000000000..ef397011c9
--- /dev/null
+++ b/dom/security/test/csp/file_blocked_uri_in_violation_event_after_redirects.sjs
@@ -0,0 +1,51 @@
+// Redirect server specifically for the needs of Bug 1542194
+
+"use strict";
+
+let REDIRECT_302_URI =
+ "http://test1.example.com/tests/dom/security/test/csp/file_blocked_uri_in_violation_event_after_redirects.sjs?test1b#ref1b";
+
+let JS_REDIRECT = `<html>
+ <body>
+ <script>
+ var url= "http://test2.example.com/tests/dom/security/test/csp/file_blocked_uri_in_violation_event_after_redirects.sjs?test2b#ref2b";
+ window.location = url;
+ </script>
+ </body>
+ </html>`;
+
+let LINK_CLICK_NAVIGATION = `<html>
+ <body>
+ <a id="navlink" href="http://test3.example.com/tests/dom/security/test/csp/file_blocked_uri_in_violation_event_after_redirects.sjs?test3b#ref3b">click me</a>
+ <script>
+ window.onload = function() { document.getElementById('navlink').click(); }
+ </script>
+ </body>
+ </html>`;
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ let query = request.queryString;
+
+ // Test 1: 302 redirect
+ if (query === "test1a") {
+ var newLocation = REDIRECT_302_URI;
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", newLocation, false);
+ return;
+ }
+
+ // Test 2: JS redirect
+ if (query === "test2a") {
+ response.setHeader("Content-Type", "text/html", false);
+ response.write(JS_REDIRECT);
+ return;
+ }
+
+ // Test 3: Link navigation
+ if (query === "test3a") {
+ response.setHeader("Content-Type", "text/html", false);
+ response.write(LINK_CLICK_NAVIGATION);
+ }
+}
diff --git a/dom/security/test/csp/file_blocked_uri_redirect_frame_src.html b/dom/security/test/csp/file_blocked_uri_redirect_frame_src.html
new file mode 100644
index 0000000000..c3af4d5a09
--- /dev/null
+++ b/dom/security/test/csp/file_blocked_uri_redirect_frame_src.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1687342 - Check blocked-uri in csp-reports after frame redirect</title>
+</head>
+<body>
+ Contents of the following iframe will be blocked<br/>
+ <iframe src="http://example.com/tests/dom/security/test/csp/file_blocked_uri_redirect_frame_src_server.sjs?doredirect#ref1"></iframe>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_blocked_uri_redirect_frame_src.html^headers^ b/dom/security/test/csp/file_blocked_uri_redirect_frame_src.html^headers^
new file mode 100644
index 0000000000..b69131f8eb
--- /dev/null
+++ b/dom/security/test/csp/file_blocked_uri_redirect_frame_src.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: frame-src http://example.com; report-uri http://mochi.test:8888/foo.sjs;
diff --git a/dom/security/test/csp/file_blocked_uri_redirect_frame_src_server.sjs b/dom/security/test/csp/file_blocked_uri_redirect_frame_src_server.sjs
new file mode 100644
index 0000000000..9bf051f29b
--- /dev/null
+++ b/dom/security/test/csp/file_blocked_uri_redirect_frame_src_server.sjs
@@ -0,0 +1,13 @@
+// Redirect server specifically for the needs of Bug 1687342
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ let query = request.queryString;
+ if (query === "doredirect") {
+ var newLocation =
+ "http://test1.example.com/tests/dom/security/test/csp/file_blocked_uri_redirect_frame_src_server.sjs?query#ref2";
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", newLocation, false);
+ }
+}
diff --git a/dom/security/test/csp/file_bug1229639.html b/dom/security/test/csp/file_bug1229639.html
new file mode 100644
index 0000000000..1e6152ead0
--- /dev/null
+++ b/dom/security/test/csp/file_bug1229639.html
@@ -0,0 +1,7 @@
+<html>
+<head> <meta charset="utf-8"> </head>
+ <body>
+ <!-- this should be allowed -->
+ <script src="http://mochi.test:8888/tests/dom/security/test/csp/%24.js"> </script>
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_bug1229639.html^headers^ b/dom/security/test/csp/file_bug1229639.html^headers^
new file mode 100644
index 0000000000..0177de7a38
--- /dev/null
+++ b/dom/security/test/csp/file_bug1229639.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: "default-src 'self'; script-src http://mochi.test:8888/tests/dom/security/test/csp/%24.js \ No newline at end of file
diff --git a/dom/security/test/csp/file_bug1312272.html b/dom/security/test/csp/file_bug1312272.html
new file mode 100644
index 0000000000..18e0e5589e
--- /dev/null
+++ b/dom/security/test/csp/file_bug1312272.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>marquee inline script tests for Bug 1312272</title>
+</head>
+<body>
+<marquee id="m" onstart="parent.postMessage('csp-violation-marquee-onstart', '*')">bug 1312272</marquee>
+<script src="file_bug1312272.js"></script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_bug1312272.html^headers^ b/dom/security/test/csp/file_bug1312272.html^headers^
new file mode 100644
index 0000000000..25a9483ea9
--- /dev/null
+++ b/dom/security/test/csp/file_bug1312272.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src *; script-src * 'unsafe-eval'
diff --git a/dom/security/test/csp/file_bug1312272.js b/dom/security/test/csp/file_bug1312272.js
new file mode 100644
index 0000000000..450013bec1
--- /dev/null
+++ b/dom/security/test/csp/file_bug1312272.js
@@ -0,0 +1,8 @@
+var m = document.getElementById("m");
+m.addEventListener("click", function () {
+ // this will trigger after onstart, obviously.
+ parent.postMessage("finish", "*");
+});
+console.log("finish-handler setup");
+m.click();
+console.log("clicked");
diff --git a/dom/security/test/csp/file_bug1452037.html b/dom/security/test/csp/file_bug1452037.html
new file mode 100644
index 0000000000..0fb41d6654
--- /dev/null
+++ b/dom/security/test/csp/file_bug1452037.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Security-Policy" content="script-src 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='">
+</head>
+<body>
+ <a href="javascript:window.parent.postMessage({}, '*');">Click here</a>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_bug1505412.sjs b/dom/security/test/csp/file_bug1505412.sjs
new file mode 100644
index 0000000000..e47bf2506a
--- /dev/null
+++ b/dom/security/test/csp/file_bug1505412.sjs
@@ -0,0 +1,34 @@
+// https://bugzilla.mozilla.org/show_bug.cgi?id=650386
+// This SJS file serves file_redirect_content.html
+// with a CSP that will trigger a violation and that will report it
+// to file_redirect_report.sjs
+//
+// This handles 301, 302, 303 and 307 redirects. The HTTP status code
+// returned/type of redirect to do comes from the query string
+// parameter passed in from the test_bug650386_* files and then also
+// uses that value in the report-uri parameter of the CSP
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ // this gets used in the CSP as part of the report URI.
+ var redirect = request.queryString;
+
+ if (!redirect) {
+ // if we somehow got some bogus redirect code here,
+ // do a 302 redirect to the same URL as the report URI
+ // redirects to - this will fail the test.
+ var loc =
+ "http://sub1.test1.example.org/tests/dom/security/test/csp/file_bug1505412.sjs?redirected";
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", loc, false);
+ return;
+ }
+
+ // response.setHeader("content-type", "text/application", false);
+ // the actual file content.
+ // this image load will (intentionally) fail due to the CSP policy of default-src: 'self'
+ // specified by the CSP string above.
+ var content = "info('Script Loaded')";
+
+ response.write(content);
+}
diff --git a/dom/security/test/csp/file_bug1505412_frame.html b/dom/security/test/csp/file_bug1505412_frame.html
new file mode 100644
index 0000000000..b58af55849
--- /dev/null
+++ b/dom/security/test/csp/file_bug1505412_frame.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <title> Bug 1505412 CSP-RO reports violations in inline-scripts with nonce</title>
+ <script src="/tests/SimpleTest/SimpleTest.js" nonce="foobar"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+
+
+<body>
+ <script src="file_bug1505412.sjs" nonce="foobar"></script>
+</body>
+
+</html>
diff --git a/dom/security/test/csp/file_bug1505412_frame.html^headers^ b/dom/security/test/csp/file_bug1505412_frame.html^headers^
new file mode 100644
index 0000000000..e60b63c29c
--- /dev/null
+++ b/dom/security/test/csp/file_bug1505412_frame.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy-Report-Only: script-src 'nonce-foobar'; report-uri file_bug1505412_reporter.sjs
diff --git a/dom/security/test/csp/file_bug1505412_reporter.sjs b/dom/security/test/csp/file_bug1505412_reporter.sjs
new file mode 100644
index 0000000000..323a4edb1c
--- /dev/null
+++ b/dom/security/test/csp/file_bug1505412_reporter.sjs
@@ -0,0 +1,18 @@
+function handleRequest(request, response) {
+ var receivedRequests = parseInt(getState("requests"));
+ if (isNaN(receivedRequests)) {
+ receivedRequests = 0;
+ }
+ if (request.queryString.includes("state")) {
+ response.write(receivedRequests);
+ return;
+ }
+ if (request.queryString.includes("flush")) {
+ setState("requests", "0");
+ response.write("OK");
+ return;
+ }
+ receivedRequests = receivedRequests + 1;
+ setState("requests", "" + receivedRequests);
+ response.write("OK");
+}
diff --git a/dom/security/test/csp/file_bug1738418_child.html b/dom/security/test/csp/file_bug1738418_child.html
new file mode 100644
index 0000000000..26e7f8f1f6
--- /dev/null
+++ b/dom/security/test/csp/file_bug1738418_child.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+<script type="text/javascript">
+ window.parent.parent.postMessage({
+ element: location.hash.substr(1),
+ domain: document.domain,
+ }, '*');
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_bug1738418_parent.html b/dom/security/test/csp/file_bug1738418_parent.html
new file mode 100644
index 0000000000..c8bdbb2c46
--- /dev/null
+++ b/dom/security/test/csp/file_bug1738418_parent.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <base href="file_bug1738418_child.html">
+</head>
+<body>
+ <iframe src="#iframe"></iframe>
+ <embed src="#embed"></embed>
+ <object data="#object"></object>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_bug1738418_parent.html^headers^ b/dom/security/test/csp/file_bug1738418_parent.html^headers^
new file mode 100644
index 0000000000..4705ce9ded
--- /dev/null
+++ b/dom/security/test/csp/file_bug1738418_parent.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: sandbox allow-scripts;
diff --git a/dom/security/test/csp/file_bug1764343.html b/dom/security/test/csp/file_bug1764343.html
new file mode 100644
index 0000000000..09781cce89
--- /dev/null
+++ b/dom/security/test/csp/file_bug1764343.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1764343 - CSP inheritance for same-origin iframes</title>
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'none'; script-src 'nonce-a' 'nonce-b'; img-src 'none'">
+</head>
+<body>
+ initial content
+</body>
+</html>
diff --git a/dom/security/test/csp/file_bug1777572.html b/dom/security/test/csp/file_bug1777572.html
new file mode 100644
index 0000000000..51f2a80d28
--- /dev/null
+++ b/dom/security/test/csp/file_bug1777572.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta http-equiv="Content-Security-Policy" content="img-src https://*;">
+ <script>
+ async function timeout (cmd) {
+ const timer = new Promise((resolve, reject) => {
+ const id = setTimeout(() => {
+ clearTimeout(id)
+ reject(new Error('Promise timed out!'))
+ }, 750)
+ })
+ return Promise.race([cmd, timer])
+ }
+
+ let ourOpener = window.opener;
+
+ if (location.search.includes("close")) {
+ window.close();
+ }
+
+ document.addEventListener('DOMContentLoaded', async () => {
+ const frame = document.createElementNS('http://www.w3.org/1999/xhtml', 'frame');
+ const image = document.createElementNS('http://www.w3.org/2000/svg', 'image');
+ document.documentElement.appendChild(frame)
+ image.setAttribute('href', 'a.png')
+ for (let i = 0; i < 5; ++i) {
+ try { await timeout(image.decode()) } catch (e) {}
+ }
+ let w = window.open();
+ // Need to run SpecialPowers in the newly opened window to avoid
+ // .wrap throwing because of dead objects.
+ let csp = w.eval("SpecialPowers.wrap(document).cspJSON;");
+ ourOpener.postMessage(csp, "*");
+ w.close();
+
+ if (!location.search.includes("close")) {
+ window.close();
+ }
+ })
+ </script>
+</head>
+</html>
diff --git a/dom/security/test/csp/file_bug663567.xsl b/dom/security/test/csp/file_bug663567.xsl
new file mode 100644
index 0000000000..b12b0d3b1d
--- /dev/null
+++ b/dom/security/test/csp/file_bug663567.xsl
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!-- Edited by XMLSpy® -->
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+<xsl:template match="/">
+ <html>
+ <body>
+ <h2 id="xsltheader">this xml file should be formatted using an xsl file(lower iframe should contain xml dump)!</h2>
+ <table border="1">
+ <tr bgcolor="#990099">
+ <th>Title</th>
+ <th>Artist</th>
+ <th>Price</th>
+ </tr>
+ <xsl:for-each select="catalog/cd">
+ <tr>
+ <td><xsl:value-of select="title"/></td>
+ <td><xsl:value-of select="artist"/></td>
+ <td><xsl:value-of select="price"/></td>
+ </tr>
+ </xsl:for-each>
+ </table>
+ </body>
+ </html>
+</xsl:template>
+</xsl:stylesheet>
+
diff --git a/dom/security/test/csp/file_bug663567_allows.xml b/dom/security/test/csp/file_bug663567_allows.xml
new file mode 100644
index 0000000000..93d3451038
--- /dev/null
+++ b/dom/security/test/csp/file_bug663567_allows.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<?xml-stylesheet type="text/xsl" href="file_bug663567.xsl"?>
+<catalog>
+ <cd>
+ <title>Empire Burlesque</title>
+ <artist>Bob Dylan</artist>
+ <country>USA</country>
+ <company>Columbia</company>
+ <price>10.90</price>
+ <year>1985</year>
+ </cd>
+ <cd>
+ <title>Hide your heart</title>
+ <artist>Bonnie Tyler</artist>
+ <country>UK</country>
+ <company>CBS Records</company>
+ <price>9.90</price>
+ <year>1988</year>
+ </cd>
+ <cd>
+ <title>Greatest Hits</title>
+ <artist>Dolly Parton</artist>
+ <country>USA</country>
+ <company>RCA</company>
+ <price>9.90</price>
+ <year>1982</year>
+ </cd>
+</catalog>
diff --git a/dom/security/test/csp/file_bug663567_allows.xml^headers^ b/dom/security/test/csp/file_bug663567_allows.xml^headers^
new file mode 100644
index 0000000000..4c6fa3c26a
--- /dev/null
+++ b/dom/security/test/csp/file_bug663567_allows.xml^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src 'self'
diff --git a/dom/security/test/csp/file_bug663567_blocks.xml b/dom/security/test/csp/file_bug663567_blocks.xml
new file mode 100644
index 0000000000..93d3451038
--- /dev/null
+++ b/dom/security/test/csp/file_bug663567_blocks.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<?xml-stylesheet type="text/xsl" href="file_bug663567.xsl"?>
+<catalog>
+ <cd>
+ <title>Empire Burlesque</title>
+ <artist>Bob Dylan</artist>
+ <country>USA</country>
+ <company>Columbia</company>
+ <price>10.90</price>
+ <year>1985</year>
+ </cd>
+ <cd>
+ <title>Hide your heart</title>
+ <artist>Bonnie Tyler</artist>
+ <country>UK</country>
+ <company>CBS Records</company>
+ <price>9.90</price>
+ <year>1988</year>
+ </cd>
+ <cd>
+ <title>Greatest Hits</title>
+ <artist>Dolly Parton</artist>
+ <country>USA</country>
+ <company>RCA</company>
+ <price>9.90</price>
+ <year>1982</year>
+ </cd>
+</catalog>
diff --git a/dom/security/test/csp/file_bug663567_blocks.xml^headers^ b/dom/security/test/csp/file_bug663567_blocks.xml^headers^
new file mode 100644
index 0000000000..baf7f3c6af
--- /dev/null
+++ b/dom/security/test/csp/file_bug663567_blocks.xml^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src *.example.com
diff --git a/dom/security/test/csp/file_bug802872.html b/dom/security/test/csp/file_bug802872.html
new file mode 100644
index 0000000000..dae040376b
--- /dev/null
+++ b/dom/security/test/csp/file_bug802872.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 802872</title>
+ <!-- Including SimpleTest.js so we can use AddLoadEvent !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <script src='file_bug802872.js'></script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_bug802872.html^headers^ b/dom/security/test/csp/file_bug802872.html^headers^
new file mode 100644
index 0000000000..4c6fa3c26a
--- /dev/null
+++ b/dom/security/test/csp/file_bug802872.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src 'self'
diff --git a/dom/security/test/csp/file_bug802872.js b/dom/security/test/csp/file_bug802872.js
new file mode 100644
index 0000000000..042e190269
--- /dev/null
+++ b/dom/security/test/csp/file_bug802872.js
@@ -0,0 +1,47 @@
+/*
+ * The policy for this test is:
+ * Content-Security-Policy: default-src 'self'
+ */
+
+function createAllowedEvent() {
+ /*
+ * Creates a new EventSource using 'http://mochi.test:8888'. Since all mochitests run on
+ * 'http://mochi.test', a default-src of 'self' allows this request.
+ */
+ var src_event = new EventSource(
+ "http://mochi.test:8888/tests/dom/security/test/csp/file_bug802872.sjs"
+ );
+
+ src_event.onmessage = function (e) {
+ src_event.close();
+ parent.dispatchEvent(new Event("allowedEventSrcCallbackOK"));
+ };
+
+ src_event.onerror = function (e) {
+ src_event.close();
+ parent.dispatchEvent(new Event("allowedEventSrcCallbackFailed"));
+ };
+}
+
+function createBlockedEvent() {
+ /*
+ * creates a new EventSource using 'http://example.com'. This domain is not allowlisted by the
+ * CSP of this page, therefore the CSP blocks this request.
+ */
+ var src_event = new EventSource(
+ "http://example.com/tests/dom/security/test/csp/file_bug802872.sjs"
+ );
+
+ src_event.onmessage = function (e) {
+ src_event.close();
+ parent.dispatchEvent(new Event("blockedEventSrcCallbackOK"));
+ };
+
+ src_event.onerror = function (e) {
+ src_event.close();
+ parent.dispatchEvent(new Event("blockedEventSrcCallbackFailed"));
+ };
+}
+
+addLoadEvent(createAllowedEvent);
+addLoadEvent(createBlockedEvent);
diff --git a/dom/security/test/csp/file_bug802872.sjs b/dom/security/test/csp/file_bug802872.sjs
new file mode 100644
index 0000000000..6877bd5833
--- /dev/null
+++ b/dom/security/test/csp/file_bug802872.sjs
@@ -0,0 +1,6 @@
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/event-stream", false);
+ response.write("data: eventsource response from server!");
+ response.write("\n\n");
+}
diff --git a/dom/security/test/csp/file_bug836922_npolicies.html b/dom/security/test/csp/file_bug836922_npolicies.html
new file mode 100644
index 0000000000..6a728813a7
--- /dev/null
+++ b/dom/security/test/csp/file_bug836922_npolicies.html
@@ -0,0 +1,12 @@
+<html>
+ <head>
+ <link rel='stylesheet' type='text/css'
+ href='/tests/dom/security/test/csp/file_CSP.sjs?testid=css_self&type=text/css' />
+
+ </head>
+ <body>
+ <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img_self&type=img/png"> </img>
+ <script src='/tests/dom/security/test/csp/file_CSP.sjs?testid=script_self&type=text/javascript'></script>
+
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_bug836922_npolicies.html^headers^ b/dom/security/test/csp/file_bug836922_npolicies.html^headers^
new file mode 100644
index 0000000000..ec6ba8c4ae
--- /dev/null
+++ b/dom/security/test/csp/file_bug836922_npolicies.html^headers^
@@ -0,0 +1,2 @@
+content-security-policy: default-src 'self'; img-src 'none'; report-uri http://mochi.test:8888/tests/dom/security/test/csp/file_bug836922_npolicies_violation.sjs
+content-security-policy-report-only: default-src *; img-src 'self'; script-src 'none'; report-uri http://mochi.test:8888/tests/dom/security/test/csp/file_bug836922_npolicies_ro_violation.sjs
diff --git a/dom/security/test/csp/file_bug836922_npolicies_ro_violation.sjs b/dom/security/test/csp/file_bug836922_npolicies_ro_violation.sjs
new file mode 100644
index 0000000000..0f5eb4b596
--- /dev/null
+++ b/dom/security/test/csp/file_bug836922_npolicies_ro_violation.sjs
@@ -0,0 +1,53 @@
+// SJS file that receives violation reports and then responds with nothing.
+
+const CC = Components.Constructor;
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+const STATE_KEY = "bug836922_ro_violations";
+
+function handleRequest(request, response) {
+ var query = {};
+ request.queryString.split("&").forEach(function (val) {
+ var [name, value] = val.split("=");
+ query[name] = unescape(value);
+ });
+
+ if ("results" in query) {
+ // if asked for the received data, send it.
+ response.setHeader("Content-Type", "text/javascript", false);
+ if (getState(STATE_KEY)) {
+ response.write(getState(STATE_KEY));
+ } else {
+ // no state has been recorded.
+ response.write(JSON.stringify({}));
+ }
+ } else if ("reset" in query) {
+ //clear state
+ setState(STATE_KEY, JSON.stringify(null));
+ } else {
+ // ... otherwise, just respond "ok".
+ response.write("null");
+
+ var bodystream = new BinaryInputStream(request.bodyInputStream);
+ var avail;
+ var bytes = [];
+ while ((avail = bodystream.available()) > 0) {
+ Array.prototype.push.apply(bytes, bodystream.readByteArray(avail));
+ }
+
+ var data = String.fromCharCode.apply(null, bytes);
+
+ // figure out which test was violating a policy
+ var testpat = new RegExp("testid=([a-z0-9_]+)");
+ var testid = testpat.exec(data)[1];
+
+ // store the violation in the persistent state
+ var s = JSON.parse(getState(STATE_KEY) || "{}");
+ s[testid] ? s[testid]++ : (s[testid] = 1);
+ setState(STATE_KEY, JSON.stringify(s));
+ }
+}
diff --git a/dom/security/test/csp/file_bug836922_npolicies_violation.sjs b/dom/security/test/csp/file_bug836922_npolicies_violation.sjs
new file mode 100644
index 0000000000..dec8b4f081
--- /dev/null
+++ b/dom/security/test/csp/file_bug836922_npolicies_violation.sjs
@@ -0,0 +1,64 @@
+// SJS file that receives violation reports and then responds with nothing.
+
+const CC = Components.Constructor;
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+const STATE = "bug836922_violations";
+
+function handleRequest(request, response) {
+ var query = {};
+ request.queryString.split("&").forEach(function (val) {
+ var [name, value] = val.split("=");
+ query[name] = unescape(value);
+ });
+
+ if ("results" in query) {
+ // if asked for the received data, send it.
+ response.setHeader("Content-Type", "text/javascript", false);
+ if (getState(STATE)) {
+ response.write(getState(STATE));
+ } else {
+ // no state has been recorded.
+ response.write(JSON.stringify({}));
+ }
+ } else if ("reset" in query) {
+ //clear state
+ setState(STATE, JSON.stringify(null));
+ } else {
+ // ... otherwise, just respond "ok".
+ response.write("null");
+
+ var bodystream = new BinaryInputStream(request.bodyInputStream);
+ var avail;
+ var bytes = [];
+ while ((avail = bodystream.available()) > 0) {
+ Array.prototype.push.apply(bytes, bodystream.readByteArray(avail));
+ }
+
+ var data = String.fromCharCode.apply(null, bytes);
+
+ // figure out which test was violating a policy
+ var testpat = new RegExp("testid=([a-z0-9_]+)");
+ var testid = testpat.exec(data)[1];
+
+ // store the violation in the persistent state
+ var s = getState(STATE);
+ if (!s) {
+ s = "{}";
+ }
+ s = JSON.parse(s);
+ if (!s) {
+ s = {};
+ }
+
+ if (!s[testid]) {
+ s[testid] = 0;
+ }
+ s[testid]++;
+ setState(STATE, JSON.stringify(s));
+ }
+}
diff --git a/dom/security/test/csp/file_bug885433_allows.html b/dom/security/test/csp/file_bug885433_allows.html
new file mode 100644
index 0000000000..c88981c4fe
--- /dev/null
+++ b/dom/security/test/csp/file_bug885433_allows.html
@@ -0,0 +1,39 @@
+<!doctype html>
+<!--
+The Content-Security-Policy header for this file is:
+
+ Content-Security-Policy: img-src 'self';
+
+It does not include any of the default-src, script-src, or style-src
+directives. It should allow the use of unsafe-inline and unsafe-eval on
+scripts, and unsafe-inline on styles, because no directives related to scripts
+or styles are specified.
+-->
+<html>
+<body>
+ <ol>
+ <li id="unsafe-inline-script-allowed">Inline script allowed (this text should be green)</li>
+ <li id="unsafe-eval-script-allowed">Eval script allowed (this text should be green)</li>
+ <li id="unsafe-inline-style-allowed">Inline style allowed (this text should be green)</li>
+ </ol>
+
+ <script>
+ // Use inline script to set a style attribute
+ document.getElementById("unsafe-inline-script-allowed").style.color = "green";
+
+ // Use eval to set a style attribute
+ // try/catch is used because CSP causes eval to throw an exception when it
+ // is blocked, which would derail the rest of the tests in this file.
+ try {
+ // eslint-disable-next-line no-eval
+ eval('document.getElementById("unsafe-eval-script-allowed").style.color = "green";');
+ } catch (e) {}
+ </script>
+
+ <style>
+ li#unsafe-inline-style-allowed {
+ color: green;
+ }
+ </style>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_bug885433_allows.html^headers^ b/dom/security/test/csp/file_bug885433_allows.html^headers^
new file mode 100644
index 0000000000..767b9ca926
--- /dev/null
+++ b/dom/security/test/csp/file_bug885433_allows.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: img-src 'self';
diff --git a/dom/security/test/csp/file_bug885433_blocks.html b/dom/security/test/csp/file_bug885433_blocks.html
new file mode 100644
index 0000000000..b9a8aeb03b
--- /dev/null
+++ b/dom/security/test/csp/file_bug885433_blocks.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<!--
+The Content-Security-Policy header for this file is:
+
+ Content-Security-Policy: default-src 'self';
+
+The Content-Security-Policy header for this file includes the default-src
+directive, which triggers the default behavior of blocking unsafe-inline and
+unsafe-eval on scripts, and unsafe-inline on styles.
+-->
+<html>
+<body>
+ <ol>
+ <li id="unsafe-inline-script-blocked">Inline script blocked (this text should be black)</li>
+ <li id="unsafe-eval-script-blocked">Eval script blocked (this text should be black)</li>
+ <li id="unsafe-inline-style-blocked">Inline style blocked (this text should be black)</li>
+ </ol>
+
+ <script>
+ // Use inline script to set a style attribute
+ document.getElementById("unsafe-inline-script-blocked").style.color = "green";
+
+ // Use eval to set a style attribute
+ // try/catch is used because CSP causes eval to throw an exception when it
+ // is blocked, which would derail the rest of the tests in this file.
+ try {
+ // eslint-disable-next-line no-eval
+ eval('document.getElementById("unsafe-eval-script-blocked").style.color = "green";');
+ } catch (e) {}
+ </script>
+
+ <style>
+ li#unsafe-inline-style-blocked {
+ color: green;
+ }
+ </style>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_bug885433_blocks.html^headers^ b/dom/security/test/csp/file_bug885433_blocks.html^headers^
new file mode 100644
index 0000000000..f82598b673
--- /dev/null
+++ b/dom/security/test/csp/file_bug885433_blocks.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src 'self';
diff --git a/dom/security/test/csp/file_bug886164.html b/dom/security/test/csp/file_bug886164.html
new file mode 100644
index 0000000000..ec8c9e7e92
--- /dev/null
+++ b/dom/security/test/csp/file_bug886164.html
@@ -0,0 +1,15 @@
+<html>
+<head> <meta charset="utf-8"> </head>
+ <body>
+ <!-- sandbox="allow-same-origin" -->
+ <!-- Content-Security-Policy: default-src 'self' -->
+
+ <!-- these should be stopped by CSP -->
+ <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img_bad&type=img/png"> </img>
+
+ <!-- these should load ok -->
+ <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img_good&type=img/png" />
+ <script src='/tests/dom/security/test/csp/file_CSP.sjs?testid=scripta_bad&type=text/javascript'></script>
+
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_bug886164.html^headers^ b/dom/security/test/csp/file_bug886164.html^headers^
new file mode 100644
index 0000000000..4c6fa3c26a
--- /dev/null
+++ b/dom/security/test/csp/file_bug886164.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src 'self'
diff --git a/dom/security/test/csp/file_bug886164_2.html b/dom/security/test/csp/file_bug886164_2.html
new file mode 100644
index 0000000000..83d36c55ae
--- /dev/null
+++ b/dom/security/test/csp/file_bug886164_2.html
@@ -0,0 +1,14 @@
+<html>
+<head> <meta charset="utf-8"> </head>
+ <body>
+ <!-- sandbox -->
+ <!-- Content-Security-Policy: default-src 'self' -->
+
+ <!-- these should be stopped by CSP -->
+ <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img2_bad&type=img/png"> </img>
+
+ <!-- these should load ok -->
+ <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img2a_good&type=img/png" />
+
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_bug886164_2.html^headers^ b/dom/security/test/csp/file_bug886164_2.html^headers^
new file mode 100644
index 0000000000..4c6fa3c26a
--- /dev/null
+++ b/dom/security/test/csp/file_bug886164_2.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src 'self'
diff --git a/dom/security/test/csp/file_bug886164_3.html b/dom/security/test/csp/file_bug886164_3.html
new file mode 100644
index 0000000000..8b4313000f
--- /dev/null
+++ b/dom/security/test/csp/file_bug886164_3.html
@@ -0,0 +1,12 @@
+<html>
+<head> <meta charset="utf-8"> </head>
+ <body>
+ <!-- sandbox -->
+ <!-- Content-Security-Policy: default-src 'none' -->
+
+ <!-- these should be stopped by CSP -->
+ <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img3_bad&type=img/png"> </img>
+ <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img3a_bad&type=img/png" />
+
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_bug886164_3.html^headers^ b/dom/security/test/csp/file_bug886164_3.html^headers^
new file mode 100644
index 0000000000..6581fd425e
--- /dev/null
+++ b/dom/security/test/csp/file_bug886164_3.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src 'none'
diff --git a/dom/security/test/csp/file_bug886164_4.html b/dom/security/test/csp/file_bug886164_4.html
new file mode 100644
index 0000000000..41137ea017
--- /dev/null
+++ b/dom/security/test/csp/file_bug886164_4.html
@@ -0,0 +1,12 @@
+<html>
+<head> <meta charset="utf-8"> </head>
+ <body>
+ <!-- sandbox -->
+ <!-- Content-Security-Policy: default-src 'none' -->
+
+ <!-- these should be stopped by CSP -->
+ <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img4_bad&type=img/png"> </img>
+ <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img4a_bad&type=img/png" />
+
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_bug886164_4.html^headers^ b/dom/security/test/csp/file_bug886164_4.html^headers^
new file mode 100644
index 0000000000..6581fd425e
--- /dev/null
+++ b/dom/security/test/csp/file_bug886164_4.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src 'none'
diff --git a/dom/security/test/csp/file_bug886164_5.html b/dom/security/test/csp/file_bug886164_5.html
new file mode 100644
index 0000000000..82c10f20c0
--- /dev/null
+++ b/dom/security/test/csp/file_bug886164_5.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head> <meta charset="utf-8"> </head>
+<script type="text/javascript">
+ function ok(result, desc) {
+ window.parent.postMessage({ok: result, desc}, "*");
+ }
+
+ function doStuff() {
+ ok(true, "documents sandboxed with allow-scripts should be able to run inline scripts");
+ }
+</script>
+<script src='file_iframe_sandbox_pass.js'></script>
+<body onLoad='ok(true, "documents sandboxed with allow-scripts should be able to run script from event listeners");doStuff();'>
+ I am sandboxed but with only inline "allow-scripts"
+
+ <!-- sandbox="allow-scripts" -->
+ <!-- Content-Security-Policy: default-src 'none' 'unsafe-inline'-->
+
+ <!-- these should be stopped by CSP -->
+ <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img5_bad&type=img/png" />
+ <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img5a_bad&type=img/png"> </img>
+ <script src='/tests/dom/security/test/csp/file_CSP.sjs?testid=script5_bad&type=text/javascript'></script>
+ <script src='http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=script5a_bad&type=text/javascript'></script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_bug886164_5.html^headers^ b/dom/security/test/csp/file_bug886164_5.html^headers^
new file mode 100644
index 0000000000..3abc190552
--- /dev/null
+++ b/dom/security/test/csp/file_bug886164_5.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src 'none' 'unsafe-inline';
diff --git a/dom/security/test/csp/file_bug886164_6.html b/dom/security/test/csp/file_bug886164_6.html
new file mode 100644
index 0000000000..f6567b470e
--- /dev/null
+++ b/dom/security/test/csp/file_bug886164_6.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<script type="text/javascript">
+ function ok(result, desc) {
+ window.parent.postMessage({ok: result, desc}, "*");
+ }
+
+ function doStuff() {
+ ok(true, "documents sandboxed with allow-scripts should be able to run inline scripts");
+
+ document.getElementById('a_form').submit();
+
+ // trigger the javascript: url test
+ sendMouseEvent({type:'click'}, 'a_link');
+ }
+</script>
+<script src='file_iframe_sandbox_pass.js'></script>
+<body onLoad='ok(true, "documents sandboxed with allow-scripts should be able to run script from event listeners");doStuff();'>
+ I am sandboxed but with "allow-scripts"
+ <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img6_bad&type=img/png"> </img>
+ <script src='http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=script6_bad&type=text/javascript'></script>
+
+ <form method="get" action="file_iframe_sandbox_form_fail.html" id="a_form">
+ First name: <input type="text" name="firstname">
+ Last name: <input type="text" name="lastname">
+ <input type="submit" onclick="doSubmit()" id="a_button">
+ </form>
+
+ <a href = 'javascript:ok(true, "documents sandboxed with allow-scripts should be able to run script from javascript: URLs");' id='a_link'>click me</a>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_bug886164_6.html^headers^ b/dom/security/test/csp/file_bug886164_6.html^headers^
new file mode 100644
index 0000000000..6f9fc3f25d
--- /dev/null
+++ b/dom/security/test/csp/file_bug886164_6.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src 'self' 'unsafe-inline';
diff --git a/dom/security/test/csp/file_bug888172.html b/dom/security/test/csp/file_bug888172.html
new file mode 100644
index 0000000000..8c0fc46066
--- /dev/null
+++ b/dom/security/test/csp/file_bug888172.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<html>
+ <body>
+ <ol>
+ <li id="unsafe-inline-script">Inline script (green if allowed, black if blocked)</li>
+ <li id="unsafe-eval-script">Eval script (green if allowed, black if blocked)</li>
+ <li id="unsafe-inline-style">Inline style (green if allowed, black if blocked)</li>
+ </ol>
+
+ <script>
+ // Use inline script to set a style attribute
+ document.getElementById("unsafe-inline-script").style.color = "green";
+
+ // Use eval to set a style attribute
+ // try/catch is used because CSP causes eval to throw an exception when it
+ // is blocked, which would derail the rest of the tests in this file.
+ try {
+ // eslint-disable-next-line no-eval
+ eval('document.getElementById("unsafe-eval-script").style.color = "green";');
+ } catch (e) {}
+ </script>
+
+ <style>
+ li#unsafe-inline-style {
+ color: green;
+ }
+ </style>
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_bug888172.sjs b/dom/security/test/csp/file_bug888172.sjs
new file mode 100644
index 0000000000..422162afc2
--- /dev/null
+++ b/dom/security/test/csp/file_bug888172.sjs
@@ -0,0 +1,49 @@
+// SJS file for CSP mochitests
+
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+
+function loadHTMLFromFile(path) {
+ // Load the HTML to return in the response from file.
+ // Since it's relative to the cwd of the test runner, we start there and
+ // append to get to the actual path of the file.
+ var testHTMLFile = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+ var dirs = path.split("/");
+ for (var i = 0; i < dirs.length; i++) {
+ testHTMLFile.append(dirs[i]);
+ }
+ var testHTMLFileStream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ testHTMLFileStream.init(testHTMLFile, -1, 0, 0);
+ var testHTML = NetUtil.readInputStreamToString(
+ testHTMLFileStream,
+ testHTMLFileStream.available()
+ );
+ return testHTML;
+}
+
+function handleRequest(request, response) {
+ var query = {};
+ request.queryString.split("&").forEach(function (val) {
+ var [name, value] = val.split("=");
+ query[name] = unescape(value);
+ });
+
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ // Deliver the CSP policy encoded in the URI
+ if (query.csp) {
+ response.setHeader("Content-Security-Policy", unescape(query.csp), false);
+ }
+
+ // Send HTML to test allowed/blocked behaviors
+ response.setHeader("Content-Type", "text/html", false);
+ response.write(
+ loadHTMLFromFile("tests/dom/security/test/csp/file_bug888172.html")
+ );
+}
diff --git a/dom/security/test/csp/file_bug909029_none.html b/dom/security/test/csp/file_bug909029_none.html
new file mode 100644
index 0000000000..0d4934a4a3
--- /dev/null
+++ b/dom/security/test/csp/file_bug909029_none.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<html>
+ <head>
+ <!-- file_CSP.sjs mocks a resource load -->
+ <link rel='stylesheet' type='text/css'
+ href='file_CSP.sjs?testid=noneExternalStylesBlocked&type=text/css' />
+ </head>
+ <body>
+ <p id="inline-style">This should be green</p>
+ <p id="inline-script">This should be black</p>
+ <style>
+ p#inline-style { color:rgb(0, 128, 0); }
+ </style>
+ <script>
+ // Use inline script to set a style attribute
+ document.getElementById("inline-script").style.color = "rgb(0, 128, 0)";
+ </script>
+ <img src="file_CSP.sjs?testid=noneExternalImgLoaded&type=img/png" />
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_bug909029_none.html^headers^ b/dom/security/test/csp/file_bug909029_none.html^headers^
new file mode 100644
index 0000000000..ecb3458750
--- /dev/null
+++ b/dom/security/test/csp/file_bug909029_none.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src * ; style-src 'none' 'unsafe-inline';
diff --git a/dom/security/test/csp/file_bug909029_star.html b/dom/security/test/csp/file_bug909029_star.html
new file mode 100644
index 0000000000..bcb907a965
--- /dev/null
+++ b/dom/security/test/csp/file_bug909029_star.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html>
+ <head>
+ <link rel='stylesheet' type='text/css'
+ href='file_CSP.sjs?testid=starExternalStylesLoaded&type=text/css' />
+ </head>
+ <body>
+ <p id="inline-style">This should be green</p>
+ <p id="inline-script">This should be black</p>
+ <style>
+ p#inline-style { color:rgb(0, 128, 0); }
+ </style>
+ <script>
+ // Use inline script to set a style attribute
+ document.getElementById("inline-script").style.color = "rgb(0, 128, 0)";
+ </script>
+ <img src="file_CSP.sjs?testid=starExternalImgLoaded&type=img/png" />
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_bug909029_star.html^headers^ b/dom/security/test/csp/file_bug909029_star.html^headers^
new file mode 100644
index 0000000000..eccc1c0110
--- /dev/null
+++ b/dom/security/test/csp/file_bug909029_star.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src *; style-src * 'unsafe-inline';
diff --git a/dom/security/test/csp/file_bug910139.sjs b/dom/security/test/csp/file_bug910139.sjs
new file mode 100644
index 0000000000..fb27f2e4a1
--- /dev/null
+++ b/dom/security/test/csp/file_bug910139.sjs
@@ -0,0 +1,56 @@
+// Server side js file for bug 910139, see file test_bug910139.html for details.
+
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+
+function loadResponseFromFile(path) {
+ var testHTMLFile = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+ var dirs = path.split("/");
+ for (var i = 0; i < dirs.length; i++) {
+ testHTMLFile.append(dirs[i]);
+ }
+ var testHTMLFileStream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ testHTMLFileStream.init(testHTMLFile, -1, 0, 0);
+ var testHTML = NetUtil.readInputStreamToString(
+ testHTMLFileStream,
+ testHTMLFileStream.available()
+ );
+ return testHTML;
+}
+
+var policies = [
+ "default-src 'self'; script-src 'self'", // CSP for checkAllowed
+ "default-src 'self'; script-src *.example.com", // CSP for checkBlocked
+];
+
+function getPolicy() {
+ var index;
+ // setState only accepts strings as arguments
+ if (!getState("counter")) {
+ index = 0;
+ setState("counter", index.toString());
+ } else {
+ index = parseInt(getState("counter"));
+ ++index;
+ setState("counter", index.toString());
+ }
+ return policies[index];
+}
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ // set the required CSP
+ response.setHeader("Content-Security-Policy", getPolicy(), false);
+
+ // return the requested XML file.
+ response.write(
+ loadResponseFromFile("tests/dom/security/test/csp/file_bug910139.xml")
+ );
+}
diff --git a/dom/security/test/csp/file_bug910139.xml b/dom/security/test/csp/file_bug910139.xml
new file mode 100644
index 0000000000..29feba9418
--- /dev/null
+++ b/dom/security/test/csp/file_bug910139.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<?xml-stylesheet type="text/xsl" href="file_bug910139.xsl"?>
+<catalog>
+ <cd>
+ <title>Empire Burlesque</title>
+ <artist>Bob Dylan</artist>
+ <country>USA</country>
+ <company>Columbia</company>
+ <price>10.90</price>
+ <year>1985</year>
+ </cd>
+ <cd>
+ <title>Hide your heart</title>
+ <artist>Bonnie Tyler</artist>
+ <country>UK</country>
+ <company>CBS Records</company>
+ <price>9.90</price>
+ <year>1988</year>
+ </cd>
+ <cd>
+ <title>Greatest Hits</title>
+ <artist>Dolly Parton</artist>
+ <country>USA</country>
+ <company>RCA</company>
+ <price>9.90</price>
+ <year>1982</year>
+ </cd>
+</catalog>
diff --git a/dom/security/test/csp/file_bug910139.xsl b/dom/security/test/csp/file_bug910139.xsl
new file mode 100644
index 0000000000..b99abca099
--- /dev/null
+++ b/dom/security/test/csp/file_bug910139.xsl
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!-- Edited by XMLSpy® -->
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+<xsl:template match="/">
+ <html>
+ <body>
+ <h2 id="xsltheader">this xml file should be formatted using an xsl file(lower iframe should contain xml dump)!</h2>
+ <table border="1">
+ <tr bgcolor="#990099">
+ <th>Title</th>
+ <th>Artist</th>
+ <th>Price</th>
+ </tr>
+ <xsl:for-each select="catalog/cd">
+ <tr>
+ <td><xsl:value-of select="title"/></td>
+ <td><xsl:value-of select="artist"/></td>
+ <td><xsl:value-of select="price"/></td>
+ </tr>
+ </xsl:for-each>
+ </table>
+ </body>
+ </html>
+</xsl:template>
+</xsl:stylesheet>
+
diff --git a/dom/security/test/csp/file_bug941404.html b/dom/security/test/csp/file_bug941404.html
new file mode 100644
index 0000000000..3a2e636e0b
--- /dev/null
+++ b/dom/security/test/csp/file_bug941404.html
@@ -0,0 +1,27 @@
+<html>
+<head> <meta charset="utf-8"> </head>
+ <body>
+
+ <!-- this should be allowed (no CSP)-->
+ <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img_good&type=img/png"> </img>
+
+
+ <script type="text/javascript">
+ var req = new XMLHttpRequest();
+ req.onload = function() {
+ //this should be allowed (no CSP)
+ try {
+ var img = document.createElement("img");
+ img.src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img2_good&type=img/png";
+ document.body.appendChild(img);
+ } catch(e) {
+ console.log("yo: "+e);
+ }
+ };
+ req.open("get", "file_bug941404_xhr.html", true);
+ req.responseType = "document";
+ req.send();
+ </script>
+
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_bug941404_xhr.html b/dom/security/test/csp/file_bug941404_xhr.html
new file mode 100644
index 0000000000..22e176f208
--- /dev/null
+++ b/dom/security/test/csp/file_bug941404_xhr.html
@@ -0,0 +1,5 @@
+<html>
+<head> <meta charset="utf-8"> </head>
+ <body>
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_bug941404_xhr.html^headers^ b/dom/security/test/csp/file_bug941404_xhr.html^headers^
new file mode 100644
index 0000000000..1e5f70cc37
--- /dev/null
+++ b/dom/security/test/csp/file_bug941404_xhr.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src 'none' 'unsafe-inline' 'unsafe-eval'
diff --git a/dom/security/test/csp/file_child-src_iframe.html b/dom/security/test/csp/file_child-src_iframe.html
new file mode 100644
index 0000000000..18749011b9
--- /dev/null
+++ b/dom/security/test/csp/file_child-src_iframe.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Bug 1045891</title>
+ </head>
+ <body>
+ <iframe id="testframe"> </iframe>
+ <script type="text/javascript">
+ page_id = window.location.hash.substring(1);
+
+ function executeTest(ev) {
+ testframe = document.getElementById('testframe');
+ testframe.contentWindow.postMessage({id:page_id, message:"execute"}, 'http://mochi.test:8888');
+ }
+
+ function reportError(ev) {
+ window.parent.postMessage({id:page_id, message:"blocked"}, 'http://mochi.test:8888');
+ cleanup();
+ }
+
+ function recvMessage(ev) {
+ if (ev.data.id == page_id) {
+ window.parent.postMessage({id:ev.data.id, message:ev.data.message}, 'http://mochi.test:8888');
+ cleanup();
+ }
+ }
+
+ function cleanup() {
+ testframe = document.getElementById('testframe');
+ window.removeEventListener('message', recvMessage);
+ testframe.removeEventListener('load', executeTest);
+ testframe.removeEventListener('error', reportError);
+ }
+
+
+ window.addEventListener('message', recvMessage);
+
+ try {
+ // Please note that file_testserver.sjs?foo does not return a response.
+ // For testing purposes this is not necessary because we only want to check
+ // whether CSP allows or blocks the load.
+ src = "file_testserver.sjs";
+ src += "?file=" + escape("tests/dom/security/test/csp/file_child-src_inner_frame.html");
+ src += "#" + escape(page_id);
+ testframe = document.getElementById('testframe');
+
+ testframe.addEventListener('load', executeTest);
+ testframe.addEventListener('error', reportError);
+
+ testframe.src = src;
+ }
+ catch (e) {
+ if (e.message.match(/Failed to load script/)) {
+ window.parent.postMessage({id:page_id, message:"blocked"}, 'http://mochi.test:8888');
+ } else {
+ window.parent.postMessage({id:page_id, message:"exception"}, 'http://mochi.test:8888');
+ }
+ }
+ </script>
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_child-src_inner_frame.html b/dom/security/test/csp/file_child-src_inner_frame.html
new file mode 100644
index 0000000000..f0c4e66fa0
--- /dev/null
+++ b/dom/security/test/csp/file_child-src_inner_frame.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Bug 1045891</title>
+ </head>
+ <body>
+ <iframe id="innermosttestframe"> </iframe>
+ <script type="text/javascript">
+ page_id = window.location.hash.substring(1);
+
+ function recvMessage(ev) {
+ if (ev.data.id == page_id) {
+ window.parent.postMessage({id:ev.data.id, message:'allowed'}, 'http://mochi.test:8888');
+ window.removeEventListener('message', recvMessage);
+ }
+ }
+
+ window.addEventListener('message', recvMessage);
+ </script>
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_child-src_service_worker.html b/dom/security/test/csp/file_child-src_service_worker.html
new file mode 100644
index 0000000000..b291a4a4e8
--- /dev/null
+++ b/dom/security/test/csp/file_child-src_service_worker.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Bug 1045891</title>
+ </head>
+ <body>
+ <script type="text/javascript">
+ page_id = window.location.hash.substring(1);
+ try {
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.register(
+ 'file_child-src_service_worker.js',
+ { scope: './' + page_id + '/' }
+ ).then(function(reg)
+ {
+ // registration worked
+ reg.unregister().then(function() {
+ window.parent.postMessage({id:page_id, message:"allowed"}, 'http://mochi.test:8888');
+ });
+ }).catch(function(error) {
+ // registration failed
+ window.parent.postMessage({id:page_id, message:"blocked"}, 'http://mochi.test:8888');
+ });
+ };
+ } catch(ex) {
+ window.parent.postMessage({id:page_id, message:"exception"}, 'http://mochi.test:8888');
+ }
+ </script>
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_child-src_service_worker.js b/dom/security/test/csp/file_child-src_service_worker.js
new file mode 100644
index 0000000000..b8445fb175
--- /dev/null
+++ b/dom/security/test/csp/file_child-src_service_worker.js
@@ -0,0 +1,3 @@
+this.addEventListener("install", function (event) {
+ close();
+});
diff --git a/dom/security/test/csp/file_child-src_shared_worker-redirect.html b/dom/security/test/csp/file_child-src_shared_worker-redirect.html
new file mode 100644
index 0000000000..313915302e
--- /dev/null
+++ b/dom/security/test/csp/file_child-src_shared_worker-redirect.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Bug 1045891</title>
+ </head>
+ <body>
+ <script type="text/javascript">
+ page_id = window.location.hash.substring(1);
+ var redir = 'none';
+
+ page_id.split('_').forEach(function (val) {
+ var [name, value] = val.split('-');
+ if (name == 'redir') {
+ redir = unescape(value);
+ }
+ });
+
+ try {
+ worker = new SharedWorker('file_redirect_worker.sjs?path='
+ + escape("/tests/dom/security/test/csp/file_child-src_shared_worker.js")
+ + "&redir=" + redir
+ + "&page_id=" + page_id,
+ page_id);
+ worker.port.start();
+
+ worker.onerror = function(evt) {
+ evt.preventDefault();
+ window.parent.postMessage({id:page_id, message:"blocked"},
+ 'http://mochi.test:8888');
+ }
+
+ worker.port.onmessage = function(ev) {
+ window.parent.postMessage({id:page_id, message:"allowed"}, 'http://mochi.test:8888');
+ };
+
+ worker.onerror = function() {
+ window.parent.postMessage({id:page_id, message:"blocked"}, 'http://mochi.test:8888');
+ };
+
+ worker.port.postMessage('foo');
+ }
+ catch (e) {
+ window.parent.postMessage({id:page_id, message:"blocked"}, 'http://mochi.test:8888');
+ }
+ </script>
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_child-src_shared_worker.html b/dom/security/test/csp/file_child-src_shared_worker.html
new file mode 100644
index 0000000000..ce0c0261ed
--- /dev/null
+++ b/dom/security/test/csp/file_child-src_shared_worker.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Bug 1045891</title>
+ </head>
+ <body>
+ <script type="text/javascript">
+ page_id = window.location.hash.substring(1);
+ try {
+ worker = new SharedWorker(
+ 'file_testserver.sjs?file='+
+ escape("tests/dom/security/test/csp/file_child-src_shared_worker.js") +
+ "&type=text/javascript",
+ page_id);
+ worker.port.start();
+
+ worker.onerror = function(evt) {
+ evt.preventDefault();
+ window.parent.postMessage({id:page_id, message:"blocked"},
+ 'http://mochi.test:8888');
+ }
+
+ worker.port.onmessage = function(ev) {
+ window.parent.postMessage({id:page_id, message:"allowed"},
+ 'http://mochi.test:8888');
+ };
+ worker.port.postMessage('foo');
+ }
+ catch (e) {
+ window.parent.postMessage({id:page_id, message:"blocked"},
+ 'http://mochi.test:8888');
+ }
+ </script>
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_child-src_shared_worker.js b/dom/security/test/csp/file_child-src_shared_worker.js
new file mode 100644
index 0000000000..dbcdf9c9d7
--- /dev/null
+++ b/dom/security/test/csp/file_child-src_shared_worker.js
@@ -0,0 +1,8 @@
+onconnect = function (e) {
+ var port = e.ports[0];
+ port.addEventListener("message", function (e) {
+ port.postMessage("success");
+ });
+
+ port.start();
+};
diff --git a/dom/security/test/csp/file_child-src_shared_worker_data.html b/dom/security/test/csp/file_child-src_shared_worker_data.html
new file mode 100644
index 0000000000..a4befe4ca3
--- /dev/null
+++ b/dom/security/test/csp/file_child-src_shared_worker_data.html
@@ -0,0 +1,37 @@
+
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Bug 1045891</title>
+ </head>
+ <body>
+ <script type="text/javascript">
+ var page_id = window.location.hash.substring(1);
+ var shared_worker = "onconnect = function(e) { " +
+ "var port = e.ports[0];" +
+ "port.addEventListener('message'," +
+ "function(e) { port.postMessage('success'); });" +
+ "port.start(); }";
+
+ try {
+ var worker = new SharedWorker('data:application/javascript;charset=UTF-8,'+
+ escape(shared_worker), page_id);
+ worker.port.start();
+
+ worker.onerror = function(evt) {
+ evt.preventDefault();
+ window.parent.postMessage({id:page_id, message:"blocked"}, 'http://mochi.test:8888');
+ }
+
+ worker.port.onmessage = function(ev) {
+ window.parent.postMessage({id:page_id, message:"allowed"}, 'http://mochi.test:8888');
+ };
+
+ worker.port.postMessage('foo');
+ }
+ catch (e) {
+ window.parent.postMessage({id:page_id, message:"blocked"}, 'http://mochi.test:8888');
+ }
+ </script>
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_child-src_worker-redirect.html b/dom/security/test/csp/file_child-src_worker-redirect.html
new file mode 100644
index 0000000000..b0029935c2
--- /dev/null
+++ b/dom/security/test/csp/file_child-src_worker-redirect.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Bug 1045891</title>
+ </head>
+ <body>
+ <script type="text/javascript">
+ var page_id = window.location.hash.substring(1);
+ var redir = 'none';
+
+ page_id.split('_').forEach(function (val) {
+ var [name, value] = val.split('-');
+ if (name == 'redir') {
+ redir = unescape(value);
+ }
+ });
+
+ try {
+ worker = new Worker('file_redirect_worker.sjs?path='
+ + escape("/tests/dom/security/test/csp/file_child-src_worker.js")
+ + "&redir=" + redir
+ + "&page_id=" + page_id
+ );
+
+ worker.onerror = function(error) {
+ // this means CSP blocked it
+ var msg = !("message" in error) ? "blocked" : e.message;
+ window.parent.postMessage({id:page_id, message:msg}, 'http://mochi.test:8888');
+ error.preventDefault();
+ };
+
+ worker.onmessage = function(ev) {
+ window.parent.postMessage({id:page_id, message:"allowed"}, 'http://mochi.test:8888');
+
+ };
+ worker.postMessage('foo');
+ }
+ catch (e) {
+ if (e.message.match(/Failed to load script/)) {
+ window.parent.postMessage({id:page_id, message:"blocked"}, 'http://mochi.test:8888');
+ } else {
+ window.parent.postMessage({id:page_id, message:"exception"}, 'http://mochi.test:8888');
+ }
+ }
+ </script>
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_child-src_worker.html b/dom/security/test/csp/file_child-src_worker.html
new file mode 100644
index 0000000000..a9fdbb3282
--- /dev/null
+++ b/dom/security/test/csp/file_child-src_worker.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Bug 1045891</title>
+ </head>
+ <body>
+ <script type="text/javascript">
+ page_id = window.location.hash.substring(1);
+ try {
+ worker = new Worker('file_testserver.sjs?file='+
+ escape("tests/dom/security/test/csp/file_child-src_worker.js")
+ +"&type=text/javascript");
+
+ worker.onerror = function(e) {
+ window.parent.postMessage({id:page_id, message:"blocked"}, 'http://mochi.test:8888');
+ e.preventDefault();
+ }
+
+ worker.onmessage = function(ev) {
+ window.parent.postMessage({id:page_id, message:"allowed"}, 'http://mochi.test:8888');
+ }
+
+ worker.postMessage('foo');
+ }
+ catch (e) {
+ if (e.message.match(/Failed to load script/)) {
+ window.parent.postMessage({id:page_id, message:"blocked"}, 'http://mochi.test:8888');
+ } else {
+ window.parent.postMessage({id:page_id, message:"exception"}, 'http://mochi.test:8888');
+ }
+ }
+ </script>
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_child-src_worker.js b/dom/security/test/csp/file_child-src_worker.js
new file mode 100644
index 0000000000..a6bb5e8044
--- /dev/null
+++ b/dom/security/test/csp/file_child-src_worker.js
@@ -0,0 +1,3 @@
+onmessage = function (e) {
+ postMessage("worker");
+};
diff --git a/dom/security/test/csp/file_child-src_worker_data.html b/dom/security/test/csp/file_child-src_worker_data.html
new file mode 100644
index 0000000000..e9e22f01da
--- /dev/null
+++ b/dom/security/test/csp/file_child-src_worker_data.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Bug 1045891</title>
+ </head>
+ <body>
+ <script type="text/javascript">
+ page_id = window.location.hash.substring(1);
+ try {
+ worker = new Worker('data:application/javascript;charset=UTF-8,'+escape('onmessage = function(e) { postMessage("worker"); };'));
+
+ worker.onerror = function(e) {
+ window.parent.postMessage({id:page_id, message:"blocked"}, 'http://mochi.test:8888');
+ e.preventDefault();
+ }
+
+ worker.onmessage = function(ev) {
+ window.parent.postMessage({id:page_id, message:"allowed"}, 'http://mochi.test:8888');
+ }
+
+ worker.postMessage('foo');
+ }
+ catch (e) {
+ if (e.message.match(/Failed to load script/)) {
+ window.parent.postMessage({id:page_id, message:"blocked"}, 'http://mochi.test:8888');
+ } else {
+ console.log(e);
+ window.parent.postMessage({id:page_id, message:"exception"}, 'http://mochi.test:8888');
+ }
+ }
+ </script>
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_connect-src-fetch.html b/dom/security/test/csp/file_connect-src-fetch.html
new file mode 100644
index 0000000000..ff9b2f740b
--- /dev/null
+++ b/dom/security/test/csp/file_connect-src-fetch.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Bug 1139667 - Test mapping of fetch() to connect-src</title>
+ </head>
+ <body>
+ <script type="text/javascript">
+
+ // Please note that file_testserver.sjs?foo does not return a response.
+ // For testing purposes this is not necessary because we only want to check
+ // whether CSP allows or blocks the load.
+ fetch( "file_testserver.sjs?foo");
+
+ </script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_connect-src.html b/dom/security/test/csp/file_connect-src.html
new file mode 100644
index 0000000000..17a940a0e0
--- /dev/null
+++ b/dom/security/test/csp/file_connect-src.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Bug 1031530 - Test mapping of XMLHttpRequest to connect-src</title>
+ </head>
+ <body>
+ <script type="text/javascript">
+
+ try {
+ // Please note that file_testserver.sjs?foo does not return a response.
+ // For testing purposes this is not necessary because we only want to check
+ // whether CSP allows or blocks the load.
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", "file_testserver.sjs?foo", false);
+ xhr.send(null);
+ }
+ catch (e) { }
+
+ </script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_csp_frame_ancestors_about_blank.html b/dom/security/test/csp/file_csp_frame_ancestors_about_blank.html
new file mode 100644
index 0000000000..6ce361a438
--- /dev/null
+++ b/dom/security/test/csp/file_csp_frame_ancestors_about_blank.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Helper file for Bug 1668071 - CSP frame-ancestors in about:blank</title>
+</head>
+<body>
+ CSP frame-ancestors in about:blank
+</body>
+</html>
diff --git a/dom/security/test/csp/file_csp_frame_ancestors_about_blank.html^headers^ b/dom/security/test/csp/file_csp_frame_ancestors_about_blank.html^headers^
new file mode 100644
index 0000000000..e5d129c3e8
--- /dev/null
+++ b/dom/security/test/csp/file_csp_frame_ancestors_about_blank.html^headers^
@@ -0,0 +1,2 @@
+Cache-Control: no-cache
+Content-Security-Policy: frame-ancestors http://mochi.test:8888 http://mochi.xorigin-test:8888
diff --git a/dom/security/test/csp/file_csp_meta_uir.html b/dom/security/test/csp/file_csp_meta_uir.html
new file mode 100644
index 0000000000..dba1030975
--- /dev/null
+++ b/dom/security/test/csp/file_csp_meta_uir.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Hello World</title>
+ <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
+</head>
+<body>
+ <script>
+ document.write("<a href='" + document.location.href.replace(/^https/, "http") + "' id='mylink'>Click me</a>");
+ </script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_data-uri_blocked.html b/dom/security/test/csp/file_data-uri_blocked.html
new file mode 100644
index 0000000000..59b7b25902
--- /dev/null
+++ b/dom/security/test/csp/file_data-uri_blocked.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1242019
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 587377</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <img width='1' height='1' title='' alt='' src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKTWlDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVN3WJP3Fj7f92UPVkLY8LGXbIEAIiOsCMgQWaIQkgBhhBASQMWFiApWFBURnEhVxILVCkidiOKgKLhnQYqIWotVXDjuH9yntX167+3t+9f7vOec5/zOec8PgBESJpHmomoAOVKFPDrYH49PSMTJvYACFUjgBCAQ5svCZwXFAADwA3l4fnSwP/wBr28AAgBw1S4kEsfh/4O6UCZXACCRAOAiEucLAZBSAMguVMgUAMgYALBTs2QKAJQAAGx5fEIiAKoNAOz0ST4FANipk9wXANiiHKkIAI0BAJkoRyQCQLsAYFWBUiwCwMIAoKxAIi4EwK4BgFm2MkcCgL0FAHaOWJAPQGAAgJlCLMwAIDgCAEMeE80DIEwDoDDSv+CpX3CFuEgBAMDLlc2XS9IzFLiV0Bp38vDg4iHiwmyxQmEXKRBmCeQinJebIxNI5wNMzgwAABr50cH+OD+Q5+bk4eZm52zv9MWi/mvwbyI+IfHf/ryMAgQAEE7P79pf5eXWA3DHAbB1v2upWwDaVgBo3/ldM9sJoFoK0Hr5i3k4/EAenqFQyDwdHAoLC+0lYqG9MOOLPv8z4W/gi372/EAe/tt68ABxmkCZrcCjg/1xYW52rlKO58sEQjFu9+cj/seFf/2OKdHiNLFcLBWK8ViJuFAiTcd5uVKRRCHJleIS6X8y8R+W/QmTdw0ArIZPwE62B7XLbMB+7gECiw5Y0nYAQH7zLYwaC5EAEGc0Mnn3AACTv/mPQCsBAM2XpOMAALzoGFyolBdMxggAAESggSqwQQcMwRSswA6cwR28wBcCYQZEQAwkwDwQQgbkgBwKoRiWQRlUwDrYBLWwAxqgEZrhELTBMTgN5+ASXIHrcBcGYBiewhi8hgkEQcgIE2EhOogRYo7YIs4IF5mOBCJhSDSSgKQg6YgUUSLFyHKkAqlCapFdSCPyLXIUOY1cQPqQ28ggMor8irxHMZSBslED1AJ1QLmoHxqKxqBz0XQ0D12AlqJr0Rq0Hj2AtqKn0UvodXQAfYqOY4DRMQ5mjNlhXIyHRWCJWBomxxZj5Vg1Vo81Yx1YN3YVG8CeYe8IJAKLgBPsCF6EEMJsgpCQR1hMWEOoJewjtBK6CFcJg4Qxwicik6hPtCV6EvnEeGI6sZBYRqwm7iEeIZ4lXicOE1+TSCQOyZLkTgohJZAySQtJa0jbSC2kU6Q+0hBpnEwm65Btyd7kCLKArCCXkbeQD5BPkvvJw+S3FDrFiOJMCaIkUqSUEko1ZT/lBKWfMkKZoKpRzame1AiqiDqfWkltoHZQL1OHqRM0dZolzZsWQ8ukLaPV0JppZ2n3aC/pdLoJ3YMeRZfQl9Jr6Afp5+mD9HcMDYYNg8dIYigZaxl7GacYtxkvmUymBdOXmchUMNcyG5lnmA+Yb1VYKvYqfBWRyhKVOpVWlX6V56pUVXNVP9V5qgtUq1UPq15WfaZGVbNQ46kJ1Bar1akdVbupNq7OUndSj1DPUV+jvl/9gvpjDbKGhUaghkijVGO3xhmNIRbGMmXxWELWclYD6yxrmE1iW7L57Ex2Bfsbdi97TFNDc6pmrGaRZp3mcc0BDsax4PA52ZxKziHODc57LQMtPy2x1mqtZq1+rTfaetq+2mLtcu0W7eva73VwnUCdLJ31Om0693UJuja6UbqFutt1z+o+02PreekJ9cr1Dund0Uf1bfSj9Rfq79bv0R83MDQINpAZbDE4Y/DMkGPoa5hpuNHwhOGoEctoupHEaKPRSaMnuCbuh2fjNXgXPmasbxxirDTeZdxrPGFiaTLbpMSkxeS+Kc2Ua5pmutG003TMzMgs3KzYrMnsjjnVnGueYb7ZvNv8jYWlRZzFSos2i8eW2pZ8ywWWTZb3rJhWPlZ5VvVW16xJ1lzrLOtt1ldsUBtXmwybOpvLtqitm63Edptt3xTiFI8p0in1U27aMez87ArsmuwG7Tn2YfYl9m32zx3MHBId1jt0O3xydHXMdmxwvOuk4TTDqcSpw+lXZxtnoXOd8zUXpkuQyxKXdpcXU22niqdun3rLleUa7rrStdP1o5u7m9yt2W3U3cw9xX2r+00umxvJXcM970H08PdY4nHM452nm6fC85DnL152Xlle+70eT7OcJp7WMG3I28Rb4L3Le2A6Pj1l+s7pAz7GPgKfep+Hvqa+It89viN+1n6Zfgf8nvs7+sv9j/i/4XnyFvFOBWABwQHlAb2BGoGzA2sDHwSZBKUHNQWNBbsGLww+FUIMCQ1ZH3KTb8AX8hv5YzPcZyya0RXKCJ0VWhv6MMwmTB7WEY6GzwjfEH5vpvlM6cy2CIjgR2yIuB9pGZkX+X0UKSoyqi7qUbRTdHF09yzWrORZ+2e9jvGPqYy5O9tqtnJ2Z6xqbFJsY+ybuIC4qriBeIf4RfGXEnQTJAntieTE2MQ9ieNzAudsmjOc5JpUlnRjruXcorkX5unOy553PFk1WZB8OIWYEpeyP+WDIEJQLxhP5aduTR0T8oSbhU9FvqKNolGxt7hKPJLmnVaV9jjdO31D+miGT0Z1xjMJT1IreZEZkrkj801WRNberM/ZcdktOZSclJyjUg1plrQr1zC3KLdPZisrkw3keeZtyhuTh8r35CP5c/PbFWyFTNGjtFKuUA4WTC+oK3hbGFt4uEi9SFrUM99m/ur5IwuCFny9kLBQuLCz2Lh4WfHgIr9FuxYji1MXdy4xXVK6ZHhp8NJ9y2jLspb9UOJYUlXyannc8o5Sg9KlpUMrglc0lamUycturvRauWMVYZVkVe9ql9VbVn8qF5VfrHCsqK74sEa45uJXTl/VfPV5bdra3kq3yu3rSOuk626s91m/r0q9akHV0IbwDa0b8Y3lG19tSt50oXpq9Y7NtM3KzQM1YTXtW8y2rNvyoTaj9nqdf13LVv2tq7e+2Sba1r/dd3vzDoMdFTve75TsvLUreFdrvUV99W7S7oLdjxpiG7q/5n7duEd3T8Wej3ulewf2Re/ranRvbNyvv7+yCW1SNo0eSDpw5ZuAb9qb7Zp3tXBaKg7CQeXBJ9+mfHvjUOihzsPcw83fmX+39QjrSHkr0jq/dawto22gPaG97+iMo50dXh1Hvrf/fu8x42N1xzWPV56gnSg98fnkgpPjp2Snnp1OPz3Umdx590z8mWtdUV29Z0PPnj8XdO5Mt1/3yfPe549d8Lxw9CL3Ytslt0utPa49R35w/eFIr1tv62X3y+1XPK509E3rO9Hv03/6asDVc9f41y5dn3m978bsG7duJt0cuCW69fh29u0XdwruTNxdeo94r/y+2v3qB/oP6n+0/rFlwG3g+GDAYM/DWQ/vDgmHnv6U/9OH4dJHzEfVI0YjjY+dHx8bDRq98mTOk+GnsqcTz8p+Vv9563Or59/94vtLz1j82PAL+YvPv655qfNy76uprzrHI8cfvM55PfGm/K3O233vuO+638e9H5ko/ED+UPPR+mPHp9BP9z7nfP78L/eE8/sl0p8zAAA6NGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxMTEgNzkuMTU4MzI1LCAyMDE1LzA5LzEwLTAxOjEwOjIwICAgICAgICAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgICAgICAgICB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIgogICAgICAgICAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgICAgICAgICAgIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAgICAgPHhtcDpDcmVhdG9yVG9vbD5BZG9iZSBQaG90b3Nob3AgQ0MgMjAxNSAoTWFjaW50b3NoKTwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8eG1wOkNyZWF0ZURhdGU+MjAxNi0wMi0wOFQxMzoyNjo1MSswOTowMDwveG1wOkNyZWF0ZURhdGU+CiAgICAgICAgIDx4bXA6TWV0YWRhdGFEYXRlPjIwMTYtMDItMDhUMTM6MjY6NTErMDk6MDA8L3htcDpNZXRhZGF0YURhdGU+CiAgICAgICAgIDx4bXA6TW9kaWZ5RGF0ZT4yMDE2LTAyLTA4VDEzOjI2OjUxKzA5OjAwPC94bXA6TW9kaWZ5RGF0ZT4KICAgICAgICAgPHhtcE1NOkluc3RhbmNlSUQ+eG1wLmlpZDplN2QyMjJjOS03Nzg2LTRhMjktOTlhMy0yYjRjNWI4YTA5MWY8L3htcE1NOkluc3RhbmNlSUQ+CiAgICAgICAgIDx4bXBNTTpEb2N1bWVudElEPmFkb2JlOmRvY2lkOnBob3Rvc2hvcDo5OWE3ZTJmOC0wZWFhLTExNzktYTE4NS05M2Y1MjZhZGU2ZDg8L3htcE1NOkRvY3VtZW50SUQ+CiAgICAgICAgIDx4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ+eG1wLmRpZDoyYmY3M2M3Ny03NzczLTQzNjAtYThiZS1kY2UyZTgzMWQ2YmM8L3htcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD4KICAgICAgICAgPHhtcE1NOkhpc3Rvcnk+CiAgICAgICAgICAgIDxyZGY6U2VxPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5jcmVhdGVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDppbnN0YW5jZUlEPnhtcC5paWQ6MmJmNzNjNzctNzc3My00MzYwLWE4YmUtZGNlMmU4MzFkNmJjPC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE2LTAyLTA4VDEzOjI2OjUxKzA5OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ0MgMjAxNSAoTWFjaW50b3NoKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPnNhdmVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDppbnN0YW5jZUlEPnhtcC5paWQ6ZTdkMjIyYzktNzc4Ni00YTI5LTk5YTMtMmI0YzViOGEwOTFmPC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE2LTAyLTA4VDEzOjI2OjUxKzA5OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ0MgMjAxNSAoTWFjaW50b3NoKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+Lzwvc3RFdnQ6Y2hhbmdlZD4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgIDwvcmRmOlNlcT4KICAgICAgICAgPC94bXBNTTpIaXN0b3J5PgogICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3BuZzwvZGM6Zm9ybWF0PgogICAgICAgICA8cGhvdG9zaG9wOkNvbG9yTW9kZT4zPC9waG90b3Nob3A6Q29sb3JNb2RlPgogICAgICAgICA8cGhvdG9zaG9wOklDQ1Byb2ZpbGU+c1JHQiBJRUM2MTk2Ni0yLjE8L3Bob3Rvc2hvcDpJQ0NQcm9maWxlPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICAgICA8dGlmZjpYUmVzb2x1dGlvbj43MjAwMDAvMTAwMDA8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOllSZXNvbHV0aW9uPjcyMDAwMC8xMDAwMDwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPGV4aWY6Q29sb3JTcGFjZT4xPC9leGlmOkNvbG9yU3BhY2U+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4xPC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjE8L2V4aWY6UGl4ZWxZRGltZW5zaW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAKPD94cGFja2V0IGVuZD0idyI/PoAp5ZwAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAABNJREFUeNpi+P//PwMAAAD//wMACPwC/tOldWEAAAAASUVORK5CYII='>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_data-uri_blocked.html^headers^ b/dom/security/test/csp/file_data-uri_blocked.html^headers^
new file mode 100644
index 0000000000..4248cca188
--- /dev/null
+++ b/dom/security/test/csp/file_data-uri_blocked.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src 'self' 'report-sample'; img-src 'none' 'report-sample'
diff --git a/dom/security/test/csp/file_data_csp_inheritance.html b/dom/security/test/csp/file_data_csp_inheritance.html
new file mode 100644
index 0000000000..4ae2fedc69
--- /dev/null
+++ b/dom/security/test/csp/file_data_csp_inheritance.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1381761 - Treating 'data:' documents as unique, opaque origins should still inherit the CSP</title>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Security-Policy" content= "img-src 'none'"/>
+</head>
+<body>
+<iframe id="dataFrame" src="data:text/html,<body>should inherit csp</body>"></iframe>
+
+<script type="application/javascript">
+ // get the csp in JSON notation from the principal
+ var frame = document.getElementById("dataFrame");
+ frame.onload = function () {
+ var contentDoc = SpecialPowers.wrap(frame).contentDocument;
+ var cspOBJ = JSON.parse(contentDoc.cspJSON);
+ // make sure we got >>one<< policy
+ var policies = cspOBJ["csp-policies"];
+ window.parent.postMessage({result: policies.length}, "*");
+ }
+</script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_data_csp_merge.html b/dom/security/test/csp/file_data_csp_merge.html
new file mode 100644
index 0000000000..88ae8febe5
--- /dev/null
+++ b/dom/security/test/csp/file_data_csp_merge.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1386183 - Meta CSP on data: URI iframe should be merged with toplevel CSP</title>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Security-Policy" content= "img-src https:"/>
+</head>
+<body>
+<iframe id="dataFrame" onload="doCSPMergeCheck()"
+ src="data:text/html,<html><head><meta http-equiv='Content-Security-Policy' content='script-src https:'/></head><body>merge csp</body></html>">
+</iframe>
+
+<script type="application/javascript">
+ function doCSPMergeCheck() {
+ // get the csp in JSON notation from the principal
+ var frame = document.getElementById("dataFrame");
+ var contentDoc = SpecialPowers.wrap(frame).contentDocument;
+ var cspOBJ = JSON.parse(contentDoc.cspJSON);
+ // make sure we got >>two<< policies
+ var policies = cspOBJ["csp-policies"];
+ window.parent.postMessage({result: policies.length}, "*");
+ }
+</script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_data_doc_ignore_meta_csp.html b/dom/security/test/csp/file_data_doc_ignore_meta_csp.html
new file mode 100644
index 0000000000..9d6e9834dd
--- /dev/null
+++ b/dom/security/test/csp/file_data_doc_ignore_meta_csp.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1382869: data document should ignore meta csp</title>
+ <meta charset="utf-8">
+</head>
+<body>
+<script type="application/javascript">
+ // 1) create a data document
+ const doc = document.implementation.createHTMLDocument();
+ // 2) add meta csp to that document
+ const metaEl = doc.createElement('meta');
+ metaEl.setAttribute('http-equiv', 'Content-Security-Policy');
+ metaEl.setAttribute('content', "img-src 'none'");
+ doc.head.appendChild(metaEl);
+ // 3) let the parent know we are done here
+ var result = "dataDocCreated";
+ window.parent.postMessage({result}, "*");
+</script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_doccomment_meta.html b/dom/security/test/csp/file_doccomment_meta.html
new file mode 100644
index 0000000000..a0f36a4bfe
--- /dev/null
+++ b/dom/security/test/csp/file_doccomment_meta.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 663570 - Test doc.write(meta csp)</title>
+ <meta charset="utf-8">
+
+ <!-- Use doc.write() to *un*apply meta csp -->
+ <script type="application/javascript">
+ document.write("<!--");
+ </script>
+
+ <meta http-equiv="Content-Security-Policy" content= "style-src 'none'; script-src 'none'; img-src 'none'">
+ -->
+
+ <!-- try to load a css on a page where meta CSP is commented out -->
+ <link rel="stylesheet" type="text/css" href="file_docwrite_meta.css">
+
+ <!-- try to load a script on a page where meta CSP is commented out -->
+ <script id="testscript" src="file_docwrite_meta.js"></script>
+
+</head>
+<body>
+
+ <!-- try to load an image on a page where meta CSP is commented out -->
+ <img id="testimage" src="http://mochi.test:8888/tests/image/test/mochitest/blue.png"></img>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_docwrite_meta.css b/dom/security/test/csp/file_docwrite_meta.css
new file mode 100644
index 0000000000..de725038b6
--- /dev/null
+++ b/dom/security/test/csp/file_docwrite_meta.css
@@ -0,0 +1,3 @@
+body {
+ background-color: rgb(255, 0, 0);
+}
diff --git a/dom/security/test/csp/file_docwrite_meta.html b/dom/security/test/csp/file_docwrite_meta.html
new file mode 100644
index 0000000000..292de3bec5
--- /dev/null
+++ b/dom/security/test/csp/file_docwrite_meta.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 663570 - Test doc.write(meta csp)</title>
+ <meta charset="utf-8">
+
+ <!-- Use doc.write() to apply meta csp -->
+ <script type="application/javascript">
+ var metaCSP = "style-src 'none'; script-src 'none'; img-src 'none'";
+ document.write("<meta http-equiv=\"Content-Security-Policy\" content=\" " + metaCSP + "\">");
+ </script>
+
+ <!-- try to load a css which is forbidden by meta CSP -->
+ <link rel="stylesheet" type="text/css" href="file_docwrite_meta.css">
+
+ <!-- try to load a script which is forbidden by meta CSP -->
+ <script id="testscript" src="file_docwrite_meta.js"></script>
+
+</head>
+<body>
+
+ <!-- try to load an image which is forbidden by meta CSP -->
+ <img id="testimage" src="http://mochi.test:8888/tests/image/test/mochitest/blue.png"></img>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_docwrite_meta.js b/dom/security/test/csp/file_docwrite_meta.js
new file mode 100644
index 0000000000..722adc235e
--- /dev/null
+++ b/dom/security/test/csp/file_docwrite_meta.js
@@ -0,0 +1,3 @@
+// set a variable on the document which we can check to verify
+// whether the external script was loaded or blocked
+document.myMetaCSPScript = "external-JS-loaded";
diff --git a/dom/security/test/csp/file_dual_header_testserver.sjs b/dom/security/test/csp/file_dual_header_testserver.sjs
new file mode 100644
index 0000000000..0efe186d57
--- /dev/null
+++ b/dom/security/test/csp/file_dual_header_testserver.sjs
@@ -0,0 +1,45 @@
+/*
+ * Custom sjs file serving a test page using *two* CSP policies.
+ * See Bug 1036399 - Multiple CSP policies should be combined towards an intersection
+ */
+
+const TIGHT_POLICY = "default-src 'self'";
+const LOOSE_POLICY = "default-src 'self' 'unsafe-inline'";
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ var csp = "";
+ // deliver *TWO* comma separated policies which is in fact the same as serving
+ // to separate CSP headers (AppendPolicy is called twice).
+ if (request.queryString == "tight") {
+ // script execution will be *blocked*
+ csp = TIGHT_POLICY + ", " + LOOSE_POLICY;
+ } else {
+ // script execution will be *allowed*
+ csp = LOOSE_POLICY + ", " + LOOSE_POLICY;
+ }
+ response.setHeader("Content-Security-Policy", csp, false);
+
+ // Send HTML to test allowed/blocked behaviors
+ response.setHeader("Content-Type", "text/html", false);
+
+ // generate an html file that contains a div container which is updated
+ // in case the inline script is *not* blocked by CSP.
+ var html =
+ "<!DOCTYPE HTML>" +
+ "<html>" +
+ "<head>" +
+ "<title>Testpage for Bug 1036399</title>" +
+ "</head>" +
+ "<body>" +
+ "<div id='testdiv'>blocked</div>" +
+ "<script type='text/javascript'>" +
+ "document.getElementById('testdiv').innerHTML = 'allowed';" +
+ "</script>" +
+ "</body>" +
+ "</html>";
+
+ response.write(html);
+}
diff --git a/dom/security/test/csp/file_dummy_pixel.png b/dom/security/test/csp/file_dummy_pixel.png
new file mode 100644
index 0000000000..52c591798e
--- /dev/null
+++ b/dom/security/test/csp/file_dummy_pixel.png
Binary files differ
diff --git a/dom/security/test/csp/file_empty_directive.html b/dom/security/test/csp/file_empty_directive.html
new file mode 100644
index 0000000000..16196bb19f
--- /dev/null
+++ b/dom/security/test/csp/file_empty_directive.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Bug 587377 - CSP keywords "'self'" and "'none'" are easy to confuse with host names "self" and "none"</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_empty_directive.html^headers^ b/dom/security/test/csp/file_empty_directive.html^headers^
new file mode 100644
index 0000000000..50dbe57bb9
--- /dev/null
+++ b/dom/security/test/csp/file_empty_directive.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: ;
diff --git a/dom/security/test/csp/file_evalscript_main.html b/dom/security/test/csp/file_evalscript_main.html
new file mode 100644
index 0000000000..e83c1d9ed7
--- /dev/null
+++ b/dom/security/test/csp/file_evalscript_main.html
@@ -0,0 +1,12 @@
+<html>
+ <head>
+ <title>CSP eval script tests</title>
+ <script type="application/javascript"
+ src="file_evalscript_main.js"></script>
+ </head>
+ <body>
+
+ Foo.
+
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_evalscript_main.html^headers^ b/dom/security/test/csp/file_evalscript_main.html^headers^
new file mode 100644
index 0000000000..b91ba384d9
--- /dev/null
+++ b/dom/security/test/csp/file_evalscript_main.html^headers^
@@ -0,0 +1,2 @@
+Cache-Control: no-cache
+Content-Security-Policy: default-src 'self'
diff --git a/dom/security/test/csp/file_evalscript_main.js b/dom/security/test/csp/file_evalscript_main.js
new file mode 100644
index 0000000000..127f5d8152
--- /dev/null
+++ b/dom/security/test/csp/file_evalscript_main.js
@@ -0,0 +1,243 @@
+/* eslint-disable no-eval */
+// some javascript for the CSP eval() tests
+
+function logResult(str, passed) {
+ var elt = document.createElement("div");
+ var color = passed ? "#cfc;" : "#fcc";
+ elt.setAttribute(
+ "style",
+ "background-color:" +
+ color +
+ "; width:100%; border:1px solid black; padding:3px; margin:4px;"
+ );
+ elt.innerHTML = str;
+ document.body.appendChild(elt);
+}
+
+window._testResults = {};
+
+// check values for return values from blocked timeout or intervals
+var verifyZeroRetVal = (function (window) {
+ return function (val, details) {
+ logResult(
+ (val === 0 ? "PASS: " : "FAIL: ") +
+ "Blocked interval/timeout should have zero return value; " +
+ details,
+ val === 0
+ );
+ window.parent.verifyZeroRetVal(val, details);
+ };
+})(window);
+
+// callback for when stuff is allowed by CSP
+var onevalexecuted = (function (window) {
+ return function (shouldrun, what, data) {
+ window._testResults[what] = "ran";
+ window.parent.scriptRan(shouldrun, what, data);
+ logResult(
+ (shouldrun ? "PASS: " : "FAIL: ") + what + " : " + data,
+ shouldrun
+ );
+ };
+})(window);
+
+// callback for when stuff is blocked
+var onevalblocked = (function (window) {
+ return function (shouldrun, what, data) {
+ window._testResults[what] = "blocked";
+ window.parent.scriptBlocked(shouldrun, what, data);
+ logResult(
+ (shouldrun ? "FAIL: " : "PASS: ") + what + " : " + data,
+ !shouldrun
+ );
+ };
+})(window);
+
+// Defer until document is loaded so that we can write the pretty result boxes
+// out.
+addEventListener(
+ "load",
+ function () {
+ // setTimeout(String) test -- mutate something in the window._testResults
+ // obj, then check it.
+ {
+ var str_setTimeoutWithStringRan =
+ 'onevalexecuted(false, "setTimeout(String)", "setTimeout with a string was enabled.");';
+ function fcn_setTimeoutWithStringCheck() {
+ if (this._testResults["setTimeout(String)"] !== "ran") {
+ onevalblocked(
+ false,
+ "setTimeout(String)",
+ "setTimeout with a string was blocked"
+ );
+ }
+ }
+ setTimeout(fcn_setTimeoutWithStringCheck.bind(window), 10);
+ // eslint-disable-next-line no-implied-eval
+ var res = setTimeout(str_setTimeoutWithStringRan, 10);
+ verifyZeroRetVal(res, "setTimeout(String)");
+ }
+
+ // setInterval(String) test -- mutate something in the window._testResults
+ // obj, then check it.
+ {
+ var str_setIntervalWithStringRan =
+ 'onevalexecuted(false, "setInterval(String)", "setInterval with a string was enabled.");';
+ function fcn_setIntervalWithStringCheck() {
+ if (this._testResults["setInterval(String)"] !== "ran") {
+ onevalblocked(
+ false,
+ "setInterval(String)",
+ "setInterval with a string was blocked"
+ );
+ }
+ }
+ setTimeout(fcn_setIntervalWithStringCheck.bind(window), 10);
+ // eslint-disable-next-line no-implied-eval
+ var res = setInterval(str_setIntervalWithStringRan, 10);
+ verifyZeroRetVal(res, "setInterval(String)");
+
+ // emergency cleanup, just in case.
+ if (res != 0) {
+ setTimeout(function () {
+ clearInterval(res);
+ }, 15);
+ }
+ }
+
+ // setTimeout(function) test -- mutate something in the window._testResults
+ // obj, then check it.
+ {
+ function fcn_setTimeoutWithFunctionRan() {
+ onevalexecuted(
+ true,
+ "setTimeout(function)",
+ "setTimeout with a function was enabled."
+ );
+ }
+ function fcn_setTimeoutWithFunctionCheck() {
+ if (this._testResults["setTimeout(function)"] !== "ran") {
+ onevalblocked(
+ true,
+ "setTimeout(function)",
+ "setTimeout with a function was blocked"
+ );
+ }
+ }
+ setTimeout(fcn_setTimeoutWithFunctionRan.bind(window), 10);
+ setTimeout(fcn_setTimeoutWithFunctionCheck.bind(window), 10);
+ }
+
+ // eval() test -- should throw exception as per spec
+ try {
+ eval('onevalexecuted(false, "eval(String)", "eval() was enabled.");');
+ } catch (e) {
+ onevalblocked(false, "eval(String)", "eval() was blocked");
+ }
+
+ // eval(foo,bar) test -- should throw exception as per spec
+ try {
+ eval(
+ 'onevalexecuted(false, "eval(String,scope)", "eval() was enabled.");',
+ 1
+ );
+ } catch (e) {
+ onevalblocked(
+ false,
+ "eval(String,object)",
+ "eval() with scope was blocked"
+ );
+ }
+
+ // [foo,bar].sort(eval) test -- should throw exception as per spec
+ try {
+ [
+ 'onevalexecuted(false, "[String, obj].sort(eval)", "eval() was enabled.");',
+ 1,
+ ].sort(eval);
+ } catch (e) {
+ onevalblocked(
+ false,
+ "[String, obj].sort(eval)",
+ "eval() with scope via sort was blocked"
+ );
+ }
+
+ // [].sort.call([foo,bar], eval) test -- should throw exception as per spec
+ try {
+ [].sort.call(
+ [
+ 'onevalexecuted(false, "[String, obj].sort(eval)", "eval() was enabled.");',
+ 1,
+ ],
+ eval
+ );
+ } catch (e) {
+ onevalblocked(
+ false,
+ "[].sort.call([String, obj], eval)",
+ "eval() with scope via sort/call was blocked"
+ );
+ }
+
+ // new Function() test -- should throw exception as per spec
+ try {
+ var fcn = new Function(
+ 'onevalexecuted(false, "new Function(String)", "new Function(String) was enabled.");'
+ );
+ fcn();
+ } catch (e) {
+ onevalblocked(
+ false,
+ "new Function(String)",
+ "new Function(String) was blocked."
+ );
+ }
+
+ // ShadowRealm.prototype.evaluate -- should throw exception as per spec.
+ try {
+ var sr = new ShadowRealm();
+ sr.evaluate("var x = 10");
+ onevalexecuted(
+ false,
+ "ShadowRealm.prototype.evaluate(String)",
+ "ShadowRealm.prototype.evaluate(String) was enabled."
+ );
+ } catch (e) {
+ onevalblocked(
+ false,
+ "ShadowRealm.prototype.evaluate(String)",
+ "ShadowRealm.prototype.evaluate(String) was blocked."
+ );
+ }
+
+ // setTimeout(eval, 0, str)
+ {
+ // error is not catchable here, instead, we're going to side-effect
+ // 'worked'.
+ var worked = false;
+
+ setTimeout(eval, 0, "worked = true");
+ setTimeout(
+ function (worked) {
+ if (worked) {
+ onevalexecuted(
+ false,
+ "setTimeout(eval, 0, str)",
+ "setTimeout(eval, 0, string) was enabled."
+ );
+ } else {
+ onevalblocked(
+ false,
+ "setTimeout(eval, 0, str)",
+ "setTimeout(eval, 0, str) was blocked."
+ );
+ }
+ },
+ 0,
+ worked
+ );
+ }
+ },
+ false
+);
diff --git a/dom/security/test/csp/file_evalscript_main_allowed.html b/dom/security/test/csp/file_evalscript_main_allowed.html
new file mode 100644
index 0000000000..274972d9bd
--- /dev/null
+++ b/dom/security/test/csp/file_evalscript_main_allowed.html
@@ -0,0 +1,12 @@
+<html>
+ <head>
+ <title>CSP eval script tests</title>
+ <script type="application/javascript"
+ src="file_evalscript_main_allowed.js"></script>
+ </head>
+ <body>
+
+ Foo.
+
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_evalscript_main_allowed.html^headers^ b/dom/security/test/csp/file_evalscript_main_allowed.html^headers^
new file mode 100644
index 0000000000..0cb5288bec
--- /dev/null
+++ b/dom/security/test/csp/file_evalscript_main_allowed.html^headers^
@@ -0,0 +1,2 @@
+Cache-Control: no-cache
+Content-Security-Policy: default-src 'self' ; script-src 'self' 'unsafe-eval'
diff --git a/dom/security/test/csp/file_evalscript_main_allowed.js b/dom/security/test/csp/file_evalscript_main_allowed.js
new file mode 100644
index 0000000000..6b014c339e
--- /dev/null
+++ b/dom/security/test/csp/file_evalscript_main_allowed.js
@@ -0,0 +1,195 @@
+/* eslint-disable no-eval */
+// some javascript for the CSP eval() tests
+// all of these evals should succeed, as the document loading this script
+// has script-src 'self' 'unsafe-eval'
+
+function logResult(str, passed) {
+ var elt = document.createElement("div");
+ var color = passed ? "#cfc;" : "#fcc";
+ elt.setAttribute(
+ "style",
+ "background-color:" +
+ color +
+ "; width:100%; border:1px solid black; padding:3px; margin:4px;"
+ );
+ elt.innerHTML = str;
+ document.body.appendChild(elt);
+}
+
+// callback for when stuff is allowed by CSP
+var onevalexecuted = (function (window) {
+ return function (shouldrun, what, data) {
+ window.parent.scriptRan(shouldrun, what, data);
+ logResult(
+ (shouldrun ? "PASS: " : "FAIL: ") + what + " : " + data,
+ shouldrun
+ );
+ };
+})(window);
+
+// callback for when stuff is blocked
+var onevalblocked = (function (window) {
+ return function (shouldrun, what, data) {
+ window.parent.scriptBlocked(shouldrun, what, data);
+ logResult(
+ (shouldrun ? "FAIL: " : "PASS: ") + what + " : " + data,
+ !shouldrun
+ );
+ };
+})(window);
+
+// Defer until document is loaded so that we can write the pretty result boxes
+// out.
+addEventListener(
+ "load",
+ function () {
+ // setTimeout(String) test -- should pass
+ try {
+ // eslint-disable-next-line no-implied-eval
+ setTimeout(
+ 'onevalexecuted(true, "setTimeout(String)", "setTimeout with a string was enabled.");',
+ 10
+ );
+ } catch (e) {
+ onevalblocked(
+ true,
+ "setTimeout(String)",
+ "setTimeout with a string was blocked"
+ );
+ }
+
+ // setTimeout(function) test -- should pass
+ try {
+ setTimeout(function () {
+ onevalexecuted(
+ true,
+ "setTimeout(function)",
+ "setTimeout with a function was enabled."
+ );
+ }, 10);
+ } catch (e) {
+ onevalblocked(
+ true,
+ "setTimeout(function)",
+ "setTimeout with a function was blocked"
+ );
+ }
+
+ // eval() test
+ try {
+ eval('onevalexecuted(true, "eval(String)", "eval() was enabled.");');
+ } catch (e) {
+ onevalblocked(true, "eval(String)", "eval() was blocked");
+ }
+
+ // eval(foo,bar) test
+ try {
+ eval(
+ 'onevalexecuted(true, "eval(String,scope)", "eval() was enabled.");',
+ 1
+ );
+ } catch (e) {
+ onevalblocked(
+ true,
+ "eval(String,object)",
+ "eval() with scope was blocked"
+ );
+ }
+
+ // [foo,bar].sort(eval) test
+ try {
+ [
+ 'onevalexecuted(true, "[String, obj].sort(eval)", "eval() was enabled.");',
+ 1,
+ ].sort(eval);
+ } catch (e) {
+ onevalblocked(
+ true,
+ "[String, obj].sort(eval)",
+ "eval() with scope via sort was blocked"
+ );
+ }
+
+ // [].sort.call([foo,bar], eval) test
+ try {
+ [].sort.call(
+ [
+ 'onevalexecuted(true, "[String, obj].sort(eval)", "eval() was enabled.");',
+ 1,
+ ],
+ eval
+ );
+ } catch (e) {
+ onevalblocked(
+ true,
+ "[].sort.call([String, obj], eval)",
+ "eval() with scope via sort/call was blocked"
+ );
+ }
+
+ // new Function() test
+ try {
+ var fcn = new Function(
+ 'onevalexecuted(true, "new Function(String)", "new Function(String) was enabled.");'
+ );
+ fcn();
+ } catch (e) {
+ onevalblocked(
+ true,
+ "new Function(String)",
+ "new Function(String) was blocked."
+ );
+ }
+
+ // ShadowRealm.prototype.evaluate
+ try {
+ var sr = new ShadowRealm();
+ sr.evaluate("var x = 10");
+ onevalexecuted(
+ true,
+ "ShadowRealm.prototype.evaluate(String)",
+ "ShadowRealm.prototype.evaluate(String) was enabled."
+ );
+ } catch (e) {
+ onevalblocked(
+ true,
+ "ShadowRealm.prototype.evaluate(String)",
+ "ShadowRealm.prototype.evaluate(String) was blocked."
+ );
+ }
+
+ function checkResult() {
+ //alert(bar);
+ if (bar) {
+ onevalexecuted(
+ true,
+ "setTimeout(eval, 0, str)",
+ "setTimeout(eval, 0, string) was enabled."
+ );
+ } else {
+ onevalblocked(
+ true,
+ "setTimeout(eval, 0, str)",
+ "setTimeout(eval, 0, str) was blocked."
+ );
+ }
+ }
+
+ var bar = false;
+
+ function foo() {
+ bar = true;
+ }
+
+ window.foo = foo;
+
+ // setTimeout(eval, 0, str)
+
+ // error is not catchable here
+
+ setTimeout(eval, 0, "window.foo();");
+
+ setTimeout(checkResult.bind(this), 0);
+ },
+ false
+);
diff --git a/dom/security/test/csp/file_fontloader.sjs b/dom/security/test/csp/file_fontloader.sjs
new file mode 100644
index 0000000000..b9b5e602fe
--- /dev/null
+++ b/dom/security/test/csp/file_fontloader.sjs
@@ -0,0 +1,57 @@
+// custom *.sjs for Bug 1195172
+// CSP: 'block-all-mixed-content'
+
+const PRE_HEAD =
+ "<!DOCTYPE HTML>" +
+ '<html><head><meta charset="utf-8">' +
+ "<title>Bug 1195172 - CSP should block font from cache</title>";
+
+const CSP_BLOCK =
+ '<meta http-equiv="Content-Security-Policy" content="font-src \'none\'">';
+
+const CSP_ALLOW =
+ '<meta http-equiv="Content-Security-Policy" content="font-src *">';
+
+const CSS =
+ "<style>" +
+ " @font-face {" +
+ " font-family: myFontTest;" +
+ " src: url(file_fontloader.woff);" +
+ " }" +
+ " div {" +
+ " font-family: myFontTest;" +
+ " }" +
+ "</style>";
+
+const POST_HEAD_AND_BODY =
+ "</head>" +
+ "<body>" +
+ "<div> Just testing the font </div>" +
+ "</body>" +
+ "</html>";
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ var queryString = request.queryString;
+
+ if (queryString == "baseline") {
+ response.write(PRE_HEAD + POST_HEAD_AND_BODY);
+ return;
+ }
+ if (queryString == "no-csp") {
+ response.write(PRE_HEAD + CSS + POST_HEAD_AND_BODY);
+ return;
+ }
+ if (queryString == "csp-block") {
+ response.write(PRE_HEAD + CSP_BLOCK + CSS + POST_HEAD_AND_BODY);
+ return;
+ }
+ if (queryString == "csp-allow") {
+ response.write(PRE_HEAD + CSP_ALLOW + CSS + POST_HEAD_AND_BODY);
+ return;
+ }
+ // we should never get here, but just in case return something unexpected
+ response.write("do'h");
+}
diff --git a/dom/security/test/csp/file_fontloader.woff b/dom/security/test/csp/file_fontloader.woff
new file mode 100644
index 0000000000..fbf7390d59
--- /dev/null
+++ b/dom/security/test/csp/file_fontloader.woff
Binary files differ
diff --git a/dom/security/test/csp/file_form-action.html b/dom/security/test/csp/file_form-action.html
new file mode 100644
index 0000000000..cfff156bae
--- /dev/null
+++ b/dom/security/test/csp/file_form-action.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 529697 - Test mapping of form submission to form-action</title>
+</head>
+<body>
+ <form action="submit-form">
+ <input id="submitButton" type="submit" value="Submit form">
+ </form>
+ <script type="text/javascript">
+ var submitButton = document.getElementById('submitButton');
+ submitButton.click();
+ </script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_form_action_server.sjs b/dom/security/test/csp/file_form_action_server.sjs
new file mode 100644
index 0000000000..0c79736d47
--- /dev/null
+++ b/dom/security/test/csp/file_form_action_server.sjs
@@ -0,0 +1,32 @@
+// Custom *.sjs file specifically for the needs of Bug 1251043
+
+const FRAME = `
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>Bug 1251043 - Test form-action blocks URL</title>
+ <meta http-equiv="Content-Security-Policy" content="form-action 'none';">
+ </head>
+ <body>
+ CONTROL-TEXT
+ <form action="file_form_action_server.sjs?formsubmission" method="GET">
+ <input type="submit" id="submitButton" value="submit">
+ </form>
+ </body>
+ </html>`;
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ // PART 1: Return a frame including the FORM and the CSP
+ if (request.queryString === "loadframe") {
+ response.write(FRAME);
+ return;
+ }
+
+ // PART 2: We should never get here because the form
+ // should not be submitted. Just in case; return
+ // something unexpected so the test fails!
+ response.write("do'h");
+}
diff --git a/dom/security/test/csp/file_frame_ancestors_ro.html b/dom/security/test/csp/file_frame_ancestors_ro.html
new file mode 100644
index 0000000000..ff5ae9cf9f
--- /dev/null
+++ b/dom/security/test/csp/file_frame_ancestors_ro.html
@@ -0,0 +1 @@
+<html><body>Child Document</body></html>
diff --git a/dom/security/test/csp/file_frame_ancestors_ro.html^headers^ b/dom/security/test/csp/file_frame_ancestors_ro.html^headers^
new file mode 100644
index 0000000000..d018af3a96
--- /dev/null
+++ b/dom/security/test/csp/file_frame_ancestors_ro.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy-Report-Only: frame-ancestors 'none'; report-uri http://mochi.test:8888/foo.sjs
diff --git a/dom/security/test/csp/file_frame_src.js b/dom/security/test/csp/file_frame_src.js
new file mode 100644
index 0000000000..d30bc0ec62
--- /dev/null
+++ b/dom/security/test/csp/file_frame_src.js
@@ -0,0 +1,20 @@
+let testframe = document.getElementById("testframe");
+testframe.onload = function () {
+ parent.postMessage(
+ {
+ result: "frame-allowed",
+ href: document.location.href,
+ },
+ "*"
+ );
+};
+testframe.onerror = function () {
+ parent.postMessage(
+ {
+ result: "frame-blocked",
+ href: document.location.href,
+ },
+ "*"
+ );
+};
+testframe.src = "file_frame_src_inner.html";
diff --git a/dom/security/test/csp/file_frame_src_child_governs.html b/dom/security/test/csp/file_frame_src_child_governs.html
new file mode 100644
index 0000000000..a51cb75be2
--- /dev/null
+++ b/dom/security/test/csp/file_frame_src_child_governs.html
@@ -0,0 +1,10 @@
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Security-Policy" content="child-src https://example.com">";
+</head>
+<body>
+<iframe id="testframe"></iframe>
+<script type="text/javascript" src="file_frame_src.js"></script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_frame_src_frame_governs.html b/dom/security/test/csp/file_frame_src_frame_governs.html
new file mode 100644
index 0000000000..2c5d5857f2
--- /dev/null
+++ b/dom/security/test/csp/file_frame_src_frame_governs.html
@@ -0,0 +1,10 @@
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Security-Policy" content="frame-src https://example.com; child-src 'none'">";
+</head>
+<body>
+<iframe id="testframe"></iframe>
+<script type="text/javascript" src="file_frame_src.js"></script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_frame_src_inner.html b/dom/security/test/csp/file_frame_src_inner.html
new file mode 100644
index 0000000000..4a2fc6095a
--- /dev/null
+++ b/dom/security/test/csp/file_frame_src_inner.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+dummy iframe
+</body>
+</html>
diff --git a/dom/security/test/csp/file_frameancestors.sjs b/dom/security/test/csp/file_frameancestors.sjs
new file mode 100644
index 0000000000..25d4b3fe08
--- /dev/null
+++ b/dom/security/test/csp/file_frameancestors.sjs
@@ -0,0 +1,69 @@
+// SJS file for CSP frame ancestor mochitests
+function handleRequest(request, response) {
+ var query = {};
+ request.queryString.split("&").forEach(function (val) {
+ var [name, value] = val.split("=");
+ query[name] = unescape(value);
+ });
+
+ var isPreflight = request.method == "OPTIONS";
+
+ //avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ // grab the desired policy from the query, and then serve a page
+ if (query.csp) {
+ response.setHeader("Content-Security-Policy", unescape(query.csp), false);
+ }
+ if (query.scriptedreport) {
+ // spit back a script that records that the page loaded
+ response.setHeader("Content-Type", "text/javascript", false);
+ if (query.double) {
+ response.write(
+ 'window.parent.parent.parent.postMessage({call: "frameLoaded", testname: "' +
+ query.scriptedreport +
+ '", uri: "window.location.toString()"}, "*");'
+ );
+ } else {
+ response.write(
+ 'window.parent.parent.postMessage({call: "frameLoaded", testname: "' +
+ query.scriptedreport +
+ '", uri: "window.location.toString()"}, "*");'
+ );
+ }
+ } else if (query.internalframe) {
+ // spit back an internal iframe (one that might be blocked)
+ response.setHeader("Content-Type", "text/html", false);
+ response.write("<html><head>");
+ if (query.double) {
+ response.write(
+ '<script src="file_frameancestors.sjs?double=1&scriptedreport=' +
+ query.testid +
+ '"></script>'
+ );
+ } else {
+ response.write(
+ '<script src="file_frameancestors.sjs?scriptedreport=' +
+ query.testid +
+ '"></script>'
+ );
+ }
+ response.write("</head><body>");
+ response.write(unescape(query.internalframe));
+ response.write("</body></html>");
+ } else if (query.externalframe) {
+ // spit back an internal iframe (one that won't be blocked, and probably
+ // has no CSP)
+ response.setHeader("Content-Type", "text/html", false);
+ response.write("<html><head>");
+ response.write("</head><body>");
+ response.write(unescape(query.externalframe));
+ response.write("</body></html>");
+ } else {
+ // default case: error.
+ response.setHeader("Content-Type", "text/html", false);
+ response.write("<html><body>");
+ response.write("ERROR: not sure what to serve.");
+ response.write("</body></html>");
+ }
+}
diff --git a/dom/security/test/csp/file_frameancestors_main.html b/dom/security/test/csp/file_frameancestors_main.html
new file mode 100644
index 0000000000..97f9cb9ac5
--- /dev/null
+++ b/dom/security/test/csp/file_frameancestors_main.html
@@ -0,0 +1,44 @@
+<html>
+ <head>
+ <title>CSP frame ancestors tests</title>
+
+ <!-- this page shouldn't have a CSP, just the sub-pages. -->
+ <script src='file_frameancestors_main.js'></script>
+
+ </head>
+ <body>
+
+<!-- These iframes will get populated by the attached javascript. -->
+<tt> aa_allow: /* innermost frame allows a */</tt><br/>
+<iframe id='aa_allow'></iframe><br/>
+
+<tt> aa_block: /* innermost frame denies a */</tt><br/>
+<iframe id='aa_block'></iframe><br/>
+
+<tt> ab_allow: /* innermost frame allows a */</tt><br/>
+<iframe id='ab_allow'></iframe><br/>
+
+<tt> ab_block: /* innermost frame denies a */</tt><br/>
+<iframe id='ab_block'></iframe><br/>
+
+<tt> aba_allow: /* innermost frame allows b,a */</tt><br/>
+<iframe id='aba_allow'></iframe><br/>
+
+<tt> aba_block: /* innermost frame denies b */</tt><br/>
+<iframe id='aba_block'></iframe><br/>
+
+<tt> aba2_block: /* innermost frame denies a */</tt><br/>
+<iframe id='aba2_block'></iframe><br/>
+
+<tt> abb_allow: /* innermost frame allows b,a */</tt><br/>
+<iframe id='abb_allow'></iframe><br/>
+
+<tt> abb_block: /* innermost frame denies b */</tt><br/>
+<iframe id='abb_block'></iframe><br/>
+
+<tt> abb2_block: /* innermost frame denies a */</tt><br/>
+<iframe id='abb2_block'></iframe><br/>
+
+
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_frameancestors_main.js b/dom/security/test/csp/file_frameancestors_main.js
new file mode 100644
index 0000000000..2c5caf739f
--- /dev/null
+++ b/dom/security/test/csp/file_frameancestors_main.js
@@ -0,0 +1,134 @@
+// Script to populate the test frames in the frame ancestors mochitest.
+//
+function setupFrames() {
+ var $ = function (v) {
+ return document.getElementById(v);
+ };
+ var base = {
+ self: "/tests/dom/security/test/csp/file_frameancestors.sjs",
+ a: "http://mochi.test:8888/tests/dom/security/test/csp/file_frameancestors.sjs",
+ b: "http://example.com/tests/dom/security/test/csp/file_frameancestors.sjs",
+ };
+
+ // In both cases (base.a, base.b) the path starts with /tests/. Let's make sure this
+ // path within the CSP policy is completely ignored when enforcing frame ancestors.
+ // To test this behavior we use /foo/ and /bar/ as dummy values for the path.
+ var host = {
+ a: "http://mochi.test:8888/foo/",
+ b: "http://example.com:80/bar/",
+ };
+
+ var innerframeuri = null;
+ var elt = null;
+
+ elt = $("aa_allow");
+ elt.src =
+ base.a +
+ "?testid=aa_allow&internalframe=aa_a&csp=" +
+ escape(
+ "default-src 'none'; frame-ancestors " + host.a + "; script-src 'self'"
+ );
+
+ elt = $("aa_block");
+ elt.src =
+ base.a +
+ "?testid=aa_block&internalframe=aa_b&csp=" +
+ escape("default-src 'none'; frame-ancestors 'none'; script-src 'self'");
+
+ elt = $("ab_allow");
+ elt.src =
+ base.b +
+ "?testid=ab_allow&internalframe=ab_a&csp=" +
+ escape(
+ "default-src 'none'; frame-ancestors " + host.a + "; script-src 'self'"
+ );
+
+ elt = $("ab_block");
+ elt.src =
+ base.b +
+ "?testid=ab_block&internalframe=ab_b&csp=" +
+ escape("default-src 'none'; frame-ancestors 'none'; script-src 'self'");
+
+ /* .... two-level framing */
+ elt = $("aba_allow");
+ innerframeuri =
+ base.a +
+ "?testid=aba_allow&double=1&internalframe=aba_a&csp=" +
+ escape(
+ "default-src 'none'; frame-ancestors " +
+ host.a +
+ " " +
+ host.b +
+ "; script-src 'self'"
+ );
+ elt.src =
+ base.b +
+ "?externalframe=" +
+ escape('<iframe src="' + innerframeuri + '"></iframe>');
+
+ elt = $("aba_block");
+ innerframeuri =
+ base.a +
+ "?testid=aba_allow&double=1&internalframe=aba_b&csp=" +
+ escape(
+ "default-src 'none'; frame-ancestors " + host.a + "; script-src 'self'"
+ );
+ elt.src =
+ base.b +
+ "?externalframe=" +
+ escape('<iframe src="' + innerframeuri + '"></iframe>');
+
+ elt = $("aba2_block");
+ innerframeuri =
+ base.a +
+ "?testid=aba_allow&double=1&internalframe=aba2_b&csp=" +
+ escape(
+ "default-src 'none'; frame-ancestors " + host.b + "; script-src 'self'"
+ );
+ elt.src =
+ base.b +
+ "?externalframe=" +
+ escape('<iframe src="' + innerframeuri + '"></iframe>');
+
+ elt = $("abb_allow");
+ innerframeuri =
+ base.b +
+ "?testid=abb_allow&double=1&internalframe=abb_a&csp=" +
+ escape(
+ "default-src 'none'; frame-ancestors " +
+ host.a +
+ " " +
+ host.b +
+ "; script-src 'self'"
+ );
+ elt.src =
+ base.b +
+ "?externalframe=" +
+ escape('<iframe src="' + innerframeuri + '"></iframe>');
+
+ elt = $("abb_block");
+ innerframeuri =
+ base.b +
+ "?testid=abb_allow&double=1&internalframe=abb_b&csp=" +
+ escape(
+ "default-src 'none'; frame-ancestors " + host.a + "; script-src 'self'"
+ );
+ elt.src =
+ base.b +
+ "?externalframe=" +
+ escape('<iframe src="' + innerframeuri + '"></iframe>');
+
+ elt = $("abb2_block");
+ innerframeuri =
+ base.b +
+ "?testid=abb_allow&double=1&internalframe=abb2_b&csp=" +
+ escape(
+ "default-src 'none'; frame-ancestors " + host.b + "; script-src 'self'"
+ );
+ elt.src =
+ base.b +
+ "?externalframe=" +
+ escape('<iframe src="' + innerframeuri + '"></iframe>');
+}
+
+window.addEventListener("load", setupFrames);
diff --git a/dom/security/test/csp/file_frameancestors_userpass.html b/dom/security/test/csp/file_frameancestors_userpass.html
new file mode 100644
index 0000000000..c840995b6c
--- /dev/null
+++ b/dom/security/test/csp/file_frameancestors_userpass.html
@@ -0,0 +1,10 @@
+<html>
+<head>
+ <title>CSP frame ancestors tests</title>
+</head>
+<body>
+ <tt>Nested Frames</tt><br/>
+ <iframe src='http://sampleuser:samplepass@mochi.test:8888/tests/dom/security/test/csp/file_frameancestors_userpass_frame_a.html'></iframe><br/>
+ <iframe src='http://sampleuser:samplepass@example.com/tests/dom/security/test/csp/file_frameancestors_userpass_frame_b.html'></iframe><br/>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_frameancestors_userpass_frame_a.html b/dom/security/test/csp/file_frameancestors_userpass_frame_a.html
new file mode 100644
index 0000000000..d5a5bb604b
--- /dev/null
+++ b/dom/security/test/csp/file_frameancestors_userpass_frame_a.html
@@ -0,0 +1,12 @@
+<html>
+<head>
+ <title>Nested frame</title>
+ <script>
+ parent.parent.postMessage({call: "frameLoaded", testname: "frame_a", uri: window.location.toString()}, "*");
+ </script>
+</head>
+<body>
+ <tt>IFRAME A</tt><br/>
+ <iframe src='http://sampleuser:samplepass@mochi.test:8888/tests/dom/security/test/csp/file_frameancestors_userpass_frame_c.html'></iframe><br/>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_frameancestors_userpass_frame_b.html b/dom/security/test/csp/file_frameancestors_userpass_frame_b.html
new file mode 100644
index 0000000000..87055ef149
--- /dev/null
+++ b/dom/security/test/csp/file_frameancestors_userpass_frame_b.html
@@ -0,0 +1,12 @@
+<html>
+<head>
+ <title>Nested frame</title>
+ <script>
+ parent.parent.postMessage({call: "frameLoaded", testname: "frame_b", uri: window.location.toString()}, "*");
+ </script>
+</head>
+<body>
+ <tt>IFRAME B</tt><br/>
+ <iframe src='http://sampleuser:samplepass@example.com/tests/dom/security/test/csp/file_frameancestors_userpass_frame_d.html'></iframe><br/>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_frameancestors_userpass_frame_c.html b/dom/security/test/csp/file_frameancestors_userpass_frame_c.html
new file mode 100644
index 0000000000..159e6c4633
--- /dev/null
+++ b/dom/security/test/csp/file_frameancestors_userpass_frame_c.html
@@ -0,0 +1,8 @@
+<html>
+<head>
+ <title>Nested frame</title>
+</head>
+<body>
+ Nested frame C content
+</body>
+</html>
diff --git a/dom/security/test/csp/file_frameancestors_userpass_frame_c.html^headers^ b/dom/security/test/csp/file_frameancestors_userpass_frame_c.html^headers^
new file mode 100644
index 0000000000..9e7dfefcda
--- /dev/null
+++ b/dom/security/test/csp/file_frameancestors_userpass_frame_c.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src 'none'; frame-ancestors http://mochi.test:8888/ ; script-src 'self';
diff --git a/dom/security/test/csp/file_frameancestors_userpass_frame_d.html b/dom/security/test/csp/file_frameancestors_userpass_frame_d.html
new file mode 100644
index 0000000000..0cb49c4836
--- /dev/null
+++ b/dom/security/test/csp/file_frameancestors_userpass_frame_d.html
@@ -0,0 +1,8 @@
+<html>
+<head>
+ <title>Nested frame</title>
+</head>
+<body>
+ Nested frame D content
+</body>
+</html>
diff --git a/dom/security/test/csp/file_frameancestors_userpass_frame_d.html^headers^ b/dom/security/test/csp/file_frameancestors_userpass_frame_d.html^headers^
new file mode 100644
index 0000000000..019fcea026
--- /dev/null
+++ b/dom/security/test/csp/file_frameancestors_userpass_frame_d.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src 'none'; frame-ancestors http://sampleuser:samplepass@example.com/ ; script-src 'self';
diff --git a/dom/security/test/csp/file_hash_source.html b/dom/security/test/csp/file_hash_source.html
new file mode 100644
index 0000000000..47eba6cf3e
--- /dev/null
+++ b/dom/security/test/csp/file_hash_source.html
@@ -0,0 +1,65 @@
+<!doctype html>
+<html>
+ <body>
+ <!-- inline scripts -->
+ <p id="inline-script-valid-hash">blocked</p>
+ <p id="inline-script-invalid-hash">blocked</p>
+ <p id="inline-script-invalid-hash-valid-nonce">blocked</p>
+ <p id="inline-script-valid-hash-invalid-nonce">blocked</p>
+ <p id="inline-script-invalid-hash-invalid-nonce">blocked</p>
+ <p id="inline-script-valid-sha512-hash">blocked</p>
+ <p id="inline-script-valid-sha384-hash">blocked</p>
+ <p id="inline-script-valid-sha1-hash">blocked</p>
+ <p id="inline-script-valid-md5-hash">blocked</p>
+
+ <!-- 'sha256-siVR8vAcqP06h2ppeNwqgjr0yZ6yned4X2VF84j4GmI=' (in policy) -->
+ <script>document.getElementById("inline-script-valid-hash").innerHTML = "allowed";</script>
+ <!-- 'sha256-cYPTF2pm0QeyDtbmJ3+xi00o2Rxrw7vphBoHgOg9EnQ=' (not in policy) -->
+ <script>document.getElementById("inline-script-invalid-hash").innerHTML = "allowed";</script>
+ <!-- 'sha256-SKtBKyfeMjBpOujES0etR9t/cklbouJu/3T4PXnjbIo=' (not in policy) -->
+ <script nonce="jPRxvuRHbiQnCWVuoCMAvQ==">document.getElementById("inline-script-invalid-hash-valid-nonce").innerHTML = "allowed";</script>
+ <!-- 'sha256-z7rzCkbOJqi08lga3CVQ3b+3948ZbJWaSxsBs8zPliE=' -->
+ <script nonce="foobar">document.getElementById("inline-script-valid-hash-invalid-nonce").innerHTML = "allowed";</script>
+ <!-- 'sha256-E5TX2PmYZ4YQOK/F3XR1wFcvFjbO7QHMmxHTT/18LbE=' (not in policy) -->
+ <script nonce="foobar">document.getElementById("inline-script-invalid-hash-invalid-nonce").innerHTML = "allowed";</script>
+ <!-- 'sha512-tMLuv22jJ5RHkvLNlv0otvA2fgw6PF16HKu6wy0ZDQ3M7UKzoygs1uxIMSfjMttgWrB5WRvIr35zrTZppMYBVw==' (in policy) -->
+ <script>document.getElementById("inline-script-valid-sha512-hash").innerHTML = "allowed";</script>
+ <!-- 'sha384-XjAD+FxZfipkxna4id1JrR2QP6OYUZfAxpn9+yHOmT1VSLVa9SQR/dz7CEb7jw7w' (in policy) -->
+ <script>document.getElementById("inline-script-valid-sha384-hash").innerHTML = "allowed";</script>
+ <!-- 'sha1-LHErkMxKGcSpa/znpzmKYkKnI30=' (in policy) -->
+ <script>document.getElementById("inline-script-valid-sha1-hash").innerHTML = "allowed";</script>
+ <!-- 'md5-/m4wX3YU+IHs158KwKOBWg==' (in policy) -->
+ <script>document.getElementById("inline-script-valid-md5-hash").innerHTML = "allowed";</script>
+
+ <!-- inline styles -->
+ <p id="inline-style-valid-hash"></p>
+ <p id="inline-style-invalid-hash"></p>
+ <p id="inline-style-invalid-hash-valid-nonce"></p>
+ <p id="inline-style-valid-hash-invalid-nonce"></p>
+ <p id="inline-style-invalid-hash-invalid-nonce"></p>
+ <p id="inline-style-valid-sha512-hash"></p>
+ <p id="inline-style-valid-sha384-hash"></p>
+ <p id="inline-style-valid-sha1-hash"></p>
+ <p id="inline-style-valid-md5-hash"></p>
+
+ <!-- 'sha256-UpNH6x+Ux99QTW1fJikQsVbBERJruIC98et0YDVKKHQ=' (in policy) -->
+ <style>p#inline-style-valid-hash { color: green; }</style>
+ <!-- 'sha256-+TYxTx+bsfTDdivWLZUwScEYyxuv6lknMbNjrgGBRZo=' (not in policy) -->
+ <style>p#inline-style-invalid-hash { color: red; }</style>
+ <!-- 'sha256-U+9UPC/CFzz3QuOrl5q3KCVNngOYWuIkE2jK6Ir0Mbs=' (not in policy) -->
+ <style nonce="ftL2UbGHlSEaZTLWMwtA5Q==">p#inline-style-invalid-hash-valid-nonce { color: green; }</style>
+ <!-- 'sha256-0IPbWW5IDJ/juvETq60oTnhC+XzOqdYp5/UBsBKCaOY=' (in policy) -->
+ <style nonce="foobar">p#inline-style-valid-hash-invalid-nonce { color: green; }</style>
+ <!-- 'sha256-KaHZgPd4nC4S8BVLT/9WjzdPDtunGWojR83C2whbd50=' (not in policy) -->
+ <style nonce="foobar">p#inline-style-invalid-hash-invalid-nonce { color: red; }</style>
+ <!-- 'sha512-EpcDbSuvFv0HIyKtU5tQMN7UtBMeEbljz1dWPfy7PNCa1RYdHKwdJWT1tie41evq/ZUL1rzadSVdEzq3jl6Twg==' (in policy) -->
+ <style>p#inline-style-valid-sha512-hash { color: green; }</style>
+ <!-- 'sha384-c5W8ON4WyeA2zEOGdrOGhRmRYI8+2UzUUmhGQFjUFP6yiPZx9FGEV3UOiQ+tIshF' (in policy) -->
+ <style>p#inline-style-valid-sha384-hash { color: green; }</style>
+ <!-- 'sha1-T/+b4sxCIiJxDr6XS9dAEyHKt2M=' (in policy) -->
+ <style>p#inline-style-valid-sha1-hash { color: red; }</style>
+ <!-- 'md5-oNrgrtzOZduwDYYi1yo12g==' (in policy) -->
+ <style>p#inline-style-valid-md5-hash { color: red; }</style>
+
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_hash_source.html^headers^ b/dom/security/test/csp/file_hash_source.html^headers^
new file mode 100644
index 0000000000..785d63391e
--- /dev/null
+++ b/dom/security/test/csp/file_hash_source.html^headers^
@@ -0,0 +1,2 @@
+Content-Security-Policy: script-src 'sha256-siVR8vAcqP06h2ppeNwqgjr0yZ6yned4X2VF84j4GmI=' 'nonce-jPRxvuRHbiQnCWVuoCMAvQ==' 'sha256-z7rzCkbOJqi08lga3CVQ3b+3948ZbJWaSxsBs8zPliE=' 'sha512-tMLuv22jJ5RHkvLNlv0otvA2fgw6PF16HKu6wy0ZDQ3M7UKzoygs1uxIMSfjMttgWrB5WRvIr35zrTZppMYBVw==' 'sha384-XjAD+FxZfipkxna4id1JrR2QP6OYUZfAxpn9+yHOmT1VSLVa9SQR/dz7CEb7jw7w' 'sha1-LHErkMxKGcSpa/znpzmKYkKnI30=' 'md5-/m4wX3YU+IHs158KwKOBWg=='; style-src 'sha256-UpNH6x+Ux99QTW1fJikQsVbBERJruIC98et0YDVKKHQ=' 'nonce-ftL2UbGHlSEaZTLWMwtA5Q==' 'sha256-0IPbWW5IDJ/juvETq60oTnhC+XzOqdYp5/UBsBKCaOY=' 'sha512-EpcDbSuvFv0HIyKtU5tQMN7UtBMeEbljz1dWPfy7PNCa1RYdHKwdJWT1tie41evq/ZUL1rzadSVdEzq3jl6Twg==' 'sha384-c5W8ON4WyeA2zEOGdrOGhRmRYI8+2UzUUmhGQFjUFP6yiPZx9FGEV3UOiQ+tIshF' 'sha1-T/+b4sxCIiJxDr6XS9dAEyHKt2M=' 'md5-oNrgrtzOZduwDYYi1yo12g==';
+Cache-Control: no-cache
diff --git a/dom/security/test/csp/file_iframe_parent_location_js.html b/dom/security/test/csp/file_iframe_parent_location_js.html
new file mode 100644
index 0000000000..0d980f9925
--- /dev/null
+++ b/dom/security/test/csp/file_iframe_parent_location_js.html
@@ -0,0 +1,10 @@
+<html>
+<head>
+ <title>Test setting parent location to javascript:</title>
+</head>
+<body>
+<script>
+ parent.window.location ="javascript:location.href";
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_iframe_sandbox_document_write.html b/dom/security/test/csp/file_iframe_sandbox_document_write.html
new file mode 100644
index 0000000000..a3a0952941
--- /dev/null
+++ b/dom/security/test/csp/file_iframe_sandbox_document_write.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+<head> <meta charset="utf-8"> </head>
+<script type="text/javascript">
+ function ok(result, desc) {
+ window.parent.postMessage({ok: result, desc}, "*");
+ }
+ function doStuff() {
+ var beforePrincipal = SpecialPowers.wrap(document).nodePrincipal;
+ document.open();
+ document.write("rewritten sandboxed document");
+ document.close();
+ var afterPrincipal = SpecialPowers.wrap(document).nodePrincipal;
+ ok(beforePrincipal.equals(afterPrincipal),
+ "document.write() does not change underlying principal");
+ }
+</script>
+<body onLoad='doStuff();'>
+ sandboxed with allow-scripts
+</body>
+</html>
diff --git a/dom/security/test/csp/file_iframe_sandbox_srcdoc.html b/dom/security/test/csp/file_iframe_sandbox_srcdoc.html
new file mode 100644
index 0000000000..bc700ed68f
--- /dev/null
+++ b/dom/security/test/csp/file_iframe_sandbox_srcdoc.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1073952 - CSP should restrict scripts in srcdoc iframe even if sandboxed</title>
+</head>
+<body>
+<iframe srcdoc="<img src=x onerror='parent.postMessage({result: `unexpected-csp-violation`}, `*`);'>"
+ sandbox="allow-scripts"></iframe>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_iframe_sandbox_srcdoc.html^headers^ b/dom/security/test/csp/file_iframe_sandbox_srcdoc.html^headers^
new file mode 100644
index 0000000000..cf869e07d4
--- /dev/null
+++ b/dom/security/test/csp/file_iframe_sandbox_srcdoc.html^headers^
@@ -0,0 +1 @@
+content-security-policy: default-src *;
diff --git a/dom/security/test/csp/file_iframe_srcdoc.sjs b/dom/security/test/csp/file_iframe_srcdoc.sjs
new file mode 100644
index 0000000000..25172b58d5
--- /dev/null
+++ b/dom/security/test/csp/file_iframe_srcdoc.sjs
@@ -0,0 +1,86 @@
+// Custom *.sjs file specifically for the needs of
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1073952
+
+"use strict";
+
+const SCRIPT = `
+ <script>
+ parent.parent.postMessage({result: &quot;allowed&quot;}, &quot;*&quot;);
+ </script>`;
+
+const SIMPLE_IFRAME_SRCDOC =
+ `
+ <!DOCTYPE html>
+ <html>
+ <head><meta charset="utf-8"></head>
+ <body>
+ <iframe sandbox="allow-scripts" srcdoc="` +
+ SCRIPT +
+ `"></iframe>
+ </body>
+ </html>`;
+
+const INNER_SRCDOC_IFRAME = `
+ <iframe sandbox='allow-scripts' srcdoc='<script>
+ parent.parent.parent.postMessage({result: &quot;allowed&quot;}, &quot;*&quot;);
+ </script>'>
+ </iframe>`;
+
+const NESTED_IFRAME_SRCDOC =
+ `
+ <!DOCTYPE html>
+ <html>
+ <head><meta charset="utf-8"></head>
+ <body>
+ <iframe sandbox="allow-scripts" srcdoc="` +
+ INNER_SRCDOC_IFRAME +
+ `"></iframe>
+ </body>
+ </html>`;
+
+const INNER_DATAURI_IFRAME = `
+ <iframe sandbox='allow-scripts' src='data:text/html,<script>
+ parent.parent.parent.postMessage({result: &quot;allowed&quot;}, &quot;*&quot;);
+ </script>'>
+ </iframe>`;
+
+const NESTED_IFRAME_SRCDOC_DATAURI =
+ `
+ <!DOCTYPE html>
+ <html>
+ <head><meta charset="utf-8"></head>
+ <body>
+ <iframe sandbox="allow-scripts" srcdoc="` +
+ INNER_DATAURI_IFRAME +
+ `"></iframe>
+ </body>
+ </html>`;
+
+function handleRequest(request, response) {
+ const query = new URLSearchParams(request.queryString);
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ if (typeof query.get("csp") === "string") {
+ response.setHeader("Content-Security-Policy", query.get("csp"), false);
+ }
+ response.setHeader("Content-Type", "text/html", false);
+
+ if (query.get("action") === "simple_iframe_srcdoc") {
+ response.write(SIMPLE_IFRAME_SRCDOC);
+ return;
+ }
+
+ if (query.get("action") === "nested_iframe_srcdoc") {
+ response.write(NESTED_IFRAME_SRCDOC);
+ return;
+ }
+
+ if (query.get("action") === "nested_iframe_srcdoc_datauri") {
+ response.write(NESTED_IFRAME_SRCDOC_DATAURI);
+ return;
+ }
+
+ // we should never get here, but just in case
+ // return something unexpected
+ response.write("do'h");
+}
diff --git a/dom/security/test/csp/file_ignore_unsafe_inline.html b/dom/security/test/csp/file_ignore_unsafe_inline.html
new file mode 100644
index 0000000000..773184201c
--- /dev/null
+++ b/dom/security/test/csp/file_ignore_unsafe_inline.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Bug 1004703 - ignore 'unsafe-inline' if nonce- or hash-source specified</title>
+</head>
+<body>
+<div id="testdiv">a</div>
+
+<!-- first script allowlisted by 'unsafe-inline' -->
+<script type="application/javascript">
+document.getElementById('testdiv').innerHTML += 'b';
+</script>
+
+<!-- second script allowlisted by hash -->
+<!-- sha256-uJXAPKP5NZxnVMZMUkDofh6a9P3UMRc1CRTevVPS/rI= -->
+<script type="application/javascript">
+document.getElementById('testdiv').innerHTML += 'c';
+</script>
+
+<!-- thrid script allowlisted by nonce -->
+<script type="application/javascript" nonce="FooNonce">
+document.getElementById('testdiv').innerHTML += 'd';
+</script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_ignore_unsafe_inline_multiple_policies_server.sjs b/dom/security/test/csp/file_ignore_unsafe_inline_multiple_policies_server.sjs
new file mode 100644
index 0000000000..32327b7575
--- /dev/null
+++ b/dom/security/test/csp/file_ignore_unsafe_inline_multiple_policies_server.sjs
@@ -0,0 +1,58 @@
+// custom *.sjs file specifically for the needs of:
+// * Bug 1004703 - ignore 'unsafe-inline' if nonce- or hash-source specified
+// * Bug 1198422: should not block inline script if default-src is not specified
+
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+
+function loadHTMLFromFile(path) {
+ // Load the HTML to return in the response from file.
+ // Since it's relative to the cwd of the test runner, we start there and
+ // append to get to the actual path of the file.
+ var testHTMLFile = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+ var dirs = path.split("/");
+ for (var i = 0; i < dirs.length; i++) {
+ testHTMLFile.append(dirs[i]);
+ }
+ var testHTMLFileStream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ testHTMLFileStream.init(testHTMLFile, -1, 0, 0);
+ var testHTML = NetUtil.readInputStreamToString(
+ testHTMLFileStream,
+ testHTMLFileStream.available()
+ );
+ return testHTML;
+}
+
+function handleRequest(request, response) {
+ var query = {};
+ request.queryString.split("&").forEach(function (val) {
+ var [name, value] = val.split("=");
+ query[name] = unescape(value);
+ });
+
+ var csp1 = query.csp1 ? unescape(query.csp1) : "";
+ var csp2 = query.csp2 ? unescape(query.csp2) : "";
+ var file = unescape(query.file);
+
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ // deliver the CSP encoded in the URI
+ // please note that comma separation of two policies
+ // acts like sending *two* separate policies
+ var csp = csp1;
+ if (csp2 !== "") {
+ csp += ", " + csp2;
+ }
+ response.setHeader("Content-Security-Policy", csp, false);
+
+ // Send HTML to test allowed/blocked behaviors
+ response.setHeader("Content-Type", "text/html", false);
+
+ response.write(loadHTMLFromFile(file));
+}
diff --git a/dom/security/test/csp/file_ignore_xfo.html b/dom/security/test/csp/file_ignore_xfo.html
new file mode 100644
index 0000000000..6746a3adba
--- /dev/null
+++ b/dom/security/test/csp/file_ignore_xfo.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1024557: Ignore x-frame-options if CSP with frame-ancestors exists</title>
+</head>
+<body>
+<div id="cspmessage">Ignoring XFO because of CSP</div>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_ignore_xfo.html^headers^ b/dom/security/test/csp/file_ignore_xfo.html^headers^
new file mode 100644
index 0000000000..e93f9e3ecb
--- /dev/null
+++ b/dom/security/test/csp/file_ignore_xfo.html^headers^
@@ -0,0 +1,3 @@
+Content-Security-Policy: frame-ancestors http://mochi.test:8888
+X-Frame-Options: deny
+Cache-Control: no-cache
diff --git a/dom/security/test/csp/file_image_document_pixel.png b/dom/security/test/csp/file_image_document_pixel.png
new file mode 100644
index 0000000000..52c591798e
--- /dev/null
+++ b/dom/security/test/csp/file_image_document_pixel.png
Binary files differ
diff --git a/dom/security/test/csp/file_image_document_pixel.png^headers^ b/dom/security/test/csp/file_image_document_pixel.png^headers^
new file mode 100644
index 0000000000..7c727854d0
--- /dev/null
+++ b/dom/security/test/csp/file_image_document_pixel.png^headers^
@@ -0,0 +1,2 @@
+Content-Security-Policy: default-src https://bug1627235.test.com
+Cache-Control: no-cache
diff --git a/dom/security/test/csp/file_image_nonce.html b/dom/security/test/csp/file_image_nonce.html
new file mode 100644
index 0000000000..5d57bb8372
--- /dev/null
+++ b/dom/security/test/csp/file_image_nonce.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <title>Bug 1355801: Nonce should not apply to images</title>
+ </head>
+<body>
+
+<img id='matchingNonce' src='http://mochi.test:8888/tests/image/test/mochitest/blue.png?a' nonce='abc'></img>
+<img id='nonMatchingNonce' src='http://mochi.test:8888/tests/image/test/mochitest/blue.png?b' nonce='bca'></img>
+<img id='noNonce' src='http://mochi.test:8888/tests/image/test/mochitest/blue.png?c'></img>
+
+<script type='application/javascript'>
+ var matchingNonce = document.getElementById('matchingNonce');
+ matchingNonce.onload = function(e) {
+ window.parent.postMessage({result: 'img-with-matching-nonce-loaded'}, '*');
+ };
+ matchingNonce.onerror = function(e) {
+ window.parent.postMessage({result: 'img-with-matching-nonce-blocked'}, '*');
+ }
+
+ var nonMatchingNonce = document.getElementById('nonMatchingNonce');
+ nonMatchingNonce.onload = function(e) {
+ window.parent.postMessage({result: 'img-with_non-matching-nonce-loaded'}, '*');
+ };
+ nonMatchingNonce.onerror = function(e) {
+ window.parent.postMessage({result: 'img-with_non-matching-nonce-blocked'}, '*');
+ }
+
+ var noNonce = document.getElementById('noNonce');
+ noNonce.onload = function(e) {
+ window.parent.postMessage({result: 'img-without-nonce-loaded'}, '*');
+ };
+ noNonce.onerror = function(e) {
+ window.parent.postMessage({result: 'img-without-nonce-blocked'}, '*');
+ }
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_image_nonce.html^headers^ b/dom/security/test/csp/file_image_nonce.html^headers^
new file mode 100644
index 0000000000..0d63558c46
--- /dev/null
+++ b/dom/security/test/csp/file_image_nonce.html^headers^
@@ -0,0 +1,2 @@
+Content-Security-Policy: img-src 'nonce-abc';
+Cache-Control: no-cache
diff --git a/dom/security/test/csp/file_independent_iframe_csp.html b/dom/security/test/csp/file_independent_iframe_csp.html
new file mode 100644
index 0000000000..0581f5ea85
--- /dev/null
+++ b/dom/security/test/csp/file_independent_iframe_csp.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1419222 - iFrame CSP should not affect parent document CSP</title>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Security-Policy" content="connect-src *; style-src * 'unsafe-inline'; "/>
+</head>
+<body>
+ <script>
+ var getCspObj = function(doc) {
+ var contentDoc = SpecialPowers.wrap(doc);
+ var cspJSON = contentDoc.cspJSON;
+ var cspOBJ = JSON.parse(cspJSON);
+ return cspOBJ;
+ }
+
+ // Add an iFrame, add an additional CSP directive to that iFrame, and
+ // return the CSP object of that iFrame.
+ var addIFrame = function() {
+ var frame = document.createElement("iframe");
+ frame.id = "nestedframe";
+ document.body.appendChild(frame);
+ var metaTag = document.createElement("meta");
+ metaTag.setAttribute("http-equiv", "Content-Security-Policy");
+ metaTag.setAttribute("content", "img-src 'self' data:;");
+ frame.contentDocument.head.appendChild(metaTag);
+ return getCspObj(frame.contentDocument);
+ }
+
+ // Get the CSP objects of the parent document before and after adding the
+ // iFrame, as well as of the iFram itself.
+ var parentBeginCspObj = getCspObj(document);
+ var iFrameCspObj = addIFrame();
+ var parentEndCspObj = getCspObj(document);
+
+ // Post a message containing the three CSP objects to the test context.
+ window.parent.postMessage(
+ {result: [parentBeginCspObj, iFrameCspObj, parentEndCspObj]},
+ "*"
+ );
+ </script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_inlinescript.html b/dom/security/test/csp/file_inlinescript.html
new file mode 100644
index 0000000000..55a9b9b180
--- /dev/null
+++ b/dom/security/test/csp/file_inlinescript.html
@@ -0,0 +1,15 @@
+<html>
+<head>
+ <title>CSP inline script tests</title>
+</head>
+<body onload="window.parent.postMessage('body-onload-fired', '*')">
+ <script type="text/javascript">
+ window.parent.postMessage("text-node-fired", "*");
+ </script>
+
+ <iframe src='javascript:window.parent.parent.postMessage("javascript-uri-fired", "*")'></iframe>
+
+ <a id='anchortoclick' href='javascript:window.parent.postMessage("javascript-uri-anchor-fired", "*")'>testlink</a>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_inlinestyle_main.html b/dom/security/test/csp/file_inlinestyle_main.html
new file mode 100644
index 0000000000..a0d2969883
--- /dev/null
+++ b/dom/security/test/csp/file_inlinestyle_main.html
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<html>
+ <head>
+ <title>CSP inline script tests</title>
+ <!-- content= "div#linkstylediv { color: #0f0; }" -->
+ <link rel="stylesheet" type="text/css"
+ href='file_CSP.sjs?type=text/css&content=div%23linkstylediv%20%7B%20color%3A%20%230f0%3B%20%7D' />
+ <!-- content= "div#modifycsstextdiv { color: #0f0; }" -->
+ <link rel="stylesheet" type="text/css"
+ href='file_CSP.sjs?type=text/css&content=div%23modifycsstextdiv%20%7B%20color%3A%20%23f00%3B%20%7D' />
+ <script>
+ function cssTest() {
+ var elem = document.getElementById('csstextstylediv');
+ elem.style.cssText = "color: #00FF00;";
+ getComputedStyle(elem, null).color;
+
+ document.styleSheets[1].cssRules[0].style.cssText = "color: #00FF00;";
+ elem = document.getElementById('modifycsstextdiv');
+ getComputedStyle(elem, null).color;
+ }
+ </script>
+ </head>
+ <body onload='cssTest()'>
+
+ <style type="text/css">
+ div#inlinestylediv {
+ color: #FF0000;
+ }
+ </style>
+
+ <div id='linkstylediv'>Link tag (external) stylesheet test (should be green)</div>
+ <div id='inlinestylediv'>Inline stylesheet test (should be black)</div>
+ <div id='attrstylediv' style="color: #FF0000;">Attribute stylesheet test (should be black)</div>
+ <div id='csstextstylediv'>cssText test (should be black)</div>
+ <div id='modifycsstextdiv'> modify rule from style sheet via cssText(should be green) </div>
+
+ <!-- tests for SMIL stuff - animations -->
+ <svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ width="100%"
+ height="100px">
+
+ <!-- Animates XML attribute, which is mapped into style. -->
+ <text id="xmlTest" x="0" y="15">
+ This shouldn't be red since the animation should be blocked by CSP.
+
+ <animate attributeName="fill" attributeType="XML"
+ values="red;orange;red" dur="2s"
+ repeatCount="indefinite" />
+ </text>
+
+ <!-- Animates override value for CSS property. -->
+ <text id="cssOverrideTest" x="0" y="35">
+ This shouldn't be red since the animation should be blocked by CSP.
+
+ <animate attributeName="fill" attributeType="CSS"
+ values="red;orange;red" dur="2s"
+ repeatCount="indefinite" />
+ </text>
+
+ <!-- Animates override value for CSS property targeted via ID. -->
+ <text id="cssOverrideTestById" x="0" y="55">
+ This shouldn't be red since the animation should be blocked by CSP.
+ </text>
+ <animate xlink:href="#cssOverrideTestById"
+ attributeName="fill"
+ values="red;orange;red"
+ dur="2s" repeatCount="indefinite" />
+
+ <!-- Sets value for CSS property targeted via ID. -->
+ <text id="cssSetTestById" x="0" y="75">
+ This shouldn't be red since the &lt;set&gt; should be blocked by CSP.
+ </text>
+ <set xlink:href="#cssSetTestById"
+ attributeName="fill"
+ to="red" />
+ </svg>
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_inlinestyle_main.html^headers^ b/dom/security/test/csp/file_inlinestyle_main.html^headers^
new file mode 100644
index 0000000000..7b6a251679
--- /dev/null
+++ b/dom/security/test/csp/file_inlinestyle_main.html^headers^
@@ -0,0 +1,2 @@
+Content-Security-Policy: default-src 'self' ; script-src 'self' 'unsafe-inline'
+Cache-Control: no-cache
diff --git a/dom/security/test/csp/file_inlinestyle_main_allowed.html b/dom/security/test/csp/file_inlinestyle_main_allowed.html
new file mode 100644
index 0000000000..9b533ef074
--- /dev/null
+++ b/dom/security/test/csp/file_inlinestyle_main_allowed.html
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<html>
+ <head>
+ <title>CSP inline script tests</title>
+ <!-- content= "div#linkstylediv { color: #0f0; }" -->
+ <link rel="stylesheet" type="text/css"
+ href='file_CSP.sjs?type=text/css&content=div%23linkstylediv%20%7B%20color%3A%20%230f0%3B%20%7D' />
+ <!-- content= "div#modifycsstextdiv { color: #f00; }" -->
+ <link rel="stylesheet" type="text/css"
+ href='file_CSP.sjs?type=text/css&content=div%23modifycsstextdiv%20%7B%20color%3A%20%23f00%3B%20%7D' />
+ <script>
+ function cssTest() {
+ // CSSStyleDeclaration.cssText
+ var elem = document.getElementById('csstextstylediv');
+ elem.style.cssText = "color: #00FF00;";
+
+ // If I call getComputedStyle as below, this test passes as the parent page
+ // correctly detects that the text is colored green - if I remove this, getComputedStyle
+ // thinks the text is black when called by the parent page.
+ getComputedStyle(elem, null).color;
+
+ document.styleSheets[1].cssRules[0].style.cssText = "color: #00FF00;";
+ elem = document.getElementById('modifycsstextdiv');
+ getComputedStyle(elem, null).color;
+ }
+ </script>
+ </head>
+ <body onload='cssTest()'>
+
+ <style type="text/css">
+ div#inlinestylediv {
+ color: #00FF00;
+ }
+ </style>
+
+ <div id='linkstylediv'>Link tag (external) stylesheet test (should be green)</div>
+ <div id='inlinestylediv'>Inline stylesheet test (should be green)</div>
+ <div id='attrstylediv' style="color: #00FF00;">Attribute stylesheet test (should be green)</div>
+ <div id='csstextstylediv'>style.cssText test (should be green)</div>
+ <div id='modifycsstextdiv'> modify rule from style sheet via cssText(should be green) </div>
+
+ <!-- tests for SMIL stuff - animations -->
+ <svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ width="100%"
+ height="100px">
+
+ <!-- Animates XML attribute, which is mapped into style. -->
+ <text id="xmlTest" x="0" y="15">
+ This should be green since the animation should be allowed by CSP.
+
+ <animate attributeName="fill" attributeType="XML"
+ values="lime;green;lime" dur="2s"
+ repeatCount="indefinite" />
+ </text>
+
+ <!-- Animates override value for CSS property. -->
+ <text id="cssOverrideTest" x="0" y="35">
+ This should be green since the animation should be allowed by CSP.
+
+ <animate attributeName="fill" attributeType="CSS"
+ values="lime;green;lime" dur="2s"
+ repeatCount="indefinite" />
+ </text>
+
+ <!-- Animates override value for CSS property targeted via ID. -->
+ <text id="cssOverrideTestById" x="0" y="55">
+ This should be green since the animation should be allowed by CSP.
+ </text>
+ <animate xlink:href="#cssOverrideTestById"
+ attributeName="fill"
+ values="lime;green;lime"
+ dur="2s" repeatCount="indefinite" />
+
+ <!-- Sets value for CSS property targeted via ID. -->
+ <text id="cssSetTestById" x="0" y="75">
+ This should be green since the &lt;set&gt; should be allowed by CSP.
+ </text>
+ <set xlink:href="#cssSetTestById"
+ attributeName="fill"
+ to="lime" />
+ </svg>
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_inlinestyle_main_allowed.html^headers^ b/dom/security/test/csp/file_inlinestyle_main_allowed.html^headers^
new file mode 100644
index 0000000000..621d2536b0
--- /dev/null
+++ b/dom/security/test/csp/file_inlinestyle_main_allowed.html^headers^
@@ -0,0 +1,2 @@
+Content-Security-Policy: default-src 'self' ; script-src 'self' 'unsafe-inline' ; style-src 'self' 'unsafe-inline'
+Cache-Control: no-cache
diff --git a/dom/security/test/csp/file_invalid_source_expression.html b/dom/security/test/csp/file_invalid_source_expression.html
new file mode 100644
index 0000000000..83bb0ec0ca
--- /dev/null
+++ b/dom/security/test/csp/file_invalid_source_expression.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Bug 1086612 - CSP: Let source expression be the empty set in case no valid source can be parsed</title>
+ </head>
+ <body>
+ <div id="testdiv">blocked</div>
+ <!-- Note, we reuse file_path_matching.js which only updates the testdiv to 'allowed' if loaded !-->
+ <script src="http://test1.example.com/tests/dom/security/test/csp/file_path_matching.js"></script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_leading_wildcard.html b/dom/security/test/csp/file_leading_wildcard.html
new file mode 100644
index 0000000000..ea5e993447
--- /dev/null
+++ b/dom/security/test/csp/file_leading_wildcard.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Bug 1032303 - CSP - Keep FULL STOP when matching *.foo.com to disallow loads from foo.com</title>
+ </head>
+ <body>
+ <!-- Please note that both scripts do *not* exist in the file system -->
+ <script src="http://test1.example.com/tests/dom/security/test/csp/leading_wildcard_allowed.js" ></script>
+ <script src="http://example.com/tests/dom/security/test/csp/leading_wildcard_blocked.js" ></script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_link_rel_preload.html b/dom/security/test/csp/file_link_rel_preload.html
new file mode 100644
index 0000000000..8af49a77fe
--- /dev/null
+++ b/dom/security/test/csp/file_link_rel_preload.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html>
+<head>
+ <title>Bug 1599791 - Test link rel=preload</title>
+ <!-- Please note that fakeServer does not exist in our testsuite -->
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'">
+ <link rel="preload" as="script" href="fakeServer?script"></link>
+ <link rel="preload" as="style" href="fakeServer?style"></link>
+ <link rel="preload" as="image" href="fakeServer?image"></link>
+ <link rel="preload" as="fetch" href="fakeServer?fetch"></link>
+ <link rel="preload" as="font" href="fakeServer?font"></link>
+
+ <link rel="stylesheet" href="fakeServer?style">
+</head>
+<body>
+<script src="fakeServer?script"></script>
+<img src="fakeServer?image"></img>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_main.html b/dom/security/test/csp/file_main.html
new file mode 100644
index 0000000000..ddc8382617
--- /dev/null
+++ b/dom/security/test/csp/file_main.html
@@ -0,0 +1,55 @@
+<html>
+ <head>
+ <link rel='stylesheet' type='text/css'
+ href='http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=style_bad&type=text/css' />
+ <link rel='stylesheet' type='text/css'
+ href='file_CSP.sjs?testid=style_good&type=text/css' />
+
+
+ <style>
+ /* CSS font embedding tests */
+ @font-face {
+ font-family: "arbitrary_good";
+ src: url('file_CSP.sjs?testid=font_good&type=application/octet-stream');
+ }
+ @font-face {
+ font-family: "arbitrary_bad";
+ src: url('http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=font_bad&type=application/octet-stream');
+ }
+
+ .div_arbitrary_good { font-family: "arbitrary_good"; }
+ .div_arbitrary_bad { font-family: "arbitrary_bad"; }
+ </style>
+ </head>
+ <body>
+ <!-- these should be stopped by CSP. :) -->
+ <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img_bad&type=img/png"> </img>
+ <audio src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=media_bad&type=audio/vorbis"></audio>
+ <script src='http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=script_bad&type=text/javascript'></script>
+ <iframe src='http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=frame_bad&content=FAIL'></iframe>
+ <object width="10" height="10">
+ <param name="movie" value="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=object_bad&type=application/x-shockwave-flash">
+ <embed src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=object_bad&type=application/x-shockwave-flash"></embed>
+ </object>
+
+ <!-- these should load ok. :) -->
+ <img src="file_CSP.sjs?testid=img_good&type=img/png" />
+ <audio src="file_CSP.sjs?testid=media_good&type=audio/vorbis"></audio>
+ <script src='file_CSP.sjs?testid=script_good&type=text/javascript'></script>
+ <iframe src='file_CSP.sjs?testid=frame_good&content=PASS'></iframe>
+
+ <object width="10" height="10">
+ <param name="movie" value="file_CSP.sjs?testid=object_good&type=application/x-shockwave-flash">
+ <embed src="file_CSP.sjs?testid=object_good&type=application/x-shockwave-flash"></embed>
+ </object>
+
+ <!-- XHR tests... they're taken care of in this script,
+ and since the URI doesn't have any 'testid' values,
+ it will just be ignored by the test framework. -->
+ <script src='file_main.js'></script>
+
+ <!-- Support elements for the @font-face test -->
+ <div class="div_arbitrary_good">arbitrary good</div>
+ <div class="div_arbitrary_bad">arbitrary_bad</div>
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_main.html^headers^ b/dom/security/test/csp/file_main.html^headers^
new file mode 100644
index 0000000000..3338de389b
--- /dev/null
+++ b/dom/security/test/csp/file_main.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src 'self' blob: ; style-src 'unsafe-inline' 'self'
diff --git a/dom/security/test/csp/file_main.js b/dom/security/test/csp/file_main.js
new file mode 100644
index 0000000000..01dd43cbf5
--- /dev/null
+++ b/dom/security/test/csp/file_main.js
@@ -0,0 +1,26 @@
+function doXHR(uri) {
+ try {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", uri);
+ xhr.send();
+ } catch (ex) {}
+}
+
+doXHR(
+ "http://mochi.test:8888/tests/dom/security/test/csp/file_CSP.sjs?testid=xhr_good"
+);
+doXHR(
+ "http://example.com/tests/dom/security/test/csp/file_CSP.sjs?testid=xhr_bad"
+);
+fetch(
+ "http://mochi.test:8888/tests/dom/security/test/csp/file_CSP.sjs?testid=fetch_good"
+);
+fetch(
+ "http://example.com/tests/dom/security/test/csp/file_CSP.sjs?testid=fetch_bad"
+);
+navigator.sendBeacon(
+ "http://mochi.test:8888/tests/dom/security/test/csp/file_CSP.sjs?testid=beacon_good"
+);
+navigator.sendBeacon(
+ "http://example.com/tests/dom/security/test/csp/file_CSP.sjs?testid=beacon_bad"
+);
diff --git a/dom/security/test/csp/file_meta_element.html b/dom/security/test/csp/file_meta_element.html
new file mode 100644
index 0000000000..17f19c7c86
--- /dev/null
+++ b/dom/security/test/csp/file_meta_element.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Security-Policy"
+ content= "img-src 'none'; script-src 'unsafe-inline'; report-uri http://www.example.com; frame-ancestors https:; sandbox allow-scripts">
+ <title>Bug 663570 - Implement Content Security Policy via meta tag</title>
+</head>
+<body>
+
+ <!-- try to load an image which is forbidden by meta CSP -->
+ <img id="testimage"></img>
+
+ <script type="application/javascript">
+ var myImg = document.getElementById("testimage");
+ myImg.onload = function(e) {
+ window.parent.postMessage({result: "img-loaded"}, "*");
+ };
+ myImg.onerror = function(e) {
+ window.parent.postMessage({result: "img-blocked"}, "*");
+ };
+ //Image should be tried to load only after onload/onerror event declaration.
+ myImg.src = "http://mochi.test:8888/tests/image/test/mochitest/blue.png";
+ </script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_meta_header_dual.sjs b/dom/security/test/csp/file_meta_header_dual.sjs
new file mode 100644
index 0000000000..445b3e444e
--- /dev/null
+++ b/dom/security/test/csp/file_meta_header_dual.sjs
@@ -0,0 +1,101 @@
+// Custom *.sjs file specifically for the needs of Bug:
+// Bug 663570 - Implement Content Security Policy via meta tag
+
+const HTML_HEAD =
+ "<!DOCTYPE HTML>" +
+ "<html>" +
+ "<head>" +
+ "<meta charset='utf-8'>" +
+ "<title>Bug 663570 - Implement Content Security Policy via <meta> tag</title>";
+
+const HTML_BODY =
+ "</head>" +
+ "<body>" +
+ "<img id='testimage' src='http://mochi.test:8888/tests/image/test/mochitest/blue.png'></img>" +
+ "<script type='application/javascript'>" +
+ " var myImg = document.getElementById('testimage');" +
+ " myImg.onload = function(e) {" +
+ " window.parent.postMessage({result: 'img-loaded'}, '*');" +
+ " };" +
+ " myImg.onerror = function(e) { " +
+ " window.parent.postMessage({result: 'img-blocked'}, '*');" +
+ " };" +
+ "</script>" +
+ "</body>" +
+ "</html>";
+
+const META_CSP_BLOCK_IMG =
+ '<meta http-equiv="Content-Security-Policy" content="img-src \'none\'">';
+
+const META_CSP_ALLOW_IMG =
+ '<meta http-equiv="Content-Security-Policy" content="img-src http://mochi.test:8888;">';
+
+const HEADER_CSP_BLOCK_IMG = "img-src 'none';";
+
+const HEADER_CSP_ALLOW_IMG = "img-src http://mochi.test:8888";
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+ var queryString = request.queryString;
+
+ if (queryString === "test1") {
+ /* load image without any CSP */
+ response.write(HTML_HEAD + HTML_BODY);
+ return;
+ }
+
+ if (queryString === "test2") {
+ /* load image where meta denies load */
+ response.write(HTML_HEAD + META_CSP_BLOCK_IMG + HTML_BODY);
+ return;
+ }
+
+ if (queryString === "test3") {
+ /* load image where meta allows load */
+ response.write(HTML_HEAD + META_CSP_ALLOW_IMG + HTML_BODY);
+ return;
+ }
+
+ if (queryString === "test4") {
+ /* load image where meta allows but header blocks */
+ response.setHeader("Content-Security-Policy", HEADER_CSP_BLOCK_IMG, false);
+ response.write(HTML_HEAD + META_CSP_ALLOW_IMG + HTML_BODY);
+ return;
+ }
+
+ if (queryString === "test5") {
+ /* load image where meta blocks but header allows */
+ response.setHeader("Content-Security-Policy", HEADER_CSP_ALLOW_IMG, false);
+ response.write(HTML_HEAD + META_CSP_BLOCK_IMG + HTML_BODY);
+ return;
+ }
+
+ if (queryString === "test6") {
+ /* load image where meta allows and header allows */
+ response.setHeader("Content-Security-Policy", HEADER_CSP_ALLOW_IMG, false);
+ response.write(HTML_HEAD + META_CSP_ALLOW_IMG + HTML_BODY);
+ return;
+ }
+
+ if (queryString === "test7") {
+ /* load image where meta1 allows but meta2 blocks */
+ response.write(
+ HTML_HEAD + META_CSP_ALLOW_IMG + META_CSP_BLOCK_IMG + HTML_BODY
+ );
+ return;
+ }
+
+ if (queryString === "test8") {
+ /* load image where meta1 allows and meta2 allows */
+ response.write(
+ HTML_HEAD + META_CSP_ALLOW_IMG + META_CSP_ALLOW_IMG + HTML_BODY
+ );
+ return;
+ }
+
+ // we should never get here, but just in case, return
+ // something unexpected
+ response.write("do'h");
+}
diff --git a/dom/security/test/csp/file_meta_whitespace_skipping.html b/dom/security/test/csp/file_meta_whitespace_skipping.html
new file mode 100644
index 0000000000..c0cfc8cc28
--- /dev/null
+++ b/dom/security/test/csp/file_meta_whitespace_skipping.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <!-- Test all the different space characters within the meta csp:
+ * U+0020 space | &#x20;
+ * U+0009 tab | &#x9;
+ * U+000A line feed | &#xa;
+ * U+000C form feed | &#xc;
+ * U+000D carriage return | &#xd;
+ !-->
+ <meta http-equiv="Content-Security-Policy"
+ content= "
+ img-src&#x20; 'none'; &#x20;
+ script-src 'unsafe-inline' &#xd;
+ ;
+ style-src&#x9;&#x9; https://example.com&#xa;
+ https://foo.com;&#xc;;;;&#xc;;;&#xc; child-src foo.com
+ bar.com
+ &#xd;;&#xd;
+ font-src 'none'">
+ <title>Bug 1261634 - Update whitespace skipping for meta csp</title>
+</head>
+<body>
+ <script type="application/javascript">
+ // notify the including document that we are done parsing the meta csp
+ window.parent.postMessage({result: "meta-csp-parsed"}, "*");
+ </script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_multi_policy_injection_bypass.html b/dom/security/test/csp/file_multi_policy_injection_bypass.html
new file mode 100644
index 0000000000..a3cb415a9e
--- /dev/null
+++ b/dom/security/test/csp/file_multi_policy_injection_bypass.html
@@ -0,0 +1,15 @@
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=717511
+-->
+ <body>
+ <!-- these should be stopped by CSP after fixing bug 717511. :) -->
+ <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img_bad&type=img/png"> </img>
+ <script src='http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=script_bad&type=text/javascript'></script>
+
+ <!-- these should load ok after fixing bug 717511. :) -->
+ <img src="file_CSP.sjs?testid=img_good&type=img/png" />
+ <script src='file_CSP.sjs?testid=script_good&type=text/javascript'></script>
+
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_multi_policy_injection_bypass.html^headers^ b/dom/security/test/csp/file_multi_policy_injection_bypass.html^headers^
new file mode 100644
index 0000000000..e1b64a9220
--- /dev/null
+++ b/dom/security/test/csp/file_multi_policy_injection_bypass.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src 'self', default-src *
diff --git a/dom/security/test/csp/file_multi_policy_injection_bypass_2.html b/dom/security/test/csp/file_multi_policy_injection_bypass_2.html
new file mode 100644
index 0000000000..3fa6c7ab91
--- /dev/null
+++ b/dom/security/test/csp/file_multi_policy_injection_bypass_2.html
@@ -0,0 +1,15 @@
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=717511
+-->
+ <body>
+ <!-- these should be stopped by CSP after fixing bug 717511. :) -->
+ <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img2_bad&type=img/png"> </img>
+ <script src='http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=script2_bad&type=text/javascript'></script>
+
+ <!-- these should load ok after fixing bug 717511. :) -->
+ <img src="file_CSP.sjs?testid=img2_good&type=img/png" />
+ <script src='file_CSP.sjs?testid=script2_good&type=text/javascript'></script>
+
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_multi_policy_injection_bypass_2.html^headers^ b/dom/security/test/csp/file_multi_policy_injection_bypass_2.html^headers^
new file mode 100644
index 0000000000..b523073cd3
--- /dev/null
+++ b/dom/security/test/csp/file_multi_policy_injection_bypass_2.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src 'self' , default-src *
diff --git a/dom/security/test/csp/file_multipart_testserver.sjs b/dom/security/test/csp/file_multipart_testserver.sjs
new file mode 100644
index 0000000000..571dd4006d
--- /dev/null
+++ b/dom/security/test/csp/file_multipart_testserver.sjs
@@ -0,0 +1,160 @@
+// SJS file specifically for the needs of bug
+// Bug 1416045/Bug 1223743 - CSP: Check baseChannel for CSP when loading multipart channel
+
+var CSP = "script-src 'unsafe-inline', img-src 'none'";
+var rootCSP = "script-src 'unsafe-inline'";
+var part1CSP = "img-src *";
+var part2CSP = "img-src 'none'";
+var BOUNDARY = "fooboundary";
+
+// small red image
+const IMG_BYTES = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
+ "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="
+);
+
+var RESPONSE = `
+ <script>
+ var myImg = new Image;
+ myImg.src = "file_multipart_testserver.sjs?img";
+ myImg.onerror = function(e) {
+ window.parent.postMessage({"test": "rootCSP_test",
+ "msg": "img-blocked"}, "*");
+ };
+ myImg.onload = function() {
+ window.parent.postMessage({"test": "rootCSP_test",
+ "msg": "img-loaded"}, "*");
+ };
+ document.body.appendChild(myImg);
+ </script>
+`;
+
+var RESPONSE1 = `
+ <body>
+ <script>
+ var triggerNextPartFrame = document.createElement('iframe');
+ var myImg = new Image;
+ myImg.src = "file_multipart_testserver.sjs?img";
+ myImg.onerror = function(e) {
+ window.parent.postMessage({"test": "part1CSP_test",
+ "msg": "part1-img-blocked"}, "*");
+ triggerNextPartFrame.src = 'file_multipart_testserver.sjs?sendnextpart';
+ };
+ myImg.onload = function() {
+ window.parent.postMessage({"test": "part1CSP_test",
+ "msg": "part1-img-loaded"}, "*");
+ triggerNextPartFrame.src = 'file_multipart_testserver.sjs?sendnextpart';
+ };
+ document.body.appendChild(myImg);
+ document.body.appendChild(triggerNextPartFrame);
+ </script>
+ </body>
+`;
+
+var RESPONSE2 = `
+ <body>
+ <script>
+ var myImg = new Image;
+ myImg.src = "file_multipart_testserver.sjs?img";
+ myImg.onerror = function(e) {
+ window.parent.postMessage({"test": "part2CSP_test",
+ "msg": "part2-img-blocked"}, "*");
+ };
+ myImg.onload = function() {
+ window.parent.postMessage({"test": "part2CSP_test",
+ "msg": "part2-img-loaded"}, "*");
+ };
+ document.body.appendChild(myImg);
+ </script>
+ </body>
+`;
+
+function setGlobalState(data, key) {
+ x = {
+ data,
+ QueryInterface(iid) {
+ return this;
+ },
+ };
+ x.wrappedJSObject = x;
+ setObjectState(key, x);
+}
+
+function getGlobalState(key) {
+ var data;
+ getObjectState(key, function (x) {
+ data = x && x.wrappedJSObject.data;
+ });
+ return data;
+}
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ if (request.queryString == "doc") {
+ response.setHeader("Content-Security-Policy", CSP, false);
+ response.setHeader(
+ "Content-Type",
+ "multipart/x-mixed-replace; boundary=" + BOUNDARY,
+ false
+ );
+ response.write(BOUNDARY + "\r\n");
+ response.write(RESPONSE);
+ response.write(BOUNDARY + "\r\n");
+ return;
+ }
+
+ if (request.queryString == "partcspdoc") {
+ response.setHeader("Content-Security-Policy", rootCSP, false);
+ response.setHeader(
+ "Content-Type",
+ "multipart/x-mixed-replace; boundary=" + BOUNDARY,
+ false
+ );
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.processAsync();
+ response.write("--" + BOUNDARY + "\r\n");
+ sendNextPart(response, 1);
+ return;
+ }
+
+ if (request.queryString == "sendnextpart") {
+ response.setStatusLine(request.httpVersion, 204, "No content");
+ var blockedResponse = getGlobalState("root-document-response");
+ if (typeof blockedResponse == "object") {
+ sendNextPart(blockedResponse, 2);
+ sendClose(blockedResponse);
+ } else {
+ dump("Couldn't find the stored response object.");
+ }
+ return;
+ }
+
+ if (request.queryString == "img") {
+ response.setHeader("Content-Type", "image/png");
+ response.write(IMG_BYTES);
+ return;
+ }
+
+ // we should never get here - return something unexpected
+ response.write("d'oh");
+}
+
+function sendClose(response) {
+ response.write("--" + BOUNDARY + "--\r\n");
+ response.finish();
+}
+
+function sendNextPart(response, partNumber) {
+ response.write("Content-type: text/html" + "\r\n");
+ if (partNumber == 1) {
+ response.write("Content-Security-Policy:" + part1CSP + "\r\n");
+ response.write(RESPONSE1);
+ setGlobalState(response, "root-document-response");
+ } else {
+ response.write("Content-Security-Policy:" + part2CSP + "\r\n");
+ response.write(RESPONSE2);
+ }
+ response.write("--" + BOUNDARY + "\r\n");
+}
diff --git a/dom/security/test/csp/file_no_log_ignore_xfo.html b/dom/security/test/csp/file_no_log_ignore_xfo.html
new file mode 100644
index 0000000000..fc5528a35c
--- /dev/null
+++ b/dom/security/test/csp/file_no_log_ignore_xfo.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1722252: "Content-Security-Policy: Ignoring ‘x-frame-options’ because of ‘frame-ancestors’ directive." warning message even when no "x-frame-options" header present</title>
+</head>
+<body>
+<div id="cspmessage">Do not log xfo ignore warning when no xfo is set.</div>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_no_log_ignore_xfo.html^headers^ b/dom/security/test/csp/file_no_log_ignore_xfo.html^headers^
new file mode 100644
index 0000000000..1fbbf3de99
--- /dev/null
+++ b/dom/security/test/csp/file_no_log_ignore_xfo.html^headers^
@@ -0,0 +1,2 @@
+Content-Security-Policy: frame-ancestors http://mochi.test:8888
+Cache-Control: no-cache
diff --git a/dom/security/test/csp/file_nonce_redirector.sjs b/dom/security/test/csp/file_nonce_redirector.sjs
new file mode 100644
index 0000000000..b56b9ded37
--- /dev/null
+++ b/dom/security/test/csp/file_nonce_redirector.sjs
@@ -0,0 +1,28 @@
+// custom *.sjs file for
+// Bug 1469150:Scripts with valid nonce get blocked if URL redirects.
+
+const URL_PATH = "example.com/tests/dom/security/test/csp/";
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+ let queryStr = request.queryString;
+
+ if (queryStr === "redirect") {
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader(
+ "Location",
+ "https://" + URL_PATH + "file_nonce_redirector.sjs?load",
+ false
+ );
+ return;
+ }
+
+ if (queryStr === "load") {
+ response.setHeader("Content-Type", "application/javascript", false);
+ response.write("console.log('script loaded');");
+ return;
+ }
+
+ // we should never get here - return something unexpected
+ response.write("d'oh");
+}
diff --git a/dom/security/test/csp/file_nonce_redirects.html b/dom/security/test/csp/file_nonce_redirects.html
new file mode 100644
index 0000000000..e291164900
--- /dev/null
+++ b/dom/security/test/csp/file_nonce_redirects.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <meta http-equiv="Content-Security-Policy" content="script-src 'nonce-abcd1234'">
+ <title>Bug 1469150:Scripts with valid nonce get blocked if URL redirects</title>
+ </head>
+<body>
+
+<script nonce='abcd1234' id='redirectScript'></script>
+
+<script nonce='abcd1234' type='application/javascript'>
+ var redirectScript = document.getElementById('redirectScript');
+ redirectScript.onload = function(e) {
+ window.parent.postMessage({result: 'script-loaded'}, '*');
+ };
+ redirectScript.onerror = function(e) {
+ window.parent.postMessage({result: 'script-blocked'}, '*');
+ }
+ redirectScript.src = 'file_nonce_redirector.sjs?redirect';
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_nonce_snapshot.sjs b/dom/security/test/csp/file_nonce_snapshot.sjs
new file mode 100644
index 0000000000..2b114fd87e
--- /dev/null
+++ b/dom/security/test/csp/file_nonce_snapshot.sjs
@@ -0,0 +1,54 @@
+"use strict";
+
+const TEST_FRAME = `<!DOCTYPE HTML>
+ <html>
+ <body>
+ <script id='myScript' nonce='123456789' type='application/javascript'></script>
+ <script nonce='123456789'>
+ let myScript = document.getElementById('myScript');
+ // 1) start loading the script using the nonce 123456789
+ myScript.src='file_nonce_snapshot.sjs?redir-script';
+ // 2) dynamically change the nonce, load should use initial nonce
+ myScript.setAttribute('nonce','987654321');
+ </script>
+ </body>
+ </html>`;
+
+const SCRIPT = "window.parent.postMessage('script-loaded', '*');";
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ let queryString = request.queryString;
+
+ if (queryString === "load-frame") {
+ response.setHeader(
+ "Content-Security-Policy",
+ "script-src 'nonce-123456789'",
+ false
+ );
+ response.setHeader("Content-Type", "text/html", false);
+ response.write(TEST_FRAME);
+ return;
+ }
+
+ if (queryString === "redir-script") {
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader(
+ "Location",
+ "file_nonce_snapshot.sjs?load-script",
+ false
+ );
+ return;
+ }
+
+ if (queryString === "load-script") {
+ response.setHeader("Content-Type", "application/javascript", false);
+ response.write(SCRIPT);
+ return;
+ }
+
+ // we should never get here but just in case return something unexpected
+ response.write("do'h");
+}
diff --git a/dom/security/test/csp/file_nonce_source.html b/dom/security/test/csp/file_nonce_source.html
new file mode 100644
index 0000000000..01d4046c37
--- /dev/null
+++ b/dom/security/test/csp/file_nonce_source.html
@@ -0,0 +1,73 @@
+<!doctype html>
+<html>
+ <head>
+ <!-- external styles -->
+ <link rel='stylesheet' nonce="correctstylenonce" href="file_CSP.sjs?testid=external_style_correct_nonce_good&type=text/css" />
+ <link rel='stylesheet' nonce="incorrectstylenonce" href="file_CSP.sjs?testid=external_style_incorrect_nonce_bad&type=text/css" />
+ <link rel='stylesheet' nonce="correctscriptnonce" href="file_CSP.sjs?testid=external_style_correct_script_nonce_bad&type=text/css" />
+ <link rel='stylesheet' href="file_CSP.sjs?testid=external_style_no_nonce_bad&type=text/css" />
+ </head>
+ <body>
+ <!-- inline scripts -->
+ <ol>
+ <li id="inline-script-correct-nonce">(inline script with correct nonce) This text should be green.</li>
+ <li id="inline-script-incorrect-nonce">(inline script with incorrect nonce) This text should be black.</li>
+ <li id="inline-script-correct-style-nonce">(inline script with correct nonce for styles, but not for scripts) This text should be black.</li>
+ <li id="inline-script-no-nonce">(inline script with no nonce) This text should be black.</li>
+ </ol>
+ <script nonce="correctscriptnonce">
+ document.getElementById("inline-script-correct-nonce").style.color = "rgb(0, 128, 0)";
+ </script>
+ <script nonce="incorrectscriptnonce">
+ document.getElementById("inline-script-incorrect-nonce").style.color = "rgb(255, 0, 0)";
+ </script>
+ <script nonce="correctstylenonce">
+ document.getElementById("inline-script-correct-style-nonce").style.color = "rgb(255, 0, 0)";
+ </script>
+ <script>
+ document.getElementById("inline-script-no-nonce").style.color = "rgb(255, 0, 0)";
+ </script>
+
+ <!-- external scripts -->
+ <script nonce="correctscriptnonce" src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=external_script_correct_nonce_good&type=text/javascript"></script>
+ <script nonce="anothercorrectscriptnonce" src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=external_script_another_correct_nonce_good&type=text/javascript"></script>
+ <script nonce="incorrectscriptnonce" src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=external_script_incorrect_nonce_bad&type=text/javascript"></script>
+ <script nonce="correctstylenonce" src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=external_script_correct_style_nonce_bad&type=text/javascript"></script>
+ <script src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=external_script_no_nonce_bad&type=text/javascript"></script>
+
+ <!-- This external script has the correct nonce and comes from a allowlisted URI. It should be allowed. -->
+ <script nonce="correctscriptnonce" src="file_CSP.sjs?testid=external_script_correct_nonce_correct_uri_good&type=text/javascript"></script>
+ <!-- This external script has an incorrect nonce, but comes from a allowlisted URI. It should be allowed. -->
+ <script nonce="incorrectscriptnonce" src="file_CSP.sjs?testid=external_script_incorrect_nonce_correct_uri_good&type=text/javascript"></script>
+ <!-- This external script has no nonce and comes from a allowlisted URI. It should be allowed. -->
+ <script src="file_CSP.sjs?testid=external_script_no_nonce_correct_uri_good&type=text/javascript"></script>
+
+ <!-- inline styles -->
+ <ol>
+ <li id=inline-style-correct-nonce>
+ (inline style with correct nonce) This text should be green
+ </li>
+ <li id=inline-style-incorrect-nonce>
+ (inline style with incorrect nonce) This text should be black
+ </li>
+ <li id=inline-style-correct-script-nonce>
+ (inline style with correct script, not style, nonce) This text should be black
+ </li>
+ <li id=inline-style-no-nonce>
+ (inline style with no nonce) This text should be black
+ </li>
+ </ol>
+ <style nonce=correctstylenonce>
+ li#inline-style-correct-nonce { color: green; }
+ </style>
+ <style nonce=incorrectstylenonce>
+ li#inline-style-incorrect-nonce { color: red; }
+ </style>
+ <style nonce=correctscriptnonce>
+ li#inline-style-correct-script-nonce { color: red; }
+ </style>
+ <style>
+ li#inline-style-no-nonce { color: red; }
+ </style>
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_nonce_source.html^headers^ b/dom/security/test/csp/file_nonce_source.html^headers^
new file mode 100644
index 0000000000..865e5fe984
--- /dev/null
+++ b/dom/security/test/csp/file_nonce_source.html^headers^
@@ -0,0 +1,2 @@
+Content-Security-Policy: script-src 'self' 'nonce-correctscriptnonce' 'nonce-anothercorrectscriptnonce'; style-src 'nonce-correctstylenonce';
+Cache-Control: no-cache
diff --git a/dom/security/test/csp/file_null_baseuri.html b/dom/security/test/csp/file_null_baseuri.html
new file mode 100644
index 0000000000..f995688b13
--- /dev/null
+++ b/dom/security/test/csp/file_null_baseuri.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Bug 1121857 - document.baseURI should not get blocked if baseURI is null</title>
+ </head>
+ <body>
+ <script type="text/javascript">
+ // check the initial base-uri
+ window.parent.postMessage({baseURI: document.baseURI, test: "initial_base_uri"}, "*");
+
+ // append a child and check the base-uri
+ var baseTag = document.head.appendChild(document.createElement('base'));
+ baseTag.href = 'http://www.base-tag.com';
+ window.parent.postMessage({baseURI: document.baseURI, test: "changed_base_uri"}, "*");
+
+ // remove the child and check that the base-uri is back to the initial one
+ document.head.remove(baseTag);
+ window.parent.postMessage({baseURI: document.baseURI, test: "initial_base_uri"}, "*");
+ </script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_object_inherit.html b/dom/security/test/csp/file_object_inherit.html
new file mode 100644
index 0000000000..76c9764162
--- /dev/null
+++ b/dom/security/test/csp/file_object_inherit.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1457100: Test OBJECT inherits CSP if needed</title>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Security-Policy" content= "img-src https://bug1457100.test.com"/>
+</head>
+<body>
+<object id="dataObject" data="data:text/html,object<script>var foo = 0;</script>"></object>
+
+<script type="application/javascript">
+ var dataObject = document.getElementById("dataObject");
+ dataObject.onload = function () {
+ var contentDoc = SpecialPowers.wrap(dataObject).contentDocument;
+ var cspJSON = contentDoc.cspJSON;
+ window.parent.postMessage({cspJSON}, "*");
+ }
+</script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_parent_location_js.html b/dom/security/test/csp/file_parent_location_js.html
new file mode 100644
index 0000000000..9c56f49905
--- /dev/null
+++ b/dom/security/test/csp/file_parent_location_js.html
@@ -0,0 +1,18 @@
+<html>
+<head>
+ <title>Test setting parent location to javascript:</title>
+ <meta http-equiv="Content-Security-Policy" content="script-src 'nonce-bug1550414'">
+ <script nonce="bug1550414">
+ document.addEventListener("securitypolicyviolation", (e) => {
+ window.parent.postMessage({
+ blockedURI: e.blockedURI,
+ violatedDirective: e.violatedDirective,
+ originalPolicy: e.originalPolicy,
+ }, '*');
+ });
+ </script>
+</head>
+<body>
+ <iframe src="file_iframe_parent_location_js.html"></iframe>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_path_matching.html b/dom/security/test/csp/file_path_matching.html
new file mode 100644
index 0000000000..662fbfb8af
--- /dev/null
+++ b/dom/security/test/csp/file_path_matching.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Bug 808292 - Implement path-level host-source matching to CSP</title>
+ </head>
+ <body>
+ <div id="testdiv">blocked</div>
+ <script src="http://test1.example.com/tests/dom/security/test/csp/file_path_matching.js#foo"></script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_path_matching.js b/dom/security/test/csp/file_path_matching.js
new file mode 100644
index 0000000000..09286d42e9
--- /dev/null
+++ b/dom/security/test/csp/file_path_matching.js
@@ -0,0 +1 @@
+document.getElementById("testdiv").innerHTML = "allowed";
diff --git a/dom/security/test/csp/file_path_matching_incl_query.html b/dom/security/test/csp/file_path_matching_incl_query.html
new file mode 100644
index 0000000000..50af2b1437
--- /dev/null
+++ b/dom/security/test/csp/file_path_matching_incl_query.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Bug 1147026 - CSP should ignore query string when checking a resource load</title>
+ </head>
+ <body>
+ <div id="testdiv">blocked</div>
+ <script src="http://test1.example.com/tests/dom/security/test/csp/file_path_matching.js?val=foo"></script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_path_matching_redirect.html b/dom/security/test/csp/file_path_matching_redirect.html
new file mode 100644
index 0000000000..a16cc90ec6
--- /dev/null
+++ b/dom/security/test/csp/file_path_matching_redirect.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Bug 808292 - Implement path-level host-source matching to CSP</title>
+ </head>
+ <body>
+ <div id="testdiv">blocked</div>
+ <script src="http://example.com/tests/dom/security/test/csp/file_path_matching_redirect_server.sjs"></script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_path_matching_redirect_server.sjs b/dom/security/test/csp/file_path_matching_redirect_server.sjs
new file mode 100644
index 0000000000..bed3a1dccf
--- /dev/null
+++ b/dom/security/test/csp/file_path_matching_redirect_server.sjs
@@ -0,0 +1,12 @@
+// Redirect server specifically to handle redirects
+// for path-level host-source matching
+// see https://bugzilla.mozilla.org/show_bug.cgi?id=808292
+
+function handleRequest(request, response) {
+ var newLocation =
+ "http://test1.example.com/tests/dom/security/test/csp/file_path_matching.js";
+
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Location", newLocation, false);
+}
diff --git a/dom/security/test/csp/file_pdfjs_not_subject_to_csp.html b/dom/security/test/csp/file_pdfjs_not_subject_to_csp.html
new file mode 100644
index 0000000000..da5c7f0a6e
--- /dev/null
+++ b/dom/security/test/csp/file_pdfjs_not_subject_to_csp.html
@@ -0,0 +1,21 @@
+<html>
+<head>
+ <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'nonce-allowPDF'; base-uri 'self'">
+</head>
+<body>
+<iframe id="pdfFrame"></iframe>
+<br/>
+<button id="pdfButton">click to load pdf</button>
+<script nonce="allowPDF">
+ async function loadPDFIntoIframe() {
+ let response = await fetch("dummy.pdf");
+ let blob = await response.blob();
+ var blobUrl = URL.createObjectURL(blob);
+ var pdfFrame = document.getElementById("pdfFrame");
+ pdfFrame.src = blobUrl;
+ }
+ let pdfButton = document.getElementById("pdfButton");
+ pdfButton.addEventListener("click", loadPDFIntoIframe);
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_ping.html b/dom/security/test/csp/file_ping.html
new file mode 100644
index 0000000000..8aaf34cc3a
--- /dev/null
+++ b/dom/security/test/csp/file_ping.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1100181 - CSP: Enforce connect-src when submitting pings</title>
+</head>
+<body>
+ <!-- we are using an image for the test, but can be anything -->
+ <a id="testlink"
+ href="http://mochi.test:8888/tests/image/test/mochitest/blue.png"
+ ping="http://mochi.test:8888/tests/image/test/mochitest/blue.png?send-ping">
+ Send ping
+ </a>
+
+ <script type="text/javascript">
+ var link = document.getElementById("testlink");
+ link.click();
+ </script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_policyuri_regression_from_multipolicy.html b/dom/security/test/csp/file_policyuri_regression_from_multipolicy.html
new file mode 100644
index 0000000000..2a75eef7e8
--- /dev/null
+++ b/dom/security/test/csp/file_policyuri_regression_from_multipolicy.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<html>
+ <body>
+ <div id=testdiv>Inline script didn't run</div>
+ <script>
+ document.getElementById('testdiv').innerHTML = "Inline Script Executed";
+ </script>
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_policyuri_regression_from_multipolicy.html^headers^ b/dom/security/test/csp/file_policyuri_regression_from_multipolicy.html^headers^
new file mode 100644
index 0000000000..c4ff8ea9fd
--- /dev/null
+++ b/dom/security/test/csp/file_policyuri_regression_from_multipolicy.html^headers^
@@ -0,0 +1 @@
+content-security-policy-report-only: policy-uri /tests/dom/security/test/csp/file_policyuri_regression_from_multipolicy_policy
diff --git a/dom/security/test/csp/file_policyuri_regression_from_multipolicy_policy b/dom/security/test/csp/file_policyuri_regression_from_multipolicy_policy
new file mode 100644
index 0000000000..a5c610cd7b
--- /dev/null
+++ b/dom/security/test/csp/file_policyuri_regression_from_multipolicy_policy
@@ -0,0 +1 @@
+default-src 'self';
diff --git a/dom/security/test/csp/file_punycode_host_src.js b/dom/security/test/csp/file_punycode_host_src.js
new file mode 100644
index 0000000000..9728e2fecc
--- /dev/null
+++ b/dom/security/test/csp/file_punycode_host_src.js
@@ -0,0 +1,2 @@
+const LOADED = true;
+parent.postMessage({ result: "script-allowed" }, "*");
diff --git a/dom/security/test/csp/file_punycode_host_src.sjs b/dom/security/test/csp/file_punycode_host_src.sjs
new file mode 100644
index 0000000000..99f76d5317
--- /dev/null
+++ b/dom/security/test/csp/file_punycode_host_src.sjs
@@ -0,0 +1,46 @@
+// custom *.sjs for Bug 1224225
+// Punycode in CSP host sources
+
+const HTML_PART1 =
+ "<!DOCTYPE HTML>" +
+ '<html><head><meta charset="utf-8">' +
+ "<title>Bug 1224225 - CSP source matching should work for punycoded domain names</title>" +
+ "</head>" +
+ "<body>" +
+ "<script id='script' src='";
+
+// U+00E4 LATIN SMALL LETTER A WITH DIAERESIS, encoded as UTF-8 code units.
+// response.write() writes out the provided string characters truncated to
+// bytes, so "ä" literally would write a literal \xE4 byte, not the desired
+// two-byte UTF-8 sequence.
+const TESTCASE1 = "http://sub2.\xC3\xA4lt.example.org/";
+const TESTCASE2 = "http://sub2.xn--lt-uia.example.org/";
+
+const HTML_PART2 =
+ "tests/dom/security/test/csp/file_punycode_host_src.js'></script>" +
+ "</body>" +
+ "</html>";
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+
+ const query = new URLSearchParams(request.queryString);
+
+ if (query.get("csp")) {
+ response.setHeader("Content-Security-Policy", query.get("csp"), false);
+ }
+ if (query.get("action") == "script-unicode-csp-punycode") {
+ response.write(HTML_PART1 + TESTCASE1 + HTML_PART2);
+ return;
+ }
+ if (query.get("action") == "script-punycode-csp-punycode") {
+ response.write(HTML_PART1 + TESTCASE2 + HTML_PART2);
+ return;
+ }
+
+ // we should never get here, but just in case
+ // return something unexpected
+ response.write("do'h");
+}
diff --git a/dom/security/test/csp/file_redirect_content.sjs b/dom/security/test/csp/file_redirect_content.sjs
new file mode 100644
index 0000000000..8d7d6a8224
--- /dev/null
+++ b/dom/security/test/csp/file_redirect_content.sjs
@@ -0,0 +1,39 @@
+// https://bugzilla.mozilla.org/show_bug.cgi?id=650386
+// This SJS file serves file_redirect_content.html
+// with a CSP that will trigger a violation and that will report it
+// to file_redirect_report.sjs
+//
+// This handles 301, 302, 303 and 307 redirects. The HTTP status code
+// returned/type of redirect to do comes from the query string
+// parameter passed in from the test_bug650386_* files and then also
+// uses that value in the report-uri parameter of the CSP
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ // this gets used in the CSP as part of the report URI.
+ var redirect = request.queryString;
+
+ if (redirect < 301 || (redirect > 303 && redirect <= 306) || redirect > 307) {
+ // if we somehow got some bogus redirect code here,
+ // do a 302 redirect to the same URL as the report URI
+ // redirects to - this will fail the test.
+ var loc = "http://example.com/some/fake/path";
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", loc, false);
+ return;
+ }
+
+ var csp =
+ "default-src 'self';report-uri http://mochi.test:8888/tests/dom/security/test/csp/file_redirect_report.sjs?" +
+ redirect;
+
+ response.setHeader("Content-Security-Policy", csp, false);
+
+ // the actual file content.
+ // this image load will (intentionally) fail due to the CSP policy of default-src: 'self'
+ // specified by the CSP string above.
+ var content =
+ '<!DOCTYPE HTML><html><body><img src = "http://some.other.domain.example.com"></body></html>';
+
+ response.write(content);
+}
diff --git a/dom/security/test/csp/file_redirect_report.sjs b/dom/security/test/csp/file_redirect_report.sjs
new file mode 100644
index 0000000000..42b69357b7
--- /dev/null
+++ b/dom/security/test/csp/file_redirect_report.sjs
@@ -0,0 +1,16 @@
+// https://bugzilla.mozilla.org/show_bug.cgi?id=650386
+// This SJS file serves as CSP violation report target
+// and issues a redirect, to make sure the browser does not post to the target
+// of the redirect, per CSP spec.
+// This handles 301, 302, 303 and 307 redirects. The HTTP status code
+// returned/type of redirect to do comes from the query string
+// parameter
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ var redirect = request.queryString;
+
+ var loc = "http://example.com/some/fake/path";
+ response.setStatusLine("1.1", redirect, "Found");
+ response.setHeader("Location", loc, false);
+}
diff --git a/dom/security/test/csp/file_redirect_worker.sjs b/dom/security/test/csp/file_redirect_worker.sjs
new file mode 100644
index 0000000000..5cf211484e
--- /dev/null
+++ b/dom/security/test/csp/file_redirect_worker.sjs
@@ -0,0 +1,34 @@
+// SJS file to serve resources for CSP redirect tests
+// This file redirects to a specified resource.
+const THIS_SITE = "http://mochi.test:8888";
+const OTHER_SITE = "http://example.com";
+
+function handleRequest(request, response) {
+ var query = {};
+ request.queryString.split("&").forEach(function (val) {
+ var [name, value] = val.split("=");
+ query[name] = unescape(value);
+ });
+
+ var resource = query.path;
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ var loc = "";
+
+ // redirect to a resource on this site
+ if (query.redir == "same") {
+ loc = THIS_SITE + resource + "#" + query.page_id;
+ }
+
+ // redirect to a resource on a different site
+ else if (query.redir == "other") {
+ loc = OTHER_SITE + resource + "#" + query.page_id;
+ }
+
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", loc, false);
+
+ response.write(
+ '<html><head><meta http-equiv="refresh" content="0; url=' + loc + '">'
+ );
+}
diff --git a/dom/security/test/csp/file_redirects_main.html b/dom/security/test/csp/file_redirects_main.html
new file mode 100644
index 0000000000..d05af88fe8
--- /dev/null
+++ b/dom/security/test/csp/file_redirects_main.html
@@ -0,0 +1,37 @@
+<html>
+<head>
+<title>CSP redirect tests</title>
+</head>
+<body>
+<div id="container"></div>
+</body>
+
+<script>
+var thisSite = "http://mochi.test:8888";
+var otherSite = "http://example.com";
+var page = "/tests/dom/security/test/csp/file_redirects_page.sjs";
+
+var tests = { "font-src": thisSite+page+"?testid=font-src",
+ "frame-src": thisSite+page+"?testid=frame-src",
+ "img-src": thisSite+page+"?testid=img-src",
+ "media-src": thisSite+page+"?testid=media-src",
+ "object-src": thisSite+page+"?testid=object-src",
+ "script-src": thisSite+page+"?testid=script-src",
+ "style-src": thisSite+page+"?testid=style-src",
+ "xhr-src": thisSite+page+"?testid=xhr-src",
+ "from-worker": thisSite+page+"?testid=from-worker",
+ "from-blob-worker": thisSite+page+"?testid=from-blob-worker",
+ "img-src-from-css": thisSite+page+"?testid=img-src-from-css",
+ };
+
+var container = document.getElementById("container");
+
+// load each test in its own iframe
+for (tid in tests) {
+ var i = document.createElement("iframe");
+ i.id = tid;
+ i.src = tests[tid];
+ container.appendChild(i);
+}
+</script>
+</html>
diff --git a/dom/security/test/csp/file_redirects_page.sjs b/dom/security/test/csp/file_redirects_page.sjs
new file mode 100644
index 0000000000..0ce9cc75ec
--- /dev/null
+++ b/dom/security/test/csp/file_redirects_page.sjs
@@ -0,0 +1,140 @@
+// SJS file for CSP redirect mochitests
+// This file serves pages which can optionally specify a Content Security Policy
+function handleRequest(request, response) {
+ var query = {};
+ request.queryString.split("&").forEach(function (val) {
+ var [name, value] = val.split("=");
+ query[name] = unescape(value);
+ });
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+
+ var resource = "/tests/dom/security/test/csp/file_redirects_resource.sjs";
+
+ // CSP header value
+ response.setHeader(
+ "Content-Security-Policy",
+ "default-src 'self' blob: ; style-src 'self' 'unsafe-inline'",
+ false
+ );
+
+ // downloadable font that redirects to another site
+ if (query.testid == "font-src") {
+ var resp =
+ '<style type="text/css"> @font-face { font-family:' +
+ '"Redirecting Font"; src: url("' +
+ resource +
+ '?res=font&redir=other&id=font-src-redir")} #test{font-family:' +
+ '"Redirecting Font"}</style></head><body>' +
+ '<div id="test">test</div></body>';
+ response.write(resp);
+ return;
+ }
+
+ // iframe that redirects to another site
+ if (query.testid == "frame-src") {
+ response.write(
+ '<iframe src="' +
+ resource +
+ '?res=iframe&redir=other&id=frame-src-redir"></iframe>'
+ );
+ return;
+ }
+
+ // image that redirects to another site
+ if (query.testid == "img-src") {
+ response.write(
+ '<img src="' + resource + '?res=image&redir=other&id=img-src-redir" />'
+ );
+ return;
+ }
+
+ // video content that redirects to another site
+ if (query.testid == "media-src") {
+ response.write(
+ '<video src="' +
+ resource +
+ '?res=media&redir=other&id=media-src-redir"></video>'
+ );
+ return;
+ }
+
+ // object content that redirects to another site
+ if (query.testid == "object-src") {
+ response.write(
+ '<object type="text/html" data="' +
+ resource +
+ '?res=object&redir=other&id=object-src-redir"></object>'
+ );
+ return;
+ }
+
+ // external script that redirects to another site
+ if (query.testid == "script-src") {
+ response.write(
+ '<script src="' +
+ resource +
+ '?res=script&redir=other&id=script-src-redir"></script>'
+ );
+ return;
+ }
+
+ // external stylesheet that redirects to another site
+ if (query.testid == "style-src") {
+ response.write(
+ '<link rel="stylesheet" type="text/css" href="' +
+ resource +
+ '?res=style&redir=other&id=style-src-redir"></link>'
+ );
+ return;
+ }
+
+ // script that XHR's to a resource that redirects to another site
+ if (query.testid == "xhr-src") {
+ response.write('<script src="' + resource + '?res=xhr"></script>');
+ return;
+ }
+
+ // for bug949706
+ if (query.testid == "img-src-from-css") {
+ // loads a stylesheet, which in turn loads an image that redirects.
+ response.write(
+ '<link rel="stylesheet" type="text/css" href="' +
+ resource +
+ '?res=cssLoader&id=img-src-redir-from-css">'
+ );
+ return;
+ }
+
+ if (query.testid == "from-worker") {
+ // loads a script; launches a worker; that worker uses importscript; which then gets redirected
+ // So it's:
+ // <script src="res=loadWorkerThatMakesRequests">
+ // .. loads Worker("res=makeRequestsWorker")
+ // .. calls importScript("res=script")
+ // .. calls xhr("res=xhr-resp")
+ // .. calls fetch("res=xhr-resp")
+ response.write(
+ '<script src="' +
+ resource +
+ '?res=loadWorkerThatMakesRequests&id=from-worker"></script>'
+ );
+ return;
+ }
+
+ if (query.testid == "from-blob-worker") {
+ // loads a script; launches a worker; that worker uses importscript; which then gets redirected
+ // So it's:
+ // <script src="res=loadBlobWorkerThatMakesRequests">
+ // .. loads Worker("res=makeRequestsWorker")
+ // .. calls importScript("res=script")
+ // .. calls xhr("res=xhr-resp")
+ // .. calls fetch("res=xhr-resp")
+ response.write(
+ '<script src="' +
+ resource +
+ '?res=loadBlobWorkerThatMakesRequests&id=from-blob-worker"></script>'
+ );
+ }
+}
diff --git a/dom/security/test/csp/file_redirects_resource.sjs b/dom/security/test/csp/file_redirects_resource.sjs
new file mode 100644
index 0000000000..df0b8101d8
--- /dev/null
+++ b/dom/security/test/csp/file_redirects_resource.sjs
@@ -0,0 +1,171 @@
+// SJS file to serve resources for CSP redirect tests
+// This file mimics serving resources, e.g. fonts, images, etc., which a CSP
+// can include. The resource may redirect to a different resource, if specified.
+function handleRequest(request, response) {
+ var query = {};
+ request.queryString.split("&").forEach(function (val) {
+ var [name, value] = val.split("=");
+ query[name] = unescape(value);
+ });
+
+ var thisSite = "http://mochi.test:8888";
+ var otherSite = "http://example.com";
+ var resource = "/tests/dom/security/test/csp/file_redirects_resource.sjs";
+
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ // redirect to a resource on this site
+ if (query.redir == "same") {
+ var loc = thisSite + resource + "?res=" + query.res + "&testid=" + query.id;
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", loc, false);
+ return;
+ }
+
+ // redirect to a resource on a different site
+ else if (query.redir == "other") {
+ var loc =
+ otherSite + resource + "?res=" + query.res + "&testid=" + query.id;
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", loc, false);
+ return;
+ }
+
+ // not a redirect. serve some content.
+ // the content doesn't have to be valid, since we're only checking whether
+ // the request for the content was sent or not.
+
+ // downloadable font
+ if (query.res == "font") {
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.setHeader("Content-Type", "text/plain", false);
+ response.write("font data...");
+ return;
+ }
+
+ // iframe with arbitrary content
+ if (query.res == "iframe") {
+ response.setHeader("Content-Type", "text/html", false);
+ response.write("iframe content...");
+ return;
+ }
+
+ // image
+ if (query.res == "image") {
+ response.setHeader("Content-Type", "image/gif", false);
+ response.write("image data...");
+ return;
+ }
+
+ // media content, e.g. Ogg video
+ if (query.res == "media") {
+ response.setHeader("Content-Type", "video/ogg", false);
+ response.write("video data...");
+ return;
+ }
+
+ // plugin content, e.g. <object>
+ if (query.res == "object") {
+ response.setHeader("Content-Type", "text/html", false);
+ response.write("object data...");
+ return;
+ }
+
+ // script
+ if (query.res == "script") {
+ response.setHeader("Content-Type", "application/javascript", false);
+ response.write("some script...");
+ return;
+ }
+
+ // external stylesheet
+ if (query.res == "style") {
+ response.setHeader("Content-Type", "text/css", false);
+ response.write("css data...");
+ return;
+ }
+
+ // internal stylesheet that loads an image from an external site
+ if (query.res == "cssLoader") {
+ let bgURL = thisSite + resource + "?redir=other&res=image&id=" + query.id;
+ response.setHeader("Content-Type", "text/css", false);
+ response.write("body { background:url('" + bgURL + "'); }");
+ return;
+ }
+
+ // script that loads an internal worker that uses importScripts on a redirect
+ // to an external script.
+ if (query.res == "loadWorkerThatMakesRequests") {
+ // this creates a worker (same origin) that imports a redirecting script.
+ let workerURL =
+ thisSite + resource + "?res=makeRequestsWorker&id=" + query.id;
+ response.setHeader("Content-Type", "application/javascript", false);
+ response.write("new Worker('" + workerURL + "');");
+ return;
+ }
+
+ // script that loads an internal worker that uses importScripts on a redirect
+ // to an external script.
+ if (query.res == "loadBlobWorkerThatMakesRequests") {
+ // this creates a worker (same origin) that imports a redirecting script.
+ let workerURL =
+ thisSite + resource + "?res=makeRequestsWorker&id=" + query.id;
+ response.setHeader("Content-Type", "application/javascript", false);
+ response.write(
+ "var x = new XMLHttpRequest(); x.open('GET', '" + workerURL + "'); "
+ );
+ response.write("x.responseType = 'blob'; x.send(); ");
+ response.write(
+ "x.onload = () => { new Worker(URL.createObjectURL(x.response)); };"
+ );
+ return;
+ }
+
+ // source for a worker that simply calls importScripts on a script that
+ // redirects.
+ if (query.res == "makeRequestsWorker") {
+ // this is code for a worker that imports a redirected script.
+ let scriptURL =
+ thisSite +
+ resource +
+ "?redir=other&res=script&id=script-src-redir-" +
+ query.id;
+ let xhrURL =
+ thisSite +
+ resource +
+ "?redir=other&res=xhr-resp&id=xhr-src-redir-" +
+ query.id;
+ let fetchURL =
+ thisSite +
+ resource +
+ "?redir=other&res=xhr-resp&id=fetch-src-redir-" +
+ query.id;
+ response.setHeader("Content-Type", "application/javascript", false);
+ response.write("try { importScripts('" + scriptURL + "'); } catch(ex) {} ");
+ response.write(
+ "var x = new XMLHttpRequest(); x.open('GET', '" + xhrURL + "'); x.send();"
+ );
+ response.write("fetch('" + fetchURL + "');");
+ return;
+ }
+
+ // script that invokes XHR
+ if (query.res == "xhr") {
+ response.setHeader("Content-Type", "application/javascript", false);
+ var resp =
+ 'var x = new XMLHttpRequest();x.open("GET", "' +
+ thisSite +
+ resource +
+ '?redir=other&res=xhr-resp&id=xhr-src-redir", false);\n' +
+ "x.send(null);";
+ response.write(resp);
+ return;
+ }
+
+ // response to XHR
+ if (query.res == "xhr-resp") {
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.setHeader("Content-Type", "text/html", false);
+ response.write("XHR response...");
+ }
+}
diff --git a/dom/security/test/csp/file_report.html b/dom/security/test/csp/file_report.html
new file mode 100644
index 0000000000..fb18af8057
--- /dev/null
+++ b/dom/security/test/csp/file_report.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Bug 1033424 - Test csp-report properties </title>
+ </head>
+ <body>
+ <script type="text/javascript">
+ var foo = "propEscFoo";
+ var bar = "propEscBar";
+ // just verifying that we properly escape newlines and quotes
+ </script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_report_chromescript.js b/dom/security/test/csp/file_report_chromescript.js
new file mode 100644
index 0000000000..8c8ddcf0a0
--- /dev/null
+++ b/dom/security/test/csp/file_report_chromescript.js
@@ -0,0 +1,68 @@
+/* eslint-env mozilla/chrome-script */
+
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+
+// eslint-disable-next-line mozilla/reject-importGlobalProperties
+Cu.importGlobalProperties(["TextDecoder"]);
+
+const reportURI = "http://mochi.test:8888/foo.sjs";
+
+var openingObserver = {
+ observe(subject, topic, data) {
+ // subject should be an nsURI
+ if (subject.QueryInterface == undefined) {
+ return;
+ }
+
+ var message = { report: "", error: false };
+
+ if (topic == "http-on-opening-request") {
+ var asciiSpec = subject.QueryInterface(Ci.nsIHttpChannel).URI.asciiSpec;
+ if (asciiSpec !== reportURI) {
+ return;
+ }
+
+ var reportText = false;
+ try {
+ // Verify that the report was properly formatted.
+ // We'll parse the report text as JSON and verify that the properties
+ // have expected values.
+ var reportText = "{}";
+ var uploadStream = subject.QueryInterface(
+ Ci.nsIUploadChannel
+ ).uploadStream;
+
+ if (uploadStream) {
+ // get the bytes from the request body
+ var binstream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ );
+ binstream.setInputStream(uploadStream);
+
+ let bytes = NetUtil.readInputStream(binstream);
+
+ // rewind stream as we are supposed to - there will be an assertion later if we don't.
+ uploadStream
+ .QueryInterface(Ci.nsISeekableStream)
+ .seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
+
+ let textDecoder = new TextDecoder();
+ reportText = textDecoder.decode(bytes);
+ }
+
+ message.report = reportText;
+ } catch (e) {
+ message.error = e.toString();
+ }
+
+ sendAsyncMessage("opening-request-completed", message);
+ }
+ },
+};
+
+Services.obs.addObserver(openingObserver, "http-on-opening-request");
+addMessageListener("finish", function () {
+ Services.obs.removeObserver(openingObserver, "http-on-opening-request");
+});
diff --git a/dom/security/test/csp/file_report_font_cache-1.html b/dom/security/test/csp/file_report_font_cache-1.html
new file mode 100644
index 0000000000..59b4908f83
--- /dev/null
+++ b/dom/security/test/csp/file_report_font_cache-1.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<style>
+@font-face {
+ font-family: "CSP Report Test Font 1";
+ src: url(Ahem.ttf?report_font_cache-1);
+}
+@font-face {
+ font-family: "CSP Report Test Font 2";
+ src: url(Ahem.ttf?report_font_cache-2);
+}
+@font-face {
+ font-family: "CSP Report Test Font 3";
+ src: url(Ahem.ttf?report_font_cache-3);
+}
+.x { font: 24px "CSP Report Test Font 1"; }
+.y { font: 24px "CSP Report Test Font 2"; }
+.z { font: 24px "CSP Report Test Font 3"; }
+</style>
+<p class=x>A</p>
+<p class=y>A</p>
+<p class=z>A</p>
+<script>
+// Wait until the fonts would have been added to the user font cache.
+document.body.offsetWidth;
+document.fonts.ready.then(() => window.parent.postMessage("first-doc-ready", "*"));
+</script>
diff --git a/dom/security/test/csp/file_report_font_cache-2.html b/dom/security/test/csp/file_report_font_cache-2.html
new file mode 100644
index 0000000000..cea9cea663
--- /dev/null
+++ b/dom/security/test/csp/file_report_font_cache-2.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<style>
+@font-face {
+ font-family: "CSP Report Test Font 1";
+ src: url(Ahem.ttf?report_font_cache-1);
+}
+@font-face {
+ font-family: "CSP Report Test Font 3";
+ src: url(Ahem.ttf?report_font_cache-3);
+}
+p { margin-right: 1ex; } /* cause cached CSP check to happen OMT (due to
+ font metrics lookup) */
+.x { font: 24px "CSP Report Test Font 1"; }
+.y { font: 24px "CSP Report Test Font 3"; }
+</style>
+<p class="x">A</p>
+<script>
+// First flush should dispatch the "Test Font 1" report that is stored
+// in the user font cache.
+document.body.offsetWidth;
+
+// Second flush should dispatch "Test Font 3" report.
+document.querySelector("p").className = "y";
+document.body.offsetWidth;
+</script>
diff --git a/dom/security/test/csp/file_report_font_cache-2.html^headers^ b/dom/security/test/csp/file_report_font_cache-2.html^headers^
new file mode 100644
index 0000000000..493f850baa
--- /dev/null
+++ b/dom/security/test/csp/file_report_font_cache-2.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: font-src 'none'; report-uri http://mochi.test:8888/foo.sjs
diff --git a/dom/security/test/csp/file_report_for_import.css b/dom/security/test/csp/file_report_for_import.css
new file mode 100644
index 0000000000..b578b77b33
--- /dev/null
+++ b/dom/security/test/csp/file_report_for_import.css
@@ -0,0 +1 @@
+@import url("http://example.com/tests/dom/security/test/csp/file_report_for_import_server.sjs?stylesheet");
diff --git a/dom/security/test/csp/file_report_for_import.html b/dom/security/test/csp/file_report_for_import.html
new file mode 100644
index 0000000000..77a36faea1
--- /dev/null
+++ b/dom/security/test/csp/file_report_for_import.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Bug 1048048 - Test sending csp-report when using import in css</title>
+ <link rel="stylesheet" type="text/css" href="file_report_for_import.css">
+ </head>
+ <body>
+ empty body, just testing @import in the included css for bug 1048048
+</body>
+</html>
diff --git a/dom/security/test/csp/file_report_for_import_server.sjs b/dom/security/test/csp/file_report_for_import_server.sjs
new file mode 100644
index 0000000000..624c7e657b
--- /dev/null
+++ b/dom/security/test/csp/file_report_for_import_server.sjs
@@ -0,0 +1,50 @@
+// Custom *.sjs file specifically for the needs of Bug:
+// Bug 1048048 - CSP violation report not sent for @import
+
+const CC = Components.Constructor;
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+ var queryString = request.queryString;
+
+ // (1) lets process the queryresult request async and
+ // wait till we have received the image request.
+ if (queryString === "queryresult") {
+ response.processAsync();
+ setObjectState("queryResult", response);
+ return;
+ }
+
+ // (2) handle the csp-report and return the JSON back to
+ // the testfile using the afore stored xml request in (1).
+ if (queryString === "report") {
+ getObjectState("queryResult", function (queryResponse) {
+ if (!queryResponse) {
+ return;
+ }
+
+ // send the report back to the XML request for verification
+ var report = new BinaryInputStream(request.bodyInputStream);
+ var avail;
+ var bytes = [];
+ while ((avail = report.available()) > 0) {
+ Array.prototype.push.apply(bytes, report.readByteArray(avail));
+ }
+ var data = String.fromCharCode.apply(null, bytes);
+ queryResponse.bodyOutputStream.write(data, data.length);
+ queryResponse.finish();
+ });
+ return;
+ }
+
+ // we should not get here ever, but just in case return
+ // something unexpected.
+ response.write("doh!");
+}
diff --git a/dom/security/test/csp/file_report_uri_missing_in_report_only_header.html b/dom/security/test/csp/file_report_uri_missing_in_report_only_header.html
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/dom/security/test/csp/file_report_uri_missing_in_report_only_header.html
diff --git a/dom/security/test/csp/file_report_uri_missing_in_report_only_header.html^headers^ b/dom/security/test/csp/file_report_uri_missing_in_report_only_header.html^headers^
new file mode 100644
index 0000000000..3f2fdfe9e6
--- /dev/null
+++ b/dom/security/test/csp/file_report_uri_missing_in_report_only_header.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy-Report-Only: default-src 'self';
diff --git a/dom/security/test/csp/file_ro_ignore_xfo.html b/dom/security/test/csp/file_ro_ignore_xfo.html
new file mode 100644
index 0000000000..85e7f0092c
--- /dev/null
+++ b/dom/security/test/csp/file_ro_ignore_xfo.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1024557: Ignore x-frame-options if CSP with frame-ancestors exists</title>
+</head>
+<body>
+<div id="cspmessage">Ignoring XFO because of CSP_RO</div>
+</body>
+</html> \ No newline at end of file
diff --git a/dom/security/test/csp/file_ro_ignore_xfo.html^headers^ b/dom/security/test/csp/file_ro_ignore_xfo.html^headers^
new file mode 100644
index 0000000000..ab8366f061
--- /dev/null
+++ b/dom/security/test/csp/file_ro_ignore_xfo.html^headers^
@@ -0,0 +1,3 @@
+Content-Security-Policy-Report-Only: frame-ancestors http://mochi.test:8888
+X-Frame-Options: deny
+Cache-Control: no-cache
diff --git a/dom/security/test/csp/file_sandbox_1.html b/dom/security/test/csp/file_sandbox_1.html
new file mode 100644
index 0000000000..ce1e80c865
--- /dev/null
+++ b/dom/security/test/csp/file_sandbox_1.html
@@ -0,0 +1,16 @@
+<html>
+<head> <meta charset="utf-8"> </head>
+ <body>
+ <!-- sandbox="allow-same-origin" -->
+ <!-- Content-Security-Policy: default-src 'self' -->
+
+ <!-- these should be stopped by CSP -->
+ <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img1_bad&type=img/png"> </img>
+
+ <!-- these should load ok -->
+ <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img1a_good&type=img/png" />
+ <!-- should not execute script -->
+ <script src='/tests/dom/security/test/csp/file_sandbox_fail.js'></script>
+
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_sandbox_10.html b/dom/security/test/csp/file_sandbox_10.html
new file mode 100644
index 0000000000..f934497eee
--- /dev/null
+++ b/dom/security/test/csp/file_sandbox_10.html
@@ -0,0 +1,12 @@
+<html>
+<head> <meta charset="utf-8"> </head>
+ <body>
+ <!-- Content-Security-Policy: default-src 'none'; sandbox -->
+
+ <!-- these should be stopped by CSP -->
+ <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img10_bad&type=img/png"> </img>
+ <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img10a_bad&type=img/png" />
+ <script src='/tests/dom/security/test/csp/file_sandbox_fail.js'></script>
+
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_sandbox_11.html b/dom/security/test/csp/file_sandbox_11.html
new file mode 100644
index 0000000000..087b5651a9
--- /dev/null
+++ b/dom/security/test/csp/file_sandbox_11.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head> <meta charset="utf-8"> </head>
+<script type="text/javascript">
+ function ok(result, desc) {
+ window.parent.postMessage({ok: result, desc}, "*");
+ }
+
+ function doStuff() {
+ ok(true, "documents sandboxed with allow-scripts should be able to run inline scripts");
+ }
+</script>
+<script src='file_sandbox_fail.js'></script>
+<body onLoad='ok(true, "documents sandboxed with allow-scripts should be able to run script from event listeners");doStuff();'>
+ I am sandboxed but with only inline "allow-scripts"
+
+ <!-- Content-Security-Policy: default-src 'none'; script-src 'unsafe-inline'; sandbox allow-scripts -->
+
+ <!-- these should be stopped by CSP -->
+ <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img11_bad&type=img/png" />
+ <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img11a_bad&type=img/png"> </img>
+ <script src='/tests/dom/security/test/csp/file_CSP.sjs?testid=script11_bad&type=text/javascript'></script>
+ <script src='http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=script11a_bad&type=text/javascript'></script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_sandbox_12.html b/dom/security/test/csp/file_sandbox_12.html
new file mode 100644
index 0000000000..79631bd394
--- /dev/null
+++ b/dom/security/test/csp/file_sandbox_12.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<script type="text/javascript">
+ function ok(result, desc) {
+ window.parent.postMessage({ok: result, desc}, "*");
+ }
+
+ function doStuff() {
+ ok(true, "documents sandboxed with allow-scripts should be able to run inline scripts");
+
+ document.getElementById('a_form').submit();
+
+ // trigger the javascript: url test
+ sendMouseEvent({type:'click'}, 'a_link');
+ }
+</script>
+<script src='file_sandbox_pass.js'></script>
+<body onLoad='ok(true, "documents sandboxed with allow-scripts should be able to run script from event listeners");doStuff();'>
+ I am sandboxed but with "allow-same-origin" and allow-scripts"
+
+
+ <!-- Content-Security-Policy: sandbox allow-same-origin allow-scripts; default-src 'self' 'unsafe-inline'; -->
+
+ <!-- these should be stopped by CSP -->
+ <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img12_bad&type=img/png"> </img>
+ <script src='http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=script12_bad&type=text/javascript'></script>
+
+ <form method="get" action="/tests/content/html/content/test/file_iframe_sandbox_form_fail.html" id="a_form">
+ First name: <input type="text" name="firstname">
+ Last name: <input type="text" name="lastname">
+ <input type="submit" onclick="doSubmit()" id="a_button">
+ </form>
+
+ <a href = 'javascript:ok(true, "documents sandboxed with allow-scripts should be able to run script from javascript: URLs");' id='a_link'>click me</a>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_sandbox_13.html b/dom/security/test/csp/file_sandbox_13.html
new file mode 100644
index 0000000000..96286db8d5
--- /dev/null
+++ b/dom/security/test/csp/file_sandbox_13.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head> <meta charset="utf-8"> </head>
+<script type="text/javascript">
+ function ok(result, desc) {
+ window.parent.postMessage({ok: result, desc}, "*");
+ }
+
+ function doStuff() {
+ ok(true, "documents sandboxed with allow-scripts should be able to run inline scripts");
+ }
+</script>
+<script src='file_sandbox_fail.js'></script>
+<body onLoad='ok(true, "documents sandboxed with allow-scripts should be able to run script from event listeners");doStuff();'>
+ I am sandboxed but with only inline "allow-scripts"
+
+ <!-- Content-Security-Policy: default-src 'none'; script-src 'unsafe-inline'; sandbox allow-scripts -->
+
+ <!-- these should be stopped by CSP -->
+ <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img13_bad&type=img/png" />
+ <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img13a_bad&type=img/png"> </img>
+ <script src='/tests/dom/security/test/csp/file_CSP.sjs?testid=script13_bad&type=text/javascript'></script>
+ <script src='http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=script13a_bad&type=text/javascript'></script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_sandbox_2.html b/dom/security/test/csp/file_sandbox_2.html
new file mode 100644
index 0000000000..b37aa1bcef
--- /dev/null
+++ b/dom/security/test/csp/file_sandbox_2.html
@@ -0,0 +1,16 @@
+<html>
+<head> <meta charset="utf-8"> </head>
+ <body>
+ <!-- sandbox -->
+ <!-- Content-Security-Policy: default-src 'self' -->
+
+ <!-- these should be stopped by CSP -->
+ <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img2_bad&type=img/png"> </img>
+
+ <!-- these should load ok -->
+ <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img2a_good&type=img/png" />
+ <!-- should not execute script -->
+ <script src='/tests/dom/security/test/csp/file_sandbox_fail.js'></script>
+
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_sandbox_3.html b/dom/security/test/csp/file_sandbox_3.html
new file mode 100644
index 0000000000..ba808e47d5
--- /dev/null
+++ b/dom/security/test/csp/file_sandbox_3.html
@@ -0,0 +1,13 @@
+<html>
+<head> <meta charset="utf-8"> </head>
+ <body>
+ <!-- sandbox="allow-same-origin" -->
+ <!-- Content-Security-Policy: default-src 'none' -->
+
+ <!-- these should be stopped by CSP -->
+ <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img3_bad&type=img/png"> </img>
+ <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img3a_bad&type=img/png" />
+ <script src='/tests/dom/security/test/csp/file_sandbox_fail.js'></script>
+
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_sandbox_4.html b/dom/security/test/csp/file_sandbox_4.html
new file mode 100644
index 0000000000..b2d4ed0940
--- /dev/null
+++ b/dom/security/test/csp/file_sandbox_4.html
@@ -0,0 +1,13 @@
+<html>
+<head> <meta charset="utf-8"> </head>
+ <body>
+ <!-- sandbox -->
+ <!-- Content-Security-Policy: default-src 'none' -->
+
+ <!-- these should be stopped by CSP -->
+ <img src="http://example.org/tests/dom/base/test/csp/file_CSP.sjs?testid=img4_bad&type=img/png"> </img>
+ <img src="/tests/dom/base/test/csp/file_CSP.sjs?testid=img4a_bad&type=img/png" />
+ <script src='/tests/dom/base/test/csp/file_sandbox_fail.js'></script>
+
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_sandbox_5.html b/dom/security/test/csp/file_sandbox_5.html
new file mode 100644
index 0000000000..c08849b689
--- /dev/null
+++ b/dom/security/test/csp/file_sandbox_5.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head> <meta charset="utf-8"> </head>
+<script type="text/javascript">
+ function ok(result, desc) {
+ window.parent.postMessage({ok: result, desc}, "*");
+ }
+
+ function doStuff() {
+ ok(true, "documents sandboxed with allow-scripts should be able to run inline scripts");
+ }
+</script>
+<script src='file_sandbox_fail.js'></script>
+<body onLoad='ok(true, "documents sandboxed with allow-scripts should be able to run script from event listeners");doStuff();'>
+ I am sandboxed but with only inline "allow-scripts"
+
+ <!-- sandbox="allow-scripts" -->
+ <!-- Content-Security-Policy: default-src 'none'; script-src 'unsafe-inline' -->
+
+ <!-- these should be stopped by CSP -->
+ <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img5_bad&type=img/png" />
+ <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img5a_bad&type=img/png"> </img>
+ <script src='/tests/dom/security/test/csp/file_CSP.sjs?testid=script5_bad&type=text/javascript'></script>
+ <script src='http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=script5a_bad&type=text/javascript'></script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_sandbox_6.html b/dom/security/test/csp/file_sandbox_6.html
new file mode 100644
index 0000000000..44705f4d2b
--- /dev/null
+++ b/dom/security/test/csp/file_sandbox_6.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<script type="text/javascript">
+ function ok(result, desc) {
+ window.parent.postMessage({ok: result, desc}, "*");
+ }
+
+ function doStuff() {
+ ok(true, "documents sandboxed with allow-scripts should be able to run inline scripts");
+
+ document.getElementById('a_form').submit();
+
+ // trigger the javascript: url test
+ sendMouseEvent({type:'click'}, 'a_link');
+ }
+</script>
+<script src='file_sandbox_pass.js'></script>
+<body onLoad='ok(true, "documents sandboxed with allow-scripts should be able to run script from event listeners");doStuff();'>
+ I am sandboxed but with "allow-same-origin" and allow-scripts"
+ <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img6_bad&type=img/png"> </img>
+ <script src='http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=script6_bad&type=text/javascript'></script>
+
+ <form method="get" action="/tests/content/html/content/test/file_iframe_sandbox_form_fail.html" id="a_form">
+ First name: <input type="text" name="firstname">
+ Last name: <input type="text" name="lastname">
+ <input type="submit" onclick="doSubmit()" id="a_button">
+ </form>
+
+ <a href = 'javascript:ok(true, "documents sandboxed with allow-scripts should be able to run script from javascript: URLs");' id='a_link'>click me</a>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_sandbox_7.html b/dom/security/test/csp/file_sandbox_7.html
new file mode 100644
index 0000000000..3b249d4101
--- /dev/null
+++ b/dom/security/test/csp/file_sandbox_7.html
@@ -0,0 +1,15 @@
+<html>
+<head> <meta charset="utf-8"> </head>
+ <body>
+ <!-- Content-Security-Policy: default-src 'self'; sandbox allow-same-origin -->
+
+ <!-- these should be stopped by CSP -->
+ <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img7_bad&type=img/png"> </img>
+
+ <!-- these should load ok -->
+ <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img7a_good&type=img/png" />
+ <!-- should not execute script -->
+ <script src='/tests/dom/security/test/csp/file_sandbox_fail.js'></script>
+
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_sandbox_8.html b/dom/security/test/csp/file_sandbox_8.html
new file mode 100644
index 0000000000..4f9cd89161
--- /dev/null
+++ b/dom/security/test/csp/file_sandbox_8.html
@@ -0,0 +1,15 @@
+<html>
+<head> <meta charset="utf-8"> </head>
+ <body>
+ <!-- Content-Security-Policy: sandbox; default-src 'self' -->
+
+ <!-- these should be stopped by CSP -->
+ <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img8_bad&type=img/png"> </img>
+
+ <!-- these should load ok -->
+ <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img8a_good&type=img/png" />
+ <!-- should not execute script -->
+ <script src='/tests/dom/security/test/csp/file_sandbox_fail.js'></script>
+
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_sandbox_9.html b/dom/security/test/csp/file_sandbox_9.html
new file mode 100644
index 0000000000..29ffc191cd
--- /dev/null
+++ b/dom/security/test/csp/file_sandbox_9.html
@@ -0,0 +1,12 @@
+<html>
+<head> <meta charset="utf-8"> </head>
+ <body>
+ <!-- Content-Security-Policy: default-src 'none'; sandbox allow-same-origin -->
+
+ <!-- these should be stopped by CSP -->
+ <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img9_bad&type=img/png"> </img>
+ <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img9a_bad&type=img/png" />
+
+ <script src='/tests/dom/security/test/csp/file_sandbox_fail.js'></script>
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_sandbox_allow_scripts.html b/dom/security/test/csp/file_sandbox_allow_scripts.html
new file mode 100644
index 0000000000..faab9f0fc6
--- /dev/null
+++ b/dom/security/test/csp/file_sandbox_allow_scripts.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <title>Bug 1396320: Fix CSP sandbox regression for allow-scripts</title>
+ </head>
+<body>
+<script type='application/javascript'>
+ window.parent.postMessage({result: document.domain }, '*');
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_sandbox_allow_scripts.html^headers^ b/dom/security/test/csp/file_sandbox_allow_scripts.html^headers^
new file mode 100644
index 0000000000..4705ce9ded
--- /dev/null
+++ b/dom/security/test/csp/file_sandbox_allow_scripts.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: sandbox allow-scripts;
diff --git a/dom/security/test/csp/file_sandbox_fail.js b/dom/security/test/csp/file_sandbox_fail.js
new file mode 100644
index 0000000000..7f43927ddf
--- /dev/null
+++ b/dom/security/test/csp/file_sandbox_fail.js
@@ -0,0 +1,7 @@
+function ok(result, desc) {
+ window.parent.postMessage({ ok: result, desc }, "*");
+}
+ok(
+ false,
+ "documents sandboxed with allow-scripts should NOT be able to run <script src=...>"
+);
diff --git a/dom/security/test/csp/file_sandbox_pass.js b/dom/security/test/csp/file_sandbox_pass.js
new file mode 100644
index 0000000000..c06341a082
--- /dev/null
+++ b/dom/security/test/csp/file_sandbox_pass.js
@@ -0,0 +1,7 @@
+function ok(result, desc) {
+ window.parent.postMessage({ ok: result, desc }, "*");
+}
+ok(
+ true,
+ "documents sandboxed with allow-scripts should be able to run <script src=...>"
+);
diff --git a/dom/security/test/csp/file_scheme_relative_sources.js b/dom/security/test/csp/file_scheme_relative_sources.js
new file mode 100644
index 0000000000..09286d42e9
--- /dev/null
+++ b/dom/security/test/csp/file_scheme_relative_sources.js
@@ -0,0 +1 @@
+document.getElementById("testdiv").innerHTML = "allowed";
diff --git a/dom/security/test/csp/file_scheme_relative_sources.sjs b/dom/security/test/csp/file_scheme_relative_sources.sjs
new file mode 100644
index 0000000000..ec2c17bece
--- /dev/null
+++ b/dom/security/test/csp/file_scheme_relative_sources.sjs
@@ -0,0 +1,44 @@
+/**
+ * Custom *.sjs specifically for the needs of
+ * Bug 921493 - CSP: test allowlisting of scheme-relative sources
+ */
+
+function handleRequest(request, response) {
+ let query = new URLSearchParams(request.queryString);
+
+ let scheme = query.get("scheme");
+ let policy = query.get("policy");
+
+ let linkUrl =
+ scheme +
+ "://example.com/tests/dom/security/test/csp/file_scheme_relative_sources.js";
+
+ let html =
+ "<!DOCTYPE HTML>" +
+ "<html>" +
+ "<head>" +
+ "<title>test schemeless sources within CSP</title>" +
+ "</head>" +
+ "<body> " +
+ "<div id='testdiv'>blocked</div>" +
+ // try to load a scheme relative script
+ "<script src='" +
+ linkUrl +
+ "'></script>" +
+ // have an inline script that reports back to the parent whether
+ // the script got loaded or not from within the sandboxed iframe.
+ "<script type='application/javascript'>" +
+ "window.onload = function() {" +
+ "var inner = document.getElementById('testdiv').innerHTML;" +
+ "window.parent.postMessage({ result: inner }, '*');" +
+ "}" +
+ "</script>" +
+ "</body>" +
+ "</html>";
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Content-Security-Policy", policy, false);
+
+ response.write(html);
+}
diff --git a/dom/security/test/csp/file_script_template.html b/dom/security/test/csp/file_script_template.html
new file mode 100644
index 0000000000..3819592912
--- /dev/null
+++ b/dom/security/test/csp/file_script_template.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline'">
+<template id="a">
+ <script src="file_script_template.js"></script>
+</template>
+</head>
+<body>
+<script>
+ var temp = document.getElementsByTagName("template")[0];
+ var clon = temp.content.cloneNode(true);
+ document.body.appendChild(clon);
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_script_template.js b/dom/security/test/csp/file_script_template.js
new file mode 100644
index 0000000000..d75869f763
--- /dev/null
+++ b/dom/security/test/csp/file_script_template.js
@@ -0,0 +1 @@
+// dummy *.js file
diff --git a/dom/security/test/csp/file_self_none_as_hostname_confusion.html b/dom/security/test/csp/file_self_none_as_hostname_confusion.html
new file mode 100644
index 0000000000..16196bb19f
--- /dev/null
+++ b/dom/security/test/csp/file_self_none_as_hostname_confusion.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Bug 587377 - CSP keywords "'self'" and "'none'" are easy to confuse with host names "self" and "none"</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/dom/security/test/csp/file_self_none_as_hostname_confusion.html^headers^ b/dom/security/test/csp/file_self_none_as_hostname_confusion.html^headers^
new file mode 100644
index 0000000000..26af7ed9b5
--- /dev/null
+++ b/dom/security/test/csp/file_self_none_as_hostname_confusion.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src 'self' SELF;
diff --git a/dom/security/test/csp/file_sendbeacon.html b/dom/security/test/csp/file_sendbeacon.html
new file mode 100644
index 0000000000..13202c65ff
--- /dev/null
+++ b/dom/security/test/csp/file_sendbeacon.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Security-Policy" content= "connect-src 'none'">
+ <title>Bug 1234813 - sendBeacon should not throw if blocked by Content Policy</title>
+</head>
+<body>
+
+<script type="application/javascript">
+try {
+ navigator.sendBeacon("http://example.com/sendbeaconintonirvana");
+ window.parent.postMessage({result: "blocked-beacon-does-not-throw"}, "*");
+ }
+ catch (e) {
+ window.parent.postMessage({result: "blocked-beacon-throws"}, "*");
+ }
+</script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_service_worker.html b/dom/security/test/csp/file_service_worker.html
new file mode 100644
index 0000000000..b819946983
--- /dev/null
+++ b/dom/security/test/csp/file_service_worker.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1208559 - ServiceWorker registration not governed by CSP</title>
+</head>
+<body>
+<script>
+ function finish(status) {
+ window.parent.postMessage({result: status}, "*");
+ }
+
+ const promises = [
+ navigator.serviceWorker.ready,
+ navigator.serviceWorker.register("file_service_worker.js", { scope: "." })
+ ];
+
+ Promise.race(promises).then(finish.bind(null, 'allowed'),
+ finish.bind(null, 'blocked'));
+ </script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_service_worker.js b/dom/security/test/csp/file_service_worker.js
new file mode 100644
index 0000000000..1bf583f4cc
--- /dev/null
+++ b/dom/security/test/csp/file_service_worker.js
@@ -0,0 +1 @@
+dump("service workers: hello world");
diff --git a/dom/security/test/csp/file_spawn_service_worker.js b/dom/security/test/csp/file_spawn_service_worker.js
new file mode 100644
index 0000000000..b262fa10a3
--- /dev/null
+++ b/dom/security/test/csp/file_spawn_service_worker.js
@@ -0,0 +1 @@
+// dummy file
diff --git a/dom/security/test/csp/file_spawn_shared_worker.js b/dom/security/test/csp/file_spawn_shared_worker.js
new file mode 100644
index 0000000000..e4f53b9ce1
--- /dev/null
+++ b/dom/security/test/csp/file_spawn_shared_worker.js
@@ -0,0 +1,7 @@
+onconnect = function (e) {
+ var port = e.ports[0];
+ port.addEventListener("message", function (e) {
+ port.postMessage("shared worker is executing");
+ });
+ port.start();
+};
diff --git a/dom/security/test/csp/file_spawn_worker.js b/dom/security/test/csp/file_spawn_worker.js
new file mode 100644
index 0000000000..acde7408c1
--- /dev/null
+++ b/dom/security/test/csp/file_spawn_worker.js
@@ -0,0 +1 @@
+postMessage("worker is executing");
diff --git a/dom/security/test/csp/file_strict_dynamic.js b/dom/security/test/csp/file_strict_dynamic.js
new file mode 100644
index 0000000000..09286d42e9
--- /dev/null
+++ b/dom/security/test/csp/file_strict_dynamic.js
@@ -0,0 +1 @@
+document.getElementById("testdiv").innerHTML = "allowed";
diff --git a/dom/security/test/csp/file_strict_dynamic_default_src.html b/dom/security/test/csp/file_strict_dynamic_default_src.html
new file mode 100644
index 0000000000..0ea79e2a96
--- /dev/null
+++ b/dom/security/test/csp/file_strict_dynamic_default_src.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1299483 - CSP: Implement 'strict-dynamic'</title>
+</head>
+<body>
+
+<div id="testdiv">blocked</div>
+<script nonce="foo" src="http://mochi.test:8888/tests/dom/security/test/csp/file_strict_dynamic_default_src.js"></script>
+
+<img id="testimage" src="http://mochi.test:8888/tests/image/test/mochitest/blue.png" data-result="blocked">
+<script>
+let img = document.getElementById("testimage");
+img.addEventListener("load", function() {
+ img.dataset.result = "allowed";
+}, { once: true });
+</script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_strict_dynamic_default_src.js b/dom/security/test/csp/file_strict_dynamic_default_src.js
new file mode 100644
index 0000000000..09286d42e9
--- /dev/null
+++ b/dom/security/test/csp/file_strict_dynamic_default_src.js
@@ -0,0 +1 @@
+document.getElementById("testdiv").innerHTML = "allowed";
diff --git a/dom/security/test/csp/file_strict_dynamic_js_url.html b/dom/security/test/csp/file_strict_dynamic_js_url.html
new file mode 100644
index 0000000000..bd53b0adb2
--- /dev/null
+++ b/dom/security/test/csp/file_strict_dynamic_js_url.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1316826 - 'strict-dynamic' blocking DOM event handlers</title>
+</head>
+<body>
+<div id="testdiv">blocked</div>
+
+<a id="jslink" href='javascript:document.getElementById("testdiv").innerHTML = "allowed"'>click me</a>
+<script nonce="foo">
+ document.getElementById("jslink").click();
+</script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_strict_dynamic_non_parser_inserted.html b/dom/security/test/csp/file_strict_dynamic_non_parser_inserted.html
new file mode 100644
index 0000000000..c51fefd72e
--- /dev/null
+++ b/dom/security/test/csp/file_strict_dynamic_non_parser_inserted.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1299483 - CSP: Implement 'strict-dynamic'</title>
+</head>
+<body>
+<div id="testdiv">blocked</div>
+
+<script nonce="foo">
+ // generates a *non* parser inserted script and should be allowed
+ var myScript = document.createElement('script');
+ myScript.src = 'http://example.com/tests/dom/security/test/csp/file_strict_dynamic.js';
+ document.head.appendChild(myScript);
+</script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_strict_dynamic_non_parser_inserted_inline.html b/dom/security/test/csp/file_strict_dynamic_non_parser_inserted_inline.html
new file mode 100644
index 0000000000..10a0f32e4b
--- /dev/null
+++ b/dom/security/test/csp/file_strict_dynamic_non_parser_inserted_inline.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1299483 - CSP: Implement 'strict-dynamic'</title>
+</head>
+<body>
+<div id="testdiv">blocked</div>
+
+<script nonce="foo">
+ var dynamicScript = document.createElement('script');
+ dynamicScript.textContent = 'document.getElementById("testdiv").textContent="allowed"';
+ document.head.appendChild(dynamicScript);
+</script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_strict_dynamic_parser_inserted_doc_write.html b/dom/security/test/csp/file_strict_dynamic_parser_inserted_doc_write.html
new file mode 100644
index 0000000000..2a3a7d4998
--- /dev/null
+++ b/dom/security/test/csp/file_strict_dynamic_parser_inserted_doc_write.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1299483 - CSP: Implement 'strict-dynamic'</title>
+</head>
+<body>
+<div id="testdiv">blocked</div>
+
+<script nonce="foo">
+ // generates a parser inserted script and should be blocked
+ document.write("<script src='http://example.com/tests/dom/security/test/csp/file_strict_dynamic.js'><\/script>");
+</script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_strict_dynamic_parser_inserted_doc_write_correct_nonce.html b/dom/security/test/csp/file_strict_dynamic_parser_inserted_doc_write_correct_nonce.html
new file mode 100644
index 0000000000..9938ef2dcd
--- /dev/null
+++ b/dom/security/test/csp/file_strict_dynamic_parser_inserted_doc_write_correct_nonce.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1299483 - CSP: Implement 'strict-dynamic'</title>
+</head>
+<body>
+<div id="testdiv">blocked</div>
+
+<script nonce="foo">
+ // generates a parser inserted script with a valid nonce- and should be allowed
+ document.write("<script nonce='foo' src='http://example.com/tests/dom/security/test/csp/file_strict_dynamic.js'><\/script>");
+</script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_strict_dynamic_script_events.html b/dom/security/test/csp/file_strict_dynamic_script_events.html
new file mode 100644
index 0000000000..0889583821
--- /dev/null
+++ b/dom/security/test/csp/file_strict_dynamic_script_events.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1316826 - 'strict-dynamic' blocking DOM event handlers</title>
+</head>
+<body>
+<div id="testdiv">blocked</div>
+
+ <img src='/nonexisting.jpg'
+ onerror='document.getElementById("testdiv").innerHTML = "allowed";'
+ style='display:none'>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_strict_dynamic_script_events_marquee.html b/dom/security/test/csp/file_strict_dynamic_script_events_marquee.html
new file mode 100644
index 0000000000..701ef32269
--- /dev/null
+++ b/dom/security/test/csp/file_strict_dynamic_script_events_marquee.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1316826 - 'strict-dynamic' blocking DOM event handlers</title>
+</head>
+<body>
+<div id="testdiv">blocked</div>
+
+<marquee onstart='document.getElementById("testdiv").innerHTML = "allowed";'>
+ Bug 1316826
+</marquee>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_strict_dynamic_script_extern.html b/dom/security/test/csp/file_strict_dynamic_script_extern.html
new file mode 100644
index 0000000000..94b6aefb19
--- /dev/null
+++ b/dom/security/test/csp/file_strict_dynamic_script_extern.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1299483 - CSP: Implement 'strict-dynamic'</title>
+</head>
+<body>
+<div id="testdiv">blocked</div>
+<script nonce="foo" src="http://example.com/tests/dom/security/test/csp/file_strict_dynamic.js"></script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_strict_dynamic_script_inline.html b/dom/security/test/csp/file_strict_dynamic_script_inline.html
new file mode 100644
index 0000000000..d17a58f279
--- /dev/null
+++ b/dom/security/test/csp/file_strict_dynamic_script_inline.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1299483 - CSP: Implement 'strict-dynamic'</title>
+</head>
+<body>
+<div id="testdiv">blocked</div>
+
+<script nonce="foo">
+ document.getElementById("testdiv").innerHTML = "allowed";
+</script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_strict_dynamic_unsafe_eval.html b/dom/security/test/csp/file_strict_dynamic_unsafe_eval.html
new file mode 100644
index 0000000000..4f54015aa8
--- /dev/null
+++ b/dom/security/test/csp/file_strict_dynamic_unsafe_eval.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1299483 - CSP: Implement 'strict-dynamic'</title>
+</head>
+<body>
+<div id="testdiv">blocked</div>
+
+<script nonce="foo">
+ // eslint-disable-next-line no-eval
+ eval('document.getElementById("testdiv").innerHTML = "allowed";');
+</script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_subframe_run_js_if_allowed.html b/dom/security/test/csp/file_subframe_run_js_if_allowed.html
new file mode 100644
index 0000000000..3ba970ce84
--- /dev/null
+++ b/dom/security/test/csp/file_subframe_run_js_if_allowed.html
@@ -0,0 +1,13 @@
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=702439
+
+This document is a child frame of a CSP document and the
+test verifies that it is permitted to run javascript: URLs
+if the parent has a policy that allows them.
+-->
+<body onload="document.getElementById('a').click()">
+<a id="a" href="javascript:parent.javascript_link_ran = true;
+ parent.checkResult();">click</a>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_subframe_run_js_if_allowed.html^headers^ b/dom/security/test/csp/file_subframe_run_js_if_allowed.html^headers^
new file mode 100644
index 0000000000..233b359310
--- /dev/null
+++ b/dom/security/test/csp/file_subframe_run_js_if_allowed.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src *; script-src 'unsafe-inline'
diff --git a/dom/security/test/csp/file_svg_inline_style_base.html b/dom/security/test/csp/file_svg_inline_style_base.html
new file mode 100644
index 0000000000..4d7ce0cd6e
--- /dev/null
+++ b/dom/security/test/csp/file_svg_inline_style_base.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+ <img src="file_svg_inline_style_server.sjs?svg_inline_style_nocsp&1">
+</body>
+</html>
diff --git a/dom/security/test/csp/file_svg_inline_style_csp.html b/dom/security/test/csp/file_svg_inline_style_csp.html
new file mode 100644
index 0000000000..040ee02e19
--- /dev/null
+++ b/dom/security/test/csp/file_svg_inline_style_csp.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'">
+</head>
+<body>
+ <img src="file_svg_inline_style_server.sjs?svg_inline_style_csp&2">
+</body>
+</html>
diff --git a/dom/security/test/csp/file_svg_inline_style_server.sjs b/dom/security/test/csp/file_svg_inline_style_server.sjs
new file mode 100644
index 0000000000..6073f36f62
--- /dev/null
+++ b/dom/security/test/csp/file_svg_inline_style_server.sjs
@@ -0,0 +1,43 @@
+"use strict";
+
+const SVG_IMG = `<svg width="200" height="200" viewBox="0 0 150 150" xmlns="http://www.w3.org/2000/svg">
+ <style>
+ circle {
+ fill: orange;
+ stroke: black;
+ stroke-width: 10px;
+ }
+ </style>
+ <circle cx="50" cy="50" r="40" />
+ </svg>`;
+
+const SVG_IMG_NO_INLINE_STYLE = `<svg width="200" height="200" viewBox="0 0 150 150" xmlns="http://www.w3.org/2000/svg">
+ <circle cx="50" cy="50" r="40" />
+ </svg>`;
+
+function handleRequest(request, response) {
+ const query = request.queryString;
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "image/svg+xml", false);
+
+ if (query.includes("svg_inline_style_csp")) {
+ response.setHeader("Content-Security-Policy", "default-src 'none'", false);
+ response.write(SVG_IMG);
+ return;
+ }
+
+ if (query.includes("svg_inline_style_nocsp")) {
+ response.write(SVG_IMG);
+ return;
+ }
+
+ if (query.includes("svg_no_inline_style")) {
+ response.write(SVG_IMG_NO_INLINE_STYLE);
+ return;
+ }
+
+ // we should never get here, but just in case
+ // return something unexpected
+ response.write("do'h");
+}
diff --git a/dom/security/test/csp/file_svg_srcset_inline_style_base.html b/dom/security/test/csp/file_svg_srcset_inline_style_base.html
new file mode 100644
index 0000000000..1754c557f0
--- /dev/null
+++ b/dom/security/test/csp/file_svg_srcset_inline_style_base.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+ <img srcset="file_svg_inline_style_server.sjs?svg_inline_style_nocsp&3">
+</body>
+</html>
diff --git a/dom/security/test/csp/file_svg_srcset_inline_style_csp.html b/dom/security/test/csp/file_svg_srcset_inline_style_csp.html
new file mode 100644
index 0000000000..418d714882
--- /dev/null
+++ b/dom/security/test/csp/file_svg_srcset_inline_style_csp.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'">
+</head>
+<body>
+ <img srcset="file_svg_inline_style_server.sjs?svg_inline_style_csp&4">
+</body>
+</html>
diff --git a/dom/security/test/csp/file_test_browser_bookmarklets.html b/dom/security/test/csp/file_test_browser_bookmarklets.html
new file mode 100644
index 0000000000..cb12e4efd0
--- /dev/null
+++ b/dom/security/test/csp/file_test_browser_bookmarklets.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+</head>
+<body>
+ <h1>Test-Document</h1>
+</body>
+</html> \ No newline at end of file
diff --git a/dom/security/test/csp/file_test_browser_bookmarklets.html^headers^ b/dom/security/test/csp/file_test_browser_bookmarklets.html^headers^
new file mode 100644
index 0000000000..e138f234fb
--- /dev/null
+++ b/dom/security/test/csp/file_test_browser_bookmarklets.html^headers^
@@ -0,0 +1,2 @@
+Content-Security-Policy: script-src 'none'
+Cache-Control: no-cache
diff --git a/dom/security/test/csp/file_testserver.sjs b/dom/security/test/csp/file_testserver.sjs
new file mode 100644
index 0000000000..77f9562218
--- /dev/null
+++ b/dom/security/test/csp/file_testserver.sjs
@@ -0,0 +1,66 @@
+// SJS file for CSP mochitests
+"use strict";
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+
+function loadHTMLFromFile(path) {
+ // Load the HTML to return in the response from file.
+ // Since it's relative to the cwd of the test runner, we start there and
+ // append to get to the actual path of the file.
+ const testHTMLFile = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+
+ const testHTMLFileStream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+
+ path
+ .split("/")
+ .filter(path => path)
+ .reduce((file, path) => {
+ testHTMLFile.append(path);
+ return testHTMLFile;
+ }, testHTMLFile);
+ testHTMLFileStream.init(testHTMLFile, -1, 0, 0);
+ const isAvailable = testHTMLFileStream.available();
+ return NetUtil.readInputStreamToString(testHTMLFileStream, isAvailable);
+}
+
+function handleRequest(request, response) {
+ const query = new URLSearchParams(request.queryString);
+
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ // Deliver the CSP policy encoded in the URL
+ if (query.has("csp")) {
+ response.setHeader("Content-Security-Policy", query.get("csp"), false);
+ }
+
+ // Deliver the CSP report-only policy encoded in the URI
+ if (query.has("cspRO")) {
+ response.setHeader(
+ "Content-Security-Policy-Report-Only",
+ query.get("cspRO"),
+ false
+ );
+ }
+
+ // Deliver the CORS header in the URL
+ if (query.has("cors")) {
+ response.setHeader("Access-Control-Allow-Origin", query.get("cors"), false);
+ }
+
+ // Send HTML to test allowed/blocked behaviors
+ let type = "text/html";
+ if (query.has("type")) {
+ type = query.get("type");
+ }
+
+ response.setHeader("Content-Type", type, false);
+ if (query.has("file")) {
+ response.write(loadHTMLFromFile(query.get("file")));
+ }
+}
diff --git a/dom/security/test/csp/file_uir_top_nav.html b/dom/security/test/csp/file_uir_top_nav.html
new file mode 100644
index 0000000000..28263e9db7
--- /dev/null
+++ b/dom/security/test/csp/file_uir_top_nav.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+
+// 1) same origin navigation
+window.open("http://example.com/tests/dom/security/test/csp/file_uir_top_nav_dummy.html");
+
+// 2) same origin navigation
+window.open("http://test1.example.com/tests/dom/security/test/csp/file_uir_top_nav_dummy.html");
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_uir_top_nav_dummy.html b/dom/security/test/csp/file_uir_top_nav_dummy.html
new file mode 100644
index 0000000000..65762f1c71
--- /dev/null
+++ b/dom/security/test/csp/file_uir_top_nav_dummy.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+just a dummy page to check uir applies to top level navigations
+<script class="testbody" type="text/javascript">
+window.onload = function() {
+ window.opener.parent.postMessage({result: window.location.href}, "*");
+ window.close();
+}
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_upgrade_insecure.html b/dom/security/test/csp/file_upgrade_insecure.html
new file mode 100644
index 0000000000..14aad3ebd6
--- /dev/null
+++ b/dom/security/test/csp/file_upgrade_insecure.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1139297 - Implement CSP upgrade-insecure-requests directive</title>
+ <!-- style -->
+ <link rel='stylesheet' type='text/css' href='http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?style' media='screen' />
+
+ <!-- font -->
+ <style>
+ @font-face {
+ font-family: "foofont";
+ src: url('http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?font');
+ }
+ .div_foo { font-family: "foofont"; }
+ </style>
+</head>
+<body>
+
+ <!-- images: -->
+ <img src="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?img"></img>
+
+ <!-- redirects: upgrade http:// to https:// redirect to http:// and then upgrade to https:// again -->
+ <img src="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?redirect-image"></img>
+
+ <!-- script: -->
+ <script src="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?script"></script>
+
+ <!-- media: -->
+ <audio src="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?media"></audio>
+
+ <!-- objects: -->
+ <object width="10" height="10" data="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?object"></object>
+
+ <!-- font: (apply font loaded in header to div) -->
+ <div class="div_foo">foo</div>
+
+ <!-- iframe: (same origin) -->
+ <iframe src="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?iframe">
+ <!-- within that iframe we load an image over http and make sure the requested gets upgraded to https -->
+ </iframe>
+
+ <!-- xhr: -->
+ <script type="application/javascript">
+ var myXHR = new XMLHttpRequest();
+ myXHR.open("GET", "http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?xhr");
+ myXHR.send(null);
+ </script>
+
+ <!-- websockets: upgrade ws:// to wss://-->
+ <script type="application/javascript">
+ // WebSocket tests are not supported on Android yet. Bug 1566168
+ const { AppConstants } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+ );
+ if (AppConstants.platform !== "android") {
+ var mySocket = new WebSocket("ws://example.com/tests/dom/security/test/csp/file_upgrade_insecure");
+ mySocket.onopen = function(e) {
+ if (mySocket.url.includes("wss://")) {
+ window.parent.postMessage({result: "websocket-ok"}, "*");
+ }
+ else {
+ window.parent.postMessage({result: "websocket-error"}, "*");
+ }
+ mySocket.close();
+ };
+ mySocket.onerror = function(e) {
+ // debug information for Bug 1316305
+ dump(" xxx mySocket.onerror: (mySocket): " + mySocket + "\n");
+ dump(" xxx mySocket.onerror: (mySocket.url): " + mySocket.url + "\n");
+ dump(" xxx mySocket.onerror: (e): " + e + "\n");
+ dump(" xxx mySocket.onerror: (e.message): " + e.message + "\n");
+ window.parent.postMessage({result: "websocket-unexpected-error"}, "*");
+ };
+ }
+ </script>
+
+ <!-- form action: (upgrade POST from http:// to https://) -->
+ <iframe name='formFrame' id='formFrame'></iframe>
+ <form target="formFrame" action="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?form" method="POST">
+ <input name="foo" value="foo">
+ <input type="submit" id="submitButton" formenctype='multipart/form-data' value="Submit form">
+ </form>
+ <script type="text/javascript">
+ var submitButton = document.getElementById('submitButton');
+ submitButton.click();
+ </script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_upgrade_insecure_cors.html b/dom/security/test/csp/file_upgrade_insecure_cors.html
new file mode 100644
index 0000000000..e675c62e9f
--- /dev/null
+++ b/dom/security/test/csp/file_upgrade_insecure_cors.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1139297 - Implement CSP upgrade-insecure-requests directive</title>
+</head>
+<body>
+
+<script type="text/javascript">
+ // === TEST 1
+ var url1 = "http://test1.example.com/tests/dom/security/test/csp/file_upgrade_insecure_cors_server.sjs?test1";
+ var xhr1 = new XMLHttpRequest();
+ xhr1.open("GET", url1, true);
+ xhr1.onload = function() {
+ window.parent.postMessage(xhr1.response, "*");
+ };
+ xhr1.onerror = function() {
+ window.parent.postMessage("test1-failed", "*");
+ };
+ xhr1.send();
+
+ // === TEST 2
+ var url2 = "http://test1.example.com/tests/dom/security/test/csp/file_upgrade_insecure_cors_server.sjs?test2";
+ var xhr2 = new XMLHttpRequest();
+ xhr2.open("GET", url2, true);
+ xhr2.onload = function() {
+ window.parent.postMessage(xhr2.response, "*");
+ };
+ xhr2.onerror = function() {
+ window.parent.postMessage("test2-failed", "*");
+ };
+ xhr2.send();
+
+ // === TEST 3
+ var url3 = "http://test2.example.com/tests/dom/security/test/csp/file_upgrade_insecure_cors_server.sjs?test3";
+ var xhr3 = new XMLHttpRequest();
+ xhr3.open("GET", url3, true);
+ xhr3.onload = function() {
+ window.parent.postMessage(xhr3.response, "*");
+ };
+ xhr3.onerror = function() {
+ window.parent.postMessage("test3-failed", "*");
+ };
+ xhr3.send();
+
+</script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_upgrade_insecure_cors_server.sjs b/dom/security/test/csp/file_upgrade_insecure_cors_server.sjs
new file mode 100644
index 0000000000..83957560c3
--- /dev/null
+++ b/dom/security/test/csp/file_upgrade_insecure_cors_server.sjs
@@ -0,0 +1,61 @@
+// Custom *.sjs file specifically for the needs of Bug:
+// Bug 1139297 - Implement CSP upgrade-insecure-requests directive
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ // perform sanity check and make sure that all requests get upgraded to use https
+ if (request.scheme !== "https") {
+ response.write("request not https");
+ return;
+ }
+
+ var queryString = request.queryString;
+
+ // TEST 1
+ if (queryString === "test1") {
+ var newLocation =
+ "http://test1.example.com/tests/dom/security/test/csp/file_upgrade_insecure_cors_server.sjs?redir-test1";
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", newLocation, false);
+ return;
+ }
+ if (queryString === "redir-test1") {
+ response.write("test1-no-cors-ok");
+ return;
+ }
+
+ // TEST 2
+ if (queryString === "test2") {
+ var newLocation =
+ "http://test1.example.com:443/tests/dom/security/test/csp/file_upgrade_insecure_cors_server.sjs?redir-test2";
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", newLocation, false);
+ return;
+ }
+ if (queryString === "redir-test2") {
+ response.write("test2-no-cors-diffport-ok");
+ return;
+ }
+
+ // TEST 3
+ response.setHeader("Access-Control-Allow-Headers", "content-type", false);
+ response.setHeader("Access-Control-Allow-Methods", "POST, GET", false);
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+
+ if (queryString === "test3") {
+ var newLocation =
+ "http://test1.example.com/tests/dom/security/test/csp/file_upgrade_insecure_cors_server.sjs?redir-test3";
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", newLocation, false);
+ return;
+ }
+ if (queryString === "redir-test3") {
+ response.write("test3-cors-ok");
+ return;
+ }
+
+ // we should not get here, but just in case return something unexpected
+ response.write("d'oh");
+}
diff --git a/dom/security/test/csp/file_upgrade_insecure_docwrite_iframe.sjs b/dom/security/test/csp/file_upgrade_insecure_docwrite_iframe.sjs
new file mode 100644
index 0000000000..a7fb0a2176
--- /dev/null
+++ b/dom/security/test/csp/file_upgrade_insecure_docwrite_iframe.sjs
@@ -0,0 +1,55 @@
+// custom *.sjs for Bug 1273430
+// META CSP: upgrade-insecure-requests
+
+// important: the IFRAME_URL is *http* and needs to be upgraded to *https* by upgrade-insecure-requests
+const IFRAME_URL =
+ "http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_docwrite_iframe.sjs?docwriteframe";
+
+const TEST_FRAME =
+ `
+ <!DOCTYPE HTML>
+ <html><head><meta charset="utf-8">
+ <title>TEST_FRAME</title>
+ <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
+ </head>
+ <body>
+ <script type="text/javascript">
+ document.write('<iframe src="` +
+ IFRAME_URL +
+ `"/>');
+ </script>
+ </body>
+ </html>`;
+
+// doc.write(iframe) sends a post message to the parent indicating the current
+// location so the parent can make sure the request was upgraded to *https*.
+const DOC_WRITE_FRAME = `
+ <!DOCTYPE HTML>
+ <html><head><meta charset="utf-8">
+ <title>DOC_WRITE_FRAME</title>
+ </head>
+ <body onload="window.parent.parent.postMessage({result: document.location.href}, '*');">
+ </body>
+ </html>`;
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+
+ var queryString = request.queryString;
+
+ if (queryString === "testframe") {
+ response.write(TEST_FRAME);
+ return;
+ }
+
+ if (queryString === "docwriteframe") {
+ response.write(DOC_WRITE_FRAME);
+ return;
+ }
+
+ // we should never get here, but just in case
+ // return something unexpected
+ response.write("do'h");
+}
diff --git a/dom/security/test/csp/file_upgrade_insecure_loopback.html b/dom/security/test/csp/file_upgrade_insecure_loopback.html
new file mode 100644
index 0000000000..b824604b6e
--- /dev/null
+++ b/dom/security/test/csp/file_upgrade_insecure_loopback.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1447784 - Implement CSP upgrade-insecure-requests directive</title>
+</head>
+<body>
+
+<script type="text/javascript">
+ // === TEST 1
+ var url1 = "http://127.0.0.1:8888/tests/dom/security/test/csp/file_upgrade_insecure_loopback_server.sjs?test1";
+ var xhr1 = new XMLHttpRequest();
+ xhr1.open("GET", url1, true);
+ xhr1.onload = function() {
+ window.parent.postMessage(xhr1.response, "*");
+ };
+ xhr1.onerror = function() {
+ window.parent.postMessage("test1-failed", "*");
+ };
+ xhr1.send();
+
+</script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_upgrade_insecure_loopback_form.html b/dom/security/test/csp/file_upgrade_insecure_loopback_form.html
new file mode 100644
index 0000000000..ed6b3b8542
--- /dev/null
+++ b/dom/security/test/csp/file_upgrade_insecure_loopback_form.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1661423 - don't apply upgrade-insecure-requests on form submissions to localhost</title>
+</head>
+<body>
+
+<form id="form" action="http://127.0.0.1/bug-1661423-dont-upgrade-localhost">
+ <input type="submit">
+</form>>
+<script type="text/javascript">
+
+ form.submit();
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_upgrade_insecure_loopback_server.sjs b/dom/security/test/csp/file_upgrade_insecure_loopback_server.sjs
new file mode 100644
index 0000000000..ff7931a1d4
--- /dev/null
+++ b/dom/security/test/csp/file_upgrade_insecure_loopback_server.sjs
@@ -0,0 +1,22 @@
+// Custom *.sjs file specifically for the needs of Bug:
+// Bug 1447784 - Implement CSP upgrade-insecure-requests directive
+
+function handleRequest(request, response) {
+ response.setHeader("Access-Control-Allow-Headers", "content-type", false);
+ response.setHeader("Access-Control-Allow-Methods", "GET", false);
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ // perform sanity check and make sure that all requests get upgraded to use https
+ if (request.scheme !== "https") {
+ response.write("request-not-https");
+ return;
+ } else {
+ response.write("request-is-https");
+ }
+
+ // we should not get here, but just in case return something unexpected
+ response.write("d'oh");
+}
diff --git a/dom/security/test/csp/file_upgrade_insecure_meta.html b/dom/security/test/csp/file_upgrade_insecure_meta.html
new file mode 100644
index 0000000000..3509e5c6fd
--- /dev/null
+++ b/dom/security/test/csp/file_upgrade_insecure_meta.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests; default-src https: wss: 'unsafe-inline'; form-action https:;">
+ <meta charset="utf-8">
+ <title>Bug 1139297 - Implement CSP upgrade-insecure-requests directive</title>
+ <!-- style -->
+ <link rel='stylesheet' type='text/css' href='http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?style' media='screen' />
+
+ <!-- font -->
+ <style>
+ @font-face {
+ font-family: "foofont";
+ src: url('http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?font');
+ }
+ .div_foo { font-family: "foofont"; }
+ </style>
+</head>
+<body>
+
+ <!-- images: -->
+ <img src="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?img"></img>
+
+ <!-- redirects: upgrade http:// to https:// redirect to http:// and then upgrade to https:// again -->
+ <img src="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?redirect-image"></img>
+
+ <!-- script: -->
+ <script src="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?script"></script>
+
+ <!-- media: -->
+ <audio src="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?media"></audio>
+
+ <!-- objects: -->
+ <object width="10" height="10" data="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?object"></object>
+
+ <!-- font: (apply font loaded in header to div) -->
+ <div class="div_foo">foo</div>
+
+ <!-- iframe: (same origin) -->
+ <iframe src="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?iframe">
+ <!-- within that iframe we load an image over http and make sure the requested gets upgraded to https -->
+ </iframe>
+
+ <!-- xhr: -->
+ <script type="application/javascript">
+ var myXHR = new XMLHttpRequest();
+ myXHR.open("GET", "http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?xhr");
+ myXHR.send(null);
+ </script>
+
+ <!-- websockets: upgrade ws:// to wss://-->
+ <script type="application/javascript">
+ // WebSocket tests are not supported on Android Yet. Bug 1566168.
+ const { AppConstants } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+ );
+ if (AppConstants.platform !== "android") {
+ var mySocket = new WebSocket("ws://example.com/tests/dom/security/test/csp/file_upgrade_insecure");
+ mySocket.onopen = function(e) {
+ if (mySocket.url.includes("wss://")) {
+ window.parent.postMessage({result: "websocket-ok"}, "*");
+ }
+ else {
+ window.parent.postMessage({result: "websocket-error"}, "*");
+ }
+ mySocket.close();
+ };
+ mySocket.onerror = function(e) {
+ window.parent.postMessage({result: "websocket-unexpected-error"}, "*");
+ };
+ }
+ </script>
+
+ <!-- form action: (upgrade POST from http:// to https://) -->
+ <iframe name='formFrame' id='formFrame'></iframe>
+ <form target="formFrame" action="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?form" method="POST">
+ <input name="foo" value="foo">
+ <input type="submit" id="submitButton" formenctype='multipart/form-data' value="Submit form">
+ </form>
+ <script type="text/javascript">
+ var submitButton = document.getElementById('submitButton');
+ submitButton.click();
+ </script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_upgrade_insecure_navigation.sjs b/dom/security/test/csp/file_upgrade_insecure_navigation.sjs
new file mode 100644
index 0000000000..46473db0ac
--- /dev/null
+++ b/dom/security/test/csp/file_upgrade_insecure_navigation.sjs
@@ -0,0 +1,78 @@
+// Custom *.sjs file specifically for the needs of
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1271173
+
+"use strict";
+
+const TEST_NAVIGATIONAL_UPGRADE = `
+ <!DOCTYPE html>
+ <html>
+ <head><meta charset="utf-8"></head>
+ <body>
+ <a href="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_navigation.sjs?action=framenav" id="testlink">clickme</a>
+ <script type="text/javascript">
+ // before navigating the current frame we open the window and check that uir applies
+ var myWin = window.open("http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_navigation.sjs?action=docnav");
+
+ window.addEventListener("message", receiveMessage, false);
+ function receiveMessage(event) {
+ myWin.close();
+ var link = document.getElementById('testlink');
+ link.click();
+ }
+ </script>
+ </body>
+ </html>`;
+
+const FRAME_NAV = `
+ <!DOCTYPE html>
+ <html>
+ <head><meta charset="utf-8"></head>
+ <body>
+ <script type="text/javascript">
+ parent.postMessage({result: document.documentURI}, "*");
+ </script>
+ </body>
+ </html>`;
+
+const DOC_NAV = `
+ <!DOCTYPE html>
+ <html>
+ <head><meta charset="utf-8"></head>
+ <body>
+ <script type="text/javascript">
+ // call back to the main testpage signaling whether the upgraded succeeded
+ window.opener.parent.postMessage({result: document.documentURI}, "*");
+ // let the opener (iframe) now that we can now close the window and move on with the test.
+ window.opener.postMessage({result: "readyToMoveOn"}, "*");
+ </script>
+ </body>
+ </html>`;
+
+function handleRequest(request, response) {
+ const query = new URLSearchParams(request.queryString);
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+ if (query.get("csp")) {
+ response.setHeader("Content-Security-Policy", query.get("csp"), false);
+ }
+
+ if (query.get("action") === "perform_navigation") {
+ response.write(TEST_NAVIGATIONAL_UPGRADE);
+ return;
+ }
+
+ if (query.get("action") === "framenav") {
+ response.write(FRAME_NAV);
+ return;
+ }
+
+ if (query.get("action") === "docnav") {
+ response.write(DOC_NAV);
+ return;
+ }
+
+ // we should never get here, but just in case
+ // return something unexpected
+ response.write("do'h");
+}
diff --git a/dom/security/test/csp/file_upgrade_insecure_navigation_redirect.sjs b/dom/security/test/csp/file_upgrade_insecure_navigation_redirect.sjs
new file mode 100644
index 0000000000..3f7f8158e0
--- /dev/null
+++ b/dom/security/test/csp/file_upgrade_insecure_navigation_redirect.sjs
@@ -0,0 +1,50 @@
+"use strict";
+
+const FINAL_DOCUMENT = `
+ <html>
+ <body>
+ final document
+ <script>
+ window.onload = function() {
+ let docURI = document.documentURI;
+ window.opener.parent.postMessage({docURI}, "*");
+ window.close();
+ }
+ </script>
+ </body>
+ </html>`;
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+
+ const query = request.queryString;
+
+ if (query === "same_origin_redirect") {
+ let newLocation =
+ "http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_navigation_redirect.sjs?finaldoc_same_origin_redirect";
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", newLocation, false);
+ return;
+ }
+
+ if (query === "cross_origin_redirect") {
+ let newLocation =
+ "http://test1.example.com/tests/dom/security/test/csp/file_upgrade_insecure_navigation_redirect.sjs?finaldoc_cross_origin_redirect";
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", newLocation, false);
+ return;
+ }
+
+ if (
+ query === "finaldoc_same_origin_redirect" ||
+ query === "finaldoc_cross_origin_redirect"
+ ) {
+ response.write(FINAL_DOCUMENT);
+ return;
+ }
+
+ // we should never get here, but just in case
+ // return something unexpected
+ response.write("do'h");
+}
diff --git a/dom/security/test/csp/file_upgrade_insecure_navigation_redirect_cross_origin.html b/dom/security/test/csp/file_upgrade_insecure_navigation_redirect_cross_origin.html
new file mode 100644
index 0000000000..dff2c9faf3
--- /dev/null
+++ b/dom/security/test/csp/file_upgrade_insecure_navigation_redirect_cross_origin.html
@@ -0,0 +1,10 @@
+<html>
+<head>
+ <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" />
+</head>
+<body>
+<script>
+ window.open("http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_navigation_redirect.sjs?cross_origin_redirect");
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_upgrade_insecure_navigation_redirect_same_origin.html b/dom/security/test/csp/file_upgrade_insecure_navigation_redirect_same_origin.html
new file mode 100644
index 0000000000..811850e08c
--- /dev/null
+++ b/dom/security/test/csp/file_upgrade_insecure_navigation_redirect_same_origin.html
@@ -0,0 +1,10 @@
+<html>
+<head>
+ <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" />
+</head>
+<body>
+<script>
+ window.open("http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_navigation_redirect.sjs?same_origin_redirect");
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_upgrade_insecure_report_only.html b/dom/security/test/csp/file_upgrade_insecure_report_only.html
new file mode 100644
index 0000000000..0c3c36493d
--- /dev/null
+++ b/dom/security/test/csp/file_upgrade_insecure_report_only.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1832249 - Consider report-only flag when upgrading insecure requests</title>
+</head>
+<body>
+ <img id="testimage"></img>
+
+ <script>
+ let route;
+ if (new URL(document.location).searchParams.get("reportonly")) {
+ route = "reportonly";
+ }
+ else if (new URL(document.location).searchParams.get("enforce")) {
+ route = "enforce";
+ }
+ var myImg = document.getElementById("testimage");
+ // we need to test http functionality here, so we need to load an http url
+ /* eslint-disable @microsoft/sdl/no-insecure-url */
+ myImg.src =
+ `http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_report_only_server.sjs?img-${route}`;
+ /* eslint-enable @microsoft/sdl/no-insecure-url */
+ myImg.onload = function(e) {
+ window.parent.postMessage({result: `${route}-img-ok`}, "*");
+ };
+ myImg.onerror = function(e) {
+ window.parent.postMessage({result: `${route}-img-error`}, "*");
+ };
+ </script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_upgrade_insecure_report_only_server.sjs b/dom/security/test/csp/file_upgrade_insecure_report_only_server.sjs
new file mode 100644
index 0000000000..412d9b352e
--- /dev/null
+++ b/dom/security/test/csp/file_upgrade_insecure_report_only_server.sjs
@@ -0,0 +1,114 @@
+// Custom *.sjs specifically for the needs of Bug 1832249 - Consider report-only flag when upgrading insecure requests
+
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+
+// small red image
+const IMG_BYTES = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
+ "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="
+);
+
+const POLICY_CSP =
+ "upgrade-insecure-requests; default-src https: 'unsafe-inline'";
+const POLICY_CSP_RO =
+ "default-src https: 'unsafe-inline'; upgrade-insecure-requests; report-uri https://example.com/tests/dom/security/test/csp/file_upgrade_insecure_report_only_server.sjs?report";
+
+function loadHTMLFromFile(path) {
+ // Load the HTML to return in the response from file.
+ // Since it's relative to the cwd of the test runner, we start there and
+ // append to get to the actual path of the file.
+ var testHTMLFile = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+ var dirs = path.split("/");
+ for (var i = 0; i < dirs.length; i++) {
+ testHTMLFile.append(dirs[i]);
+ }
+ var testHTMLFileStream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ testHTMLFileStream.init(testHTMLFile, -1, 0, 0);
+ var testHTML = NetUtil.readInputStreamToString(
+ testHTMLFileStream,
+ testHTMLFileStream.available()
+ );
+ return testHTML;
+}
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ // (1) Store the query that will report back whether the violation report was
+ // received
+ if (request.queryString.startsWith("queryresult-")) {
+ let route = request.queryString.substring("queryresult-".length);
+ response.processAsync();
+ setObjectState(`queryResult-${route}`, response);
+ return;
+ }
+
+ // (2) We load a page using a report only CSP
+ if (request.queryString.endsWith("=true")) {
+ let route = request.queryString.split("=")[0];
+ if (route === "enforce") {
+ response.setHeader("Content-Security-Policy", POLICY_CSP, false);
+ }
+ response.setHeader(
+ "Content-Security-Policy-Report-Only",
+ POLICY_CSP_RO + "-" + route,
+ false
+ );
+ response.setHeader("Content-Type", "text/html", false);
+ response.write(
+ loadHTMLFromFile(
+ "tests/dom/security/test/csp/file_upgrade_insecure_report_only.html"
+ )
+ );
+ return;
+ }
+
+ // (3a) Return the image back to the client if http and refuse if https for
+ // report only image
+ if (request.queryString.startsWith("img-")) {
+ let route = request.queryString.substring("img-".length);
+ if (
+ (request.scheme === "http" && route === "reportonly") ||
+ (request.scheme === "https" && route === "enforce")
+ ) {
+ response.setHeader("Content-Type", "image/png");
+ response.write(IMG_BYTES);
+ return;
+ } else {
+ response.setStatusLine(metadata.httpVersion, 404, "NO");
+ response.write("Aww, 404, I wanted a peanut.");
+ return;
+ }
+ }
+
+ // (4) Once we receive the report send it to the client via the saved
+ // queryresult response object
+ if (request.queryString.startsWith("report-")) {
+ let route = request.queryString.substring("report-".length);
+ getObjectState(`queryResult-${route}`, function (queryResponse) {
+ if (!queryResponse) {
+ return;
+ }
+ queryResponse.setHeader("Content-Type", "application/json");
+ queryResponse.write(
+ NetUtil.readInputStreamToString(
+ request.bodyInputStream,
+ request.bodyInputStream.available()
+ )
+ );
+ queryResponse.finish();
+ });
+ return;
+ }
+
+ // we should never get here, but just in case ...
+ response.setHeader("Content-Type", "text/plain");
+ response.write("doh!");
+}
diff --git a/dom/security/test/csp/file_upgrade_insecure_reporting.html b/dom/security/test/csp/file_upgrade_insecure_reporting.html
new file mode 100644
index 0000000000..c78e9a784d
--- /dev/null
+++ b/dom/security/test/csp/file_upgrade_insecure_reporting.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1139297 - Implement CSP upgrade-insecure-requests directive</title>
+</head>
+<body>
+
+ <!-- upgrade img from http:// to https:// -->
+ <img id="testimage" src="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_reporting_server.sjs?img"></img>
+
+ <script type="application/javascript">
+ var myImg = document.getElementById("testimage");
+ myImg.onload = function(e) {
+ window.parent.postMessage({result: "img-ok"}, "*");
+ };
+ myImg.onerror = function(e) {
+ window.parent.postMessage({result: "img-error"}, "*");
+ };
+ </script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_upgrade_insecure_reporting_server.sjs b/dom/security/test/csp/file_upgrade_insecure_reporting_server.sjs
new file mode 100644
index 0000000000..0d17288802
--- /dev/null
+++ b/dom/security/test/csp/file_upgrade_insecure_reporting_server.sjs
@@ -0,0 +1,89 @@
+// Custom *.sjs specifically for the needs of Bug
+// Bug 1139297 - Implement CSP upgrade-insecure-requests directive
+
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+
+// small red image
+const IMG_BYTES = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
+ "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="
+);
+
+const REPORT_URI =
+ "https://example.com/tests/dom/security/test/csp/file_upgrade_insecure_reporting_server.sjs?report";
+const POLICY = "upgrade-insecure-requests; default-src https: 'unsafe-inline'";
+const POLICY_RO =
+ "default-src https: 'unsafe-inline'; report-uri " + REPORT_URI;
+
+function loadHTMLFromFile(path) {
+ // Load the HTML to return in the response from file.
+ // Since it's relative to the cwd of the test runner, we start there and
+ // append to get to the actual path of the file.
+ var testHTMLFile = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+ var dirs = path.split("/");
+ for (var i = 0; i < dirs.length; i++) {
+ testHTMLFile.append(dirs[i]);
+ }
+ var testHTMLFileStream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ testHTMLFileStream.init(testHTMLFile, -1, 0, 0);
+ var testHTML = NetUtil.readInputStreamToString(
+ testHTMLFileStream,
+ testHTMLFileStream.available()
+ );
+ return testHTML;
+}
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ // (1) Store the query that will report back whether the violation report was received
+ if (request.queryString == "queryresult") {
+ response.processAsync();
+ setObjectState("queryResult", response);
+ return;
+ }
+
+ // (2) We load a page using a CSP and a report only CSP
+ if (request.queryString == "toplevel") {
+ response.setHeader("Content-Security-Policy", POLICY, false);
+ response.setHeader("Content-Security-Policy-Report-Only", POLICY_RO, false);
+ response.setHeader("Content-Type", "text/html", false);
+ response.write(
+ loadHTMLFromFile(
+ "tests/dom/security/test/csp/file_upgrade_insecure_reporting.html"
+ )
+ );
+ return;
+ }
+
+ // (3) Return the image back to the client
+ if (request.queryString == "img") {
+ response.setHeader("Content-Type", "image/png");
+ response.write(IMG_BYTES);
+ return;
+ }
+
+ // (4) Finally we receive the report, let's return the request from (1)
+ // signaling that we received the report correctly
+ if (request.queryString == "report") {
+ getObjectState("queryResult", function (queryResponse) {
+ if (!queryResponse) {
+ return;
+ }
+ queryResponse.write("report-ok");
+ queryResponse.finish();
+ });
+ return;
+ }
+
+ // we should never get here, but just in case ...
+ response.setHeader("Content-Type", "text/plain");
+ response.write("doh!");
+}
diff --git a/dom/security/test/csp/file_upgrade_insecure_server.sjs b/dom/security/test/csp/file_upgrade_insecure_server.sjs
new file mode 100644
index 0000000000..05d027c078
--- /dev/null
+++ b/dom/security/test/csp/file_upgrade_insecure_server.sjs
@@ -0,0 +1,112 @@
+// Custom *.sjs file specifically for the needs of Bug:
+// Bug 1139297 - Implement CSP upgrade-insecure-requests directive
+
+const TOTAL_EXPECTED_REQUESTS = 11;
+
+const IFRAME_CONTENT =
+ "<!DOCTYPE HTML>" +
+ "<html>" +
+ "<head><meta charset='utf-8'>" +
+ "<title>Bug 1139297 - Implement CSP upgrade-insecure-requests directive</title>" +
+ "</head>" +
+ "<body>" +
+ "<img src='http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?nested-img'></img>" +
+ "</body>" +
+ "</html>";
+
+const expectedQueries = [
+ "script",
+ "style",
+ "img",
+ "iframe",
+ "form",
+ "xhr",
+ "media",
+ "object",
+ "font",
+ "img-redir",
+ "nested-img",
+];
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+ var queryString = request.queryString;
+
+ // initialize server variables and save the object state
+ // of the initial request, which returns async once the
+ // server has processed all requests.
+ if (queryString == "queryresult") {
+ setState("totaltests", TOTAL_EXPECTED_REQUESTS.toString());
+ setState("receivedQueries", "");
+ response.processAsync();
+ setObjectState("queryResult", response);
+ return;
+ }
+
+ // handle img redirect (https->http)
+ if (queryString == "redirect-image") {
+ var newLocation =
+ "http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?img-redir";
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", newLocation, false);
+ return;
+ }
+
+ // just in case error handling for unexpected queries
+ if (expectedQueries.indexOf(queryString) == -1) {
+ response.write("doh!");
+ return;
+ }
+
+ // make sure all the requested queries are indeed https
+ queryString += request.scheme == "https" ? "-ok" : "-error";
+
+ var receivedQueries = getState("receivedQueries");
+
+ // images, scripts, etc. get queried twice, do not
+ // confuse the server by storing the preload as
+ // well as the actual load. If either the preload
+ // or the actual load is not https, then we would
+ // append "-error" in the array and the test would
+ // fail at the end.
+ if (receivedQueries.includes(queryString)) {
+ return;
+ }
+
+ // append the result to the total query string array
+ if (receivedQueries != "") {
+ receivedQueries += ",";
+ }
+ receivedQueries += queryString;
+ setState("receivedQueries", receivedQueries);
+
+ // keep track of how many more requests the server
+ // is expecting
+ var totaltests = parseInt(getState("totaltests"));
+ totaltests -= 1;
+ setState("totaltests", totaltests.toString());
+
+ // return content (img) for the nested iframe to test
+ // that subresource requests within nested contexts
+ // get upgraded as well. We also have to return
+ // the iframe context in case of an error so we
+ // can test both, using upgrade-insecure as well
+ // as the base case of not using upgrade-insecure.
+ if (queryString == "iframe-ok" || queryString == "iframe-error") {
+ response.write(IFRAME_CONTENT);
+ }
+
+ // if we have received all the requests, we return
+ // the result back.
+ if (totaltests == 0) {
+ getObjectState("queryResult", function (queryResponse) {
+ if (!queryResponse) {
+ return;
+ }
+ var receivedQueries = getState("receivedQueries");
+ queryResponse.write(receivedQueries);
+ queryResponse.finish();
+ });
+ }
+}
diff --git a/dom/security/test/csp/file_upgrade_insecure_wsh.py b/dom/security/test/csp/file_upgrade_insecure_wsh.py
new file mode 100644
index 0000000000..b7159c742b
--- /dev/null
+++ b/dom/security/test/csp/file_upgrade_insecure_wsh.py
@@ -0,0 +1,6 @@
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ pass
diff --git a/dom/security/test/csp/file_web_manifest.html b/dom/security/test/csp/file_web_manifest.html
new file mode 100644
index 0000000000..0f6a67460e
--- /dev/null
+++ b/dom/security/test/csp/file_web_manifest.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<meta charset=utf-8>
+<head>
+<link rel="manifest" href="file_web_manifest.json">
+</head>
+<h1>Support Page for Web Manifest Tests</h1> \ No newline at end of file
diff --git a/dom/security/test/csp/file_web_manifest.json b/dom/security/test/csp/file_web_manifest.json
new file mode 100644
index 0000000000..eb88b50445
--- /dev/null
+++ b/dom/security/test/csp/file_web_manifest.json
@@ -0,0 +1 @@
+{ "name": "loaded" }
diff --git a/dom/security/test/csp/file_web_manifest.json^headers^ b/dom/security/test/csp/file_web_manifest.json^headers^
new file mode 100644
index 0000000000..e0e00c4be0
--- /dev/null
+++ b/dom/security/test/csp/file_web_manifest.json^headers^
@@ -0,0 +1 @@
+Access-Control-Allow-Origin: http://example.org \ No newline at end of file
diff --git a/dom/security/test/csp/file_web_manifest_https.html b/dom/security/test/csp/file_web_manifest_https.html
new file mode 100644
index 0000000000..b0ff9ef853
--- /dev/null
+++ b/dom/security/test/csp/file_web_manifest_https.html
@@ -0,0 +1,4 @@
+<!doctype html>
+<meta charset=utf-8>
+<link rel="manifest" href="https://example.com:443/tests/dom/security/test/csp/file_web_manifest_https.json">
+<h1>Support Page for Web Manifest Tests</h1> \ No newline at end of file
diff --git a/dom/security/test/csp/file_web_manifest_https.json b/dom/security/test/csp/file_web_manifest_https.json
new file mode 100644
index 0000000000..eb88b50445
--- /dev/null
+++ b/dom/security/test/csp/file_web_manifest_https.json
@@ -0,0 +1 @@
+{ "name": "loaded" }
diff --git a/dom/security/test/csp/file_web_manifest_mixed_content.html b/dom/security/test/csp/file_web_manifest_mixed_content.html
new file mode 100644
index 0000000000..55f17c0f92
--- /dev/null
+++ b/dom/security/test/csp/file_web_manifest_mixed_content.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<meta charset=utf-8>
+<head>
+<link
+ rel="manifest"
+ href="http://example.org/tests/dom/security/test/csp/file_testserver.sjs?file=/test/dom/security/test/csp/file_web_manifest.json&amp;cors=*">
+</head>
+<h1>Support Page for Web Manifest Tests</h1>
+<p>Used to try to load a resource over an insecure connection to trigger mixed content blocking.</p> \ No newline at end of file
diff --git a/dom/security/test/csp/file_web_manifest_remote.html b/dom/security/test/csp/file_web_manifest_remote.html
new file mode 100644
index 0000000000..7ecf8eec43
--- /dev/null
+++ b/dom/security/test/csp/file_web_manifest_remote.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<meta charset=utf-8>
+<link rel="manifest"
+ crossorigin
+ href="//mochi.test:8888/tests/dom/security/test/csp/file_testserver.sjs?file=/tests/dom/security/test/csp/file_web_manifest.json&amp;cors=*">
+
+<h1>Support Page for Web Manifest Tests</h1>
+<p>Loads a manifest from mochi.test:8888 with CORS set to "*".</p> \ No newline at end of file
diff --git a/dom/security/test/csp/file_websocket_csp_upgrade.html b/dom/security/test/csp/file_websocket_csp_upgrade.html
new file mode 100644
index 0000000000..9302a6e637
--- /dev/null
+++ b/dom/security/test/csp/file_websocket_csp_upgrade.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1729897: Allow unsecure websocket from localhost page with CSP: upgrade-insecure </title>
+ <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
+</head>
+<body>
+ <script type="application/javascript">
+ /* load socket using ws */
+ var wsSocket = new WebSocket("ws://localhost/tests/dom/security/test/csp/file_websocket_self");
+ wsSocket.onopen = function(e) {
+ window.parent.postMessage({result: "self-ws-loaded", url: wsSocket.url}, "*");
+ };
+ wsSocket.onerror = function(e) {
+ window.parent.postMessage({result: "self-ws-blocked"}, "*");
+ };
+ </script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_websocket_explicit.html b/dom/security/test/csp/file_websocket_explicit.html
new file mode 100644
index 0000000000..51462ab741
--- /dev/null
+++ b/dom/security/test/csp/file_websocket_explicit.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1345615: Allow websocket schemes when using 'self' in CSP</title>
+ <meta http-equiv="Content-Security-Policy" content="connect-src ws:">
+</head>
+<body>
+ <script type="application/javascript">
+ /* load socket using ws */
+ var wsSocket = new WebSocket("ws://example.com/tests/dom/security/test/csp/file_websocket_self");
+ wsSocket.onopen = function(e) {
+ window.parent.postMessage({result: "explicit-ws-loaded"}, "*");
+ wsSocket.close();
+ };
+ wsSocket.onerror = function(e) {
+ window.parent.postMessage({result: "explicit-ws-blocked"}, "*");
+ };
+
+ /* load socket using wss */
+ var wssSocket = new WebSocket("wss://example.com/tests/dom/security/test/csp/file_websocket_self");
+ wssSocket.onopen = function(e) {
+ window.parent.postMessage({result: "explicit-wss-loaded"}, "*");
+ wssSocket.close();
+ };
+ wssSocket.onerror = function(e) {
+ window.parent.postMessage({result: "explicit-wss-blocked"}, "*");
+ };
+ </script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_websocket_self.html b/dom/security/test/csp/file_websocket_self.html
new file mode 100644
index 0000000000..3ff5f05580
--- /dev/null
+++ b/dom/security/test/csp/file_websocket_self.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1345615: Allow websocket schemes when using 'self' in CSP</title>
+ <meta http-equiv="Content-Security-Policy" content="connect-src 'self'">
+</head>
+<body>
+ <script type="application/javascript">
+ /* load socket using ws */
+ var wsSocket = new WebSocket("ws://example.com/tests/dom/security/test/csp/file_websocket_self");
+ wsSocket.onopen = function(e) {
+ window.parent.postMessage({result: "self-ws-loaded"}, "*");
+ wsSocket.close();
+ };
+ wsSocket.onerror = function(e) {
+ window.parent.postMessage({result: "self-ws-blocked"}, "*");
+ };
+
+ /* load socket using wss */
+ var wssSocket = new WebSocket("wss://example.com/tests/dom/security/test/csp/file_websocket_self");
+ wssSocket.onopen = function(e) {
+ window.parent.postMessage({result: "self-wss-loaded"}, "*");
+ wssSocket.close();
+ };
+ wssSocket.onerror = function(e) {
+ window.parent.postMessage({result: "self-wss-blocked"}, "*");
+ };
+ </script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_websocket_self_wsh.py b/dom/security/test/csp/file_websocket_self_wsh.py
new file mode 100644
index 0000000000..eb45e224f3
--- /dev/null
+++ b/dom/security/test/csp/file_websocket_self_wsh.py
@@ -0,0 +1,6 @@
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ pass
diff --git a/dom/security/test/csp/file_win_open_blocked.html b/dom/security/test/csp/file_win_open_blocked.html
new file mode 100644
index 0000000000..2d0828a872
--- /dev/null
+++ b/dom/security/test/csp/file_win_open_blocked.html
@@ -0,0 +1,3 @@
+<script>
+ window.opener.postMessage('window-opened', '*');
+</script>
diff --git a/dom/security/test/csp/file_windowwatcher_frameA.html b/dom/security/test/csp/file_windowwatcher_frameA.html
new file mode 100644
index 0000000000..9e544142ce
--- /dev/null
+++ b/dom/security/test/csp/file_windowwatcher_frameA.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+frame A<br/>
+<iframe name='frameB' src="http://example.com/tests/dom/security/test/csp/file_windowwatcher_subframeB.html"></iframe>
+<iframe name='frameC' src="http://example.com/tests/dom/security/test/csp/file_windowwatcher_subframeC.html"></iframe>
+<iframe name='frameD' src="http://example.com/tests/dom/security/test/csp/file_windowwatcher_subframeD.html"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+window.onload = function() {
+ frameB.openWin();
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_windowwatcher_subframeB.html b/dom/security/test/csp/file_windowwatcher_subframeB.html
new file mode 100644
index 0000000000..e7ef422313
--- /dev/null
+++ b/dom/security/test/csp/file_windowwatcher_subframeB.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+subFrame B
+
+<script>
+function openWin() {
+ parent.frameC.open.call(parent.frameD, "http://example.com/tests/dom/security/test/csp/file_windowwatcher_win_open.html");
+}
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_windowwatcher_subframeC.html b/dom/security/test/csp/file_windowwatcher_subframeC.html
new file mode 100644
index 0000000000..b97c40432e
--- /dev/null
+++ b/dom/security/test/csp/file_windowwatcher_subframeC.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
+</head>
+<body>
+subFrame C
+</body>
+</html>
diff --git a/dom/security/test/csp/file_windowwatcher_subframeD.html b/dom/security/test/csp/file_windowwatcher_subframeD.html
new file mode 100644
index 0000000000..2f778ea4cd
--- /dev/null
+++ b/dom/security/test/csp/file_windowwatcher_subframeD.html
@@ -0,0 +1,6 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+subFrame D
+</body>
+</html>
diff --git a/dom/security/test/csp/file_windowwatcher_win_open.html b/dom/security/test/csp/file_windowwatcher_win_open.html
new file mode 100644
index 0000000000..0237e49377
--- /dev/null
+++ b/dom/security/test/csp/file_windowwatcher_win_open.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+Opened Window<br/>
+
+<script>
+
+window.onload = function() {
+ window.opener.parent.parent.postMessage({result: window.location.href}, "*");
+ window.close();
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_worker_src.js b/dom/security/test/csp/file_worker_src.js
new file mode 100644
index 0000000000..ce60379fef
--- /dev/null
+++ b/dom/security/test/csp/file_worker_src.js
@@ -0,0 +1,73 @@
+var mySharedWorker = new SharedWorker("file_spawn_shared_worker.js");
+mySharedWorker.port.onmessage = function (ev) {
+ parent.postMessage(
+ {
+ result: "shared-worker-allowed",
+ href: document.location.href,
+ },
+ "*"
+ );
+ mySharedWorker.port.close();
+};
+mySharedWorker.onerror = function (evt) {
+ evt.preventDefault();
+ parent.postMessage(
+ {
+ result: "shared-worker-blocked",
+ href: document.location.href,
+ },
+ "*"
+ );
+ mySharedWorker.port.close();
+};
+mySharedWorker.port.start();
+mySharedWorker.port.postMessage("foo");
+
+// --------------------------------------------
+
+let myWorker = new Worker("file_spawn_worker.js");
+myWorker.onmessage = function (event) {
+ parent.postMessage(
+ {
+ result: "worker-allowed",
+ href: document.location.href,
+ },
+ "*"
+ );
+};
+myWorker.onerror = function (event) {
+ parent.postMessage(
+ {
+ result: "worker-blocked",
+ href: document.location.href,
+ },
+ "*"
+ );
+};
+
+// --------------------------------------------
+
+navigator.serviceWorker
+ .register("file_spawn_service_worker.js")
+ .then(function (reg) {
+ // registration worked
+ reg.unregister().then(function () {
+ parent.postMessage(
+ {
+ result: "service-worker-allowed",
+ href: document.location.href,
+ },
+ "*"
+ );
+ });
+ })
+ .catch(function (error) {
+ // registration failed
+ parent.postMessage(
+ {
+ result: "service-worker-blocked",
+ href: document.location.href,
+ },
+ "*"
+ );
+ });
diff --git a/dom/security/test/csp/file_worker_src_child_governs.html b/dom/security/test/csp/file_worker_src_child_governs.html
new file mode 100644
index 0000000000..ca8a683aac
--- /dev/null
+++ b/dom/security/test/csp/file_worker_src_child_governs.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Security-Policy" content="child-src https://example.com; script-src 'nonce-foo'">";
+</head>
+<body>
+<script type="text/javascript" src="file_worker_src.js" nonce="foo"></script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_worker_src_script_governs.html b/dom/security/test/csp/file_worker_src_script_governs.html
new file mode 100644
index 0000000000..0385fee57c
--- /dev/null
+++ b/dom/security/test/csp/file_worker_src_script_governs.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Security-Policy" content="script-src 'nonce-foo' https://example.com">";
+</head>
+<body>
+<script type="text/javascript" src="file_worker_src.js" nonce="foo"></script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_worker_src_worker_governs.html b/dom/security/test/csp/file_worker_src_worker_governs.html
new file mode 100644
index 0000000000..93c8f61225
--- /dev/null
+++ b/dom/security/test/csp/file_worker_src_worker_governs.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Security-Policy" content="worker-src https://example.com; child-src 'none'; script-src 'nonce-foo'">";
+</head>
+<body>
+<script type="text/javascript" src="file_worker_src.js" nonce="foo"></script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_xslt_inherits_csp.xml b/dom/security/test/csp/file_xslt_inherits_csp.xml
new file mode 100644
index 0000000000..a6d99c3081
--- /dev/null
+++ b/dom/security/test/csp/file_xslt_inherits_csp.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet type="text/xsl" href="file_xslt_inherits_csp.xsl"?>
+
+<root>
+ <t>This is some Title</t>
+</root>
diff --git a/dom/security/test/csp/file_xslt_inherits_csp.xml^headers^ b/dom/security/test/csp/file_xslt_inherits_csp.xml^headers^
new file mode 100644
index 0000000000..635af0a4d9
--- /dev/null
+++ b/dom/security/test/csp/file_xslt_inherits_csp.xml^headers^
@@ -0,0 +1,2 @@
+Content-Security-Policy: script-src 'self'
+Cache-Control: no-cache
diff --git a/dom/security/test/csp/file_xslt_inherits_csp.xsl b/dom/security/test/csp/file_xslt_inherits_csp.xsl
new file mode 100644
index 0000000000..82a4b0ad97
--- /dev/null
+++ b/dom/security/test/csp/file_xslt_inherits_csp.xsl
@@ -0,0 +1,26 @@
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.1">
+ <xsl:output method="html"/>
+ <xsl:variable name="title" select="/root/t"/>
+ <xsl:template match="/">
+ <html>
+ <head>
+ <title>
+ <xsl:value-of select="$title"/>
+ </title>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+ </head>
+ <body>
+ <p>
+ Below is some inline JavaScript generating some red text.
+ </p>
+
+ <p id="bug"/>
+ <script>
+ document.body.append("JS DID EXCECUTE");
+ </script>
+
+ <a onClick='document.body.append("JS DID EXCECUTE");' href="#">link with lineOnClick</a>
+ </body>
+ </html>
+ </xsl:template>
+</xsl:stylesheet>
diff --git a/dom/security/test/csp/main_csp_worker.html b/dom/security/test/csp/main_csp_worker.html
new file mode 100644
index 0000000000..8957e3fd25
--- /dev/null
+++ b/dom/security/test/csp/main_csp_worker.html
@@ -0,0 +1,439 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Bug 1475849: Test CSP worker inheritance</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="worker_helper.js"></script>
+
+ </head>
+ <body>
+ <script type="application/javascript">
+ const SJS = "worker.sjs";
+ const SAME_BASE = "http://mochi.test:8888/tests/dom/security/test/csp/file_CSP.sjs";
+ const CROSS_BASE = "http://example.com/tests/dom/security/test/csp/file_CSP.sjs";
+
+ SimpleTest.waitForExplicitFinish();
+ /* test data format :
+ {
+ id: test id, short description of test,
+ base: URL of the request in worker,
+ action: type of request in worker (fetch, xhr, importscript)
+ type: how do we create the worker, from URL or Blob,
+ csp: csp of worker,
+ child: how do we create the child worker, from URL or Blob,
+ childCsp: csp of child worker
+ expectedBlock: result when CSP policy, true or false
+ }
+ */
+
+ // Document's CSP is defined in main_csp_worker.html^headers^
+ // Content-Security-Policy: default-src 'self' blob: 'unsafe-inline'
+ var tests = [
+ // create new Worker(url), worker's csp should be deliveried from header.
+ // csp should be: default-src 'self' blob: ; connect-src CROSS_BASE
+ {
+ id: "worker_url_fetch_same_bad",
+ base: SAME_BASE,
+ action: "fetch",
+ type: "url",
+ csp: "default-src 'self' blob: ; connect-src http://example.com",
+ expectBlocked: true
+ },
+ {
+ id: "worker_url_importScripts_same_good",
+ base: SAME_BASE,
+ action: "importScripts",
+ type: "url",
+ csp: "default-src 'self' blob: ; connect-src http://example.com",
+ expectBlocked: false
+ },
+ {
+ id: "worker_url_xhr_same_bad",
+ base: SAME_BASE,
+ action: "xhr",
+ type: "url",
+ csp: "default-src 'self' blob: ; connect-src http://example.com",
+ expectBlocked: true
+ },
+ {
+ id: "worker_url_fetch_cross_good",
+ base: CROSS_BASE,
+ action: "fetch",
+ type: "url",
+ csp: "default-src 'self' blob: ; connect-src http://example.com",
+ expectBlocked: false
+ },
+ {
+ id: "worker_url_importScripts_cross_bad",
+ base: CROSS_BASE,
+ action: "importScripts",
+ type: "url",
+ csp: "default-src 'self' blob: ; connect-src http://example.com",
+ expectBlocked: true
+ },
+ {
+ id: "worker_url_xhr_cross_good",
+ base: CROSS_BASE,
+ action: "xhr",
+ type: "url",
+ csp: "default-src 'self' blob: ; connect-src http://example.com",
+ expectBlocked: false
+ },
+
+ // create new Worker(blob:), worker's csp should be inherited from
+ // document.
+ // csp should be : default-src 'self' blob: 'unsafe-inline'
+ {
+ id: "worker_blob_fetch_same_good",
+ base: SAME_BASE,
+ action: "fetch",
+ type: "blob",
+ csp: "default-src 'self' blob: ; connect-src http://example.com",
+ expectBlocked: false
+ },
+ {
+ id: "worker_blob_xhr_same_good",
+ base: SAME_BASE,
+ action: "xhr",
+ type: "blob",
+ csp: "default-src 'self' blob: ; connect-src http://example.com",
+ expectBlocked: false
+ },
+ {
+ id: "worker_blob_importScripts_same_good",
+ base: SAME_BASE,
+ action: "importScripts",
+ type: "blob",
+ csp: "default-src 'self' blob: ; connect-src http://example.com",
+ expectBlocked: false
+ },
+ {
+ id: "worker_blob_fetch_cross_bad",
+ base: CROSS_BASE,
+ action: "fetch",
+ type: "blob",
+ csp: "default-src 'self' blob: ; connect-src http://example.com",
+ expectBlocked: true
+ },
+ {
+ id: "worker_blob_xhr_cross_bad",
+ base: CROSS_BASE,
+ action: "xhr",
+ type: "blob",
+ csp: "default-src 'self' blob: ; connect-src http://example.com",
+ expectBlocked: true
+ },
+ {
+ id: "worker_blob_importScripts_cross_bad",
+ base: CROSS_BASE,
+ action: "importScripts",
+ type: "blob",
+ csp: "default-src 'self' blob: ; connect-src http://example.com",
+ expectBlocked: true
+ },
+
+ // create parent worker from url, child worker from blob,
+ // Parent delivery csp then propagate to child
+ // csp should be: "default-src 'self' blob: ; connect-src 'self' http://example.com",
+ {
+ id: "worker_url_child_blob_fetch_same_good",
+ base: SAME_BASE,
+ action: "fetch",
+ child: "blob",
+ childCsp: "default-src 'none'",
+ type: "url",
+ csp: "default-src 'self' blob: ; connect-src 'self' http://example.com",
+ expectBlocked: false
+ },
+ {
+ id: "worker_url_child_blob_importScripts_same_good",
+ base: SAME_BASE,
+ action: "importScripts",
+ child: "blob",
+ childCsp: "default-src 'none'",
+ type: "url",
+ csp: "default-src 'self' blob: ; connect-src 'self' http://example.com",
+ expectBlocked: false
+ },
+ {
+ id: "worker_url_child_blob_xhr_same_good",
+ base: SAME_BASE,
+ child: "blob",
+ childCsp: "default-src 'none'",
+ action: "xhr",
+ type: "url",
+ csp: "default-src 'self' blob: ; connect-src 'self' http://example.com",
+ expectBlocked: false
+ },
+ {
+ id: "worker_url_child_blob_fetch_cross_good",
+ base: CROSS_BASE,
+ action: "fetch",
+ child: "blob",
+ childCsp: "default-src 'none'",
+ type: "url",
+ csp: "default-src 'self' blob: ; connect-src 'self' http://example.com",
+ expectBlocked: false
+ },
+ {
+ id: "worker_url_child_blob_importScripts_cross_bad",
+ base: CROSS_BASE,
+ action: "importScripts",
+ child: "blob",
+ childCsp: "default-src 'none'",
+ type: "url",
+ csp: "default-src 'self' blob: ; connect-src 'self' http://example.com",
+ expectBlocked: true
+ },
+ {
+ id: "worker_url_child_blob_xhr_cross_godd",
+ base: CROSS_BASE,
+ child: "blob",
+ childCsp: "default-src 'none'",
+ action: "xhr",
+ type: "url",
+ csp: "default-src 'self' blob: ; connect-src 'self' http://example.com",
+ expectBlocked: false
+ },
+
+
+ // create parent worker from blob, child worker from blob,
+ // Csp: document->parent->child
+ // csp should be : default-src 'self' blob: 'unsafe-inline'
+ {
+ id: "worker_blob_child_blob_fetch_same_good",
+ base: SAME_BASE,
+ child: "blob",
+ childCsp: "default-src 'none'",
+ action: "fetch",
+ type: "blob",
+ csp: "default-src 'self' blob:",
+ expectBlocked: false
+ },
+ {
+ id: "worker_blob_child_blob_xhr_same_good",
+ base: SAME_BASE,
+ child: "blob",
+ childCsp: "default-src 'none'",
+ action: "xhr",
+ type: "blob",
+ csp: "default-src 'self' blob:",
+ expectBlocked: false
+ },
+ {
+ id: "worker_blob_child_blob_importScripts_same_good",
+ base: SAME_BASE,
+ action: "importScripts",
+ child: "blob",
+ childCsp: "default-src 'none'",
+ type: "blob",
+ csp: "default-src 'self' blob:",
+ expectBlocked: false
+ },
+ {
+ id: "worker_blob_child_blob_fetch_cross_bad",
+ base: CROSS_BASE,
+ child: "blob",
+ childCsp: "default-src 'none'",
+ action: "fetch",
+ type: "blob",
+ csp: "default-src 'self' blob:",
+ expectBlocked: true
+ },
+ {
+ id: "worker_blob_child_blob_xhr_cross_bad",
+ base: CROSS_BASE,
+ child: "blob",
+ childCsp: "default-src 'none'",
+ action: "xhr",
+ type: "blob",
+ csp: "default-src 'self' blob:",
+ expectBlocked: true
+ },
+ {
+ id: "worker_blob_child_blob_importScripts_cross_bad",
+ base: CROSS_BASE,
+ action: "importScripts",
+ child: "blob",
+ childCsp: "default-src 'none'",
+ type: "blob",
+ csp: "default-src 'self' blob:",
+ expectBlocked: true
+ },
+
+ // create parent worker from url, child worker from url,
+ // child delivery csp from header
+ // csp should be : default-src 'none'
+ {
+ id: "worker_url_child_url_fetch_cross_bad",
+ base: CROSS_BASE,
+ action: "fetch",
+ child: "url",
+ childCsp: "default-src 'none'",
+ type: "url",
+ csp: "default-src 'self' blob:",
+ expectBlocked: true
+ },
+ {
+ id: "worker_url_child_url_xhr_cross_bad",
+ base: CROSS_BASE,
+ child: "url",
+ childCsp: "default-src 'none'",
+ action: "xhr",
+ type: "url",
+ csp: "default-src 'self' blob:",
+ expectBlocked: true
+ },
+ {
+ id: "worker_url_child_url_importScripts_cross_bad",
+ base: CROSS_BASE,
+ action: "importScripts",
+ child: "url",
+ childCsp: "default-src 'none'",
+ type: "url",
+ csp: "default-src 'self' blob:",
+ expectBlocked: true
+ },
+ {
+ id: "worker_url_child_url_fetch_same_bad",
+ base: SAME_BASE,
+ action: "fetch",
+ child: "url",
+ childCsp: "default-src 'none'",
+ type: "url",
+ csp: "default-src 'self' blob:",
+ expectBlocked: true
+ },
+ {
+ id: "worker_url_child_url_xhr_same_bad",
+ base: SAME_BASE,
+ child: "url",
+ childCsp: "default-src 'none'",
+ action: "xhr",
+ type: "url",
+ csp: "default-src 'self' blob:",
+ expectBlocked: true
+ },
+ {
+ id: "worker_url_child_url_importScripts_same_bad",
+ base: SAME_BASE,
+ action: "importScripts",
+ child: "url",
+ childCsp: "default-src 'none'",
+ type: "url",
+ csp: "default-src 'self' blob:",
+ expectBlocked: true
+ },
+
+ // create parent worker from blob, child worker from url,
+ // child delivery csp from header
+ // csp should be : default-src 'none'
+ {
+ id: "worker_blob_child_url_fetch_cross_bad",
+ base: CROSS_BASE,
+ child: "url",
+ childCsp: "default-src 'none'",
+ action: "fetch",
+ type: "blob",
+ csp: "default-src 'self' blob:",
+ expectBlocked: true
+ },
+ {
+ id: "worker_blob_child_url_xhr_cross_bad",
+ base: CROSS_BASE,
+ child: "url",
+ childCsp: "default-src 'none'",
+ action: "xhr",
+ type: "blob",
+ csp: "default-src 'self' blob:",
+ expectBlocked: true
+ },
+ {
+ id: "worker_blob_child_url_importScripts_cross_bad",
+ base: CROSS_BASE,
+ action: "importScripts",
+ child: "url",
+ childCsp: "default-src 'none'",
+ type: "blob",
+ csp: "default-src 'self' blob:",
+ expectBlocked: true
+ },
+ {
+ id: "worker_blob_child_url_fetch_same_bad",
+ base: SAME_BASE,
+ child: "url",
+ childCsp: "default-src 'none'",
+ action: "fetch",
+ type: "blob",
+ csp: "default-src 'self' blob:",
+ expectBlocked: true
+ },
+ {
+ id: "worker_blob_child_url_xhr_same_bad",
+ base: SAME_BASE,
+ child: "url",
+ childCsp: "default-src 'none'",
+ action: "xhr",
+ type: "blob",
+ csp: "default-src 'self' blob:",
+ expectBlocked: true
+ },
+ {
+ id: "worker_blob_child_url_importScripts_same_bad",
+ base: SAME_BASE,
+ action: "importScripts",
+ child: "url",
+ childCsp: "default-src 'none'",
+ type: "blob",
+ csp: "default-src 'self' blob:",
+ expectBlocked: true
+ },
+
+
+ ];
+
+ async function runWorkerTest(data) {
+ let src = SJS;
+ src += "?base=" + escape(data.base);
+ src += "&action=" + escape(data.action);
+ src += "&csp=" + escape(data.csp);
+ src += "&id=" + escape(data.id);
+
+ if (data.child) {
+ src += "&child=" + escape(data.child);
+ }
+
+ if (data.childCsp) {
+ src += "&childCsp=" + escape(data.childCsp);
+ }
+
+ switch (data.type) {
+ case "url":
+ new Worker(src);
+ break;
+
+ case "blob":
+ new Worker(URL.createObjectURL(await doXHRGetBlob(src)));
+ break;
+
+ default:
+ throw "Unsupport type";
+ }
+
+ let checkUri = data.base + "?id=" + data.id;
+ await assertCSPBlock(checkUri, data.expectBlocked);
+ runNextTest();
+ };
+
+ tests.forEach(function(test) {
+ addAsyncTest(async function() {
+ runWorkerTest(test);
+ });
+ });
+
+ runNextTest();
+ </script>
+
+ </body>
+</html>
diff --git a/dom/security/test/csp/main_csp_worker.html^headers^ b/dom/security/test/csp/main_csp_worker.html^headers^
new file mode 100644
index 0000000000..4597e01040
--- /dev/null
+++ b/dom/security/test/csp/main_csp_worker.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src 'self' blob: 'unsafe-inline'
diff --git a/dom/security/test/csp/mochitest.toml b/dom/security/test/csp/mochitest.toml
new file mode 100644
index 0000000000..8d8c6c31f5
--- /dev/null
+++ b/dom/security/test/csp/mochitest.toml
@@ -0,0 +1,821 @@
+[DEFAULT]
+support-files = [
+ "file_base_uri_server.sjs",
+ "file_blob_data_schemes.html",
+ "file_blob_uri_blocks_modals.html",
+ "file_blob_uri_blocks_modals.html^headers^",
+ "file_blob_top_nav_block_modals.html",
+ "file_blob_top_nav_block_modals.html^headers^",
+ "file_connect-src.html",
+ "file_connect-src-fetch.html",
+ "file_CSP.css",
+ "file_CSP.sjs",
+ "file_dummy_pixel.png",
+ "file_allow_https_schemes.html",
+ "file_bug663567.xsl",
+ "file_bug663567_allows.xml",
+ "file_bug663567_allows.xml^headers^",
+ "file_bug663567_blocks.xml",
+ "file_bug663567_blocks.xml^headers^",
+ "file_bug802872.html",
+ "file_bug802872.html^headers^",
+ "file_bug802872.js",
+ "file_bug802872.sjs",
+ "file_bug885433_allows.html",
+ "file_bug885433_allows.html^headers^",
+ "file_bug885433_blocks.html",
+ "file_bug885433_blocks.html^headers^",
+ "file_bug888172.html",
+ "file_bug888172.sjs",
+ "file_evalscript_main.js",
+ "file_evalscript_main_allowed.js",
+ "file_evalscript_main.html",
+ "file_evalscript_main.html^headers^",
+ "file_evalscript_main_allowed.html",
+ "file_evalscript_main_allowed.html^headers^",
+ "file_frameancestors_main.html",
+ "file_frameancestors_main.js",
+ "file_frameancestors.sjs",
+ "file_frameancestors_userpass.html",
+ "file_frameancestors_userpass_frame_a.html",
+ "file_frameancestors_userpass_frame_b.html",
+ "file_frameancestors_userpass_frame_c.html",
+ "file_frameancestors_userpass_frame_c.html^headers^",
+ "file_frameancestors_userpass_frame_d.html",
+ "file_frameancestors_userpass_frame_d.html^headers^",
+ "file_inlinescript.html",
+ "file_inlinestyle_main.html",
+ "file_inlinestyle_main.html^headers^",
+ "file_inlinestyle_main_allowed.html",
+ "file_inlinestyle_main_allowed.html^headers^",
+ "file_invalid_source_expression.html",
+ "file_main.html",
+ "file_main.html^headers^",
+ "file_main.js",
+ "file_web_manifest.html",
+ "file_web_manifest_remote.html",
+ "file_web_manifest_https.html",
+ "file_web_manifest.json",
+ "file_web_manifest.json^headers^",
+ "file_web_manifest_https.json",
+ "file_web_manifest_mixed_content.html",
+ "file_bug836922_npolicies.html",
+ "file_bug836922_npolicies.html^headers^",
+ "file_bug836922_npolicies_ro_violation.sjs",
+ "file_bug836922_npolicies_violation.sjs",
+ "file_bug886164.html",
+ "file_bug886164.html^headers^",
+ "file_bug886164_2.html",
+ "file_bug886164_2.html^headers^",
+ "file_bug886164_3.html",
+ "file_bug886164_3.html^headers^",
+ "file_bug886164_4.html",
+ "file_bug886164_4.html^headers^",
+ "file_bug886164_5.html",
+ "file_bug886164_5.html^headers^",
+ "file_bug886164_6.html",
+ "file_bug886164_6.html^headers^",
+ "file_redirects_main.html",
+ "file_redirects_page.sjs",
+ "file_redirects_resource.sjs",
+ "file_bug910139.sjs",
+ "file_bug910139.xml",
+ "file_bug910139.xsl",
+ "file_bug909029_star.html",
+ "file_bug909029_star.html^headers^",
+ "file_bug909029_none.html",
+ "file_bug909029_none.html^headers^",
+ "file_bug1229639.html",
+ "file_bug1229639.html^headers^",
+ "file_bug1312272.html",
+ "file_bug1312272.js",
+ "file_bug1312272.html^headers^",
+ "file_bug1452037.html",
+ "file_bug1505412.sjs",
+ "file_bug1505412_reporter.sjs",
+ "file_bug1505412_frame.html",
+ "file_bug1505412_frame.html^headers^",
+ "file_policyuri_regression_from_multipolicy.html",
+ "file_policyuri_regression_from_multipolicy.html^headers^",
+ "file_policyuri_regression_from_multipolicy_policy",
+ "file_nonce_source.html",
+ "file_nonce_source.html^headers^",
+ "file_nonce_redirects.html",
+ "file_nonce_redirector.sjs",
+ "file_bug941404.html",
+ "file_bug941404_xhr.html",
+ "file_bug941404_xhr.html^headers^",
+ "file_frame_ancestors_ro.html",
+ "file_frame_ancestors_ro.html^headers^",
+ "file_hash_source.html",
+ "file_dual_header_testserver.sjs",
+ "file_hash_source.html^headers^",
+ "file_scheme_relative_sources.js",
+ "file_scheme_relative_sources.sjs",
+ "file_ignore_unsafe_inline.html",
+ "file_ignore_unsafe_inline_multiple_policies_server.sjs",
+ "file_self_none_as_hostname_confusion.html",
+ "file_self_none_as_hostname_confusion.html^headers^",
+ "file_empty_directive.html",
+ "file_empty_directive.html^headers^",
+ "file_path_matching.html",
+ "file_path_matching_incl_query.html",
+ "file_path_matching.js",
+ "file_path_matching_redirect.html",
+ "file_path_matching_redirect_server.sjs",
+ "file_testserver.sjs",
+ "file_report_uri_missing_in_report_only_header.html",
+ "file_report_uri_missing_in_report_only_header.html^headers^",
+ "file_report.html",
+ "file_report_chromescript.js",
+ "file_redirect_content.sjs",
+ "file_redirect_report.sjs",
+ "file_subframe_run_js_if_allowed.html",
+ "file_subframe_run_js_if_allowed.html^headers^",
+ "file_leading_wildcard.html",
+ "file_multi_policy_injection_bypass.html",
+ "file_multi_policy_injection_bypass.html^headers^",
+ "file_multi_policy_injection_bypass_2.html",
+ "file_multi_policy_injection_bypass_2.html^headers^",
+ "file_null_baseuri.html",
+ "file_form-action.html",
+ "referrerdirective.sjs",
+ "file_upgrade_insecure.html",
+ "file_upgrade_insecure_meta.html",
+ "file_upgrade_insecure_server.sjs",
+ "file_upgrade_insecure_wsh.py",
+ "file_upgrade_insecure_reporting.html",
+ "file_upgrade_insecure_reporting_server.sjs",
+ "file_upgrade_insecure_cors.html",
+ "file_upgrade_insecure_cors_server.sjs",
+ "file_upgrade_insecure_loopback.html",
+ "file_upgrade_insecure_loopback_form.html",
+ "file_upgrade_insecure_loopback_server.sjs",
+ "file_report_for_import.css",
+ "file_report_for_import.html",
+ "file_report_for_import_server.sjs",
+ "file_service_worker.html",
+ "file_service_worker.js",
+ "file_child-src_iframe.html",
+ "file_child-src_inner_frame.html",
+ "file_child-src_worker.html",
+ "file_child-src_worker_data.html",
+ "file_child-src_worker-redirect.html",
+ "file_child-src_worker.js",
+ "file_child-src_service_worker.html",
+ "file_child-src_service_worker.js",
+ "file_child-src_shared_worker.html",
+ "file_child-src_shared_worker_data.html",
+ "file_child-src_shared_worker-redirect.html",
+ "file_child-src_shared_worker.js",
+ "file_redirect_worker.sjs",
+ "file_meta_element.html",
+ "file_meta_header_dual.sjs",
+ "file_docwrite_meta.html",
+ "file_doccomment_meta.html",
+ "file_docwrite_meta.css",
+ "file_docwrite_meta.js",
+ "file_multipart_testserver.sjs",
+ "file_fontloader.sjs",
+ "file_fontloader.woff",
+ "file_block_all_mcb.sjs",
+ "file_block_all_mixed_content_frame_navigation1.html",
+ "file_block_all_mixed_content_frame_navigation2.html",
+ "file_form_action_server.sjs",
+ "!/image/test/mochitest/blue.png",
+ "file_meta_whitespace_skipping.html",
+ "file_ping.html",
+ "test_iframe_sandbox_top_1.html^headers^",
+ "file_iframe_sandbox_document_write.html",
+ "file_sandbox_pass.js",
+ "file_sandbox_fail.js",
+ "file_sandbox_1.html",
+ "file_sandbox_2.html",
+ "file_sandbox_3.html",
+ "file_sandbox_4.html",
+ "file_sandbox_5.html",
+ "file_sandbox_6.html",
+ "file_sandbox_7.html",
+ "file_sandbox_8.html",
+ "file_sandbox_9.html",
+ "file_sandbox_10.html",
+ "file_sandbox_11.html",
+ "file_sandbox_12.html",
+ "file_sandbox_13.html",
+ "file_sendbeacon.html",
+ "file_upgrade_insecure_docwrite_iframe.sjs",
+ "file_data-uri_blocked.html",
+ "file_data-uri_blocked.html^headers^",
+ "file_strict_dynamic_js_url.html",
+ "file_strict_dynamic_script_events.html",
+ "file_strict_dynamic_script_events_marquee.html",
+ "file_strict_dynamic_script_inline.html",
+ "file_strict_dynamic_script_extern.html",
+ "file_strict_dynamic.js",
+ "file_strict_dynamic_parser_inserted_doc_write.html",
+ "file_strict_dynamic_parser_inserted_doc_write_correct_nonce.html",
+ "file_strict_dynamic_non_parser_inserted.html",
+ "file_strict_dynamic_non_parser_inserted_inline.html",
+ "file_strict_dynamic_unsafe_eval.html",
+ "file_strict_dynamic_default_src.html",
+ "file_strict_dynamic_default_src.js",
+ "file_upgrade_insecure_navigation.sjs",
+ "file_punycode_host_src.sjs",
+ "file_punycode_host_src.js",
+ "file_iframe_srcdoc.sjs",
+ "file_iframe_sandbox_srcdoc.html",
+ "file_iframe_sandbox_srcdoc.html^headers^",
+ "file_websocket_self.html",
+ "file_websocket_csp_upgrade.html",
+ "file_websocket_explicit.html",
+ "file_websocket_self_wsh.py",
+ "file_win_open_blocked.html",
+ "file_image_nonce.html",
+ "file_image_nonce.html^headers^",
+ "file_ignore_xfo.html",
+ "file_ignore_xfo.html^headers^",
+ "file_ro_ignore_xfo.html",
+ "file_ro_ignore_xfo.html^headers^",
+ "file_no_log_ignore_xfo.html",
+ "file_no_log_ignore_xfo.html^headers^",
+ "file_data_csp_inheritance.html",
+ "file_data_csp_merge.html",
+ "file_data_doc_ignore_meta_csp.html",
+ "file_report_font_cache-1.html",
+ "file_report_font_cache-2.html",
+ "file_report_font_cache-2.html^headers^",
+ "Ahem.ttf",
+ "file_independent_iframe_csp.html",
+ "file_upgrade_insecure_report_only.html",
+ "file_upgrade_insecure_report_only_server.sjs",
+]
+prefs = [
+ "security.mixed_content.upgrade_display_content=false",
+ "javascript.options.experimental.shadow_realms=true",
+]
+
+["test_301_redirect.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_302_redirect.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_303_redirect.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_307_redirect.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_CSP.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_allow_https_schemes.html"]
+
+["test_base-uri.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_blob_data_schemes.html"]
+
+["test_blob_uri_blocks_modals.html"]
+skip-if = [
+ "xorigin",
+ "os == 'linux'",
+ "asan", # alert should be blocked by CSP - got false, expected true
+ "tsan", # alert should be blocked by CSP - got false, expected true
+ "http3",
+ "http2",
+]
+
+["test_block_all_mixed_content.html"]
+tags = "mcb"
+
+["test_block_all_mixed_content_frame_navigation.html"]
+tags = "mcb"
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_blocked_uri_in_reports.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_blocked_uri_in_violation_event_after_redirects.html"]
+support-files = [
+ "file_blocked_uri_in_violation_event_after_redirects.html",
+ "file_blocked_uri_in_violation_event_after_redirects.sjs",
+]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_blocked_uri_redirect_frame_src.html"]
+support-files = [
+ "file_blocked_uri_redirect_frame_src.html",
+ "file_blocked_uri_redirect_frame_src.html^headers^",
+ "file_blocked_uri_redirect_frame_src_server.sjs",
+]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug663567.html"]
+skip-if = ["fission && xorigin && debug && os == 'win'"] # Bug 1716406 - New fission platform triage
+
+["test_bug802872.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug836922_npolicies.html"]
+skip-if = [
+ "verify",
+ "http3",
+ "http2",
+]
+
+["test_bug885433.html"]
+
+["test_bug886164.html"]
+
+["test_bug888172.html"]
+
+["test_bug909029.html"]
+
+["test_bug910139.html"]
+skip-if = ["verify"]
+
+["test_bug941404.html"]
+
+["test_bug1229639.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug1242019.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug1312272.html"]
+
+["test_bug1452037.html"]
+
+["test_bug1505412.html"]
+skip-if = ["!debug"]
+
+["test_bug1579094.html"]
+
+["test_bug1738418.html"]
+support-files = [
+ "file_bug1738418_parent.html",
+ "file_bug1738418_parent.html^headers^",
+ "file_bug1738418_child.html",
+]
+
+["test_bug1764343.html"]
+support-files = [
+ "file_bug1764343.html",
+]
+
+["test_bug1777572.html"]
+support-files = ["file_bug1777572.html"]
+skip-if = ["os == 'android'"] # This unusual window.close/open test times out on Android.
+
+["test_child-src_iframe.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_child-src_worker-redirect.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_child-src_worker.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_child-src_worker_data.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_connect-src.html"]
+
+["test_csp_frame_ancestors_about_blank.html"]
+support-files = [
+ "file_csp_frame_ancestors_about_blank.html",
+ "file_csp_frame_ancestors_about_blank.html^headers^",
+]
+
+["test_csp_style_src_empty_hash.html"]
+
+["test_csp_worker_inheritance.html"]
+support-files = [
+ "worker.sjs",
+ "worker_helper.js",
+ "main_csp_worker.html",
+ "main_csp_worker.html^headers^",
+]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_data_csp_inheritance.html"]
+
+["test_data_csp_merge.html"]
+
+["test_data_doc_ignore_meta_csp.html"]
+
+["test_docwrite_meta.html"]
+
+["test_dual_header.html"]
+
+["test_empty_directive.html"]
+
+["test_evalscript.html"]
+
+["test_evalscript_allowed_by_strict_dynamic.html"]
+
+["test_evalscript_blocked_by_strict_dynamic.html"]
+
+["test_fontloader.html"]
+
+["test_form-action.html"]
+
+["test_form_action_blocks_url.html"]
+
+["test_frame_ancestors_ro.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_frame_src.html"]
+support-files = [
+ "file_frame_src_frame_governs.html",
+ "file_frame_src_child_governs.html",
+ "file_frame_src.js",
+ "file_frame_src_inner.html",
+]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_frameancestors.html"]
+skip-if = [
+ "xorigin", # JavaScript error: http://mochi.xorigin-test:8888/tests/SimpleTest/TestRunner.js, line 157: SecurityError: Permission denied to access property "wrappedJSObject" on cross-origin object
+ "http3",
+ "http2",
+]
+
+["test_frameancestors_userpass.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_hash_source.html"]
+skip-if = ["fission && xorigin && debug"] # Bug 1716406 - New fission platform triage
+
+["test_iframe_sandbox.html"]
+skip-if = [
+ "fission && xorigin && debug && (os == 'win' || os == 'linux')", # Bug 1716406 - New fission platform triage
+ "http3",
+ "http2",
+]
+
+["test_iframe_sandbox_srcdoc.html"]
+skip-if = ["fission && xorigin && debug && os == 'win'"] # Bug 1716406 - New fission platform triage
+
+["test_iframe_sandbox_top_1.html"]
+
+["test_iframe_srcdoc.html"]
+
+["test_ignore_unsafe_inline.html"]
+skip-if = ["xorigin"] # JavaScript error: http://mochi.xorigin-test:8888/tests/SimpleTest/TestRunner.js, line 157: SecurityError: Permission denied to access property "wrappedJSObject" on cross-origin object, [Child 3789, Main Thread] WARNING: NS_ENSURE_TRUE(request) failed: file /builds/worker/checkouts/gecko/netwerk/base/nsLoadGroup.cpp, line 591
+
+["test_ignore_xfo.html"]
+skip-if = [
+ "xorigin", # JavaScript error: http://mochi.xorigin-test:8888/tests/SimpleTest/TestRunner.js, line 157: SecurityError: Permission denied to access property "wrappedJSObject" on cross-origin object
+ "http3",
+ "http2",
+]
+
+["test_image_document.html"]
+support-files = [
+ "file_image_document_pixel.png",
+ "file_image_document_pixel.png^headers^",
+]
+
+["test_image_nonce.html"]
+
+["test_independent_iframe_csp.html"]
+
+["test_inlinescript.html"]
+
+["test_inlinestyle.html"]
+
+["test_invalid_source_expression.html"]
+
+["test_leading_wildcard.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_link_rel_preload.html"]
+support-files = ["file_link_rel_preload.html"]
+
+["test_meta_csp_self.html"]
+
+["test_meta_element.html"]
+
+["test_meta_header_dual.html"]
+
+["test_meta_whitespace_skipping.html"]
+
+["test_multi_policy_injection_bypass.html"]
+
+["test_multipartchannel.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_nonce_redirects.html"]
+
+["test_nonce_snapshot.html"]
+support-files = ["file_nonce_snapshot.sjs"]
+
+["test_nonce_source.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_null_baseuri.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_object_inherit.html"]
+support-files = ["file_object_inherit.html"]
+
+["test_parent_location_js.html"]
+support-files = [
+ "file_parent_location_js.html",
+ "file_iframe_parent_location_js.html",
+]
+
+["test_path_matching.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_path_matching_redirect.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_ping.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_policyuri_regression_from_multipolicy.html"]
+
+["test_punycode_host_src.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_redirects.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_report.html"]
+fail-if = ["xorigin"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_report_font_cache.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_report_for_import.html"]
+fail-if = ["xorigin"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_report_uri_missing_in_report_only_header.html"]
+
+["test_sandbox.html"]
+skip-if = ["true"] # Bug 1657934
+
+["test_sandbox_allow_scripts.html"]
+support-files = [
+ "file_sandbox_allow_scripts.html",
+ "file_sandbox_allow_scripts.html^headers^",
+]
+
+["test_scheme_relative_sources.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_script_template.html"]
+support-files = [
+ "file_script_template.html",
+ "file_script_template.js",
+]
+
+["test_security_policy_violation_event.html"]
+
+["test_self_none_as_hostname_confusion.html"]
+
+["test_sendbeacon.html"]
+
+["test_service_worker.html"]
+
+["test_strict_dynamic.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_strict_dynamic_default_src.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_strict_dynamic_parser_inserted.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_subframe_run_js_if_allowed.html"]
+
+["test_svg_inline_style.html"]
+support-files = [
+ "file_svg_inline_style_base.html",
+ "file_svg_inline_style_csp.html",
+ "file_svg_srcset_inline_style_base.html",
+ "file_svg_srcset_inline_style_csp.html",
+ "file_svg_inline_style_server.sjs",
+]
+
+["test_uir_top_nav.html"]
+support-files = [
+ "file_uir_top_nav.html",
+ "file_uir_top_nav_dummy.html",
+]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_uir_windowwatcher.html"]
+support-files = [
+ "file_windowwatcher_frameA.html",
+ "file_windowwatcher_subframeB.html",
+ "file_windowwatcher_subframeC.html",
+ "file_windowwatcher_subframeD.html",
+ "file_windowwatcher_win_open.html",
+]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_upgrade_insecure.html"]
+skip-if = [
+ "os == 'linux' && bits == 64", # Bug 1620516
+ "os == 'android'", # Bug 1777028
+]
+
+["test_upgrade_insecure_cors.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_upgrade_insecure_docwrite_iframe.html"]
+
+["test_upgrade_insecure_loopback.html"]
+
+["test_upgrade_insecure_navigation.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_upgrade_insecure_navigation_redirect.html"]
+support-files = [
+ "file_upgrade_insecure_navigation_redirect.sjs",
+ "file_upgrade_insecure_navigation_redirect_same_origin.html",
+ "file_upgrade_insecure_navigation_redirect_cross_origin.html",
+]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_upgrade_insecure_report_only.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_upgrade_insecure_reporting.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_websocket_localhost.html"]
+skip-if = [
+ "os == 'android'", # no websocket support Bug 982828
+ "http3",
+ "http2",
+]
+
+["test_websocket_self.html"]
+skip-if = [
+ "os == 'android'", # no websocket support Bug 982828
+ "http3",
+ "http2",
+]
+
+["test_win_open_blocked.html"]
+
+["test_worker_src.html"]
+support-files = [
+ "file_worker_src_worker_governs.html",
+ "file_worker_src_child_governs.html",
+ "file_worker_src_script_governs.html",
+ "file_worker_src.js",
+ "file_spawn_worker.js",
+ "file_spawn_shared_worker.js",
+ "file_spawn_service_worker.js",
+]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_xslt_inherits_csp.html"]
+support-files = [
+ "file_xslt_inherits_csp.xml",
+ "file_xslt_inherits_csp.xml^headers^",
+ "file_xslt_inherits_csp.xsl",
+]
diff --git a/dom/security/test/csp/referrerdirective.sjs b/dom/security/test/csp/referrerdirective.sjs
new file mode 100644
index 0000000000..267eaaede2
--- /dev/null
+++ b/dom/security/test/csp/referrerdirective.sjs
@@ -0,0 +1,40 @@
+// Used for bug 965727 to serve up really simple scripts reflecting the
+// referrer sent to load this back to the loader.
+
+function handleRequest(request, response) {
+ // skip speculative loads.
+
+ var splits = request.queryString.split("&");
+ var params = {};
+ splits.forEach(function (v) {
+ let parts = v.split("=");
+ params[parts[0]] = unescape(parts[1]);
+ });
+
+ var loadType = params.type;
+ var referrerLevel = "error";
+
+ if (request.hasHeader("Referer")) {
+ var referrer = request.getHeader("Referer");
+ if (referrer.indexOf("file_testserver.sjs") > -1) {
+ referrerLevel = "full";
+ } else {
+ referrerLevel = "origin";
+ }
+ } else {
+ referrerLevel = "none";
+ }
+
+ var theScript =
+ 'window.postResult("' + loadType + '", "' + referrerLevel + '");';
+ response.setHeader(
+ "Content-Type",
+ "application/javascript; charset=utf-8",
+ false
+ );
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ if (request.method != "OPTIONS") {
+ response.write(theScript);
+ }
+}
diff --git a/dom/security/test/csp/test_301_redirect.html b/dom/security/test/csp/test_301_redirect.html
new file mode 100644
index 0000000000..0aaed5bcf2
--- /dev/null
+++ b/dom/security/test/csp/test_301_redirect.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=650386
+Test that CSP violation reports are not sent when a 301 redirect is encountered
+-->
+<head>
+ <title>Test for Bug 650386</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=650386">Mozilla Bug 650386</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe id = "content_iframe"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 650386 **/
+
+// This is used to watch the redirect of the report POST get blocked
+function examiner() {
+ SpecialPowers.addObserver(this, "csp-on-violate-policy");
+ SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
+}
+
+examiner.prototype = {
+ observe(subject, topic, data) {
+ if (topic === "specialpowers-http-notify-request") {
+ // this is used to fail the test - if we see the POST to the target of the redirect
+ // we know this is a fail
+ var uri = data;
+ if (uri == "http://example.com/some/fake/path")
+ window.done(false);
+ }
+
+ if(topic === "csp-on-violate-policy") {
+ // something was blocked, but we are looking specifically for the redirect being blocked
+ if (data == "denied redirect while sending violation report")
+ window.done(true);
+ }
+ },
+
+ // must eventually call this to remove the listener,
+ // or mochitests might get borked.
+ remove() {
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
+ }
+}
+
+window.examiner = new examiner();
+
+// result == true if we saw the redirect blocked notify, false if we saw the post
+// to the redirect target go out
+window.done = function(result) {
+ ok(result, "a 301 redirect when posting violation report should be blocked");
+
+ // clean up observers and finish the test
+ window.examiner.remove();
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// save this for last so that our listeners are registered.
+// ... this loads the testbed of good and bad requests.
+document.getElementById('content_iframe').src = 'file_redirect_content.sjs?301';
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_302_redirect.html b/dom/security/test/csp/test_302_redirect.html
new file mode 100644
index 0000000000..330c1a64e9
--- /dev/null
+++ b/dom/security/test/csp/test_302_redirect.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=650386
+Test that CSP violation reports are not sent when a 302 redirect is encountered
+-->
+<head>
+ <title>Test for Bug 650386</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=650386">Mozilla Bug 650386</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe id = "content_iframe"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 650386 **/
+
+// This is used to watch the redirect of the report POST get blocked
+function examiner() {
+ SpecialPowers.addObserver(this, "csp-on-violate-policy");
+ SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
+}
+
+examiner.prototype = {
+ observe(subject, topic, data) {
+ if (topic === "specialpowers-http-notify-request") {
+ // this is used to fail the test - if we see the POST to the target of the redirect
+ // we know this is a fail
+ var uri = data;
+ if (uri == "http://example.com/some/fake/path")
+ window.done(false);
+ }
+
+ if(topic === "csp-on-violate-policy") {
+ // something was blocked, but we are looking specifically for the redirect being blocked
+ if (data == "denied redirect while sending violation report")
+ window.done(true);
+ }
+ },
+
+ // must eventually call this to remove the listener,
+ // or mochitests might get borked.
+ remove() {
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
+ }
+}
+
+window.examiner = new examiner();
+
+// result == true if we saw the redirect blocked notify, false if we saw the post
+// to the redirect target go out
+window.done = function(result) {
+ ok(result, "a 302 redirect when posting violation report should be blocked");
+
+ // clean up observers and finish the test
+ window.examiner.remove();
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// save this for last so that our listeners are registered.
+// ... this loads the testbed of good and bad requests.
+document.getElementById('content_iframe').src = 'file_redirect_content.sjs?302';
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_303_redirect.html b/dom/security/test/csp/test_303_redirect.html
new file mode 100644
index 0000000000..ecff523967
--- /dev/null
+++ b/dom/security/test/csp/test_303_redirect.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=650386
+Test that CSP violation reports are not sent when a 303 redirect is encountered
+-->
+<head>
+ <title>Test for Bug 650386</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=650386">Mozilla Bug 650386</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe id = "content_iframe"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 650386 **/
+
+// This is used to watch the redirect of the report POST get blocked
+function examiner() {
+ SpecialPowers.addObserver(this, "csp-on-violate-policy");
+ SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
+}
+
+examiner.prototype = {
+ observe(subject, topic, data) {
+ if (topic === "specialpowers-http-notify-request") {
+ // this is used to fail the test - if we see the POST to the target of the redirect
+ // we know this is a fail
+ var uri = data;
+ if (uri == "http://example.com/some/fake/path")
+ window.done(false);
+ }
+
+ if(topic === "csp-on-violate-policy") {
+ // something was blocked, but we are looking specifically for the redirect being blocked
+ if (data == "denied redirect while sending violation report")
+ window.done(true);
+ }
+ },
+
+ // must eventually call this to remove the listener,
+ // or mochitests might get borked.
+ remove() {
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
+ }
+}
+
+window.examiner = new examiner();
+
+// result == true if we saw the redirect blocked notify, false if we saw the post
+// to the redirect target go out
+window.done = function(result) {
+ ok(result, "a 303 redirect when posting violation report should be blocked");
+
+ // clean up observers and finish the test
+ window.examiner.remove();
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// save this for last so that our listeners are registered.
+// ... this loads the testbed of good and bad requests.
+document.getElementById('content_iframe').src = 'file_redirect_content.sjs?303';
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_307_redirect.html b/dom/security/test/csp/test_307_redirect.html
new file mode 100644
index 0000000000..40ebd592b3
--- /dev/null
+++ b/dom/security/test/csp/test_307_redirect.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=650386
+Test that CSP violation reports are not sent when a 307 redirect is encountered
+-->
+<head>
+ <title>Test for Bug 650386</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=650386">Mozilla Bug 650386</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe id = "content_iframe"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 650386 **/
+
+// This is used to watch the redirect of the report POST get blocked
+function examiner() {
+ SpecialPowers.addObserver(this, "csp-on-violate-policy");
+ SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
+}
+
+examiner.prototype = {
+ observe(subject, topic, data) {
+ if (topic === "specialpowers-http-notify-request") {
+ // this is used to fail the test - if we see the POST to the target of the redirect
+ // we know this is a fail
+ var uri = data;
+ if (uri == "http://example.com/some/fake/path")
+ window.done(false);
+ }
+
+ if(topic === "csp-on-violate-policy") {
+ // something was blocked, but we are looking specifically for the redirect being blocked
+ if (data == "denied redirect while sending violation report")
+ window.done(true);
+ }
+ },
+
+ // must eventually call this to remove the listener,
+ // or mochitests might get borked.
+ remove() {
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
+ }
+}
+
+window.examiner = new examiner();
+
+// result == true if we saw the redirect blocked notify, false if we saw the post
+// to the redirect target go out
+window.done = function(result) {
+ ok(result, "a 307 redirect when posting violation report should be blocked");
+
+ // clean up observers and finish the test
+ window.examiner.remove();
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// save this for last so that our listeners are registered.
+// ... this loads the testbed of good and bad requests.
+document.getElementById('content_iframe').src = 'file_redirect_content.sjs?307';
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_CSP.html b/dom/security/test/csp/test_CSP.html
new file mode 100644
index 0000000000..babb9db9bc
--- /dev/null
+++ b/dom/security/test/csp/test_CSP.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Content Security Policy Connections</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<iframe style="width:200px;height:200px;" id='cspframe'></iframe>
+<script class="testbody" type="text/javascript">
+
+// These are test results: -1 means it hasn't run,
+// true/false is the pass/fail result.
+window.tests = {
+ img_good: -1,
+ img_bad: -1,
+ style_good: -1,
+ style_bad: -1,
+ frame_good: -1,
+ frame_bad: -1,
+ script_good: -1,
+ script_bad: -1,
+ xhr_good: -1,
+ xhr_bad: -1,
+ fetch_good: -1,
+ fetch_bad: -1,
+ beacon_good: -1,
+ beacon_bad: -1,
+ media_good: -1,
+ media_bad: -1,
+ font_good: -1,
+ font_bad: -1,
+ object_good: -1,
+ object_bad: -1,
+};
+
+SpecialPowers.registerObservers("csp-on-violate-policy");
+
+// This is used to watch the blocked data bounce off CSP and allowed data
+// get sent out to the wire.
+function examiner() {
+ SpecialPowers.addObserver(this, "csp-on-violate-policy");
+ SpecialPowers.addObserver(this, "specialpowers-csp-on-violate-policy");
+ SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
+}
+examiner.prototype = {
+ observe(subject, topic, data) {
+ var testpat = new RegExp("testid=([a-z0-9_]+)");
+
+ //_good things better be allowed!
+ //_bad things better be stopped!
+
+ // This is a special observer topic that is proxied from
+ // http-on-modify-request in the parent process to inform us when a URI is
+ // loaded
+ if (topic === "specialpowers-http-notify-request") {
+ var uri = data;
+ if (!testpat.test(uri)) return;
+ var testid = testpat.exec(uri)[1];
+
+ window.testResult(testid,
+ /_good/.test(testid),
+ uri + " allowed by csp");
+ }
+
+ if (topic === "csp-on-violate-policy" ||
+ topic === "specialpowers-csp-on-violate-policy") {
+ // these were blocked... record that they were blocked
+ var asciiSpec = SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIURI"), "asciiSpec");
+ if (!testpat.test(asciiSpec)) return;
+ var testid = testpat.exec(asciiSpec)[1];
+ window.testResult(testid,
+ /_bad/.test(testid),
+ asciiSpec + " blocked by \"" + data + "\"");
+ }
+ },
+
+ // must eventually call this to remove the listener,
+ // or mochitests might get borked.
+ remove() {
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ SpecialPowers.removeObserver(this, "specialpowers-csp-on-violate-policy");
+ SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
+ }
+}
+
+window.examiner = new examiner();
+
+window.testResult = function(testname, result, msg) {
+ // test already complete.... forget it... remember the first result.
+ if (window.tests[testname] != -1)
+ return;
+
+ ok(testname in window.tests, "It's a real test");
+ window.tests[testname] = result;
+ is(result, true, testname + ' test: ' + msg);
+
+ // if any test is incomplete, keep waiting
+ for (var v in window.tests)
+ if(tests[v] == -1)
+ return;
+
+ // ... otherwise, finish
+ window.examiner.remove();
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv(
+ {'set':[// On a cellular connection the default preload value is 0 ("preload
+ // none"). Our Android emulators emulate a cellular connection, and
+ // so by default preload no media data. This causes the media_* tests
+ // to timeout. We set the default used by cellular connections to the
+ // same as used by non-cellular connections in order to get
+ // consistent behavior across platforms/devices.
+ ["media.preload.default", 2],
+ ["media.preload.default.cellular", 2]]},
+ function() {
+ // save this for last so that our listeners are registered.
+ // ... this loads the testbed of good and bad requests.
+ document.getElementById('cspframe').src = 'file_main.html';
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_allow_https_schemes.html b/dom/security/test/csp/test_allow_https_schemes.html
new file mode 100644
index 0000000000..be1f030fb9
--- /dev/null
+++ b/dom/security/test/csp/test_allow_https_schemes.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 826805 - Allow http and https for scheme-less sources</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="visibility: hidden">
+ <iframe style="width:100%;" id="testframe"></iframe>
+ </div>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+/* Description of the test:
+ * We are loading the following url (including a fragment portion):
+ * https://example.com/tests/dom/security/test/csp/file_path_matching.js#foo
+ * using different policies that lack specification of a scheme.
+ *
+ * Since the file is served over http:, the upgrade to https should be
+ * permitted by CSP in case no port is specified.
+ */
+
+var policies = [
+ ["allowed", "example.com"],
+ ["allowed", "example.com:443"],
+ ["allowed", "example.com:80"],
+ ["allowed", "http://*:80"],
+ ["allowed", "https://*:443"],
+ // our testing framework only supports :80 and :443, but
+ // using :8000 in a policy does the trick for the test.
+ ["blocked", "example.com:8000"],
+]
+
+var counter = 0;
+var policy;
+
+function loadNextTest() {
+ if (counter == policies.length) {
+ SimpleTest.finish();
+ }
+ else {
+ policy = policies[counter++];
+ var src = "file_testserver.sjs";
+ // append the file that should be served
+ src += "?file=" + escape("tests/dom/security/test/csp/file_allow_https_schemes.html");
+ // append the CSP that should be used to serve the file
+ src += "&csp=" + escape("default-src 'none'; script-src " + policy[1]);
+
+ document.getElementById("testframe").addEventListener("load", test);
+ document.getElementById("testframe").src = src;
+ }
+}
+
+function test() {
+ try {
+ document.getElementById("testframe").removeEventListener('load', test);
+ var testframe = document.getElementById("testframe");
+ var divcontent = testframe.contentWindow.document.getElementById('testdiv').innerHTML;
+ is(divcontent, policy[0], "should be " + policy[0] + " in test " + (counter - 1) + "!");
+ }
+ catch (e) {
+ ok(false, "ERROR: could not access content in test " + (counter - 1) + "!");
+ }
+ loadNextTest();
+}
+
+loadNextTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_base-uri.html b/dom/security/test/csp/test_base-uri.html
new file mode 100644
index 0000000000..4d5c5504af
--- /dev/null
+++ b/dom/security/test/csp/test_base-uri.html
@@ -0,0 +1,124 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1045897 - Test CSP base-uri directive</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="visibility: hidden">
+ <iframe style="width:100%;" id="testframe"></iframe>
+ </div>
+
+<script class="testbody" type="text/javascript">
+
+/*
+ * Description of the test:
+ * We load a page in an iframe (served over http://example.com) that tries to
+ * modify the 'base' either through setting or also removing the base-uri. We
+ * load that page using different policies and verify that setting the base-uri
+ * is correctly blocked by CSP.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+var tests = [
+ { csp: "base-uri http://mochi.test;",
+ base1: "http://mochi.test",
+ base2: "",
+ action: "enforce-csp",
+ result: "http://mochi.test",
+ desc: "CSP allows base uri"
+ },
+ { csp: "base-uri http://example.com;",
+ base1: "http://mochi.test",
+ base2: "",
+ action: "enforce-csp",
+ result: "http://example.com",
+ desc: "CSP blocks base uri"
+ },
+ { csp: "base-uri https:",
+ base1: "http://mochi.test",
+ base2: "",
+ action: "enforce-csp",
+ result: "http://example.com",
+ desc: "CSP blocks http base"
+ },
+ { csp: "base-uri 'none'",
+ base1: "http://mochi.test",
+ base2: "",
+ action: "enforce-csp",
+ result: "http://example.com",
+ desc: "CSP allows no base modification"
+ },
+ { csp: "",
+ base1: "http://foo:foo/",
+ base2: "",
+ action: "enforce-csp",
+ result: "http://example.com",
+ desc: "Invalid base should be ignored"
+ },
+ { csp: "base-uri http://mochi.test",
+ base1: "http://mochi.test",
+ base2: "http://test1.example.com",
+ action: "remove-base1",
+ result: "http://example.com",
+ desc: "Removing first base should result in fallback base"
+ },
+ { csp: "",
+ base1: "http://mochi.test",
+ base2: "http://test1.example.com",
+ action: "remove-base1",
+ result: "http://test1.example.com",
+ desc: "Removing first base should result in the second base"
+ },
+];
+
+// initializing to -1 so we start at index 0 when we start the test
+var counter = -1;
+
+function finishTest() {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+// a postMessage handler that is used by sandboxed iframes without
+// 'allow-same-origin' to bubble up results back to this main page.
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ var result = event.data.result;
+ // we only care about the base uri, so instead of comparing the complete uri
+ // we just make sure that the base is correct which is sufficient here.
+ ok(result.startsWith(tests[counter].result),
+ `${tests[counter].desc}: Expected a base URI that starts
+ with ${tests[counter].result} but got ${result}`);
+ loadNextTest();
+}
+
+function loadNextTest() {
+ counter++;
+ if (counter == tests.length) {
+ finishTest();
+ return;
+ }
+ var src = "http://example.com/tests/dom/security/test/csp/file_base_uri_server.sjs";
+ // append the CSP that should be used to serve the file
+ // please note that we have to include 'unsafe-inline' to permit sending the postMessage
+ src += "?csp=" + escape("script-src 'unsafe-inline'; " + tests[counter].csp);
+ // append potential base tags
+ src += "&base1=" + escape(tests[counter].base1);
+ src += "&base2=" + escape(tests[counter].base2);
+ // append potential action
+ src += "&action=" + escape(tests[counter].action);
+
+ document.getElementById("testframe").src = src;
+}
+
+// start running the tests
+loadNextTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_blob_data_schemes.html b/dom/security/test/csp/test_blob_data_schemes.html
new file mode 100644
index 0000000000..37a22db050
--- /dev/null
+++ b/dom/security/test/csp/test_blob_data_schemes.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1086999 - Wildcard should not match blob:, data:</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the test:
+ * We load an image using a data: and a blob: scheme and make
+ * sure a CSP containing a single ASTERISK (*) does not allowlist
+ * those loads. The single ASTERISK character should not match a
+ * URI's scheme of a type designating globally unique identifier
+ * (such as blob:, data:, or filesystem:)
+ */
+
+var tests = [
+ {
+ policy : "default-src 'unsafe-inline' blob: data:",
+ expected : "allowed",
+ },
+ {
+ policy : "default-src 'unsafe-inline' *",
+ expected : "blocked"
+ }
+];
+
+var testIndex = 0;
+var messageCounter = 0;
+var curTest;
+
+// onError handler is over-reporting, hence we make sure that
+// we get an error for both testcases: data and blob before we
+// move on to the next test.
+var dataRan = false;
+var blobRan = false;
+
+// a postMessage handler to communicate the results back to the parent.
+window.addEventListener("message", receiveMessage);
+
+function receiveMessage(event)
+{
+ is(event.data.result, curTest.expected, event.data.scheme + " should be " + curTest.expected);
+
+ if (event.data.scheme === "data") {
+ dataRan = true;
+ }
+ if (event.data.scheme === "blob") {
+ blobRan = true;
+ }
+ if (dataRan && blobRan) {
+ loadNextTest();
+ }
+}
+
+function loadNextTest() {
+ if (testIndex === tests.length) {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+ return;
+ }
+
+ dataRan = false;
+ blobRan = false;
+
+ curTest = tests[testIndex++];
+ // reset the messageCounter to make sure we receive all the postMessages from the iframe
+ messageCounter = 0;
+
+ var src = "file_testserver.sjs";
+ // append the file that should be served
+ src += "?file=" + escape("tests/dom/security/test/csp/file_blob_data_schemes.html");
+ // append the CSP that should be used to serve the file
+ src += "&csp=" + escape(curTest.policy);
+
+ document.getElementById("testframe").src = src;
+}
+
+SimpleTest.waitForExplicitFinish();
+loadNextTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_blob_uri_blocks_modals.html b/dom/security/test/csp/test_blob_uri_blocks_modals.html
new file mode 100644
index 0000000000..8d593ea256
--- /dev/null
+++ b/dom/security/test/csp/test_blob_uri_blocks_modals.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1432170 - Block alert box and new window open as per the sandbox
+ allow-scripts CSP</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js">
+ </script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+<script>
+
+/* Description of the test:
+ * We apply the sanbox allow-scripts CSP to the blob iframe and check
+ * if the alert box and new window open is blocked correctly by the CSP.
+ */
+var testsToRun = {
+ block_window_open_test: false,
+ block_alert_test: false,
+ block_top_nav_alert_test: false,
+};
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("have to test that alert dialogue is blocked");
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ switch (event.data.test) {
+ case "block_window_open_test":
+ testsToRun.block_window_open_test = true;
+ break;
+ case "block_alert_test":
+ is(event.data.msg, "alert blocked by CSP", "alert blocked by CSP");
+ testsToRun.block_alert_test = true;
+ break;
+ case "block_top_nav_alert_test":
+ testsToRun.block_top_nav_alert_test = true;
+ break;
+ }
+}
+
+var w;
+document.getElementById("testframe").src = "file_blob_uri_blocks_modals.html";
+w = window.open("file_blob_top_nav_block_modals.html");
+
+
+// If alert window is not blocked by CSP then event message is not recieved and
+// test fails after setTimeout interval of 1 second.
+setTimeout(function () {
+ is(testsToRun.block_top_nav_alert_test, true,
+ "blob top nav alert should be blocked by CSP");
+ testsToRun.block_top_nav_alert_test = true;
+ is(testsToRun.block_alert_test, true,
+ "alert should be blocked by CSP");
+ testsToRun.block_alert_test = true;
+ checkTestsCompleted();
+ },1000);
+
+function checkTestsCompleted() {
+ for (var prop in testsToRun) {
+ // some test hasn't run yet so we're not done
+ if (!testsToRun[prop]) {
+ return;
+ }
+ }
+ window.removeEventListener("message", receiveMessage);
+ w.close();
+ SimpleTest.finish();
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_block_all_mixed_content.html b/dom/security/test/csp/test_block_all_mixed_content.html
new file mode 100644
index 0000000000..d60f904b6c
--- /dev/null
+++ b/dom/security/test/csp/test_block_all_mixed_content.html
@@ -0,0 +1,99 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1122236 - CSP: Implement block-all-mixed-content</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the tests:
+ * Test 1:
+ * We load mixed display content in a frame using the CSP
+ * directive 'block-all-mixed-content' and observe that the image is blocked.
+ *
+ * Test 2:
+ * We load mixed display content in a frame using a CSP that allows the load
+ * and observe that the image is loaded.
+ *
+ * Test 3:
+ * We load mixed display content in a frame not using a CSP at all
+ * and observe that the image is loaded.
+ *
+ * Test 4:
+ * We load mixed display content in a frame using the CSP
+ * directive 'block-all-mixed-content' and observe that the image is blocked.
+ * Please note that Test 3 loads the image we are about to load in Test 4 into
+ * the img cache. Let's make sure the cached (mixed display content) image is
+ * not allowed to be loaded.
+ */
+
+const BASE_URI = "https://example.com/tests/dom/security/test/csp/";
+
+const tests = [
+ { // Test 1
+ query: "csp-block",
+ expected: "img-blocked",
+ description: "(csp-block) block-all-mixed content should block mixed display content"
+ },
+ { // Test 2
+ query: "csp-allow",
+ expected: "img-loaded",
+ description: "(csp-allow) mixed display content should be loaded"
+ },
+ { // Test 3
+ query: "no-csp",
+ expected: "img-loaded",
+ description: "(no-csp) mixed display content should be loaded"
+ },
+ { // Test 4
+ query: "csp-block",
+ expected: "img-blocked",
+ description: "(csp-block) block-all-mixed content should block insecure cache loads"
+ },
+ { // Test 5
+ query: "cspro-block",
+ expected: "img-loaded",
+ description: "(cspro-block) block-all-mixed in report only mode should not block"
+ },
+];
+
+var curTest;
+var counter = -1;
+
+function checkResults(result) {
+ is(result, curTest.expected, curTest.description);
+ loadNextTest();
+}
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ checkResults(event.data.result);
+}
+
+function loadNextTest() {
+ counter++;
+ if (counter == tests.length) {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+ return;
+ }
+ curTest = tests[counter];
+ testframe.src = BASE_URI + "file_block_all_mcb.sjs?" + curTest.query;
+}
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv(
+ { 'set': [["security.mixed_content.block_display_content", false]] },
+ function() { loadNextTest(); }
+);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_block_all_mixed_content_frame_navigation.html b/dom/security/test/csp/test_block_all_mixed_content_frame_navigation.html
new file mode 100644
index 0000000000..b32c1fccd5
--- /dev/null
+++ b/dom/security/test/csp/test_block_all_mixed_content_frame_navigation.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1122236 - CSP: Implement block-all-mixed-content</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the test:
+ *
+ * http://a.com embeds https://b.com.
+ * https://b.com has a CSP using 'block-all-mixed-content'.
+ * | site | http://a.com
+ * | embeds | https://b.com (uses block-all-mixed-content)
+ *
+ * The user navigates the embedded frame from
+ * https://b.com -> http://c.com.
+ * The test makes sure that such a navigation is not blocked
+ * by block-all-mixed-content.
+ */
+
+function checkResults(result) {
+ is(result, "frame-navigated", "frame should be allowed to be navigated");
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ checkResults(event.data.result);
+}
+
+SimpleTest.waitForExplicitFinish();
+// http://a.com loads https://b.com
+document.getElementById("testframe").src =
+ "https://example.com/tests/dom/security/test/csp/file_block_all_mixed_content_frame_navigation1.html";
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_blocked_uri_in_reports.html b/dom/security/test/csp/test_blocked_uri_in_reports.html
new file mode 100644
index 0000000000..f40d98efc5
--- /dev/null
+++ b/dom/security/test/csp/test_blocked_uri_in_reports.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1069762 - Check blocked-uri in csp-reports after redirect</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<iframe style="width:200px;height:200px;" id='cspframe'></iframe>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+/* Description of the test:
+ * We try to load a script from:
+ * http://example.com/tests/dom/security/test/csp/file_path_matching_redirect_server.sjs
+ * which gets redirected to:
+ * http://test1.example.com/tests/dom/security//test/csp/file_path_matching.js
+ *
+ * The blocked-uri in the csp-report should be the original URI:
+ * http://example.com/tests/dom/security/test/csp/file_path_matching_redirect_server.sjs
+ * instead of the redirected URI:
+ * http://test1.example.com/tests/com/security/test/csp/file_path_matching.js
+ *
+ * see also: http://www.w3.org/TR/CSP/#violation-reports
+ *
+ * Note, that we reuse the test-setup from
+ * test_path_matching_redirect.html
+ */
+
+const reportURI = "http://mochi.test:8888/foo.sjs";
+const policy = "script-src http://example.com; report-uri " + reportURI;
+const testfile = "tests/dom/security/test/csp/file_path_matching_redirect.html";
+
+var chromeScriptUrl = SimpleTest.getTestFileURL("file_report_chromescript.js");
+var script = SpecialPowers.loadChromeScript(chromeScriptUrl);
+
+script.addMessageListener('opening-request-completed', function ml(msg) {
+ if (msg.error) {
+ ok(false, "Could not query report (exception: " + msg.error + ")");
+ } else {
+ try {
+ var reportObj = JSON.parse(msg.report);
+ } catch (e) {
+ ok(false, "Could not parse JSON (exception: " + e + ")");
+ }
+ try {
+ var cspReport = reportObj["csp-report"];
+ // blocked-uri should only be the asciiHost instead of:
+ // http://test1.example.com/tests/dom/security/test/csp/file_path_matching.js
+ is(cspReport["blocked-uri"], "http://example.com/tests/dom/security/test/csp/file_path_matching_redirect_server.sjs", "Incorrect blocked-uri");
+ } catch (e) {
+ ok(false, "Could not query report (exception: " + e + ")");
+ }
+ }
+
+ script.removeMessageListener('opening-request-completed', ml);
+ script.sendAsyncMessage("finish");
+ SimpleTest.finish();
+});
+
+SimpleTest.waitForExplicitFinish();
+
+function runTest() {
+ var src = "file_testserver.sjs";
+ // append the file that should be served
+ src += "?file=" + escape(testfile);
+ // append the CSP that should be used to serve the file
+ src += "&csp=" + escape(policy);
+
+ document.getElementById("cspframe").src = src;
+}
+
+runTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_blocked_uri_in_violation_event_after_redirects.html b/dom/security/test/csp/test_blocked_uri_in_violation_event_after_redirects.html
new file mode 100644
index 0000000000..6965cbeb92
--- /dev/null
+++ b/dom/security/test/csp/test_blocked_uri_in_violation_event_after_redirects.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1542194 - Check blockedURI in violation reports after redirects</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<iframe id='testframe'></iframe>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+let seenViolations = 0;
+let expectedViolations = 3;
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+
+ seenViolations++;
+
+ let blockedURI = event.data.blockedURI;
+
+ if (blockedURI.includes("test1")) {
+ is(blockedURI,
+ "http://example.com/tests/dom/security/test/csp/file_blocked_uri_in_violation_event_after_redirects.sjs?test1a",
+ "Test 1 should be the URI before redirect");
+ } else if (blockedURI.includes("test2")) {
+ is(blockedURI,
+ "http://test2.example.com",
+ "Test 2 should be the redirected pre-path URI");
+ } else if (blockedURI.includes("test3")) {
+ is(blockedURI,
+ "http://test3.example.com",
+ "Test 3 should be the redirected pre-path URI");
+ } else {
+ ok(false, "sanity: how can we end up here?");
+ }
+
+ if (seenViolations < expectedViolations) {
+ return;
+ }
+
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+let testframe = document.getElementById("testframe");
+// This has to be same-origin with the test1 URL.
+testframe.src = "http://example.com/tests/dom/security/test/csp/file_blocked_uri_in_violation_event_after_redirects.html";
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_blocked_uri_redirect_frame_src.html b/dom/security/test/csp/test_blocked_uri_redirect_frame_src.html
new file mode 100644
index 0000000000..a946718bc2
--- /dev/null
+++ b/dom/security/test/csp/test_blocked_uri_redirect_frame_src.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1687342 - Check blocked-uri in csp-reports after frame redirect</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<iframe id='testframe'></iframe>
+
+<script class="testbody" type="text/javascript">
+
+ /* Description of the test:
+ * We load a document from http://mochi.test with a CSP of `frame-src example.com`.
+ * We then load an iframe from example.com which redirects to test1.example.com and
+ * ensure that the report-uri is the origin of the frame before the blocked redirect.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+const BLOCKED_URI = "http://example.com";
+
+var chromeScriptUrl = SimpleTest.getTestFileURL("file_report_chromescript.js");
+var script = SpecialPowers.loadChromeScript(chromeScriptUrl);
+
+script.addMessageListener('opening-request-completed', function ml(msg) {
+ if (msg.error) {
+ ok(false, "Could not query report (exception: " + msg.error + ")");
+ return;
+ }
+ try {
+ var reportObj = JSON.parse(msg.report);
+ } catch (e) {
+ ok(false, "Could not parse JSON (exception: " + e + ")");
+ }
+ try {
+ var cspReport = reportObj["csp-report"];
+ is(cspReport["blocked-uri"], BLOCKED_URI, "Incorrect blocked-uri");
+ } catch (e) {
+ ok(false, "Could not query report (exception: " + e + ")");
+ }
+
+ script.removeMessageListener('opening-request-completed', ml);
+ script.sendAsyncMessage("finish");
+ SimpleTest.finish();
+});
+
+SimpleTest.waitForExplicitFinish();
+
+function runTest() {
+ let testframe = document.getElementById("testframe");
+ testframe.src = "file_blocked_uri_redirect_frame_src.html";
+}
+
+runTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_bug1229639.html b/dom/security/test/csp/test_bug1229639.html
new file mode 100644
index 0000000000..e224fe1ffb
--- /dev/null
+++ b/dom/security/test/csp/test_bug1229639.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1229639 - Percent encoded CSP path matching.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+
+<iframe style="width:200px;height:200px;" id='cspframe'></iframe>
+<script class="testbody" type="text/javascript">
+
+// This is used to watch the blocked data bounce off CSP and allowed data
+// get sent out to the wire.
+function examiner() {
+ SpecialPowers.addObserver(this, "csp-on-violate-policy");
+ SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
+}
+
+examiner.prototype = {
+ observe(subject, topic, data) {
+ if (data === 'http://mochi.test:8888/tests/dom/security/test/csp/%24.js') {
+ is(topic, "specialpowers-http-notify-request");
+ this.remove();
+ SimpleTest.finish();
+ }
+ },
+
+ // must eventually call this to remove the listener,
+ // or mochitests might get borked.
+ remove() {
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
+ }
+}
+
+window.examiner = new examiner();
+
+SimpleTest.waitForExplicitFinish();
+
+// save this for last so that our listeners are registered.
+// ... this loads the testbed of good and bad requests.
+document.getElementById('cspframe').src = 'file_bug1229639.html';
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_bug1242019.html b/dom/security/test/csp/test_bug1242019.html
new file mode 100644
index 0000000000..14e8f74baa
--- /dev/null
+++ b/dom/security/test/csp/test_bug1242019.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1242019
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1242019</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1242019">Mozilla Bug 1242019</a>
+<p id="display"></p>
+
+<iframe id="cspframe"></iframe>
+
+<pre id="test">
+
+<script class="testbody" type="text/javascript">
+function cleanup() {
+ SpecialPowers.postConsoleSentinel();
+ SimpleTest.finish();
+};
+
+var expectedURI = "data:image/png;base64,iVBORw0KGgoAAAANSU"
+
+SpecialPowers.registerConsoleListener(function ConsoleMsgListener(aMsg) {
+ // look for the message with data uri and see the data uri is truncated to 40 chars
+ data_start = aMsg.message.indexOf(expectedURI)
+ if (data_start > -1) {
+ data_uri = "";
+ data_uri = aMsg.message.substr(data_start);
+ // this will either match the elipsis after the URI or the . at the end of the message
+ data_uri = data_uri.substr(0, data_uri.indexOf("…"));
+ if (data_uri == "") {
+ return;
+ }
+
+ ok(data_uri.length == 40, "Data URI only shows 40 characters in the console");
+ SimpleTest.executeSoon(cleanup);
+ }
+});
+
+// set up and start testing
+SimpleTest.waitForExplicitFinish();
+document.getElementById('cspframe').src = 'file_data-uri_blocked.html';
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_bug1312272.html b/dom/security/test/csp/test_bug1312272.html
new file mode 100644
index 0000000000..b06b08d092
--- /dev/null
+++ b/dom/security/test/csp/test_bug1312272.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+
+ <title>Test for bug 1312272</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe id="cspframe" style="width:100%"></iframe>
+
+<script type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+function handler(evt) {
+ console.log(evt);
+ if (evt.data === "finish") {
+ ok(true, 'Other events continue to work fine.')
+ SimpleTest.finish();
+ //removeEventListener('message', handler);
+ } else {
+ ok(false, "Should not get any other message")
+ }
+}
+var cspframe = document.getElementById("cspframe");
+cspframe.src = "file_bug1312272.html";
+addEventListener("message", handler);
+console.log("assignign frame");
+</script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/test_bug1388015.html b/dom/security/test/csp/test_bug1388015.html
new file mode 100644
index 0000000000..5ca0605688
--- /dev/null
+++ b/dom/security/test/csp/test_bug1388015.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <title>Bug 1388015 - Test if Firefox respect Port in Wildcard Host </title>
+ <meta http-equiv="Content-Security-Policy" content="img-src https://*:443">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+
+ <img alt="Should be Blocked">
+ <script class="testbody" type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ let image = document.querySelector("img");
+
+ Promise.race([
+ new Promise((res) => {
+ window.addEventListener("securitypolicyviolation", () => res(true), {once:true});
+ }),
+ new Promise((res) => {
+ image.addEventListener("load", () => res(false),{once:true});
+ })])
+ .then((result) => {
+ ok(result, " CSP did block Image with wildcard and mismatched Port");
+ })
+ .then(()=> Promise.race([
+ new Promise((res) => {
+ window.addEventListener("securitypolicyviolation", () => res(false), {once:true});
+ }),
+ new Promise((res) => {
+ image.addEventListener("load", () => res(true),{once:true});
+ requestIdleCallback(()=>{
+ image.src = "https://example.com:443/tests/dom/security/test/csp/file_dummy_pixel.png"
+ })
+ })]))
+ .then((result) => {
+ ok(result, " CSP did load the Image with wildcard and matching Port");
+ SimpleTest.finish();
+ })
+ image.src = "file_dummy_pixel.png" // mochi.test:8888
+ </script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_bug1452037.html b/dom/security/test/csp/test_bug1452037.html
new file mode 100644
index 0000000000..fa46e91291
--- /dev/null
+++ b/dom/security/test/csp/test_bug1452037.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test if "script-src: sha-... " Allowlists "javascript:" URIs</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <iframe></iframe>
+
+<script class="testbody">
+ SimpleTest.requestCompleteLog();
+ SimpleTest.waitForExplicitFinish();
+
+ let frame = document.querySelector("iframe");
+
+ window.addEventListener("message", (msg) => {
+ ok(false, "The CSP did not block javascript:uri");
+ SimpleTest.finish();
+ });
+
+ document.addEventListener("securitypolicyviolation", () => {
+ ok(true, "The CSP did block javascript:uri");
+ SimpleTest.finish();
+ });
+
+ frame.addEventListener("load", () => {
+ let link = frame.contentWindow.document.querySelector("a");
+ frame.contentWindow.document.addEventListener("securitypolicyviolation", () => {
+ ok(true, "The CSP did block javascript:uri");
+ SimpleTest.finish();
+ })
+ link.click();
+ });
+ frame.src = "file_bug1452037.html";
+
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_bug1505412.html b/dom/security/test/csp/test_bug1505412.html
new file mode 100644
index 0000000000..717af2054b
--- /dev/null
+++ b/dom/security/test/csp/test_bug1505412.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <title> Bug 1505412 CSP-RO reports violations in inline-scripts with nonce</title>
+ <script src="/tests/SimpleTest/SimpleTest.js" nonce="foobar"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1505412">Test for 1505412 </a>
+ <script class="testbody" type="text/javascript" nonce="foobar">
+ /* Description of the test:
+ 1: We setup a Proxy that will cause the Test to Fail
+ if Firefox sends a CSP-Report to /report
+ 2: We Load an iframe with has a Script pointing to
+ file_bug1505412.sjs
+ 3: The Preloader will fetch the file and Gets redirected
+ 4: If correct, the File should be loaded and no CSP-Report
+ should be send.
+ */
+
+
+
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.requestCompleteLog();
+ SimpleTest.requestLongerTimeout(2); // Or might fail for Linux-Debug in some cases.
+ var script;
+
+ window.addEventListener("load",()=>{
+ let t = document.querySelector("#target");
+ t.src = "file_bug1505412_frame.html";
+ t.addEventListener("load",async () => {
+ let reportCount = await fetch("file_bug1505412_reporter.sjs?state").then(r => r.text());
+ info(reportCount);
+ ok(reportCount == 0 , "Script Loaded without CSP beeing triggered");
+ await fetch("file_bug1505412_reporter.sjs?flush");
+ SimpleTest.finish();
+ });
+ })
+
+ </script>
+ <iframe id="target" frameborder="0"></iframe>
+</body>
+
+</html> \ No newline at end of file
diff --git a/dom/security/test/csp/test_bug1579094.html b/dom/security/test/csp/test_bug1579094.html
new file mode 100644
index 0000000000..b3568586d4
--- /dev/null
+++ b/dom/security/test/csp/test_bug1579094.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test if Wildcard CSP supports ExternalProtocol</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <meta meta http-equiv="Content-security-policy" content="frame-src SomeExternalProto://*">
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+<script class="testbody" type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ document.addEventListener("securitypolicyviolation",()=>{
+ ok(false, "Error: ExternalProtocol Was blocked");
+ SimpleTest.finish();
+ });
+
+ window.addEventListener("load", ()=>{
+ ok(true, "Error: ExternalProtocol was passed");
+ SimpleTest.finish();
+ });
+</script>
+
+<iframe src="SomeExternalProto:foo@bar.com">
+
+
+</body>
+</html>
diff --git a/dom/security/test/csp/test_bug1738418.html b/dom/security/test/csp/test_bug1738418.html
new file mode 100644
index 0000000000..9fdc723b80
--- /dev/null
+++ b/dom/security/test/csp/test_bug1738418.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1738418: CSP sandbox for embed/object frames</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe id="testframe"></iframe>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var elements = new Set(["iframe", "embed", "object"]);
+
+window.addEventListener("message", event => {
+ is(event.data.domain, "", `document in <${event.data.element}> should have sandboxed origin`);
+ elements.delete(event.data.element);
+ if (elements.size == 0) {
+ SimpleTest.finish();
+ }
+});
+
+document.getElementById("testframe").src = "file_bug1738418_parent.html";
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_bug1764343.html b/dom/security/test/csp/test_bug1764343.html
new file mode 100644
index 0000000000..1af9a710fe
--- /dev/null
+++ b/dom/security/test/csp/test_bug1764343.html
@@ -0,0 +1,116 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1764343 - CSP inheritance for same-origin iframes</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+
+ <meta http-equiv="Content-Security-Policy" content="style-src 'unsafe-inline'; script-src 'nonce-parent' 'nonce-a' 'nonce-b' 'nonce-c'; img-src 'self' data:">
+</head>
+<body>
+ <iframe id="sameOriginMetaFrame"></iframe>
+ <iframe id="aboutBlankMetaFrame"></iframe>
+<script nonce='parent'>
+SimpleTest.waitForExplicitFinish();
+
+const NEW_HTML =`
+ <head>
+ <meta http-equiv="Content-Security-Policy" content="script-src 'nonce-a' 'nonce-c' 'nonce-d';">
+ </head>
+ <body>
+ <style>
+ body { background-color: rgb(255, 0, 0); }
+ </style>
+ <script nonce="a">
+ document.a = true;
+ <\/script>
+ <script nonce="b">
+ document.b = true;
+ <\/script>
+ <script nonce="c">
+ document.c = true;
+ <\/script>
+ <script nonce="d">
+ document.d = true;
+ <\/script>
+ <img id="testInlineImage"></img>
+ </body>
+ `;
+
+// test file's CSP meta tags shouldn't overwrite same-origin iframe's CSP meta tags
+async function testBlocked() {
+ info("testBlocked");
+
+ let sameOriginMetaFrame = document.getElementById("sameOriginMetaFrame");
+ let onFrameLoad = new Promise(resolve => {
+ sameOriginMetaFrame.addEventListener('load', resolve, {once: true});
+ });
+ sameOriginMetaFrame.src = 'file_bug1764343.html';
+ await onFrameLoad;
+
+ let doc = sameOriginMetaFrame.contentDocument;
+ doc.open();
+ doc.write(NEW_HTML);
+
+ let bgcolor = window.getComputedStyle(doc.body).getPropertyValue("background-color");
+ is(bgcolor, "rgba(0, 0, 0, 0)", "inital background value in FF should be 'transparent'");
+
+ let img = doc.getElementById("testInlineImage");
+ let onImgError = new Promise(resolve => {
+ img.addEventListener('error', resolve, {once: true});
+ });
+ img.src = "//mochi.test:8888/tests/image/test/mochitest/blue.png";
+ await onImgError;
+ is(img.complete, false, "image should not be loaded");
+
+ // Make sure that CSP policy can further restrict (no 'nonce-b'), but not weak (adding 'nonce-c' or 'nonce-d')
+ is(doc.a, true, "doc.a should be true (script 'nonce-a' allowed)");
+ is(doc.b, undefined, "doc.b should be undefined (script 'nonce-b' blocked)");
+ is(doc.c, undefined, "doc.c should be undefined (script 'nonce-c' blocked)");
+ is(doc.d, undefined, "doc.d should be undefined (script 'nonce-d' blocked)");
+}
+
+ // test file's CSP meta tags should apply to about blank iframe's CSP meta tags
+async function testNotBlocked() {
+ info("testNotBlocked");
+
+ let aboutBlankMetaFrame = document.getElementById("aboutBlankMetaFrame");
+ let onFrameLoad = new Promise(resolve => {
+ aboutBlankMetaFrame.addEventListener('load', resolve, {once: true});
+ });
+ aboutBlankMetaFrame.src = 'about:blank';
+ await onFrameLoad;
+
+ let doc = aboutBlankMetaFrame.contentDocument;
+ doc.open();
+ doc.write(NEW_HTML);
+
+ let bgcolor = window.getComputedStyle(doc.body).getPropertyValue("background-color");
+ is(bgcolor, "rgb(255, 0, 0)", "background value should be updated to red");
+
+ let img = doc.getElementById("testInlineImage");
+ let onImgLoad = new Promise(resolve => {
+ img.addEventListener('load', resolve, {once: true});
+ });
+ img.src = "//mochi.test:8888/tests/image/test/mochitest/blue.png";
+ await onImgLoad;
+ is(img.complete, true, "image should be loaded");
+
+ // New HTML contains 'nonce-a/c/d' and no CSP in about:blank.
+ // (Can not weaken parent with 'nonce-d')
+ is(doc.a, true, "doc.a should be true (script 'nonce-a' allowed)");
+ is(doc.b, undefined, "doc.b should be undefined (script 'nonce-b' blocked)");
+ is(doc.c, true, "doc.c should be true (script 'nonce-c' allowed)");
+ is(doc.d, undefined, "doc.d should be true (script 'nonce-d' blocked)");
+}
+
+(async function () {
+ await testBlocked();
+ await testNotBlocked();
+ SimpleTest.finish();
+})();
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_bug1777572.html b/dom/security/test/csp/test_bug1777572.html
new file mode 100644
index 0000000000..f735f4fb6a
--- /dev/null
+++ b/dom/security/test/csp/test_bug1777572.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>bug 1777572</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+
+ async function testCSPInheritance(closeOpenerWindow) {
+ let url = "file_bug1777572.html";
+ if (closeOpenerWindow) {
+ url += "?close";
+ }
+ let win = window.open(url);
+ return new Promise((resolve) => {
+ window.addEventListener("message", (event) => {
+ ok(event.data.includes("img-src"), "Got expected data " + event.data);
+ resolve();
+ }, { once: true});
+ });
+ }
+
+ async function run() {
+ // Test that CSP inheritance to the initial about:blank works the same way
+ // whether or not the opener is already closed when window.open is called.
+ await testCSPInheritance(false);
+ await testCSPInheritance(true);
+ SimpleTest.finish();
+ }
+
+ </script>
+</head>
+<body onload="run()">
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_bug663567.html b/dom/security/test/csp/test_bug663567.html
new file mode 100644
index 0000000000..137d459654
--- /dev/null
+++ b/dom/security/test/csp/test_bug663567.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test if XSLT stylesheet is subject to document's CSP</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <iframe style="width:100%;" id='xsltframe'></iframe>
+ <iframe style="width:100%;" id='xsltframe2'></iframe>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// define the expected output of this test
+var header = "this xml file should be formatted using an xsl file(lower iframe should contain xml dump)!";
+
+var finishedTests = 0;
+var numberOfTests = 2;
+
+var checkExplicitFinish = function() {
+ finishedTests++;
+ if (finishedTests == numberOfTests) {
+ SimpleTest.finish();
+ }
+}
+
+function checkAllowed () {
+ /* The policy for this test is:
+ * Content-Security-Policy: default-src 'self'
+ *
+ * we load the xsl file using:
+ * <?xml-stylesheet type="text/xsl" href="file_bug663467.xsl"?>
+ */
+ try {
+ var cspframe = document.getElementById('xsltframe');
+ var xsltAllowedHeader = cspframe.contentWindow.document.getElementById('xsltheader').innerHTML;
+ is(xsltAllowedHeader, header, "XSLT loaded from 'self' should be allowed!");
+ }
+ catch (e) {
+ ok(false, "Error: could not access content in xsltframe!")
+ }
+ checkExplicitFinish();
+}
+
+function checkBlocked () {
+ /* The policy for this test is:
+ * Content-Security-Policy: default-src *.example.com
+ *
+ * we load the xsl file using:
+ * <?xml-stylesheet type="text/xsl" href="file_bug663467.xsl"?>
+ */
+ try {
+ var cspframe = document.getElementById('xsltframe2');
+ var xsltBlockedHeader = cspframe.contentWindow.document.getElementById('xsltheader');
+ is(xsltBlockedHeader, null, "XSLT loaded from different host should be blocked!");
+ }
+ catch (e) {
+ ok(false, "Error: could not access content in xsltframe2!")
+ }
+ checkExplicitFinish();
+}
+
+document.getElementById('xsltframe').addEventListener('load', checkAllowed);
+document.getElementById('xsltframe').src = 'file_bug663567_allows.xml';
+
+document.getElementById('xsltframe2').addEventListener('load', checkBlocked);
+document.getElementById('xsltframe2').src = 'file_bug663567_blocks.xml';
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_bug802872.html b/dom/security/test/csp/test_bug802872.html
new file mode 100644
index 0000000000..956159ddcc
--- /dev/null
+++ b/dom/security/test/csp/test_bug802872.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 802872</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <iframe style="width:100%;" id='eventframe'></iframe>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var finishedTests = 0;
+var numberOfTests = 2;
+
+var checkExplicitFinish = function () {
+ finishedTests++;
+ if (finishedTests == numberOfTests) {
+ SimpleTest.finish();
+ }
+}
+
+// add event listeners for CSP-permitted EventSrc callbacks
+addEventListener('allowedEventSrcCallbackOK', function (e) {
+ ok(true, "OK: CSP allows EventSource for allowlisted domain!");
+ checkExplicitFinish();
+}, false);
+addEventListener('allowedEventSrcCallbackFailed', function (e) {
+ ok(false, "Error: CSP blocks EventSource for allowlisted domain!");
+ checkExplicitFinish();
+}, false);
+
+// add event listeners for CSP-blocked EventSrc callbacks
+addEventListener('blockedEventSrcCallbackOK', function (e) {
+ ok(false, "Error: CSP allows EventSource to not allowlisted domain!");
+ checkExplicitFinish();
+}, false);
+addEventListener('blockedEventSrcCallbackFailed', function (e) {
+ ok(true, "OK: CSP blocks EventSource for not allowlisted domain!");
+ checkExplicitFinish();
+}, false);
+
+// load it
+document.getElementById('eventframe').src = 'file_bug802872.html';
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_bug836922_npolicies.html b/dom/security/test/csp/test_bug836922_npolicies.html
new file mode 100644
index 0000000000..e418969e3d
--- /dev/null
+++ b/dom/security/test/csp/test_bug836922_npolicies.html
@@ -0,0 +1,235 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Content Security Policy multiple policy support (regular and Report-Only mode)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<iframe style="width:200px;height:200px;" id='cspframe'></iframe>
+<script class="testbody" type="text/javascript">
+
+var path = "/tests/dom/security/test/csp/";
+
+// These are test results: verified indicates whether or not the test has run.
+// true/false is the pass/fail result.
+window.loads = {
+ css_self: {expected: true, verified: false},
+ img_self: {expected: false, verified: false},
+ script_self: {expected: true, verified: false},
+};
+
+window.violation_reports = {
+ css_self:
+ {expected: 0, expected_ro: 0}, /* totally fine */
+ img_self:
+ {expected: 1, expected_ro: 0}, /* violates enforced CSP */
+ script_self:
+ {expected: 0, expected_ro: 1}, /* violates report-only */
+};
+
+// This is used to watch the blocked data bounce off CSP and allowed data
+// get sent out to the wire. This also watches for violation reports to go out.
+function examiner() {
+ SpecialPowers.addObserver(this, "csp-on-violate-policy");
+ SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
+}
+examiner.prototype = {
+ observe(subject, topic, data) {
+ var testpat = new RegExp("testid=([a-z0-9_]+)");
+
+ if (topic === "specialpowers-http-notify-request") {
+ var uri = data;
+ if (!testpat.test(uri)) return;
+ var testid = testpat.exec(uri)[1];
+
+ // violation reports don't come through here, but the requested resources do
+ // if the test has already finished, move on. Some things throw multiple
+ // requests (preloads and such)
+ try {
+ if (window.loads[testid].verified) return;
+ } catch(e) { return; }
+
+ // these are requests that were allowed by CSP
+ var testid = testpat.exec(uri)[1];
+ window.testResult(testid, 'allowed', uri + " allowed by csp");
+ }
+
+ if(topic === "csp-on-violate-policy") {
+ // if the violated policy was report-only, the resource will still be
+ // loaded even if this topic is notified.
+ var asciiSpec = SpecialPowers.getPrivilegedProps(
+ SpecialPowers.do_QueryInterface(subject, "nsIURI"),
+ "asciiSpec");
+ if (!testpat.test(asciiSpec)) return;
+ var testid = testpat.exec(asciiSpec)[1];
+
+ // if the test has already finished, move on.
+ try {
+ if (window.loads[testid].verified) return;
+ } catch(e) { return; }
+
+ // record the ones that were supposed to be blocked, but don't use this
+ // as an indicator for tests that are not blocked but do generate reports.
+ // We skip recording the result if the load is expected since a
+ // report-only policy will generate a request *and* a violation note.
+ if (!window.loads[testid].expected) {
+ window.testResult(testid,
+ 'blocked',
+ asciiSpec + " blocked by \"" + data + "\"");
+ }
+ }
+
+ // if any test is unverified, keep waiting
+ for (var v in window.loads) {
+ if(!window.loads[v].verified) {
+ return;
+ }
+ }
+
+ window.bug836922examiner.remove();
+ window.resultPoller.pollForFinish();
+ },
+
+ // must eventually call this to remove the listener,
+ // or mochitests might get borked.
+ remove() {
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
+ }
+}
+window.bug836922examiner = new examiner();
+
+
+// Poll for results and see if enough reports came in. Keep trying
+// for a few seconds before failing with lack of reports.
+// Have to do this because there's a race between the async reporting
+// and this test finishing, and we don't want to win the race.
+window.resultPoller = {
+
+ POLL_ATTEMPTS_LEFT: 14,
+
+ pollForFinish() {
+ var vr = resultPoller.tallyReceivedReports();
+ if (resultPoller.verifyReports(vr, resultPoller.POLL_ATTEMPTS_LEFT < 1)) {
+ // report success condition.
+ resultPoller.resetReportServer();
+ SimpleTest.finish();
+ } else {
+ resultPoller.POLL_ATTEMPTS_LEFT--;
+ // try again unless we reached the threshold.
+ setTimeout(resultPoller.pollForFinish, 100);
+ }
+ },
+
+ resetReportServer() {
+ var xhr = new XMLHttpRequest();
+ var xhr_ro = new XMLHttpRequest();
+ xhr.open("GET", "file_bug836922_npolicies_violation.sjs?reset", false);
+ xhr_ro.open("GET", "file_bug836922_npolicies_ro_violation.sjs?reset", false);
+ xhr.send(null);
+ xhr_ro.send(null);
+ },
+
+ tallyReceivedReports() {
+ var xhr = new XMLHttpRequest();
+ var xhr_ro = new XMLHttpRequest();
+ xhr.open("GET", "file_bug836922_npolicies_violation.sjs?results", false);
+ xhr_ro.open("GET", "file_bug836922_npolicies_ro_violation.sjs?results", false);
+ xhr.send(null);
+ xhr_ro.send(null);
+
+ var received = JSON.parse(xhr.responseText);
+ var received_ro = JSON.parse(xhr_ro.responseText);
+
+ var results = {enforced: {}, reportonly: {}};
+ for (var r in window.violation_reports) {
+ results.enforced[r] = 0;
+ results.reportonly[r] = 0;
+ }
+
+ for (var r in received) {
+ results.enforced[r] += received[r];
+ }
+ for (var r in received_ro) {
+ results.reportonly[r] += received_ro[r];
+ }
+
+ return results;
+ },
+
+ verifyReports(receivedCounts, lastAttempt) {
+ for (var r in window.violation_reports) {
+ var exp = window.violation_reports[r].expected;
+ var exp_ro = window.violation_reports[r].expected_ro;
+ var rec = receivedCounts.enforced[r];
+ var rec_ro = receivedCounts.reportonly[r];
+
+ // if this test breaks, these are helpful dumps:
+ //dump(">>> Verifying " + r + "\n");
+ //dump(" > Expected: " + exp + " / " + exp_ro + " (ro)\n");
+ //dump(" > Received: " + rec + " / " + rec_ro + " (ro) \n");
+
+ // in all cases, we're looking for *at least* the expected number of
+ // reports of each type (there could be more in some edge cases).
+ // If there are not enough, we keep waiting and poll the server again
+ // later. If there are enough, we can successfully finish.
+
+ if (exp == 0)
+ is(rec, 0,
+ "Expected zero enforced-policy violation " +
+ "reports for " + r + ", got " + rec);
+ else if (lastAttempt)
+ ok(rec >= exp,
+ "Received (" + rec + "/" + exp + ") " +
+ "enforced-policy reports for " + r);
+ else if (rec < exp)
+ return false; // continue waiting for more
+
+ if(exp_ro == 0)
+ is(rec_ro, 0,
+ "Expected zero report-only-policy violation " +
+ "reports for " + r + ", got " + rec_ro);
+ else if (lastAttempt)
+ ok(rec_ro >= exp_ro,
+ "Received (" + rec_ro + "/" + exp_ro + ") " +
+ "report-only-policy reports for " + r);
+ else if (rec_ro < exp_ro)
+ return false; // continue waiting for more
+ }
+
+ // if we complete the loop, we've found all of the violation
+ // reports we expect.
+ if (lastAttempt) return true;
+
+ // Repeat successful tests once more to record successes via ok()
+ return resultPoller.verifyReports(receivedCounts, true);
+ }
+};
+
+window.testResult = function(testname, result, msg) {
+ // otherwise, make sure the allowed ones are expected and blocked ones are not.
+ if (window.loads[testname].expected) {
+ is(result, 'allowed', ">> " + msg);
+ } else {
+ is(result, 'blocked', ">> " + msg);
+ }
+ window.loads[testname].verified = true;
+}
+
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+
+// save this for last so that our listeners are registered.
+// ... this loads the testbed of good and bad requests.
+document.getElementById('cspframe').src = 'http://mochi.test:8888' + path + 'file_bug836922_npolicies.html';
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_bug885433.html b/dom/security/test/csp/test_bug885433.html
new file mode 100644
index 0000000000..c7c17d25b6
--- /dev/null
+++ b/dom/security/test/csp/test_bug885433.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Content Security Policy inline stylesheets stuff</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<iframe style="width:100%;" id='cspframe'></iframe>
+<iframe style="width:100%;" id='cspframe2'></iframe>
+<script class="testbody" type="text/javascript">
+
+//////////////////////////////////////////////////////////////////////
+// set up and go
+SimpleTest.waitForExplicitFinish();
+
+// utilities for check functions
+// black means the style wasn't applied, applied styles are green
+var green = 'rgb(0, 128, 0)';
+var black = 'rgb(0, 0, 0)';
+
+// We test both script and style execution by observing changes in computed styles
+function checkAllowed () {
+ var cspframe = document.getElementById('cspframe');
+ var color;
+
+ color = window.getComputedStyle(cspframe.contentDocument.getElementById('unsafe-inline-script-allowed')).color;
+ ok(color === green, "Inline script should be allowed");
+ color = window.getComputedStyle(cspframe.contentDocument.getElementById('unsafe-eval-script-allowed')).color;
+ ok(color === green, "Eval should be allowed");
+ color = window.getComputedStyle(cspframe.contentDocument.getElementById('unsafe-inline-style-allowed')).color;
+ ok(color === green, "Inline style should be allowed");
+
+ document.getElementById('cspframe2').src = 'file_bug885433_blocks.html';
+ document.getElementById('cspframe2').addEventListener('load', checkBlocked);
+}
+
+function checkBlocked () {
+ var cspframe = document.getElementById('cspframe2');
+ var color;
+
+ color = window.getComputedStyle(cspframe.contentDocument.getElementById('unsafe-inline-script-blocked')).color;
+ ok(color === black, "Inline script should be blocked");
+ color = window.getComputedStyle(cspframe.contentDocument.getElementById('unsafe-eval-script-blocked')).color;
+ ok(color === black, "Eval should be blocked");
+ color = window.getComputedStyle(cspframe.contentDocument.getElementById('unsafe-inline-style-blocked')).color;
+ ok(color === black, "Inline style should be blocked");
+
+ SimpleTest.finish();
+}
+
+document.getElementById('cspframe').src = 'file_bug885433_allows.html';
+document.getElementById('cspframe').addEventListener('load', checkAllowed);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_bug886164.html b/dom/security/test/csp/test_bug886164.html
new file mode 100644
index 0000000000..5347d42ed8
--- /dev/null
+++ b/dom/security/test/csp/test_bug886164.html
@@ -0,0 +1,172 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 886164 - Enforce CSP in sandboxed iframe</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<iframe style="width:200px;height:200px;" id='cspframe' sandbox="allow-same-origin"></iframe>
+<iframe style="width:200px;height:200px;" id='cspframe2' sandbox></iframe>
+<iframe style="width:200px;height:200px;" id='cspframe3' sandbox="allow-same-origin"></iframe>
+<iframe style="width:200px;height:200px;" id='cspframe4' sandbox></iframe>
+<iframe style="width:200px;height:200px;" id='cspframe5' sandbox="allow-scripts"></iframe>
+<iframe style="width:200px;height:200px;" id='cspframe6' sandbox="allow-same-origin allow-scripts"></iframe>
+<script class="testbody" type="text/javascript">
+
+
+var path = "/tests/dom/security/test/csp/";
+
+// These are test results: -1 means it hasn't run,
+// true/false is the pass/fail result.
+window.tests = {
+ // sandbox allow-same-origin; 'self'
+ img_good: -1, // same origin
+ img_bad: -1, //example.com
+
+ // sandbox; 'self'
+ img2_bad: -1, //example.com
+ img2a_good: -1, // same origin & is image
+
+ // sandbox allow-same-origin; 'none'
+ img3_bad: -1,
+ img3a_bad: -1,
+
+ // sandbox; 'none'
+ img4_bad: -1,
+ img4a_bad: -1,
+
+ // sandbox allow-scripts; 'none' 'unsafe-inline'
+ img5_bad: -1,
+ img5a_bad: -1,
+ script5_bad: -1,
+ script5a_bad: -1,
+
+ // sandbox allow-same-origin allow-scripts; 'self' 'unsafe-inline'
+ img6_bad: -1,
+ script6_bad: -1,
+};
+
+// a postMessage handler that is used by sandboxed iframes without
+// 'allow-same-origin' to communicate pass/fail back to this main page.
+// it expects to be called with an object like {ok: true/false, desc:
+// <description of the test> which it then forwards to ok()
+window.addEventListener("message", receiveMessage);
+
+function receiveMessage(event)
+{
+ ok_wrapper(event.data.ok, event.data.desc);
+}
+
+var cspTestsDone = false;
+var iframeSandboxTestsDone = false;
+
+// iframe related
+var completedTests = 0;
+var passedTests = 0;
+
+function ok_wrapper(result, desc) {
+ ok(result, desc);
+
+ completedTests++;
+
+ if (result) {
+ passedTests++;
+ }
+
+ if (completedTests === 5) {
+ iframeSandboxTestsDone = true;
+ if (cspTestsDone) {
+ SimpleTest.finish();
+ }
+ }
+}
+
+
+//csp related
+
+// This is used to watch the blocked data bounce off CSP and allowed data
+// get sent out to the wire.
+function examiner() {
+ SpecialPowers.addObserver(this, "csp-on-violate-policy");
+ SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
+}
+examiner.prototype = {
+ observe(subject, topic, data) {
+ var testpat = new RegExp("testid=([a-z0-9_]+)");
+
+ //_good things better be allowed!
+ //_bad things better be stopped!
+
+ if (topic === "specialpowers-http-notify-request") {
+ //these things were allowed by CSP
+ var uri = data;
+ if (!testpat.test(uri)) return;
+ var testid = testpat.exec(uri)[1];
+
+ window.testResult(testid,
+ /_good/.test(testid),
+ uri + " allowed by csp");
+ }
+
+ if(topic === "csp-on-violate-policy") {
+ //these were blocked... record that they were blocked
+ var asciiSpec = SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIURI"), "asciiSpec");
+ if (!testpat.test(asciiSpec)) return;
+ var testid = testpat.exec(asciiSpec)[1];
+ window.testResult(testid,
+ /_bad/.test(testid),
+ asciiSpec + " blocked by \"" + data + "\"");
+ }
+ },
+
+ // must eventually call this to remove the listener,
+ // or mochitests might get borked.
+ remove() {
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
+ }
+}
+
+window.examiner = new examiner();
+
+window.testResult = function(testname, result, msg) {
+ //test already complete.... forget it... remember the first result.
+ if (window.tests[testname] != -1)
+ return;
+
+ window.tests[testname] = result;
+ ok(result, testname + ' test: ' + msg);
+
+ // if any test is incomplete, keep waiting
+ for (var v in window.tests)
+ if(tests[v] == -1)
+ return;
+
+ // ... otherwise, finish
+ window.examiner.remove();
+ cspTestsDone = true;
+ if (iframeSandboxTestsDone) {
+ SimpleTest.finish();
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// save this for last so that our listeners are registered.
+// ... this loads the testbed of good and bad requests.
+document.getElementById('cspframe').src = 'file_bug886164.html';
+document.getElementById('cspframe2').src = 'file_bug886164_2.html';
+document.getElementById('cspframe3').src = 'file_bug886164_3.html';
+document.getElementById('cspframe4').src = 'file_bug886164_4.html';
+document.getElementById('cspframe5').src = 'file_bug886164_5.html';
+document.getElementById('cspframe6').src = 'file_bug886164_6.html';
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_bug888172.html b/dom/security/test/csp/test_bug888172.html
new file mode 100644
index 0000000000..a78258e21f
--- /dev/null
+++ b/dom/security/test/csp/test_bug888172.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 888172 - CSP 1.0 does not process 'unsafe-inline' or 'unsafe-eval' for default-src</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<iframe style="width:100%;" id='testframe1'></iframe>
+<iframe style="width:100%;" id='testframe2'></iframe>
+<iframe style="width:100%;" id='testframe3'></iframe>
+<script class="testbody" type="text/javascript">
+
+//////////////////////////////////////////////////////////////////////
+// set up and go
+SimpleTest.waitForExplicitFinish();
+
+// utilities for check functions
+// black means the style wasn't applied, applied styles are green
+var green = 'rgb(0, 128, 0)';
+var black = 'rgb(0, 0, 0)';
+
+function getElementColorById(doc, id) {
+ return window.getComputedStyle(doc.contentDocument.getElementById(id)).color;
+}
+
+// We test both script and style execution by observing changes in computed styles
+function checkDefaultSrcOnly() {
+ var testframe = document.getElementById('testframe1');
+
+ ok(getElementColorById(testframe, 'unsafe-inline-script') === green, "Inline script should be allowed");
+ ok(getElementColorById(testframe, 'unsafe-eval-script') === green, "Eval should be allowed");
+ ok(getElementColorById(testframe, 'unsafe-inline-style') === green, "Inline style should be allowed");
+
+ document.getElementById('testframe2').src = 'file_bug888172.sjs?csp=' +
+ escape("default-src 'self' 'unsafe-inline' 'unsafe-eval'; script-src 'self'");
+ document.getElementById('testframe2').addEventListener('load', checkDefaultSrcWithScriptSrc);
+}
+
+function checkDefaultSrcWithScriptSrc() {
+ var testframe = document.getElementById('testframe2');
+
+ ok(getElementColorById(testframe, 'unsafe-inline-script') === black, "Inline script should be blocked");
+ ok(getElementColorById(testframe, 'unsafe-eval-script') === black, "Eval should be blocked");
+ ok(getElementColorById(testframe, 'unsafe-inline-style') === green, "Inline style should be allowed");
+
+ document.getElementById('testframe3').src = 'file_bug888172.sjs?csp=' +
+ escape("default-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self'");
+ document.getElementById('testframe3').addEventListener('load', checkDefaultSrcWithStyleSrc);
+}
+
+function checkDefaultSrcWithStyleSrc() {
+ var testframe = document.getElementById('testframe3');
+
+ ok(getElementColorById(testframe, 'unsafe-inline-script') === green, "Inline script should be allowed");
+ ok(getElementColorById(testframe, 'unsafe-eval-script') === green, "Eval should be allowed");
+ ok(getElementColorById(testframe, 'unsafe-inline-style') === black, "Inline style should be blocked");
+
+ // last test calls finish
+ SimpleTest.finish();
+}
+
+document.getElementById('testframe1').src = 'file_bug888172.sjs?csp=' +
+ escape("default-src 'self' 'unsafe-inline' 'unsafe-eval'");
+document.getElementById('testframe1').addEventListener('load', checkDefaultSrcOnly);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_bug909029.html b/dom/security/test/csp/test_bug909029.html
new file mode 100644
index 0000000000..7a3ac81a1b
--- /dev/null
+++ b/dom/security/test/csp/test_bug909029.html
@@ -0,0 +1,129 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Bug 909029 - CSP source-lists ignore some source expressions like 'unsafe-inline' when * or 'none' are used (e.g., style-src, script-src)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <div id=content style="visibility:hidden">
+ <iframe id=testframe1></iframe>
+ <iframe id=testframe2></iframe>
+ </div>
+ <script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+window.tests = {
+ starExternalStylesLoaded: -1,
+ starExternalImgLoaded: -1,
+ noneExternalStylesBlocked: -1,
+ noneExternalImgLoaded: -1,
+ starInlineStyleAllowed: -1,
+ starInlineScriptBlocked: -1,
+ noneInlineStyleAllowed: -1,
+ noneInlineScriptBlocked: -1
+}
+
+function examiner() {
+ SpecialPowers.addObserver(this, "csp-on-violate-policy");
+ SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
+}
+examiner.prototype = {
+ observe(subject, topic, data) {
+ var testpat = new RegExp("testid=([a-zA-Z]+)");
+
+ if (topic === "specialpowers-http-notify-request") {
+ var uri = data;
+ if (!testpat.test(uri)) return;
+ var testid = testpat.exec(uri)[1];
+ window.testResult(testid,
+ /Loaded/.test(testid),
+ "resource loaded");
+ }
+
+ if(topic === "csp-on-violate-policy") {
+ // these were blocked... record that they were blocked
+ // try because the subject could be an nsIURI or an nsISupportsCString
+ try {
+ var asciiSpec = SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIURI"), "asciiSpec");
+ if (!testpat.test(asciiSpec)) return;
+ var testid = testpat.exec(asciiSpec)[1];
+ window.testResult(testid,
+ /Blocked/.test(testid),
+ "resource blocked by CSP");
+ } catch(e) {
+ // if that fails, the subject is probably a string. Strings are only
+ // reported for inline and eval violations. Since we are testing those
+ // via the observed effects of script on CSSOM, we can simply ignore
+ // these subjects.
+ }
+ }
+ },
+
+ // must eventually call this to remove the listener,
+ // or mochitests might get borked.
+ remove() {
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
+ }
+}
+
+window.examiner = new examiner();
+
+window.testResult = function(testname, result, msg) {
+ //dump("in testResult: testname = " + testname + "\n");
+
+ //test already complete.... forget it... remember the first result.
+ if (window.tests[testname] != -1)
+ return;
+
+ window.tests[testname] = result;
+ is(result, true, testname + ' test: ' + msg);
+
+ // if any test is incomplete, keep waiting
+ for (var v in window.tests)
+ if(tests[v] == -1)
+ return;
+
+ // ... otherwise, finish
+ window.examiner.remove();
+ SimpleTest.finish();
+}
+
+// Helpers for inline script/style checks
+var black = 'rgb(0, 0, 0)';
+var green = 'rgb(0, 128, 0)';
+function getElementColorById(doc, id) {
+ return window.getComputedStyle(doc.contentDocument.getElementById(id)).color;
+}
+
+function checkInlineWithStar() {
+ var testframe = document.getElementById('testframe1');
+ window.testResult("starInlineStyleAllowed",
+ getElementColorById(testframe, 'inline-style') === green,
+ "Inline styles should be allowed (style-src 'unsafe-inline' with star)");
+ window.testResult("starInlineScriptBlocked",
+ getElementColorById(testframe, 'inline-script') === black,
+ "Inline scripts should be blocked (style-src 'unsafe-inline' with star)");
+}
+
+function checkInlineWithNone() {
+ // If a directive has 'none' in addition to other sources, 'none' is ignored
+ // and the other sources are used. 'none' is only a valid source if it is
+ // used by itself.
+ var testframe = document.getElementById('testframe2');
+ window.testResult("noneInlineStyleAllowed",
+ getElementColorById(testframe, 'inline-style') === green,
+ "Inline styles should be allowed (style-src 'unsafe-inline' with none)");
+ window.testResult("noneInlineScriptBlocked",
+ getElementColorById(testframe, 'inline-script') === black,
+ "Inline scripts should be blocked (style-src 'unsafe-inline' with none)");
+}
+
+document.getElementById('testframe1').src = 'file_bug909029_star.html';
+document.getElementById('testframe1').addEventListener('load', checkInlineWithStar);
+document.getElementById('testframe2').src = 'file_bug909029_none.html';
+document.getElementById('testframe2').addEventListener('load', checkInlineWithNone);
+ </script>
+ </body>
+</html>
diff --git a/dom/security/test/csp/test_bug910139.html b/dom/security/test/csp/test_bug910139.html
new file mode 100644
index 0000000000..bbebedf877
--- /dev/null
+++ b/dom/security/test/csp/test_bug910139.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>CSP should block XSLT as script, not as style</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <iframe style="width:100%;" id='xsltframe'></iframe>
+ <iframe style="width:100%;" id='xsltframe2'></iframe>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// define the expected output of this test
+var header = "this xml file should be formatted using an xsl file(lower iframe should contain xml dump)!";
+
+function checkAllowed () {
+ /* The policy for this test is:
+ * Content-Security-Policy: default-src 'self'; script-src 'self'
+ *
+ * we load the xsl file using:
+ * <?xml-stylesheet type="text/xsl" href="file_bug910139.xsl"?>
+ */
+ try {
+ var cspframe = document.getElementById('xsltframe');
+ var xsltAllowedHeader = cspframe.contentWindow.document.getElementById('xsltheader').innerHTML;
+ is(xsltAllowedHeader, header, "XSLT loaded from 'self' should be allowed!");
+ }
+ catch (e) {
+ ok(false, "Error: could not access content in xsltframe!")
+ }
+
+ // continue with the next test
+ document.getElementById('xsltframe2').addEventListener('load', checkBlocked);
+ document.getElementById('xsltframe2').src = 'file_bug910139.sjs';
+}
+
+function checkBlocked () {
+ /* The policy for this test is:
+ * Content-Security-Policy: default-src 'self'; script-src *.example.com
+ *
+ * we load the xsl file using:
+ * <?xml-stylesheet type="text/xsl" href="file_bug910139.xsl"?>
+ */
+ try {
+ var cspframe = document.getElementById('xsltframe2');
+ var xsltBlockedHeader = cspframe.contentWindow.document.getElementById('xsltheader');
+ is(xsltBlockedHeader, null, "XSLT loaded from different host should be blocked!");
+ }
+ catch (e) {
+ ok(false, "Error: could not access content in xsltframe2!")
+ }
+ SimpleTest.finish();
+}
+
+document.getElementById('xsltframe').addEventListener('load', checkAllowed);
+document.getElementById('xsltframe').src = 'file_bug910139.sjs';
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_bug941404.html b/dom/security/test/csp/test_bug941404.html
new file mode 100644
index 0000000000..7c35c38aa1
--- /dev/null
+++ b/dom/security/test/csp/test_bug941404.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 941404 - Data documents should not set CSP</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+
+</div>
+
+<iframe style="width:200px;height:200px;" id='cspframe'></iframe>
+<script class="testbody" type="text/javascript">
+
+
+var path = "/tests/dom/security/test/csp/";
+
+// These are test results: -1 means it hasn't run,
+// true/false is the pass/fail result.
+window.tests = {
+ img_good: -1,
+ img2_good: -1,
+};
+
+
+//csp related
+
+// This is used to watch the blocked data bounce off CSP and allowed data
+// get sent out to the wire.
+function examiner() {
+ SpecialPowers.addObserver(this, "csp-on-violate-policy");
+ SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
+}
+
+examiner.prototype = {
+ observe(subject, topic, data) {
+ var testpat = new RegExp("testid=([a-z0-9_]+)");
+
+ //_good things better be allowed!
+ //_bad things better be stopped!
+
+ if (topic === "specialpowers-http-notify-request") {
+ //these things were allowed by CSP
+ var uri = data;
+ if (!testpat.test(uri)) return;
+ var testid = testpat.exec(uri)[1];
+
+ window.testResult(testid,
+ /_good/.test(testid),
+ uri + " allowed by csp");
+ }
+
+ if(topic === "csp-on-violate-policy") {
+ //these were blocked... record that they were blocked
+ var asciiSpec = SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIURI"), "asciiSpec");
+ if (!testpat.test(asciiSpec)) return;
+ var testid = testpat.exec(asciiSpec)[1];
+ window.testResult(testid,
+ /_bad/.test(testid),
+ asciiSpec + " blocked by \"" + data + "\"");
+ }
+ },
+
+ // must eventually call this to remove the listener,
+ // or mochitests might get borked.
+ remove() {
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
+ }
+}
+
+window.examiner = new examiner();
+
+window.testResult = function(testname, result, msg) {
+ //test already complete.... forget it... remember the first result.
+ if (window.tests[testname] != -1)
+ return;
+
+ window.tests[testname] = result;
+ is(result, true, testname + ' test: ' + msg);
+
+ // if any test is incomplete, keep waiting
+ for (var v in window.tests)
+ if(tests[v] == -1) {
+ console.log(v + " is not complete");
+ return;
+ }
+
+ // ... otherwise, finish
+ window.examiner.remove();
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// save this for last so that our listeners are registered.
+// ... this loads the testbed of good and bad requests.
+document.getElementById('cspframe').src = 'file_bug941404.html';
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_child-src_iframe.html b/dom/security/test/csp/test_child-src_iframe.html
new file mode 100644
index 0000000000..2b85e280bd
--- /dev/null
+++ b/dom/security/test/csp/test_child-src_iframe.html
@@ -0,0 +1,113 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1045891</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="visibility: hidden">
+ </div>
+
+<script class="testbody" type="text/javascript">
+
+/*
+ * Description of the test:
+ * We load a page with a given CSP and verify that child frames and workers are correctly
+ * evaluated through the "child-src" directive.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+var IFRAME_SRC="file_child-src_iframe.html"
+
+var tests = {
+ 'same-src': {
+ id: "same-src",
+ file: IFRAME_SRC,
+ result : "allowed",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; child-src http://mochi.test:8888"
+ },
+ 'star-src': {
+ id: "star-src",
+ file: IFRAME_SRC,
+ result : "allowed",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; child-src *"
+ },
+ 'other-src': {
+ id: "other-src",
+ file: IFRAME_SRC,
+ result : "blocked",
+ policy : "default-src http://mochi.test:8888; script-src 'unsafe-inline'; child-src http://www.example.com"
+ },
+ 'same-src-by-frame-src': {
+ id: "same-src-by-frame-src",
+ file: IFRAME_SRC,
+ result : "allowed",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; child-src 'none'; frame-src http://mochi.test:8888"
+ },
+ 'star-src-by-frame-src': {
+ id: "star-src-by-frame-src",
+ file: IFRAME_SRC,
+ result : "allowed",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; child-src 'none'; frame-src *"
+ },
+ 'other-src-by-frame-src': {
+ id: "other-src-by-frame-src",
+ file: IFRAME_SRC,
+ result : "blocked",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; child-src http://mochi.test:8888; frame-src http://www.example.com"
+ },
+ 'none-src-by-frame-src': {
+ id: "none-src-by-frame-src",
+ file: IFRAME_SRC,
+ result : "blocked",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; child-src http://mochi.test:8888; frame-src 'none'"
+ }
+};
+
+finished = {};
+
+function checkFinished() {
+ if (Object.keys(finished).length == Object.keys(tests).length) {
+ window.removeEventListener('message', recvMessage);
+ SimpleTest.finish();
+ }
+}
+
+function recvMessage(ev) {
+ is(ev.data.message, tests[ev.data.id].result, "CSP child-src test " + ev.data.id);
+ finished[ev.data.id] = ev.data.message;
+
+ checkFinished();
+}
+
+window.addEventListener('message', recvMessage);
+
+function loadNextTest() {
+ for (item in tests) {
+ test = tests[item];
+ var src = "file_testserver.sjs";
+ // append the file that should be served
+ src += "?file=" + escape("tests/dom/security/test/csp/" + test.file);
+ // append the CSP that should be used to serve the file
+ src += "&csp=" + escape(test.policy);
+ // add our identifier
+ src += "#" + escape(test.id);
+
+ content = document.getElementById('content');
+ testframe = document.createElement("iframe");
+ testframe.setAttribute('id', test.id);
+ content.appendChild(testframe);
+ testframe.src = src;
+ }
+}
+
+// start running the tests
+loadNextTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_child-src_worker-redirect.html b/dom/security/test/csp/test_child-src_worker-redirect.html
new file mode 100644
index 0000000000..a1b1c9c2b4
--- /dev/null
+++ b/dom/security/test/csp/test_child-src_worker-redirect.html
@@ -0,0 +1,125 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Bug 1045891</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <p id="display"></p>
+ <div id="content" style="visibility: hidden">
+ </div>
+
+ <script class="testbody" type="text/javascript">
+ /*
+ * Description of the test:
+ * We load a page with a given CSP and verify that child frames and workers are correctly
+ * evaluated through the "child-src" directive.
+ */
+
+ SimpleTest.waitForExplicitFinish();
+
+ var WORKER_REDIRECT_TEST_FILE = "file_child-src_worker-redirect.html";
+ var SHARED_WORKER_REDIRECT_TEST_FILE = "file_child-src_shared_worker-redirect.html";
+
+ var tests = {
+ 'same-src-worker_redir-same': {
+ id: "same-src-worker_redir-same",
+ file: WORKER_REDIRECT_TEST_FILE,
+ result : "allowed",
+ redir: "same",
+ policy : "default-src 'none'; script-src 'self' 'unsafe-inline'; child-src http://mochi.test:8888"
+ },
+ 'same-src-worker_redir-other': {
+ id: "same-src-worker_redir-other",
+ file: WORKER_REDIRECT_TEST_FILE,
+ result : "blocked",
+ redir: "other",
+ policy : "default-src 'none'; script-src 'self' 'unsafe-inline'; child-src http://mochi.test:8888"
+ },
+ 'star-src-worker_redir-same': {
+ id: "star-src-worker_redir-same",
+ file: WORKER_REDIRECT_TEST_FILE,
+ redir: "same",
+ result : "allowed",
+ policy : "default-src 'none'; script-src 'self' 'unsafe-inline'; child-src *"
+ },
+ 'other-src-worker_redir-same': {
+ id: "other-src-worker_redir-same",
+ file: WORKER_REDIRECT_TEST_FILE,
+ redir: "same",
+ result : "blocked",
+ policy : "default-src 'none'; script-src 'self' 'unsafe-inline'; child-src https://www.example.org"
+ },
+ /* shared workers */
+ 'same-src-shared_worker_redir-same': {
+ id: "same-src-shared_worker_redir-same",
+ file: SHARED_WORKER_REDIRECT_TEST_FILE,
+ result : "allowed",
+ redir: "same",
+ policy : "default-src 'none'; script-src 'self' 'unsafe-inline'; child-src http://mochi.test:8888"
+ },
+ 'same-src-shared_worker_redir-other': {
+ id: "same-src-shared_worker_redir-other",
+ file: SHARED_WORKER_REDIRECT_TEST_FILE,
+ result : "blocked",
+ redir: "other",
+ policy : "default-src 'none'; script-src 'self' 'unsafe-inline'; child-src http://mochi.test:8888"
+ },
+ 'star-src-shared_worker_redir-same': {
+ id: "star-src-shared_worker_redir-same",
+ file: SHARED_WORKER_REDIRECT_TEST_FILE,
+ redir: "same",
+ result : "allowed",
+ policy : "default-src 'none'; script-src 'self' 'unsafe-inline'; child-src *"
+ },
+ 'other-src-shared_worker_redir-same': {
+ id: "other-src-shared_worker_redir-same",
+ file: SHARED_WORKER_REDIRECT_TEST_FILE,
+ redir: "same",
+ result : "blocked",
+ policy : "default-src 'none'; script-src 'self' 'unsafe-inline'; child-src https://www.example.org"
+ },
+ };
+
+ finished = {};
+
+ function recvMessage(ev) {
+ is(ev.data.message, tests[ev.data.id].result, "CSP child-src worker test " + ev.data.id);
+ finished[ev.data.id] = ev.data.message;
+
+ if (Object.keys(finished).length == Object.keys(tests).length) {
+ window.removeEventListener('message', recvMessage);
+ SimpleTest.finish();
+ }
+ }
+
+ window.addEventListener('message', recvMessage);
+
+ function loadNextTest() {
+ for (item in tests) {
+ test = tests[item];
+ var src = "file_testserver.sjs";
+ // append the file that should be served
+ src += "?file=" + escape("tests/dom/security/test/csp/" + test.file);
+ // append the CSP that should be used to serve the file
+ src += "&csp=" + escape(test.policy);
+ // add whether redirect is to same or different
+ src += "&redir=" + escape(test.policy);
+ // add our identifier
+ src += "#" + escape(test.id);
+
+ content = document.getElementById('content');
+ testframe = document.createElement("iframe");
+ testframe.setAttribute('id', test.id);
+ content.appendChild(testframe);
+ testframe.src = src;
+ }
+ }
+
+ // start running the tests
+ loadNextTest();
+ </script>
+ </body>
+</html>
diff --git a/dom/security/test/csp/test_child-src_worker.html b/dom/security/test/csp/test_child-src_worker.html
new file mode 100644
index 0000000000..205b67f098
--- /dev/null
+++ b/dom/security/test/csp/test_child-src_worker.html
@@ -0,0 +1,147 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Bug 1045891</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <p id="display"></p>
+ <div id="content" style="visibility: hidden">
+ </div>
+
+ <script class="testbody" type="text/javascript">
+ /*
+ * Description of the test:
+ * We load a page with a given CSP and verify that child frames and workers are correctly
+ * evaluated through the "child-src" directive.
+ */
+
+ SimpleTest.waitForExplicitFinish();
+
+ var WORKER_TEST_FILE = "file_child-src_worker.html";
+ var SERVICE_WORKER_TEST_FILE = "file_child-src_service_worker.html";
+ var SHARED_WORKER_TEST_FILE = "file_child-src_shared_worker.html";
+
+ var tests = {
+ 'same-src-worker': {
+ id: "same-src-worker",
+ file: WORKER_TEST_FILE,
+ result : "allowed",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; child-src http://mochi.test:8888"
+ },
+ 'same-src-service_worker': {
+ id: "same-src-service_worker",
+ file: SERVICE_WORKER_TEST_FILE,
+ result : "allowed",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; child-src http://mochi.test:8888"
+ },
+ 'same-src-shared_worker': {
+ id: "same-src-shared_worker",
+ file: SHARED_WORKER_TEST_FILE,
+ result : "allowed",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; child-src http://mochi.test:8888"
+ },
+ 'star-src-worker': {
+ id: "star-src-worker",
+ file: WORKER_TEST_FILE,
+ result : "allowed",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; child-src *"
+ },
+ 'star-src-service_worker': {
+ id: "star-src-service_worker",
+ file: SERVICE_WORKER_TEST_FILE,
+ result : "allowed",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; child-src *"
+ },
+ 'star-src-shared_worker': {
+ id: "star-src-shared_worker",
+ file: SHARED_WORKER_TEST_FILE,
+ result : "allowed",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; child-src *"
+ },
+ 'other-src-worker': {
+ id: "other-src-worker",
+ file: WORKER_TEST_FILE,
+ result : "blocked",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; child-src https://www.example.org"
+ },
+ 'other-src-service_worker': {
+ id: "other-src-service_worker",
+ file: SERVICE_WORKER_TEST_FILE,
+ result : "blocked",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; child-src https://www.example.org"
+ },
+ 'other-src-shared_worker': {
+ id: "other-src-shared_worker",
+ file: SHARED_WORKER_TEST_FILE,
+ result : "blocked",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; child-src https://www.example.org"
+ },
+ 'script-src-worker': {
+ id: "script-src-worker",
+ file: WORKER_TEST_FILE,
+ result : "blocked",
+ policy : "default-src 'none'; script-src https://www.example.org 'unsafe-inline'"
+ },
+ 'script-src-service_worker': {
+ id: "script-src-service_worker",
+ file: SERVICE_WORKER_TEST_FILE,
+ result : "blocked",
+ policy : "default-src 'none'; script-src https://www.example.org 'unsafe-inline'"
+ },
+ 'script-src-self-shared_worker': {
+ id: "script-src-self-shared_worker",
+ file: SHARED_WORKER_TEST_FILE,
+ result : "blocked",
+ policy : "default-src 'none'; script-src https://www.example.org 'unsafe-inline'"
+ },
+ };
+
+ finished = {};
+
+ function recvMessage(ev) {
+ is(ev.data.message, tests[ev.data.id].result, "CSP child-src worker test " + ev.data.id);
+ finished[ev.data.id] = ev.data.message;
+
+ if (Object.keys(finished).length == Object.keys(tests).length) {
+ window.removeEventListener('message', recvMessage);
+ SimpleTest.finish();
+ }
+ }
+
+ window.addEventListener('message', recvMessage);
+
+ function loadNextTest() {
+ for (item in tests) {
+ test = tests[item];
+ var src = "file_testserver.sjs";
+ // append the file that should be served
+ src += "?file=" + escape("tests/dom/security/test/csp/" + test.file);
+ // append the CSP that should be used to serve the file
+ src += "&csp=" + escape(test.policy);
+ // add our identifier
+ src += "#" + escape(test.id);
+
+ content = document.getElementById('content');
+ testframe = document.createElement("iframe");
+ testframe.setAttribute('id', test.id);
+ content.appendChild(testframe);
+ testframe.src = src;
+ }
+ }
+
+ onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]}, loadNextTest);
+ };
+
+ // start running the tests
+ //loadNextTest();
+ </script>
+ </body>
+</html>
diff --git a/dom/security/test/csp/test_child-src_worker_data.html b/dom/security/test/csp/test_child-src_worker_data.html
new file mode 100644
index 0000000000..9d36e73e0c
--- /dev/null
+++ b/dom/security/test/csp/test_child-src_worker_data.html
@@ -0,0 +1,126 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Bug 1045891</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <p id="display"></p>
+ <div id="content" style="visibility: hidden">
+ </div>
+
+ <script class="testbody" type="text/javascript">
+ /*
+ * Description of the test:
+ * We load a page with a given CSP and verify that child frames and workers are correctly
+ * evaluated through the "child-src" directive.
+ */
+
+ SimpleTest.waitForExplicitFinish();
+
+ var WORKER_TEST_FILE = "file_child-src_worker_data.html";
+ var SHARED_WORKER_TEST_FILE = "file_child-src_shared_worker_data.html";
+
+ var tests = {
+ 'same-src-worker-no-data': {
+ id: "same-src-worker-no-data",
+ file: WORKER_TEST_FILE,
+ result : "blocked",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; child-src 'self'"
+ },
+ 'same-src-worker': {
+ id: "same-src-worker",
+ file: WORKER_TEST_FILE,
+ result : "allowed",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; child-src 'self' data:"
+ },
+ 'same-src-shared_worker-no-data': {
+ id: "same-src-shared_worker-no-data",
+ file: SHARED_WORKER_TEST_FILE,
+ result : "blocked",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; child-src 'self'"
+ },
+ 'same-src-shared_worker': {
+ id: "same-src-shared_worker",
+ file: SHARED_WORKER_TEST_FILE,
+ result : "allowed",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; child-src 'self' data:"
+ },
+ 'star-src-worker': {
+ id: "star-src-worker",
+ file: WORKER_TEST_FILE,
+ result : "allowed",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; child-src * data:"
+ },
+ 'star-src-worker-no-data': {
+ id: "star-src-worker-no-data",
+ file: WORKER_TEST_FILE,
+ result : "blocked",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; child-src *"
+ },
+ 'star-src-shared_worker-no-data': {
+ id: "star-src-shared_worker-no-data",
+ file: SHARED_WORKER_TEST_FILE,
+ result : "blocked",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; child-src *"
+ },
+ 'star-src-shared_worker': {
+ id: "star-src-shared_worker",
+ file: SHARED_WORKER_TEST_FILE,
+ result : "allowed",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; child-src * data:"
+ },
+ 'other-src-worker-no-data': {
+ id: "other-src-worker-no-data",
+ file: WORKER_TEST_FILE,
+ result : "blocked",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; child-src https://www.example.org"
+ },
+ 'other-src-shared_worker-no-data': {
+ id: "other-src-shared_worker-no-data",
+ file: SHARED_WORKER_TEST_FILE,
+ result : "blocked",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; child-src https://www.example.org"
+ },
+ };
+
+ finished = {};
+
+ function recvMessage(ev) {
+ is(ev.data.message, tests[ev.data.id].result, "CSP child-src worker test " + ev.data.id);
+ finished[ev.data.id] = ev.data.message;
+
+ if (Object.keys(finished).length == Object.keys(tests).length) {
+ window.removeEventListener('message', recvMessage);
+ SimpleTest.finish();
+ }
+ }
+
+ window.addEventListener('message', recvMessage);
+
+ function loadNextTest() {
+ for (item in tests) {
+ test = tests[item];
+ var src = "file_testserver.sjs";
+ // append the file that should be served
+ src += "?file=" + escape("tests/dom/security/test/csp/" + test.file);
+ // append the CSP that should be used to serve the file
+ src += "&csp=" + escape(test.policy);
+ // add our identifier
+ src += "#" + escape(test.id);
+
+ content = document.getElementById('content');
+ testframe = document.createElement("iframe");
+ testframe.setAttribute('id', test.id);
+ content.appendChild(testframe);
+ testframe.src = src;
+ }
+ }
+
+ // start running the tests
+ loadNextTest();
+ </script>
+ </body>
+</html>
diff --git a/dom/security/test/csp/test_connect-src.html b/dom/security/test/csp/test_connect-src.html
new file mode 100644
index 0000000000..1ae4482dd8
--- /dev/null
+++ b/dom/security/test/csp/test_connect-src.html
@@ -0,0 +1,129 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1031530 and Bug 1139667 - Test mapping of XMLHttpRequest and fetch() to connect-src</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="visibility: hidden">
+ <iframe style="width:100%;" id="testframe"></iframe>
+ </div>
+
+<script class="testbody" type="text/javascript">
+
+/*
+ * Description of the test:
+ * We load a page with a given CSP and verify that XMLHttpRequests and fetches are correctly
+ * evaluated through the "connect-src" directive. All XMLHttpRequests are served
+ * using http://mochi.test:8888, which allows the requests to succeed for the first
+ * two policies and to fail for the last policy. Please note that we have to add
+ * 'unsafe-inline' so we can run the JS test code in file_connect-src.html.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+var tests = [
+ {
+ file: "file_connect-src.html",
+ result : "allowed",
+ policy : "default-src 'none' script-src 'unsafe-inline'; connect-src http://mochi.test:8888"
+ },
+ {
+ file: "file_connect-src.html",
+ result : "allowed",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; connect-src *"
+ },
+ {
+ file: "file_connect-src.html",
+ result : "blocked",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; connect-src http://www.example.com"
+ },
+ {
+ file: "file_connect-src-fetch.html",
+ result : "allowed",
+ policy : "default-src 'none' script-src 'unsafe-inline'; connect-src http://mochi.test:8888"
+ },
+ {
+ file: "file_connect-src-fetch.html",
+ result : "allowed",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; connect-src *"
+ },
+ {
+ file: "file_connect-src-fetch.html",
+ result : "blocked",
+ policy : "default-src 'none'; script-src 'unsafe-inline'; connect-src http://www.example.com"
+ }
+];
+
+// initializing to -1 so we start at index 0 when we start the test
+var counter = -1;
+
+function checkResult(aResult) {
+ is(aResult, tests[counter].result, "should be " + tests[counter].result + " in test " + counter + "!");
+ loadNextTest();
+}
+
+// We use the examiner to identify requests that hit the wire and requests
+// that are blocked by CSP and bubble up the result to the including iframe
+// document (parent).
+function examiner() {
+ SpecialPowers.addObserver(this, "csp-on-violate-policy");
+ SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
+}
+examiner.prototype = {
+ observe(subject, topic, data) {
+ if (topic === "specialpowers-http-notify-request") {
+ // making sure we do not bubble a result for something other
+ // then the request in question.
+ if (!data.includes("file_testserver.sjs?foo")) {
+ return;
+ }
+ checkResult("allowed");
+ }
+
+ if (topic === "csp-on-violate-policy") {
+ // making sure we do not bubble a result for something other
+ // then the request in question.
+ var asciiSpec = SpecialPowers.getPrivilegedProps(
+ SpecialPowers.do_QueryInterface(subject, "nsIURI"),
+ "asciiSpec");
+
+ if (!asciiSpec.includes("file_testserver.sjs?foo")) {
+ return;
+ }
+ checkResult("blocked");
+ }
+ },
+ remove() {
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
+ }
+}
+window.ConnectSrcExaminer = new examiner();
+
+function loadNextTest() {
+ counter++;
+ if (counter == tests.length) {
+ window.ConnectSrcExaminer.remove();
+ SimpleTest.finish();
+ return;
+ }
+
+ var src = "file_testserver.sjs";
+ // append the file that should be served
+ src += "?file=" + escape("tests/dom/security/test/csp/" + tests[counter].file);
+ // append the CSP that should be used to serve the file
+ src += "&csp=" + escape(tests[counter].policy);
+
+ document.getElementById("testframe").src = src;
+}
+
+// start running the tests
+loadNextTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_csp_frame_ancestors_about_blank.html b/dom/security/test/csp/test_csp_frame_ancestors_about_blank.html
new file mode 100644
index 0000000000..8f57d9e133
--- /dev/null
+++ b/dom/security/test/csp/test_csp_frame_ancestors_about_blank.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1668071 - CSP frame-ancestors in about:blank</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the test:
+ * We dynamically load an about:blank iframe which then loads a testframe
+ * including a CSP frame-ancestors directive which matches the including
+ * security context. We make sure that we not incorrectly block on
+ * about:blank which should inherit the security context.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+let aboutBlankFrame = document.createElement("iframe");
+document.body.appendChild(aboutBlankFrame);
+
+aboutBlankFrame.onload = function() {
+ ok(true, "aboutBlankFrame onload should fire");
+ let aboutBlankDoc = aboutBlankFrame.contentDocument;
+ is(aboutBlankDoc.documentURI, "about:blank",
+ "sanity: aboutBlankFrame URI should be about:blank");
+
+ let testframe = aboutBlankDoc.createElement("iframe");
+ aboutBlankDoc.body.appendChild(testframe);
+ testframe.onload = function() {
+ ok(true, "testframe onload should fire");
+ let testDoc = SpecialPowers.wrap(testframe.contentDocument);
+ ok(testDoc.documentURI.endsWith("file_csp_frame_ancestors_about_blank.html"),
+ "sanity: document in testframe should be the testfile");
+
+ let cspJSON = testDoc.cspJSON;
+ ok(cspJSON.includes("frame-ancestors"), "found frame-ancestors directive");
+ ok(cspJSON.includes("http://mochi.test:8888"), "found frame-ancestors value");
+
+ SimpleTest.finish();
+ }
+
+ testframe.onerror = function() {
+ ok(false, "testframe onerror should not fire");
+ }
+ testframe.src = "file_csp_frame_ancestors_about_blank.html";
+}
+
+aboutBlankFrame.onerror = function() {
+ ok(false, "aboutBlankFrame onerror should not be called");
+}
+aboutBlankFrame.src = "about:blank";
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_csp_style_src_empty_hash.html b/dom/security/test/csp/test_csp_style_src_empty_hash.html
new file mode 100644
index 0000000000..b500c196e6
--- /dev/null
+++ b/dom/security/test/csp/test_csp_style_src_empty_hash.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1609122 - Empty Style Element with valid style-src hash </title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+
+ <meta http-equiv="Content-Security-Policy" content="style-src 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=';">
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+/* Description of the test:
+ * We try to load a stylesheet that is empty with a matching hash
+ * sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=' should match ""
+ * thus allow the style to be accessed
+ */
+
+
+var s = document.head.appendChild(document.createElement("style"));
+s.disabled = true;
+
+is(s.disabled, true, "Empty Stylesheet with matching Hash was not blocked");
+SimpleTest.finish();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_csp_worker_inheritance.html b/dom/security/test/csp/test_csp_worker_inheritance.html
new file mode 100644
index 0000000000..ebf6bea8a6
--- /dev/null
+++ b/dom/security/test/csp/test_csp_worker_inheritance.html
@@ -0,0 +1,20 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+<html>
+ <head>
+ <title>Test for Bug 1475849</title>
+ </head>
+ <body>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <iframe style="width:200px;height:200px;" id='cspframe'></iframe>
+ <script class="testbody" type="text/javascript">
+ document.getElementById('cspframe').src = 'main_csp_worker.html';
+ </script>
+
+ </body>
+</html>
diff --git a/dom/security/test/csp/test_data_csp_inheritance.html b/dom/security/test/csp/test_data_csp_inheritance.html
new file mode 100644
index 0000000000..dd7f3174a2
--- /dev/null
+++ b/dom/security/test/csp/test_data_csp_inheritance.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1381761 - Treating 'data:' documents as unique, opaque origins should still inherit the CSP</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+/* Description of the test:
+ * We load an iframe using a meta CSP which includes another iframe
+ * using a data: URI. We make sure the CSP gets inherited into
+ * the data: URI iframe.
+ */
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ window.removeEventListener("message", receiveMessage);
+ // toplevel CSP should apply to data: URI iframe hence resulting
+ // in 1 applied policy.
+ is(event.data.result, 1,
+ "data: URI iframe inherits CSP from including context");
+ SimpleTest.finish();
+}
+
+document.getElementById("testframe").src = "file_data_csp_inheritance.html";
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_data_csp_merge.html b/dom/security/test/csp/test_data_csp_merge.html
new file mode 100644
index 0000000000..87219c406d
--- /dev/null
+++ b/dom/security/test/csp/test_data_csp_merge.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1386183 - Meta CSP on data: URI iframe should be merged with toplevel CSP</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+/* Description of the test:
+ * We load an iframe using a meta CSP which includes another iframe
+ * using a data: URI which also defines a meta CSP. We make sure the
+ * CSP from the including document gets merged with the data: URI
+ * CSP and applies to the data: URI iframe.
+ */
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ window.removeEventListener("message", receiveMessage);
+ // toplevel CSP + data: URI iframe meta CSP => 2 CSP policies
+ is(event.data.result, 2,
+ "CSP on data: URI iframe gets merged with CSP from including context");
+ SimpleTest.finish();
+}
+
+document.getElementById("testframe").src = "file_data_csp_merge.html";
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_data_doc_ignore_meta_csp.html b/dom/security/test/csp/test_data_doc_ignore_meta_csp.html
new file mode 100644
index 0000000000..6f0a3fbbf6
--- /dev/null
+++ b/dom/security/test/csp/test_data_doc_ignore_meta_csp.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1382869: data document should ignore meta csp</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+/* Description of the test:
+ * We load an iframe creating a new data document which defines
+ * a meta csp. We make sure the meta CSP is ignored and does not
+ * apply to the actual iframe document.
+ */
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ window.removeEventListener("message", receiveMessage);
+ is(event.data.result, "dataDocCreated", "sanity: received msg from loaded frame");
+
+ var frame = document.getElementById("testframe");
+ var contentDoc = SpecialPowers.wrap(frame).contentDocument;
+ var cspOBJ = JSON.parse(contentDoc.cspJSON);
+ // make sure we got no policy attached
+ var policies = cspOBJ["csp-policies"];
+ is(policies.length, 0, "there should be no CSP attached to the iframe");
+ SimpleTest.finish();
+}
+
+document.getElementById("testframe").src = "file_data_doc_ignore_meta_csp.html";
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_docwrite_meta.html b/dom/security/test/csp/test_docwrite_meta.html
new file mode 100644
index 0000000000..776f1bb32f
--- /dev/null
+++ b/dom/security/test/csp/test_docwrite_meta.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 663570 - Implement Content Security Policy via meta tag</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<iframe style="width:100%;" id="writemetacspframe"></iframe>
+<iframe style="width:100%;" id="commentmetacspframe"></iframe>
+
+
+<script class="testbody" type="text/javascript">
+/* Description of the test:
+ * We load two frames, where the first frame does doc.write(meta csp) and
+ * the second does doc.write(comment out meta csp).
+ * We make sure to reuse/invalidate preloads depending on the policy.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+var writemetacspframe = document.getElementById("writemetacspframe");
+var commentmetacspframe = document.getElementById("commentmetacspframe");
+var seenResults = 0;
+
+function checkTestsDone() {
+ seenResults++;
+ if (seenResults < 2) {
+ return;
+ }
+ SimpleTest.finish();
+}
+
+// document.write(<meta csp ...>) should block resources from being included in the doc
+function checkResultsBlocked() {
+ writemetacspframe.removeEventListener('load', checkResultsBlocked);
+
+ // stylesheet: default background color within FF is transparent
+ var bgcolor = window.getComputedStyle(writemetacspframe.contentDocument.body)
+ .getPropertyValue("background-color");
+ is(bgcolor, "rgba(0, 0, 0, 0)", "inital background value in FF should be 'transparent'");
+
+ // image: make sure image is blocked
+ var img = writemetacspframe.contentDocument.getElementById("testimage");
+ is(img.naturalWidth, 0, "image width should be 0");
+ is(img.naturalHeight, 0, "image height should be 0");
+
+ // script: make sure defined variable in external script is undefined
+ is(writemetacspframe.contentDocument.myMetaCSPScript, undefined, "myMetaCSPScript should be 'undefined'");
+
+ checkTestsDone();
+}
+
+// document.write(<--) to comment out meta csp should allow resources to be loaded
+// after the preload failed
+function checkResultsAllowed() {
+ commentmetacspframe.removeEventListener('load', checkResultsAllowed);
+
+ // stylesheet: should be applied; bgcolor should be red
+ var bgcolor = window.getComputedStyle(commentmetacspframe.contentDocument.body).getPropertyValue("background-color");
+ is(bgcolor, "rgb(255, 0, 0)", "background should be red/rgb(255, 0, 0)");
+
+ // image: should be completed
+ var img = commentmetacspframe.contentDocument.getElementById("testimage");
+ ok(img.complete, "image should not be loaded");
+
+ // script: defined variable in external script should be accessible
+ is(commentmetacspframe.contentDocument.myMetaCSPScript, "external-JS-loaded", "myMetaCSPScript should be 'external-JS-loaded'");
+
+ checkTestsDone();
+}
+
+// doc.write(meta csp) should should allow preloads but should block actual loads
+writemetacspframe.src = 'file_docwrite_meta.html';
+writemetacspframe.addEventListener('load', checkResultsBlocked);
+
+// commenting out a meta CSP should result in loaded image, script, style
+commentmetacspframe.src = 'file_doccomment_meta.html';
+commentmetacspframe.addEventListener('load', checkResultsAllowed);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_dual_header.html b/dom/security/test/csp/test_dual_header.html
new file mode 100644
index 0000000000..cfea86103b
--- /dev/null
+++ b/dom/security/test/csp/test_dual_header.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1036399 - Multiple CSP policies should be combined towards an intersection</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="visibility: hidden">
+ <iframe style="width:100%;" id="testframe"></iframe>
+ </div>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the test:
+ * We have two tests where each tests serves a page using two CSP policies:
+ * a) * default-src 'self'
+ * * default-src 'self' 'unsafe-inline'
+ *
+ * b) * default-src 'self' 'unsafe-inline'
+ * * default-src 'self' 'unsafe-inline'
+ *
+ * We make sure the inline script is *blocked* for test (a) but *allowed* for test (b).
+ * Multiple CSPs should be combined towards an intersection and it shouldn't be possible
+ * to open up (loosen) a CSP policy.
+ */
+
+const TESTS = [
+ { query: "tight", result: "blocked" },
+ { query: "loose", result: "allowed" }
+];
+var testCounter = -1;
+
+function ckeckResult() {
+ try {
+ document.getElementById("testframe").removeEventListener('load', ckeckResult);
+ var testframe = document.getElementById("testframe");
+ var divcontent = testframe.contentWindow.document.getElementById('testdiv').innerHTML;
+ is(divcontent, curTest.result, "should be 'blocked'!");
+ }
+ catch (e) {
+ ok(false, "error: could not access content in div container!");
+ }
+ loadNextTest();
+}
+
+function loadNextTest() {
+ testCounter++;
+ if (testCounter >= TESTS.length) {
+ SimpleTest.finish();
+ return;
+ }
+ curTest = TESTS[testCounter];
+ var src = "file_dual_header_testserver.sjs?" + curTest.query;
+ document.getElementById("testframe").addEventListener("load", ckeckResult);
+ document.getElementById("testframe").src = src;
+}
+
+SimpleTest.waitForExplicitFinish();
+loadNextTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_empty_directive.html b/dom/security/test/csp/test_empty_directive.html
new file mode 100644
index 0000000000..81c5df8403
--- /dev/null
+++ b/dom/security/test/csp/test_empty_directive.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1439425
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1439425</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1439425">Mozilla Bug 1439425</a>
+<p id="display"></p>
+
+<iframe id="cspframe"></iframe>
+
+<pre id="test">
+
+<script class="testbody" type="text/javascript">
+let consoleCount = 0;
+
+function cleanup() {
+ SpecialPowers.postConsoleSentinel();
+}
+
+function finish() {
+ SimpleTest.finish();
+}
+
+SpecialPowers.registerConsoleListener(function ConsoleMsgListener(aMsg) {
+ if (aMsg.message == "SENTINEL") {
+ is(consoleCount, 0);
+ SimpleTest.executeSoon(finish);
+ } else if (aMsg.message.includes("Content-Security-Policy")) {
+ ++consoleCount;
+ ok(false, "Must never see a console warning here");
+ }
+});
+
+// set up and start testing
+SimpleTest.waitForExplicitFinish();
+let frame = document.getElementById('cspframe');
+frame.onload = () => {
+ SimpleTest.executeSoon(cleanup);
+};
+frame.src = 'file_empty_directive.html';
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_evalscript.html b/dom/security/test/csp/test_evalscript.html
new file mode 100644
index 0000000000..bf1621f81e
--- /dev/null
+++ b/dom/security/test/csp/test_evalscript.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Content Security Policy "no eval" base restriction</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<iframe style="width:100%;height:300px;" id='cspframe'></iframe>
+<iframe style="width:100%;height:300px;" id='cspframe2'></iframe>
+<script class="testbody" type="text/javascript">
+
+var evalScriptsThatRan = 0;
+var evalScriptsBlocked = 0;
+var evalScriptsTotal = 19;
+
+// called by scripts that run
+var scriptRan = function(shouldrun, testname, data) {
+ evalScriptsThatRan++;
+ ok(shouldrun, 'EVAL SCRIPT RAN: ' + testname + '(' + data + ')');
+ checkTestResults();
+}
+
+// called when a script is blocked
+var scriptBlocked = function(shouldrun, testname, data) {
+ evalScriptsBlocked++;
+ ok(!shouldrun, 'EVAL SCRIPT BLOCKED: ' + testname + '(' + data + ')');
+ checkTestResults();
+}
+
+var verifyZeroRetVal = function(val, testname) {
+ ok(val === 0, 'RETURN VALUE SHOULD BE ZERO, was ' + val + ': ' + testname);
+}
+
+// Check to see if all the tests have run
+var checkTestResults = function() {
+ // if any test is incomplete, keep waiting
+ if (evalScriptsTotal - evalScriptsBlocked - evalScriptsThatRan > 0)
+ return;
+
+ // ... otherwise, finish
+ SimpleTest.finish();
+}
+
+//////////////////////////////////////////////////////////////////////
+// set up and go
+SimpleTest.waitForExplicitFinish();
+
+// save this for last so that our listeners are registered.
+// ... this loads the testbed of good and bad requests.
+document.getElementById('cspframe').src = 'file_evalscript_main.html';
+document.getElementById('cspframe2').src = 'file_evalscript_main_allowed.html';
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_evalscript_allowed_by_strict_dynamic.html b/dom/security/test/csp/test_evalscript_allowed_by_strict_dynamic.html
new file mode 100644
index 0000000000..9b06bdaf82
--- /dev/null
+++ b/dom/security/test/csp/test_evalscript_allowed_by_strict_dynamic.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Security-Policy"
+ content="script-src 'nonce-foobar' 'strict-dynamic' 'unsafe-eval'">
+ <title>Bug 1439330 - CSP: eval is not blocked if 'strict-dynamic' is enabled
+ </title>
+ <script nonce="foobar" type="application/javascript" src="/tests/SimpleTest/SimpleTest.js">
+ </script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script nonce="foobar">
+
+/* Description of the test:
+ * We apply the script-src 'nonce-foobar' 'strict-dynamic' 'unsafe-eval' CSP and
+ * check if the eval function is allowed correctly by the CSP.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+// start the test
+try {
+ // eslint-disable-next-line no-eval
+ eval("1");
+ ok(true, "eval allowed by CSP");
+}
+catch (ex) {
+ ok(false, "eval should be allowed by CSP");
+}
+
+SimpleTest.finish();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_evalscript_blocked_by_strict_dynamic.html b/dom/security/test/csp/test_evalscript_blocked_by_strict_dynamic.html
new file mode 100644
index 0000000000..ee94f250d7
--- /dev/null
+++ b/dom/security/test/csp/test_evalscript_blocked_by_strict_dynamic.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Security-Policy"
+ content="script-src 'nonce-foobar' 'strict-dynamic'">
+ <title>Bug 1439330 - CSP: eval is not blocked if 'strict-dynamic' is enabled
+ </title>
+ <script nonce="foobar" type="application/javascript" src="/tests/SimpleTest/SimpleTest.js">
+ </script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script nonce="foobar">
+
+/* Description of the test:
+ * We apply the script-src 'nonce-foobar' 'strict-dynamic' CSP and
+ * check if the eval function is blocked correctly by the CSP.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+// start the test
+try {
+ // eslint-disable-next-line no-eval
+ eval("1");
+ ok(false, "eval should be blocked by CSP");
+}
+catch (ex) {
+ ok(true, "eval blocked by CSP");
+}
+
+SimpleTest.finish();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_fontloader.html b/dom/security/test/csp/test_fontloader.html
new file mode 100644
index 0000000000..2f68223af1
--- /dev/null
+++ b/dom/security/test/csp/test_fontloader.html
@@ -0,0 +1,98 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1122236 - CSP: Implement block-all-mixed-content</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <!-- Including WindowSnapshot.js so we can take screenshots of containers !-->
+ <script src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="setupTests()">
+<iframe style="width:100%;" id="baselineframe"></iframe>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the tests:
+ * We load a baselineFrame and compare the testFrame using
+ * compareSnapshots whether the font got loaded or blocked.
+ * Test 1: Use font-src 'none' so font gets blocked
+ * Test 2: Use font-src * so font gets loaded
+ * Test 3: Use no csp so font gets loaded
+ * Test 4: Use font-src 'none' so font gets blocked
+ * Makes sure the cache gets invalidated.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+const BASE_URI = "https://example.com/tests/dom/security/test/csp/";
+
+const tests = [
+ { // test 1
+ query: "csp-block",
+ expected: true, // frames should be equal since font is *not* allowed to load
+ description: "font should be blocked by csp (csp-block)"
+ },
+ { // test 2
+ query: "csp-allow",
+ expected: false, // frames should *not* be equal since font is loaded
+ description: "font should load and apply (csp-allow)"
+ },
+ { // test 3
+ query: "no-csp",
+ expected: false, // frames should *not* be equals since font is loaded
+ description: "font should load and apply (no-csp)"
+ },
+ { // test 4
+ query: "csp-block",
+ expected: true, // frames should be equal since font is *not* allowed to load
+ description: "font should be blocked by csp (csp-block) [apply csp to cache]"
+ }
+];
+
+var curTest;
+var counter = -1;
+var baselineframe = document.getElementById("baselineframe");
+var testframe = document.getElementById("testframe");
+
+async function checkResult() {
+ testframe.removeEventListener('load', checkResult);
+ try {
+ ok(compareSnapshots(await snapshotWindow(baselineframe.contentWindow),
+ await snapshotWindow(testframe.contentWindow),
+ curTest.expected)[0],
+ curTest.description);
+ } catch(err) {
+ ok(false, "error: " + err.message);
+ }
+ loadNextTest();
+}
+
+function loadNextTest() {
+ counter++;
+ if (counter == tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+ curTest = tests[counter];
+ testframe.addEventListener("load", checkResult);
+ testframe.src = BASE_URI + "file_fontloader.sjs?" + curTest.query;
+}
+
+// once the baselineframe is loaded we can start running tests
+function startTests() {
+ baselineframe.removeEventListener('load', startTests);
+ loadNextTest();
+}
+
+// make sure the main page is loaded before we start the test
+function setupTests() {
+ baselineframe.addEventListener("load", startTests);
+ baselineframe.src = BASE_URI + "file_fontloader.sjs?baseline";
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_form-action.html b/dom/security/test/csp/test_form-action.html
new file mode 100644
index 0000000000..7bbc52a116
--- /dev/null
+++ b/dom/security/test/csp/test_form-action.html
@@ -0,0 +1,105 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 529697 - Test mapping of form submission to form-action</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="visibility: hidden">
+ <iframe style="width:100%;" id="testframe"></iframe>
+ </div>
+
+<script class="testbody" type="text/javascript">
+
+/*
+ * Description of the test:
+ * We load a page with a given CSP and verify that form submissions are correctly
+ * evaluated through the "form-action" directive.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+var tests = [
+ {
+ page : "file_form-action.html",
+ result : "allowed",
+ policy : "form-action 'self'"
+ },
+ {
+ page : "file_form-action.html",
+ result : "blocked",
+ policy : "form-action 'none'"
+ }
+];
+
+// initializing to -1 so we start at index 0 when we start the test
+var counter = -1;
+
+function checkResult(aResult) {
+ is(aResult, tests[counter].result, "should be " + tests[counter].result + " in test " + counter + "!");
+ loadNextTest();
+}
+
+// We use the examiner to identify requests that hit the wire and requests
+// that are blocked by CSP and bubble up the result to the including iframe
+// document (parent).
+function examiner() {
+ SpecialPowers.addObserver(this, "csp-on-violate-policy");
+ SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
+}
+examiner.prototype = {
+ observe(subject, topic, data) {
+ if (topic === "specialpowers-http-notify-request") {
+ // making sure we do not bubble a result for something other
+ // then the request in question.
+ if (!data.includes("submit-form")) {
+ return;
+ }
+ checkResult("allowed");
+ }
+
+ if (topic === "csp-on-violate-policy") {
+ // making sure we do not bubble a result for something other
+ // then the request in question.
+ var asciiSpec = SpecialPowers.getPrivilegedProps(
+ SpecialPowers.do_QueryInterface(subject, "nsIURI"),
+ "asciiSpec");
+ if (!asciiSpec.includes("submit-form")) {
+ return;
+ }
+ checkResult("blocked");
+ }
+ },
+ remove() {
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
+ }
+}
+window.FormActionExaminer = new examiner();
+
+function loadNextTest() {
+ counter++;
+ if (counter == tests.length) {
+ window.FormActionExaminer.remove();
+ SimpleTest.finish();
+ return;
+ }
+
+ var src = "file_testserver.sjs";
+ // append the file that should be served
+ src += "?file=" + escape("tests/dom/security/test/csp/" + tests[counter].page);
+ // append the CSP that should be used to serve the file
+ src += "&csp=" + escape(tests[counter].policy);
+
+ document.getElementById("testframe").src = src;
+}
+
+// start running the tests
+loadNextTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_form_action_blocks_url.html b/dom/security/test/csp/test_form_action_blocks_url.html
new file mode 100644
index 0000000000..f835504ff4
--- /dev/null
+++ b/dom/security/test/csp/test_form_action_blocks_url.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Bug 1251043 - Test form-action blocks URL</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <iframe id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+/*
+ * Description of the test:
+ * 1) Let's load a form into an iframe which uses a CSP of: form-action 'none';
+ * 2) Let's hit the submit button and make sure the form is not submitted.
+ *
+ * Since a blocked form submission does not fire any event handler, we have to
+ * use timeout triggered function that verifies that the form didn't get submitted.
+ */
+
+SimpleTest.requestFlakyTimeout(
+ "Form submission blocked by CSP does not fire any events " +
+ "hence we have to check back after 300ms to make sure the form " +
+ "is not submitted");
+SimpleTest.waitForExplicitFinish();
+
+const FORM_SUBMITTED = "form submission succeeded";
+var timeOutId;
+var testframe = document.getElementById("testframe");
+
+// In case the form gets submitted, the test would receive an 'load'
+// event and would trigger the test to fail early.
+function logFormSubmittedError() {
+ clearTimeout(timeOutId);
+ testframe.removeEventListener('load', logFormSubmittedError);
+ ok(false, "form submission should be blocked");
+ SimpleTest.finish();
+}
+
+// After 300ms we verify the form did not get submitted.
+function verifyFormNotSubmitted() {
+ clearTimeout(timeOutId);
+ var frameContent = testframe.contentWindow.document.body.innerHTML;
+ isnot(frameContent.indexOf("CONTROL-TEXT"), -1,
+ "form should not be submitted and still contain the control text");
+ SimpleTest.finish();
+}
+
+function submitForm() {
+ // Part 1: The form has loaded in the testframe
+ // unregister the current event handler
+ testframe.removeEventListener('load', submitForm);
+
+ // Part 2: Register a new load event handler. In case the
+ // form gets submitted, this load event fires and we can
+ // fail the test right away.
+ testframe.addEventListener("load", logFormSubmittedError);
+
+ // Part 3: Since blocking the form does not throw any kind of error;
+ // Firefox just logs the CSP error to the console we have to register
+ // this timeOut function which then verifies that the form didn't
+ // get submitted.
+ timeOutId = setTimeout(verifyFormNotSubmitted, 300);
+
+ // Part 4: We are ready, let's hit the submit button of the form.
+ var submitButton = testframe.contentWindow.document.getElementById('submitButton');
+ submitButton.click();
+}
+
+testframe.addEventListener("load", submitForm);
+testframe.src = "file_form_action_server.sjs?loadframe";
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_frame_ancestors_ro.html b/dom/security/test/csp/test_frame_ancestors_ro.html
new file mode 100644
index 0000000000..1cfe6be1cd
--- /dev/null
+++ b/dom/security/test/csp/test_frame_ancestors_ro.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for frame-ancestors support in Content-Security-Policy-Report-Only</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width: 100%" id="cspframe"></iframe>
+<script type="text/javascript">
+const docUri = "http://mochi.test:8888/tests/dom/security/test/csp/file_frame_ancestors_ro.html";
+const frame = document.getElementById("cspframe");
+
+let testResults = {
+ reportFired: false,
+ frameLoaded: false
+};
+
+function checkResults(reportObj) {
+ let cspReport = reportObj["csp-report"];
+ is(cspReport["document-uri"], docUri, "Incorrect document-uri");
+
+ // we can not test for the whole referrer since it includes platform specific information
+ is(cspReport.referrer, document.location.toString(), "Incorrect referrer");
+ is(cspReport["blocked-uri"], document.location.toString(), "Incorrect blocked-uri");
+ is(cspReport["violated-directive"], "frame-ancestors", "Incorrect violated-directive");
+ is(cspReport["original-policy"], "frame-ancestors 'none'; report-uri http://mochi.test:8888/foo.sjs", "Incorrect original-policy");
+ testResults.reportFired = true;
+}
+
+let chromeScriptUrl = SimpleTest.getTestFileURL("file_report_chromescript.js");
+let script = SpecialPowers.loadChromeScript(chromeScriptUrl);
+
+script.addMessageListener('opening-request-completed', function ml(msg) {
+ if (msg.error) {
+ ok(false, "Could not query report (exception: " + msg.error + ")");
+ } else {
+ try {
+ let reportObj = JSON.parse(msg.report);
+ // test for the proper values in the report object
+ checkResults(reportObj);
+ } catch (e) {
+ ok(false, "Error verifying report object (exception: " + e + ")");
+ }
+ }
+
+ script.removeMessageListener('opening-request-completed', ml);
+ script.sendAsyncMessage("finish");
+ checkTestResults();
+});
+
+frame.addEventListener( 'load', () => {
+ // Make sure the frame is still loaded
+ testResults.frameLoaded = true;
+ checkTestResults()
+} );
+
+function checkTestResults() {
+ if( testResults.reportFired && testResults.frameLoaded ) {
+ SimpleTest.finish();
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+frame.src = docUri;
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_frame_src.html b/dom/security/test/csp/test_frame_src.html
new file mode 100644
index 0000000000..f87549b72b
--- /dev/null
+++ b/dom/security/test/csp/test_frame_src.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1302667 - Test frame-src</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+/* Description of the test:
+ * We load a page inlcuding a frame a CSP of:
+ * >> frame-src https://example.com; child-src 'none'
+ * and make sure that frame-src governs frames correctly. In addition,
+ * we make sure that child-src is discarded in case frame-src is specified.
+ */
+
+const ORIGIN_1 = "https://example.com/tests/dom/security/test/csp/";
+const ORIGIN_2 = "https://test1.example.com/tests/dom/security/test/csp/";
+
+let TESTS = [
+ // frame-src tests
+ ORIGIN_1 + "file_frame_src_frame_governs.html",
+ ORIGIN_2 + "file_frame_src_frame_governs.html",
+ // child-src tests
+ ORIGIN_1 + "file_frame_src_child_governs.html",
+ ORIGIN_2 + "file_frame_src_child_governs.html",
+];
+
+let testIndex = 0;
+
+function checkFinish() {
+ if (testIndex >= TESTS.length) {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+ return;
+ }
+ runNextTest();
+}
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ let href = event.data.href;
+ let result = event.data.result;
+
+ if (href.startsWith("https://example.com")) {
+ if (result == "frame-allowed") {
+ ok(true, "allowing frame from https://example.com (" + result + ")");
+ }
+ else {
+ ok(false, "blocking frame from https://example.com (" + result + ")");
+ }
+ }
+ else if (href.startsWith("https://test1.example.com")) {
+ if (result == "frame-blocked") {
+ ok(true, "blocking frame from https://test1.example.com (" + result + ")");
+ }
+ else {
+ ok(false, "allowing frame from https://test1.example.com (" + result + ")");
+ }
+ }
+ else {
+ // sanity check, we should never enter that branch, bust just in case...
+ ok(false, "unexpected result: " + result);
+ }
+ checkFinish();
+}
+
+function runNextTest() {
+ document.getElementById("testframe").src = TESTS[testIndex];
+ testIndex++;
+}
+
+// fire up the tests
+runNextTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_frameancestors.html b/dom/security/test/csp/test_frameancestors.html
new file mode 100644
index 0000000000..8b44ba72fb
--- /dev/null
+++ b/dom/security/test/csp/test_frameancestors.html
@@ -0,0 +1,160 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Content Security Policy Frame Ancestors directive</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<iframe style="width:100%;height:300px;" id='cspframe'></iframe>
+<script class="testbody" type="text/javascript">
+
+// These are test results: -1 means it hasn't run,
+// true/false is the pass/fail result.
+var framesThatShouldLoad = {
+ aa_allow: -1, /* innermost frame allows a *
+ //aa_block: -1, /* innermost frame denies a */
+ ab_allow: -1, /* innermost frame allows a */
+ //ab_block: -1, /* innermost frame denies a */
+ aba_allow: -1, /* innermost frame allows b,a */
+ //aba_block: -1, /* innermost frame denies b */
+ //aba2_block: -1, /* innermost frame denies a */
+ abb_allow: -1, /* innermost frame allows b,a */
+ //abb_block: -1, /* innermost frame denies b */
+ //abb2_block: -1, /* innermost frame denies a */
+};
+
+// we normally expect _6_ violations (6 test cases that cause blocks), but many
+// of the cases cause violations due to the // origin of the test harness (same
+// as 'a'). When the violation is cross-origin, the URI passed to the observers
+// is null (see bug 846978). This means we can't tell if it's part of the test
+// case or if it is the test harness frame (also origin 'a').
+// As a result, we'll get an extra violation for the following cases:
+// ab_block "frame-ancestors 'none'" (outer frame and test harness)
+// aba2_block "frame-ancestors b" (outer frame and test harness)
+// abb2_block "frame-ancestors b" (outer frame and test harness)
+//
+// and while we can detect the test harness check for this one case since
+// the violations are not cross-origin and we get the URI:
+// aba2_block "frame-ancestors b" (outer frame and test harness)
+//
+// we can't for these other ones:
+// ab_block "frame-ancestors 'none'" (outer frame and test harness)
+// abb2_block "frame-ancestors b" (outer frame and test harness)
+//
+// so that results in 2 extra violation notifications due to the test harness.
+// expected = 6, total = 8
+//
+// Number of tests that pass for this file should be 12 (8 violations 4 loads)
+var expectedViolationsLeft = 8;
+
+// CSP frame-ancestor checks happen in the parent, hence we have to
+// proxy the csp violation notifications.
+SpecialPowers.registerObservers("csp-on-violate-policy");
+
+// This is used to watch the blocked data bounce off CSP and allowed data
+// get sent out to the wire.
+function examiner() {
+ SpecialPowers.addObserver(this, "specialpowers-csp-on-violate-policy");
+}
+examiner.prototype = {
+ observe(subject, topic, data) {
+ // subject should be an nsURI... though could be null since CSP
+ // prohibits cross-origin URI reporting during frame ancestors checks.
+ if (subject && !SpecialPowers.can_QI(subject))
+ return;
+
+ var asciiSpec = subject;
+
+ try {
+ asciiSpec = SpecialPowers.getPrivilegedProps(
+ SpecialPowers.do_QueryInterface(subject, "nsIURI"),
+ "asciiSpec");
+
+ // skip checks on the test harness -- can't do this skipping for
+ // cross-origin blocking since the observer doesn't get the URI. This
+ // can cause this test to over-succeed (but only in specific cases).
+ if (asciiSpec.includes("test_frameancestors.html")) {
+ return;
+ }
+ } catch (ex) {
+ // was not an nsIURI, so it was probably a cross-origin report.
+ }
+
+ if (topic === "specialpowers-csp-on-violate-policy") {
+ //these were blocked... record that they were blocked
+ window.frameBlocked(asciiSpec, data);
+ }
+ },
+
+ // must eventually call this to remove the listener,
+ // or mochitests might get borked.
+ remove() {
+ SpecialPowers.removeObserver(this, "specialpowers-csp-on-violate-policy");
+ }
+}
+
+// called when a frame is loaded
+// -- if it's not enumerated above, it should not load!
+var frameLoaded = function(testname, uri) {
+ //test already complete.... forget it... remember the first result.
+ if (window.framesThatShouldLoad[testname] != -1)
+ return;
+
+ if (typeof window.framesThatShouldLoad[testname] === 'undefined') {
+ // uh-oh, we're not expecting this frame to load!
+ ok(false, testname + ' framed site should not have loaded: ' + uri);
+ } else {
+ framesThatShouldLoad[testname] = true;
+ ok(true, testname + ' framed site when allowed by csp (' + uri + ')');
+ }
+ checkTestResults();
+}
+
+// called when a frame is blocked
+// -- we can't determine *which* frame was blocked, but at least we can count them
+var frameBlocked = function(uri, policy) {
+ ok(true, 'a CSP policy blocked frame from being loaded: ' + policy);
+ expectedViolationsLeft--;
+ checkTestResults();
+}
+
+
+// Check to see if all the tests have run
+var checkTestResults = function() {
+ // if any test is incomplete, keep waiting
+ for (var v in framesThatShouldLoad)
+ if(window.framesThatShouldLoad[v] == -1)
+ return;
+
+ if (window.expectedViolationsLeft > 0)
+ return;
+
+ // ... otherwise, finish
+ window.examiner.remove();
+ SimpleTest.finish();
+}
+
+window.addEventListener("message", receiveMessage);
+
+function receiveMessage(event) {
+ if (event.data.call && event.data.call == 'frameLoaded')
+ frameLoaded(event.data.testname, event.data.uri);
+}
+
+//////////////////////////////////////////////////////////////////////
+// set up and go
+window.examiner = new examiner();
+SimpleTest.waitForExplicitFinish();
+
+// save this for last so that our listeners are registered.
+// ... this loads the testbed of good and bad requests.
+document.getElementById('cspframe').src = 'file_frameancestors_main.html';
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_frameancestors_userpass.html b/dom/security/test/csp/test_frameancestors_userpass.html
new file mode 100644
index 0000000000..332318fe17
--- /dev/null
+++ b/dom/security/test/csp/test_frameancestors_userpass.html
@@ -0,0 +1,148 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Userpass in Frame Ancestors directive</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<iframe style="width:100%;height:300px;" id='cspframe'></iframe>
+<script class="testbody" type="text/javascript">
+
+// These are test results: -1 means it hasn't run,
+// true/false is the pass/fail result.
+var framesThatShouldLoad = {
+ frame_a: -1, /* frame a allowed */
+ frame_b: -1, /* frame b allowed */
+};
+
+// Number of tests that pass for this file should be 1
+var expectedViolationsLeft = 1;
+
+// CSP frame-ancestor checks happen in the parent, hence we have to
+// proxy the csp violation notifications.
+SpecialPowers.registerObservers("csp-on-violate-policy");
+
+// This is used to watch the blocked data bounce off CSP and allowed data
+// get sent out to the wire.
+function examiner() {
+ SpecialPowers.addObserver(this, "specialpowers-csp-on-violate-policy");
+}
+examiner.prototype = {
+ observe(subject, topic, data) {
+ // subject should be an nsURI... though could be null since CSP
+ // prohibits cross-origin URI reporting during frame ancestors checks.
+ if (subject && !SpecialPowers.can_QI(subject))
+ return;
+
+ var asciiSpec = subject;
+
+ try {
+ asciiSpec = SpecialPowers.getPrivilegedProps(
+ SpecialPowers.do_QueryInterface(subject, "nsIURI"),
+ "asciiSpec");
+
+ // skip checks on the test harness -- can't do this skipping for
+ // cross-origin blocking since the observer doesn't get the URI. This
+ // can cause this test to over-succeed (but only in specific cases).
+ if (asciiSpec.includes("test_frameancestors_userpass.html")) {
+ return;
+ }
+ } catch (ex) {
+ // was not an nsIURI, so it was probably a cross-origin report.
+ }
+
+ if (topic === "specialpowers-csp-on-violate-policy") {
+ //these were blocked... record that they were blocked
+ window.frameBlocked(asciiSpec, data);
+ }
+ },
+
+ // must eventually call this to remove the listener,
+ // or mochitests might get borked.
+ remove() {
+ SpecialPowers.removeObserver(this, "specialpowers-csp-on-violate-policy");
+ }
+}
+
+// called when a frame is loaded
+// -- if it's not enumerated above, it should not load!
+var frameLoaded = function(testname, uri) {
+ //test already complete.... forget it... remember the first result.
+ if (window.framesThatShouldLoad[testname] != -1)
+ return;
+
+ if (typeof window.framesThatShouldLoad[testname] === 'undefined') {
+ // uh-oh, we're not expecting this frame to load!
+ ok(false, testname + ' framed site should not have loaded: ' + uri);
+ } else {
+ //Check if @ symbol is there in URI.
+ if (uri.includes('@')) {
+ ok(false, ' URI contains userpass. Fetched URI is ' + uri);
+ } else {
+ framesThatShouldLoad[testname] = true;
+ ok(true, ' URI doesn\'t contain userpass. Fetched URI is ' + uri);
+ }
+ }
+ checkTestResults();
+}
+
+// called when a frame is blocked
+// -- we can't determine *which* frame was blocked, but at least we can count them
+var frameBlocked = function(uri, policy) {
+
+ //Check if @ symbol is there in URI or in csp policy.
+ // Bug 1557712: Intermittent failure -> not sure why the 'uri' might ever
+ // be non existing at this point, however if there is no uri, there can
+ // also be no userpass!
+ if (policy.includes('@') ||
+ (typeof uri === 'string' && uri.includes('@'))) {
+ ok(false, ' a CSP policy blocked frame from being loaded. But contains' +
+ ' userpass. Policy is: ' + policy + ';URI is: ' + uri );
+ } else {
+ ok(true, ' a CSP policy blocked frame from being loaded. Doesn\'t contain'+
+ ' userpass. Policy is: ' + policy + ';URI is: ' + uri );
+ }
+ expectedViolationsLeft--;
+ checkTestResults();
+}
+
+
+// Check to see if all the tests have run
+var checkTestResults = function() {
+ // if any test is incomplete, keep waiting
+ for (var v in framesThatShouldLoad)
+ if(window.framesThatShouldLoad[v] == -1)
+ return;
+
+ if (window.expectedViolationsLeft > 0)
+ return;
+
+ // ... otherwise, finish
+ window.examiner.remove();
+ SimpleTest.finish();
+}
+
+window.addEventListener("message", receiveMessage);
+
+function receiveMessage(event) {
+ if (event.data.call && event.data.call == 'frameLoaded')
+ frameLoaded(event.data.testname, event.data.uri);
+}
+
+//////////////////////////////////////////////////////////////////////
+// set up and go
+window.examiner = new examiner();
+SimpleTest.waitForExplicitFinish();
+
+// save this for last so that our listeners are registered.
+// ... this loads the testbed of good and bad requests.
+document.getElementById('cspframe').src = 'file_frameancestors_userpass.html';
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_hash_source.html b/dom/security/test/csp/test_hash_source.html
new file mode 100644
index 0000000000..2334ae0101
--- /dev/null
+++ b/dom/security/test/csp/test_hash_source.html
@@ -0,0 +1,135 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test CSP 1.1 hash-source for inline scripts and styles</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="visibility:hidden">
+ <iframe style="width:100%;" id='cspframe'></iframe>
+</div>
+<script class="testbody" type="text/javascript">
+
+function cleanup() {
+ // finish the tests
+ SimpleTest.finish();
+}
+
+function checkInline () {
+ var cspframe = document.getElementById('cspframe').contentDocument;
+
+ var inlineScriptTests = {
+ 'inline-script-valid-hash': {
+ shouldBe: 'allowed',
+ message: 'Inline script with valid hash should be allowed'
+ },
+ 'inline-script-invalid-hash': {
+ shouldBe: 'blocked',
+ message: 'Inline script with invalid hash should be blocked'
+ },
+ 'inline-script-invalid-hash-valid-nonce': {
+ shouldBe: 'allowed',
+ message: 'Inline script with invalid hash and valid nonce should be allowed'
+ },
+ 'inline-script-valid-hash-invalid-nonce': {
+ shouldBe: 'allowed',
+ message: 'Inline script with valid hash and invalid nonce should be allowed'
+ },
+ 'inline-script-invalid-hash-invalid-nonce': {
+ shouldBe: 'blocked',
+ message: 'Inline script with invalid hash and invalid nonce should be blocked'
+ },
+ 'inline-script-valid-sha512-hash': {
+ shouldBe: 'allowed',
+ message: 'Inline script with a valid sha512 hash should be allowed'
+ },
+ 'inline-script-valid-sha384-hash': {
+ shouldBe: 'allowed',
+ message: 'Inline script with a valid sha384 hash should be allowed'
+ },
+ 'inline-script-valid-sha1-hash': {
+ shouldBe: 'blocked',
+ message: 'Inline script with a valid sha1 hash should be blocked, because sha1 is not a valid hash function'
+ },
+ 'inline-script-valid-md5-hash': {
+ shouldBe: 'blocked',
+ message: 'Inline script with a valid md5 hash should be blocked, because md5 is not a valid hash function'
+ }
+ }
+
+ for (testId in inlineScriptTests) {
+ var test = inlineScriptTests[testId];
+ is(cspframe.getElementById(testId).innerHTML, test.shouldBe, test.message);
+ }
+
+ // Inline style tries to change an element's color to green. If blocked, the
+ // element's color will be the default black.
+ var green = "rgb(0, 128, 0)";
+ var black = "rgb(0, 0, 0)";
+
+ var getElementColorById = function (id) {
+ return window.getComputedStyle(cspframe.getElementById(id)).color;
+ };
+
+ var inlineStyleTests = {
+ 'inline-style-valid-hash': {
+ shouldBe: green,
+ message: 'Inline style with valid hash should be allowed'
+ },
+ 'inline-style-invalid-hash': {
+ shouldBe: black,
+ message: 'Inline style with invalid hash should be blocked'
+ },
+ 'inline-style-invalid-hash-valid-nonce': {
+ shouldBe: green,
+ message: 'Inline style with invalid hash and valid nonce should be allowed'
+ },
+ 'inline-style-valid-hash-invalid-nonce': {
+ shouldBe: green,
+ message: 'Inline style with valid hash and invalid nonce should be allowed'
+ },
+ 'inline-style-invalid-hash-invalid-nonce' : {
+ shouldBe: black,
+ message: 'Inline style with invalid hash and invalid nonce should be blocked'
+ },
+ 'inline-style-valid-sha512-hash': {
+ shouldBe: green,
+ message: 'Inline style with a valid sha512 hash should be allowed'
+ },
+ 'inline-style-valid-sha384-hash': {
+ shouldBe: green,
+ message: 'Inline style with a valid sha384 hash should be allowed'
+ },
+ 'inline-style-valid-sha1-hash': {
+ shouldBe: black,
+ message: 'Inline style with a valid sha1 hash should be blocked, because sha1 is not a valid hash function'
+ },
+ 'inline-style-valid-md5-hash': {
+ shouldBe: black,
+ message: 'Inline style with a valid md5 hash should be blocked, because md5 is not a valid hash function'
+ }
+ }
+
+ for (testId in inlineStyleTests) {
+ var test = inlineStyleTests[testId];
+ is(getElementColorById(testId), test.shouldBe, test.message);
+ }
+
+ cleanup();
+}
+
+//////////////////////////////////////////////////////////////////////
+// set up and go
+SimpleTest.waitForExplicitFinish();
+
+// save this for last so that our listeners are registered.
+// ... this loads the testbed of good and bad requests.
+document.getElementById('cspframe').src = 'file_hash_source.html';
+document.getElementById('cspframe').addEventListener('load', checkInline);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_iframe_sandbox.html b/dom/security/test/csp/test_iframe_sandbox.html
new file mode 100644
index 0000000000..cd7417bc8b
--- /dev/null
+++ b/dom/security/test/csp/test_iframe_sandbox.html
@@ -0,0 +1,240 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=671389
+Bug 671389 - Implement CSP sandbox directive
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tests for Bug 671389</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script type="application/javascript">
+
+ SimpleTest.waitForExplicitFinish();
+
+ // Check if two sandbox flags are the same, ignoring case-sensitivity.
+ // getSandboxFlags returns a list of sandbox flags (if any) or
+ // null if the flag is not set.
+ // This function checks if two flags are the same, i.e., they're
+ // either not set or have the same flags.
+ function eqFlags(a, b) {
+ if (a === null && b === null) { return true; }
+ if (a === null || b === null) { return false; }
+ if (a.length !== b.length) { return false; }
+ var a_sorted = a.map(function(e) { return e.toLowerCase(); }).sort();
+ var b_sorted = b.map(function(e) { return e.toLowerCase(); }).sort();
+ for (var i in a_sorted) {
+ if (a_sorted[i] !== b_sorted[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // Get the sandbox flags of document doc.
+ // If the flag is not set sandboxFlagsAsString returns null,
+ // this function also returns null.
+ // If the flag is set it may have some flags; in this case
+ // this function returns the (potentially empty) list of flags.
+ function getSandboxFlags(doc) {
+ var flags = doc.sandboxFlagsAsString;
+ if (flags === null) { return null; }
+ return flags? flags.split(" "):[];
+ }
+
+ // Constructor for a CSP sandbox flags test. The constructor
+ // expectes a description 'desc' and set of options 'opts':
+ // - sandboxAttribute: [null] or string corresponding to the iframe sandbox attributes
+ // - csp: [null] or string corresponding to the CSP sandbox flags
+ // - cspReportOnly: [null] or string corresponding to the CSP report-only sandbox flags
+ // - file: [null] or string corresponding to file the server should serve
+ // Above, we use [brackets] to denote default values.
+ function CSPFlagsTest(desc, opts) {
+ function ifundef(x, v) {
+ return (x !== undefined) ? x : v;
+ }
+
+ function intersect(as, bs) { // Intersect two csp attributes:
+ as = as === null ? null
+ : as.split(' ').filter(function(x) { return !!x; });
+ bs = bs === null ? null
+ : bs.split(' ').filter(function(x) { return !!x; });
+
+ if (as === null) { return bs; }
+ if (bs === null) { return as; }
+
+ var cs = [];
+ as.forEach(function(a) {
+ if (a && bs.includes(a))
+ cs.push(a);
+ });
+ return cs;
+ }
+
+ this.desc = desc || "Untitled test";
+ this.attr = ifundef(opts.sandboxAttribute, null);
+ this.csp = ifundef(opts.csp, null);
+ this.cspRO = ifundef(opts.cspReportOnly, null);
+ this.file = ifundef(opts.file, null);
+ this.expected = intersect(this.attr, this.csp);
+ }
+
+ // Return function that checks that the actual flags are the same as the
+ // expected flags
+ CSPFlagsTest.prototype.checkFlags = function(iframe) {
+ var this_ = this;
+ return function() {
+ try {
+ var actual = getSandboxFlags(SpecialPowers.wrap(iframe).contentDocument);
+ ok(eqFlags(actual, this_.expected),
+ this_.desc + ' - expected: "' + this_.expected + '", got: "' + actual + '"');
+ } catch (e) {
+ ok(false,
+ this_.desc + ' - expected: "' + this_.expected + '", failed with: "' + e + '"');
+ }
+ runNextTest();
+ };
+ };
+
+ // Set the iframe src and sandbox attribute
+ CSPFlagsTest.prototype.runTest = function () {
+ var iframe = document.createElement('iframe');
+ document.getElementById("content").appendChild(iframe);
+ iframe.onload = this.checkFlags(iframe);
+
+ // set sandbox attribute
+ if (this.attr === null) {
+ iframe.removeAttribute('sandbox');
+ } else {
+ iframe.sandbox = this.attr;
+ }
+
+ // set query string
+ var src = 'http://mochi.test:8888/tests/dom/security/test/csp/file_testserver.sjs';
+
+ var delim = '?';
+
+ if (this.csp !== null) {
+ src += delim + 'csp=' + escape('sandbox ' + this.csp);
+ delim = '&';
+ }
+
+ if (this.cspRO !== null) {
+ src += delim + 'cspRO=' + escape('sandbox ' + this.cspRO);
+ delim = '&';
+ }
+
+ if (this.file !== null) {
+ src += delim + 'file=' + escape(this.file);
+ delim = '&';
+ }
+
+ iframe.src = src;
+ iframe.width = iframe.height = 10;
+
+ }
+
+ testCases = [
+ {
+ desc: "Test 1: Header should not override attribute",
+ sandboxAttribute: "",
+ csp: "allow-forms aLLOw-POinter-lock alLOW-popups aLLOW-SAME-ORIGin ALLOW-SCRIPTS allow-top-navigation"
+ },
+ {
+ desc: "Test 2: Attribute should not override header",
+ sandboxAttribute: "sandbox allow-forms allow-pointer-lock allow-popups allow-same-origin allow-scripts allow-top-navigation",
+ csp: ""
+ },
+ {
+ desc: "Test 3: Header and attribute intersect",
+ sandboxAttribute: "allow-same-origin allow-scripts",
+ csp: "allow-forms allow-same-origin allow-scripts"
+ },
+ {
+ desc: "Test 4: CSP sandbox sets the right flags (pt 1)",
+ csp: "alLOW-FORms ALLOW-pointer-lock allow-popups allow-same-origin allow-scripts ALLOW-TOP-NAVIGation"
+ },
+ {
+ desc: "Test 5: CSP sandbox sets the right flags (pt 2)",
+ csp: "allow-same-origin allow-TOP-navigation"
+ },
+ {
+ desc: "Test 6: CSP sandbox sets the right flags (pt 3)",
+ csp: "allow-FORMS ALLOW-scripts"
+ },
+ {
+ desc: "Test 7: CSP sandbox sets the right flags (pt 4)",
+ csp: ""
+ },
+ {
+ desc: "Test 8: CSP sandbox sets the right flags (pt 5)",
+ csp: null
+ },
+ {
+ desc: "Test 9: Read-only header should not override attribute",
+ sandboxAttribute: "",
+ cspReportOnly: "allow-forms ALLOW-pointer-lock allow-POPUPS allow-same-origin ALLOW-scripts allow-top-NAVIGATION"
+ },
+ {
+ desc: "Test 10: Read-only header should not override CSP header",
+ csp: "allow-forms allow-scripts",
+ cspReportOnly: "allow-forms aLlOw-PoInTeR-lOcK aLLow-pOPupS aLLoW-SaME-oRIgIN alLow-scripts allow-tOp-navigation"
+ },
+ {
+ desc: "Test 11: Read-only header should not override attribute or CSP header",
+ sandboxAttribute: "allow-same-origin allow-scripts",
+ csp: "allow-forms allow-same-origin allow-scripts",
+ cspReportOnly: "allow-forms allow-pointer-lock allow-popups allow-same-origin allow-scripts allow-top-navigation"
+ },
+ {
+ desc: "Test 12: CSP sandbox not affected by document.write()",
+ csp: "allow-scripts",
+ file: 'tests/dom/security/test/csp/file_iframe_sandbox_document_write.html'
+ },
+ ].map(function(t) { return (new CSPFlagsTest(t.desc,t)); });
+
+
+ var testCaseIndex = 0;
+
+ // Track ok messages from iframes
+ var childMessages = 0;
+ var totalChildMessages = 1;
+
+
+ // Check to see if we ran all the tests and received all messges
+ // from child iframes. If so, finish.
+ function tryFinish() {
+ if (testCaseIndex === testCases.length && childMessages === totalChildMessages){
+ SimpleTest.finish();
+ }
+ }
+
+ function runNextTest() {
+
+ tryFinish();
+
+ if (testCaseIndex < testCases.length) {
+ testCases[testCaseIndex].runTest();
+ testCaseIndex++;
+ }
+ }
+
+ function receiveMessage(event) {
+ ok(event.data.ok, event.data.desc);
+ childMessages++;
+ tryFinish();
+ }
+
+ window.addEventListener("message", receiveMessage);
+
+ addLoadEvent(runNextTest);
+</script>
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=671389">Mozilla Bug 671389</a> - Implement CSP sandbox directive
+ <p id="display"></p>
+ <div id="content">
+ </div>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_iframe_sandbox_srcdoc.html b/dom/security/test/csp/test_iframe_sandbox_srcdoc.html
new file mode 100644
index 0000000000..9c36aa5447
--- /dev/null
+++ b/dom/security/test/csp/test_iframe_sandbox_srcdoc.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1073952 - CSP should restrict scripts in srcdoc iframe even if sandboxed</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display">Bug 1073952</p>
+<iframe style="width:200px;height:200px;" id='cspframe'></iframe>
+<script class="testbody" type="text/javascript">
+
+// This is used to watch the blocked data bounce off CSP and allowed data
+// get sent out to the wire.
+function examiner() {
+ SpecialPowers.addObserver(this, "csp-on-violate-policy");
+}
+
+examiner.prototype = {
+ observe(subject, topic, data) {
+
+ if(topic === "csp-on-violate-policy") {
+ var violationString = SpecialPowers.getPrivilegedProps(SpecialPowers.
+ do_QueryInterface(subject, "nsISupportsCString"), "data");
+ // the violation subject for inline script violations is unfortunately vague,
+ // all we can do is match the string.
+ if (!violationString.includes("Inline Script")) {
+ return
+ }
+ ok(true, "CSP inherited into sandboxed srcdoc iframe, script blocked.");
+ window.testFinished();
+ }
+ },
+
+ // must eventually call this to remove the listener,
+ // or mochitests might get borked.
+ remove() {
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ }
+}
+
+window.examiner = new examiner();
+
+function testFinished() {
+ window.examiner.remove();
+ SimpleTest.finish();
+}
+
+addEventListener("message", function(e) {
+ ok(false, "We should not execute JS in srcdoc iframe.");
+ window.testFinished();
+})
+SimpleTest.waitForExplicitFinish();
+
+// save this for last so that our listeners are registered.
+// ... this loads the testbed of good and bad requests.
+document.getElementById('cspframe').src = 'file_iframe_sandbox_srcdoc.html';
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_iframe_sandbox_top_1.html b/dom/security/test/csp/test_iframe_sandbox_top_1.html
new file mode 100644
index 0000000000..c1ade7ac6c
--- /dev/null
+++ b/dom/security/test/csp/test_iframe_sandbox_top_1.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=671389
+Bug 671389 - Implement CSP sandbox directive
+
+Tests CSP sandbox attribute on top-level page.
+
+Minimal flags: allow-same-origin allow-scripts:
+Since we need to load the SimpleTest files, we have to set the
+allow-same-origin flag. Additionally, we set the allow-scripts flag
+since we need JS to check the flags.
+
+Though not necessary, for this test we also set the allow-forms flag.
+We may later wish to extend the testing suite with sandbox_csp_top_*
+tests that set different permutations of the flags.
+
+CSP header: Content-Security-Policy: sandbox allow-forms allow-scripts allow-same-origin
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tests for Bug 671389</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// Check if two sandbox flags are the same.
+// getSandboxFlags returns a list of sandbox flags (if any) or
+// null if the flag is not set.
+// This function checks if two flags are the same, i.e., they're
+// either not set or have the same flags.
+function eqFlags(a, b) {
+ if (a === null && b === null) { return true; }
+ if (a === null || b === null) { return false; }
+ if (a.length !== b.length) { return false; }
+ var a_sorted = a.sort();
+ var b_sorted = b.sort();
+ for (var i in a_sorted) {
+ if (a_sorted[i] !== b_sorted[i]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+// Get the sandbox flags of document doc.
+// If the flag is not set sandboxFlagsAsString returns null,
+// this function also returns null.
+// If the flag is set it may have some flags; in this case
+// this function returns the (potentially empty) list of flags.
+function getSandboxFlags(doc) {
+ var flags = doc.sandboxFlagsAsString;
+ if (flags === null) { return null; }
+ return flags? flags.split(" "):[];
+}
+
+function checkFlags(expected) {
+ try {
+ var flags = getSandboxFlags(SpecialPowers.wrap(document));
+ ok(eqFlags(flags, expected), name + ' expected: "' + expected + '", got: "' + flags + '"');
+ } catch (e) {
+ ok(false, name + ' expected "' + expected + ', but failed with ' + e);
+ }
+ SimpleTest.finish();
+}
+
+</script>
+
+<body onLoad='checkFlags(["allow-forms", "allow-scripts", "allow-same-origin"]);'>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=671389">Mozilla Bug 671389</a> - Implement CSP sandbox directive
+<p id="display"></p>
+<div id="content">
+ I am a top-level page sandboxed with "allow-scripts allow-forms
+ allow-same-origin".
+</div>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_iframe_sandbox_top_1.html^headers^ b/dom/security/test/csp/test_iframe_sandbox_top_1.html^headers^
new file mode 100644
index 0000000000..d9cd0606e7
--- /dev/null
+++ b/dom/security/test/csp/test_iframe_sandbox_top_1.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: sAnDbOx aLLow-FOrms aLlOw-ScRiPtS ALLOW-same-origin
diff --git a/dom/security/test/csp/test_iframe_srcdoc.html b/dom/security/test/csp/test_iframe_srcdoc.html
new file mode 100644
index 0000000000..04694aa5e0
--- /dev/null
+++ b/dom/security/test/csp/test_iframe_srcdoc.html
@@ -0,0 +1,140 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1073952 - Test CSP enforcement within iframe srcdoc</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/*
+ * Description of the test:
+ * (1) We serve a site which makes use of script-allowed sandboxed iframe srcdoc
+ * and make sure that CSP applies to the nested browsing context
+ * within the iframe.
+ * [PAGE WITH CSP [IFRAME SANDBOX SRCDOC [SCRIPT]]]
+ *
+ * (2) We serve a site which nests script within an script-allowed sandboxed
+ * iframe srcdoc within another script-allowed sandboxed iframe srcdoc and
+ * make sure that CSP applies to the nested browsing context
+ * within the iframe*s*.
+ * [PAGE WITH CSP [IFRAME SANDBOX SRCDOC [IFRAME SANDBOX SRCDOC [SCRIPT]]]]
+ *
+ * Please note that the test relies on the "csp-on-violate-policy" observer.
+ * Whenever the script within the iframe is blocked observers are notified.
+ * In turn, this renders the 'result' within tests[] unused. In case the script
+ * would execute however, the postMessageHandler would bubble up 'allowed' and
+ * the test would fail.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+var tests = [
+ // [PAGE *WITHOUT* CSP [IFRAME SRCDOC [SCRIPT]]]
+ { csp: "",
+ result: "allowed",
+ query: "simple_iframe_srcdoc",
+ desc: "No CSP should run script within script-allowed sandboxed iframe srcdoc"
+ },
+ { csp: "script-src https://test1.com",
+ result: "blocked",
+ query: "simple_iframe_srcdoc",
+ desc: "CSP should block script within script-allowed sandboxediframe srcdoc"
+ },
+ // [PAGE *WITHOUT* CSP [IFRAME SRCDOC [IFRAME SRCDOC [SCRIPT]]]]
+ { csp: "",
+ result: "allowed",
+ query: "nested_iframe_srcdoc",
+ desc: "No CSP should run script within script-allowed sandboxed iframe srcdoc nested within another script-allowed sandboxed iframe srcdoc"
+ },
+ // [PAGE WITH CSP [IFRAME SRCDOC ]]
+ { csp: "script-src https://test2.com",
+ result: "blocked",
+ query: "nested_iframe_srcdoc",
+ desc: "CSP should block script within script-allowed sandboxed iframe srcdoc nested within another script-allowed sandboxed iframe srcdoc"
+ },
+ { csp: "",
+ result: "allowed",
+ query: "nested_iframe_srcdoc_datauri",
+ desc: "No CSP, should run script within script-allowed sandboxed iframe src with data URL nested within another script-allowed sandboxed iframe srcdoc"
+ },
+ { csp: "script-src https://test3.com",
+ result: "blocked",
+ query: "nested_iframe_srcdoc_datauri",
+ desc: "CSP should block script within script-allowed sandboxed iframe src with data URL nested within another script-allowed sandboxed iframe srcdoc"
+ },
+
+];
+
+// initializing to -1 so we start at index 0 when we start the test
+var counter = -1;
+
+function finishTest() {
+ window.removeEventListener("message", receiveMessage);
+ window.examiner.remove();
+ SimpleTest.finish();
+}
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ var result = event.data.result;
+ testComplete(result, tests[counter].result, tests[counter].desc);
+}
+
+function examiner() {
+ SpecialPowers.addObserver(this, "csp-on-violate-policy");
+}
+
+examiner.prototype = {
+ observe(subject, topic, data) {
+ if (topic === "csp-on-violate-policy") {
+ var violationString = SpecialPowers.getPrivilegedProps(SpecialPowers.
+ do_QueryInterface(subject, "nsISupportsCString"), "data");
+ // the violation subject for inline script violations is unfortunately vague,
+ // all we can do is match the string.
+ if (!violationString.includes("Inline Script")) {
+ return
+ }
+ testComplete("blocked", tests[counter].result, tests[counter].desc);
+ }
+ },
+ remove() {
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ }
+}
+
+function testComplete(result, expected, desc) {
+ is(result, expected, desc);
+ // ignore cases when we get csp violations and postMessage from the same frame.
+ var frameURL = new URL(document.getElementById("testframe").src);
+ var params = new URLSearchParams(frameURL.search);
+ var counterInFrame = params.get("counter");
+ if (counterInFrame == counter) {
+ loadNextTest();
+ }
+}
+
+function loadNextTest() {
+ counter++;
+ if (counter == tests.length) {
+ finishTest();
+ return;
+ }
+ var src = "file_iframe_srcdoc.sjs";
+ src += "?csp=" + escape(tests[counter].csp);
+ src += "&action=" + escape(tests[counter].query);
+ src += "&counter=" + counter;
+ document.getElementById("testframe").src = src;
+}
+
+// start running the tests
+window.examiner = new examiner();
+loadNextTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_ignore_unsafe_inline.html b/dom/security/test/csp/test_ignore_unsafe_inline.html
new file mode 100644
index 0000000000..09d08157da
--- /dev/null
+++ b/dom/security/test/csp/test_ignore_unsafe_inline.html
@@ -0,0 +1,122 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1004703 - ignore 'unsafe-inline' if nonce- or hash-source specified</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+/* Description of the test:
+ * We load a page that contains three scripts using different policies
+ * and make sure 'unsafe-inline' is ignored within script-src if hash-source
+ * or nonce-source is specified.
+ *
+ * The expected output of each test is a sequence of chars.
+ * E.g. the default char we expect is 'a', depending on what inline scripts
+ * are allowed to run we also expect 'b', 'c', 'd'.
+ *
+ * The test also covers the handling of multiple policies where the second
+ * policy makes use of a directive that should *not* fallback to
+ * default-src, see Bug 1198422.
+ */
+
+const POLICY_PREFIX = "default-src 'none'; script-src ";
+
+var tests = [
+ {
+ policy1: POLICY_PREFIX + "'unsafe-inline'",
+ policy2: "frame-ancestors 'self'",
+ description: "'unsafe-inline' allows all scripts to execute",
+ file: "file_ignore_unsafe_inline.html",
+ result: "abcd",
+ },
+ {
+ policy1: POLICY_PREFIX + "'unsafe-inline' 'sha256-uJXAPKP5NZxnVMZMUkDofh6a9P3UMRc1CRTevVPS/rI='",
+ policy2: "base-uri http://mochi.test",
+ description: "defining a hash should only allow one script to execute",
+ file: "file_ignore_unsafe_inline.html",
+ result: "ac",
+ },
+ {
+ policy1: POLICY_PREFIX + "'unsafe-inline' 'nonce-FooNonce'",
+ policy2: "form-action 'none'",
+ description: "defining a nonce should only allow one script to execute",
+ file: "file_ignore_unsafe_inline.html",
+ result: "ad",
+ },
+ {
+ policy1: POLICY_PREFIX + "'unsafe-inline' 'sha256-uJXAPKP5NZxnVMZMUkDofh6a9P3UMRc1CRTevVPS/rI=' 'nonce-FooNonce'",
+ policy2: "upgrade-insecure-requests",
+ description: "defining hash and nonce should allow two scripts to execute",
+ file: "file_ignore_unsafe_inline.html",
+ result: "acd",
+ },
+ {
+ policy1: POLICY_PREFIX + "'unsafe-inline' 'sha256-uJXAPKP5NZxnVMZMUkDofh6a9P3UMRc1CRTevVPS/rI=' 'nonce-FooNonce' 'unsafe-inline'",
+ policy2: "referrer origin",
+ description: "defining hash, nonce and 'unsafe-inline' twice should still only allow two scripts to execute",
+ file: "file_ignore_unsafe_inline.html",
+ result: "acd",
+ },
+ {
+ policy1: "default-src 'unsafe-inline' 'sha256-uJXAPKP5NZxnVMZMUkDofh6a9P3UMRc1CRTevVPS/rI=' 'nonce-FooNonce' ",
+ policy2: "sandbox allow-scripts allow-same-origin",
+ description: "unsafe-inline should be ignored within default-src when a hash or nonce is specified",
+ file: "file_ignore_unsafe_inline.html",
+ result: "acd",
+ },
+];
+
+var counter = 0;
+var curTest;
+
+function loadNextTest() {
+ if (counter == tests.length) {
+ document.getElementById("testframe").removeEventListener("load", test);
+ SimpleTest.finish();
+ return;
+ }
+
+ curTest = tests[counter++];
+ var src = "file_ignore_unsafe_inline_multiple_policies_server.sjs?file=";
+ // append the file that should be served
+ src += escape("tests/dom/security/test/csp/" + curTest.file);
+
+ // append the first CSP that should be used to serve the file
+ src += "&csp1=" + escape(curTest.policy1);
+ // append the second CSP that should be used to serve the file
+ src += "&csp2=" + escape(curTest.policy2);
+
+ document.getElementById("testframe").addEventListener("load", test);
+ document.getElementById("testframe").src = src;
+}
+
+function test() {
+ try {
+ document.getElementById("testframe").removeEventListener('load', test);
+ var testframe = document.getElementById("testframe");
+ var divcontent = testframe.contentWindow.document.getElementById('testdiv').innerHTML;
+ // sort the characters to make sure the result is in ascending order
+ // in case handlers run out of order
+ divcontent = divcontent.split('').sort().join('');
+
+ is(divcontent, curTest.result, curTest.description);
+ }
+ catch (e) {
+ ok(false, "error: could not access content for test " + curTest.description + "!");
+ }
+ loadNextTest();
+}
+
+loadNextTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_ignore_xfo.html b/dom/security/test/csp/test_ignore_xfo.html
new file mode 100644
index 0000000000..5dbfecd18d
--- /dev/null
+++ b/dom/security/test/csp/test_ignore_xfo.html
@@ -0,0 +1,120 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1024557: Ignore x-frame-options if CSP with frame-ancestors exists</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="csp_testframe"></iframe>
+<iframe style="width:100%;" id="csp_testframe_no_xfo"></iframe>
+<iframe style="width:100%;" id="csp_ro_testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/*
+ * We load two frames using:
+ * x-frame-options: deny
+ * where the first frame uses a csp and the second a csp_ro including frame-ancestors.
+ * We make sure that xfo is ignored for regular csp but not for csp_ro.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+var script = SpecialPowers.loadChromeScript(() => {
+/* eslint-env mozilla/chrome-script */
+ let ignoreCount = 0;
+ function listener(msg) {
+ if(msg.message.includes("Content-Security-Policy: Ignoring ‘x-frame-options’ because of ‘frame-ancestors’ directive.")) {
+ ignoreCount++;
+ if(ignoreCount == 2) {
+ ok(false, 'The "Content-Security-Policy: Ignoring ‘x-frame-options’ because of ‘frame-ancestors’ directive." warning should only appear once for the csp_testframe.');
+ }
+ }
+ }
+ Services.console.registerListener(listener);
+
+ addMessageListener("cleanup", () => {
+ Services.console.unregisterListener(listener);
+ });
+});
+
+SimpleTest.registerCleanupFunction(async () => {
+ await script.sendQuery("cleanup");
+});
+
+var testcounter = 0;
+function checkFinished() {
+ testcounter++;
+ if (testcounter < 4) {
+ return;
+ }
+ // remove the listener and we are done.
+ window.examiner.remove();
+ SimpleTest.finish();
+}
+
+// X-Frame-Options checks happen in the parent, hence we have to
+// proxy the xfo violation notifications.
+SpecialPowers.registerObservers("xfo-on-violate-policy");
+
+function examiner() {
+ SpecialPowers.addObserver(this, "specialpowers-xfo-on-violate-policy");
+}
+examiner.prototype = {
+ observe(subject, topic, data) {
+ var asciiSpec = SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIURI"), "asciiSpec");
+
+ is(asciiSpec, "http://mochi.test:8888/tests/dom/security/test/csp/file_ro_ignore_xfo.html", "correct subject");
+ ok(topic.endsWith("xfo-on-violate-policy"), "correct topic");
+ is(data, "deny", "correct data");
+ checkFinished();
+ },
+ remove() {
+ SpecialPowers.removeObserver(this, "specialpowers-xfo-on-violate-policy");
+ }
+}
+window.examiner = new examiner();
+
+// 1) test XFO with CSP
+var csp_testframe = document.getElementById("csp_testframe");
+csp_testframe.onload = function() {
+ var msg = csp_testframe.contentDocument.getElementById("cspmessage");
+ is(msg.innerHTML, "Ignoring XFO because of CSP", "Loading frame with with XFO and CSP");
+ checkFinished();
+}
+csp_testframe.onerror = function() {
+ ok(false, "sanity: should not fire onerror for csp_testframe");
+ checkFinished();
+}
+csp_testframe.src = "file_ignore_xfo.html";
+
+// 2) test XFO with CSP_RO
+var csp_ro_testframe = document.getElementById("csp_ro_testframe");
+// If XFO denies framing then the onload event should fire.
+csp_ro_testframe.onload = function() {
+ ok(true, "sanity: should fire onload for csp_ro_testframe");
+ checkFinished();
+}
+csp_ro_testframe.onerror = function() {
+ ok(false, "sanity: should not fire onerror for csp_ro_testframe");
+ checkFinished();
+}
+csp_ro_testframe.src = "file_ro_ignore_xfo.html";
+
+var csp_testframe_no_xfo = document.getElementById("csp_testframe_no_xfo");
+csp_testframe_no_xfo.onload = function() {
+ var msg = csp_testframe_no_xfo.contentDocument.getElementById("cspmessage");
+ is(msg.innerHTML, "Do not log xfo ignore warning when no xfo is set.", "Loading frame with with no XFO and CSP");
+ checkFinished();
+}
+csp_testframe_no_xfo.onerror = function() {
+ ok(false, "sanity: should not fire onerror for csp_testframe_no_xfo");
+ checkFinished();
+}
+csp_testframe_no_xfo.src = "file_no_log_ignore_xfo.html";
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_image_document.html b/dom/security/test/csp/test_image_document.html
new file mode 100644
index 0000000000..eba83f95a7
--- /dev/null
+++ b/dom/security/test/csp/test_image_document.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1627235: Test CSP for images loaded as iframe</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<iframe id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+let testframe = document.getElementById("testframe");
+
+testframe.onload = function() {
+ ok(true, "sanity: should fire onload for image document");
+
+ let contentDoc = SpecialPowers.wrap(testframe.contentDocument);
+ let cspJSON = contentDoc.cspJSON;
+ ok(cspJSON.includes("default-src"), "found default-src directive");
+ ok(cspJSON.includes("https://bug1627235.test.com"), "found default-src value");
+ SimpleTest.finish();
+}
+testframe.onerror = function() {
+ ok(false, "sanity: should not fire onerror for image document");
+}
+testframe.src = "file_image_document_pixel.png";
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_image_nonce.html b/dom/security/test/csp/test_image_nonce.html
new file mode 100644
index 0000000000..dd6bc13922
--- /dev/null
+++ b/dom/security/test/csp/test_image_nonce.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1139297 - Implement CSP upgrade-insecure-requests directive</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the test:
+ * We load three images: (a) with a matching nonce,
+ (b) with a non matching nonce,
+ * (c) with no nonce
+ * and make sure that all three images get blocked because
+ * "img-src nonce-bla" should not allow an image load, not
+ * even if the nonce matches*.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+var counter = 0;
+
+function finishTest() {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+function checkResults(aResult) {
+ counter++;
+ if (aResult === "img-with-matching-nonce-blocked" ||
+ aResult === "img-with_non-matching-nonce-blocked" ||
+ aResult === "img-without-nonce-blocked") {
+ ok (true, "correct result for: " + aResult);
+ }
+ else {
+ ok(false, "unexpected result: " + aResult + "\n\n");
+ }
+ if (counter < 3) {
+ return;
+ }
+ finishTest();
+}
+
+// a postMessage handler that is used by sandboxed iframes without
+// 'allow-same-origin' to bubble up results back to this main page.
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ checkResults(event.data.result);
+}
+
+document.getElementById("testframe").src = "file_image_nonce.html";
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_independent_iframe_csp.html b/dom/security/test/csp/test_independent_iframe_csp.html
new file mode 100644
index 0000000000..9549263a11
--- /dev/null
+++ b/dom/security/test/csp/test_independent_iframe_csp.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1419222 - iFrame CSP should not affect parent document CSP</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<iframe style="width:100%;" id="testframe" src="file_independent_iframe_csp.html"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+ /* Description of the test:
+ This test makes sure adding a CSP directive to an iFrame does not propagate
+ the new directive to the parent document.
+
+ The test page records it's own CSP before and after adding an iFrame
+ with an additional CSP directive.
+
+ CSPs before and after adding the iFrame are compared to make sure they do
+ not differ.
+ */
+
+ SimpleTest.waitForExplicitFinish();
+
+ function finishTest() {
+ window.removeEventListener("message", compareCSPs);
+ SimpleTest.finish();
+ }
+
+ function compareCSPs(event) {
+ try {
+ var beginCspObj = event.data.result[0];
+ var iFrameCspObj = event.data.result[1];
+ var endCspObj = event.data.result[2];
+
+ // make sure the parent document had one policy from the start.
+ var beginPolicies = beginCspObj["csp-policies"];
+ is(beginPolicies.length, 1, "The parent doc should start with one policy applied.");
+
+ // make sure the parent document still has one policy after adding the iFrame.
+ var endPolicies = endCspObj["csp-policies"];
+ is(endPolicies.length, 1, "The parent doc should still have one policy applied after adding the iFrame.");
+
+ // make sure the iFrame has an additional CSP policy.
+ var iFramePolicies = iFrameCspObj["csp-policies"];
+ is(iFramePolicies.length, 2, "The iFrame should have two policies applied");
+
+ var beginDirs = [];
+ var endDirs = [];
+ for (var dir in beginPolicies[0]) {
+ beginDirs.push(dir);
+ }
+ for (var dir in endPolicies[0]) {
+ endDirs.push(dir);
+ }
+ // Check correct number of CSP diretives.
+ is(beginDirs.length, 3, "The parent's CSP policy should contain 3 directives.");
+ // Compare the parent'S CSP directives before and after adding the iFrame.
+ ok((beginDirs.length == endDirs.length && beginDirs.every((value, index) => value == endDirs[index])),
+ "Begin and end CSP directives of the parent should not differ");
+ }
+ catch (e) {
+ ok(false, "uuh, something went wrong within independent iFrame csp test");
+ }
+
+ finishTest();
+ }
+
+ // a postMessage handler to initiate the checks after the iFrame was added to
+ // the test page.
+ window.addEventListener("message", compareCSPs);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_inlinescript.html b/dom/security/test/csp/test_inlinescript.html
new file mode 100644
index 0000000000..99b055f0c7
--- /dev/null
+++ b/dom/security/test/csp/test_inlinescript.html
@@ -0,0 +1,123 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Content Security Policy Frame Ancestors directive</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<iframe style="width:100%;height:300px;" id='testframe'></iframe>
+
+<script class="testbody" type="text/javascript">
+
+var tests = [
+ {
+ /* test allowed */
+ csp: "default-src 'self'; script-src 'self' 'unsafe-inline'",
+ results: ["body-onload-fired", "text-node-fired",
+ "javascript-uri-fired", "javascript-uri-anchor-fired"],
+ desc: "allow inline scripts",
+ received: 0, // counter to make sure we received all 4 reports
+ },
+ {
+ /* test blocked */
+ csp: "default-src 'self'",
+ results: ["inline-script-blocked"],
+ desc: "block inline scripts",
+ received: 0, // counter to make sure we received all 4 reports
+ }
+];
+
+var counter = 0;
+var curTest;
+
+// This is used to watch the blocked data bounce off CSP and allowed data
+// get sent out to the wire.
+function examiner() {
+ SpecialPowers.addObserver(this, "csp-on-violate-policy");
+}
+examiner.prototype = {
+ observe(subject, topic, data) {
+ if (topic !== "csp-on-violate-policy") {
+ return;
+ }
+
+ var what = SpecialPowers.getPrivilegedProps(SpecialPowers.
+ do_QueryInterface(subject, "nsISupportsCString"), "data");
+
+ if (!what.includes("Inline Script had invalid hash") &&
+ !what.includes("Inline Scripts will not execute")) {
+ return;
+ }
+ window.checkResults("inline-script-blocked");
+ },
+ remove() {
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ }
+}
+
+function finishTest() {
+ window.examiner.remove();
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+// Check to see if all the tests have run
+var checkResults = function(result) {
+ var index = curTest.results.indexOf(result);
+ isnot(index, -1, "should find result (" + result +") within test: " + curTest.desc);
+ if (index > -1) {
+ curTest.received += 1;
+ }
+
+ // make sure we receive all the 4 reports for the 4 inline scripts
+ if (curTest.received < 4) {
+ return;
+ }
+
+ if (counter < tests.length) {
+ loadNextTest();
+ return;
+ }
+ finishTest();
+}
+
+// a postMessage handler that is used to bubble up results from the testframe
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ checkResults(event.data);
+}
+
+function clickit() {
+ document.getElementById("testframe").removeEventListener('load', clickit);
+ var testframe = document.getElementById('testframe');
+ var a = testframe.contentDocument.getElementById('anchortoclick');
+ sendMouseEvent({type:'click'}, a, testframe.contentWindow);
+}
+
+function loadNextTest() {
+ curTest = tests[counter++];
+ var src = "file_testserver.sjs?file=";
+ // append the file that should be served
+ src += escape("tests/dom/security/test/csp/file_inlinescript.html");
+ // append the CSP that should be used to serve the file
+ src += "&csp=" + escape(curTest.csp);
+
+ document.getElementById("testframe").src = src;
+ document.getElementById("testframe").addEventListener("load", clickit);
+}
+
+// set up the test and go
+window.examiner = new examiner();
+SimpleTest.waitForExplicitFinish();
+loadNextTest();
+
+</script>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/test_inlinestyle.html b/dom/security/test/csp/test_inlinestyle.html
new file mode 100644
index 0000000000..dc15dc5078
--- /dev/null
+++ b/dom/security/test/csp/test_inlinestyle.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Content Security Policy inline stylesheets stuff</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<iframe style="width:100%;height:300px;" id='cspframe1'></iframe>
+<iframe style="width:100%;height:300px;" id='cspframe2'></iframe>
+<script class="testbody" type="text/javascript">
+
+//////////////////////////////////////////////////////////////////////
+// set up and go
+SimpleTest.waitForExplicitFinish();
+
+var done = 0;
+
+// When a CSP 1.0 compliant policy is specified we should block inline
+// styles applied by <style> element, style attribute, and SMIL <animate> and <set> tags
+// (when it's not explicitly allowed.)
+function checkStyles(evt) {
+ var cspframe = document.getElementById('cspframe1');
+ var color;
+
+ // black means the style wasn't applied. green colors are used for styles
+ //expected to be applied. A color is red if a style is erroneously applied
+ color = window.getComputedStyle(cspframe.contentDocument.getElementById('linkstylediv')).color;
+ ok('rgb(0, 255, 0)' === color, 'External Stylesheet (' + color + ')');
+ color = window.getComputedStyle(cspframe.contentDocument.getElementById('inlinestylediv')).color;
+ ok('rgb(0, 0, 0)' === color, 'Inline Style TAG (' + color + ')');
+ color = window.getComputedStyle(cspframe.contentDocument.getElementById('attrstylediv')).color;
+ ok('rgb(0, 0, 0)' === color, 'Style Attribute (' + color + ')');
+ color = window.getComputedStyle(cspframe.contentDocument.getElementById('csstextstylediv')).color;
+ ok('rgb(0, 255, 0)' === color, 'cssText (' + color + ')');
+ // SMIL tests
+ color = window.getComputedStyle(cspframe.contentDocument.getElementById('xmlTest',null)).fill;
+ ok('rgb(0, 0, 0)' === color, 'XML Attribute styling (SMIL) (' + color + ')');
+ color = window.getComputedStyle(cspframe.contentDocument.getElementById('cssOverrideTest',null)).fill;
+ ok('rgb(0, 0, 0)' === color, 'CSS Override styling (SMIL) (' + color + ')');
+ color = window.getComputedStyle(cspframe.contentDocument.getElementById('cssOverrideTestById',null)).fill;
+ ok('rgb(0, 0, 0)' === color, 'CSS Override styling via ID lookup (SMIL) (' + color + ')');
+ color = window.getComputedStyle(cspframe.contentDocument.getElementById('cssSetTestById',null)).fill;
+ ok('rgb(0, 0, 0)' === color, 'CSS Set Element styling via ID lookup (SMIL) (' + color + ')');
+
+ color = window.getComputedStyle(cspframe.contentDocument.getElementById('modifycsstextdiv')).color;
+ ok('rgb(0, 255, 0)' === color, 'Modify loaded style sheet via cssText (' + color + ')');
+
+ checkIfDone();
+}
+
+// When a CSP 1.0 compliant policy is specified we should allow inline
+// styles when it is explicitly allowed.
+function checkStylesAllowed(evt) {
+ var cspframe = document.getElementById('cspframe2');
+ var color;
+
+ // black means the style wasn't applied. green colors are used for styles
+ // expected to be applied. A color is red if a style is erroneously applied
+ color = window.getComputedStyle(cspframe.contentDocument.getElementById('linkstylediv')).color;
+ ok('rgb(0, 255, 0)' === color, 'External Stylesheet (' + color + ')');
+ color = window.getComputedStyle(cspframe.contentDocument.getElementById('inlinestylediv')).color;
+ ok('rgb(0, 255, 0)' === color, 'Inline Style TAG (' + color + ')');
+ color = window.getComputedStyle(cspframe.contentDocument.getElementById('attrstylediv')).color;
+ ok('rgb(0, 255, 0)' === color, 'Style Attribute (' + color + ')');
+
+ // Note that the below test will fail if "script-src: 'unsafe-inline'" breaks,
+ // since it relies on executing script to set .cssText
+ color = window.getComputedStyle(cspframe.contentDocument.getElementById('csstextstylediv')).color;
+ ok('rgb(0, 255, 0)' === color, 'style.cssText (' + color + ')');
+ // SMIL tests
+ color = window.getComputedStyle(cspframe.contentDocument.getElementById('xmlTest',null)).fill;
+ ok('rgb(0, 255, 0)' === color, 'XML Attribute styling (SMIL) (' + color + ')');
+ color = window.getComputedStyle(cspframe.contentDocument.getElementById('cssOverrideTest',null)).fill;
+ ok('rgb(0, 255, 0)' === color, 'CSS Override styling (SMIL) (' + color + ')');
+ color = window.getComputedStyle(cspframe.contentDocument.getElementById('cssOverrideTestById',null)).fill;
+ ok('rgb(0, 255, 0)' === color, 'CSS Override styling via ID lookup (SMIL) (' + color + ')');
+ color = window.getComputedStyle(cspframe.contentDocument.getElementById('cssSetTestById',null)).fill;
+ ok('rgb(0, 255, 0)' === color, 'CSS Set Element styling via ID lookup (SMIL) (' + color + ')');
+
+ color = window.getComputedStyle(cspframe.contentDocument.getElementById('modifycsstextdiv')).color;
+ ok('rgb(0, 255, 0)' === color, 'Modify loaded style sheet via cssText (' + color + ')');
+
+ checkIfDone();
+}
+
+function checkIfDone() {
+ done++;
+ if (done == 2)
+ SimpleTest.finish();
+}
+
+// save this for last so that our listeners are registered.
+// ... this loads the testbed of good and bad requests.
+document.getElementById('cspframe1').src = 'file_inlinestyle_main.html';
+document.getElementById('cspframe1').addEventListener('load', checkStyles);
+document.getElementById('cspframe2').src = 'file_inlinestyle_main_allowed.html';
+document.getElementById('cspframe2').addEventListener('load', checkStylesAllowed);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_invalid_source_expression.html b/dom/security/test/csp/test_invalid_source_expression.html
new file mode 100644
index 0000000000..c170dc2a27
--- /dev/null
+++ b/dom/security/test/csp/test_invalid_source_expression.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1086612 - CSP: Let source expression be the empty set in case no valid source can be parsed</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="visibility: hidden">
+ <iframe style="width:100%;" id="testframe"></iframe>
+ </div>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+/* Description of the test:
+ * We try to parse a policy:
+ * script-src bankid:/*
+ * where the source expression (bankid:/*) is invalid. In that case the source-expression
+ * should be the empty set ('none'), see: http://www.w3.org/TR/CSP11/#source-list-parsing
+ * We confirm that the script is blocked by CSP.
+ */
+
+const policy = "script-src bankid:/*";
+
+function runTest() {
+ var src = "file_testserver.sjs";
+ // append the file that should be served
+ src += "?file=" + escape("tests/dom/security/test/csp/file_invalid_source_expression.html");
+ // append the CSP that should be used to serve the file
+ src += "&csp=" + escape(policy);
+
+ document.getElementById("testframe").addEventListener("load", test);
+ document.getElementById("testframe").src = src;
+}
+
+function test() {
+ try {
+ document.getElementById("testframe").removeEventListener('load', test);
+ var testframe = document.getElementById("testframe");
+ var divcontent = testframe.contentWindow.document.getElementById('testdiv').innerHTML;
+ is(divcontent, "blocked", "should be 'blocked'!");
+ }
+ catch (e) {
+ ok(false, "ERROR: could not access content!");
+ }
+ SimpleTest.finish();
+}
+
+runTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_leading_wildcard.html b/dom/security/test/csp/test_leading_wildcard.html
new file mode 100644
index 0000000000..53994b0013
--- /dev/null
+++ b/dom/security/test/csp/test_leading_wildcard.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1032303 - CSP - Keep FULL STOP when matching *.foo.com to disallow loads from foo.com</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="visibility: hidden">
+ <iframe style="width:100%;" id="testframe"></iframe>
+ </div>
+
+<script class="testbody" type="text/javascript">
+
+/*
+ * Description of the test:
+ * We load a page with a CSP that allows scripts to be loaded from *.example.com.
+ * On that page we try to load two scripts:
+ * a) [allowed] leading_wildcard_allowed.js which is served from test1.example.com
+ * b) [blocked] leading_wildcard_blocked.js which is served from example.com
+ *
+ * We verify that only the allowed script executes by registering observers which listen
+ * to CSP violations and http-notifications. Please note that both scripts do *not* exist
+ * in the file system. The test just verifies that CSP blocks correctly.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+var policy = "default-src 'none' script-src *.example.com";
+var testsExecuted = 0;
+
+function finishTest() {
+ if (++testsExecuted < 2) {
+ return;
+ }
+ window.wildCardExaminer.remove();
+ SimpleTest.finish();
+}
+
+// We use the examiner to identify requests that hit the wire and requests
+// that are blocked by CSP.
+function examiner() {
+ SpecialPowers.addObserver(this, "csp-on-violate-policy");
+ SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
+}
+examiner.prototype = {
+ observe(subject, topic, data) {
+
+ // allowed requests
+ if (topic === "specialpowers-http-notify-request") {
+ if (data.includes("leading_wildcard_allowed.js")) {
+ ok (true, "CSP should allow file_leading_wildcard_allowed.js!");
+ finishTest();
+ }
+ if (data.includes("leading_wildcard_blocked.js")) {
+ ok(false, "CSP should not allow file_leading_wildcard_blocked.js!");
+ finishTest();
+ }
+ }
+
+ // blocked requests
+ if (topic === "csp-on-violate-policy") {
+ var asciiSpec = SpecialPowers.getPrivilegedProps(
+ SpecialPowers.do_QueryInterface(subject, "nsIURI"),
+ "asciiSpec");
+
+ if (asciiSpec.includes("leading_wildcard_allowed.js")) {
+ ok (false, "CSP should not block file_leading_wildcard_allowed.js!");
+ finishTest();
+ }
+ if (asciiSpec.includes("leading_wildcard_blocked.js")) {
+ ok (true, "CSP should block file_leading_wildcard_blocked.js!");
+ finishTest();
+ }
+ }
+ },
+ remove() {
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
+ }
+}
+window.wildCardExaminer = new examiner();
+
+function runTest() {
+ var src = "file_testserver.sjs";
+ // append the file that should be served
+ src += "?file=" + escape("tests/dom/security/test/csp/file_leading_wildcard.html");
+ // append the CSP that should be used to serve the file
+ src += "&csp=" + escape(policy);
+
+ document.getElementById("testframe").src = src;
+}
+
+// start running the tests
+runTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_link_rel_preload.html b/dom/security/test/csp/test_link_rel_preload.html
new file mode 100644
index 0000000000..e2b226ff05
--- /dev/null
+++ b/dom/security/test/csp/test_link_rel_preload.html
@@ -0,0 +1,73 @@
+<!doctype html>
+<html>
+<head>
+ <title>Bug 1599791 - Test link rel=preload</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe id=testframe></iframe>
+<script class="testbody" type="text/javascript">
+
+// Please note that 'fakeServer' does not exist because the test relies
+// on "csp-on-violate-policy" , and "specialpowers-http-notify-request"
+// which fire if either the request is blocked or fires. The test does
+// not rely on the result of the load.
+
+let TOTAL_TESTS = 3; // script, style, image
+let seenTests = 0;
+
+function examiner() {
+ SpecialPowers.addObserver(this, "csp-on-violate-policy");
+ SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
+}
+examiner.prototype = {
+ observe(subject, topic, data) {
+ if (topic === "csp-on-violate-policy") {
+ let asciiSpec = SpecialPowers.getPrivilegedProps(
+ SpecialPowers.do_QueryInterface(subject, "nsIURI"),
+ "asciiSpec");
+ if (asciiSpec.includes("fakeServer?script") ||
+ asciiSpec.includes("fakeServer?style") ||
+ asciiSpec.includes("fakeServer?fetch") ||
+ asciiSpec.includes("fakeServer?font") ||
+ asciiSpec.includes("fakeServer?image")) {
+ let type = asciiSpec.substring(asciiSpec.indexOf("?") + 1);
+ ok (true, type + " should be blocked by CSP");
+ checkFinished();
+ }
+ }
+
+ if (topic === "specialpowers-http-notify-request") {
+ if (data.includes("fakeServer?script") ||
+ data.includes("fakeServer?style") ||
+ data.includes("fakeServer?fetch") ||
+ data.includes("fakeServer?font") ||
+ data.includes("fakeServer?image")) {
+ let type = data.substring(data.indexOf("?") + 1);
+ ok (false, type + " should not be loaded");
+ checkFinished();
+ }
+ }
+ },
+ remove() {
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
+ }
+}
+
+window.examiner = new examiner();
+
+function checkFinished() {
+ seenTests++;
+ if (seenTests == TOTAL_TESTS) {
+ window.examiner.remove();
+ SimpleTest.finish();
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+document.getElementById("testframe").src = "file_link_rel_preload.html";
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_meta_csp_self.html b/dom/security/test/csp/test_meta_csp_self.html
new file mode 100644
index 0000000000..8d7d5812a9
--- /dev/null
+++ b/dom/security/test/csp/test_meta_csp_self.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1387871 - CSP: Test 'self' within meta csp in data: URI iframe</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+/* Description of the test:
+ * We load a data: URI into an iframe which provides a meta-csp
+ * including the keyword 'self'. We make sure 'self' does not
+ * allow a data: image to load.
+ */
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ window.removeEventListener("message", receiveMessage);
+ is(event.data.result, "dataFrameReady", "sanity: received msg from loaded frame");
+
+ var frame = document.getElementById("testframe");
+
+ // make sure the img was blocked
+ var img = SpecialPowers.wrap(frame).contentDocument.getElementById("testimg");
+ is(img.naturalWidth, 0, "img should be blocked - width should be 0");
+ is(img.naturalHeight, 0, "img should be blocked - height should be 0");
+
+ // sanity check, make sure 'self' translates into data
+ var contentDoc = SpecialPowers.wrap(frame).contentDocument;
+ // parse the cspJSON in a csp-object
+ var cspOBJ = JSON.parse(contentDoc.cspJSON);
+ ok(cspOBJ, "sanity: was able to parse the CSP JSON");
+
+ // make sure we only got one policy
+ var policies = cspOBJ["csp-policies"];
+ is(policies.length, 1, "sanity: received one CSP policy");
+
+ var policy = policies[0];
+ var val = policy['img-src'];
+ is(val.toString(), "'self'", "'self' should translate into data");
+ SimpleTest.finish();
+}
+
+let DATA_URI = `data:text/html,
+ <html>
+ <head>
+ <meta http-equiv="Content-Security-Policy" content="img-src 'self'">
+ </head>
+ <body onload="parent.postMessage({result:'dataFrameReady'},'*');">
+ data: URI frame with meta-csp including 'self'<br/>
+ <img id="testimg" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" />
+ </body>
+ </html>`;
+document.getElementById("testframe").src = DATA_URI;
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_meta_element.html b/dom/security/test/csp/test_meta_element.html
new file mode 100644
index 0000000000..42cddbacbf
--- /dev/null
+++ b/dom/security/test/csp/test_meta_element.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 663570 - Implement Content Security Policy via <meta> tag</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<iframe style="width:100%;" id="testframe" src="file_meta_element.html"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the test:
+ * The test is twofold:
+ * First, by loading a page using meta csp (into an iframe) we make sure that
+ * images get correctly blocked as the csp policy includes "img-src 'none'";
+ *
+ * Second, we make sure meta csp ignores the following directives:
+ * * report-uri
+ * * frame-ancestors
+ * * sandbox
+ *
+ * Please note that the CSP sanbdox directive (bug 671389) has not landed yet.
+ * Once bug 671389 lands this test will fail and needs to be updated.
+ */
+
+SimpleTest.waitForExplicitFinish();
+const EXPECTED_DIRS = ["img-src", "script-src"];
+
+function finishTest() {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+function checkResults(result) {
+ is(result, "img-blocked", "loading images should be blocked by meta csp");
+
+ try {
+ // get the csp in JSON notation from the principal
+ var frame = document.getElementById("testframe");
+ var contentDoc = SpecialPowers.wrap(frame.contentDocument);
+ var cspJSON = contentDoc.cspJSON;
+
+ ok(cspJSON, "CSP applied through meta element");
+
+ // parse the cspJSON in a csp-object
+ var cspOBJ = JSON.parse(cspJSON);
+ ok(cspOBJ, "was able to parse the JSON");
+
+ // make sure we only got one policy
+ var policies = cspOBJ["csp-policies"];
+ is(policies.length, 1, "there should be one policy applied");
+
+ // iterate the policy and make sure to only encounter
+ // expected directives.
+ var policy = policies[0];
+ for (var dir in policy) {
+ // special case handling for report-only which is not a directive
+ // but present in the JSON notation of the CSP.
+ if (dir === "report-only") {
+ continue;
+ }
+ var index = EXPECTED_DIRS.indexOf(dir);
+ isnot(index, -1, "meta csp contains directive: " + dir + "!");
+
+ // take the element out of the array so we can make sure
+ // that we have seen all the expected values in the end.
+ EXPECTED_DIRS.splice(index, 1);
+ }
+ is(EXPECTED_DIRS.length, 0, "have seen all the expected values");
+ }
+ catch (e) {
+ ok(false, "uuh, something went wrong within meta csp test");
+ }
+
+ finishTest();
+}
+
+// a postMessage handler used to bubble up the onsuccess/onerror state
+// from within the iframe.
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ checkResults(event.data.result);
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_meta_header_dual.html b/dom/security/test/csp/test_meta_header_dual.html
new file mode 100644
index 0000000000..679512d068
--- /dev/null
+++ b/dom/security/test/csp/test_meta_header_dual.html
@@ -0,0 +1,135 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 663570 - Implement Content Security Policy via meta tag</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the test:
+ * We test all sorts of CSPs on documents, including documents with no
+ * CSP, with meta CSP and with meta CSP in combination with a CSP header.
+ */
+
+const TESTS = [
+ {
+ /* load image without any CSP */
+ query: "test1",
+ result: "img-loaded",
+ policyLen: 0,
+ desc: "no CSP should allow load",
+ },
+ {
+ /* load image where meta denies load */
+ query: "test2",
+ result: "img-blocked",
+ policyLen: 1,
+ desc: "meta (img-src 'none') should block load"
+ },
+ {
+ /* load image where meta allows load */
+ query: "test3",
+ result: "img-loaded",
+ policyLen: 1,
+ desc: "meta (img-src http://mochi.test) should allow load"
+ },
+ {
+ /* load image where meta allows but header blocks */
+ query: "test4", // triggers speculative load
+ result: "img-blocked",
+ policyLen: 2,
+ desc: "meta (img-src http://mochi.test), header (img-src 'none') should block load"
+ },
+ {
+ /* load image where meta blocks but header allows */
+ query: "test5", // triggers speculative load
+ result: "img-blocked",
+ policyLen: 2,
+ desc: "meta (img-src 'none'), header (img-src http://mochi.test) should block load"
+ },
+ {
+ /* load image where meta allows and header allows */
+ query: "test6", // triggers speculative load
+ result: "img-loaded",
+ policyLen: 2,
+ desc: "meta (img-src http://mochi.test), header (img-src http://mochi.test) should allow load"
+ },
+ {
+ /* load image where meta1 allows but meta2 blocks */
+ query: "test7",
+ result: "img-blocked",
+ policyLen: 2,
+ desc: "meta1 (img-src http://mochi.test), meta2 (img-src 'none') should allow blocked"
+ },
+ {
+ /* load image where meta1 allows and meta2 allows */
+ query: "test8",
+ result: "img-loaded",
+ policyLen: 2,
+ desc: "meta1 (img-src http://mochi.test), meta2 (img-src http://mochi.test) should allow allowed"
+ },
+];
+
+var curTest;
+var counter = -1;
+
+function finishTest() {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+function checkResults(result) {
+ // make sure the image got loaded or blocked
+ is(result, curTest.result, curTest.query + ": " + curTest.desc);
+
+ if (curTest.policyLen != 0) {
+ // make sure that meta policy got not parsed and appended twice
+ try {
+ // get the csp in JSON notation from the principal
+ var frame = document.getElementById("testframe");
+ var contentDoc = SpecialPowers.wrap(frame.contentDocument);
+ var cspOBJ = JSON.parse(contentDoc.cspJSON);
+ // make sure that the speculative policy and the actual policy
+ // are not appended twice.
+ var policies = cspOBJ["csp-policies"];
+ is(policies.length, curTest.policyLen, curTest.query + " should have: " + curTest.policyLen + " policies");
+ }
+ catch (e) {
+ ok(false, "uuh, something went wrong within cspToJSON in " + curTest.query);
+ }
+ }
+ // move on to the next test
+ runNextTest();
+}
+
+// a postMessage handler used to bubble up the
+// onsuccess/onerror state from within the iframe.
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ checkResults(event.data.result);
+}
+
+function runNextTest() {
+ if (++counter == TESTS.length) {
+ finishTest();
+ return;
+ }
+ curTest = TESTS[counter];
+ // load next test
+ document.getElementById("testframe").src = "file_meta_header_dual.sjs?" + curTest.query;
+}
+
+// start the test
+SimpleTest.waitForExplicitFinish();
+runNextTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_meta_whitespace_skipping.html b/dom/security/test/csp/test_meta_whitespace_skipping.html
new file mode 100644
index 0000000000..2f622c3a33
--- /dev/null
+++ b/dom/security/test/csp/test_meta_whitespace_skipping.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1261634 - Update whitespace skipping for meta csp</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe" src="file_meta_whitespace_skipping.html"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the test:
+ * We load a site using meta CSP into an iframe. We make sure that all directives
+ * are parsed correclty by the CSP parser even though the directives are separated
+ * not only by whitespace but also by line breaks
+ */
+
+SimpleTest.waitForExplicitFinish();
+const EXPECTED_DIRS = [
+ "img-src", "script-src", "style-src", "child-src", "font-src"];
+
+function finishTest() {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+function checkResults(result) {
+ // sanity check that the site was loaded and the meta csp was parsed.
+ is(result, "meta-csp-parsed", "loading images should be blocked by meta csp");
+
+ try {
+ // get the csp in JSON notation from the principal
+ var frame = document.getElementById("testframe");
+ var contentDoc = SpecialPowers.wrap(frame.contentDocument);
+ var cspJSON = contentDoc.cspJSON;
+ ok(cspJSON, "CSP applied through meta element");
+
+ // parse the cspJSON in a csp-object
+ var cspOBJ = JSON.parse(cspJSON);
+ ok(cspOBJ, "was able to parse the JSON");
+
+ // make sure we only got one policy
+ var policies = cspOBJ["csp-policies"];
+ is(policies.length, 1, "there should be one policy applied");
+
+ // iterate the policy and make sure to only encounter
+ // expected directives.
+ var policy = policies[0];
+ for (var dir in policy) {
+ // special case handling for report-only which is not a directive
+ // but present in the JSON notation of the CSP.
+ if (dir === "report-only") {
+ continue;
+ }
+ var index = EXPECTED_DIRS.indexOf(dir);
+ isnot(index, -1, "meta csp contains directive: " + dir + "!");
+
+ // take the element out of the array so we can make sure
+ // that we have seen all the expected values in the end.
+ EXPECTED_DIRS.splice(index, 1);
+ }
+ is(EXPECTED_DIRS.length, 0, "have seen all the expected values");
+ }
+ catch (e) {
+ ok(false, "uuh, something went wrong within meta csp test");
+ }
+
+ finishTest();
+}
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ checkResults(event.data.result);
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_multi_policy_injection_bypass.html b/dom/security/test/csp/test_multi_policy_injection_bypass.html
new file mode 100644
index 0000000000..cbb981405b
--- /dev/null
+++ b/dom/security/test/csp/test_multi_policy_injection_bypass.html
@@ -0,0 +1,119 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=717511
+-->
+<head>
+ <title>Test for Bug 717511</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+
+</div>
+
+<iframe style="width:200px;height:200px;" id='cspframe'></iframe>
+<iframe style="width:200px;height:200px;" id='cspframe2'></iframe>
+<script class="testbody" type="text/javascript">
+
+var path = "/tests/dom/security/test/csp/";
+
+// These are test results: -1 means it hasn't run,
+// true/false is the pass/fail result.
+// This is not exhaustive, just double-checking the 'self' vs * policy conflict in the two HTTP headers.
+window.tests = {
+ img_good: -1,
+ img_bad: -1,
+ script_good: -1,
+ script_bad: -1,
+ img2_good: -1,
+ img2_bad: -1,
+ script2_good: -1,
+ script2_bad: -1,
+};
+
+
+// This is used to watch the blocked data bounce off CSP and allowed data
+// get sent out to the wire.
+function examiner() {
+ SpecialPowers.addObserver(this, "csp-on-violate-policy");
+ SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
+}
+examiner.prototype = {
+ observe(subject, topic, data) {
+ var testpat = new RegExp("testid=([a-z0-9_]+)");
+
+ //_good things better be allowed!
+ //_bad things better be stopped!
+
+ if (topic === "specialpowers-http-notify-request") {
+ //these things were allowed by CSP
+ var asciiSpec = data;
+ if (!testpat.test(asciiSpec)) return;
+ var testid = testpat.exec(asciiSpec)[1];
+ window.testResult(testid,
+ /_good/.test(testid),
+ asciiSpec + " allowed by csp");
+
+ }
+
+ if(topic === "csp-on-violate-policy") {
+ // subject should be an nsIURI for csp-on-violate-policy
+ if (!SpecialPowers.can_QI(subject)) {
+ return;
+ }
+
+ //these were blocked... record that they were blocked
+ var asciiSpec = SpecialPowers.getPrivilegedProps(
+ SpecialPowers.do_QueryInterface(subject, "nsIURI"),
+ "asciiSpec");
+ if (!testpat.test(asciiSpec)) return;
+ var testid = testpat.exec(asciiSpec)[1];
+ window.testResult(testid,
+ /_bad/.test(testid),
+ asciiSpec + " blocked by \"" + data + "\"");
+ }
+ },
+
+ // must eventually call this to remove the listener,
+ // or mochitests might get borked.
+ remove() {
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
+ }
+}
+
+window.examiner = new examiner();
+
+window.testResult = function(testname, result, msg) {
+
+ //test already complete.... forget it... remember the first result.
+ if (window.tests[testname] != -1)
+ return;
+
+ window.tests[testname] = result;
+ is(result, true, testname + ' test: ' + msg);
+
+ // if any test is incomplete, keep waiting
+ for (var v in window.tests)
+ if(tests[v] == -1)
+ return;
+
+ // ... otherwise, finish
+ window.examiner.remove();
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// save this for last so that our listeners are registered.
+// ... this loads the testbed of good and bad requests.
+document.getElementById('cspframe').src = 'file_multi_policy_injection_bypass.html';
+document.getElementById('cspframe2').src = 'file_multi_policy_injection_bypass_2.html';
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_multipartchannel.html b/dom/security/test/csp/test_multipartchannel.html
new file mode 100644
index 0000000000..2708611e6d
--- /dev/null
+++ b/dom/security/test/csp/test_multipartchannel.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1416045/Bug 1223743 - CSP: Check baseChannel for CSP when loading multipart channel</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+<iframe style="width:100%;" id="testPartCSPframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+var testsToRunMultipartCSP = {
+ rootCSP_test: false,
+ part1CSP_test: false,
+ part2CSP_test: false,
+};
+
+SimpleTest.waitForExplicitFinish();
+
+function checkTestsCompleted() {
+ for (var prop in testsToRunMultipartCSP) {
+ // some test hasn't run yet so we're not done
+ if (!testsToRunMultipartCSP[prop]) {
+ return;
+ }
+ }
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+/* Description of the test:
+ * We apply a CSP to a multipart channel and then try to load an image
+ * within a segment making sure the image is blocked correctly by CSP.
+ * We also provide CSP for each part and try to load an image in each
+ * part and make sure the image is loaded in first part and blocked in
+ * second part correctly based on its CSP accordingly.
+ */
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ switch (event.data.test) {
+ case "rootCSP_test":
+ is(event.data.msg, "img-blocked", "image should be blocked");
+ testsToRunMultipartCSP.rootCSP_test = true;
+ break;
+ case "part1CSP_test":
+ is(event.data.msg, "part1-img-loaded", "Part1 image should be loaded");
+ testsToRunMultipartCSP.part1CSP_test = true;
+ break;
+ case "part2CSP_test":
+ is(event.data.msg, "part2-img-blocked", "Part2 image should be blocked");
+ testsToRunMultipartCSP.part2CSP_test = true;
+ break;
+ }
+ checkTestsCompleted();
+}
+
+// start the test
+document.getElementById("testframe").src = "file_multipart_testserver.sjs?doc";
+document.getElementById("testPartCSPframe").src =
+ "file_multipart_testserver.sjs?partcspdoc";
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_nonce_redirects.html b/dom/security/test/csp/test_nonce_redirects.html
new file mode 100644
index 0000000000..9b9e5e347d
--- /dev/null
+++ b/dom/security/test/csp/test_nonce_redirects.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1469150:Scripts with valid nonce get blocked if URL redirects</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the test:
+ * We load a script with a matching nonce, which redirects
+ * and we make sure that script is allowed.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+function finishTest() {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+function checkResults(aResult) {
+
+ if (aResult === "script-loaded") {
+ ok(true, "expected result: script loaded");
+ }
+ else {
+ ok(false, "unexpected result: script blocked");
+ }
+ finishTest();
+}
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ checkResults(event.data.result);
+}
+
+document.getElementById("testframe").src = "file_nonce_redirects.html";
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_nonce_snapshot.html b/dom/security/test/csp/test_nonce_snapshot.html
new file mode 100644
index 0000000000..6670d6868f
--- /dev/null
+++ b/dom/security/test/csp/test_nonce_snapshot.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1509738 - Snapshot nonce at load start time</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the test:
+ * a) the test starts loading a script using allowlisted nonce
+ * b) the nonce of the script gets modified
+ * c) the script hits a 302 server side redirect
+ * d) we ensure the script still loads and does not use the modified nonce
+ */
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ is(event.data, "script-loaded", "script loaded even though nonce was dynamically modified");
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+
+SimpleTest.waitForExplicitFinish();
+let src = "file_nonce_snapshot.sjs?load-frame";
+document.getElementById("testframe").src = src;
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_nonce_source.html b/dom/security/test/csp/test_nonce_source.html
new file mode 100644
index 0000000000..e11452c6e1
--- /dev/null
+++ b/dom/security/test/csp/test_nonce_source.html
@@ -0,0 +1,122 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test CSP 1.1 nonce-source for scripts and styles</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="visibility:hidden">
+ <iframe style="width:100%;" id='cspframe'></iframe>
+</div>
+<script class="testbody" type="text/javascript">
+
+var testsRun = 0;
+var totalTests = 20;
+
+// This is used to watch the blocked data bounce off CSP
+function examiner() {
+ SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
+ SpecialPowers.addObserver(this, "csp-on-violate-policy");
+}
+
+examiner.prototype = {
+ observe(subject, topic, data) {
+ var testid_re = new RegExp("testid=([a-z0-9_]+)");
+
+ //_good things better be allowed!
+ //_bad things better be blocked!
+
+ if (topic === "specialpowers-http-notify-request") {
+ var uri = data;
+ if (!testid_re.test(uri)) return;
+ var testid = testid_re.exec(uri)[1];
+ ok(/_good/.test(testid), "should allow URI with good testid " + testid);
+ ranTests(1);
+ }
+
+ if (topic === "csp-on-violate-policy") {
+ try {
+ // if it is an blocked external load, subject will be the URI of the resource
+ var blocked_uri = SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIURI"), "asciiSpec");
+ if (!testid_re.test(blocked_uri)) return;
+ var testid = testid_re.exec(blocked_uri)[1];
+ ok(/_bad/.test(testid), "should block URI with bad testid " + testid);
+ ranTests(1);
+ } catch (e) {
+ // if the subject is blocked inline, data will be a violation message
+ // we can't distinguish which resources triggered these, so we ignore them
+ }
+ }
+ },
+ // must eventually call this to remove the listener, or mochitests might get borked.
+ remove() {
+ SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ }
+}
+
+function cleanup() {
+ // remove the observer so we don't bork other tests
+ window.examiner.remove();
+ // finish the tests
+ SimpleTest.finish();
+}
+
+function ranTests(num) {
+ testsRun += num;
+ if (testsRun < totalTests) {
+ return;
+ }
+ cleanup();
+}
+
+function checkInlineScriptsAndStyles () {
+ var cspframe = document.getElementById('cspframe');
+ var getElementColorById = function (id) {
+ return window.getComputedStyle(cspframe.contentDocument.getElementById(id)).color;
+ };
+ // Inline style tries to change an element's color to green. If blocked, the
+ // element's color will be the (unchanged) default black.
+ var green = "rgb(0, 128, 0)";
+ var red = "rgb(255,0,0)";
+ var black = "rgb(0, 0, 0)";
+
+ // inline script tests
+ is(getElementColorById('inline-script-correct-nonce'), green,
+ "Inline script with correct nonce should execute");
+ is(getElementColorById('inline-script-incorrect-nonce'), black,
+ "Inline script with incorrect nonce should not execute");
+ is(getElementColorById('inline-script-correct-style-nonce'), black,
+ "Inline script with correct nonce for styles (but not for scripts) should not execute");
+ is(getElementColorById('inline-script-no-nonce'), black,
+ "Inline script with no nonce should not execute");
+
+ // inline style tests
+ is(getElementColorById('inline-style-correct-nonce'), green,
+ "Inline style with correct nonce should be allowed");
+ is(getElementColorById('inline-style-incorrect-nonce'), black,
+ "Inline style with incorrect nonce should be blocked");
+ is(getElementColorById('inline-style-correct-script-nonce'), black,
+ "Inline style with correct nonce for scripts (but incorrect nonce for styles) should be blocked");
+ is(getElementColorById('inline-style-no-nonce'), black,
+ "Inline style with no nonce should be blocked");
+
+ ranTests(8);
+}
+
+//////////////////////////////////////////////////////////////////////
+// set up and go
+window.examiner = new examiner();
+SimpleTest.waitForExplicitFinish();
+
+// save this for last so that our listeners are registered.
+// ... this loads the testbed of good and bad requests.
+document.getElementById('cspframe').src = 'file_nonce_source.html';
+document.getElementById('cspframe').addEventListener('load', checkInlineScriptsAndStyles);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_null_baseuri.html b/dom/security/test/csp/test_null_baseuri.html
new file mode 100644
index 0000000000..324b644f83
--- /dev/null
+++ b/dom/security/test/csp/test_null_baseuri.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1121857 - document.baseURI should not get blocked if baseURI is null</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="visibility: hidden">
+ <iframe style="width:100%;" id="testframe"></iframe>
+ </div>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the test:
+ * Creating a 'base' element and appending that element
+ * to document.head. After setting baseTag.href and finally
+ * removing the created element from the head, the baseURI
+ * should be the inital baseURI of the page.
+ */
+
+const TOTAL_TESTS = 3;
+var test_counter = 0;
+
+// a postMessage handler to communicate the results back to the parent.
+window.addEventListener("message", receiveMessage);
+
+function receiveMessage(event)
+{
+ // make sure the base-uri before and after the test is the initial base uri of the page
+ if (event.data.test === "initial_base_uri") {
+ ok(event.data.baseURI.startsWith("http://mochi.test"), "baseURI should be 'http://mochi.test'!");
+ }
+ // check that appending the child and setting the base tag actually affects the base-uri
+ else if (event.data.test === "changed_base_uri") {
+ ok(event.data.baseURI === "http://www.base-tag.com/", "baseURI should be 'http://www.base-tag.com'!");
+ }
+ // we shouldn't get here, but just in case, throw an error.
+ else {
+ ok(false, "unrecognized test!");
+ }
+
+ if (++test_counter === TOTAL_TESTS) {
+ SimpleTest.finish();
+ }
+}
+
+function startTest() {
+ var src = "file_testserver.sjs";
+ // append the file that should be served
+ src += "?file=" + escape("tests/dom/security/test/csp/file_null_baseuri.html");
+ // using 'unsafe-inline' since we load the testcase using an inline script
+ // within file_null_baseuri.html
+ src += "&csp=" + escape("default-src * 'unsafe-inline';");
+
+ document.getElementById("testframe").src = src;
+}
+
+
+SimpleTest.waitForExplicitFinish();
+startTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_object_inherit.html b/dom/security/test/csp/test_object_inherit.html
new file mode 100644
index 0000000000..0d563bde3f
--- /dev/null
+++ b/dom/security/test/csp/test_object_inherit.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1457100: Test OBJECT inherits CSP if needed</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ window.removeEventListener("message", receiveMessage);
+
+ var cspJSON = event.data.cspJSON;
+ ok(cspJSON.includes("img-src"), "found img-src directive");
+ ok(cspJSON.includes("https://bug1457100.test.com"), "found img-src value");
+
+ SimpleTest.finish();
+}
+
+document.getElementById("testframe").src = "file_object_inherit.html";
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_parent_location_js.html b/dom/security/test/csp/test_parent_location_js.html
new file mode 100644
index 0000000000..d456c809f2
--- /dev/null
+++ b/dom/security/test/csp/test_parent_location_js.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1550414: Add CSP test for setting parent location to javascript:</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/**
+ * Description of the test:
+ * Load a document with a CSP of essentially script-src 'none' which includes a
+ * same origin iframe which tries to modify the parent.location using a javascript:
+ * URI -> make sure the javascript: URI is blocked correctly!
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+function receiveMessage(event) {
+ window.removeEventListener("message", receiveMessage);
+ is(event.data.blockedURI, "inline", "blockedURI");
+ is(event.data.violatedDirective, "script-src-elem", "violatedDirective")
+ is(event.data.originalPolicy, "script-src 'nonce-bug1550414'", "originalPolicy");
+ SimpleTest.finish();
+}
+
+// using a postMessage handler to report the result back from
+// within the sandboxed iframe without 'allow-same-origin'.
+window.addEventListener("message", receiveMessage);
+
+document.getElementById("testframe").src = "file_parent_location_js.html";
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_path_matching.html b/dom/security/test/csp/test_path_matching.html
new file mode 100644
index 0000000000..a54de0a25c
--- /dev/null
+++ b/dom/security/test/csp/test_path_matching.html
@@ -0,0 +1,115 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 808292 - Implement path-level host-source matching to CSP</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="visibility: hidden">
+ <iframe style="width:100%;" id="testframe"></iframe>
+ </div>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+/* Description of the test:
+ * We are loading the following url (including a fragment portion):
+ * http://test1.example.com/tests/dom/security/test/csp/file_path_matching.js#foo
+ * using different policies and verify that the applied policy is accurately enforced.
+ */
+
+var policies = [
+ ["allowed", "*"],
+ ["allowed", "http://*"], // test for bug 1075230, enforcing scheme and wildcard
+ ["allowed", "test1.example.com"],
+ ["allowed", "test1.example.com/"],
+ ["allowed", "test1.example.com/tests/dom/security/test/csp/"],
+ ["allowed", "test1.example.com/tests/dom/security/test/csp/file_path_matching.js"],
+
+ ["allowed", "test1.example.com?foo=val"],
+ ["allowed", "test1.example.com/?foo=val"],
+ ["allowed", "test1.example.com/tests/dom/security/test/csp/?foo=val"],
+ ["allowed", "test1.example.com/tests/dom/security/test/csp/file_path_matching.js?foo=val"],
+
+ ["allowed", "test1.example.com#foo"],
+ ["allowed", "test1.example.com/#foo"],
+ ["allowed", "test1.example.com/tests/dom/security/test/csp/#foo"],
+ ["allowed", "test1.example.com/tests/dom/security/test/csp/file_path_matching.js#foo"],
+
+ ["allowed", "*.example.com"],
+ ["allowed", "*.example.com/"],
+ ["allowed", "*.example.com/tests/dom/security/test/csp/"],
+ ["allowed", "*.example.com/tests/dom/security/test/csp/file_path_matching.js"],
+
+ ["allowed", "test1.example.com:80"],
+ ["allowed", "test1.example.com:80/"],
+ ["allowed", "test1.example.com:80/tests/dom/security/test/csp/"],
+ ["allowed", "test1.example.com:80/tests/dom/security/test/csp/file_path_matching.js"],
+
+ ["allowed", "test1.example.com:*"],
+ ["allowed", "test1.example.com:*/"],
+ ["allowed", "test1.example.com:*/tests/dom/security/test/csp/"],
+ ["allowed", "test1.example.com:*/tests/dom/security/test/csp/file_path_matching.js"],
+
+ ["blocked", "test1.example.com/tests"],
+ ["blocked", "test1.example.com/tests/dom/security/test/csp"],
+ ["blocked", "test1.example.com/tests/dom/security/test/csp/file_path_matching.py"],
+
+ ["blocked", "test1.example.com:8888/tests"],
+ ["blocked", "test1.example.com:8888/tests/dom/security/test/csp"],
+ ["blocked", "test1.example.com:8888/tests/dom/security/test/csp/file_path_matching.py"],
+
+ // case insensitive matching for scheme and host, but case sensitive matching for paths
+ ["allowed", "HTTP://test1.EXAMPLE.com/tests/"],
+ ["allowed", "test1.EXAMPLE.com/tests/"],
+ ["blocked", "test1.example.com/tests/dom/security/test/CSP/?foo=val"],
+ ["blocked", "test1.example.com/tests/dom/security/test/csp/FILE_path_matching.js?foo=val"],
+]
+
+var counter = 0;
+var policy;
+
+function loadNextTest() {
+ if (counter == policies.length) {
+ SimpleTest.finish();
+ }
+ else {
+ policy = policies[counter++];
+ var src = "file_testserver.sjs?file=";
+ // append the file that should be served
+ src += (counter % 2 == 0)
+ // load url including ref: example.com#foo
+ ? escape("tests/dom/security/test/csp/file_path_matching.html")
+ // load url including query: example.com?val=foo (bug 1147026)
+ : escape("tests/dom/security/test/csp/file_path_matching_incl_query.html");
+
+ // append the CSP that should be used to serve the file
+ src += "&csp=" + escape("default-src 'none'; script-src " + policy[1]);
+
+ document.getElementById("testframe").addEventListener("load", test);
+ document.getElementById("testframe").src = src;
+ }
+}
+
+function test() {
+ try {
+ document.getElementById("testframe").removeEventListener('load', test);
+ var testframe = document.getElementById("testframe");
+ var divcontent = testframe.contentWindow.document.getElementById('testdiv').innerHTML;
+ is(divcontent, policy[0], "should be " + policy[0] + " in test " + (counter - 1) + "!");
+ }
+ catch (e) {
+ ok(false, "ERROR: could not access content in test " + (counter - 1) + "!");
+ }
+ loadNextTest();
+}
+
+loadNextTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_path_matching_redirect.html b/dom/security/test/csp/test_path_matching_redirect.html
new file mode 100644
index 0000000000..d3b2771d0a
--- /dev/null
+++ b/dom/security/test/csp/test_path_matching_redirect.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 808292 - Implement path-level host-source matching to CSP (redirects)</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="visibility: hidden">
+ <iframe style="width:100%;" id="testframe"></iframe>
+ </div>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+/* Description of the test:
+ * First, we try to load a script where the *path* does not match.
+ * Second, we try to load a script which is allowed by the CSPs
+ * script-src directive. The script then gets redirected to
+ * an URL where the host matches, but the path wouldn't.
+ * Since 'paths' should not be taken into account after redirects,
+ * that load should succeed. We are using a similar test setup
+ * as described in the spec, see:
+ * http://www.w3.org/TR/CSP11/#source-list-paths-and-redirects
+ */
+
+var policy = "script-src http://example.com http://test1.example.com/CSPAllowsScriptsInThatFolder";
+
+var tests = [
+ {
+ // the script in file_path_matching.html
+ // <script src="http://test1.example.com/tests/dom/security/..">
+ // is not within the allowlisted path by the csp-policy
+ // hence the script is 'blocked' by CSP.
+ expected: "blocked",
+ uri: "tests/dom/security/test/csp/file_path_matching.html"
+ },
+ {
+ // the script in file_path_matching_redirect.html
+ // <script src="http://example.com/tests/dom/..">
+ // gets redirected to: http://test1.example.com/tests/dom
+ // where after the redirect the path of the policy is not enforced
+ // anymore and hence execution of the script is 'allowed'.
+ expected: "allowed",
+ uri: "tests/dom/security/test/csp/file_path_matching_redirect.html"
+ },
+];
+
+var counter = 0;
+var curTest;
+
+function checkResult() {
+ try {
+ document.getElementById("testframe").removeEventListener('load', checkResult);
+ var testframe = document.getElementById("testframe");
+ var divcontent = testframe.contentWindow.document.getElementById('testdiv').innerHTML;
+ is(divcontent, curTest.expected, "should be blocked in test " + (counter - 1) + "!");
+ }
+ catch (e) {
+ ok(false, "ERROR: could not access content in test " + (counter - 1) + "!");
+ }
+ loadNextTest();
+}
+
+function loadNextTest() {
+ if (counter == tests.length) {
+ SimpleTest.finish();
+ }
+ else {
+ curTest = tests[counter++];
+ var src = "file_testserver.sjs";
+ // append the file that should be served
+ src += "?file=" + escape(curTest.uri);
+ // append the CSP that should be used to serve the file
+ src += "&csp=" + escape(policy);
+
+ document.getElementById("testframe").addEventListener("load", checkResult);
+ document.getElementById("testframe").src = src;
+ }
+}
+
+loadNextTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_ping.html b/dom/security/test/csp/test_ping.html
new file mode 100644
index 0000000000..3f911b7b6a
--- /dev/null
+++ b/dom/security/test/csp/test_ping.html
@@ -0,0 +1,103 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1100181 - CSP: Enforce connect-src when submitting pings</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/*
+ * Description of the test:
+ * We load a page with a given CSP and verify that hyperlink auditing
+ * is correctly evaluated through the "connect-src" directive.
+ */
+
+// Need to pref hyperlink auditing on since it's disabled by default.
+SpecialPowers.setBoolPref("browser.send_pings", true);
+
+SimpleTest.waitForExplicitFinish();
+
+var tests = [
+ {
+ result : "allowed",
+ policy : "connect-src 'self'"
+ },
+ {
+ result : "blocked",
+ policy : "connect-src 'none'"
+ }
+];
+
+// initializing to -1 so we start at index 0 when we start the test
+var counter = -1;
+
+function checkResult(aResult) {
+ is(aResult, tests[counter].result, "should be " + tests[counter].result + " in test " + counter + "!");
+ loadNextTest();
+}
+
+// We use the examiner to identify requests that hit the wire and requests
+// that are blocked by CSP and bubble up the result to the including iframe
+// document (parent).
+function examiner() {
+ SpecialPowers.addObserver(this, "csp-on-violate-policy");
+ SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
+}
+examiner.prototype = {
+ observe(subject, topic, data) {
+ if (topic === "specialpowers-http-notify-request") {
+ // making sure we do not bubble a result for something
+ // other then the request in question.
+ if (!data.includes("send-ping")) {
+ return;
+ }
+ checkResult("allowed");
+ return;
+ }
+
+ if (topic === "csp-on-violate-policy") {
+ // making sure we do not bubble a result for something
+ // other then the request in question.
+ var asciiSpec = SpecialPowers.getPrivilegedProps(
+ SpecialPowers.do_QueryInterface(subject, "nsIURI"), "asciiSpec");
+ if (!asciiSpec.includes("send-ping")) {
+ return;
+ }
+ checkResult("blocked");
+ }
+ },
+ remove() {
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
+ }
+}
+window.ConnectSrcExaminer = new examiner();
+
+function loadNextTest() {
+ counter++;
+ if (counter == tests.length) {
+ window.ConnectSrcExaminer.remove();
+ SimpleTest.finish();
+ return;
+ }
+
+ var src = "file_testserver.sjs";
+ // append the file that should be served
+ src += "?file=" + escape("tests/dom/security/test/csp/file_ping.html");
+ // append the CSP that should be used to serve the file
+ src += "&csp=" + escape(tests[counter].policy);
+
+ document.getElementById("testframe").src = src;
+}
+
+// start running the tests
+loadNextTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_policyuri_regression_from_multipolicy.html b/dom/security/test/csp/test_policyuri_regression_from_multipolicy.html
new file mode 100644
index 0000000000..8838f2fc45
--- /dev/null
+++ b/dom/security/test/csp/test_policyuri_regression_from_multipolicy.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Bug 924708</title>
+ <!--
+ test that a report-only policy that uses policy-uri is not incorrectly
+ enforced due to regressions introduced by Bug 836922.
+ -->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:200px;height:200px;" id='testframe'></iframe>
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+var testframe = document.getElementById('testframe');
+testframe.src = 'file_policyuri_regression_from_multipolicy.html';
+testframe.addEventListener('load', function checkInlineScriptExecuted () {
+ is(this.contentDocument.getElementById('testdiv').innerHTML,
+ 'Inline Script Executed',
+ 'Inline script should execute (it would be blocked by the policy, but the policy is report-only)');
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_punycode_host_src.html b/dom/security/test/csp/test_punycode_host_src.html
new file mode 100644
index 0000000000..3735275d34
--- /dev/null
+++ b/dom/security/test/csp/test_punycode_host_src.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1224225 - CSP source matching should work for punycoded domain names</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the test:
+ * We load scripts within an iframe and make sure that the
+ * CSP matching is same for punycode domain names as well as IDNA.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+
+var curTest;
+var counter = -1;
+
+const tests = [
+ { // test 1
+ description: "loads script as sub2.ält.example.org, but allowlist in CSP as sub2.xn--lt-uia.example.org",
+ action: "script-unicode-csp-punycode",
+ csp: "script-src http://sub2.xn--lt-uia.example.org;",
+ expected: "script-allowed",
+
+ },
+ { // test 2
+ description: "loads script as sub2.xn--lt-uia.example.org, and allowlist in CSP as sub2.xn--lt-uia.example.org",
+ action: "script-punycode-csp-punycode",
+ csp: "script-src http://sub2.xn--lt-uia.example.org;",
+ expected: "script-allowed",
+
+ },
+ { // test 3
+ description: "loads script as sub2.xn--lt-uia.example.org, and allowlist in CSP as sub2.xn--lt-uia.example.org",
+ action: "script-punycode-csp-punycode",
+ csp: "script-src *.xn--lt-uia.example.org;",
+ expected: "script-allowed",
+
+ },
+
+];
+
+function finishTest() {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+function checkResults(result) {
+ is(result, curTest.expected, curTest.description);
+ loadNextTest();
+}
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ checkResults(event.data.result);
+}
+
+function loadNextTest() {
+ counter++;
+ if (counter == tests.length) {
+ finishTest();
+ return;
+ }
+ curTest = tests[counter];
+ var testframe = document.getElementById("testframe");
+ testframe.src = `file_punycode_host_src.sjs?action=${curTest.action}&csp=${curTest.csp}`;
+}
+
+loadNextTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_redirects.html b/dom/security/test/csp/test_redirects.html
new file mode 100644
index 0000000000..3993560c14
--- /dev/null
+++ b/dom/security/test/csp/test_redirects.html
@@ -0,0 +1,142 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Tests for Content Security Policy during redirects</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+
+<iframe style="width:100%;height:300px;" id="harness"></iframe>
+<pre id="log"></pre>
+<script class="testbody" type="text/javascript">
+
+var path = "/tests/dom/security/test/csp/";
+
+// debugging
+function log(s) {
+ // dump("**" + s + "\n");
+ // var log = document.getElementById("log");
+ // log.textContent = log.textContent+s+"\n";
+}
+
+SpecialPowers.registerObservers("csp-on-violate-policy");
+
+// used to watch if requests are blocked by CSP or allowed through
+function examiner() {
+ SpecialPowers.addObserver(this, "csp-on-violate-policy");
+ SpecialPowers.addObserver(this, "specialpowers-csp-on-violate-policy");
+ SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
+}
+examiner.prototype = {
+ observe(subject, topic, data) {
+ var testpat = new RegExp("testid=([a-z0-9-]+)");
+ var asciiSpec;
+ var testid;
+
+ if (topic === "specialpowers-http-notify-request") {
+ // request was sent
+ var allowedUri = data;
+ if (!testpat.test(allowedUri)) return;
+ testid = testpat.exec(allowedUri)[1];
+ if (testExpectedResults[testid] == "completed") return;
+ log("allowed: "+allowedUri);
+ window.testResult(testid, allowedUri, true);
+ }
+
+ else if (topic === "csp-on-violate-policy" || topic === "specialpowers-csp-on-violate-policy") {
+ // request was blocked
+ asciiSpec = SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIURI"), "asciiSpec");
+ if (!testpat.test(asciiSpec)) return;
+ testid = testpat.exec(asciiSpec)[1];
+ // had to add this check because http-on-modify-request can fire after
+ // csp-on-violate-policy, apparently, even though the request does
+ // not hit the wire.
+ if (testExpectedResults[testid] == "completed") return;
+ log("BLOCKED: "+asciiSpec);
+ window.testResult(testid, asciiSpec, false);
+ }
+ },
+
+ remove() {
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ SpecialPowers.removeObserver(this, "specialpowers-csp-on-violate-policy");
+ SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
+ }
+}
+window.examiner = new examiner();
+
+// contains { test_frame_id : expected_result }
+var testExpectedResults = { "font-src": true,
+ "font-src-redir": false,
+ "frame-src": true,
+ "frame-src-redir": false,
+ "img-src": true,
+ "img-src-redir": false,
+ "media-src": true,
+ "media-src-redir": false,
+ "object-src": true,
+ "object-src-redir": false,
+ "script-src": true,
+ "script-src-redir": false,
+ "style-src": true,
+ "style-src-redir": false,
+ "xhr-src": true,
+ "xhr-src-redir": false,
+ "from-worker": true,
+ "script-src-redir-from-worker": true, // redir is allowed since policy isn't inherited
+ "xhr-src-redir-from-worker": true, // redir is allowed since policy isn't inherited
+ "fetch-src-redir-from-worker": true, // redir is allowed since policy isn't inherited
+ "from-blob-worker": true,
+ "script-src-redir-from-blob-worker": false,
+ "xhr-src-redir-from-blob-worker": false,
+ "fetch-src-redir-from-blob-worker": false,
+ "img-src-from-css": true,
+ "img-src-redir-from-css": false,
+ };
+
+// takes the name of the test, the URL that was tested, and whether the
+// load occurred
+var testResult = function(testName, url, result) {
+ log(" testName: "+testName+", result: "+result+", expected: "+testExpectedResults[testName]+"\n");
+ is(result, testExpectedResults[testName], testName+" test: "+url);
+
+ // mark test as completed
+ testExpectedResults[testName] = "completed";
+
+ // don't finish until we've run all the tests
+ for (var t in testExpectedResults) {
+ if (testExpectedResults[t] != "completed") {
+ return;
+ }
+ }
+
+ window.examiner.remove();
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv(
+ {'set':[// On a cellular connection the default preload value is 0 ("preload
+ // none"). Our Android emulators emulate a cellular connection, and
+ // so by default preload no media data. This causes the media_* tests
+ // to timeout. We set the default used by cellular connections to the
+ // same as used by non-cellular connections in order to get
+ // consistent behavior across platforms/devices.
+ ["media.preload.default", 2],
+ ["media.preload.default.cellular", 2]]},
+ function() {
+ // save this for last so that our listeners are registered.
+ // ... this loads the testbed of good and bad requests.
+ document.getElementById("harness").src = "file_redirects_main.html";
+ });
+</script>
+</pre>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/test_report.html b/dom/security/test/csp/test_report.html
new file mode 100644
index 0000000000..fc10cd0341
--- /dev/null
+++ b/dom/security/test/csp/test_report.html
@@ -0,0 +1,113 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=548193
+-->
+<head>
+ <title>Test for Bug 548193</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<iframe style="width:200px;height:200px;" id='cspframe'></iframe>
+<script class="testbody" type="text/javascript">
+
+/*
+ * Description of the test:
+ * We try to load an inline-src using a policy that constrains
+ * all scripts from running (default-src 'none'). We verify that
+ * the generated csp-report contains the expceted values. If any
+ * of the JSON is not formatted properly (e.g. not properly escaped)
+ * then JSON.parse will fail, which allows to pinpoint such errors
+ * in the catch block, and the test will fail. Since we use an
+ * observer, we can set the actual report-uri to a foo value.
+ */
+
+const testfile = "tests/dom/security/test/csp/file_report.html";
+const reportURI = "http://mochi.test:8888/foo.sjs";
+const policy = "default-src 'none' 'report-sample'; report-uri " + reportURI;
+const docUri = "http://mochi.test:8888/tests/dom/security/test/csp/file_testserver.sjs" +
+ "?file=tests/dom/security/test/csp/file_report.html" +
+ "&csp=default-src%20%27none%27%20%27report-sample%27%3B%20report-uri%20http%3A//mochi.test%3A8888/foo.sjs";
+
+window.checkResults = function(reportObj) {
+ var cspReport = reportObj["csp-report"];
+
+ // The following uris' fragments should be stripped before reporting:
+ // * document-uri
+ // * blocked-uri
+ // * source-file
+ // see http://www.w3.org/TR/CSP11/#violation-reports
+ is(cspReport["document-uri"], docUri, "Incorrect document-uri");
+
+ // we can not test for the whole referrer since it includes platform specific information
+ ok(cspReport.referrer.startsWith("http://mochi.test:8888/tests/dom/security/test/csp/test_report.html"),
+ "Incorrect referrer");
+
+ is(cspReport["blocked-uri"], "inline", "Incorrect blocked-uri");
+
+ is(cspReport["effective-directive"], "script-src-elem", "Incorrect effective-directive");
+ is(cspReport["violated-directive"], "script-src-elem", "Incorrect violated-directive");
+
+ is(cspReport["original-policy"], "default-src 'none' 'report-sample'; report-uri http://mochi.test:8888/foo.sjs",
+ "Incorrect original-policy");
+
+ is(cspReport.disposition, "enforce", "Incorrect disposition");
+
+ is(cspReport["status-code"], 200, "Incorrect status-code");
+
+ is(cspReport["source-file"], docUri, "Incorrect source-file");
+
+ is(cspReport["script-sample"], "\n var foo = \"propEscFoo\";\n var bar…",
+ "Incorrect script-sample");
+
+ is(cspReport["line-number"], 7, "Incorrect line-number");
+}
+
+var chromeScriptUrl = SimpleTest.getTestFileURL("file_report_chromescript.js");
+var script = SpecialPowers.loadChromeScript(chromeScriptUrl);
+
+script.addMessageListener('opening-request-completed', function ml(msg) {
+ if (msg.error) {
+ ok(false, "Could not query report (exception: " + msg.error + ")");
+ } else {
+ try {
+ var reportObj = JSON.parse(msg.report);
+ } catch (e) {
+ ok(false, "Could not parse JSON (exception: " + e + ")");
+ }
+ try {
+ // test for the proper values in the report object
+ window.checkResults(reportObj);
+ } catch (e) {
+ ok(false, "Could not query report (exception: " + e + ")");
+ }
+ }
+
+ script.removeMessageListener('opening-request-completed', ml);
+ script.sendAsyncMessage("finish");
+ SimpleTest.finish();
+});
+
+SimpleTest.waitForExplicitFinish();
+
+// load the resource which will generate a CSP violation report
+// save this for last so that our listeners are registered.
+var src = "file_testserver.sjs";
+// append the file that should be served
+src += "?file=" + escape(testfile);
+// append the CSP that should be used to serve the file
+src += "&csp=" + escape(policy);
+// appending a fragment so we can test that it's correctly stripped
+// for document-uri and source-file.
+src += "#foo";
+document.getElementById("cspframe").src = src;
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_report_font_cache.html b/dom/security/test/csp/test_report_font_cache.html
new file mode 100644
index 0000000000..40577a1e00
--- /dev/null
+++ b/dom/security/test/csp/test_report_font_cache.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+<iframe id="f"></iframe>
+
+<script>
+var chromeScriptUrl = SimpleTest.getTestFileURL("file_report_chromescript.js");
+var script = SpecialPowers.loadChromeScript(chromeScriptUrl);
+
+var reportedFont1 = false;
+var reportedFont3 = false;
+
+function reportListener(msg) {
+ if (!msg.error) {
+ // Step 3: Check the specific blocked URLs from the CSP reports.
+ let blocked = JSON.parse(msg.report)["csp-report"]["blocked-uri"]
+ .replace(/^.*\//, "");
+ switch (blocked) {
+ case "Ahem.ttf?report_font_cache-1":
+ ok(!reportedFont1, "should not have already reported Test Font 1");
+ ok(!reportedFont3, "should not have reported Test Font 3 before Test Font 1");
+ reportedFont1 = true;
+ break;
+ case "Ahem.ttf?report_font_cache-2":
+ ok(false, "should not have reported Test Font 2");
+ break;
+ case "Ahem.ttf?report_font_cache-3":
+ ok(!reportedFont3, "should not have already reported Test Font 3");
+ reportedFont3 = true;
+ break;
+ }
+ if (reportedFont1 && reportedFont3) {
+ script.removeMessageListener("opening-request-completed", reportListener);
+ script.sendAsyncMessage("finish");
+ SimpleTest.finish();
+ }
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+
+script.addMessageListener("opening-request-completed", reportListener);
+
+window.onmessage = function(message) {
+ // Step 2: Navigate to the second document, which will attempt to use the
+ // cached "Test Font 1" and then a new "Test Font 3", both of which will
+ // generate CSP reports. The "Test Font 2" entry in the user font cache
+ // should not cause a CSP report from this document.
+ is(message.data, "first-doc-ready");
+ f.src = "file_report_font_cache-2.html";
+};
+
+// Step 1: Prime the user font cache with entries for "Test Font 1",
+// "Test Font 2" and "Test Font 3".
+f.src = "file_report_font_cache-1.html";
+</script>
diff --git a/dom/security/test/csp/test_report_for_import.html b/dom/security/test/csp/test_report_for_import.html
new file mode 100644
index 0000000000..ddeee3b507
--- /dev/null
+++ b/dom/security/test/csp/test_report_for_import.html
@@ -0,0 +1,109 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=548193
+-->
+<head>
+ <title>Test for Bug 548193</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<iframe style="width:200px;height:200px;" id='cspframe'></iframe>
+<script class="testbody" type="text/javascript">
+
+/*
+ * Description of the test:
+ * We are loading a stylesheet using a csp policy that only allows styles from 'self'
+ * to be loaded. In other words, the *.css file itself should be allowed to load, but
+ * the @import file within the CSS should get blocked. We verify that the generated
+ * csp-report is sent and contains all the expected values.
+ * In detail, the test starts by sending an XHR request to the report-server
+ * which waits on the server side till the report was received and hands the
+ * report in JSON format back to the testfile which then verifies accuracy
+ * of all the different report fields in the CSP report.
+ */
+
+const TEST_FILE = "tests/dom/security/test/csp/file_report_for_import.html";
+const REPORT_URI =
+ "http://mochi.test:8888/tests/dom/security/test/csp/file_report_for_import_server.sjs?report";
+const POLICY = "style-src 'self'; report-uri " + REPORT_URI;
+
+const DOC_URI =
+ "http://mochi.test:8888/tests/dom/security/test/csp/file_testserver.sjs?" +
+ "file=tests/dom/security/test/csp/file_report_for_import.html&" +
+ "csp=style-src%20%27self%27%3B%20" +
+ "report-uri%20http%3A//mochi.test%3A8888/tests/dom/security/test/csp/" +
+ "file_report_for_import_server.sjs%3Freport";
+
+function checkResults(reportStr) {
+ try {
+ var reportObj = JSON.parse(reportStr);
+ var cspReport = reportObj["csp-report"];
+
+ is(cspReport["document-uri"], DOC_URI, "Incorrect document-uri");
+ is(cspReport.referrer,
+ "http://mochi.test:8888/tests/dom/security/test/csp/test_report_for_import.html",
+ "Incorrect referrer");
+ is(cspReport["violated-directive"],
+ "style-src-elem",
+ "Incorrect violated-directive");
+ is(cspReport["original-policy"], POLICY, "Incorrect original-policy");
+ is(cspReport["blocked-uri"],
+ "http://example.com/tests/dom/security/test/csp/file_report_for_import_server.sjs?stylesheet",
+ "Incorrect blocked-uri");
+
+ // we do not always set the following fields
+ is(cspReport["source-file"], undefined, "Incorrect source-file");
+ is(cspReport["script-sample"], undefined, "Incorrect script-sample");
+ is(cspReport["line-number"], undefined, "Incorrect line-number");
+ }
+ catch (e) {
+ ok(false, "Could not parse JSON (exception: " + e + ")");
+ }
+}
+
+function loadTestPageIntoFrame() {
+ // load the resource which will generate a CSP violation report
+ // save this for last so that our listeners are registered.
+ var src = "file_testserver.sjs";
+ // append the file that should be served
+ src += "?file=" + escape(TEST_FILE);
+ // append the CSP that should be used to serve the file
+ src += "&csp=" + escape(POLICY);
+ // appending a fragment so we can test that it's correctly stripped
+ // for document-uri and source-file.
+ src += "#foo";
+ document.getElementById("cspframe").src = src;
+}
+
+function runTest() {
+ // send an xhr request to the server which is processed async, which only
+ // returns after the server has received the csp report.
+ var myXHR = new XMLHttpRequest();
+ myXHR.open("GET", "file_report_for_import_server.sjs?queryresult");
+ myXHR.onload = function(e) {
+ checkResults(myXHR.responseText);
+ SimpleTest.finish();
+ }
+ myXHR.onerror = function(e) {
+ ok(false, "could not query results from server (" + e.message + ")");
+ SimpleTest.finish();
+ }
+ myXHR.send();
+
+ // give it some time and run the testpage
+ SimpleTest.executeSoon(loadTestPageIntoFrame);
+}
+
+SimpleTest.waitForExplicitFinish();
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_report_uri_missing_in_report_only_header.html b/dom/security/test/csp/test_report_uri_missing_in_report_only_header.html
new file mode 100644
index 0000000000..dc7eff2ac8
--- /dev/null
+++ b/dom/security/test/csp/test_report_uri_missing_in_report_only_header.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=847081
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 847081</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=847081">Mozilla Bug 847081</a>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<iframe id="cspframe"></iframe>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var stringBundleService = SpecialPowers.Cc["@mozilla.org/intl/stringbundle;1"]
+ .getService(SpecialPowers.Ci.nsIStringBundleService);
+var localizer = stringBundleService.createBundle("chrome://global/locale/security/csp.properties");
+var warningMsg = localizer.formatStringFromName("reportURInotInReportOnlyHeader", [window.location.origin]);
+
+function cleanup() {
+ SpecialPowers.postConsoleSentinel();
+ SimpleTest.finish();
+}
+
+// Since Bug 1584993 we parse the CSP in the parent too, hence the
+// same error message appears twice in the console.
+var recordConsoleMsgOnce = false;
+
+SpecialPowers.registerConsoleListener(function ConsoleMsgListener(aMsg) {
+ if (aMsg.message.indexOf(warningMsg) > -1) {
+ if (recordConsoleMsgOnce) {
+ return;
+ }
+ recordConsoleMsgOnce = true;
+
+ ok(true, "report-uri not specified in Report-Only should throw a CSP warning.");
+ SimpleTest.executeSoon(cleanup);
+ }
+ // Otherwise, if some other console message is present, we wait.
+});
+
+
+// set up and start testing
+SimpleTest.waitForExplicitFinish();
+document.getElementById('cspframe').src = 'file_report_uri_missing_in_report_only_header.html';
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_sandbox.html b/dom/security/test/csp/test_sandbox.html
new file mode 100644
index 0000000000..9fa123eadf
--- /dev/null
+++ b/dom/security/test/csp/test_sandbox.html
@@ -0,0 +1,249 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests for bugs 886164 and 671389</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+</div>
+
+<script class="testbody" type="text/javascript">
+
+var testCases = [
+ {
+ // Test 1: don't load image from non-same-origin; allow loading
+ // images from same-same origin
+ sandboxAttribute: "allow-same-origin",
+ csp: "default-src 'self'",
+ file: "file_sandbox_1.html",
+ results: { img1a_good: -1, img1_bad: -1 }
+ // fails if scripts execute
+ },
+ {
+ // Test 2: don't load image from non-same-origin; allow loading
+ // images from same-same origin, even without allow-same-origin
+ // flag
+ sandboxAttribute: "",
+ csp: "default-src 'self'",
+ file: "file_sandbox_2.html",
+ results: { img2_bad: -1, img2a_good: -1 }
+ // fails if scripts execute
+ },
+ {
+ // Test 3: disallow loading images from any host, even with
+ // allow-same-origin flag set
+ sandboxAttribute: "allow-same-origin",
+ csp: "default-src 'none'",
+ file: "file_sandbox_3.html",
+ results: { img3_bad: -1, img3a_bad: -1 },
+ // fails if scripts execute
+ },
+ {
+ // Test 4: disallow loading images from any host
+ sandboxAttribute: "",
+ csp: "default-src 'none'",
+ file: "file_sandbox_4.html",
+ results: { img4_bad: -1, img4a_bad: -1 }
+ // fails if scripts execute
+ },
+ {
+ // Test 5: disallow loading images or scripts, allow inline scripts
+ sandboxAttribute: "allow-scripts",
+ csp: "default-src 'none'; script-src 'unsafe-inline';",
+ file: "file_sandbox_5.html",
+ results: { img5_bad: -1, img5a_bad: -1, script5_bad: -1, script5a_bad: -1 },
+ nrOKmessages: 2 // sends 2 ok message
+ // fails if scripts execute
+ },
+ {
+ // Test 6: disallow non-same-origin images, allow inline and same origin scripts
+ sandboxAttribute: "allow-same-origin allow-scripts",
+ csp: "default-src 'self' 'unsafe-inline';",
+ file: "file_sandbox_6.html",
+ results: { img6_bad: -1, script6_bad: -1 },
+ nrOKmessages: 4 // sends 4 ok message
+ // fails if forms are not disallowed
+ },
+ {
+ // Test 7: same as Test 1
+ csp: "default-src 'self'; sandbox allow-same-origin",
+ file: "file_sandbox_7.html",
+ results: { img7a_good: -1, img7_bad: -1 }
+ },
+ {
+ // Test 8: same as Test 2
+ csp: "sandbox allow-same-origin; default-src 'self'",
+ file: "file_sandbox_8.html",
+ results: { img8_bad: -1, img8a_good: -1 }
+ },
+ {
+ // Test 9: same as Test 3
+ csp: "default-src 'none'; sandbox allow-same-origin",
+ file: "file_sandbox_9.html",
+ results: { img9_bad: -1, img9a_bad: -1 }
+ },
+ {
+ // Test 10: same as Test 4
+ csp: "default-src 'none'; sandbox allow-same-origin",
+ file: "file_sandbox_10.html",
+ results: { img10_bad: -1, img10a_bad: -1 }
+ },
+ {
+ // Test 11: same as Test 5
+ csp: "default-src 'none'; script-src 'unsafe-inline'; sandbox allow-scripts allow-same-origin",
+ file: "file_sandbox_11.html",
+ results: { img11_bad: -1, img11a_bad: -1, script11_bad: -1, script11a_bad: -1 },
+ nrOKmessages: 2 // sends 2 ok message
+ },
+ {
+ // Test 12: same as Test 6
+ csp: "sandbox allow-same-origin allow-scripts; default-src 'self' 'unsafe-inline';",
+ file: "file_sandbox_12.html",
+ results: { img12_bad: -1, script12_bad: -1 },
+ nrOKmessages: 4 // sends 4 ok message
+ },
+ {
+ // Test 13: same as Test 5 and Test 11, but:
+ // * using sandbox flag 'allow-scripts' in CSP and not as iframe attribute
+ // * not using allow-same-origin in CSP (so a new NullPrincipal is created).
+ csp: "default-src 'none'; script-src 'unsafe-inline'; sandbox allow-scripts",
+ file: "file_sandbox_13.html",
+ results: { img13_bad: -1, img13a_bad: -1, script13_bad: -1, script13a_bad: -1 },
+ nrOKmessages: 2 // sends 2 ok message
+ },
+];
+
+// a postMessage handler that is used by sandboxed iframes without
+// 'allow-same-origin' to communicate pass/fail back to this main page.
+// it expects to be called with an object like:
+// { ok: true/false,
+// desc: <description of the test> which it then forwards to ok() }
+window.addEventListener("message", receiveMessage);
+
+function receiveMessage(event) {
+ ok_wrapper(event.data.ok, event.data.desc);
+}
+
+var completedTests = 0;
+var passedTests = 0;
+
+var totalTests = (function() {
+ var nrCSPloadTests = 0;
+ for(var i = 0; i < testCases.length; i++) {
+ nrCSPloadTests += Object.keys(testCases[i].results).length;
+ if (testCases[i].nrOKmessages) {
+ // + number of expected postMessages from iframe
+ nrCSPloadTests += testCases[i].nrOKmessages;
+ }
+ }
+ return nrCSPloadTests;
+})();
+
+function ok_wrapper(result, desc) {
+ ok(result, desc);
+
+ completedTests++;
+
+ if (result) {
+ passedTests++;
+ }
+
+ if (completedTests === totalTests) {
+ window.examiner.remove();
+ SimpleTest.finish();
+ }
+}
+
+// Set the iframe src and sandbox attribute
+function runTest(test) {
+ var iframe = document.createElement('iframe');
+
+ document.getElementById('content').appendChild(iframe);
+
+ // set sandbox attribute
+ if (test.sandboxAttribute !== undefined) {
+ iframe.sandbox = test.sandboxAttribute;
+ }
+
+ // set query string
+ var src = 'file_testserver.sjs';
+ // path where the files are
+ var path = '/tests/dom/security/test/csp/';
+
+ src += '?file=' + escape(path+test.file);
+
+ if (test.csp !== undefined) {
+ src += '&csp=' + escape(test.csp);
+ }
+
+ iframe.src = src;
+ iframe.width = iframe.height = 10;
+}
+
+// Examiner related
+
+// This is used to watch the blocked data bounce off CSP and allowed data
+// get sent out to the wire.
+function examiner() {
+ SpecialPowers.addObserver(this, "csp-on-violate-policy");
+ SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
+}
+
+examiner.prototype = {
+ observe(subject, topic, data) {
+ var testpat = new RegExp("testid=([a-z0-9_]+)");
+
+ //_good things better be allowed!
+ //_bad things better be stopped!
+
+ if (topic === "specialpowers-http-notify-request") {
+ //these things were allowed by CSP
+ var uri = data;
+ if (!testpat.test(uri)) return;
+ var testid = testpat.exec(uri)[1];
+
+ if(/_good/.test(testid)) {
+ ok_wrapper(true, uri + " is allowed by csp");
+ } else {
+ ok_wrapper(false, uri + " should not be allowed by csp");
+ }
+ }
+
+ if(topic === "csp-on-violate-policy") {
+ //these were blocked... record that they were blocked
+ var asciiSpec = SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIURI"), "asciiSpec");
+ if (!testpat.test(asciiSpec)) return;
+ var testid = testpat.exec(asciiSpec)[1];
+ if(/_bad/.test(testid)) {
+ ok_wrapper(true, asciiSpec + " was blocked by \"" + data + "\"");
+ } else {
+ ok_wrapper(false, asciiSpec + " should have been blocked by \"" + data + "\"");
+ }
+ }
+ },
+
+ // must eventually call this to remove the listener,
+ // or mochitests might get borked.
+ remove() {
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
+ }
+}
+
+window.examiner = new examiner();
+
+SimpleTest.waitForExplicitFinish();
+
+(function() { // Run tests:
+ for(var i = 0; i < testCases.length; i++) {
+ runTest(testCases[i]);
+ }
+})();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_sandbox_allow_scripts.html b/dom/security/test/csp/test_sandbox_allow_scripts.html
new file mode 100644
index 0000000000..68544a5178
--- /dev/null
+++ b/dom/security/test/csp/test_sandbox_allow_scripts.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1396320: Fix CSP sandbox regression for allow-scripts</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+<script class="testbody" type="text/javascript">
+
+/* Description of the test:
+ * Load an iframe using a CSP of 'sandbox allow-scripts' and make sure
+ * the security context of the iframe is sandboxed (cross origin)
+ */
+SimpleTest.waitForExplicitFinish();
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ is(event.data.result, "",
+ "document.domain of sandboxed iframe should be opaque");
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+let testframe = document.getElementById("testframe");
+testframe.src = "file_sandbox_allow_scripts.html";
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_scheme_relative_sources.html b/dom/security/test/csp/test_scheme_relative_sources.html
new file mode 100644
index 0000000000..3de3d98d69
--- /dev/null
+++ b/dom/security/test/csp/test_scheme_relative_sources.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 921493 - CSP: test allowlisting of scheme-relative sources</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+/* Description of the test:
+ * We load http and https pages and verify that scheme relative sources
+ * are allowed unless its a downgrade from https -> http.
+ *
+ * Please note that the policy contains 'unsafe-inline' so we can use
+ * an inline script to query the result from within the sandboxed iframe
+ * and report it back to the parent document.
+ */
+
+var POLICY = "default-src 'none'; script-src 'unsafe-inline' example.com;";
+
+var tests = [
+ {
+ description: "http -> http",
+ from: "http",
+ to: "http",
+ result: "allowed",
+ },
+ {
+ description: "http -> https",
+ from: "http",
+ to: "https",
+ result: "allowed",
+ },
+ {
+ description: "https -> https",
+ from: "https",
+ to: "https",
+ result: "allowed",
+ },
+ {
+ description: "https -> http",
+ from: "https",
+ to: "http",
+ result: "blocked",
+ }
+];
+
+var counter = 0;
+var curTest;
+
+function loadNextTest() {
+ if (counter == tests.length) {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+ return;
+ }
+
+ curTest = tests[counter++];
+
+ var src = curTest.from +
+ "://example.com/tests/dom/security/test/csp/file_scheme_relative_sources.sjs" +
+ "?scheme=" + curTest.to +
+ "&policy=" + escape(POLICY);
+
+ document.getElementById("testframe").src = src;
+}
+
+// using a postMessage handler to report the result back from
+// within the sandboxed iframe without 'allow-same-origin'.
+window.addEventListener("message", receiveMessage);
+
+function receiveMessage(event) {
+
+ is(event.data.result, curTest.result,
+ "should be " + curTest.result + " in test (" + curTest.description + ")!");
+
+ loadNextTest();
+}
+
+// get the test started
+loadNextTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_script_template.html b/dom/security/test/csp/test_script_template.html
new file mode 100644
index 0000000000..a71ebfe960
--- /dev/null
+++ b/dom/security/test/csp/test_script_template.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1548385 - CSP: Test script template</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/**
+ * Description of the test:
+ * We load a document using a CSP of "default-src 'unsafe-inline'"
+ * and make sure that an external script within a template gets
+ * blocked correctly.
+ */
+
+const CSP_BLOCKED_SUBJECT = "csp-on-violate-policy";
+const CSP_ALLOWED_SUBJECT = "specialpowers-http-notify-request";
+
+SimpleTest.waitForExplicitFinish();
+
+function examiner() {
+ SpecialPowers.addObserver(this, CSP_BLOCKED_SUBJECT);
+ SpecialPowers.addObserver(this, CSP_ALLOWED_SUBJECT);
+}
+
+examiner.prototype = {
+ observe(subject, topic, data) {
+ if (topic == CSP_BLOCKED_SUBJECT) {
+ let jsFileName = SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIURI"), "asciiSpec");
+ if (jsFileName.endsWith("file_script_template.js")) {
+ ok(true, "js file blocked by CSP");
+ this.removeAndFinish();
+ }
+ }
+
+ if (topic == CSP_ALLOWED_SUBJECT) {
+ if (data.endsWith("file_script_template.js")) {
+ ok(false, "js file allowed by CSP");
+ this.removeAndFinish();
+ }
+ }
+ },
+
+ removeAndFinish() {
+ SpecialPowers.removeObserver(this, CSP_BLOCKED_SUBJECT);
+ SpecialPowers.removeObserver(this, CSP_ALLOWED_SUBJECT);
+ SimpleTest.finish();
+ }
+}
+
+window.examiner = new examiner();
+document.getElementById("testframe").src = "file_script_template.html";
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_security_policy_violation_event.html b/dom/security/test/csp/test_security_policy_violation_event.html
new file mode 100644
index 0000000000..0d5cfade9c
--- /dev/null
+++ b/dom/security/test/csp/test_security_policy_violation_event.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta http-equiv="Content-Security-Policy" content="img-src 'none'">
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script>
+SimpleTest.waitForExplicitFinish();
+
+document.addEventListener("securitypolicyviolation", (e) => {
+ SimpleTest.is(e.blockedURI, "http://mochi.test:8888/foo/bar.jpg", "blockedURI");
+ SimpleTest.is(e.violatedDirective, "img-src", "violatedDirective")
+ SimpleTest.is(e.originalPolicy, "img-src 'none'", "originalPolicy");
+ SimpleTest.finish();
+});
+</script>
+<img src="http://mochi.test:8888/foo/bar.jpg">
diff --git a/dom/security/test/csp/test_self_none_as_hostname_confusion.html b/dom/security/test/csp/test_self_none_as_hostname_confusion.html
new file mode 100644
index 0000000000..5f6958220e
--- /dev/null
+++ b/dom/security/test/csp/test_self_none_as_hostname_confusion.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=587377
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 587377</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=587377">Mozilla Bug 587377</a>
+<p id="display"></p>
+
+<iframe id="cspframe"></iframe>
+
+<pre id="test">
+
+<script class="testbody" type="text/javascript">
+// Load locale string during mochitest
+var stringBundleService = SpecialPowers.Cc["@mozilla.org/intl/stringbundle;1"]
+ .getService(SpecialPowers.Ci.nsIStringBundleService);
+var localizer = stringBundleService.createBundle("chrome://global/locale/security/csp.properties");
+var confusionMsg = localizer.formatStringFromName("hostNameMightBeKeyword", ["SELF", "self"]);
+
+function cleanup() {
+ SpecialPowers.postConsoleSentinel();
+ SimpleTest.finish();
+};
+
+// To prevent the test from asserting twice and calling SimpleTest.finish() twice,
+// startTest will be marked false as soon as the confusionMsg is detected.
+startTest = false;
+SpecialPowers.registerConsoleListener(function ConsoleMsgListener(aMsg) {
+ if (startTest) {
+ if (aMsg.message.indexOf(confusionMsg) > -1) {
+ startTest = false;
+ ok(true, "CSP header with a hostname similar to keyword should be warned");
+ SimpleTest.executeSoon(cleanup);
+ }
+ // Otherwise, the warning hasn't happened yet so we wait.
+ }
+});
+
+// set up and start testing
+SimpleTest.waitForExplicitFinish();
+document.getElementById('cspframe').src = 'file_self_none_as_hostname_confusion.html';
+startTest = true;
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_sendbeacon.html b/dom/security/test/csp/test_sendbeacon.html
new file mode 100644
index 0000000000..3b0df34c05
--- /dev/null
+++ b/dom/security/test/csp/test_sendbeacon.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1234813 - sendBeacon should not throw if blocked by Content Policy</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<iframe style="width:100%;" id="testframe" src="file_sendbeacon.html"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the test:
+ * Let's try to fire a sendBeacon which gets blocked by CSP. Let's make sure
+ * sendBeacon does not throw an exception.
+ */
+SimpleTest.waitForExplicitFinish();
+
+// a postMessage handler used to bubble up the
+// result from within the iframe.
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ var result = event.data.result;
+ is(result, "blocked-beacon-does-not-throw", "sendBeacon should not throw");
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_service_worker.html b/dom/security/test/csp/test_service_worker.html
new file mode 100644
index 0000000000..1c274990f8
--- /dev/null
+++ b/dom/security/test/csp/test_service_worker.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1208559 - ServiceWorker registration not governed by CSP</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the test:
+ * Spawning a worker from https://example.com but script-src is 'test1.example.com'
+ * CSP is not consulted
+ */
+SimpleTest.waitForExplicitFinish();
+
+var tests = [
+ {
+ policy: "default-src 'self'; script-src 'unsafe-inline'; child-src test1.example.com;",
+ expected: "blocked"
+ },
+];
+
+var counter = 0;
+var curTest;
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ is(event.data.result, curTest.expected, "Should be (" + curTest.expected + ") in Test " + counter + "!");
+ loadNextTest();
+}
+
+onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["privacy.partition.serviceWorkers", true],
+ ]}, loadNextTest);
+}
+
+function loadNextTest() {
+ if (counter == tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+ curTest = tests[counter++];
+ var src = "https://example.com/tests/dom/security/test/csp/file_testserver.sjs";
+ // append the file that should be served
+ src += "?file=" + escape("tests/dom/security/test/csp/file_service_worker.html");
+ // append the CSP that should be used to serve the file
+ src += "&csp=" + escape(curTest.policy);
+ document.getElementById("testframe").src = src;
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_strict_dynamic.html b/dom/security/test/csp/test_strict_dynamic.html
new file mode 100644
index 0000000000..f894e6d447
--- /dev/null
+++ b/dom/security/test/csp/test_strict_dynamic.html
@@ -0,0 +1,133 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1299483 - CSP: Implement 'strict-dynamic'</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+/* Description of the test:
+ * We load scripts with a CSP of 'strict-dynamic' with valid
+ * and invalid nonces and make sure scripts are allowed/blocked
+ * accordingly. Different tests load inline and external scripts
+ * also using a CSP including http: and https: making sure
+ * other srcs are invalided by 'strict-dynamic'.
+ */
+
+var tests = [
+ {
+ desc: "strict-dynamic with valid nonce should be allowed",
+ result: "allowed",
+ file: "file_strict_dynamic_script_extern.html",
+ policy: "script-src 'strict-dynamic' 'nonce-foo' https: 'none' 'self'"
+ },
+ {
+ desc: "strict-dynamic with invalid nonce should be blocked",
+ result: "blocked",
+ file: "file_strict_dynamic_script_extern.html",
+ policy: "script-src 'strict-dynamic' 'nonce-bar' http: http://example.com"
+ },
+ {
+ desc: "strict-dynamic, allowlist and invalid nonce should be blocked",
+ result: "blocked",
+ file: "file_strict_dynamic_script_extern.html",
+ policy: "script-src 'strict-dynamic' 'nonce-bar' 'unsafe-inline' http: http://example.com"
+ },
+ {
+ desc: "strict-dynamic with no 'nonce-' should be blocked",
+ result: "blocked",
+ file: "file_strict_dynamic_script_extern.html",
+ policy: "script-src 'strict-dynamic'"
+ },
+ // inline scripts
+ {
+ desc: "strict-dynamic with valid nonce should be allowed",
+ result: "allowed",
+ file: "file_strict_dynamic_script_inline.html",
+ policy: "script-src 'strict-dynamic' 'nonce-foo' https: 'none' 'self'"
+ },
+ {
+ desc: "strict-dynamic with invalid nonce should be blocked",
+ result: "blocked",
+ file: "file_strict_dynamic_script_inline.html",
+ policy: "script-src 'strict-dynamic' 'nonce-bar' http: http://example.com"
+ },
+ {
+ desc: "strict-dynamic, unsafe-inline and invalid nonce should be blocked",
+ result: "blocked",
+ file: "file_strict_dynamic_script_inline.html",
+ policy: "script-src 'strict-dynamic' 'nonce-bar' 'unsafe-inline' http: http://example.com"
+ },
+ {
+ desc: "strict-dynamic with no 'nonce-' should be blocked",
+ result: "blocked",
+ file: "file_strict_dynamic_script_inline.html",
+ policy: "script-src 'strict-dynamic'"
+ },
+ {
+ desc: "strict-dynamic with DOM events should be blocked",
+ result: "blocked",
+ file: "file_strict_dynamic_script_events.html",
+ policy: "script-src 'strict-dynamic' 'nonce-foo'"
+ },
+ {
+ // marquee is a special snowflake
+ desc: "strict-dynamic with DOM events should be blocked (marquee)",
+ result: "blocked",
+ file: "file_strict_dynamic_script_events_marquee.html",
+ policy: "script-src 'strict-dynamic' 'nonce-foo'"
+ },
+ {
+ desc: "strict-dynamic with JS URLs should be blocked",
+ result: "blocked",
+ file: "file_strict_dynamic_js_url.html",
+ policy: "script-src 'strict-dynamic' 'nonce-foo'"
+ },
+];
+
+var counter = 0;
+var curTest;
+
+function loadNextTest() {
+ if (counter == tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ curTest = tests[counter++];
+ var src = "file_testserver.sjs?file=";
+ // append the file that should be served
+ src += escape("tests/dom/security/test/csp/" + curTest.file)
+ // append the CSP that should be used to serve the file
+ src += "&csp=" + escape(curTest.policy);
+
+ document.getElementById("testframe").addEventListener("load", test);
+ document.getElementById("testframe").src = src;
+}
+
+function test() {
+ try {
+ document.getElementById("testframe").removeEventListener('load', test);
+ var testframe = document.getElementById("testframe");
+ var divcontent = testframe.contentWindow.document.getElementById('testdiv').innerHTML;
+ is(divcontent, curTest.result, curTest.desc);
+ }
+ catch (e) {
+ ok(false, "ERROR: could not access content for test: '" + curTest.desc + "'");
+ }
+ loadNextTest();
+}
+
+// start running the tests
+loadNextTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_strict_dynamic_default_src.html b/dom/security/test/csp/test_strict_dynamic_default_src.html
new file mode 100644
index 0000000000..53eb899ab2
--- /dev/null
+++ b/dom/security/test/csp/test_strict_dynamic_default_src.html
@@ -0,0 +1,136 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1299483 - CSP: Implement 'strict-dynamic'</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+/* Description of the test:
+ * We load scripts and images with a CSP of 'strict-dynamic' making sure
+ * allowlists get ignored for scripts but not for images when strict-dynamic
+ * appears in default-src.
+ *
+ * Please note that we do not support strict-dynamic within default-src yet,
+ * see Bug 1313937. When updating this test please do not change the
+ * csp policies, but only replace todo_is() with is().
+ */
+
+var tests = [
+ {
+ script_desc: "(test1) script should be allowed because of valid nonce",
+ img_desc: "(test1) img should be allowed because of 'self'",
+ script_result: "allowed",
+ img_result: "allowed",
+ policy: "default-src 'strict-dynamic' 'self'; script-src 'nonce-foo'"
+ },
+ {
+ script_desc: "(test 2) script should be blocked because of invalid nonce",
+ img_desc: "(test 2) img should be allowed because of valid scheme-src",
+ script_result: "blocked",
+ img_result: "allowed",
+ policy: "default-src 'strict-dynamic' http:; script-src 'nonce-bar' http:"
+ },
+ {
+ script_desc: "(test 3) script should be blocked because of invalid nonce",
+ img_desc: "(test 3) img should be allowed because of valid host-src",
+ script_result: "blocked",
+ script_enforced: "",
+ img_result: "allowed",
+ policy: "default-src 'strict-dynamic' mochi.test; script-src 'nonce-bar' http:"
+ },
+ {
+ script_desc: "(test 4) script should be allowed because of valid nonce",
+ img_desc: "(test 4) img should be blocked because of default-src 'strict-dynamic'",
+ script_result: "allowed",
+ img_result: "blocked",
+ policy: "default-src 'strict-dynamic'; script-src 'nonce-foo'"
+ },
+ // some reverse order tests (have script-src appear before default-src)
+ {
+ script_desc: "(test 5) script should be allowed because of valid nonce",
+ img_desc: "(test 5) img should be blocked because of default-src 'strict-dynamic'",
+ script_result: "allowed",
+ img_result: "blocked",
+ policy: "script-src 'nonce-foo'; default-src 'strict-dynamic';"
+ },
+ {
+ script_desc: "(test 6) script should be allowed because of valid nonce",
+ img_desc: "(test 6) img should be blocked because of default-src http:",
+ script_result: "blocked",
+ img_result: "blocked",
+ policy: "script-src 'nonce-bar' http:; default-src 'strict-dynamic' http:;"
+ },
+ {
+ script_desc: "(test 7) script should be allowed because of invalid nonce",
+ img_desc: "(test 7) img should be blocked because of image-src http:",
+ script_result: "blocked",
+ img_result: "blocked",
+ policy: "script-src 'nonce-bar' http:; default-src 'strict-dynamic' http:; img-src http:"
+ },
+];
+
+var counter = 0;
+var curTest;
+
+function loadNextTest() {
+ if (counter == tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ curTest = tests[counter++];
+ var src = "file_testserver.sjs?file=";
+ // append the file that should be served
+ src += escape("tests/dom/security/test/csp/file_strict_dynamic_default_src.html");
+ // append the CSP that should be used to serve the file
+ src += "&csp=" + escape(curTest.policy);
+
+ document.getElementById("testframe").addEventListener("load", checkResults);
+ document.getElementById("testframe").src = src;
+}
+
+function checkResults() {
+ try {
+ var testframe = document.getElementById("testframe");
+ testframe.removeEventListener('load', checkResults);
+
+ // check if script loaded
+ var divcontent = testframe.contentWindow.document.getElementById('testdiv').innerHTML;
+ var imgcontent = testframe.contentWindow.document.getElementById('testimage').dataset.result;
+ if (curTest.script_result === "blocked") {
+ todo_is(divcontent, curTest.script_result, curTest.script_desc);
+ }
+ else {
+ is(divcontent, curTest.script_result, curTest.script_desc);
+ }
+
+ // check if image loaded
+ var testimg = testframe.contentWindow.document.getElementById("testimage");
+ if (curTest.img_result === "allowed") {
+ todo_is(imgcontent, curTest.img_result, curTest.img_desc);
+ }
+ else {
+ is(imgcontent, curTest.img_result, curTest.img_desc);
+ }
+ }
+ catch (e) {
+ ok(false, "ERROR: could not access content for test: '" + curTest.script_desc + "'");
+ }
+
+ loadNextTest();
+}
+
+// start running the tests
+loadNextTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_strict_dynamic_parser_inserted.html b/dom/security/test/csp/test_strict_dynamic_parser_inserted.html
new file mode 100644
index 0000000000..63d2c5a256
--- /dev/null
+++ b/dom/security/test/csp/test_strict_dynamic_parser_inserted.html
@@ -0,0 +1,94 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1299483 - CSP: Implement 'strict-dynamic'</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+/* Description of the test:
+ * We loader parser and non parser inserted scripts making sure that
+ * parser inserted scripts are blocked if strict-dynamic is present
+ * and no valid nonce and also making sure that non-parser inserted
+ * scripts are allowed to execute.
+ */
+
+var tests = [
+ {
+ desc: "(parser inserted script) using doc.write(<script>) should be blocked",
+ result: "blocked",
+ file: "file_strict_dynamic_parser_inserted_doc_write.html",
+ policy: "script-src 'strict-dynamic' 'nonce-foo' http:"
+ },
+ {
+ desc: "(parser inserted script with valid nonce) using doc.write(<script>) should be allowed",
+ result: "allowed",
+ file: "file_strict_dynamic_parser_inserted_doc_write_correct_nonce.html",
+ policy: "script-src 'strict-dynamic' 'nonce-foo' https:"
+ },
+ {
+ desc: "(non parser inserted script) using appendChild() should allow external script",
+ result: "allowed",
+ file: "file_strict_dynamic_non_parser_inserted.html",
+ policy: "script-src 'strict-dynamic' 'nonce-foo' https:"
+ },
+ {
+ desc: "(non parser inserted script) using appendChild() should allow inline script",
+ result: "allowed",
+ file: "file_strict_dynamic_non_parser_inserted_inline.html",
+ policy: "script-src 'strict-dynamic' 'nonce-foo' https:"
+ },
+ {
+ desc: "strict-dynamic should not invalidate 'unsafe-eval'",
+ result: "allowed",
+ file: "file_strict_dynamic_unsafe_eval.html",
+ policy: "script-src 'strict-dynamic' 'nonce-foo' 'unsafe-eval'"
+ },
+];
+
+var counter = 0;
+var curTest;
+
+function loadNextTest() {
+ if (counter == tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ curTest = tests[counter++];
+ var src = "file_testserver.sjs?file=";
+ // append the file that should be served
+ src += escape("tests/dom/security/test/csp/" + curTest.file)
+ // append the CSP that should be used to serve the file
+ src += "&csp=" + escape(curTest.policy);
+
+ document.getElementById("testframe").addEventListener("load", test);
+ document.getElementById("testframe").src = src;
+}
+
+function test() {
+ try {
+ document.getElementById("testframe").removeEventListener('load', test);
+ var testframe = document.getElementById("testframe");
+ var divcontent = testframe.contentWindow.document.getElementById('testdiv').innerHTML;
+ is(divcontent, curTest.result, curTest.desc);
+ }
+ catch (e) {
+ ok(false, "ERROR: could not access content for test: '" + curTest.desc + "'");
+ }
+ loadNextTest();
+}
+
+// start running the tests
+loadNextTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_subframe_run_js_if_allowed.html b/dom/security/test/csp/test_subframe_run_js_if_allowed.html
new file mode 100644
index 0000000000..fbf5a885cd
--- /dev/null
+++ b/dom/security/test/csp/test_subframe_run_js_if_allowed.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=702439
+
+This test verifies that child iframes of CSP documents are
+permitted to execute javascript: URLs assuming the policy
+allows this.
+-->
+<head>
+ <title>Test for Bug 702439</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe id="i"></iframe>
+<script class="testbody" type="text/javascript">
+var javascript_link_ran = false;
+
+// check that the script in the child frame's javascript: URL ran
+function checkResult()
+{
+ is(javascript_link_ran, true,
+ "javascript URL didn't execute");
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+document.getElementById('i').src = 'file_subframe_run_js_if_allowed.html';
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_svg_inline_style.html b/dom/security/test/csp/test_svg_inline_style.html
new file mode 100644
index 0000000000..c05ca20467
--- /dev/null
+++ b/dom/security/test/csp/test_svg_inline_style.html
@@ -0,0 +1,135 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1262842: Test CSP inline style within svg image</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe id="img_base"></iframe>
+<iframe id="img_csp"></iframe>
+<iframe id="img_base_srcset"></iframe>
+<iframe id="img_csp_srcset"></iframe>
+<iframe id="doc_base"></iframe>
+<iframe id="doc_csp"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+// Description of the two tests:
+// * CSP should not apply to SVGs loaded as images (in src or srcset)
+// * CSP should apply to SVGs loaded as document
+// Since we have to test inline styles within SVGs, we loaded the SVGs
+// and then take screenshots to comopare that the two SVGs are identical.
+
+SimpleTest.waitForExplicitFinish();
+
+let img_base = document.getElementById("img_base");
+let img_csp = document.getElementById("img_csp");
+let img_base_srcset = document.getElementById("img_base_srcset");
+let img_csp_srcset = document.getElementById("img_csp_srcset");
+let doc_base = document.getElementById("doc_base");
+let doc_csp = document.getElementById("doc_csp");
+
+let loadedFrames = 0;
+
+async function compareSVGs() {
+ loadedFrames++;
+ if (loadedFrames != 6) {
+ return;
+ }
+ // compare the two iframes where SVGs are loaded as images
+ try {
+ let img_base_snap = await snapshotWindow(img_base.contentWindow);
+ let img_csp_snap = await snapshotWindow(img_csp.contentWindow);
+
+ ok(compareSnapshots(img_base_snap, img_csp_snap, true)[0],
+ "CSP should not apply to SVG loaded as image");
+ } catch(err) {
+ ok(false, "img error: " + err.message);
+ }
+
+ // compare the two iframes where SVGs are loaded as images with srcset
+ try {
+ let img_base_snap_srcset = await snapshotWindow(img_base_srcset.contentWindow);
+ let img_csp_snap_srcset = await snapshotWindow(img_csp_srcset.contentWindow);
+
+ ok(compareSnapshots(img_base_snap_srcset, img_csp_snap_srcset, true)[0],
+ "CSP should not apply to SVG loaded as image with srcset");
+ } catch(err) {
+ ok(false, "img error: " + err.message);
+ }
+
+ // compare the two iframes where SVGs are loaded as documents
+ try {
+ let doc_base_snap = await snapshotWindow(doc_base.contentWindow);
+ let doc_csp_snap = await snapshotWindow(doc_csp.contentWindow);
+
+ ok(compareSnapshots(doc_base_snap, doc_csp_snap, true)[0],
+ "CSP should apply to SVG loaded as document");
+ } catch(err) {
+ ok(false, "doc error: " + err.message);
+ }
+
+ SimpleTest.finish();
+}
+
+// load SVG as images
+img_base.onerror = function() {
+ ok(false, "sanity: img_base onerror should not fire");
+}
+img_base.onload = function() {
+ ok(true, "sanity: img_base onload should fire");
+ compareSVGs();
+}
+img_base.src = "file_svg_inline_style_base.html";
+
+img_csp.onerror = function() {
+ ok(false, "sanity: img_csp onerror should not fire");
+}
+img_csp.onload = function() {
+ ok(true, "sanity: img_csp onload should fire");
+ compareSVGs();
+}
+img_csp.src = "file_svg_inline_style_csp.html";
+
+img_base_srcset.onerror = function() {
+ ok(false, "sanity: img_base_srcset onerror should not fire");
+}
+img_base_srcset.onload = function() {
+ ok(true, "sanity: img_base_srcset onload should fire");
+ compareSVGs();
+}
+img_base_srcset.src = "file_svg_srcset_inline_style_base.html";
+
+img_csp_srcset.onerror = function() {
+ ok(false, "sanity: img_csp_srcset onerror should not fire");
+}
+img_csp_srcset.onload = function() {
+ ok(true, "sanity: img_csp_srcset onload should fire");
+ compareSVGs();
+}
+img_csp_srcset.src = "file_svg_srcset_inline_style_csp.html";
+
+// load SVG as documnents
+doc_base.onerror = function() {
+ ok(false, "sanity: doc_base onerror should not fire");
+}
+doc_base.onload = function() {
+ ok(true, "sanity: doc_base onload should fire");
+ compareSVGs();
+}
+doc_base.src = "file_svg_inline_style_server.sjs?svg_no_inline_style&5";
+
+doc_csp.onerror = function() {
+ ok(false, "sanity: doc_csp onerror should not fire");
+}
+doc_csp.onload = function() {
+ ok(true, "sanity: doc_csp onload should fire");
+ compareSVGs();
+}
+doc_csp.src = "file_svg_inline_style_server.sjs?svg_inline_style_csp&6";
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_uir_top_nav.html b/dom/security/test/csp/test_uir_top_nav.html
new file mode 100644
index 0000000000..57005ba6f9
--- /dev/null
+++ b/dom/security/test/csp/test_uir_top_nav.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1391011: Test uir for toplevel navigations</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+/* Description of the test:
+ * We load an https page which defines upgrade-insecure-requests into an iframe
+ * and perform a same origin and a cross origin toplevel load and make sure that
+ * upgrade-insecure-requests applies to the same origin load.
+ */
+
+let totalTests = 2;
+let testCounter = 0;
+
+function checkResults(aResult) {
+ ok(aResult == "https://example.com/tests/dom/security/test/csp/file_uir_top_nav_dummy.html" ||
+ aResult == "http://test1.example.com/tests/dom/security/test/csp/file_uir_top_nav_dummy.html",
+ "same origin should be upgraded to https, cross origin should remain http");
+ if (++testCounter < totalTests) {
+ return;
+ }
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ checkResults(event.data.result);
+}
+
+function startTest() {
+ document.getElementById("testframe").src =
+ "https://example.com/tests/dom/security/test/csp/file_uir_top_nav.html";
+}
+
+// Don't upgrade to https to test that upgrade-insecure-requests acts correctly and
+// start test
+SpecialPowers.pushPrefEnv({ set: [
+ ["dom.security.https_first", false]
+ ]}, startTest);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_uir_windowwatcher.html b/dom/security/test/csp/test_uir_windowwatcher.html
new file mode 100644
index 0000000000..f16b3c93a6
--- /dev/null
+++ b/dom/security/test/csp/test_uir_windowwatcher.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1529893 - Test upgrade-insecure-requests for opening window through nsWindowWatcher</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<iframe name='frameA' width="100%" src="http://example.com/tests/dom/security/test/csp/file_windowwatcher_frameA.html"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+// The CSP of subframe C should cause the window to be opened to be upgraded from http to https.
+
+SimpleTest.waitForExplicitFinish();
+
+let finalURI = "https://example.com/tests/dom/security/test/csp/file_windowwatcher_win_open.html";
+
+window.addEventListener("message", receiveMessage);
+
+function receiveMessage(event) {
+ is(event.data.result, finalURI, "opened window correctly upgraded to https");
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_upgrade_insecure.html b/dom/security/test/csp/test_upgrade_insecure.html
new file mode 100644
index 0000000000..0827b7f2df
--- /dev/null
+++ b/dom/security/test/csp/test_upgrade_insecure.html
@@ -0,0 +1,192 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1139297 - Implement CSP upgrade-insecure-requests directive</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the test:
+ * We load resources (img, script, sytle, etc) over *http* and make sure
+ * that all the resources get upgraded to use >> https << when the
+ * csp-directive "upgrade-insecure-requests" is specified. We further
+ * test that subresources within nested contexts (iframes) get upgraded
+ * and also test the handling of server side redirects.
+ *
+ * In detail:
+ * We perform an XHR request to the *.sjs file which is processed async on
+ * the server and waits till all the requests were processed by the server.
+ * Once the server received all the different requests, the server responds
+ * to the initial XHR request with an array of results which must match
+ * the expected results from each test, making sure that all requests
+ * received by the server (*.sjs) were actually *https* requests.
+ */
+
+const { AppConstants } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+const UPGRADE_POLICY =
+ "upgrade-insecure-requests;" + // upgrade all http requests to https
+ "block-all-mixed-content;" + // upgrade should be enforced before block-all.
+ "default-src https: wss: 'unsafe-inline';" + // only allow https: and wss:
+ "form-action https:;"; // explicit, no fallback to default-src
+
+const UPGRADE_POLICY_NO_DEFAULT_SRC =
+ "upgrade-insecure-requests;" + // upgrade all http requests to https
+ "script-src 'unsafe-inline' *"; // we have to allowlist the inline scripts
+ // in the test.
+const NO_UPGRADE_POLICY =
+ "default-src http: ws: 'unsafe-inline';" + // allow http:// and ws://
+ "form-action http:;"; // explicit, no fallback to default-src
+
+var tests = [
+ { // (1) test that all requests within an >> https << page get updated
+ policy: UPGRADE_POLICY,
+ topLevelScheme: "https://",
+ description: "upgrade all requests on toplevel https",
+ deliveryMethod: "header",
+ results: [
+ "iframe-ok", "script-ok", "img-ok", "img-redir-ok", "font-ok", "xhr-ok", "style-ok",
+ "media-ok", "object-ok", "form-ok", "nested-img-ok"
+ ]
+ },
+ { // (2) test that all requests within an >> http << page get updated
+ policy: UPGRADE_POLICY,
+ topLevelScheme: "http://",
+ description: "upgrade all requests on toplevel http",
+ deliveryMethod: "header",
+ results: [
+ "iframe-ok", "script-ok", "img-ok", "img-redir-ok", "font-ok", "xhr-ok", "style-ok",
+ "media-ok", "object-ok", "form-ok", "nested-img-ok"
+ ]
+ },
+ { // (3) test that all requests within an >> http << page get updated, but do
+ // not specify a default-src directive.
+ policy: UPGRADE_POLICY_NO_DEFAULT_SRC,
+ topLevelScheme: "http://",
+ description: "upgrade all requests on toplevel http where default-src is not specified",
+ deliveryMethod: "header",
+ results: [
+ "iframe-ok", "script-ok", "img-ok", "img-redir-ok", "font-ok", "xhr-ok", "style-ok",
+ "media-ok", "object-ok", "form-ok", "nested-img-ok"
+ ]
+ },
+ { // (4) test that no requests get updated if >> upgrade-insecure-requests << is not used
+ policy: NO_UPGRADE_POLICY,
+ topLevelScheme: "http://",
+ description: "do not upgrade any requests on toplevel http",
+ deliveryMethod: "header",
+ results: [
+ "iframe-error", "script-error", "img-error", "img-redir-error", "font-error",
+ "xhr-error", "style-error", "media-error", "object-error", "form-error",
+ "nested-img-error"
+ ]
+ },
+ { // (5) test that all requests within an >> https << page using meta CSP get updated
+ // policy: UPGRADE_POLICY, that test uses UPGRADE_POLICY within
+ // file_upgrade_insecure_meta.html
+ // no need to define it within that object.
+ topLevelScheme: "https://",
+ description: "upgrade all requests on toplevel https using meta csp",
+ deliveryMethod: "meta",
+ results: [
+ "iframe-ok", "script-ok", "img-ok", "img-redir-ok", "font-ok", "xhr-ok", "style-ok",
+ "media-ok", "object-ok", "form-ok", "nested-img-ok"
+ ]
+ },
+];
+
+// TODO: WebSocket tests are not supported on Android Yet. Bug 1566168.
+if (AppConstants.platform !== "android") {
+ for (let test of tests) {
+ test.results.push(test.results[0] == "iframe-ok" ? "websocket-ok" : "websocket-error");
+ }
+}
+
+var counter = 0;
+var curTest;
+
+function loadTestPage() {
+ curTest = tests[counter++];
+ var src = curTest.topLevelScheme + "example.com/tests/dom/security/test/csp/file_testserver.sjs?file=";
+ if (curTest.deliveryMethod === "header") {
+ // append the file that should be served
+ src += escape("tests/dom/security/test/csp/file_upgrade_insecure.html");
+ // append the CSP that should be used to serve the file
+ src += "&csp=" + escape(curTest.policy);
+ }
+ else {
+ src += escape("tests/dom/security/test/csp/file_upgrade_insecure_meta.html");
+ // no csp here, since it's in the meta element
+ }
+ document.getElementById("testframe").src = src;
+}
+
+function finishTest() {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+function checkResults(result) {
+ // try to find the expected result within the results array
+ var index = curTest.results.indexOf(result);
+ isnot(index, -1, curTest.description + " (result: " + result + ")");
+
+ // take the element out the array and continue till the results array is empty
+ if (index != -1) {
+ curTest.results.splice(index, 1);
+ }
+ // lets check if we are expecting more results to bubble up
+ if (curTest.results.length) {
+ return;
+ }
+ // lets see if we ran all the tests
+ if (counter == tests.length) {
+ finishTest();
+ return;
+ }
+ // otherwise it's time to run the next test
+ runNextTest();
+}
+
+// a postMessage handler that is used by sandboxed iframes without
+// 'allow-same-origin' to bubble up results back to this main page.
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ checkResults(event.data.result);
+}
+
+function runNextTest() {
+ // sends an xhr request to the server which is processed async, which only
+ // returns after the server has received all the expected requests.
+ var myXHR = new XMLHttpRequest();
+ myXHR.open("GET", "file_upgrade_insecure_server.sjs?queryresult");
+ myXHR.onload = function(e) {
+ var results = myXHR.responseText.split(",");
+ for (var index in results) {
+ checkResults(results[index]);
+ }
+ }
+ myXHR.onerror = function(e) {
+ ok(false, "could not query results from server (" + e.message + ")");
+ finishTest();
+ }
+ myXHR.send();
+
+ // give it some time and run the testpage
+ SimpleTest.executeSoon(loadTestPage);
+}
+
+SimpleTest.waitForExplicitFinish();
+runNextTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_upgrade_insecure_cors.html b/dom/security/test/csp/test_upgrade_insecure_cors.html
new file mode 100644
index 0000000000..3ed53d8108
--- /dev/null
+++ b/dom/security/test/csp/test_upgrade_insecure_cors.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1139297 - Implement CSP upgrade-insecure-requests directive</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the test:
+ * We load a page serving two XHR requests (including being redirected);
+ * one that should not require CORS and one that should require cors, in particular:
+ *
+ * Test 1:
+ * Main page: https://test1.example.com
+ * XHR request: http://test1.example.com
+ * Redirect to: http://test1.example.com
+ * Description: Upgrade insecure should upgrade from http to https and also
+ * surpress CORS for that case.
+ *
+ * Test 2:
+ * Main page: https://test1.example.com
+ * XHR request: http://test1.example.com
+ * Redirect to: http://test1.example.com:443
+ * Description: Upgrade insecure should upgrade from http to https and also
+ * prevent CORS for that case.
+ * Note: If redirecting to a different port, then CORS *should* be enforced (unless
+ * it's port 443). Unfortunately we can't test that because of the setup of our
+ * *.sjs files; they only are able to listen to port 443, see:
+ * http://mxr.mozilla.org/mozilla-central/source/build/pgo/server-locations.txt#98
+ *
+ * Test 3:
+ * Main page: https://test1.example.com
+ * XHR request: http://test2.example.com
+ * Redirect to: http://test1.example.com
+ * Description: Upgrade insecure should *not* prevent CORS since
+ * the page performs a cross origin xhr.
+ *
+ */
+
+const CSP_POLICY = "upgrade-insecure-requests; script-src 'unsafe-inline'";
+var tests = 3;
+
+function loadTest() {
+ var src = "https://test1.example.com/tests/dom/security/test/csp/file_testserver.sjs?file=";
+ // append the file that should be served
+ src += escape("tests/dom/security/test/csp/file_upgrade_insecure_cors.html")
+ // append the CSP that should be used to serve the file
+ src += "&csp=" + escape(CSP_POLICY);
+ document.getElementById("testframe").src = src;
+}
+
+function checkResult(result) {
+ if (result === "test1-no-cors-ok" ||
+ result === "test2-no-cors-diffport-ok" ||
+ result === "test3-cors-ok") {
+ ok(true, "'upgrade-insecure-requests' acknowledges CORS (" + result + ")");
+ }
+ else {
+ ok(false, "'upgrade-insecure-requests' acknowledges CORS (" + result + ")");
+ }
+ if (--tests > 0) {
+ return;
+ }
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+// a postMessage handler that is used to bubble up results from
+// within the iframe.
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ checkResult(event.data);
+}
+
+SimpleTest.waitForExplicitFinish();
+loadTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_upgrade_insecure_docwrite_iframe.html b/dom/security/test/csp/test_upgrade_insecure_docwrite_iframe.html
new file mode 100644
index 0000000000..dc6039ec35
--- /dev/null
+++ b/dom/security/test/csp/test_upgrade_insecure_docwrite_iframe.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1273430 - Test CSP upgrade-insecure-requests for doc.write(iframe)</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the test:
+ * Load an iframe which ships with a CSP of upgrade-insecure-requests.
+ * Within that iframe a script performs doc.write(iframe) using an
+ * *http* URL. Make sure, the URL is upgraded to *https*.
+ *
+ * +-----------------------------------------+
+ * | |
+ * | http(s); csp: upgrade-insecure-requests | |
+ * | +---------------------------------+ |
+ * | | | |
+ * | | doc.write(<iframe src='http'>); | <--------- upgrade to https
+ * | | | |
+ * | +---------------------------------+ |
+ * | |
+ * +-----------------------------------------+
+ *
+ */
+
+const TEST_FRAME_URL =
+ "https://example.com/tests/dom/security/test/csp/file_upgrade_insecure_docwrite_iframe.sjs?testframe";
+
+// important: the RESULT should have a scheme of *https*
+const RESULT =
+ "https://example.com/tests/dom/security/test/csp/file_upgrade_insecure_docwrite_iframe.sjs?docwriteframe";
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ is(event.data.result, RESULT, "doc.write(iframe) of http should be upgraded to https!");
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+// start the test
+SimpleTest.waitForExplicitFinish();
+var testframe = document.getElementById("testframe");
+testframe.src = TEST_FRAME_URL;
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_upgrade_insecure_loopback.html b/dom/security/test/csp/test_upgrade_insecure_loopback.html
new file mode 100644
index 0000000000..f72f95215e
--- /dev/null
+++ b/dom/security/test/csp/test_upgrade_insecure_loopback.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1447784 - Implement CSP upgrade-insecure-requests directive</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the test:
+ * We load a page that performs a CORS XHR to 127.0.0.1 which shouldn't be upgraded to https:
+ *
+ * Test 1:
+ * Main page: https://127.0.0.1:8080
+ * XHR request: http://127.0.0.1:8080
+ * No redirect to https://
+ * Description: Upgrade insecure should *NOT* upgrade from http to https.
+ */
+
+const CSP_POLICY = "upgrade-insecure-requests; script-src 'unsafe-inline'";
+let testFiles = ["tests/dom/security/test/csp/file_upgrade_insecure_loopback.html",
+ "tests/dom/security/test/csp/file_upgrade_insecure_loopback_form.html"];
+
+function examiner() {
+ SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
+}
+examiner.prototype = {
+ observe(subject, topic, data) {
+ if (topic === "specialpowers-http-notify-request") {
+ // we skip looking at other requests that might be observed accidentally
+ // e.g., we saw kinto requests when running this test locally
+ if (data.includes("bug-1661423-dont-upgrade-localhost")) {
+ let urlObj = new URL(data);
+ is(urlObj.protocol, "http:", "Didn't upgrade localhost URL");
+ loadTest();
+ }
+ }
+ },
+ remove() {
+ SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
+ }
+};
+
+window.examiner = new examiner();
+
+
+function loadTest() {
+ if (!testFiles.length) {
+ removeAndFinish();
+ return;
+ }
+ var src = "https://example.com/tests/dom/security/test/csp/file_testserver.sjs?file=";
+ // append the file that should be served
+ src += escape(testFiles.shift())
+ // append the CSP that should be used to serve the file
+ src += "&csp=" + escape(CSP_POLICY);
+ document.getElementById("testframe").src = src;
+}
+
+function removeAndFinish() {
+ window.removeEventListener("message", receiveMessage);
+ window.examiner.remove();
+ SimpleTest.finish();
+}
+
+// a postMessage handler that is used to bubble up results from
+// within the iframe.
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ if (event.data === "request-not-https") {
+ ok(true, "Didn't upgrade 127.0.0.1:8080 to https://");
+ loadTest();
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// By default, proxies don't apply to 127.0.0.1.
+// We need them to for this test (at least on android), though:
+SpecialPowers.pushPrefEnv({set: [
+ ["network.proxy.allow_hijacking_localhost", true]
+]}).then(loadTest);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_upgrade_insecure_navigation.html b/dom/security/test/csp/test_upgrade_insecure_navigation.html
new file mode 100644
index 0000000000..5694deb15a
--- /dev/null
+++ b/dom/security/test/csp/test_upgrade_insecure_navigation.html
@@ -0,0 +1,105 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1271173 - Missing spec on Upgrade Insecure Requests(Navigational Upgrades) </title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+<iframe style="width:100%;" id="sandboxedtestframe"
+ sandbox="allow-scripts allow-top-navigation allow-same-origin allow-pointer-lock allow-popups"></iframe>
+
+<script class="testbody" type="text/javascript">
+/*
+ * Description of the test:
+ * We load a page into an iframe that performs a navigational request.
+ * We make sure that upgrade-insecure-requests applies and the page
+ * gets upgraded to https if same origin.
+ * Please note that uir only applies to sandboxed iframes if
+ * the value 'allow-same-origin' is specified.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+var tests = [
+ {
+ csp: "upgrade-insecure-requests;",
+ result: "https",
+ origin: "http://example.com",
+ desc: "upgrade-insecure-requests same origin should upgrade"
+ },
+ {
+ csp: "",
+ result: "http",
+ origin: "http://example.com",
+ desc: "No upgrade-insecure-requests same origin should not upgrade"
+ },
+ {
+ csp: "upgrade-insecure-requests;",
+ result: "http",
+ origin: "http://mochi.test:8888",
+ desc: "upgrade-insecure-requests cross origin should not upgrade"
+ },
+ {
+ csp: "",
+ result: "http",
+ origin: "http://mochi.test:8888",
+ desc: "No upgrade-insecure-requests cross origin should not upgrade"
+ },
+];
+
+// initializing to -1 so we start at index 0 when we start the test
+var counter = -1;
+
+function finishTest() {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+var subtests = 0;
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ var result = event.data.result;
+ // query the scheme from the URL before comparing the result
+ var scheme = result.substring(0, result.indexOf(":"));
+ is(scheme, tests[counter].result, tests[counter].desc);
+
+ // @hardcoded 4:
+ // each test run contains of two subtests (frame and top-level)
+ // and we load each test into a regular iframe and into a
+ // sandboxed iframe. only move on to the next test once all
+ // four results from the subtests have bubbled up.
+ subtests++;
+ if (subtests != 4) {
+ return;
+ }
+ subtests = 0;
+ loadNextTest();
+}
+
+function loadNextTest() {
+ counter++;
+ if (counter == tests.length) {
+ finishTest();
+ return;
+ }
+
+ var src = tests[counter].origin;
+ src += "/tests/dom/security/test/csp/file_upgrade_insecure_navigation.sjs";
+ src += "?csp=" + escape(tests[counter].csp);
+ src += "&action=perform_navigation";
+ document.getElementById("testframe").src = src;
+ document.getElementById("sandboxedtestframe").src = src;
+}
+// Don't upgrade to https to test that upgrade-insecure-requests acts correctly
+// start running the tests
+SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_first", false]]
+}, loadNextTest);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_upgrade_insecure_navigation_redirect.html b/dom/security/test/csp/test_upgrade_insecure_navigation_redirect.html
new file mode 100644
index 0000000000..17655e6316
--- /dev/null
+++ b/dom/security/test/csp/test_upgrade_insecure_navigation_redirect.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1422284 - Upgrade insecure requests should only apply to top-level same-origin redirects </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe id="redirect_same_origin_frame"></iframe>
+<iframe id="redirect_cross_origin_frame"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+let testCounter = 0;
+
+function checkFinished() {
+ // hardcoded 2 because we have a same-origin and a cross-origin test
+ if (++testCounter == 2) {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+ }
+}
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ let docURI = event.data.docURI;
+ let url = docURI.split('?')[0];
+ let query = docURI.split('?')[1];
+
+ if (query === "finaldoc_same_origin_redirect") {
+ // scheme schould be https
+ is (
+ url,
+ "https://example.com/tests/dom/security/test/csp/file_upgrade_insecure_navigation_redirect.sjs",
+ "upgrade-insecure-requests same origin redirect should upgrade",
+ );
+ }
+ else if (query === "finaldoc_cross_origin_redirect") {
+ // scheme schould be http
+ is (
+ url,
+ "http://test1.example.com/tests/dom/security/test/csp/file_upgrade_insecure_navigation_redirect.sjs",
+ "upgrade-insecure-requests cross origin redirect should not upgrade",
+ );
+ }
+ else {
+ ok(false, "sanity: how can we ever get here?");
+ }
+ checkFinished();
+}
+
+function startTest() {
+ document.getElementById("redirect_same_origin_frame").src =
+ "http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_navigation_redirect_same_origin.html";
+ document.getElementById("redirect_cross_origin_frame").src =
+ "http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_navigation_redirect_cross_origin.html";
+}
+
+// do not upgrade tests by https-first, only by UIR for this test
+SpecialPowers.pushPrefEnv({ set: [["dom.security.https_first", false]]}, startTest);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_upgrade_insecure_report_only.html b/dom/security/test/csp/test_upgrade_insecure_report_only.html
new file mode 100644
index 0000000000..230d6a3e60
--- /dev/null
+++ b/dom/security/test/csp/test_upgrade_insecure_report_only.html
@@ -0,0 +1,98 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1832249 - Consider report-only flag when upgrading insecure requests</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe id="reportonlyframe"></iframe>
+<iframe id="enforceframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the test:
+ * When we load an http page with the `content-security-policy-report-only: upgrade-insecure-requests`
+ * header the `upgrade-insecure-requests` directive must be ignored according to the spec.
+ * https://w3c.github.io/webappsec-upgrade-insecure-requests/#delivery
+ */
+
+var expectedResults = 4;
+
+function finishTest() {
+ // need to wait until all of the tests have resolved before exiting
+ if (--expectedResults > 0) {
+ return;
+ }
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ // make sure the image was correctly loaded. this is the primary purpose of
+ // this test. if image isn't loaded correctly then that means we attempted to
+ // upgrade the request when we shouldn't have and vice-versa.
+ let result = event.data.result;
+ if (result === "reportonly-img-ok") {
+ ok(true, "successfully loaded insecure image from http without upgrade");
+ finishTest();
+ }
+ if (result === "enforce-img-ok") {
+ ok(true, "successfully loaded insecure image from http with upgrade");
+ finishTest();
+ }
+ if (result === "reportonly-img-error") {
+ ok (false, "failed to load reportonly image correctly");
+ finishTest();
+ }
+ if (result === "enforce-img-error") {
+ ok (false, "failed to load enforce image correctly");
+ finishTest();
+ }
+}
+
+function runTest(route) {
+ // Send off an XHR request which will return once the server receives the
+ // violation report from the report only policy.
+ var myXHR = new XMLHttpRequest();
+ myXHR.open("GET", `file_upgrade_insecure_report_only_server.sjs?queryresult-${route}`);
+ myXHR.onload = function(e) {
+ // make sure that the csp violation report we get is the one we expected
+ let report = JSON.parse(myXHR.responseText)["csp-report"];
+ ok(
+ report["original-policy"].includes("upgrade-insecure-requests"),
+ "report should be given by malformed report-only policy"
+ );
+ ok(
+ report["blocked-uri"].startsWith("http:") && report["blocked-uri"].endsWith(`.sjs?img-${route}`),
+ "request should be for an img load"
+ );
+
+ finishTest();
+ }
+ myXHR.onerror = function(e) {
+ ok(false, "could not query result for csp-report from server (" + e.message + ")");
+ SimpleTest.finish();
+ }
+ myXHR.send();
+
+ // We load a page that is served using a report only CSP which loads an image.
+ SimpleTest.executeSoon(function() {
+ // we need to test http functionality here, so we need to load an http url
+ /* eslint-disable @microsoft/sdl/no-insecure-url */
+ document.getElementById(`${route}frame`).src =
+ `http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_report_only_server.sjs?${route}=true`;
+ /* eslint-enable @microsoft/sdl/no-insecure-url */
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+runTest("reportonly");
+runTest("enforce");
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_upgrade_insecure_reporting.html b/dom/security/test/csp/test_upgrade_insecure_reporting.html
new file mode 100644
index 0000000000..4966b8627e
--- /dev/null
+++ b/dom/security/test/csp/test_upgrade_insecure_reporting.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1139297 - Implement CSP upgrade-insecure-requests directive</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the test:
+ * We load an https page which includes an http image. We make sure that
+ * the image request gets upgraded to https but also make sure that a report
+ * is sent when a CSP report only is used which only allows https requests.
+ */
+
+var expectedResults = 2;
+
+function finishTest() {
+ // let's wait till the image was loaded and the report was received
+ if (--expectedResults > 0) {
+ return;
+ }
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+function runTest() {
+ // (1) Lets send off an XHR request which will return once the server receives
+ // the violation report from the report only policy.
+ var myXHR = new XMLHttpRequest();
+ myXHR.open("GET", "file_upgrade_insecure_reporting_server.sjs?queryresult");
+ myXHR.onload = function(e) {
+ is(myXHR.responseText, "report-ok", "csp-report was sent correctly");
+ finishTest();
+ }
+ myXHR.onerror = function(e) {
+ ok(false, "could not query result for csp-report from server (" + e.message + ")");
+ finishTest();
+ }
+ myXHR.send();
+
+ // (2) We load a page that is served using a CSP and a CSP report only which loads
+ // an image over http.
+ SimpleTest.executeSoon(function() {
+ document.getElementById("testframe").src =
+ "https://example.com/tests/dom/security/test/csp/file_upgrade_insecure_reporting_server.sjs?toplevel";
+ });
+}
+
+// a postMessage handler that is used by sandboxed iframes without
+// 'allow-same-origin' to bubble up results back to this main page.
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ // (3) make sure the image was correctly loaded
+ is(event.data.result, "img-ok", "upgraded insecure image load from http -> https");
+ finishTest();
+}
+
+SimpleTest.waitForExplicitFinish();
+runTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_websocket_localhost.html b/dom/security/test/csp/test_websocket_localhost.html
new file mode 100644
index 0000000000..6bcc93fceb
--- /dev/null
+++ b/dom/security/test/csp/test_websocket_localhost.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1729897: Allow unsecure websocket from localhost page with CSP: upgrade-insecure </title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="test_ws_self_frame"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+function finishTest() {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ is(event.data.result, "self-ws-loaded", "websocket loaded");
+ ok(event.data.url.startsWith("ws://"), `scheme must be ws:// but got ${event.data.url}`);
+ finishTest();
+}
+
+SpecialPowers.pushPrefEnv({set: [
+ ["network.proxy.allow_hijacking_localhost", true],
+ ["network.proxy.testing_localhost_is_secure_when_hijacked", true],
+]}).then(function() {
+ const HOST = "http://localhost/tests/dom/security/test/csp/";
+ var test_ws_self_frame = document.getElementById("test_ws_self_frame");
+ test_ws_self_frame.src = HOST + "file_websocket_csp_upgrade.html";
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_websocket_self.html b/dom/security/test/csp/test_websocket_self.html
new file mode 100644
index 0000000000..3eae83bfbf
--- /dev/null
+++ b/dom/security/test/csp/test_websocket_self.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1345615: Allow websocket schemes when using 'self' in CSP</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="test_ws_self_frame"></iframe>
+<iframe style="width:100%;" id="test_ws_explicit_frame"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the test:
+ * We load an iframe using connect-src 'self' and one
+ * iframe using connect-src ws: and make
+ * sure that in both cases ws: as well as wss: is allowed to load.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+function finishTest() {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+const TOTAL_TESTS = 4;
+var counter = 0;
+
+function checkResults(result) {
+ counter++;
+ if (result === "self-ws-loaded" || result === "self-wss-loaded" ||
+ result === "explicit-ws-loaded" || result === "explicit-wss-loaded") {
+ ok(true, "Evaluating: " + result);
+ }
+ else {
+ ok(false, "Evaluating: " + result);
+ }
+ if (counter < TOTAL_TESTS) {
+ return;
+ }
+ finishTest();
+}
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ checkResults(event.data.result);
+}
+
+const HOST = "http://example.com/tests/dom/security/test/csp/";
+var test_ws_self_frame = document.getElementById("test_ws_self_frame");
+test_ws_self_frame.src = HOST + "file_websocket_self.html";
+
+var test_ws_explicit_frame = document.getElementById("test_ws_explicit_frame");
+test_ws_explicit_frame.src = HOST + "file_websocket_explicit.html";
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_win_open_blocked.html b/dom/security/test/csp/test_win_open_blocked.html
new file mode 100644
index 0000000000..1335c9d272
--- /dev/null
+++ b/dom/security/test/csp/test_win_open_blocked.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <!-- we have to allowlist the actual script that spawns the tests,
+ hence the nonce.-->
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none';
+ script-src 'nonce-foo'; style-src 'nonce-foo'">
+ <script nonce="foo" src="/tests/SimpleTest/SimpleTest.js">
+ </script>
+ <link nonce="foo" rel="stylesheet" type="text/css"
+ href="/tests/SimpleTest/test.css"/>
+ <!-- this script block with window.open and document.open will not
+ be executed, since default-src is none -->
+ <script>
+ let win = window.open('file_default_src_none_csp.html');
+ document.open();
+ document.write("<script type='application/javascript'>" +
+ " window.opener.postMessage('document-opened', '*');" +
+ "<\/script>");
+ document.close();
+ </script>
+ <script nonce="foo">
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.requestFlakyTimeout("have to test that opening a " +
+ "new window/document has not succeeded");
+ window.addEventListener("message", receiveMessage);
+ let checkWindowStatus = false;
+ let checkDocumentStatus = false;
+
+ function receiveMessage(event) {
+ window.removeEventListener("message", receiveMessage);
+ if (event.data == "window-opened") {
+ checkWindowStatus = true;
+ win.close();
+ }
+ if (event.data == "document-opened") {
+ checkDocumentStatus = true;
+ doc.close();
+ }
+ }
+ setTimeout(function () {
+ is(checkWindowStatus, false,
+ "window shouldn't be opened");
+ is(checkDocumentStatus, false,
+ "document shouldn't be opened");
+ SimpleTest.finish();
+ }, 1500);
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_worker_src.html b/dom/security/test/csp/test_worker_src.html
new file mode 100644
index 0000000000..5aa8f7bc56
--- /dev/null
+++ b/dom/security/test/csp/test_worker_src.html
@@ -0,0 +1,105 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1302667 - Test worker-src</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(3);
+
+/* Description of the test:
+ * We load a page inlcuding a worker, a shared worker as well as a
+ * service worker with a CSP of:
+ * >> worker-src https://example.com; child-src 'none'; script-src 'nonce-foo'
+ * and make sure that worker-src governs these three kinds of workers correctly.
+ * In addition, we make sure that child-src as well as script-src is discarded
+ * in case worker-src is specified. Ideally we would use "script-src 'none'" but
+ * we have to allowlist the actual script that spawns the workers, hence the nonce.
+ */
+
+let ALLOWED_HOST = "https://example.com/tests/dom/security/test/csp/";
+let BLOCKED_HOST = "https://test1.example.com/tests/dom/security/test/csp/";
+
+let TESTS = [
+ // allowed
+ ALLOWED_HOST + "file_worker_src_worker_governs.html",
+ ALLOWED_HOST + "file_worker_src_child_governs.html",
+ ALLOWED_HOST + "file_worker_src_script_governs.html",
+ // blocked
+ BLOCKED_HOST + "file_worker_src_worker_governs.html",
+ BLOCKED_HOST + "file_worker_src_child_governs.html",
+ BLOCKED_HOST + "file_worker_src_script_governs.html",
+];
+
+let numberSubTests = 3; // 1 web worker, 1 shared worker, 1 service worker
+let subTestCounter = 0; // keeps track of how many
+let testIndex = 0;
+
+function checkFinish() {
+ subTestCounter = 0;
+ testIndex++;
+ if (testIndex < TESTS.length) {
+ runNextTest();
+ return;
+ }
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ let href = event.data.href;
+ let result = event.data.result;
+
+ if (href.startsWith("https://example.com")) {
+ if (result == "worker-allowed" ||
+ result == "shared-worker-allowed" ||
+ result == "service-worker-allowed") {
+ ok(true, "allowing worker from https://example.com (" + result + ")");
+ }
+ else {
+ ok(false, "blocking worker from https://example.com (" + result + ")");
+ }
+ }
+ else if (href.startsWith("https://test1.example.com")) {
+ if (result == "worker-blocked" ||
+ result == "shared-worker-blocked" ||
+ result == "service-worker-blocked") {
+ ok(true, "blocking worker from https://test1.example.com (" + result + ")");
+ }
+ else {
+ ok(false, "allowing worker from https://test1.example.com (" + result + ")");
+ }
+ }
+ else {
+ // sanity check, we should never enter that branch, bust just in case...
+ ok(false, "unexpected result: " + result);
+ }
+ subTestCounter++;
+ if (subTestCounter < numberSubTests) {
+ return;
+ }
+ checkFinish();
+}
+
+function runNextTest() {
+ document.getElementById("testframe").src = TESTS[testIndex];
+}
+
+SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+]}, function() {
+ runNextTest();
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_xslt_inherits_csp.html b/dom/security/test/csp/test_xslt_inherits_csp.html
new file mode 100644
index 0000000000..90e8372db1
--- /dev/null
+++ b/dom/security/test/csp/test_xslt_inherits_csp.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1597645: Make sure XSLT inherits the CSP r=ckerschb</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<body>
+ <iframe src="file_xslt_inherits_csp.xml"></iframe>
+
+<script class="testbody">
+ SimpleTest.requestCompleteLog();
+ SimpleTest.waitForExplicitFinish();
+
+ let frame = document.querySelector("iframe");
+
+ window.addEventListener("load",()=>{
+ let link = frame.contentWindow.document.querySelector("a");
+ link.click(); //
+
+ requestAnimationFrame(()=>{
+ // Wait one Frame to let the browser catch up
+ // before checking the dom.
+ let res = !frame.contentWindow.document.body.innerText.includes("JS DID EXCECUTE");
+ ok(res, "The CSP did block injected JS ");
+ SimpleTest.finish();
+ });
+ })
+</script>
+</html>
diff --git a/dom/security/test/csp/worker.sjs b/dom/security/test/csp/worker.sjs
new file mode 100644
index 0000000000..9176b62cb5
--- /dev/null
+++ b/dom/security/test/csp/worker.sjs
@@ -0,0 +1,111 @@
+const SJS = "http://mochi.test:8888/tests/dom/security/test/csp/worker.sjs";
+
+function createFetchWorker(url) {
+ return `fetch("${url}");`;
+}
+
+function createXHRWorker(url) {
+ return `
+ try {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", "${url}");
+ xhr.send();
+ } catch(ex) {}
+ `;
+}
+
+function createImportScriptsWorker(url) {
+ return `
+ try {
+ importScripts("${url}");
+ } catch(ex) {}
+ `;
+}
+
+function createChildWorkerURL(params) {
+ let url = SJS + "?" + params.toString();
+ return `new Worker("${url}");`;
+}
+
+function createChildWorkerBlob(params) {
+ let url = SJS + "?" + params.toString();
+ return `
+ try {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", "${url}");
+ xhr.responseType = "blob";
+ xhr.send();
+ xhr.onload = () => {
+ new Worker(URL.createObjectURL(xhr.response));};
+ } catch(ex) {}
+ `;
+}
+
+function handleRequest(request, response) {
+ let params = new URLSearchParams(request.queryString);
+
+ let id = params.get("id");
+ let base = unescape(params.get("base"));
+ let child = params.has("child") ? params.get("child") : "";
+
+ //avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "application/javascript");
+
+ // Deliver the CSP policy encoded in the URL
+ if (params.has("csp")) {
+ response.setHeader(
+ "Content-Security-Policy",
+ unescape(params.get("csp")),
+ false
+ );
+ }
+
+ if (child) {
+ let childCsp = params.has("childCsp") ? params.get("childCsp") : "";
+ params.delete("csp");
+ params.delete("child");
+ params.delete("childCsp");
+ params.append("csp", childCsp);
+
+ switch (child) {
+ case "blob":
+ response.write(createChildWorkerBlob(params));
+ break;
+
+ case "url":
+ response.write(createChildWorkerURL(params));
+ break;
+
+ default:
+ response.setStatusLine(request.httpVersion, 400, "Bad request");
+ break;
+ }
+
+ return;
+ }
+
+ if (params.has("action")) {
+ switch (params.get("action")) {
+ case "fetch":
+ response.write(createFetchWorker(base + "?id=" + id));
+ break;
+
+ case "xhr":
+ response.write(createXHRWorker(base + "?id=" + id));
+ break;
+
+ case "importScripts":
+ response.write(createImportScriptsWorker(base + "?id=" + id));
+ break;
+
+ default:
+ response.setStatusLine(request.httpVersion, 400, "Bad request");
+ break;
+ }
+
+ return;
+ }
+
+ response.write("I don't know action ");
+}
diff --git a/dom/security/test/csp/worker_helper.js b/dom/security/test/csp/worker_helper.js
new file mode 100644
index 0000000000..3cadec9ea1
--- /dev/null
+++ b/dom/security/test/csp/worker_helper.js
@@ -0,0 +1,91 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var _tests = [];
+function addTest(test) {
+ _tests.push(test);
+}
+
+function addAsyncTest(fn) {
+ _tests.push(() => fn().catch(ok.bind(null, false)));
+}
+
+function runNextTest() {
+ if (!_tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+ const fn = _tests.shift();
+ try {
+ fn();
+ } catch (ex) {
+ info(
+ "Test function " +
+ (fn.name ? "'" + fn.name + "' " : "") +
+ "threw an exception: " +
+ ex
+ );
+ }
+}
+
+/**
+ * Helper to perform an XHR then blob response to create worker
+ */
+function doXHRGetBlob(uri) {
+ return new Promise(resolve => {
+ const xhr = new XMLHttpRequest();
+ xhr.open("GET", uri);
+ xhr.responseType = "blob";
+ xhr.addEventListener("load", function () {
+ is(
+ xhr.status,
+ 200,
+ "doXHRGetBlob load uri='" + uri + "' status=" + xhr.status
+ );
+ resolve(xhr.response);
+ });
+ xhr.send();
+ });
+}
+
+function removeObserver(observer) {
+ SpecialPowers.removeObserver(observer, "specialpowers-http-notify-request");
+ SpecialPowers.removeObserver(observer, "csp-on-violate-policy");
+}
+
+/**
+ * Helper to perform an assert to check if the request should be blocked or
+ * allowed by CSP
+ */
+function assertCSPBlock(url, shouldBlock) {
+ return new Promise((resolve, reject) => {
+ let observer = {
+ observe(subject, topic, data) {
+ if (topic === "specialpowers-http-notify-request") {
+ if (data == url) {
+ is(shouldBlock, false, "Should allow request uri='" + url);
+ removeObserver(observer);
+ resolve();
+ }
+ }
+
+ if (topic === "csp-on-violate-policy") {
+ let asciiSpec = SpecialPowers.getPrivilegedProps(
+ SpecialPowers.do_QueryInterface(subject, "nsIURI"),
+ "asciiSpec"
+ );
+ if (asciiSpec == url) {
+ is(shouldBlock, true, "Should block request uri='" + url);
+ removeObserver(observer);
+ resolve();
+ }
+ }
+ },
+ };
+
+ SpecialPowers.addObserver(observer, "csp-on-violate-policy");
+ SpecialPowers.addObserver(observer, "specialpowers-http-notify-request");
+ });
+}
diff --git a/dom/security/test/general/browser.toml b/dom/security/test/general/browser.toml
new file mode 100644
index 0000000000..0f4ec5b224
--- /dev/null
+++ b/dom/security/test/general/browser.toml
@@ -0,0 +1,81 @@
+[DEFAULT]
+
+["browser_file_nonscript.js"]
+support-files = [
+ "file_loads_nonscript.html",
+ "file_nonscript",
+ "file_nonscript.xyz",
+ "file_nonscript.html",
+ "file_nonscript.txt",
+ "file_nonscript.json",
+ "file_script.js",
+]
+
+["browser_restrict_privileged_about_script.js"]
+# This test intentionally asserts when in debug builds. Let's rely on opt builds when in CI.
+skip-if = ["debug"]
+support-files = [
+ "file_about_child.html",
+ "file_1767581.js",
+]
+
+["browser_same_site_cookies_bug1748693.js"]
+support-files = ["file_same_site_cookies_bug1748693.sjs"]
+
+["browser_test_assert_systemprincipal_documents.js"]
+skip-if = ["!nightly_build"]
+support-files = [
+ "file_assert_systemprincipal_documents.html",
+ "file_assert_systemprincipal_documents_iframe.html",
+]
+
+["browser_test_data_download.js"]
+support-files = ["file_data_download.html"]
+
+["browser_test_data_text_csv.js"]
+support-files = ["file_data_text_csv.html"]
+
+["browser_test_framing_error_pages.js"]
+support-files = [
+ "file_framing_error_pages_csp.html",
+ "file_framing_error_pages_xfo.html",
+ "file_framing_error_pages.sjs",
+]
+
+["browser_test_gpc_privateBrowsingMode.js"]
+support-files = [
+ "file_empty.html",
+ "file_gpc_server.sjs",
+]
+
+["browser_test_referrer_loadInOtherProcess.js"]
+
+["browser_test_report_blocking.js"]
+support-files = [
+ "file_framing_error_pages_xfo.html",
+ "file_framing_error_pages_csp.html",
+ "file_framing_error_pages.sjs",
+]
+
+["browser_test_toplevel_data_navigations.js"]
+skip-if = [
+ "verify && debug && os == 'mac'",
+ "debug && (os == 'mac' || os == 'linux')", # Bug 1403815
+]
+support-files = [
+ "file_toplevel_data_navigations.sjs",
+ "file_toplevel_data_meta_redirect.html",
+]
+
+["browser_test_view_image_data_navigation.js"]
+support-files = [
+ "file_view_image_data_navigation.html",
+ "file_view_bg_image_data_navigation.html",
+]
+
+["browser_test_xfo_embed_object.js"]
+support-files = [
+ "file_framing_xfo_embed.html",
+ "file_framing_xfo_object.html",
+ "file_framing_xfo_embed_object.sjs",
+]
diff --git a/dom/security/test/general/browser_file_nonscript.js b/dom/security/test/general/browser_file_nonscript.js
new file mode 100644
index 0000000000..95243c32a7
--- /dev/null
+++ b/dom/security/test/general/browser_file_nonscript.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_fileurl_nonscript_load() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.block_fileuri_script_with_wrong_mime", true]],
+ });
+
+ let file = getChromeDir(getResolvedURI(gTestPath));
+ file.append("file_loads_nonscript.html");
+ let uriString = Services.io.newFileURI(file).spec;
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, uriString);
+ registerCleanupFunction(async function () {
+ BrowserTestUtils.removeTab(tab);
+ });
+
+ let counter = await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ Cu.exportFunction(Assert.equal.bind(Assert), content.window, {
+ defineAs: "equal",
+ });
+ content.window.postMessage("run", "*");
+
+ await new Promise(resolve => {
+ content.window.addEventListener("message", event => {
+ if (event.data === "done") {
+ resolve();
+ }
+ });
+ });
+
+ return content.window.wrappedJSObject.counter;
+ });
+
+ is(counter, 1, "Only one script should have run");
+});
diff --git a/dom/security/test/general/browser_restrict_privileged_about_script.js b/dom/security/test/general/browser_restrict_privileged_about_script.js
new file mode 100644
index 0000000000..0baa6e3d4d
--- /dev/null
+++ b/dom/security/test/general/browser_restrict_privileged_about_script.js
@@ -0,0 +1,70 @@
+"use strict";
+
+const kChildPage = getRootDirectory(gTestPath) + "file_about_child.html";
+
+const kAboutPagesRegistered = BrowserTestUtils.registerAboutPage(
+ registerCleanupFunction,
+ "test-about-privileged-with-scripts",
+ kChildPage,
+ Ci.nsIAboutModule.ALLOW_SCRIPT |
+ Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD |
+ Ci.nsIAboutModule.URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS |
+ Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT |
+ Ci.nsIAboutModule.IS_SECURE_CHROME_UI
+);
+
+add_task(async function test_principal_click() {
+ await kAboutPagesRegistered;
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.skip_about_page_has_csp_assert", true]],
+ });
+ await BrowserTestUtils.withNewTab(
+ "about:test-about-privileged-with-scripts",
+ async function (browser) {
+ // Wait for page to fully load
+ info("Waiting for tab to be loaded..");
+ // let's look into the fully loaded about page
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ let channel = content.docShell.currentDocumentChannel;
+ is(
+ channel.originalURI.asciiSpec,
+ "about:test-about-privileged-with-scripts",
+ "sanity check - make sure we test the principal for the correct URI"
+ );
+
+ let triggeringPrincipal = channel.loadInfo.triggeringPrincipal;
+ ok(
+ triggeringPrincipal.isSystemPrincipal,
+ "loading about: from privileged page must have a triggering of System"
+ );
+
+ let contentPolicyType = channel.loadInfo.externalContentPolicyType;
+ is(
+ contentPolicyType,
+ Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ "sanity check - loading a top level document"
+ );
+
+ let loadingPrincipal = channel.loadInfo.loadingPrincipal;
+ is(
+ loadingPrincipal,
+ null,
+ "sanity check - load of TYPE_DOCUMENT must have a null loadingPrincipal"
+ );
+ ok(
+ !content.document.nodePrincipal.isSystemPrincipal,
+ "sanity check - loaded about page does not have the system principal"
+ );
+ isnot(
+ content.testResult,
+ "fail-script-was-loaded",
+ "The script from https://example.com shouldn't work in an about: page."
+ );
+ }
+ );
+ }
+ );
+});
diff --git a/dom/security/test/general/browser_same_site_cookies_bug1748693.js b/dom/security/test/general/browser_same_site_cookies_bug1748693.js
new file mode 100644
index 0000000000..66a7927889
--- /dev/null
+++ b/dom/security/test/general/browser_same_site_cookies_bug1748693.js
@@ -0,0 +1,61 @@
+"use strict";
+
+const HTTPS_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const HTTP_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // Disable eslint, since we explicitly need a insecure URL here for this test.
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com"
+);
+
+function checkCookies(expectedCookies = {}) {
+ info(JSON.stringify(expectedCookies));
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [expectedCookies],
+ async function (expectedCookies) {
+ let cookies = content.document.getElementById("msg").innerHTML;
+ info(cookies);
+ for (const [cookie, expected] of Object.entries(expectedCookies)) {
+ if (expected) {
+ ok(cookies.includes(cookie), `${cookie} should be sent`);
+ } else {
+ ok(!cookies.includes(cookie), `${cookie} should not be sent`);
+ }
+ }
+ }
+ );
+}
+
+add_task(async function bug1748693() {
+ waitForExplicitFinish();
+
+ // HTTPS-First would interfere with this test. We want to check wether
+ // cookies orignally set on a secure site without a "Secure" attribute
+ // get loaded on a insecure site. For that, we need to visit a
+ // insecure site, which would otherwise be upgraded by HTTPS-First.
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_first", false]],
+ });
+
+ let loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser,
+ `${HTTPS_PATH}file_same_site_cookies_bug1748693.sjs?setcookies`
+ );
+ await loaded;
+
+ loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser,
+ `${HTTP_PATH}file_same_site_cookies_bug1748693.sjs`
+ );
+ await loaded;
+
+ await checkCookies({ auth: true, auth_secure: false });
+
+ finish();
+});
diff --git a/dom/security/test/general/browser_test_assert_systemprincipal_documents.js b/dom/security/test/general/browser_test_assert_systemprincipal_documents.js
new file mode 100644
index 0000000000..8804e85b2c
--- /dev/null
+++ b/dom/security/test/general/browser_test_assert_systemprincipal_documents.js
@@ -0,0 +1,41 @@
+//"use strict"
+
+const kTestPath = getRootDirectory(gTestPath);
+const kTestURI = kTestPath + "file_assert_systemprincipal_documents.html";
+
+add_setup(async function () {
+ // We expect the assertion in function
+ // CheckSystemPrincipalLoads as defined in
+ // file dom/security/nsContentSecurityManager.cpp
+ SimpleTest.expectAssertions(1);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.disallow_non_local_systemprincipal_in_tests", true],
+ ["security.allow_unsafe_parent_loads", true],
+ ],
+ });
+});
+
+add_task(async function open_test_iframe_in_tab() {
+ // This looks at the iframe (load type SUBDOCUMENT)
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: kTestURI },
+ async browser => {
+ await SpecialPowers.spawn(browser, [], async function () {
+ let outerPrincipal = content.document.nodePrincipal;
+ ok(
+ outerPrincipal.isSystemPrincipal,
+ "Sanity: Using SystemPrincipal for test file on chrome://"
+ );
+ const iframeDoc =
+ content.document.getElementById("testframe").contentDocument;
+ is(
+ iframeDoc.body.innerHTML,
+ "",
+ "iframe with systemprincipal should be empty document"
+ );
+ });
+ }
+ );
+});
diff --git a/dom/security/test/general/browser_test_data_download.js b/dom/security/test/general/browser_test_data_download.js
new file mode 100644
index 0000000000..df5a8aeac4
--- /dev/null
+++ b/dom/security/test/general/browser_test_data_download.js
@@ -0,0 +1,113 @@
+"use strict";
+
+const kTestPath = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+const kTestURI = kTestPath + "file_data_download.html";
+
+function addWindowListener(aURL) {
+ return new Promise(resolve => {
+ Services.wm.addListener({
+ onOpenWindow(aXULWindow) {
+ info("window opened, waiting for focus");
+ Services.wm.removeListener(this);
+ var domwindow = aXULWindow.docShell.domWindow;
+ waitForFocus(function () {
+ is(
+ domwindow.document.location.href,
+ aURL,
+ "should have seen the right window open"
+ );
+ resolve(domwindow);
+ }, domwindow);
+ },
+ onCloseWindow(aXULWindow) {},
+ });
+ });
+}
+
+function waitDelay(delay) {
+ return new Promise((resolve, reject) => {
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ window.setTimeout(resolve, delay);
+ });
+}
+
+function promisePanelOpened() {
+ if (DownloadsPanel.panel && DownloadsPanel.panel.state == "open") {
+ return Promise.resolve();
+ }
+ return BrowserTestUtils.waitForEvent(DownloadsPanel.panel, "popupshown");
+}
+
+add_task(async function test_with_downloads_pref_disabled() {
+ waitForExplicitFinish();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.data_uri.block_toplevel_data_uri_navigations", true],
+ ["browser.download.always_ask_before_handling_new_types", true],
+ ],
+ });
+ let windowPromise = addWindowListener(
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml"
+ );
+ BrowserTestUtils.startLoadingURIString(gBrowser, kTestURI);
+ let win = await windowPromise;
+
+ is(
+ win.document.getElementById("location").value,
+ "data-foo.html",
+ "file name of download should match"
+ );
+
+ let mainWindowActivated = BrowserTestUtils.waitForEvent(window, "activate");
+ await BrowserTestUtils.closeWindow(win);
+ await mainWindowActivated;
+});
+
+add_task(async function test_with_always_ask_pref_disabled() {
+ waitForExplicitFinish();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.data_uri.block_toplevel_data_uri_navigations", true],
+ ["browser.download.always_ask_before_handling_new_types", false],
+ ],
+ });
+ let downloadsPanelPromise = promisePanelOpened();
+ let downloadsPromise = Downloads.getList(Downloads.PUBLIC);
+
+ BrowserTestUtils.startLoadingURIString(gBrowser, kTestURI);
+ // wait until downloadsPanel opens before continuing with test
+ await downloadsPanelPromise;
+ let downloadList = await downloadsPromise;
+
+ is(DownloadsPanel.isPanelShowing, true, "DownloadsPanel should be open.");
+ is(
+ downloadList._downloads.length,
+ 1,
+ "File should be successfully downloaded."
+ );
+
+ let [download] = downloadList._downloads;
+ is(download.contentType, "text/html", "File contentType should be correct.");
+ is(
+ download.source.url,
+ "data:text/html,<body>data download</body>",
+ "File name should be correct."
+ );
+
+ info("cleaning up downloads");
+ try {
+ if (Services.appinfo.OS === "WINNT") {
+ // We need to make the file writable to delete it on Windows.
+ await IOUtils.setPermissions(download.target.path, 0o600);
+ }
+ await IOUtils.remove(download.target.path);
+ } catch (error) {
+ info("The file " + download.target.path + " is not removed, " + error);
+ }
+
+ await downloadList.remove(download);
+ await download.finalize();
+});
diff --git a/dom/security/test/general/browser_test_data_text_csv.js b/dom/security/test/general/browser_test_data_text_csv.js
new file mode 100644
index 0000000000..9855ddce46
--- /dev/null
+++ b/dom/security/test/general/browser_test_data_text_csv.js
@@ -0,0 +1,108 @@
+"use strict";
+
+const kTestPath = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+const kTestURI = kTestPath + "file_data_text_csv.html";
+
+function addWindowListener(aURL, aCallback) {
+ return new Promise(resolve => {
+ Services.wm.addListener({
+ onOpenWindow(aXULWindow) {
+ info("window opened, waiting for focus");
+ Services.wm.removeListener(this);
+ var domwindow = aXULWindow.docShell.domWindow;
+ waitForFocus(function () {
+ is(
+ domwindow.document.location.href,
+ aURL,
+ "should have seen the right window open"
+ );
+ resolve(domwindow);
+ }, domwindow);
+ },
+ onCloseWindow(aXULWindow) {},
+ });
+ });
+}
+
+function promisePanelOpened() {
+ if (DownloadsPanel.panel && DownloadsPanel.panel.state == "open") {
+ return Promise.resolve();
+ }
+ return BrowserTestUtils.waitForEvent(DownloadsPanel.panel, "popupshown");
+}
+
+add_task(async function test_with_pref_enabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.data_uri.block_toplevel_data_uri_navigations", true],
+ ["browser.download.always_ask_before_handling_new_types", true],
+ ],
+ });
+
+ let windowPromise = addWindowListener(
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml"
+ );
+ BrowserTestUtils.startLoadingURIString(gBrowser, kTestURI);
+ let win = await windowPromise;
+
+ let expectedValue = "Untitled.csv";
+ is(
+ win.document.getElementById("location").value,
+ expectedValue,
+ "file name of download should match"
+ );
+ let mainWindowActivated = BrowserTestUtils.waitForEvent(window, "activate");
+ await BrowserTestUtils.closeWindow(win);
+ await mainWindowActivated;
+});
+
+add_task(async function test_with_pref_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.data_uri.block_toplevel_data_uri_navigations", true],
+ ["browser.download.always_ask_before_handling_new_types", false],
+ ],
+ });
+ let downloadsPanelPromise = promisePanelOpened();
+ let downloadsPromise = Downloads.getList(Downloads.PUBLIC);
+ let sourceURLBit = "text/csv;foo,bar,foobar";
+
+ info("Loading URI for pref enabled");
+ BrowserTestUtils.startLoadingURIString(gBrowser, kTestURI);
+ info("Waiting for downloads panel to open");
+ await downloadsPanelPromise;
+ info("Getting downloads info after opening downloads panel");
+ let downloadList = await downloadsPromise;
+
+ is(DownloadsPanel.isPanelShowing, true, "DownloadsPanel should be open.");
+ is(
+ downloadList._downloads.length,
+ 1,
+ "File should be successfully downloaded."
+ );
+
+ let [download] = downloadList._downloads;
+ is(download.contentType, "text/csv", "File contentType should be correct.");
+ is(
+ download.source.url,
+ `data:${sourceURLBit}`,
+ "File name should be correct."
+ );
+
+ info("Cleaning up downloads");
+ try {
+ if (Services.appinfo.OS === "WINNT") {
+ // We need to make the file writable to delete it on Windows.
+ await IOUtils.setPermissions(download.target.path, 0o600);
+ }
+ await IOUtils.remove(download.target.path);
+ } catch (ex) {
+ info("The file " + download.target.path + " is not removed, " + ex);
+ }
+
+ await downloadList.remove(download);
+ await download.finalize();
+});
diff --git a/dom/security/test/general/browser_test_framing_error_pages.js b/dom/security/test/general/browser_test_framing_error_pages.js
new file mode 100644
index 0000000000..9fc10f34c7
--- /dev/null
+++ b/dom/security/test/general/browser_test_framing_error_pages.js
@@ -0,0 +1,53 @@
+"use strict";
+
+const kTestPath = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const kTestXFrameOptionsURI = kTestPath + "file_framing_error_pages_xfo.html";
+const kTestXFrameOptionsURIFrame =
+ kTestPath + "file_framing_error_pages.sjs?xfo";
+
+const kTestFrameAncestorsURI = kTestPath + "file_framing_error_pages_csp.html";
+const kTestFrameAncestorsURIFrame =
+ kTestPath + "file_framing_error_pages.sjs?csp";
+
+add_task(async function open_test_xfo_error_page() {
+ await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ browser,
+ true,
+ kTestXFrameOptionsURIFrame,
+ true
+ );
+ BrowserTestUtils.startLoadingURIString(browser, kTestXFrameOptionsURI);
+ await loaded;
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ const iframeDoc =
+ content.document.getElementById("testframe").contentDocument;
+ let errorPage = iframeDoc.body.innerHTML;
+ ok(errorPage.includes("csp-xfo-error-title"), "xfo error page correct");
+ });
+ });
+});
+
+add_task(async function open_test_csp_frame_ancestor_error_page() {
+ await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ browser,
+ true,
+ kTestFrameAncestorsURIFrame,
+ true
+ );
+ BrowserTestUtils.startLoadingURIString(browser, kTestFrameAncestorsURI);
+ await loaded;
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ const iframeDoc =
+ content.document.getElementById("testframe").contentDocument;
+ let errorPage = iframeDoc.body.innerHTML;
+ ok(errorPage.includes("csp-xfo-error-title"), "csp error page correct");
+ });
+ });
+});
diff --git a/dom/security/test/general/browser_test_gpc_privateBrowsingMode.js b/dom/security/test/general/browser_test_gpc_privateBrowsingMode.js
new file mode 100644
index 0000000000..5c056395a8
--- /dev/null
+++ b/dom/security/test/general/browser_test_gpc_privateBrowsingMode.js
@@ -0,0 +1,67 @@
+"use strict";
+
+const kTestPath = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const kTestURI = kTestPath + "file_empty.html";
+
+add_task(async function test_privateModeGPCEnabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.globalprivacycontrol.enabled", false],
+ ["privacy.globalprivacycontrol.pbmode.enabled", true],
+ ["privacy.globalprivacycontrol.functionality.enabled", true],
+ ],
+ });
+ let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ let tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, kTestURI);
+ let browser = win.gBrowser.getBrowserForTab(tab);
+ let result = await SpecialPowers.spawn(browser, [], async function () {
+ return content.window
+ .fetch("file_gpc_server.sjs")
+ .then(response => response.text())
+ .then(response => {
+ is(response, "true", "GPC header provided");
+ is(
+ content.window.navigator.globalPrivacyControl,
+ true,
+ "GPC on navigator"
+ );
+ // Bug 1320796: Service workers are not enabled in PB Mode
+ return true;
+ });
+ });
+ ok(result, "Promise chain resolves in content process");
+ win.close();
+});
+
+add_task(async function test_privateModeGPCDisabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.globalprivacycontrol.enabled", false],
+ ["privacy.globalprivacycontrol.pbmode.enabled", false],
+ ["privacy.globalprivacycontrol.functionality.enabled", true],
+ ],
+ });
+ let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ let tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, kTestURI);
+ let browser = win.gBrowser.getBrowserForTab(tab);
+ let result = await SpecialPowers.spawn(browser, [], async function () {
+ return content.window
+ .fetch("file_gpc_server.sjs")
+ .then(response => response.text())
+ .then(response => {
+ isnot(response, "true", "GPC header provided");
+ isnot(
+ content.window.navigator.globalPrivacyControl,
+ true,
+ "GPC on navigator"
+ );
+ // Bug 1320796: Service workers are not enabled in PB Mode
+ return true;
+ });
+ });
+ ok(result, "Promise chain resolves in content process");
+ win.close();
+});
diff --git a/dom/security/test/general/browser_test_referrer_loadInOtherProcess.js b/dom/security/test/general/browser_test_referrer_loadInOtherProcess.js
new file mode 100644
index 0000000000..7da60b727d
--- /dev/null
+++ b/dom/security/test/general/browser_test_referrer_loadInOtherProcess.js
@@ -0,0 +1,156 @@
+const TEST_PAGE =
+ "https://example.org/browser/browser/base/content/test/general/dummy_page.html";
+const TEST_REFERRER = "http://mochi.test:8888/";
+
+const ReferrerInfo = Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+);
+
+let referrerInfo = new ReferrerInfo(
+ Ci.nsIReferrerInfo.ORIGIN,
+ true,
+ Services.io.newURI(TEST_REFERRER)
+);
+let deReferrerInfo = E10SUtils.serializeReferrerInfo(referrerInfo);
+
+var checkResult = async function (isRemote, browserKey, uri) {
+ is(
+ gBrowser.selectedBrowser.isRemoteBrowser,
+ isRemote,
+ "isRemoteBrowser should be correct"
+ );
+
+ is(
+ gBrowser.selectedBrowser.permanentKey,
+ browserKey,
+ "browser.permanentKey should be correct"
+ );
+
+ if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ let sessionHistory =
+ gBrowser.selectedBrowser.browsingContext.sessionHistory;
+ let entry = sessionHistory.getEntryAtIndex(sessionHistory.count - 1);
+ let args = { uri, referrerInfo: deReferrerInfo, isRemote };
+ Assert.equal(entry.URI.spec, args.uri, "Uri should be correct");
+
+ // Main process like about:mozilla does not trigger the real network request.
+ // So we don't store referrerInfo in sessionHistory in that case.
+ // Besides, the referrerInfo stored in sessionHistory was computed, we only
+ // check pre-computed things.
+ if (args.isRemote) {
+ let resultReferrerInfo = entry.referrerInfo;
+ let expectedReferrerInfo = E10SUtils.deserializeReferrerInfo(
+ args.referrerInfo
+ );
+
+ Assert.equal(
+ resultReferrerInfo.originalReferrer.spec,
+ expectedReferrerInfo.originalReferrer.spec,
+ "originalReferrer should be correct"
+ );
+ Assert.equal(
+ resultReferrerInfo.sendReferrer,
+ expectedReferrerInfo.sendReferrer,
+ "sendReferrer should be correct"
+ );
+ Assert.equal(
+ resultReferrerInfo.referrerPolicy,
+ expectedReferrerInfo.referrerPolicy,
+ "referrerPolicy should be correct"
+ );
+ } else {
+ Assert.equal(entry.referrerInfo, null, "ReferrerInfo should be correct");
+ }
+
+ return;
+ }
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ uri, referrerInfo: deReferrerInfo, isRemote }],
+ async function (args) {
+ let webNav = content.docShell.QueryInterface(Ci.nsIWebNavigation);
+ let sessionHistory = webNav.sessionHistory;
+ let entry = sessionHistory.legacySHistory.getEntryAtIndex(
+ sessionHistory.count - 1
+ );
+
+ var { E10SUtils } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://gre/modules/E10SUtils.sys.mjs"
+ );
+
+ Assert.equal(entry.URI.spec, args.uri, "Uri should be correct");
+
+ // Main process like about:mozilla does not trigger the real network request.
+ // So we don't store referrerInfo in sessionHistory in that case.
+ // Besides, the referrerInfo stored in sessionHistory was computed, we only
+ // check pre-computed things.
+ if (args.isRemote) {
+ let resultReferrerInfo = entry.referrerInfo;
+ let expectedReferrerInfo = E10SUtils.deserializeReferrerInfo(
+ args.referrerInfo
+ );
+
+ Assert.equal(
+ resultReferrerInfo.originalReferrer.spec,
+ expectedReferrerInfo.originalReferrer.spec,
+ "originalReferrer should be correct"
+ );
+ Assert.equal(
+ resultReferrerInfo.sendReferrer,
+ expectedReferrerInfo.sendReferrer,
+ "sendReferrer should be correct"
+ );
+ Assert.equal(
+ resultReferrerInfo.referrerPolicy,
+ expectedReferrerInfo.referrerPolicy,
+ "referrerPolicy should be correct"
+ );
+ } else {
+ Assert.equal(
+ entry.referrerInfo,
+ null,
+ "ReferrerInfo should be correct"
+ );
+ }
+ }
+ );
+};
+var waitForLoad = async function (uri) {
+ info("waitForLoad " + uri);
+ let loadURIOptions = {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ referrerInfo,
+ };
+ gBrowser.selectedBrowser.webNavigation.loadURI(
+ Services.io.newURI(uri),
+ loadURIOptions
+ );
+
+ await BrowserTestUtils.browserStopped(gBrowser, uri);
+};
+
+// Tests referrerInfo when navigating from a page in the remote process to main
+// process and vice versa.
+add_task(async function test_navigation() {
+ // Navigate from non remote to remote
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+ let testURI = TEST_PAGE;
+ let { permanentKey } = gBrowser.selectedBrowser;
+ await waitForLoad(testURI);
+ await checkResult(true, permanentKey, testURI);
+ gBrowser.removeCurrentTab();
+
+ // Navigate from remote to non-remote
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, TEST_PAGE);
+ // Wait for the non-blank page to finish loading
+ await BrowserTestUtils.browserStopped(gBrowser, TEST_PAGE);
+ testURI = "about:mozilla";
+ permanentKey = gBrowser.selectedBrowser.permanentKey;
+ await waitForLoad(testURI);
+ await checkResult(false, permanentKey, testURI);
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/dom/security/test/general/browser_test_report_blocking.js b/dom/security/test/general/browser_test_report_blocking.js
new file mode 100644
index 0000000000..ebd7514097
--- /dev/null
+++ b/dom/security/test/general/browser_test_report_blocking.js
@@ -0,0 +1,218 @@
+"use strict";
+
+const { TelemetryArchiveTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryArchiveTesting.sys.mjs"
+);
+
+const kTestPath = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+const kTestXFrameOptionsURI = kTestPath + "file_framing_error_pages_xfo.html";
+const kTestCspURI = kTestPath + "file_framing_error_pages_csp.html";
+const kTestXFrameOptionsURIFrame =
+ kTestPath + "file_framing_error_pages.sjs?xfo";
+const kTestCspURIFrame = kTestPath + "file_framing_error_pages.sjs?csp";
+
+const kTestExpectedPingXFO = [
+ [["payload", "error_type"], "xfo"],
+ [["payload", "xfo_header"], "deny"],
+ [["payload", "csp_header"], ""],
+ [["payload", "frame_hostname"], "example.com"],
+ [["payload", "top_hostname"], "example.com"],
+ [
+ ["payload", "frame_uri"],
+ "https://example.com/browser/dom/security/test/general/file_framing_error_pages.sjs",
+ ],
+ [
+ ["payload", "top_uri"],
+ "https://example.com/browser/dom/security/test/general/file_framing_error_pages_xfo.html",
+ ],
+];
+
+const kTestExpectedPingCSP = [
+ [["payload", "error_type"], "csp"],
+ [["payload", "xfo_header"], ""],
+ [["payload", "csp_header"], "'none'"],
+ [["payload", "frame_hostname"], "example.com"],
+ [["payload", "top_hostname"], "example.com"],
+ [
+ ["payload", "frame_uri"],
+ "https://example.com/browser/dom/security/test/general/file_framing_error_pages.sjs",
+ ],
+ [
+ ["payload", "top_uri"],
+ "https://example.com/browser/dom/security/test/general/file_framing_error_pages_csp.html",
+ ],
+];
+
+const TEST_CASES = [
+ {
+ type: "xfo",
+ test_uri: kTestXFrameOptionsURI,
+ frame_uri: kTestXFrameOptionsURIFrame,
+ expected_ping: kTestExpectedPingXFO,
+ },
+ {
+ type: "csp",
+ test_uri: kTestCspURI,
+ frame_uri: kTestCspURIFrame,
+ expected_ping: kTestExpectedPingCSP,
+ },
+];
+
+add_setup(async function () {
+ Services.telemetry.setEventRecordingEnabled("security.ui.xfocsperror", true);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.xfocsp.errorReporting.enabled", true],
+ ["security.xfocsp.errorReporting.automatic", false],
+ ],
+ });
+});
+
+add_task(async function testReportingCases() {
+ for (const test of TEST_CASES) {
+ await testReporting(test);
+ }
+});
+
+async function testReporting(test) {
+ // Clear telemetry event before testing.
+ Services.telemetry.clearEvents();
+
+ let telemetryChecker = new TelemetryArchiveTesting.Checker();
+ await telemetryChecker.promiseInit();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+ let browser = tab.linkedBrowser;
+
+ let loaded = BrowserTestUtils.browserLoaded(
+ browser,
+ true,
+ test.frame_uri,
+ true
+ );
+ BrowserTestUtils.startLoadingURIString(browser, test.test_uri);
+ await loaded;
+
+ let { type } = test;
+
+ let frameBC = await SpecialPowers.spawn(browser, [], async _ => {
+ const iframe = content.document.getElementById("testframe");
+ return iframe.browsingContext;
+ });
+
+ await SpecialPowers.spawn(frameBC, [type], async obj => {
+ // Wait until the reporting UI is visible.
+ await ContentTaskUtils.waitForCondition(() => {
+ let reportUI = content.document.getElementById("blockingErrorReporting");
+ return ContentTaskUtils.isVisible(reportUI);
+ });
+
+ let reportCheckBox = content.document.getElementById(
+ "automaticallyReportBlockingInFuture"
+ );
+ is(
+ reportCheckBox.checked,
+ false,
+ "The checkbox of the reporting ui should be not checked."
+ );
+
+ // Click on the checkbox.
+ await EventUtils.synthesizeMouseAtCenter(reportCheckBox, {}, content);
+ });
+ BrowserTestUtils.removeTab(tab);
+
+ // Open the error page again
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+ browser = tab.linkedBrowser;
+
+ loaded = BrowserTestUtils.browserLoaded(browser, true, test.frame_uri, true);
+ BrowserTestUtils.startLoadingURIString(browser, test.test_uri);
+ await loaded;
+
+ frameBC = await SpecialPowers.spawn(browser, [], async _ => {
+ const iframe = content.document.getElementById("testframe");
+ return iframe.browsingContext;
+ });
+
+ await SpecialPowers.spawn(frameBC, [], async _ => {
+ // Wait until the reporting UI is visible.
+ await ContentTaskUtils.waitForCondition(() => {
+ let reportUI = content.document.getElementById("blockingErrorReporting");
+ return ContentTaskUtils.isVisible(reportUI);
+ });
+
+ let reportCheckBox = content.document.getElementById(
+ "automaticallyReportBlockingInFuture"
+ );
+ is(
+ reportCheckBox.checked,
+ true,
+ "The checkbox of the reporting ui should be checked."
+ );
+
+ // Click on the checkbox again to disable the reporting.
+ await EventUtils.synthesizeMouseAtCenter(reportCheckBox, {}, content);
+
+ is(
+ reportCheckBox.checked,
+ false,
+ "The checkbox of the reporting ui should be unchecked."
+ );
+ });
+ BrowserTestUtils.removeTab(tab);
+
+ // Open the error page again to see if the reporting is disabled.
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+ browser = tab.linkedBrowser;
+
+ loaded = BrowserTestUtils.browserLoaded(browser, true, test.frame_uri, true);
+ BrowserTestUtils.startLoadingURIString(browser, test.test_uri);
+ await loaded;
+
+ frameBC = await SpecialPowers.spawn(browser, [], async _ => {
+ const iframe = content.document.getElementById("testframe");
+ return iframe.browsingContext;
+ });
+
+ await SpecialPowers.spawn(frameBC, [], async _ => {
+ // Wait until the reporting UI is visible.
+ await ContentTaskUtils.waitForCondition(() => {
+ let reportUI = content.document.getElementById("blockingErrorReporting");
+ return ContentTaskUtils.isVisible(reportUI);
+ });
+
+ let reportCheckBox = content.document.getElementById(
+ "automaticallyReportBlockingInFuture"
+ );
+ is(
+ reportCheckBox.checked,
+ false,
+ "The checkbox of the reporting ui should be unchecked."
+ );
+ });
+ BrowserTestUtils.removeTab(tab);
+
+ // Finally, check if the ping has been archived.
+ await new Promise(resolve => {
+ telemetryChecker
+ .promiseFindPing("xfocsp-error-report", test.expected_ping)
+ .then(
+ found => {
+ ok(found, "Telemetry ping submitted successfully");
+ resolve();
+ },
+ err => {
+ ok(false, "Exception finding telemetry ping: " + err);
+ resolve();
+ }
+ );
+ });
+}
diff --git a/dom/security/test/general/browser_test_toplevel_data_navigations.js b/dom/security/test/general/browser_test_toplevel_data_navigations.js
new file mode 100644
index 0000000000..0e006f1fd2
--- /dev/null
+++ b/dom/security/test/general/browser_test_toplevel_data_navigations.js
@@ -0,0 +1,70 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+"use strict";
+
+const kDataBody = "toplevel navigation to data: URI allowed";
+const kDataURI = "data:text/html,<body>" + kDataBody + "</body>";
+const kTestPath = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+const kRedirectURI = kTestPath + "file_toplevel_data_navigations.sjs";
+const kMetaRedirectURI = kTestPath + "file_toplevel_data_meta_redirect.html";
+
+add_task(async function test_nav_data_uri() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.data_uri.block_toplevel_data_uri_navigations", true]],
+ });
+ await BrowserTestUtils.withNewTab(kDataURI, async function (browser) {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ kDataBody }],
+ async function ({ kDataBody }) {
+ // eslint-disable-line
+ is(
+ content.document.body.innerHTML,
+ kDataBody,
+ "data: URI navigation from system should be allowed"
+ );
+ }
+ );
+ });
+});
+
+add_task(async function test_nav_data_uri_redirect() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.data_uri.block_toplevel_data_uri_navigations", true]],
+ });
+ let tab = BrowserTestUtils.addTab(gBrowser, kRedirectURI);
+ registerCleanupFunction(async function () {
+ BrowserTestUtils.removeTab(tab);
+ });
+ // wait to make sure data: URI did not load before checking that it got blocked
+ await new Promise(resolve => setTimeout(resolve, 500));
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ is(
+ content.document.body.innerHTML,
+ "",
+ "data: URI navigation after server redirect should be blocked"
+ );
+ });
+});
+
+add_task(async function test_nav_data_uri_meta_redirect() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.data_uri.block_toplevel_data_uri_navigations", true]],
+ });
+ let tab = BrowserTestUtils.addTab(gBrowser, kMetaRedirectURI);
+ registerCleanupFunction(async function () {
+ BrowserTestUtils.removeTab(tab);
+ });
+ // wait to make sure data: URI did not load before checking that it got blocked
+ await new Promise(resolve => setTimeout(resolve, 500));
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ is(
+ content.document.body.innerHTML,
+ "",
+ "data: URI navigation after meta redirect should be blocked"
+ );
+ });
+});
diff --git a/dom/security/test/general/browser_test_view_image_data_navigation.js b/dom/security/test/general/browser_test_view_image_data_navigation.js
new file mode 100644
index 0000000000..90aace1e3e
--- /dev/null
+++ b/dom/security/test/general/browser_test_view_image_data_navigation.js
@@ -0,0 +1,71 @@
+"use strict";
+
+add_task(async function test_principal_right_click_open_link_in_new_tab() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.data_uri.block_toplevel_data_uri_navigations", true]],
+ });
+
+ const TEST_PAGE =
+ getRootDirectory(gTestPath) + "file_view_image_data_navigation.html";
+
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async function (browser) {
+ let loadPromise = BrowserTestUtils.waitForNewTab(gBrowser, null, true);
+
+ // simulate right-click->view-image
+ BrowserTestUtils.waitForEvent(document, "popupshown", false, event => {
+ // These are operations that must be executed synchronously with the event.
+ document.getElementById("context-viewimage").doCommand();
+ event.target.hidePopup();
+ return true;
+ });
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#testimage",
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+ let tab = await loadPromise;
+
+ let spec = tab.linkedBrowser.currentURI.spec;
+ ok(
+ spec.startsWith("data:image/svg+xml;"),
+ "data:image/svg navigation allowed through right-click view-image"
+ );
+
+ gBrowser.removeTab(tab);
+ });
+});
+
+add_task(async function test_right_click_open_bg_image() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.data_uri.block_toplevel_data_uri_navigations", true]],
+ });
+
+ const TEST_PAGE =
+ getRootDirectory(gTestPath) + "file_view_bg_image_data_navigation.html";
+
+ await BrowserTestUtils.withNewTab(TEST_PAGE, async function (browser) {
+ let loadPromise = BrowserTestUtils.waitForNewTab(gBrowser, null, true);
+
+ // simulate right-click->view-image
+ BrowserTestUtils.waitForEvent(document, "popupshown", false, event => {
+ // These are operations that must be executed synchronously with the event.
+ document.getElementById("context-viewimage").doCommand();
+ event.target.hidePopup();
+ return true;
+ });
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#testbody",
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+ let tab = await loadPromise;
+
+ let spec = tab.linkedBrowser.currentURI.spec;
+ ok(
+ spec.startsWith("data:image/svg+xml;"),
+ "data:image/svg navigation allowed through right-click view-image with background image"
+ );
+
+ gBrowser.removeTab(tab);
+ });
+});
diff --git a/dom/security/test/general/browser_test_xfo_embed_object.js b/dom/security/test/general/browser_test_xfo_embed_object.js
new file mode 100644
index 0000000000..bcc48b984c
--- /dev/null
+++ b/dom/security/test/general/browser_test_xfo_embed_object.js
@@ -0,0 +1,41 @@
+"use strict";
+
+const kTestPath = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const kTestXFOEmbedURI = kTestPath + "file_framing_xfo_embed.html";
+const kTestXFOObjectURI = kTestPath + "file_framing_xfo_object.html";
+
+const errorMessage = `The loading of “https://example.com/browser/dom/security/test/general/file_framing_xfo_embed_object.sjs†in a frame is denied by “X-Frame-Options“ directive set to “deny“`;
+
+let xfoBlocked = false;
+
+function onXFOMessage(msgObj) {
+ const message = msgObj.message;
+
+ if (message.includes(errorMessage)) {
+ ok(true, "XFO error message logged");
+ xfoBlocked = true;
+ }
+}
+
+add_task(async function open_test_xfo_embed_blocked() {
+ xfoBlocked = false;
+ await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ Services.console.registerListener(onXFOMessage);
+ BrowserTestUtils.startLoadingURIString(browser, kTestXFOEmbedURI);
+ await BrowserTestUtils.waitForCondition(() => xfoBlocked);
+ Services.console.unregisterListener(onXFOMessage);
+ });
+});
+
+add_task(async function open_test_xfo_object_blocked() {
+ xfoBlocked = false;
+ await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ Services.console.registerListener(onXFOMessage);
+ BrowserTestUtils.startLoadingURIString(browser, kTestXFOObjectURI);
+ await BrowserTestUtils.waitForCondition(() => xfoBlocked);
+ Services.console.unregisterListener(onXFOMessage);
+ });
+});
diff --git a/dom/security/test/general/bug1277803.html b/dom/security/test/general/bug1277803.html
new file mode 100644
index 0000000000..c8033551a0
--- /dev/null
+++ b/dom/security/test/general/bug1277803.html
@@ -0,0 +1,11 @@
+<html>
+
+<head>
+ <link rel='icon' href='favicon_bug1277803.ico'>
+</head>
+
+<body>
+Nothing to see here...
+</body>
+
+</html>
diff --git a/dom/security/test/general/chrome.toml b/dom/security/test/general/chrome.toml
new file mode 100644
index 0000000000..88feda944b
--- /dev/null
+++ b/dom/security/test/general/chrome.toml
@@ -0,0 +1,15 @@
+[DEFAULT]
+support-files = [
+ "favicon_bug1277803.ico",
+ "bug1277803.html",
+]
+
+["test_bug1277803.xhtml"]
+skip-if = [
+ "os == 'android'",
+ "verify",
+]
+
+["test_innerhtml_sanitizer.html"]
+
+["test_innerhtml_sanitizer.xhtml"]
diff --git a/dom/security/test/general/closeWindow.sjs b/dom/security/test/general/closeWindow.sjs
new file mode 100644
index 0000000000..996db36f6f
--- /dev/null
+++ b/dom/security/test/general/closeWindow.sjs
@@ -0,0 +1,24 @@
+const BODY = `
+ <script>
+ opener.postMessage("ok!", "*");
+ close();
+ </script>`;
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ if (request.queryString.includes("unset")) {
+ response.setHeader("Set-Cookie", "test=wow", true);
+ }
+
+ if (request.queryString.includes("none")) {
+ response.setHeader("Set-Cookie", "test2=wow2; samesite=none", true);
+ }
+
+ if (request.queryString.includes("lax")) {
+ response.setHeader("Set-Cookie", "test3=wow3; samesite=lax", true);
+ }
+
+ response.write(BODY);
+}
diff --git a/dom/security/test/general/favicon_bug1277803.ico b/dom/security/test/general/favicon_bug1277803.ico
new file mode 100644
index 0000000000..d44438903b
--- /dev/null
+++ b/dom/security/test/general/favicon_bug1277803.ico
Binary files differ
diff --git a/dom/security/test/general/file_1767581.js b/dom/security/test/general/file_1767581.js
new file mode 100644
index 0000000000..259435b1e4
--- /dev/null
+++ b/dom/security/test/general/file_1767581.js
@@ -0,0 +1 @@
+window.testResult = "fail-script-was-loaded";
diff --git a/dom/security/test/general/file_about_child.html b/dom/security/test/general/file_about_child.html
new file mode 100644
index 0000000000..d83e0e4d41
--- /dev/null
+++ b/dom/security/test/general/file_about_child.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1767581</title>
+ <script id="script" src="https://example.com/browser/dom/security/test/general/file_1767581.js"></script>
+</head>
+<body>
+ Just an about page that loads in the privileged about process!
+</body>
+</html> \ No newline at end of file
diff --git a/dom/security/test/general/file_assert_systemprincipal_documents.html b/dom/security/test/general/file_assert_systemprincipal_documents.html
new file mode 100644
index 0000000000..2d7ff4d253
--- /dev/null
+++ b/dom/security/test/general/file_assert_systemprincipal_documents.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1543579: Block web documents loading into system land </title>
+</head>
+<body>
+<h1>This page loads documents from the SystemPrincipal (which should be blocked)</h1>
+<iframe type="chrome" id="testframe" src="http://example.com/browser/dom/security/test/general/file_assert_systemprincipal_documents_iframe.html"></iframe>
+</body>
+</html>
+
diff --git a/dom/security/test/general/file_assert_systemprincipal_documents_iframe.html b/dom/security/test/general/file_assert_systemprincipal_documents_iframe.html
new file mode 100644
index 0000000000..704625a1da
--- /dev/null
+++ b/dom/security/test/general/file_assert_systemprincipal_documents_iframe.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1543579: Block web documents loading into system land </title>
+</head>
+<body>
+<h1>This is the iframe that should not load.</h1>
+</body>
+</html>
diff --git a/dom/security/test/general/file_block_script_wrong_mime_server.sjs b/dom/security/test/general/file_block_script_wrong_mime_server.sjs
new file mode 100644
index 0000000000..d034c797a4
--- /dev/null
+++ b/dom/security/test/general/file_block_script_wrong_mime_server.sjs
@@ -0,0 +1,37 @@
+// Custom *.sjs specifically for the needs of:
+// Bug 1288361 - Block scripts with wrong MIME type
+
+"use strict";
+
+const WORKER = `
+ onmessage = function(event) {
+ postMessage("worker-loaded");
+ };`;
+
+function handleRequest(request, response) {
+ const query = new URLSearchParams(request.queryString);
+
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ // Set MIME type
+ response.setHeader("Content-Type", query.get("mime"), false);
+
+ // Deliver response
+ switch (query.get("type")) {
+ case "script":
+ response.write("");
+ break;
+ case "worker":
+ response.write(WORKER);
+ break;
+ case "worker-import":
+ response.write(
+ `importScripts("file_block_script_wrong_mime_server.sjs?type=script&mime=${query.get(
+ "mime"
+ )}");`
+ );
+ response.write(WORKER);
+ break;
+ }
+}
diff --git a/dom/security/test/general/file_block_subresource_redir_to_data.sjs b/dom/security/test/general/file_block_subresource_redir_to_data.sjs
new file mode 100644
index 0000000000..1e312bc810
--- /dev/null
+++ b/dom/security/test/general/file_block_subresource_redir_to_data.sjs
@@ -0,0 +1,33 @@
+"use strict";
+
+let SCRIPT_DATA = "alert('this alert should be blocked');";
+let WORKER_DATA =
+ "onmessage = function(event) { postMessage('worker-loaded'); }";
+
+function handleRequest(request, response) {
+ const query = request.queryString;
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setStatusLine("1.1", 302, "Found");
+
+ if (query === "script" || query === "modulescript") {
+ response.setHeader(
+ "Location",
+ "data:text/javascript," + escape(SCRIPT_DATA),
+ false
+ );
+ return;
+ }
+
+ if (query === "worker") {
+ response.setHeader(
+ "Location",
+ "data:text/javascript," + escape(WORKER_DATA),
+ false
+ );
+ return;
+ }
+
+ // we should never get here; just in case return something unexpected
+ response.write("do'h");
+}
diff --git a/dom/security/test/general/file_block_toplevel_data_navigation.html b/dom/security/test/general/file_block_toplevel_data_navigation.html
new file mode 100644
index 0000000000..d6e083a247
--- /dev/null
+++ b/dom/security/test/general/file_block_toplevel_data_navigation.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Toplevel data navigation</title>
+</head>
+<body>
+test1: clicking data: URI tries to navigate window<br/>
+<!-- postMessage will not be sent if data: URI is blocked -->
+<a id="testlink" href="data:text/html,<body>toplevel data: URI navigations
+should be blocked</body>">click me</a>
+<script>
+ document.getElementById('testlink').click();
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/file_block_toplevel_data_navigation2.html b/dom/security/test/general/file_block_toplevel_data_navigation2.html
new file mode 100644
index 0000000000..957189ce07
--- /dev/null
+++ b/dom/security/test/general/file_block_toplevel_data_navigation2.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Toplevel data navigation</title>
+</head>
+<body>
+test2: data: URI in iframe tries to window.open(data:, _blank);<br/>
+<iframe id="testFrame" src=""></iframe>
+<script>
+ let DATA_URI = `data:text/html,<body><script>
+ var win = window.open("data:text/html,<body>toplevel data: URI navigations should be blocked</body>", "_blank");
+ <\/script></body>`;
+ document.getElementById('testFrame').src = DATA_URI;
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/file_block_toplevel_data_navigation3.html b/dom/security/test/general/file_block_toplevel_data_navigation3.html
new file mode 100644
index 0000000000..3743a72034
--- /dev/null
+++ b/dom/security/test/general/file_block_toplevel_data_navigation3.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Toplevel data navigation</title>
+</head>
+<body>
+test3: performing data: URI navigation through win.loc.href<br/>
+<script>
+ // postMessage will not be sent if data: URI is blocked
+ window.location.href = "data:text/html,<body><script>" +
+ "window.opener.postMessage('test3','*');<\/script>toplevel data: URI " +
+ "navigations should be blocked</body>";
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/file_block_toplevel_data_redirect.sjs b/dom/security/test/general/file_block_toplevel_data_redirect.sjs
new file mode 100644
index 0000000000..c03ace5f23
--- /dev/null
+++ b/dom/security/test/general/file_block_toplevel_data_redirect.sjs
@@ -0,0 +1,13 @@
+// Custom *.sjs file specifically for the needs of Bug:
+// Bug 1394554 - Block toplevel data: URI navigations after redirect
+
+var DATA_URI =
+ "<body>toplevel data: URI navigations after redirect should be blocked</body>";
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", "data:text/html," + escape(DATA_URI), false);
+}
diff --git a/dom/security/test/general/file_cache_splitting_isloaded.sjs b/dom/security/test/general/file_cache_splitting_isloaded.sjs
new file mode 100644
index 0000000000..a40b9674e5
--- /dev/null
+++ b/dom/security/test/general/file_cache_splitting_isloaded.sjs
@@ -0,0 +1,35 @@
+/*
+ Helper Server -
+ Send a Request with ?queryResult - response will be the
+ queryString of the next request.
+
+*/
+// small red image
+const IMG_BYTES = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
+ "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="
+);
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ // save the object state of the initial request, which returns
+ // async once the server has processed the img request.
+ if (request.queryString.includes("wait")) {
+ response.processAsync();
+ setObjectState("wait", response);
+ return;
+ }
+
+ response.write(IMG_BYTES);
+
+ // return the result
+ getObjectState("wait", function (queryResponse) {
+ if (!queryResponse) {
+ return;
+ }
+ queryResponse.write("1");
+ queryResponse.finish();
+ });
+}
diff --git a/dom/security/test/general/file_cache_splitting_server.sjs b/dom/security/test/general/file_cache_splitting_server.sjs
new file mode 100644
index 0000000000..da75986f74
--- /dev/null
+++ b/dom/security/test/general/file_cache_splitting_server.sjs
@@ -0,0 +1,27 @@
+function handleRequest(request, response) {
+ var receivedRequests = parseInt(getState("requests"));
+ if (isNaN(receivedRequests)) {
+ receivedRequests = 0;
+ }
+ if (request.queryString.includes("state")) {
+ response.write(receivedRequests);
+ return;
+ }
+ if (request.queryString.includes("flush")) {
+ setState("requests", "0");
+ response.write("OK");
+ return;
+ }
+ response.setHeader("Cache-Control", "max-age=999999"); // Force caching
+ response.setHeader("Content-Type", "text/css");
+ receivedRequests = receivedRequests + 1;
+ setState("requests", "" + receivedRequests);
+ response.write(`
+ .test{
+ color:red;
+ }
+ .test h1{
+ font-size:200px;
+ }
+ `);
+}
diff --git a/dom/security/test/general/file_cache_splitting_window.html b/dom/security/test/general/file_cache_splitting_window.html
new file mode 100644
index 0000000000..59a2ff2ca9
--- /dev/null
+++ b/dom/security/test/general/file_cache_splitting_window.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>Document</title>
+ <link rel="stylesheet" href="https://example.com/tests/dom/security/test/general/file_cache_splitting_server.sjs">
+</head>
+<body>
+ <h1>HELLO WORLD!</h1>
+
+ <script>
+ window.addEventListener("load",()=>{
+ fetch("file_cache_splitting_isloaded.sjs");
+ });
+ </script>
+</body>
+</html>
diff --git a/dom/security/test/general/file_contentpolicytype_targeted_link_iframe.sjs b/dom/security/test/general/file_contentpolicytype_targeted_link_iframe.sjs
new file mode 100644
index 0000000000..9ee73ae3c4
--- /dev/null
+++ b/dom/security/test/general/file_contentpolicytype_targeted_link_iframe.sjs
@@ -0,0 +1,45 @@
+// custom *.sjs for Bug 1255240
+
+const TEST_FRAME = `
+ <!DOCTYPE HTML>
+ <html>
+ <head><meta charset='utf-8'></head>
+ <body>
+ <a id='testlink' target='innerframe' href='file_contentpolicytype_targeted_link_iframe.sjs?innerframe'>click me</a>
+ <iframe name='innerframe'></iframe>
+ <script type='text/javascript'>
+ var link = document.getElementById('testlink');
+ testlink.click();
+ </script>
+ </body>
+ </html> `;
+
+const INNER_FRAME = `
+ <!DOCTYPE HTML>
+ <html>
+ <head><meta charset='utf-8'></head>
+ hello world!
+ </body>
+ </html>`;
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+
+ var queryString = request.queryString;
+
+ if (queryString === "testframe") {
+ response.write(TEST_FRAME);
+ return;
+ }
+
+ if (queryString === "innerframe") {
+ response.write(INNER_FRAME);
+ return;
+ }
+
+ // we should never get here, but just in case
+ // return something unexpected
+ response.write("do'h");
+}
diff --git a/dom/security/test/general/file_data_download.html b/dom/security/test/general/file_data_download.html
new file mode 100644
index 0000000000..4cc92fe8f5
--- /dev/null
+++ b/dom/security/test/general/file_data_download.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test download attribute for data: URI</title>
+</head>
+<body>
+ <a href="data:text/html,<body>data download</body>" download="data-foo.html" id="testlink">download data</a>
+ <script>
+ // click the link to have the downoad panel appear
+ let testlink = document.getElementById("testlink");
+ testlink.click();
+ </script>
+ </body>
+</html>
diff --git a/dom/security/test/general/file_data_text_csv.html b/dom/security/test/general/file_data_text_csv.html
new file mode 100644
index 0000000000..a9ac369d16
--- /dev/null
+++ b/dom/security/test/general/file_data_text_csv.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test open data:text/csv</title>
+</head>
+<body>
+ <a href="data:text/csv;foo,bar,foobar" id="testlink">test text/csv</a>
+ <script>
+ // click the link to have the downoad panel appear
+ let testlink = document.getElementById("testlink");
+ testlink.click();
+ </script>
+ </body>
+</html>
diff --git a/dom/security/test/general/file_empty.html b/dom/security/test/general/file_empty.html
new file mode 100644
index 0000000000..865879c583
--- /dev/null
+++ b/dom/security/test/general/file_empty.html
@@ -0,0 +1 @@
+<!-- this file intentionally left blank -->
diff --git a/dom/security/test/general/file_framing_error_pages.sjs b/dom/security/test/general/file_framing_error_pages.sjs
new file mode 100644
index 0000000000..fb62a34bdb
--- /dev/null
+++ b/dom/security/test/general/file_framing_error_pages.sjs
@@ -0,0 +1,27 @@
+"use strict";
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+
+ let query = request.queryString;
+ if (query === "xfo") {
+ response.setHeader("x-frame-options", "deny", false);
+ response.write("<html>xfo test loaded</html>");
+ return;
+ }
+
+ if (query === "csp") {
+ response.setHeader(
+ "content-security-policy",
+ "frame-ancestors 'none'",
+ false
+ );
+ response.write("<html>csp test loaded</html>");
+ return;
+ }
+
+ // we should never get here, but just in case
+ // return something unexpected
+ response.write("do'h");
+}
diff --git a/dom/security/test/general/file_framing_error_pages_csp.html b/dom/security/test/general/file_framing_error_pages_csp.html
new file mode 100644
index 0000000000..2764ed4aa6
--- /dev/null
+++ b/dom/security/test/general/file_framing_error_pages_csp.html
@@ -0,0 +1,7 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+iframe should be blocked <br/>
+<iframe id="testframe" src="https://example.com/browser/dom/security/test/general/file_framing_error_pages.sjs?csp" height=800 width=800></iframe>
+</body>
+</html>
diff --git a/dom/security/test/general/file_framing_error_pages_xfo.html b/dom/security/test/general/file_framing_error_pages_xfo.html
new file mode 100644
index 0000000000..82dd1ee459
--- /dev/null
+++ b/dom/security/test/general/file_framing_error_pages_xfo.html
@@ -0,0 +1,7 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+iframe should be blocked <br/>
+<iframe id="testframe" src="https://example.com/browser/dom/security/test/general/file_framing_error_pages.sjs?xfo" height=800 width=800></iframe>
+</body>
+</html>
diff --git a/dom/security/test/general/file_framing_xfo_embed.html b/dom/security/test/general/file_framing_xfo_embed.html
new file mode 100644
index 0000000000..f5cc761b5b
--- /dev/null
+++ b/dom/security/test/general/file_framing_xfo_embed.html
@@ -0,0 +1,7 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+ embed should be blocked <br/>
+ <embed src="https://example.com/browser/dom/security/test/general/file_framing_xfo_embed_object.sjs"></embed>
+</body>
+</html>
diff --git a/dom/security/test/general/file_framing_xfo_embed_object.sjs b/dom/security/test/general/file_framing_xfo_embed_object.sjs
new file mode 100644
index 0000000000..56616b7930
--- /dev/null
+++ b/dom/security/test/general/file_framing_xfo_embed_object.sjs
@@ -0,0 +1,7 @@
+"use strict";
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("x-frame-options", "deny", false);
+ response.write("<html>doc with x-frame-options: deny</html>");
+}
diff --git a/dom/security/test/general/file_framing_xfo_object.html b/dom/security/test/general/file_framing_xfo_object.html
new file mode 100644
index 0000000000..c8480a2c42
--- /dev/null
+++ b/dom/security/test/general/file_framing_xfo_object.html
@@ -0,0 +1,7 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+ object should be blocked <br/>
+ <object data="https://example.com/browser/dom/security/test/general/file_framing_xfo_embed_object.sjs"></object>
+</body>
+</html>
diff --git a/dom/security/test/general/file_gpc_server.sjs b/dom/security/test/general/file_gpc_server.sjs
new file mode 100644
index 0000000000..d0b14215b4
--- /dev/null
+++ b/dom/security/test/general/file_gpc_server.sjs
@@ -0,0 +1,14 @@
+"use strict";
+
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ var gpc = request.hasHeader("Sec-GPC") ? request.getHeader("Sec-GPC") : "";
+
+ if (gpc === "1") {
+ response.write("true");
+ } else {
+ response.write("false");
+ }
+}
diff --git a/dom/security/test/general/file_loads_nonscript.html b/dom/security/test/general/file_loads_nonscript.html
new file mode 100644
index 0000000000..f7692b8066
--- /dev/null
+++ b/dom/security/test/general/file_loads_nonscript.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>File that loads a non-script file-extension as script</title>
+</head>
+<body>
+ <script>
+ /* global equal */
+
+ const files = ["file_nonscript",
+ "file_nonscript.xyz",
+ "file_nonscript.html",
+ "file_nonscript.txt",
+ "file_nonscript.json"];
+
+ async function run() {
+ window.counter = 0;
+
+ for (let file of files) {
+ let script = document.createElement("script");
+ let promise = new Promise((resolve, reject) => {
+ script.addEventListener("error", resolve, {once: true});
+ script.addEventListener("load", reject, {once: true});
+ });
+ script.src = file;
+ document.body.append(script);
+
+ let event = await promise;
+ equal(event.type, "error");
+ equal(window.counter, 0);
+ }
+
+ let script = document.createElement("script");
+ let promise = new Promise((resolve, reject) => {
+ script.addEventListener("load", resolve, {once: true});
+ script.addEventListener("error", reject, {once: true});
+ });
+ script.src = "file_script.js";
+ document.body.append(script);
+
+ let event = await promise;
+ equal(event.type, "load");
+ equal(window.counter, 1);
+
+ window.postMessage("done", "*");
+ }
+ window.addEventListener("message", run, {once: true})
+ </script>
+</html>
diff --git a/dom/security/test/general/file_meta_referrer_in_head.html b/dom/security/test/general/file_meta_referrer_in_head.html
new file mode 100644
index 0000000000..9c4c4cd695
--- /dev/null
+++ b/dom/security/test/general/file_meta_referrer_in_head.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<meta name="referrer" content="no-referrer" />
+<title>Bug 1704473 - Remove head requirement for meta name=referrer</title>
+<script type="application/javascript">
+ fetch("https://example.com");
+</script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/security/test/general/file_meta_referrer_notin_head.html b/dom/security/test/general/file_meta_referrer_notin_head.html
new file mode 100644
index 0000000000..55bd38e4c5
--- /dev/null
+++ b/dom/security/test/general/file_meta_referrer_notin_head.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Bug 1704473 - Remove head requirement for meta name=referrer</title>
+
+</head>
+<body>
+ <meta name="referrer" content="no-referrer" />
+ <script type="application/javascript">
+ fetch("https://example.com");
+ </script>
+</body>
+</html>
diff --git a/dom/security/test/general/file_nonscript b/dom/security/test/general/file_nonscript
new file mode 100644
index 0000000000..c339e45d5d
--- /dev/null
+++ b/dom/security/test/general/file_nonscript
@@ -0,0 +1 @@
+window.counter++;
diff --git a/dom/security/test/general/file_nonscript.html b/dom/security/test/general/file_nonscript.html
new file mode 100644
index 0000000000..c339e45d5d
--- /dev/null
+++ b/dom/security/test/general/file_nonscript.html
@@ -0,0 +1 @@
+window.counter++;
diff --git a/dom/security/test/general/file_nonscript.json b/dom/security/test/general/file_nonscript.json
new file mode 100644
index 0000000000..c339e45d5d
--- /dev/null
+++ b/dom/security/test/general/file_nonscript.json
@@ -0,0 +1 @@
+window.counter++;
diff --git a/dom/security/test/general/file_nonscript.txt b/dom/security/test/general/file_nonscript.txt
new file mode 100644
index 0000000000..c339e45d5d
--- /dev/null
+++ b/dom/security/test/general/file_nonscript.txt
@@ -0,0 +1 @@
+window.counter++;
diff --git a/dom/security/test/general/file_nonscript.xyz b/dom/security/test/general/file_nonscript.xyz
new file mode 100644
index 0000000000..c339e45d5d
--- /dev/null
+++ b/dom/security/test/general/file_nonscript.xyz
@@ -0,0 +1 @@
+window.counter++;
diff --git a/dom/security/test/general/file_nosniff_navigation.sjs b/dom/security/test/general/file_nosniff_navigation.sjs
new file mode 100644
index 0000000000..d332d860fd
--- /dev/null
+++ b/dom/security/test/general/file_nosniff_navigation.sjs
@@ -0,0 +1,39 @@
+// Custom *.sjs file specifically for the needs of Bug 1286861
+
+// small red image
+const IMG = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
+ "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="
+);
+
+// https://stackoverflow.com/questions/17279712/what-is-the-smallest-possible-valid-pdf
+const PDF = `%PDF-1.0
+1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 3 3]>>endobj
+trailer<</Size 4/Root 1 0 R>>`;
+
+function getSniffableContent(type) {
+ switch (type) {
+ case "xml":
+ return `<?xml version="1.0"?><test/>`;
+ case "html":
+ return `<!Doctype html> <html> <head></head> <body> Test test </body></html>`;
+ case "css":
+ return `*{ color: pink !important; }`;
+ case "json":
+ return `{ 'test':'yes' }`;
+ case "img":
+ return IMG;
+ case "pdf":
+ return PDF;
+ }
+ return "Basic UTF-8 Text";
+}
+
+function handleRequest(request, response) {
+ let query = new URLSearchParams(request.queryString);
+
+ // avoid confusing cache behaviors (XXXX no sure what this means?)
+ response.setHeader("X-Content-Type-Options", "nosniff"); // Disable Sniffing
+ response.setHeader("Content-Type", query.get("mime"));
+ response.write(getSniffableContent(query.get("content")));
+}
diff --git a/dom/security/test/general/file_nosniff_testserver.sjs b/dom/security/test/general/file_nosniff_testserver.sjs
new file mode 100644
index 0000000000..d3e52979a4
--- /dev/null
+++ b/dom/security/test/general/file_nosniff_testserver.sjs
@@ -0,0 +1,60 @@
+"use strict";
+
+const SCRIPT = "var foo = 24;";
+const CSS = "body { background-color: green; }";
+
+// small red image
+const IMG = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
+ "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="
+);
+
+function handleRequest(request, response) {
+ const query = new URLSearchParams(request.queryString);
+
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ // set the nosniff header
+ response.setHeader("X-Content-Type-Options", " NoSniFF , foo ", false);
+
+ if (query.has("cssCorrectType")) {
+ response.setHeader("Content-Type", "teXt/cSs", false);
+ response.write(CSS);
+ return;
+ }
+
+ if (query.has("cssWrongType")) {
+ response.setHeader("Content-Type", "text/html", false);
+ response.write(CSS);
+ return;
+ }
+
+ if (query.has("scriptCorrectType")) {
+ response.setHeader("Content-Type", "appLIcation/jAvaScriPt;blah", false);
+ response.write(SCRIPT);
+ return;
+ }
+
+ if (query.has("scriptWrongType")) {
+ response.setHeader("Content-Type", "text/html", false);
+ response.write(SCRIPT);
+ return;
+ }
+
+ if (query.has("imgCorrectType")) {
+ response.setHeader("Content-Type", "iMaGe/pnG;blah", false);
+ response.write(IMG);
+ return;
+ }
+
+ if (query.has("imgWrongType")) {
+ response.setHeader("Content-Type", "text/html", false);
+ response.write(IMG);
+ return;
+ }
+
+ // we should never get here, but just in case
+ response.setHeader("Content-Type", "text/html", false);
+ response.write("do'h");
+}
diff --git a/dom/security/test/general/file_same_site_cookies_about.sjs b/dom/security/test/general/file_same_site_cookies_about.sjs
new file mode 100644
index 0000000000..421eb999be
--- /dev/null
+++ b/dom/security/test/general/file_same_site_cookies_about.sjs
@@ -0,0 +1,99 @@
+// Custom *.sjs file specifically for the needs of Bug 1454721
+
+// small red image
+const IMG_BYTES = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
+ "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="
+);
+
+const IFRAME_INC = `<iframe src='http://mochi.test:8888/tests/dom/security/test/general/file_same_site_cookies_about.sjs?inclusion'></iframe>`;
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ // using startsWith and discard the math random
+ if (request.queryString.startsWith("setSameSiteCookie")) {
+ response.setHeader(
+ "Set-Cookie",
+ "myKey=mySameSiteAboutCookie; samesite=strict",
+ true
+ );
+ response.setHeader("Content-Type", "image/png");
+ response.write(IMG_BYTES);
+ return;
+ }
+
+ // navigation tests
+ if (request.queryString.includes("loadsrcdocframeNav")) {
+ let FRAME = `
+ <iframe srcdoc="foo"
+ onload="document.location='http://mochi.test:8888/tests/dom/security/test/general/file_same_site_cookies_about.sjs?navigation'">
+ </iframe>`;
+ response.write(FRAME);
+ return;
+ }
+
+ if (request.queryString.includes("loadblankframeNav")) {
+ let FRAME = `
+ <iframe src="about:blank"
+ onload="document.location='http://mochi.test:8888/tests/dom/security/test/general/file_same_site_cookies_about.sjs?navigation'">
+ </iframe>`;
+ response.write(FRAME);
+ return;
+ }
+
+ // inclusion tets
+ if (request.queryString.includes("loadsrcdocframeInc")) {
+ response.write('<iframe srcdoc="' + IFRAME_INC + '"></iframe>');
+ return;
+ }
+
+ if (request.queryString.includes("loadblankframeInc")) {
+ let FRAME =
+ `
+ <iframe id="blankframe" src="about:blank"></iframe>
+ <script>
+ document.getElementById("blankframe").contentDocument.write(\"` +
+ IFRAME_INC +
+ `\");
+ <\/script>`;
+ response.write(FRAME);
+ return;
+ }
+
+ if (request.queryString.includes("navigation")) {
+ const cookies = request.hasHeader("Cookie")
+ ? request.getHeader("Cookie")
+ : "";
+ response.write(`
+ <!DOCTYPE html>
+ <html>
+ <body>
+ <script type="application/javascript">
+ window.parent.postMessage({result: "${cookies}" }, '*');
+ </script>
+ </body>
+ </html>
+ `);
+ }
+
+ if (request.queryString.includes("inclusion")) {
+ const cookies = request.hasHeader("Cookie")
+ ? request.getHeader("Cookie")
+ : "";
+ response.write(`
+ <!DOCTYPE html>
+ <html>
+ <body>
+ <script type="application/javascript">
+ window.parent.parent.parent.postMessage({result: "${cookies}" }, '*');
+ </script>
+ </body>
+ </html>
+ `);
+ }
+
+ // we should never get here, but just in case return something unexpected
+ response.write("D'oh");
+}
diff --git a/dom/security/test/general/file_same_site_cookies_blob_iframe_inclusion.html b/dom/security/test/general/file_same_site_cookies_blob_iframe_inclusion.html
new file mode 100644
index 0000000000..b3456f0b90
--- /dev/null
+++ b/dom/security/test/general/file_same_site_cookies_blob_iframe_inclusion.html
@@ -0,0 +1,34 @@
+<html>
+<body>
+<iframe id="testframe"></iframe>
+<script type="application/javascript">
+
+ // simply passing on the message from the child to parent
+ window.addEventListener("message", receiveMessage);
+ function receiveMessage(event) {
+ window.removeEventListener("message", receiveMessage);
+ window.parent.postMessage({result: event.data.result}, '*');
+ }
+
+ const NESTED_IFRAME_INCLUSION = `
+ <html>
+ <body>
+ <script type="application/javascript">
+ window.addEventListener("message", receiveMessage);
+ function receiveMessage(event) {
+ window.removeEventListener("message", receiveMessage);
+ window.parent.postMessage({result: event.data.result}, '*');
+ }
+ <\/script>
+ <iframe src="http://mochi.test:8888/tests/dom/security/test/general/file_same_site_cookies_iframe.sjs"></iframe>
+ </body>
+ </html>`;
+
+ let NESTED_BLOB_IFRAME_INCLUSION = new Blob([NESTED_IFRAME_INCLUSION], {type:'text/html'});
+
+ // query the testframe and set blob URL
+ let testframe = document.getElementById("testframe");
+ testframe.src = window.URL.createObjectURL(NESTED_BLOB_IFRAME_INCLUSION);
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/file_same_site_cookies_blob_iframe_navigation.html b/dom/security/test/general/file_same_site_cookies_blob_iframe_navigation.html
new file mode 100644
index 0000000000..815c6a6bfc
--- /dev/null
+++ b/dom/security/test/general/file_same_site_cookies_blob_iframe_navigation.html
@@ -0,0 +1,30 @@
+<html>
+<body>
+<iframe id="testframe"></iframe>
+<script type="application/javascript">
+
+ // simply passing on the message from the child to parent
+ window.addEventListener("message", receiveMessage);
+ function receiveMessage(event) {
+ window.removeEventListener("message", receiveMessage);
+ window.parent.postMessage({result: event.data.result}, '*');
+ }
+
+ const NESTED_IFRAME_NAVIGATION = `
+ <html>
+ <body>
+ <a id="testlink" href="http://mochi.test:8888/tests/dom/security/test/general/file_same_site_cookies_iframe.sjs"></a>
+ <script type="application/javascript">
+ let link = document.getElementById("testlink");
+ link.click();
+ <\/script>
+ </body>
+ </html>`;
+ let NESTED_BLOB_IFRAME_NAVIGATION = new Blob([NESTED_IFRAME_NAVIGATION], {type:'text/html'});
+
+ // query the testframe and set blob URL
+ let testframe = document.getElementById("testframe");
+ testframe.src = window.URL.createObjectURL(NESTED_BLOB_IFRAME_NAVIGATION);
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/file_same_site_cookies_bug1748693.sjs b/dom/security/test/general/file_same_site_cookies_bug1748693.sjs
new file mode 100644
index 0000000000..6890bafa17
--- /dev/null
+++ b/dom/security/test/general/file_same_site_cookies_bug1748693.sjs
@@ -0,0 +1,31 @@
+const MESSAGE_PAGE = function (msg) {
+ return `
+<!DOCTYPE html>
+<html>
+ <body>
+ <p id="msg">${msg}</p>
+ <body>
+</html>
+`;
+};
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-store");
+ response.setHeader("Content-Type", "text/html");
+
+ if (request.queryString.includes("setcookies")) {
+ response.setHeader(
+ "Set-Cookie",
+ "auth_secure=foo; SameSite=None; HttpOnly; Secure",
+ true
+ );
+ response.setHeader("Set-Cookie", "auth=foo; HttpOnly;", true);
+ response.write(MESSAGE_PAGE(request.queryString));
+ return;
+ }
+
+ const cookies = request.hasHeader("Cookie")
+ ? request.getHeader("Cookie")
+ : "";
+ response.write(MESSAGE_PAGE(cookies));
+}
diff --git a/dom/security/test/general/file_same_site_cookies_cross_origin_context.sjs b/dom/security/test/general/file_same_site_cookies_cross_origin_context.sjs
new file mode 100644
index 0000000000..9103941653
--- /dev/null
+++ b/dom/security/test/general/file_same_site_cookies_cross_origin_context.sjs
@@ -0,0 +1,54 @@
+// Custom *.sjs file specifically for the needs of Bug 1452496
+
+// small red image
+const IMG_BYTES = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
+ "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="
+);
+
+const FRAME = `
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>Bug 1452496 - Do not allow same-site cookies in cross site context</title>
+ </head>
+ <body>
+ <script type="application/javascript">
+ let cookie = document.cookie;
+ // now reset the cookie for the next test
+ document.cookie = "myKey=;" + "expires=Thu, 01 Jan 1970 00:00:00 GMT";
+ window.parent.postMessage({result: cookie}, 'http://mochi.test:8888');
+ </script>
+ </body>
+ </html>`;
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ if (request.queryString.includes("setSameSiteCookie")) {
+ response.setHeader(
+ "Set-Cookie",
+ "myKey=strictSameSiteCookie; samesite=strict",
+ true
+ );
+ response.setHeader("Content-Type", "image/png");
+ response.write(IMG_BYTES);
+ return;
+ }
+
+ if (request.queryString.includes("setRegularCookie")) {
+ response.setHeader("Set-Cookie", "myKey=regularCookie;", true);
+ response.setHeader("Content-Type", "image/png");
+ response.write(IMG_BYTES);
+ return;
+ }
+
+ if (request.queryString.includes("loadFrame")) {
+ response.write(FRAME);
+ return;
+ }
+
+ // we should never get here, but just in case return something unexpected
+ response.write("D'oh");
+}
diff --git a/dom/security/test/general/file_same_site_cookies_from_script.sjs b/dom/security/test/general/file_same_site_cookies_from_script.sjs
new file mode 100644
index 0000000000..0df217cf45
--- /dev/null
+++ b/dom/security/test/general/file_same_site_cookies_from_script.sjs
@@ -0,0 +1,48 @@
+// Custom *.sjs file specifically for the needs of Bug 1452496
+
+const SET_COOKIE_FRAME = `
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>Bug 1452496 - Do not allow same-site cookies in cross site context</title>
+ </head>
+ <body>
+ <script type="application/javascript">
+ document.cookie = "myKey=sameSiteCookieInlineScript;SameSite=strict";
+ </script>
+ </body>
+ </html>`;
+
+const GET_COOKIE_FRAME = `
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>Bug 1452496 - Do not allow same-site cookies in cross site context</title>
+ </head>
+ <body>
+ <script type="application/javascript">
+ let cookie = document.cookie;
+ // now reset the cookie for the next test
+ document.cookie = "myKey=;" + "expires=Thu, 01 Jan 1970 00:00:00 GMT";
+ window.parent.postMessage({result: cookie}, 'http://mochi.test:8888');
+ </script>
+ </body>
+ </html>`;
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ if (request.queryString.includes("setSameSiteCookieUsingInlineScript")) {
+ response.write(SET_COOKIE_FRAME);
+ return;
+ }
+
+ if (request.queryString.includes("getCookieFrame")) {
+ response.write(GET_COOKIE_FRAME);
+ return;
+ }
+
+ // we should never get here, but just in case return something unexpected
+ response.write("D'oh");
+}
diff --git a/dom/security/test/general/file_same_site_cookies_iframe.sjs b/dom/security/test/general/file_same_site_cookies_iframe.sjs
new file mode 100644
index 0000000000..7b511257c3
--- /dev/null
+++ b/dom/security/test/general/file_same_site_cookies_iframe.sjs
@@ -0,0 +1,99 @@
+// Custom *.sjs file specifically for the needs of Bug 1454027
+
+// small red image
+const IMG_BYTES = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
+ "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="
+);
+
+const NESTED_IFRAME_NAVIGATION = `
+ <html>
+ <body>
+ <a id="testlink" href="http://mochi.test:8888/tests/dom/security/test/general/file_same_site_cookies_iframe.sjs"></a>
+ <script type="application/javascript">
+ let link = document.getElementById("testlink");
+ link.click();
+ <\/script>
+ </body>
+ </html>`;
+
+const NESTED_IFRAME_INCLUSION = `
+ <html>
+ <body>
+ <script type="application/javascript">
+ // simply passing on the message from the child to parent
+ window.addEventListener("message", receiveMessage);
+ function receiveMessage(event) {
+ window.removeEventListener("message", receiveMessage);
+ window.parent.postMessage({result: event.data.result}, '*');
+ }
+ <\/script>
+ <iframe src="http://mochi.test:8888/tests/dom/security/test/general/file_same_site_cookies_iframe.sjs"></iframe>
+ </body>
+ </html>`;
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ // using startsWith and discard the math random
+ if (request.queryString.startsWith("setSameSiteCookie")) {
+ response.setHeader(
+ "Set-Cookie",
+ "myKey=mySameSiteIframeTestCookie; samesite=strict",
+ true
+ );
+ response.setHeader("Content-Type", "image/png");
+ response.write(IMG_BYTES);
+ return;
+ }
+
+ // navigation tests
+ if (request.queryString === "nestedIframeNavigation") {
+ response.write(NESTED_IFRAME_NAVIGATION);
+ return;
+ }
+
+ if (request.queryString === "nestedSandboxIframeNavigation") {
+ response.setHeader(
+ "Content-Security-Policy",
+ "sandbox allow-scripts",
+ false
+ );
+ response.write(NESTED_IFRAME_NAVIGATION);
+ return;
+ }
+
+ // inclusion tests
+ if (request.queryString === "nestedIframeInclusion") {
+ response.write(NESTED_IFRAME_INCLUSION);
+ return;
+ }
+
+ if (request.queryString === "nestedSandboxIframeInclusion") {
+ response.setHeader(
+ "Content-Security-Policy",
+ "sandbox allow-scripts",
+ false
+ );
+ response.write(NESTED_IFRAME_INCLUSION);
+ return;
+ }
+
+ const cookies = request.hasHeader("Cookie")
+ ? request.getHeader("Cookie")
+ : "";
+ response.write(`
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>Bug 1454027 - Update SameSite cookie handling inside iframes</title>
+ </head>
+ <body>
+ <script type="application/javascript">
+ window.parent.postMessage({result: "${cookies}" }, '*');
+ </script>
+ </body>
+ </html>
+ `);
+}
diff --git a/dom/security/test/general/file_same_site_cookies_redirect.sjs b/dom/security/test/general/file_same_site_cookies_redirect.sjs
new file mode 100644
index 0000000000..f7451fb504
--- /dev/null
+++ b/dom/security/test/general/file_same_site_cookies_redirect.sjs
@@ -0,0 +1,103 @@
+// Custom *.sjs file specifically for the needs of Bug 1453814
+
+// small red image
+const IMG_BYTES = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
+ "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="
+);
+
+const FRAME = `
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>Bug 1453814 - Do not allow same-site cookies for cross origin redirect</title>
+ </head>
+ <body>
+ <script type="application/javascript">
+ let cookie = document.cookie;
+ // now reset the cookie for the next test
+ document.cookie = "myKey=;" + "expires=Thu, 01 Jan 1970 00:00:00 GMT";
+ window.parent.postMessage({result: cookie}, 'http://mochi.test:8888');
+ </script>
+ </body>
+ </html>`;
+
+const SAME_ORIGIN = "http://mochi.test:8888/";
+const CROSS_ORIGIN = "http://example.com/";
+const PATH =
+ "tests/dom/security/test/general/file_same_site_cookies_redirect.sjs";
+
+const FRAME_META_REFRESH_SAME =
+ `
+ <html><head>
+ <meta http-equiv="refresh" content="0;
+ url='` +
+ SAME_ORIGIN +
+ PATH +
+ `?loadFrame'">
+ </head></html>`;
+
+const FRAME_META_REFRESH_CROSS =
+ `
+ <html><head>
+ <meta http-equiv="refresh" content="0;
+ url='` +
+ CROSS_ORIGIN +
+ PATH +
+ `?loadFrame'">
+ </head></html>`;
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ if (request.queryString === "setSameSiteCookie") {
+ response.setHeader(
+ "Set-Cookie",
+ "myKey=strictSameSiteCookie; samesite=strict",
+ true
+ );
+ response.setHeader("Content-Type", "image/png");
+ response.write(IMG_BYTES);
+ return;
+ }
+
+ if (request.queryString === "sameToSameRedirect") {
+ let URL = SAME_ORIGIN + PATH + "?loadFrame";
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", URL, false);
+ return;
+ }
+
+ if (request.queryString === "sameToCrossRedirect") {
+ let URL = CROSS_ORIGIN + PATH + "?loadFrame";
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", URL, false);
+ return;
+ }
+
+ if (request.queryString === "crossToSameRedirect") {
+ let URL = SAME_ORIGIN + PATH + "?loadFrame";
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", URL, false);
+ return;
+ }
+
+ if (request.queryString === "sameToCrossRedirectMeta") {
+ response.write(FRAME_META_REFRESH_CROSS);
+ return;
+ }
+
+ if (request.queryString === "crossToSameRedirectMeta") {
+ response.write(FRAME_META_REFRESH_SAME);
+ return;
+ }
+
+ if (request.queryString === "loadFrame") {
+ response.write(FRAME);
+ return;
+ }
+
+ // we should never get here, but just in case return something unexpected
+ response.write("D'oh");
+}
diff --git a/dom/security/test/general/file_same_site_cookies_subrequest.sjs b/dom/security/test/general/file_same_site_cookies_subrequest.sjs
new file mode 100644
index 0000000000..fdc81344ef
--- /dev/null
+++ b/dom/security/test/general/file_same_site_cookies_subrequest.sjs
@@ -0,0 +1,82 @@
+// Custom *.sjs file specifically for the needs of Bug 1286861
+
+// small red image
+const IMG_BYTES = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
+ "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="
+);
+
+const FRAME = `
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>Bug 1286861 - Add support for same site cookies</title>
+ </head>
+ <body>
+ <img src = "http://mochi.test:8888/tests/dom/security/test/general/file_same_site_cookies_subrequest.sjs?checkCookie">
+ </body>
+ </html>`;
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ if (request.queryString.includes("setStrictSameSiteCookie")) {
+ response.setHeader(
+ "Set-Cookie",
+ "myKey=strictSameSiteCookie; samesite=strict",
+ true
+ );
+ response.setHeader("Content-Type", "image/png");
+ response.write(IMG_BYTES);
+ return;
+ }
+
+ if (request.queryString.includes("setLaxSameSiteCookie")) {
+ response.setHeader(
+ "Set-Cookie",
+ "myKey=laxSameSiteCookie; samesite=lax",
+ true
+ );
+ response.setHeader("Content-Type", "image/png");
+ response.write(IMG_BYTES);
+ return;
+ }
+
+ // save the object state of the initial request, which returns
+ // async once the server has processed the img request.
+ if (request.queryString.includes("queryresult")) {
+ response.processAsync();
+ setObjectState("queryResult", response);
+ return;
+ }
+
+ if (request.queryString.includes("loadFrame")) {
+ response.write(FRAME);
+ return;
+ }
+
+ if (request.queryString.includes("checkCookie")) {
+ var cookie = "unitialized";
+ if (request.hasHeader("Cookie")) {
+ cookie = request.getHeader("Cookie");
+ } else {
+ cookie = "myKey=noCookie";
+ }
+ response.setHeader("Content-Type", "image/png");
+ response.write(IMG_BYTES);
+
+ // return the result
+ getObjectState("queryResult", function (queryResponse) {
+ if (!queryResponse) {
+ return;
+ }
+ queryResponse.write(cookie);
+ queryResponse.finish();
+ });
+ return;
+ }
+
+ // we should never get here, but just in case return something unexpected
+ response.write("D'oh");
+}
diff --git a/dom/security/test/general/file_same_site_cookies_toplevel_nav.sjs b/dom/security/test/general/file_same_site_cookies_toplevel_nav.sjs
new file mode 100644
index 0000000000..45b515a28b
--- /dev/null
+++ b/dom/security/test/general/file_same_site_cookies_toplevel_nav.sjs
@@ -0,0 +1,96 @@
+// Custom *.sjs file specifically for the needs of Bug 1286861
+
+// small red image
+const IMG_BYTES = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
+ "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="
+);
+
+const FRAME = `
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>Bug 1286861 - Add support for same site cookies</title>
+ </head>
+ <body>
+ <script type="application/javascript">
+ let myWin = window.open("http://mochi.test:8888/tests/dom/security/test/general/file_same_site_cookies_toplevel_nav.sjs?loadWin");
+ </script>
+ </body>
+ </html>`;
+
+const WIN = `
+ <!DOCTYPE html>
+ <html>
+ <body>
+ just a dummy window
+ <script>
+ window.addEventListener("load",()=>{
+ window.close();
+ });
+ </script>
+ </body>
+ </html>`;
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ if (request.queryString.includes("setStrictSameSiteCookie")) {
+ response.setHeader(
+ "Set-Cookie",
+ "myKey=strictSameSiteCookie; samesite=strict",
+ true
+ );
+ response.setHeader("Content-Type", "image/png");
+ response.write(IMG_BYTES);
+ return;
+ }
+
+ if (request.queryString.includes("setLaxSameSiteCookie")) {
+ response.setHeader(
+ "Set-Cookie",
+ "myKey=laxSameSiteCookie; samesite=lax",
+ true
+ );
+ response.setHeader("Content-Type", "image/png");
+ response.write(IMG_BYTES);
+ return;
+ }
+
+ // save the object state of the initial request, which returns
+ // async once the server has processed the img request.
+ if (request.queryString.includes("queryresult")) {
+ response.processAsync();
+ setObjectState("queryResult", response);
+ return;
+ }
+
+ if (request.queryString.includes("loadFrame")) {
+ response.write(FRAME);
+ return;
+ }
+
+ if (request.queryString.includes("loadWin")) {
+ var cookie = "unitialized";
+ if (request.hasHeader("Cookie")) {
+ cookie = request.getHeader("Cookie");
+ } else {
+ cookie = "myKey=noCookie";
+ }
+ response.write(WIN);
+
+ // return the result
+ getObjectState("queryResult", function (queryResponse) {
+ if (!queryResponse) {
+ return;
+ }
+ queryResponse.write(cookie);
+ queryResponse.finish();
+ });
+ return;
+ }
+
+ // we should never get here, but just in case return something unexpected
+ response.write("D'oh");
+}
diff --git a/dom/security/test/general/file_same_site_cookies_toplevel_set_cookie.sjs b/dom/security/test/general/file_same_site_cookies_toplevel_set_cookie.sjs
new file mode 100644
index 0000000000..34dfe40e23
--- /dev/null
+++ b/dom/security/test/general/file_same_site_cookies_toplevel_set_cookie.sjs
@@ -0,0 +1,68 @@
+// Custom *.sjs file specifically for the needs of Bug 1454242
+
+const WIN = `
+ <html>
+ <body>
+ <script type="application/javascript">
+ let newWin = window.open("http://mochi.test:8888/tests/dom/security/test/general/file_same_site_cookies_toplevel_set_cookie.sjs?loadWinAndSetCookie");
+ newWin.onload = function() {
+ newWin.close();
+ }
+ </script>
+ </body>
+ </html>`;
+
+const DUMMY_WIN = `
+ <html>
+ <body>
+ just a dummy window that sets a same-site=lax cookie
+ <script type="application/javascript">
+ window.opener.opener.postMessage({value: 'testSetupComplete'}, '*');
+ </script>
+ </body>
+ </html>`;
+
+const FRAME = `
+ <html>
+ <body>
+ <script type="application/javascript">
+ let cookie = document.cookie;
+ // now reset the cookie for the next test
+ document.cookie = "myKey=;" + "expires=Thu, 01 Jan 1970 00:00:00 GMT";
+ window.parent.postMessage({value: cookie}, 'http://mochi.test:8888');
+ </script>
+ </body>
+ </html>`;
+
+const SAME_ORIGIN = "http://mochi.test:8888/";
+const CROSS_ORIGIN = "http://example.com/";
+const PATH =
+ "tests/dom/security/test/general/file_same_site_cookies_redirect.sjs";
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ if (request.queryString === "loadWin") {
+ response.write(WIN);
+ return;
+ }
+
+ if (request.queryString === "loadWinAndSetCookie") {
+ response.setHeader(
+ "Set-Cookie",
+ "myKey=laxSameSiteCookie; samesite=lax",
+ true
+ );
+ response.write(DUMMY_WIN);
+ return;
+ }
+
+ if (request.queryString === "checkCookie") {
+ response.write(FRAME);
+ return;
+ }
+
+ // we should never get here, but just in case return something unexpected
+ response.write("D'oh");
+}
diff --git a/dom/security/test/general/file_script.js b/dom/security/test/general/file_script.js
new file mode 100644
index 0000000000..c339e45d5d
--- /dev/null
+++ b/dom/security/test/general/file_script.js
@@ -0,0 +1 @@
+window.counter++;
diff --git a/dom/security/test/general/file_toplevel_data_meta_redirect.html b/dom/security/test/general/file_toplevel_data_meta_redirect.html
new file mode 100644
index 0000000000..e94a61ed48
--- /dev/null
+++ b/dom/security/test/general/file_toplevel_data_meta_redirect.html
@@ -0,0 +1,10 @@
+<html>
+<body>
+<head>
+ <meta http-equiv="refresh"
+ content="0; url='data:text/html,<body>toplevel meta redirect to data: URI should be blocked</body>'">
+</head>
+<body>
+Meta Redirect to data: URI
+</body>
+</html>
diff --git a/dom/security/test/general/file_toplevel_data_navigations.sjs b/dom/security/test/general/file_toplevel_data_navigations.sjs
new file mode 100644
index 0000000000..57c4b527dd
--- /dev/null
+++ b/dom/security/test/general/file_toplevel_data_navigations.sjs
@@ -0,0 +1,13 @@
+// Custom *.sjs file specifically for the needs of Bug:
+// Bug 1394554 - Block toplevel data: URI navigations after redirect
+
+var DATA_URI =
+ "data:text/html,<body>toplevel data: URI navigations after redirect should be blocked</body>";
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", DATA_URI, false);
+}
diff --git a/dom/security/test/general/file_view_bg_image_data_navigation.html b/dom/security/test/general/file_view_bg_image_data_navigation.html
new file mode 100644
index 0000000000..d9aa6ca8b6
--- /dev/null
+++ b/dom/security/test/general/file_view_bg_image_data_navigation.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1658244: Test navigation for right-click view-bg-image on data:image/svg</title>
+<style>
+body {
+ background: fixed #222 url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjUwMCIgd2lkdGg9IjUwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGZpbHRlciBpZD0iYSI+PGZlVHVyYnVsZW5jZSBiYXNlRnJlcXVlbmN5PSIuOSIgbnVtT2N0YXZlcz0iMTAiIHN0aXRjaFRpbGVzPSJzdGl0Y2giIHR5cGU9ImZyYWN0YWxOb2lzZSIvPjwvZmlsdGVyPjxwYXRoIGQ9Im0wIDBoNTAwdjUwMGgtNTAweiIgZmlsbD0iIzExMSIvPjxwYXRoIGQ9Im0wIDBoNTAwdjUwMGgtNTAweiIgZmlsdGVyPSJ1cmwoI2EpIiBvcGFjaXR5PSIuMiIvPjwvc3ZnPgo=");
+ color: #ccc;
+}
+</style>
+</head>
+<body id="testbody">
+ This page has an inline SVG image as a background.
+</body>
+</html>
diff --git a/dom/security/test/general/file_view_image_data_navigation.html b/dom/security/test/general/file_view_image_data_navigation.html
new file mode 100644
index 0000000000..a3f9acfb4d
--- /dev/null
+++ b/dom/security/test/general/file_view_image_data_navigation.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1407891: Test navigation for right-click view-image on data:image/svg</title>
+</head>
+<body>
+
+<img id="testimage" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij4KICA8cGF0aCBkPSJNOCwxMkwzLDcsNCw2bDQsNCw0LTQsMSwxWiIgZmlsbD0iIzZBNkE2QSIgLz4KPC9zdmc+Cg=="></img>
+
+</body>
+</html>
diff --git a/dom/security/test/general/file_xfo_error_page.sjs b/dom/security/test/general/file_xfo_error_page.sjs
new file mode 100644
index 0000000000..b1fa33cbd4
--- /dev/null
+++ b/dom/security/test/general/file_xfo_error_page.sjs
@@ -0,0 +1,8 @@
+"use strict";
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("x-frame-options", "deny", false);
+ response.write("<html>xfo test loaded</html>");
+}
diff --git a/dom/security/test/general/mochitest.toml b/dom/security/test/general/mochitest.toml
new file mode 100644
index 0000000000..c46b5ecf57
--- /dev/null
+++ b/dom/security/test/general/mochitest.toml
@@ -0,0 +1,148 @@
+[DEFAULT]
+support-files = [
+ "file_contentpolicytype_targeted_link_iframe.sjs",
+ "file_nosniff_testserver.sjs",
+ "file_nosniff_navigation.sjs",
+ "file_block_script_wrong_mime_server.sjs",
+ "file_block_toplevel_data_navigation.html",
+ "file_block_toplevel_data_navigation2.html",
+ "file_block_toplevel_data_navigation3.html",
+ "file_block_toplevel_data_redirect.sjs",
+ "file_block_subresource_redir_to_data.sjs",
+ "file_same_site_cookies_subrequest.sjs",
+ "file_same_site_cookies_toplevel_nav.sjs",
+ "file_same_site_cookies_cross_origin_context.sjs",
+ "file_same_site_cookies_from_script.sjs",
+ "file_same_site_cookies_redirect.sjs",
+ "file_same_site_cookies_toplevel_set_cookie.sjs",
+ "file_same_site_cookies_blob_iframe_navigation.html",
+ "file_same_site_cookies_blob_iframe_inclusion.html",
+ "file_same_site_cookies_iframe.sjs",
+ "file_same_site_cookies_about.sjs",
+ "file_cache_splitting_server.sjs",
+ "file_cache_splitting_isloaded.sjs",
+ "file_cache_splitting_window.html",
+ "window_nosniff_navigation.html",
+]
+
+["test_allow_opening_data_json.html"]
+
+["test_allow_opening_data_pdf.html"]
+skip-if = ["os == 'android'"] # no pdf reader on Android
+
+["test_assert_about_page_no_csp.html"]
+skip-if = ["!debug"]
+
+["test_block_script_wrong_mime.html"]
+
+["test_block_subresource_redir_to_data.html"]
+
+["test_block_toplevel_data_img_navigation.html"]
+
+["test_block_toplevel_data_navigation.html"]
+
+["test_bug1450853.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug1660452_http.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug1660452_https.html"]
+scheme = "https"
+
+["test_cache_split.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_contentpolicytype_targeted_link_iframe.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_gpc.html"]
+support-files = ["file_gpc_server.sjs"]
+
+["test_meta_referrer.html"]
+support-files = [
+ "file_meta_referrer_in_head.html",
+ "file_meta_referrer_notin_head.html",
+]
+
+["test_nosniff.html"]
+
+["test_nosniff_navigation.html"]
+
+["test_same_site_cookies_about.html"]
+fail-if = ["xorigin"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_same_site_cookies_cross_origin_context.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_same_site_cookies_from_script.html"]
+fail-if = ["xorigin"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_same_site_cookies_iframe.html"]
+fail-if = ["xorigin"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_same_site_cookies_laxByDefault.html"]
+skip-if = ["debug"]
+support-files = ["closeWindow.sjs"]
+
+["test_same_site_cookies_redirect.html"]
+fail-if = ["xorigin"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_same_site_cookies_subrequest.html"]
+fail-if = ["xorigin"] # Cookies set incorrectly
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_same_site_cookies_toplevel_nav.html"]
+fail-if = ["xorigin"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_same_site_cookies_toplevel_set_cookie.html"]
+fail-if = ["xorigin"] # Cookies not set
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_xfo_error_page.html"]
+support-files = ["file_xfo_error_page.sjs"]
+skip-if = [
+ "http3",
+ "http2",
+]
diff --git a/dom/security/test/general/test_allow_opening_data_json.html b/dom/security/test/general/test_allow_opening_data_json.html
new file mode 100644
index 0000000000..4b37931e1f
--- /dev/null
+++ b/dom/security/test/general/test_allow_opening_data_json.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1403814: Allow toplevel data URI navigation data:application/json</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+function test_toplevel_data_json() {
+ const DATA_JSON = "data:application/json,{'my_json_key':'my_json_value'}";
+
+ let win = window.open(DATA_JSON);
+ let wrappedWin = SpecialPowers.wrap(win);
+
+ // Unfortunately we can't detect whether the JSON has loaded or not using some
+ // event, hence we are constantly polling location.href till we see that
+ // the data: URI appears. Test times out on failure.
+ var jsonLoaded = setInterval(function() {
+ if (wrappedWin.document.location.href.startsWith("data:application/json")) {
+ clearInterval(jsonLoaded);
+ ok(true, "navigating to data:application/json allowed");
+ wrappedWin.close();
+ SimpleTest.finish();
+ }
+ }, 200);
+}
+
+SpecialPowers.pushPrefEnv({
+ set: [["security.data_uri.block_toplevel_data_uri_navigations", true]]
+}, test_toplevel_data_json);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/test_allow_opening_data_pdf.html b/dom/security/test/general/test_allow_opening_data_pdf.html
new file mode 100644
index 0000000000..007b3e8801
--- /dev/null
+++ b/dom/security/test/general/test_allow_opening_data_pdf.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1398692: Allow toplevel navigation to a data:application/pdf</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+function test_toplevel_data_pdf() {
+ // The PDF contains one page and it is a 3/72" square, the minimum allowed by the spec
+ const DATA_PDF =
+ "data:application/pdf;base64,JVBERi0xLjANCjEgMCBvYmo8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFI+PmVuZG9iaiAyIDAgb2JqPDwvVHlwZS9QYWdlcy9LaWRzWzMgMCBSXS9Db3VudCAxPj5lbmRvYmogMyAwIG9iajw8L1R5cGUvUGFnZS9NZWRpYUJveFswIDAgMyAzXT4+ZW5kb2JqDQp4cmVmDQowIDQNCjAwMDAwMDAwMDAgNjU1MzUgZg0KMDAwMDAwMDAxMCAwMDAwMCBuDQowMDAwMDAwMDUzIDAwMDAwIG4NCjAwMDAwMDAxMDIgMDAwMDAgbg0KdHJhaWxlcjw8L1NpemUgNC9Sb290IDEgMCBSPj4NCnN0YXJ0eHJlZg0KMTQ5DQolRU9G";
+
+ let win = window.open(DATA_PDF);
+ let wrappedWin = SpecialPowers.wrap(win);
+
+ // Unfortunately we can't detect whether the PDF has loaded or not using some
+ // event, hence we are constantly polling location.href till we see that
+ // the data: URI appears. Test times out on failure.
+ var pdfLoaded = setInterval(function() {
+ if (wrappedWin.document.location.href.startsWith("data:application/pdf")) {
+ clearInterval(pdfLoaded);
+ ok(true, "navigating to data:application/pdf allowed");
+ wrappedWin.close();
+ SimpleTest.finish();
+ }
+ }, 200);
+}
+
+SpecialPowers.pushPrefEnv({
+ set: [["security.data_uri.block_toplevel_data_uri_navigations", true]]
+}, test_toplevel_data_pdf);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/test_assert_about_page_no_csp.html b/dom/security/test/general/test_assert_about_page_no_csp.html
new file mode 100644
index 0000000000..06be4ce460
--- /dev/null
+++ b/dom/security/test/general/test_assert_about_page_no_csp.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1490977: Test Assertion if content privileged about: page has no CSP</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe id="testframe"></iframe>
+<script class="testbody" type="text/javascript">
+
+ // Test Setup: The test overrules the allowlist of about: pages that are allowed to load
+ // without a CSP and makes sure to hit the assertion within AssertAboutPageHasCSP().
+
+ SpecialPowers.setBoolPref("dom.security.skip_about_page_csp_allowlist_and_assert", true);
+
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.expectAssertions(0, 1);
+
+ ok(true, "sanity: prefs flipped and test runs");
+ let myFrame = document.getElementById("testframe");
+ myFrame.src = "about:blank";
+ // booom :-)
+
+ SpecialPowers.setBoolPref("dom.security.skip_about_page_csp_allowlist_and_assert", false);
+ SimpleTest.finish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/general/test_block_script_wrong_mime.html b/dom/security/test/general/test_block_script_wrong_mime.html
new file mode 100644
index 0000000000..93a4b9d220
--- /dev/null
+++ b/dom/security/test/general/test_block_script_wrong_mime.html
@@ -0,0 +1,92 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1288361 - Block scripts with incorrect MIME type</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script class="testbody" type="text/javascript">
+
+const MIMETypes = [
+ ["application/javascript", true],
+ ["text/javascript", true],
+
+ ["audio/mpeg", false],
+ ["audio/", false],
+ ["image/jpeg", false],
+ ["image/", false],
+ ["video/mpeg", false],
+ ["video/", false],
+ ["text/csv", false],
+];
+
+// <script src="">
+function testScript([mime, shouldLoad]) {
+ return new Promise((resolve, reject) => {
+ let script = document.createElement("script");
+ script.onload = () => {
+ document.body.removeChild(script);
+ ok(shouldLoad, `script with mime '${mime}' should load`);
+ resolve();
+ };
+ script.onerror = () => {
+ document.body.removeChild(script);
+ ok(!shouldLoad, `script with wrong mime '${mime}' should be blocked`);
+ resolve();
+ };
+ script.src = "file_block_script_wrong_mime_server.sjs?type=script&mime="+mime;
+ document.body.appendChild(script);
+ });
+}
+
+// new Worker()
+function testWorker([mime, shouldLoad]) {
+ return new Promise((resolve, reject) => {
+ let worker = new Worker("file_block_script_wrong_mime_server.sjs?type=worker&mime="+mime);
+ worker.onmessage = (event) => {
+ ok(shouldLoad, `worker with mime '${mime}' should load`)
+ is(event.data, "worker-loaded", "worker should send correct message");
+ resolve();
+ };
+ worker.onerror = (error) => {
+ ok(!shouldLoad, `worker with wrong mime '${mime}' should be blocked`);
+ error.preventDefault();
+ resolve();
+ }
+ worker.postMessage("dummy");
+ });
+}
+
+// new Worker() with importScripts()
+function testWorkerImportScripts([mime, shouldLoad]) {
+ return new Promise((resolve, reject) => {
+ let worker = new Worker("file_block_script_wrong_mime_server.sjs?type=worker-import&mime="+mime);
+ worker.onmessage = (event) => {
+ ok(shouldLoad, `worker/importScripts with mime '${mime}' should load`)
+ is(event.data, "worker-loaded", "worker should send correct message");
+ resolve();
+ };
+ worker.onerror = (error) => {
+ ok(!shouldLoad, `worker/importScripts with wrong mime '${mime}' should be blocked`);
+ error.preventDefault();
+ resolve();
+ }
+ worker.postMessage("dummy");
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+Promise.all(MIMETypes.map(testScript)).then(() => {
+ return Promise.all(MIMETypes.map(testWorker));
+}).then(() => {
+ return Promise.all(MIMETypes.map(testWorkerImportScripts));
+}).then(() => {
+ return SpecialPowers.popPrefEnv();
+}).then(SimpleTest.finish);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/test_block_subresource_redir_to_data.html b/dom/security/test/general/test_block_subresource_redir_to_data.html
new file mode 100644
index 0000000000..21a85515ec
--- /dev/null
+++ b/dom/security/test/general/test_block_subresource_redir_to_data.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1428793: Block insecure redirects to data: URIs</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script id="testScriptRedirectToData"></script>
+<script id="testModuleScriptRedirectToData" type="module"></script>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+const NUM_TESTS = 3;
+
+var testCounter = 0;
+function checkFinish() {
+ testCounter++;
+ if (testCounter === NUM_TESTS) {
+ SimpleTest.finish();
+ }
+}
+
+// --- test regular scripts
+let testScriptRedirectToData = document.getElementById("testScriptRedirectToData");
+testScriptRedirectToData.onerror = function() {
+ ok(true, "script that redirects to data: URI should not load");
+ checkFinish();
+}
+testScriptRedirectToData.onload = function() {
+ ok(false, "script that redirects to data: URI should not load");
+ checkFinish();
+}
+testScriptRedirectToData.src = "file_block_subresource_redir_to_data.sjs?script";
+
+// --- test workers
+let worker = new Worker("file_block_subresource_redir_to_data.sjs?worker");
+worker.onerror = function() {
+ // please note that workers need to be same origin, hence the data: URI
+ // redirect is blocked by worker code and not the content security manager!
+ ok(true, "worker script that redirects to data: URI should not load");
+ checkFinish();
+}
+worker.onmessage = function() {
+ ok(false, "worker script that redirects to data: URI should not load");
+ checkFinish();
+};
+worker.postMessage("dummy");
+
+// --- test script modules
+ let testModuleScriptRedirectToData = document.getElementById("testModuleScriptRedirectToData");
+ testModuleScriptRedirectToData.onerror = function() {
+ ok(true, "module script that redirects to data: URI should not load");
+ checkFinish();
+ }
+ testModuleScriptRedirectToData.onload = function() {
+ ok(false, "module script that redirects to data: URI should not load");
+ checkFinish();
+ }
+ testModuleScriptRedirectToData.src = "file_block_subresource_redir_to_data.sjs?modulescript";
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/test_block_toplevel_data_img_navigation.html b/dom/security/test/general/test_block_toplevel_data_img_navigation.html
new file mode 100644
index 0000000000..07e46b1f2f
--- /dev/null
+++ b/dom/security/test/general/test_block_toplevel_data_img_navigation.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1396798: Do not block toplevel data: navigation to image (except svgs)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+SpecialPowers.setBoolPref("security.data_uri.block_toplevel_data_uri_navigations", true);
+SimpleTest.registerCleanupFunction(() => {
+ SpecialPowers.clearUserPref("security.data_uri.block_toplevel_data_uri_navigations");
+});
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("have to test that top level data:image loading is blocked/allowed");
+
+function test_toplevel_data_image() {
+ const DATA_PNG =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+ let win1 = window.open(DATA_PNG);
+ let wrappedWin1 = SpecialPowers.wrap(win1);
+ setTimeout(function () {
+ let images = wrappedWin1.document.getElementsByTagName('img');
+ is(images.length, 1, "Loading data:image/png should be allowed");
+ is(images[0].src, DATA_PNG, "Sanity: img src matches");
+ wrappedWin1.close();
+ test_toplevel_data_image_svg();
+ }, 1000);
+}
+
+function test_toplevel_data_image_svg() {
+ const DATA_SVG =
+ "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij4KICA8cGF0aCBkPSJNOCwxMkwzLDcsNCw2bDQsNCw0LTQsMSwxWiIgZmlsbD0iIzZBNkE2QSIgLz4KPC9zdmc+Cg==";
+ let win2 = window.open(DATA_SVG);
+ // Unfortunately we can't detect whether the window was closed using some event,
+ // hence we are constantly polling till we see that win == null.
+ // Test times out on failure.
+ var win2Closed = setInterval(function() {
+ if (win2 == null || win2.closed) {
+ clearInterval(win2Closed);
+ ok(true, "Loading data:image/svg+xml should be blocked");
+ SimpleTest.finish();
+ }
+ }, 200);
+}
+// fire up the tests
+test_toplevel_data_image();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/test_block_toplevel_data_navigation.html b/dom/security/test/general/test_block_toplevel_data_navigation.html
new file mode 100644
index 0000000000..bbadacb218
--- /dev/null
+++ b/dom/security/test/general/test_block_toplevel_data_navigation.html
@@ -0,0 +1,134 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1331351 - Block top level window data: URI navigations</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+
+async function expectBlockedToplevelData() {
+ await SpecialPowers.spawnChrome([], async () => {
+ let progressListener;
+ let bid = await new Promise(resolve => {
+ let bcs = [];
+ progressListener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener", "nsISupportsWeakReference"]),
+ onStateChange(webProgress, request, stateFlags, status) {
+ if (!(request instanceof Ci.nsIChannel) || !webProgress.isTopLevel ||
+ !(stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) ||
+ !(stateFlags & Ci.nsIWebProgressListener.STATE_STOP)) {
+ return;
+ }
+
+ if (!["NS_ERROR_DOM_BAD_URI", "NS_ERROR_CORRUPTED_CONTENT"].includes(ChromeUtils.getXPCOMErrorName(status))) {
+ info(ChromeUtils.getXPCOMErrorName(status));
+ isnot(request.URI.scheme, "data");
+ return;
+ }
+
+ // We can't check for the scheme to be "data" because in the case of a
+ // redirected load, we'll get a `NS_ERROR_DOM_BAD_URI` load error
+ // before observing the redirect, cancelling the load. Instead we just
+ // wait for any load to error with `NS_ERROR_DOM_BAD_URI`.
+ for (let bc of bcs) {
+ try {
+ bc.webProgress.removeProgressListener(progressListener);
+ } catch(e) { }
+ }
+ bcs = [];
+ Services.obs.removeObserver(observer, "browsing-context-attached");
+ resolve(webProgress.browsingContext.browserId);
+ }
+ };
+
+ function observer(subject, topic) {
+ if (!bcs.includes(subject.webProgress)) {
+ bcs.push(subject.webProgress);
+ subject.webProgress.addProgressListener(progressListener, Ci.nsIWebProgress.NOTIFY_ALL);
+ }
+ }
+ Services.obs.addObserver(observer, "browsing-context-attached");
+ });
+ return bid;
+ });
+}
+
+async function expectBlockedURIWarning() {
+ await SpecialPowers.spawnChrome([], async () => {
+ return new Promise(resolve => {
+ Services.console.registerListener(function onConsoleMessage(msg) {
+ info("Seeing console message: " + msg.message);
+ if (!(msg instanceof Ci.nsIScriptError)) {
+ return;
+ }
+ if (msg.category != "DATA_URI_BLOCKED") {
+ return;
+ }
+
+ Services.console.unregisterListener(onConsoleMessage);
+ resolve();
+ });
+ });
+ });
+}
+
+async function expectBrowserDiscarded(browserId) {
+ await SpecialPowers.spawnChrome([browserId], async (browserId) => {
+ return new Promise(resolve => {
+ function check() {
+ if (!BrowsingContext.getCurrentTopByBrowserId(browserId)) {
+ ok(true, `BrowserID ${browserId} discarded`);
+ resolve();
+ Services.obs.removeObserver(check, "browsing-context-discarded");
+ }
+ }
+ Services.obs.addObserver(check, "browsing-context-discarded");
+ check();
+ });
+ });
+}
+
+async function popupTest(uri, expectClose) {
+ info(`Running expect blocked test for ${uri}`);
+ let reqBlockedPromise = expectBlockedToplevelData();
+ let warningPromise = expectBlockedURIWarning();
+ let win = window.open(uri);
+ let browserId = await reqBlockedPromise;
+ await warningPromise;
+ if (expectClose) {
+ await expectBrowserDiscarded(browserId);
+ }
+ win.close();
+}
+
+add_task(async function() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.data_uri.block_toplevel_data_uri_navigations", true]],
+ });
+
+ // simple data: URI click navigation should be prevented
+ await popupTest("file_block_toplevel_data_navigation.html", false);
+
+ // data: URI in iframe which opens data: URI in _blank should be blocked
+ await popupTest("file_block_toplevel_data_navigation2.html", false);
+
+ // navigating to a data: URI using window.location.href should be blocked
+ await popupTest("file_block_toplevel_data_navigation3.html", false);
+
+ // navigating to a data: URI using window.open() should be blocked
+ await popupTest("data:text/html,<body>toplevel data: URI navigations should be blocked</body>", false);
+
+ // navigating to a URI which redirects to a data: URI using window.open() should be blocked
+ await popupTest("file_block_toplevel_data_redirect.sjs", false);
+
+ // navigating to a data: URI without a Content Type should be blocked
+ await popupTest("data:,DataURIsWithNoContentTypeShouldBeBlocked", false);
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/test_bug1277803.xhtml b/dom/security/test/general/test_bug1277803.xhtml
new file mode 100644
index 0000000000..30cc82310b
--- /dev/null
+++ b/dom/security/test/general/test_bug1277803.xhtml
@@ -0,0 +1,65 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Bug 1277803 test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="600"
+ height="600"
+ onload="runTest();">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ </body>
+
+ <script type="application/javascript"><![CDATA[
+ SimpleTest.requestCompleteLog();
+
+ const BASE_URI = "http://mochi.test:8888/chrome/dom/security/test/general/";
+ const FAVICON_URI = BASE_URI + "favicon_bug1277803.ico";
+ const LOADING_URI = BASE_URI + "bug1277803.html";
+ let testWindow; //will be used to trigger favicon load
+
+ let expectedPrincipal = Services.scriptSecurityManager
+ .createContentPrincipal(Services.io.newURI(LOADING_URI), {});
+ let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+
+ function runTest() {
+ // Register our observer to intercept favicon requests.
+ function observer(aSubject, aTopic, aData) {
+ // Make sure this is a favicon request.
+ let httpChannel = aSubject.QueryInterface(Ci.nsIHttpChannel);
+ if (FAVICON_URI != httpChannel.URI.spec) {
+ return;
+ }
+
+ // Ensure the topic is the one we set an observer for.
+ is(aTopic, "http-on-modify-request", "Expected observer topic");
+
+ // Check for the correct loadingPrincipal, triggeringPrincipal.
+ let triggeringPrincipal = httpChannel.loadInfo.triggeringPrincipal;
+ let loadingPrincipal = httpChannel.loadInfo.loadingPrincipal;
+
+ ok(loadingPrincipal.equals(expectedPrincipal), "Should be loading with the expected principal.");
+ ok(triggeringPrincipal.equals(expectedPrincipal), "Should be triggered with the expected principal.");
+
+ Services.obs.removeObserver(this, "http-on-modify-request");
+ SimpleTest.finish();
+ }
+ Services.obs.addObserver(observer, "http-on-modify-request");
+
+ // Now that the observer is set up, trigger a favicon load with navigation
+ testWindow = window.open(LOADING_URI);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.registerCleanupFunction(function() {
+ if (testWindow) {
+ testWindow.close();
+ }
+ });
+ ]]></script>
+
+ <browser type="content" primary="true" flex="1" id="content" src="about:blank"/>
+</window>
diff --git a/dom/security/test/general/test_bug1450853.html b/dom/security/test/general/test_bug1450853.html
new file mode 100644
index 0000000000..e6b61ecce0
--- /dev/null
+++ b/dom/security/test/general/test_bug1450853.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1450853
+-->
+<head>
+<meta charset="utf-8">
+<title>Test for Cross-origin resouce status leak via MediaError</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/ChromeTask.js"></script>
+<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+
+<audio autoplay id="audio"></audio>
+
+<script type="application/javascript">
+
+/** Test for Bug 1450853 **/
+var CONST_GENERIC_ERROR_MESSAGE = "Failed to open media";
+
+add_task(function() {
+ return new Promise((resolve) => {
+ let audioElement = document.getElementById("audio");
+
+ audioElement.onerror = function() {
+ let err = this.error;
+ let message = err.message;
+
+ info(`Got Audio Error -> ${message}`);
+ ok(message.includes("404"), "Same-Origin Error Message should contain status data");
+ resolve();
+ };
+ audioElement.src = "http://mochi.test:8888/media/test.mp3";
+ });
+});
+
+add_task(function() {
+ return new Promise((resolve) => {
+ let audioElement = document.getElementById("audio");
+
+ audioElement.onerror = function() {
+ let err = this.error;
+ let message = err.message;
+
+ info(`Got Audio Error -> ${message}`);
+ is(message, CONST_GENERIC_ERROR_MESSAGE, "Cross-Origin Same-Site Error Message should be generic");
+ resolve();
+ };
+ audioElement.src = "http://mochi.test:9999/media/test.mp3";
+ });
+});
+
+add_task(function() {
+ return new Promise((resolve) => {
+ let audioElement = document.getElementById("audio");
+
+ audioElement.onerror = function() {
+ let err = this.error;
+ let message = err.message;
+
+ info(`Got Audio Error -> ${message}`);
+ is(message, CONST_GENERIC_ERROR_MESSAGE, "Cross-Origin Same-Site Error Message should be generic");
+ resolve();
+ };
+ audioElement.src = "http://sub.mochi.test:8888/media/test.mp3";
+ });
+});
+
+add_task(function() {
+ return new Promise((resolve) => {
+ let audioElement = document.getElementById("audio");
+
+ audioElement.onerror = function() {
+ let err = this.error;
+ let message = err.message;
+
+ info(`Got Audio Error -> ${message}`);
+ is(message, CONST_GENERIC_ERROR_MESSAGE, "Cross-Origin Error Message should be generic");
+ resolve();
+ };
+ audioElement.src = "https://example.com/media/test.mp3";
+ });
+});
+
+</script>
+</head>
+
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1450853">Mozilla Bug 1450853</a>
+ <iframe width="0" height="0"></iframe>
+ </body>
+</html>
diff --git a/dom/security/test/general/test_bug1660452_http.html b/dom/security/test/general/test_bug1660452_http.html
new file mode 100644
index 0000000000..3a6512da21
--- /dev/null
+++ b/dom/security/test/general/test_bug1660452_http.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Bug 1660452: NullPrincipals need to know whether they were spun off of a Secure Context</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+ok(!window.isSecureContext, "top level should not be a secure context");
+
+// eslint-disable-next-line
+let newWin = window.open("data:text/html,<script><"+"/script>");
+ok(!newWin.isSecureContext, "data uri window should not be a secure context");
+newWin.close();
+
+window.addEventListener("message", (event) => {
+ ok(!event.data.isSecureContext, "data uri frames should not be a secure context");
+ if(event.data.finish) {
+ SimpleTest.finish();
+ return;
+ }
+ let f2 = document.createElement("iframe");
+ // eslint-disable-next-line
+ f2.src = "data:text/html,<iframe src=\"data:text/html,<script>parent.parent.postMessage({isSecureContext: window.isSecureContext, finish: true}, '*');<"+"/script>\"></iframe>";
+ document.body.appendChild(f2);
+});
+
+let f = document.createElement("iframe");
+// eslint-disable-next-line
+f.src = "data:text/html,<script>parent.postMessage({isSecureContext: window.isSecureContext}, '*');<"+"/script>";
+document.body.appendChild(f);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/test_bug1660452_https.html b/dom/security/test/general/test_bug1660452_https.html
new file mode 100644
index 0000000000..1aed356a21
--- /dev/null
+++ b/dom/security/test/general/test_bug1660452_https.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Bug 1660452: NullPrincipals need to know whether they were spun off of a Secure Context</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+ok(window.isSecureContext, "top level should be a secure context");
+
+// eslint-disable-next-line
+let newWin = window.open("data:text/html,<script><"+"/script>");
+ok(newWin.isSecureContext, "data uri window should be a secure context");
+newWin.close();
+
+window.addEventListener("message", (event) => {
+ ok(event.data.isSecureContext, "data uri frames should be a secure context");
+ if(event.data.finish) {
+ SimpleTest.finish();
+ return;
+ }
+ let f2 = document.createElement("iframe");
+ // eslint-disable-next-line
+ f2.src = "data:text/html,<iframe src=\"data:text/html,<script>parent.parent.postMessage({isSecureContext: window.isSecureContext, finish: true}, '*');<"+"/script>\"></iframe>";
+ document.body.appendChild(f2);
+});
+
+let f = document.createElement("iframe");
+// eslint-disable-next-line
+f.src = "data:text/html,<script>parent.postMessage({isSecureContext: window.isSecureContext}, '*');<"+"/script>";
+document.body.appendChild(f);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/test_cache_split.html b/dom/security/test/general/test_cache_split.html
new file mode 100644
index 0000000000..f0fc056bce
--- /dev/null
+++ b/dom/security/test/general/test_cache_split.html
@@ -0,0 +1,153 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1454721 - Add same-site cookie test for about:blank and about:srcdoc</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ChromeTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+ <img id="cookieImage">
+ <script class="testbody" type="text/javascript">
+ SimpleTest.requestLongerTimeout(2);
+
+ const CROSS_ORIGIN = "http://mochi.test:8888/";
+ const SAME_ORIGIN= "https://example.com/";
+ const PATH = "file_cache_splitting_server.sjs";
+
+ async function getCount() {
+ return fetch(`${PATH}?state`).then(r => r.text());
+ }
+ async function resetCount() {
+ return fetch(`${PATH}?flush`).then(r => r.text());
+ }
+ async function ensureLoaded() {
+ // This Fetch is geting the Response "1", once file_cache_splitting_isloaded
+ // gets a request without a query String issued from the cache_splitting_window.html
+ info("Waiting for Pageload");
+ let result = await fetch("file_cache_splitting_isloaded.sjs?wait").then(r => r.text);
+ info("Page has been Loaded");
+ return result;
+ }
+
+
+ async function openAndLoadWindow(origin) {
+ let isLoaded = ensureLoaded();
+ let url = `${origin}tests/dom/security/test/general/file_cache_splitting_window.html`;
+ let w = window.open(url);
+ // let ew = SpecialPowers.wrap(w);
+ await isLoaded;
+ return w;
+ }
+
+ async function checkStep(step = [SAME_ORIGIN, 1], name) {
+ info(`Doing Step ${JSON.stringify(step)}`);
+ let url = step[0];
+ let should_count = step[1];
+ let w = await openAndLoadWindow(url);
+ let count = await getCount();
+ ok(
+ count == should_count,
+ `${name} req to: ${
+ url == SAME_ORIGIN ? "Same Origin" : "Cross Origin"
+ } expected ${should_count} request to Server, got ${count}`
+ );
+ w.close()
+ }
+ async function clearCache(){
+ info("Clearing Cache");
+ SpecialPowers.DOMWindowUtils.clearSharedStyleSheetCache();
+ await ChromeTask.spawn(null,(()=>{
+ Services.cache2.clear();
+ }));
+ }
+ async function runTest(test) {
+ info(`Starting Job with - ${test.steps.length} - Requests`);
+ await resetCount();
+ let { prefs, steps, name } = test;
+ await SpecialPowers.pushPrefEnv(prefs);
+ for (let step of steps) {
+ await checkStep(step, name);
+ }
+ await clearCache();
+ };
+
+
+ add_task(
+ async () =>
+ runTest({
+ name: `Isolated Cache`,
+ steps: [[SAME_ORIGIN, 1], [SAME_ORIGIN, 1], [CROSS_ORIGIN, 2]],
+ prefs: {
+ set: [
+ ["privacy.partition.network_state", true]
+ ],
+ },
+ })
+ );
+ // Negative Test: The CROSS_ORIGIN should be able to
+ // acess the cache of SAME_ORIGIN
+ add_task(
+ async () =>
+ runTest({
+ name: `Non Isolated Cache`,
+ steps: [[SAME_ORIGIN, 1], [SAME_ORIGIN, 1], [CROSS_ORIGIN, 1]],
+ prefs: {
+ set: [
+ ["privacy.partition.network_state", false]
+ ],
+ },
+ })
+ );
+ // Test that FPI does not affect Cache Isolation
+ add_task(
+ async () =>
+ runTest({
+ name: `FPI interaction`,
+ steps: [[SAME_ORIGIN, 1], [SAME_ORIGIN, 1], [CROSS_ORIGIN, 2]],
+ prefs: {
+ set: [
+ ["privacy.firstparty.isolate", true],
+ ["privacy.partition.network_state", false],
+ ],
+ },
+ })
+ );
+ // Test that cookieBehavior does not affect Cache Isolation
+ for (let i = 0; i < SpecialPowers.Ci.nsICookieService.BEHAVIOR_LAST ; i++) {
+ add_task(
+ async () =>
+ runTest({
+ name: `cookieBehavior interaction ${i}`,
+ steps: [[SAME_ORIGIN, 1], [SAME_ORIGIN, 1], [CROSS_ORIGIN, 2]],
+ prefs: {
+ set: [
+ ["privacy.firstparty.isolate", false],
+ ["network.cookie.cookieBehavior", i],
+ ["privacy.partition.network_state", true],
+ ],
+ },
+ })
+ );
+ }
+ add_task(
+ async () =>
+ runTest({
+ name: `FPI interaction - 2`,
+ steps: [[SAME_ORIGIN, 1], [SAME_ORIGIN, 1], [CROSS_ORIGIN, 2]],
+ prefs: {
+ set: [
+ ["privacy.firstparty.isolate", true],
+ ["privacy.partition.network_state", false],
+ ],
+ },
+ })
+ );
+
+ </script>
+</body>
+
+</html>
diff --git a/dom/security/test/general/test_contentpolicytype_targeted_link_iframe.html b/dom/security/test/general/test_contentpolicytype_targeted_link_iframe.html
new file mode 100644
index 0000000000..24ec5dbdd9
--- /dev/null
+++ b/dom/security/test/general/test_contentpolicytype_targeted_link_iframe.html
@@ -0,0 +1,103 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1255240 - Test content policy types within content policies for targeted links in iframes</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the test:
+ * Let's load a link into a targeted iframe and make sure the content policy
+ * type used for content policy checks is of TYPE_SUBDOCUMENT.
+ */
+
+function createChromeScript() {
+ /* eslint-env mozilla/chrome-script */
+ const POLICYNAME = "@mozilla.org/testpolicy;1";
+ const POLICYID = Components.ID("{6cc95ef3-40e1-4d59-87f0-86f100373227}");
+ const EXPECTED_URL =
+ "http://mochi.test:8888/tests/dom/security/test/general/file_contentpolicytype_targeted_link_iframe.sjs?innerframe";
+
+ var policy = {
+ // nsISupports implementation
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIFactory",
+ "nsIContentPolicy",
+ ]),
+
+ // nsIFactory implementation
+ createInstance(iid) {
+ return this.QueryInterface(iid);
+ },
+
+ // nsIContentPolicy implementation
+ shouldLoad(contentLocation, loadInfo) {
+ if (contentLocation.asciiSpec === EXPECTED_URL) {
+ sendAsyncMessage("loadBlocked", { policyType: loadInfo.externalContentPolicyType});
+ Services.catMan.deleteCategoryEntry(
+ "content-policy",
+ POLICYNAME,
+ false
+ );
+ componentManager.unregisterFactory(POLICYID, policy);
+ return Ci.nsIContentPolicy.REJECT_REQUEST;
+ }
+ return Ci.nsIContentPolicy.ACCEPT;
+ },
+
+ shouldProcess(contentLocation, loadInfo) {
+ return Ci.nsIContentPolicy.ACCEPT;
+ }
+ };
+
+ // Register content policy
+ var componentManager = Components.manager.QueryInterface(
+ Ci.nsIComponentRegistrar
+ );
+
+ componentManager.registerFactory(
+ POLICYID,
+ "Test content policy",
+ POLICYNAME,
+ policy
+ );
+ Services.catMan.addCategoryEntry(
+ "content-policy",
+ POLICYNAME,
+ POLICYNAME,
+ false,
+ true
+ );
+
+ // Adding a new category dispatches an event to update
+ // caches, so we need to also dispatch an event to make
+ // sure we don't start the load until after that happens.
+ Services.tm.dispatchToMainThread(() => {
+ sendAsyncMessage("setupComplete");
+ });
+}
+
+add_task(async function() {
+ let chromeScript = SpecialPowers.loadChromeScript(createChromeScript);
+ await chromeScript.promiseOneMessage("setupComplete");
+
+ var testframe = document.getElementById("testframe");
+ testframe.src =
+ "file_contentpolicytype_targeted_link_iframe.sjs?testframe";
+
+ let result = await chromeScript.promiseOneMessage("loadBlocked");
+
+ is(result.policyType, SpecialPowers.Ci.nsIContentPolicy.TYPE_SUBDOCUMENT,
+ "content policy type should TYPESUBDOCUMENT");
+
+ chromeScript.destroy();
+});
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/test_gpc.html b/dom/security/test/general/test_gpc.html
new file mode 100644
index 0000000000..506629554d
--- /dev/null
+++ b/dom/security/test/general/test_gpc.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Global Privacy Control headers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="application/javascript">
+
+add_task(async function testGlobalPrivacyControlDisabled() {
+ await SpecialPowers.pushPrefEnv({ set: [
+ ["privacy.globalprivacycontrol.enabled", false],
+ ["privacy.globalprivacycontrol.functionality.enabled", true],
+ ]})
+ .then(() => fetch("file_gpc_server.sjs"))
+ .then((response) => response.text())
+ .then((response) => {
+ is(response, "false", "GPC disabled so header unsent");
+ is(navigator.globalPrivacyControl, false, "GPC disabled so navigator property is 0");
+
+ let worker = new Worker(window.URL.createObjectURL(new Blob(["postMessage(navigator.globalPrivacyControl);"])));
+ return new Promise((resolve) => { worker.onmessage = (e) => { resolve(e.data) } });
+ })
+ .then((response) => {
+ is(response, false, "GPC disabled so worker's navigator property is 0");
+ });
+});
+
+add_task(async function testGlobalPrivacyControlEnabled() {
+ await SpecialPowers.pushPrefEnv({ set: [
+ ["privacy.globalprivacycontrol.enabled", true],
+ ["privacy.globalprivacycontrol.functionality.enabled", true],
+ ]})
+ .then(() => fetch("file_gpc_server.sjs"))
+ .then((response) => response.text())
+ .then((response) => {
+ is(response, "true", "GPC enabled so header sent and received");
+ is(navigator.globalPrivacyControl, true, "GPC enabled so navigator property is 1");
+
+ let worker = new Worker(window.URL.createObjectURL(new Blob(["postMessage(navigator.globalPrivacyControl);"])));
+ return new Promise((resolve) => { worker.onmessage = (e) => { resolve(e.data) } });
+ })
+ .then((response) => {
+ is(response, true, "GPC enabled so worker's navigator property is 1");
+ });
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/test_innerhtml_sanitizer.html b/dom/security/test/general/test_innerhtml_sanitizer.html
new file mode 100644
index 0000000000..4a4e4efed1
--- /dev/null
+++ b/dom/security/test/general/test_innerhtml_sanitizer.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset=utf-8>
+ <title>Test for Bug 1667113</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1667113">Mozilla Bug 1667113</a>
+<div></div>
+<script>
+SimpleTest.waitForExplicitFinish();
+
+// Please note that 'fakeServer' does not exist because the test relies
+// on "csp-on-violate-policy" , and "specialpowers-http-notify-request"
+// which fire if either the request is blocked or fires. The test does
+// not rely on the result of the load.
+
+function fail() {
+ ok(false, "Should not call this")
+}
+
+function examiner() {
+ SpecialPowers.addObserver(this, "csp-on-violate-policy");
+ SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
+}
+examiner.prototype = {
+ observe(subject, topic, data) {
+ if (topic === "csp-on-violate-policy") {
+ let asciiSpec = SpecialPowers.getPrivilegedProps(
+ SpecialPowers.do_QueryInterface(subject, "nsIURI"),
+ "asciiSpec");
+ if (asciiSpec.includes("fakeServer")) {
+ ok (false, "Should not attempt fetch, not even blocked by CSP.");
+ }
+ }
+
+ if (topic === "specialpowers-http-notify-request") {
+ if (data.includes("fakeServer")) {
+ ok (false, "Should not try fetch");
+ }
+ }
+ },
+ remove() {
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
+ }
+}
+
+window.examiner = new examiner();
+
+let div = document.getElementsByTagName("div")[0];
+div.innerHTML = "<svg><style><title><audio src=fakeServer onerror=fail() onload=fail()>";
+
+let svg = div.firstChild;
+is(svg.nodeName, "svg", "Node name should be svg");
+
+let style = svg.firstChild;
+if (style) {
+ is(style.firstChild, null, "Style should not have child nodes.");
+} else {
+ ok(false, "Should have gotten a node.");
+}
+
+
+SimpleTest.executeSoon(function() {
+ window.examiner.remove();
+ SimpleTest.finish();
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/test_innerhtml_sanitizer.xhtml b/dom/security/test/general/test_innerhtml_sanitizer.xhtml
new file mode 100644
index 0000000000..4d938bc23b
--- /dev/null
+++ b/dom/security/test/general/test_innerhtml_sanitizer.xhtml
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for Bug 1667113</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1667113">Mozilla Bug 1667113</a>
+<div></div>
+<script><![CDATA[
+SimpleTest.waitForExplicitFinish();
+
+// Please note that 'fakeServer' does not exist because the test relies
+// on "csp-on-violate-policy" , and "specialpowers-http-notify-request"
+// which fire if either the request is blocked or fires. The test does
+// not rely on the result of the load.
+
+function fail() {
+ ok(false, "Should not call this")
+}
+
+function examiner() {
+ SpecialPowers.addObserver(this, "csp-on-violate-policy");
+ SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
+}
+examiner.prototype = {
+ observe(subject, topic, data) {
+ if (topic === "csp-on-violate-policy") {
+ let asciiSpec = SpecialPowers.getPrivilegedProps(
+ SpecialPowers.do_QueryInterface(subject, "nsIURI"),
+ "asciiSpec");
+ if (asciiSpec.includes("fakeServer")) {
+ ok (false, "Should not attempt fetch, not even blocked by CSP.");
+ }
+ }
+
+ if (topic === "specialpowers-http-notify-request") {
+ if (data.includes("fakeServer")) {
+ ok (false, "Should not try fetch");
+ }
+ }
+ },
+ remove() {
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
+ }
+}
+
+window.examiner = new examiner();
+
+let div = document.getElementsByTagName("div")[0];
+div.innerHTML = "<svg xmlns='http://www.w3.org/2000/svg'><style><title><audio xmlns='http://www.w3.org/1999/xhtml' src='fakeServer' onerror='fail()' onload='fail()'></audio></title></style></svg>";
+
+let svg = div.firstChild;
+is(svg.nodeName, "svg", "Node name should be svg");
+
+let style = svg.firstChild;
+if (style) {
+ is(style.firstChild, null, "Style should not have child nodes.");
+} else {
+ ok(false, "Should have gotten a node.");
+}
+
+
+SimpleTest.executeSoon(function() {
+ window.examiner.remove();
+ SimpleTest.finish();
+});
+
+]]></script>
+</body>
+</html>
diff --git a/dom/security/test/general/test_meta_referrer.html b/dom/security/test/general/test_meta_referrer.html
new file mode 100644
index 0000000000..f5e8b649f4
--- /dev/null
+++ b/dom/security/test/general/test_meta_referrer.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1704473 - Remove head requirement for meta name=referrer</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<iframe id="frame_meta_in_head"></iframe>
+<iframe id="frame_meta_notin_head"></iframe>
+
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+let testCounter = 0;
+function checkTestsDone() {
+ testCounter++;
+ if(testCounter == 2) {
+ SimpleTest.finish();
+ }
+}
+var script = SpecialPowers.loadChromeScript(() => {
+ /* eslint-env mozilla/chrome-script */
+ let counter = 0;
+ Services.obs.addObserver(function onExamResp(subject, topic, data) {
+ let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+ if (!channel.URI.spec.startsWith("https://example.com") || counter >= 2) {
+ return;
+ }
+
+ let refererHeaderSet = false;
+ try {
+ channel.getRequestHeader("referer");
+ refererHeaderSet = true;
+ } catch (e) {
+ refererHeaderSet = false;
+ }
+ ok(!refererHeaderSet, "the referer header should not be set");
+ counter++;
+ sendAsyncMessage("checked-referer-header");
+ }, "http-on-stop-request");
+});
+
+script.addMessageListener("checked-referer-header", checkTestsDone);
+
+let frame1 = document.getElementById("frame_meta_in_head");
+frame1.src = "/tests/dom/security/test/general/file_meta_referrer_in_head.html";
+let frame2 = document.getElementById("frame_meta_notin_head");
+frame2.src = "/tests/dom/security/test/general/file_meta_referrer_notin_head.html";
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/test_nosniff.html b/dom/security/test/general/test_nosniff.html
new file mode 100644
index 0000000000..a22386aea0
--- /dev/null
+++ b/dom/security/test/general/test_nosniff.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 471020 - Add X-Content-Type-Options: nosniff support to Firefox</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+
+ <!-- add the two css tests -->
+ <link rel="stylesheet" id="cssCorrectType">
+ <link rel="stylesheet" id="cssWrongType">
+</head>
+<body>
+
+<!-- add the two script tests -->
+<script id="scriptCorrectType"></script>
+<script id="scriptWrongType"></script>
+
+<script class="testbody" type="text/javascript">
+/* Description of the test:
+ * We load 2 css files, 2 script files and 2 image files, where
+ * the sever either responds with the right mime type or
+ * the wrong mime type for each test.
+ */
+
+SimpleTest.waitForExplicitFinish();
+const NUM_TESTS = 4;
+
+var testCounter = 0;
+function checkFinish() {
+ testCounter++;
+ if (testCounter === NUM_TESTS) {
+ SimpleTest.finish();
+ }
+}
+
+ // 1) Test CSS with correct mime type
+ var cssCorrectType = document.getElementById("cssCorrectType");
+ cssCorrectType.onload = function() {
+ ok(true, "style nosniff correct type should load");
+ checkFinish();
+ }
+ cssCorrectType.onerror = function() {
+ ok(false, "style nosniff correct type should load");
+ checkFinish();
+ }
+ cssCorrectType.href = "file_nosniff_testserver.sjs?cssCorrectType";
+
+ // 2) Test CSS with wrong mime type
+ var cssWrongType = document.getElementById("cssWrongType");
+ cssWrongType.onload = function() {
+ ok(false, "style nosniff wrong type should not load");
+ checkFinish();
+ }
+ cssWrongType.onerror = function() {
+ ok(true, "style nosniff wrong type should not load");
+ checkFinish();
+ }
+ cssWrongType.href = "file_nosniff_testserver.sjs?cssWrongType";
+
+ // 3) Test SCRIPT with correct mime type
+ var scriptCorrectType = document.getElementById("scriptCorrectType");
+ scriptCorrectType.onload = function() {
+ ok(true, "script nosniff correct type should load");
+ checkFinish();
+ }
+ scriptCorrectType.onerror = function() {
+ ok(false, "script nosniff correct type should load");
+ checkFinish();
+ }
+ scriptCorrectType.src = "file_nosniff_testserver.sjs?scriptCorrectType";
+
+ // 4) Test SCRIPT with wrong mime type
+ var scriptWrongType = document.getElementById("scriptWrongType");
+ scriptWrongType.onload = function() {
+ ok(false, "script nosniff wrong type should not load");
+ checkFinish();
+ }
+ scriptWrongType.onerror = function() {
+ ok(true, "script nosniff wrong type should not load");
+ checkFinish();
+ }
+ scriptWrongType.src = "file_nosniff_testserver.sjs?scriptWrongType";
+
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/test_nosniff_navigation.html b/dom/security/test/general/test_nosniff_navigation.html
new file mode 100644
index 0000000000..6710f4f5b9
--- /dev/null
+++ b/dom/security/test/general/test_nosniff_navigation.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <title>Bug 1428473 Support X-Content-Type-Options: nosniff when navigating</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+
+ <!-- add the two script tests -->
+ <script id="scriptCorrectType"></script>
+ <script id="scriptWrongType"></script>
+
+ <script class="testbody" type="text/javascript">
+ /* Description of the test:
+ * We're testing if Firefox respects the nosniff Header for Top-Level
+ * Navigations.
+ * If Firefox cant Display the Page, it will prompt a download
+ * and the URL of the Page will be about:blank.
+ * So we will try to open different content send with
+ * no-mime, mismatched-mime and garbage-mime types.
+ *
+ */
+
+ SimpleTest.waitForExplicitFinish();
+
+ window.addEventListener("load", async () => {
+ window.open("window_nosniff_navigation.html");
+ });
+ </script>
+</body>
+</html>
diff --git a/dom/security/test/general/test_same_site_cookies_about.html b/dom/security/test/general/test_same_site_cookies_about.html
new file mode 100644
index 0000000000..faf2caab9a
--- /dev/null
+++ b/dom/security/test/general/test_same_site_cookies_about.html
@@ -0,0 +1,116 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1454721 - Add same-site cookie test for about:blank and about:srcdoc</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<img id="cookieImage">
+<iframe id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/*
+ * Description of the test:
+ * 1) We load an image from http://mochi.test which sets a same site cookie
+ * 2) We then load the following iframes:
+ * (a) cross-origin iframe
+ * (b) same-origin iframe
+ * which both load a:
+ * * nested about:srcdoc frame and nested about:blank frame
+ * * navigate about:srcdoc frame and navigate about:blank frame
+ * 3) We evaluate that the same-site cookie is available in the same-origin case.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+const SAME_ORIGIN = "http://mochi.test:8888/"
+const CROSS_ORIGIN = "http://example.com/";
+const PATH = "tests/dom/security/test/general/file_same_site_cookies_about.sjs";
+
+let curTest = 0;
+
+var tests = [
+ // NAVIGATION TESTS
+ {
+ description: "nested same origin iframe about:srcdoc navigation [mochi.test -> mochi.test -> about:srcdoc -> mochi.test]",
+ frameSRC: SAME_ORIGIN + PATH + "?loadsrcdocframeNav",
+ result: "myKey=mySameSiteAboutCookie", // cookie should be set for baseline test
+ },
+ {
+ description: "nested cross origin iframe about:srcdoc navigation [mochi.test -> example.com -> about:srcdoc -> mochi.test]",
+ frameSRC: CROSS_ORIGIN + PATH + "?loadsrcdocframeNav",
+ result: "", // no same-site cookie should be available
+ },
+ {
+ description: "nested same origin iframe about:blank navigation [mochi.test -> mochi.test -> about:blank -> mochi.test]",
+ frameSRC: SAME_ORIGIN + PATH + "?loadblankframeNav",
+ result: "myKey=mySameSiteAboutCookie", // cookie should be set for baseline test
+ },
+ {
+ description: "nested cross origin iframe about:blank navigation [mochi.test -> example.com -> about:blank -> mochi.test]",
+ frameSRC: CROSS_ORIGIN + PATH + "?loadblankframeNav",
+ result: "", // no same-site cookie should be available
+ },
+ // INCLUSION TESTS
+ {
+ description: "nested same origin iframe about:srcdoc inclusion [mochi.test -> mochi.test -> about:srcdoc -> mochi.test]",
+ frameSRC: SAME_ORIGIN + PATH + "?loadsrcdocframeInc",
+ result: "myKey=mySameSiteAboutCookie", // cookie should be set for baseline test
+ },
+ {
+ description: "nested cross origin iframe about:srcdoc inclusion [mochi.test -> example.com -> about:srcdoc -> mochi.test]",
+ frameSRC: CROSS_ORIGIN + PATH + "?loadsrcdocframeInc",
+ result: "", // no same-site cookie should be available
+ },
+ {
+ description: "nested same origin iframe about:blank inclusion [mochi.test -> mochi.test -> about:blank -> mochi.test]",
+ frameSRC: SAME_ORIGIN + PATH + "?loadblankframeInc",
+ result: "myKey=mySameSiteAboutCookie", // cookie should be set for baseline test
+ },
+ {
+ description: "nested cross origin iframe about:blank inclusion [mochi.test -> example.com -> about:blank -> mochi.test]",
+ frameSRC: CROSS_ORIGIN + PATH + "?loadblankframeInc",
+ result: "", // no same-site cookie should be available
+ },
+];
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ is(event.data.result, tests[curTest].result, tests[curTest].description);
+ curTest += 1;
+
+ // lets see if we ran all the tests
+ if (curTest == tests.length) {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+ return;
+ }
+ // otherwise it's time to run the next test
+ setCookieAndInitTest();
+}
+
+function setupQueryResultAndRunTest() {
+ let testframe = document.getElementById("testframe");
+ testframe.src = tests[curTest].frameSRC + curTest;
+}
+
+function setCookieAndInitTest() {
+ var cookieImage = document.getElementById("cookieImage");
+ cookieImage.onload = function() {
+ ok(true, "trying to set cookie for test (" + tests[curTest].description + ")");
+ setupQueryResultAndRunTest();
+ }
+ cookieImage.onerror = function() {
+ ok(false, "could not load image for test (" + tests[curTest].description + ")");
+ }
+ cookieImage.src = SAME_ORIGIN + PATH + "?setSameSiteCookie" + curTest;
+}
+
+// fire up the test
+setCookieAndInitTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/test_same_site_cookies_cross_origin_context.html b/dom/security/test/general/test_same_site_cookies_cross_origin_context.html
new file mode 100644
index 0000000000..9294a3d030
--- /dev/null
+++ b/dom/security/test/general/test_same_site_cookies_cross_origin_context.html
@@ -0,0 +1,93 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1452496 - Do not allow same-site cookies in cross site context</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<img id="cookieImage">
+<iframe id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/*
+ * Description of the test:
+ * 1) We load an image from http://example.com which tries to
+ * a) a same site cookie
+ * b) a regular cookie
+ * in the context of http://mochi.test
+ * 2) We load an iframe from http://example.com and check if the cookie
+ * is available.
+ * 3) We observe that:
+ * (a) same site cookie has been discarded in a cross origin context.
+ * (b) the regular cookie is available.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+const CROSS_ORIGIN = "http://example.com/";
+const PATH = "tests/dom/security/test/general/file_same_site_cookies_cross_origin_context.sjs";
+
+let curTest = 0;
+
+var tests = [
+ {
+ description: "regular cookie in cross origin context",
+ imgSRC: CROSS_ORIGIN + PATH + "?setRegularCookie",
+ frameSRC: CROSS_ORIGIN + PATH + "?loadFrame",
+ result: "myKey=regularCookie",
+ },
+ {
+ description: "same-site cookie in cross origin context",
+ imgSRC: CROSS_ORIGIN + PATH + "?setSameSiteCookie",
+ frameSRC: CROSS_ORIGIN + PATH + "?loadFrame",
+ result: "", // no cookie should be set
+ },
+];
+
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ is(event.data.result, tests[curTest].result, tests[curTest].description);
+ curTest += 1;
+
+ // lets see if we ran all the tests
+ if (curTest == tests.length) {
+ window.removeEventListener("message", receiveMessage);
+ SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault");
+ SimpleTest.finish();
+ return;
+ }
+ // otherwise it's time to run the next test
+ setCookieAndInitTest();
+}
+
+function setupQueryResultAndRunTest() {
+ let testframe = document.getElementById("testframe");
+ testframe.src = tests[curTest].frameSRC + curTest;
+}
+
+function setCookieAndInitTest() {
+ var cookieImage = document.getElementById("cookieImage");
+ cookieImage.onload = function() {
+ ok(true, "trying to set cookie for test (" + tests[curTest].description + ")");
+ setupQueryResultAndRunTest();
+ }
+ cookieImage.onerror = function() {
+ ok(false, "could not load image for test (" + tests[curTest].description + ")");
+ }
+ cookieImage.src = tests[curTest].imgSRC + curTest;
+}
+
+// fire up the test
+SpecialPowers.pushPrefEnv({
+ "set": [
+ // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default"
+ ["network.cookie.sameSite.laxByDefault", false],
+ ]
+}, setCookieAndInitTest);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/test_same_site_cookies_from_script.html b/dom/security/test/general/test_same_site_cookies_from_script.html
new file mode 100644
index 0000000000..74c38b6249
--- /dev/null
+++ b/dom/security/test/general/test_same_site_cookies_from_script.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1452496 - Do not allow same-site cookies in cross site context</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<iframe id="setCookieFrame"></iframe>
+<iframe id="getCookieFrame"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/*
+ * Description of the test:
+ * 1) We load an iframe which tries to set a same site cookie using an
+ * inline script in top-level context of http://mochi.test.
+ * 2) We load an iframe from http://example.com and check if the cookie
+ * is available.
+ * 3) We observe that:
+ * (a) same site cookie is available in same origin context.
+ * (a) same site cookie has been discarded in a cross origin context.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+const SAME_ORIGIN = "http://mochi.test:8888/";
+const CROSS_ORIGIN = "http://example.com/";
+const PATH = "tests/dom/security/test/general/file_same_site_cookies_from_script.sjs";
+
+let curTest = 0;
+
+var tests = [
+ {
+ description: "same-site cookie inline script within same-site context",
+ setCookieSrc: SAME_ORIGIN + PATH + "?setSameSiteCookieUsingInlineScript",
+ getCookieSrc: SAME_ORIGIN + PATH + "?getCookieFrame",
+ result: "myKey=sameSiteCookieInlineScript",
+ },
+ {
+ description: "same-site cookie inline script within cross-site context",
+ setCookieSrc: CROSS_ORIGIN + PATH + "?setSameSiteCookieUsingInlineScript",
+ getCookieSrc: CROSS_ORIGIN + PATH + "?getCookieFrame",
+ result: "", // same-site cookie should be discarded in cross site context
+ },
+];
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ is(event.data.result, tests[curTest].result, tests[curTest].description);
+ curTest += 1;
+
+ // lets see if we ran all the tests
+ if (curTest == tests.length) {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+ return;
+ }
+ // otherwise it's time to run the next test
+ setCookieAndInitTest();
+}
+
+function setupQueryResultAndRunTest() {
+ let getCookieFrame = document.getElementById("getCookieFrame");
+ getCookieFrame.src = tests[curTest].getCookieSrc + curTest;
+}
+
+function setCookieAndInitTest() {
+ var setCookieFrame = document.getElementById("setCookieFrame");
+ setCookieFrame.onload = function() {
+ ok(true, "trying to set cookie for test (" + tests[curTest].description + ")");
+ setupQueryResultAndRunTest();
+ }
+ setCookieFrame.onerror = function() {
+ ok(false, "could not load image for test (" + tests[curTest].description + ")");
+ }
+ setCookieFrame.src = tests[curTest].setCookieSrc + curTest;
+}
+
+// fire up the test
+setCookieAndInitTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/test_same_site_cookies_iframe.html b/dom/security/test/general/test_same_site_cookies_iframe.html
new file mode 100644
index 0000000000..45d5d5830a
--- /dev/null
+++ b/dom/security/test/general/test_same_site_cookies_iframe.html
@@ -0,0 +1,168 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1454027 - Update SameSite cookie handling inside iframes</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<img id="cookieImage">
+<iframe id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/*
+ * Description of the test:
+ * 1) We load an image from http://mochi.test which sets a same site cookie
+ * 2) We then load the following iframes:
+ * (a) cross-origin iframe
+ * (b) sandboxed iframe
+ * (c) data: URI iframe
+ * (d) same origin iframe which loads blob: URI iframe (to simulate same origin blobs)
+ * (e) cross origin iframe which loads blob: URI iframe (to simulate cross origin blobs)
+ * which all:
+ * * navigate the iframe to http://mochi.test
+ * * include another iframe from http://mochi.test
+ * 3) We observe that none of the nested iframes have access to the same-site cookie.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+const SAME_ORIGIN = "http://mochi.test:8888/"
+const CROSS_ORIGIN = "http://example.com/";
+const PATH = "tests/dom/security/test/general/";
+const SERVER_FILE = "file_same_site_cookies_iframe.sjs";
+
+const NESTED_DATA_IFRAME_NAVIGATION = `
+ data:text/html,
+ <html>
+ <body>
+ <a id="testlink" href="http://mochi.test:8888/tests/dom/security/test/general/file_same_site_cookies_iframe.sjs"></a>
+ <script type="application/javascript">
+ let link = document.getElementById("testlink");
+ link.click();
+ <\/script>
+ </body>
+ </html>`;
+
+const NESTED_DATA_IFRAME_INCLUSION = `
+ data:text/html,
+ <html>
+ <body>
+ <script type="application/javascript">
+ window.addEventListener("message", receiveMessage);
+ function receiveMessage(event) {
+ window.removeEventListener("message", receiveMessage);
+ window.parent.postMessage({result: event.data.result}, '*');
+ }
+ <\/script>
+ <iframe src="http://mochi.test:8888/tests/dom/security/test/general/file_same_site_cookies_iframe.sjs"></iframe>
+ </body>
+ </html>`;
+
+let curTest = 0;
+
+var tests = [
+ // NAVIGATION TESTS
+ {
+ description: "nested same origin iframe navigation [mochi.test -> mochi.test -> mochi.test]",
+ frameSRC: SAME_ORIGIN + PATH + SERVER_FILE + "?nestedIframeNavigation",
+ result: "myKey=mySameSiteIframeTestCookie", // cookie should be set for baseline test
+ },
+ {
+ description: "nested cross origin iframe navigation [mochi.test -> example.com -> mochi.test]",
+ frameSRC: CROSS_ORIGIN + PATH + SERVER_FILE + "?nestedIframeNavigation",
+ result: "", // no cookie should be set
+ },
+ {
+ description: "nested sandboxed iframe navigation [mochi.test -> sandbox -> mochi.test]",
+ frameSRC: CROSS_ORIGIN + PATH + SERVER_FILE + "?nestedSandboxIframeNavigation",
+ result: "", // no cookie should be set
+ },
+ {
+ description: "nested data iframe navigation [mochi.test -> data: -> mochi.test]",
+ frameSRC: NESTED_DATA_IFRAME_NAVIGATION,
+ result: "", // no cookie should be set
+ },
+ {
+ description: "nested same site blob iframe navigation [mochi.test -> mochi.test -> blob: -> mochi.test]",
+ frameSRC: SAME_ORIGIN + PATH + "file_same_site_cookies_blob_iframe_navigation.html",
+ result: "myKey=mySameSiteIframeTestCookie", // cookie should be set, blobs inherit security context
+ },
+ {
+ description: "nested cross site blob iframe navigation [mochi.test -> example.com -> blob: -> mochi.test]",
+ frameSRC: CROSS_ORIGIN + PATH + "file_same_site_cookies_blob_iframe_navigation.html",
+ result: "", // no cookie should be set
+ },
+ // INCLUSION TESTS
+ {
+ description: "nested same origin iframe inclusion [mochi.test -> mochi.test -> mochi.test]",
+ frameSRC: SAME_ORIGIN + PATH + SERVER_FILE + "?nestedIframeInclusion",
+ result: "myKey=mySameSiteIframeTestCookie", // cookie should be set for baseline test
+ },
+ {
+ description: "nested cross origin iframe inclusion [mochi.test -> example.com -> mochi.test]",
+ frameSRC: CROSS_ORIGIN + PATH + SERVER_FILE + "?nestedIframeInclusion",
+ result: "", // no cookie should be set
+ },
+ {
+ description: "nested sandboxed iframe inclusion [mochi.test -> sandbox -> mochi.test]",
+ frameSRC: CROSS_ORIGIN + PATH + SERVER_FILE + "?nestedSandboxIframeInclusion",
+ result: "", // no cookie should be set
+ },
+ {
+ description: "nested data iframe inclusion [mochi.test -> data: -> mochi.test]",
+ frameSRC: NESTED_DATA_IFRAME_INCLUSION,
+ result: "", // no cookie should be set
+ },
+ {
+ description: "nested same site blob iframe inclusion [mochi.test -> mochi.test -> blob: -> mochi.test]",
+ frameSRC: SAME_ORIGIN + PATH + "file_same_site_cookies_blob_iframe_inclusion.html",
+ result: "myKey=mySameSiteIframeTestCookie", // cookie should be set, blobs inherit security context
+ },
+ {
+ description: "same-site cookie, nested cross site blob iframe inclusion [mochi.test -> example.com -> blob: -> mochi.test]",
+ frameSRC: CROSS_ORIGIN + PATH + "file_same_site_cookies_blob_iframe_inclusion.html",
+ result: "", // no cookie should be set
+ },
+];
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ is(event.data.result, tests[curTest].result, tests[curTest].description);
+ curTest += 1;
+
+ // // lets see if we ran all the tests
+ if (curTest == tests.length) {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+ return;
+ }
+ // otherwise it's time to run the next test
+ setCookieAndInitTest();
+}
+
+function setupQueryResultAndRunTest() {
+ let testframe = document.getElementById("testframe");
+ testframe.src = tests[curTest].frameSRC;
+}
+
+function setCookieAndInitTest() {
+ var cookieImage = document.getElementById("cookieImage");
+ cookieImage.onload = function() {
+ ok(true, "trying to set cookie for test (" + tests[curTest].description + ")");
+ setupQueryResultAndRunTest();
+ }
+ cookieImage.onerror = function() {
+ ok(false, "could not load image for test (" + tests[curTest].description + ")");
+ }
+ // appending math.random to avoid any unexpected caching behavior
+ cookieImage.src = SAME_ORIGIN + PATH + SERVER_FILE + "?setSameSiteCookie" + Math.random();
+}
+
+// fire up the test
+setCookieAndInitTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/test_same_site_cookies_laxByDefault.html b/dom/security/test/general/test_same_site_cookies_laxByDefault.html
new file mode 100644
index 0000000000..9fd0d0b704
--- /dev/null
+++ b/dom/security/test/general/test_same_site_cookies_laxByDefault.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1551798 - SameSite=lax by default</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+
+const CROSS_ORIGIN = "http://example.com/";
+const PATH = "tests/dom/security/test/general/closeWindow.sjs";
+
+async function realTest(noneRequiresSecure) {
+ let types = ["unset", "lax", "none"];
+ for (let i = 0; i < types.length; ++i) {
+ info("Loading a new top-level page (" + types[i] + ")");
+ await new Promise(resolve => {
+ window.addEventListener("message", _ => {
+ resolve();
+ }, { once: true });
+ window.open(CROSS_ORIGIN + PATH + "?" + types[i]);
+ });
+ }
+
+ info("Check cookies");
+ let chromeScript = SpecialPowers.loadChromeScript(() => {
+ /* eslint-env mozilla/chrome-script */
+ const {sendAsyncMessage} = this;
+ let cookies = { test: null, test2: null, test3: null };
+
+ for (let cookie of Services.cookies.cookies) {
+ if (cookie.host != "example.com") continue;
+
+ if (cookie.name == "test" && cookie.value == "wow") {
+ cookies.test = cookie.sameSite == Ci.nsICookie.SAMESITE_LAX ? 'lax' : 'none';
+ }
+
+ if (cookie.name == "test2" && cookie.value == "wow2") {
+ cookies.test2 = cookie.sameSite == Ci.nsICookie.SAMESITE_LAX ? 'lax' : 'none';
+ }
+
+ if (cookie.name == "test3" && cookie.value == "wow3") {
+ cookies.test3 = cookie.sameSite == Ci.nsICookie.SAMESITE_LAX ? 'lax' : 'none';
+ }
+ }
+
+ Services.cookies.removeAll();
+ sendAsyncMessage('result', cookies);
+ });
+
+ let cookies = await new Promise(resolve => {
+ chromeScript.addMessageListener('result', cookies => {
+ chromeScript.destroy();
+ resolve(cookies);
+ });
+ });
+
+ is(cookies.test, "lax", "Cookie set without samesite is lax by default");
+ if (noneRequiresSecure) {
+ is(cookies.test2, null, "Cookie set with samesite none, but not secure");
+ } else {
+ is(cookies.test2, "none", "Cookie set with samesite none");
+ }
+ is(cookies.test3, "lax", "Cookie set with samesite lax");
+}
+
+SpecialPowers.pushPrefEnv({"set": [
+ ["network.cookie.sameSite.laxByDefault", true],
+ ["network.cookie.sameSite.noneRequiresSecure", false],
+]}).then(_ => {
+ return realTest(false);
+}).then(_ => {
+ return SpecialPowers.pushPrefEnv({"set": [
+ ["network.cookie.sameSite.laxByDefault", true],
+ ["network.cookie.sameSite.noneRequiresSecure", true]]});
+}).then(_ => {
+ return realTest(true);
+}).then(SimpleTest.finish);
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/test_same_site_cookies_redirect.html b/dom/security/test/general/test_same_site_cookies_redirect.html
new file mode 100644
index 0000000000..59f98b2263
--- /dev/null
+++ b/dom/security/test/general/test_same_site_cookies_redirect.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1453814 - Do not allow same-site cookies for cross origin redirect</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<img id="cookieImage">
+<iframe id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/*
+ * Description of the test:
+ * 1) We load an image from http://mochi.test which set a same site cookie
+ * 2) We then load an iframe that redirects
+ * (a) from same-origin to cross-origin
+ * (b) from cross-origin to same-origin
+ * 3) We observe that in both cases same-site cookies should not be send
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+const SAME_ORIGIN = location.origin + "/";
+const CROSS_ORIGIN = "http://example.com/";
+const PATH = "tests/dom/security/test/general/file_same_site_cookies_redirect.sjs";
+
+let curTest = 0;
+
+var tests = [
+ {
+ description: "baseline: same-site cookie, redirect same-site to same-site",
+ imgSRC: SAME_ORIGIN + PATH + "?setSameSiteCookie",
+ frameSRC: SAME_ORIGIN + PATH + "?sameToSameRedirect",
+ result: "myKey=strictSameSiteCookie",
+ },
+ {
+ description: "same-site cookie, redirect same-site to cross-site",
+ imgSRC: SAME_ORIGIN + PATH + "?setSameSiteCookie",
+ frameSRC: SAME_ORIGIN + PATH + "?sameToCrossRedirect",
+ result: "", // no cookie should be set
+ },
+ {
+ description: "same-site cookie, redirect cross-site to same-site",
+ imgSRC: SAME_ORIGIN + PATH + "?setSameSiteCookie",
+ frameSRC: CROSS_ORIGIN + PATH + "?crossToSameRedirect",
+ result: "", // no cookie should be set
+ },
+ {
+ description: "same-site cookie, meta redirect same-site to cross-site",
+ imgSRC: SAME_ORIGIN + PATH + "?setSameSiteCookie",
+ frameSRC: SAME_ORIGIN + PATH + "?sameToCrossRedirectMeta",
+ result: "", // no cookie should be set
+ },
+ {
+ description: "same-site cookie, meta redirect cross-site to same-site",
+ imgSRC: SAME_ORIGIN + PATH + "?setSameSiteCookie",
+ frameSRC: CROSS_ORIGIN + PATH + "?crossToSameRedirectMeta",
+ result: "", // no cookie should be set
+ },
+];
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ is(event.data.result, tests[curTest].result, tests[curTest].description);
+ curTest += 1;
+
+ // // lets see if we ran all the tests
+ if (curTest == tests.length) {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+ return;
+ }
+ // otherwise it's time to run the next test
+ setCookieAndInitTest();
+}
+
+function setupQueryResultAndRunTest() {
+ let testframe = document.getElementById("testframe");
+ testframe.src = tests[curTest].frameSRC;
+}
+
+function setCookieAndInitTest() {
+ var cookieImage = document.getElementById("cookieImage");
+ cookieImage.onload = function() {
+ ok(true, "trying to set cookie for test (" + tests[curTest].description + ")");
+ setupQueryResultAndRunTest();
+ }
+ cookieImage.onerror = function() {
+ ok(false, "could not load image for test (" + tests[curTest].description + ")");
+ }
+ cookieImage.src = tests[curTest].imgSRC;
+}
+
+// fire up the test
+setCookieAndInitTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/test_same_site_cookies_subrequest.html b/dom/security/test/general/test_same_site_cookies_subrequest.html
new file mode 100644
index 0000000000..304dbafa9a
--- /dev/null
+++ b/dom/security/test/general/test_same_site_cookies_subrequest.html
@@ -0,0 +1,113 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1286861 - Test same site cookies on subrequests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<img id="cookieImage">
+<iframe id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/*
+ * Description of the test:
+ * 1) We load an image from http://mochi.test which sets a same site cookie
+ * 2) We load an iframe from:
+ * * http://mochi.test which loads another image from http://mochi.test
+ * * http://example.com which loads another image from http://mochi.test
+ * 3) We observe that the same site cookie is sent in the same origin case,
+ * but not in the cross origin case.
+ *
+ * In detail:
+ * We perform an XHR request to the *.sjs file which is processed async on
+ * the server and waits till the image request has been processed by the server.
+ * Once the image requets was processed, the server responds to the initial
+ * XHR request with the expecuted result (the cookie value).
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+const SAME_ORIGIN = "http://mochi.test:8888/";
+const CROSS_ORIGIN = "http://example.com/";
+const PATH = "tests/dom/security/test/general/file_same_site_cookies_subrequest.sjs";
+
+let curTest = 0;
+
+var tests = [
+ {
+ description: "same origin site using cookie policy 'samesite=strict'",
+ imgSRC: SAME_ORIGIN + PATH + "?setStrictSameSiteCookie",
+ frameSRC: SAME_ORIGIN + PATH + "?loadFrame",
+ result: "myKey=strictSameSiteCookie",
+ },
+ {
+ description: "cross origin site using cookie policy 'samesite=strict'",
+ imgSRC: SAME_ORIGIN + PATH + "?setStrictSameSiteCookie",
+ frameSRC: CROSS_ORIGIN + PATH + "?loadFrame",
+ result: "myKey=noCookie",
+ },
+ {
+ description: "same origin site using cookie policy 'samesite=lax'",
+ imgSRC: SAME_ORIGIN + PATH + "?setLaxSameSiteCookie",
+ frameSRC: SAME_ORIGIN + PATH + "?loadFrame",
+ result: "myKey=laxSameSiteCookie",
+ },
+ {
+ description: "cross origin site using cookie policy 'samesite=lax'",
+ imgSRC: SAME_ORIGIN + PATH + "?setLaxSameSiteCookie",
+ frameSRC: CROSS_ORIGIN + PATH + "?loadFrame",
+ result: "myKey=noCookie",
+ },
+];
+
+function checkResult(aCookieVal) {
+ is(aCookieVal, tests[curTest].result, tests[curTest].description);
+ curTest += 1;
+
+ // lets see if we ran all the tests
+ if (curTest == tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+ // otherwise it's time to run the next test
+ setCookieAndInitTest();
+}
+
+function setupQueryResultAndRunTest() {
+ var myXHR = new XMLHttpRequest();
+ myXHR.open("GET", "file_same_site_cookies_subrequest.sjs?queryresult" + curTest);
+ myXHR.onload = function(e) {
+ checkResult(myXHR.responseText);
+ }
+ myXHR.onerror = function(e) {
+ ok(false, "could not query results from server (" + e.message + ")");
+ }
+ myXHR.send();
+
+ // give it some time and load the test frame
+ SimpleTest.executeSoon(function() {
+ let testframe = document.getElementById("testframe");
+ testframe.src = tests[curTest].frameSRC + curTest;
+ });
+}
+
+function setCookieAndInitTest() {
+ var cookieImage = document.getElementById("cookieImage");
+ cookieImage.onload = function() {
+ ok(true, "set cookie for test (" + tests[curTest].description + ")");
+ setupQueryResultAndRunTest();
+ }
+ cookieImage.onerror = function() {
+ ok(false, "could not set cookie for test (" + tests[curTest].description + ")");
+ }
+ cookieImage.src = tests[curTest].imgSRC + curTest;
+}
+
+// fire up the test
+setCookieAndInitTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/test_same_site_cookies_toplevel_nav.html b/dom/security/test/general/test_same_site_cookies_toplevel_nav.html
new file mode 100644
index 0000000000..aba825916b
--- /dev/null
+++ b/dom/security/test/general/test_same_site_cookies_toplevel_nav.html
@@ -0,0 +1,117 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1286861 - Test same site cookies on top-level navigations</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<img id="cookieImage">
+
+<script class="testbody" type="text/javascript">
+
+/*
+ * Description of the test:
+ * 1) We load an image from http://mochi.test which sets a same site cookie
+ * 2) We open a new window to
+ * * a same origin location
+ * * a cross origin location
+ * 3) We observe that the same site cookie is sent in the same origin case,
+ * but not in the cross origin case, unless the policy = 'lax', which should
+ * send the cookie in a top-level navigation case.
+ *
+ * In detail:
+ * We perform an XHR request to the *.sjs file which is processed async on
+ * the server and waits till the image request has been processed by the server.
+ * Once the image requets was processed, the server responds to the initial
+ * XHR request with the expecuted result (the cookie value).
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+const SAME_ORIGIN = "http://mochi.test:8888/";
+const CROSS_ORIGIN = "http://example.com/";
+const PATH = "tests/dom/security/test/general/file_same_site_cookies_toplevel_nav.sjs";
+
+let curTest = 0;
+
+let currentWindow;
+var tests = [
+ {
+ description: "same origin navigation using cookie policy 'samesite=strict'",
+ imgSRC: SAME_ORIGIN + PATH + "?setStrictSameSiteCookie",
+ frameSRC: SAME_ORIGIN + PATH + "?loadFrame",
+ result: "myKey=strictSameSiteCookie",
+ },
+ {
+ description: "cross origin navigation using cookie policy 'samesite=strict'",
+ imgSRC: SAME_ORIGIN + PATH + "?setStrictSameSiteCookie",
+ frameSRC: CROSS_ORIGIN + PATH + "?loadFrame",
+ result: "myKey=noCookie",
+ },
+ {
+ description: "same origin navigation using cookie policy 'samesite=lax'",
+ imgSRC: SAME_ORIGIN + PATH + "?setLaxSameSiteCookie",
+ frameSRC: SAME_ORIGIN + PATH + "?loadFrame",
+ result: "myKey=laxSameSiteCookie",
+ },
+ {
+ description: "cross origin navigation using cookie policy 'samesite=lax'",
+ imgSRC: SAME_ORIGIN + PATH + "?setLaxSameSiteCookie",
+ frameSRC: CROSS_ORIGIN + PATH + "?loadFrame",
+ result: "myKey=laxSameSiteCookie",
+ },
+];
+
+function checkResult(aCookieVal) {
+ if(currentWindow){
+ currentWindow.close();
+ currentWindow= null;
+ }
+ is(aCookieVal, tests[curTest].result, tests[curTest].description);
+ curTest += 1;
+
+ // lets see if we ran all the tests
+ if (curTest == tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+ // otherwise it's time to run the next test
+ setCookieAndInitTest();
+}
+
+function setupQueryResultAndRunTest() {
+ var myXHR = new XMLHttpRequest();
+ myXHR.open("GET", "file_same_site_cookies_toplevel_nav.sjs?queryresult" + curTest);
+ myXHR.onload = function(e) {
+ checkResult( myXHR.responseText);
+ }
+ myXHR.onerror = function(e) {
+ ok(false, "could not query results from server (" + e.message + ")");
+ }
+ myXHR.send();
+
+ // give it some time and load the test window
+ SimpleTest.executeSoon(function() {
+ currentWindow = window.open(tests[curTest].frameSRC + curTest);
+ });
+}
+
+function setCookieAndInitTest() {
+ var cookieImage = document.getElementById("cookieImage");
+ cookieImage.onload = function() {
+ ok(true, "set cookie for test (" + tests[curTest].description + ")");
+ setupQueryResultAndRunTest();
+ }
+ cookieImage.onerror = function() {
+ ok(false, "could not set cookie for test (" + tests[curTest].description + ")");
+ }
+ cookieImage.src = tests[curTest].imgSRC + curTest;
+}
+
+// fire up the test
+setCookieAndInitTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/test_same_site_cookies_toplevel_set_cookie.html b/dom/security/test/general/test_same_site_cookies_toplevel_set_cookie.html
new file mode 100644
index 0000000000..cae2a6174e
--- /dev/null
+++ b/dom/security/test/general/test_same_site_cookies_toplevel_set_cookie.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1454242: Setting samesite cookie should not rely on CookieCommons::IsSameSiteForeign</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<img id="cookieImage">
+<iframe id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/*
+ * Description of the test:
+ * 1) We load a window from example.com which loads a window from mochi.test
+ * which then sets a same-site cookie for mochi.test.
+ * 2) We load an iframe from mochi.test.
+ * 3) We observe that the cookie within (1) was allowed to be set and
+ * is available for mochi.test.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+const SAME_ORIGIN = "http://mochi.test:8888/"
+const CROSS_ORIGIN = "http://example.com/";
+const PATH = "tests/dom/security/test/general/file_same_site_cookies_toplevel_set_cookie.sjs";
+
+let testWin = null;
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ // once the second window (which sets the cookie) loaded, we get a notification
+ // that the test setup is correct and we can now try to query the same-site cookie
+ if (event.data.value === "testSetupComplete") {
+ ok(true, "cookie setup worked");
+ let testframe = document.getElementById("testframe");
+ testframe.src = SAME_ORIGIN + PATH + "?checkCookie";
+ return;
+ }
+
+ // thie second message is the cookie value from verifying the
+ // cookie has been set correctly.
+ is(event.data.value, "myKey=laxSameSiteCookie",
+ "setting same-site cookie on cross origin top-level page");
+
+ window.removeEventListener("message", receiveMessage);
+ testWin.close();
+ SimpleTest.finish();
+}
+
+// fire up the test
+testWin = window.open(CROSS_ORIGIN + PATH + "?loadWin");
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/test_xfo_error_page.html b/dom/security/test/general/test_xfo_error_page.html
new file mode 100644
index 0000000000..218413b4f9
--- /dev/null
+++ b/dom/security/test/general/test_xfo_error_page.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1626249: Ensure correct display of neterror page for XFO</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="xfo_testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+const XFO_ERROR_PAGE_MSG = "This page has an X-Frame-Options policy that prevents it from being loaded in this context";
+
+let xfo_testframe = document.getElementById("xfo_testframe");
+
+xfo_testframe.onload = function() {
+ let wrappedXFOFrame = SpecialPowers.wrap(xfo_testframe.contentWindow);
+ let frameContentXFO = wrappedXFOFrame.document.body.innerHTML;
+ ok(frameContentXFO.includes(XFO_ERROR_PAGE_MSG), "xfo error page correct");
+ SimpleTest.finish();
+}
+
+xfo_testframe.onerror = function() {
+ ok(false, "sanity: should not fire onerror for xfo_testframe");
+ SimpleTest.finish();
+}
+
+xfo_testframe.src = "file_xfo_error_page.sjs";
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/general/window_nosniff_navigation.html b/dom/security/test/general/window_nosniff_navigation.html
new file mode 100644
index 0000000000..1287e451b1
--- /dev/null
+++ b/dom/security/test/general/window_nosniff_navigation.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1428473 Support X-Content-Type-Options: nosniff when navigating</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <style>
+ iframe{
+ border: 1px solid orange;
+ }
+ </style>
+
+ <!-- Using Content-Type: */* -->
+ <iframe class="no-mime" src="file_nosniff_navigation.sjs?mime=*%2F*&content=xml"></iframe>
+ <iframe class="no-mime" src="file_nosniff_navigation.sjs?mime=*%2F*&content=html"></iframe>
+ <iframe class="no-mime" src="file_nosniff_navigation.sjs?mime=*%2F*&content=css" ></iframe>
+ <iframe class="no-mime" src="file_nosniff_navigation.sjs?mime=*%2F*&content=json"></iframe>
+ <iframe class="no-mime" src="file_nosniff_navigation.sjs?mime=*%2F*&content=img"></iframe>
+ <iframe class="no-mime" src="file_nosniff_navigation.sjs?mime=*%2F*&content=pdf"></iframe>
+ <iframe class="no-mime" src="file_nosniff_navigation.sjs?mime=*%2F*"></iframe>
+ <hr>
+ <!-- Using Content-Type: image/png -->
+ <iframe class="mismatch-mime" src="file_nosniff_navigation.sjs?mime=image%2Fpng&content=xml"></iframe>
+ <iframe class="mismatch-mime" src="file_nosniff_navigation.sjs?mime=image%2Fpng&content=html"></iframe>
+ <iframe class="mismatch-mime" src="file_nosniff_navigation.sjs?mime=image%2Fpng&content=css"></iframe>
+ <iframe class="mismatch-mime" src="file_nosniff_navigation.sjs?mime=image%2Fpng&content=json"></iframe>
+ <iframe class="mismatch-mime" src="file_nosniff_navigation.sjs?mime=image%2Fpng&content=img"></iframe>
+ <iframe class="mismatch-mime" src="file_nosniff_navigation.sjs?mime=image%2Fpng&content=pdf"></iframe>
+ <iframe class="mismatch-mime" src="file_nosniff_navigation.sjs?mime=image%2Fpng"></iframe>
+ <hr>
+ <!-- Using Content-Type: garbage/garbage -->
+ <iframe class="garbage-mime" src="file_nosniff_navigation.sjs?mime=garbage%2Fgarbage&content=xml"> </iframe>
+ <iframe class="garbage-mime" src="file_nosniff_navigation.sjs?mime=garbage%2Fgarbage&content=html"></iframe>
+ <iframe class="garbage-mime" src="file_nosniff_navigation.sjs?mime=garbage%2Fgarbage&content=css" ></iframe>
+ <iframe class="garbage-mime" src="file_nosniff_navigation.sjs?mime=garbage%2Fgarbage&content=json"></iframe>
+ <iframe class="garbage-mime" src="file_nosniff_navigation.sjs?mime=garbage%2Fgarbage&content=img"></iframe>
+ <iframe class="garbage-mime" src="file_nosniff_navigation.sjs?mime=garbage%2Fgarbage&content=pdf"></iframe>
+ <iframe class="garbage-mime" src="file_nosniff_navigation.sjs?mime=garbage%2Fgarbage"></iframe>
+</head>
+
+<body>
+
+<!-- add the two script tests -->
+<script id="scriptCorrectType"></script>
+<script id="scriptWrongType"></script>
+
+<script class="testbody" type="text/javascript">
+/* Description of the test:
+ * We're testing if Firefox respects the nosniff Header for Top-Level
+ * Navigations.
+ * If Firefox cant Display the Page, it will prompt a download
+ * and the URL of the Page will be about:blank.
+ * So we will try to open different content send with
+ * no-mime, mismatched-mime and garbage-mime types.
+ *
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+window.addEventListener("load", ()=>{
+ let noMimeFrames = Array.from(document.querySelectorAll(".no-mime"));
+ noMimeFrames.forEach(frame => {
+ let doc = frame.contentWindow.document;
+ // In case of no Provided Content Type, not rendering or assuming text/plain is valid
+ let result = doc.URL == "about:blank" || doc.contentType == "text/plain";
+ let sniffTarget = (new URL(frame.src)).searchParams.get("content");
+ window.opener.ok(result, `${sniffTarget} without MIME - was not sniffed`);
+ });
+
+ let mismatchedMimes = Array.from(document.querySelectorAll(".mismatch-mime"));
+ mismatchedMimes.forEach(frame => {
+ // In case the Server mismatches the Mime Type (sends content X as image/png)
+ // assert that we do not sniff and correct this.
+ let result = frame.contentWindow.document.contentType == "image/png";
+ let sniffTarget = (new URL(frame.src)).searchParams.get("content");
+ window.opener.ok(result, `${sniffTarget} send as image/png - was not Sniffed`);
+ });
+
+ let badMimeFrames = Array.from(document.querySelectorAll(".garbage-mime"));
+ badMimeFrames.forEach(frame => {
+ // In the case we got a bogous mime, assert that we dont sniff.
+ // We must not default here to text/plain
+ // as the Server at least provided a mime type.
+ let result = frame.contentWindow.document.URL == "about:blank";
+ let sniffTarget = (new URL(frame.src)).searchParams.get("content");
+ window.opener.ok(result, `${sniffTarget} send as garbage/garbage - was not Sniffed`);
+ });
+
+ window.opener.SimpleTest.finish();
+ this.close();
+});
+</script>
+</body>
+
+</html> \ No newline at end of file
diff --git a/dom/security/test/gtest/TestCSPParser.cpp b/dom/security/test/gtest/TestCSPParser.cpp
new file mode 100644
index 0000000000..b8a4e986b6
--- /dev/null
+++ b/dom/security/test/gtest/TestCSPParser.cpp
@@ -0,0 +1,1148 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+
+#include <string.h>
+#include <stdlib.h>
+
+#include "nsIContentSecurityPolicy.h"
+#include "nsNetUtil.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/dom/nsCSPContext.h"
+#include "mozilla/gtest/MozAssertions.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsStringFwd.h"
+
+/*
+ * Testing the parser is non trivial, especially since we can not call
+ * parser functionality directly in compiled code tests.
+ * All the tests (except the fuzzy tests at the end) follow the same schemata:
+ * a) create an nsIContentSecurityPolicy object
+ * b) set the selfURI in SetRequestContextWithPrincipal
+ * c) append one or more policies by calling AppendPolicy
+ * d) check if the policy count is correct by calling GetPolicyCount
+ * e) compare the result of the policy with the expected output
+ * using the struct PolicyTest;
+ *
+ * In general we test:
+ * a) policies that the parser should accept
+ * b) policies that the parser should reject
+ * c) policies that are randomly generated (fuzzy tests)
+ *
+ * Please note that fuzzy tests are *DISABLED* by default and shold only
+ * be run *OFFLINE* whenever code in nsCSPParser changes.
+ * To run fuzzy tests, flip RUN_OFFLINE_TESTS to 1.
+ *
+ */
+
+#define RUN_OFFLINE_TESTS 0
+
+/*
+ * Offline tests are separated in three different groups:
+ * * TestFuzzyPolicies - complete random ASCII input
+ * * TestFuzzyPoliciesIncDir - a directory name followed by random ASCII
+ * * TestFuzzyPoliciesIncDirLimASCII - a directory name followed by limited
+ * ASCII which represents more likely user input.
+ *
+ * We run each of this categories |kFuzzyRuns| times.
+ */
+
+#if RUN_OFFLINE_TESTS
+static const uint32_t kFuzzyRuns = 10000;
+#endif
+
+// For fuzzy testing we actually do not care about the output,
+// we just want to make sure that the parser can handle random
+// input, therefore we use kFuzzyExpectedPolicyCount to return early.
+static const uint32_t kFuzzyExpectedPolicyCount = 111;
+
+static const uint32_t kMaxPolicyLength = 96;
+
+struct PolicyTest {
+ char policy[kMaxPolicyLength];
+ char expectedResult[kMaxPolicyLength];
+};
+
+nsresult runTest(
+ uint32_t aExpectedPolicyCount, // this should be 0 for policies which
+ // should fail to parse
+ const char* aPolicy, const char* aExpectedResult) {
+ nsresult rv;
+
+ // we init the csp with http://www.selfuri.com
+ nsCOMPtr<nsIURI> selfURI;
+ rv = NS_NewURI(getter_AddRefs(selfURI), "http://www.selfuri.com");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrincipal> selfURIPrincipal;
+ mozilla::OriginAttributes attrs;
+ selfURIPrincipal =
+ mozilla::BasePrincipal::CreateContentPrincipal(selfURI, attrs);
+ NS_ENSURE_TRUE(selfURIPrincipal, NS_ERROR_FAILURE);
+
+ // create a CSP object
+ nsCOMPtr<nsIContentSecurityPolicy> csp =
+ do_CreateInstance(NS_CSPCONTEXT_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // for testing the parser we only need to set a principal which is needed
+ // to translate the keyword 'self' into an actual URI.
+ rv =
+ csp->SetRequestContextWithPrincipal(selfURIPrincipal, selfURI, u""_ns, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // append a policy
+ nsString policyStr;
+ policyStr.AssignASCII(aPolicy);
+ rv = csp->AppendPolicy(policyStr, false, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // when executing fuzzy tests we do not care about the actual output
+ // of the parser, we just want to make sure that the parser is not crashing.
+ if (aExpectedPolicyCount == kFuzzyExpectedPolicyCount) {
+ return NS_OK;
+ }
+
+ // verify that the expected number of policies exists
+ uint32_t actualPolicyCount;
+ rv = csp->GetPolicyCount(&actualPolicyCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (actualPolicyCount != aExpectedPolicyCount) {
+ EXPECT_TRUE(false)
+ << "Actual policy count not equal to expected policy count ("
+ << actualPolicyCount << " != " << aExpectedPolicyCount
+ << ") for policy: " << aPolicy;
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ // if the expected policy count is 0, we can return, because
+ // we can not compare any output anyway. Used when parsing
+ // errornous policies.
+ if (aExpectedPolicyCount == 0) {
+ return NS_OK;
+ }
+
+ // compare the parsed policy against the expected result
+ nsString parsedPolicyStr;
+ // checking policy at index 0, which is the one what we appended.
+ rv = csp->GetPolicyString(0, parsedPolicyStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!NS_ConvertUTF16toUTF8(parsedPolicyStr).EqualsASCII(aExpectedResult)) {
+ EXPECT_TRUE(false) << "Actual policy does not match expected policy ("
+ << NS_ConvertUTF16toUTF8(parsedPolicyStr).get()
+ << " != " << aExpectedResult << ")";
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ return NS_OK;
+}
+
+// ============================= run Tests ========================
+
+nsresult runTestSuite(const PolicyTest* aPolicies, uint32_t aPolicyCount,
+ uint32_t aExpectedPolicyCount) {
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+
+ // Add prefs you need to set to parse CSP here, see comments for example
+ // bool examplePref = false;
+ if (prefs) {
+ // prefs->GetBoolPref("security.csp.examplePref", &examplePref);
+ // prefs->SetBoolPref("security.csp.examplePref", true);
+ }
+
+ for (uint32_t i = 0; i < aPolicyCount; i++) {
+ rv = runTest(aExpectedPolicyCount, aPolicies[i].policy,
+ aPolicies[i].expectedResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (prefs) {
+ // prefs->SetBoolPref("security.csp.examplePref", examplePref);
+ }
+
+ return NS_OK;
+}
+
+// ============================= TestDirectives ========================
+
+TEST(CSPParser, Directives)
+{
+ static const PolicyTest policies[] = {
+ // clang-format off
+ { "connect-src xn--mnchen-3ya.de",
+ "connect-src http://xn--mnchen-3ya.de"},
+ { "default-src http://www.example.com",
+ "default-src http://www.example.com" },
+ { "script-src http://www.example.com",
+ "script-src http://www.example.com" },
+ { "object-src http://www.example.com",
+ "object-src http://www.example.com" },
+ { "style-src http://www.example.com",
+ "style-src http://www.example.com" },
+ { "img-src http://www.example.com",
+ "img-src http://www.example.com" },
+ { "media-src http://www.example.com",
+ "media-src http://www.example.com" },
+ { "frame-src http://www.example.com",
+ "frame-src http://www.example.com" },
+ { "font-src http://www.example.com",
+ "font-src http://www.example.com" },
+ { "connect-src http://www.example.com",
+ "connect-src http://www.example.com" },
+ { "report-uri http://www.example.com",
+ "report-uri http://www.example.com/" },
+ { "script-src 'nonce-correctscriptnonce'",
+ "script-src 'nonce-correctscriptnonce'" },
+ { "script-src 'nonce-a'",
+ "script-src 'nonce-a'" },
+ { "script-src 'sha256-a'",
+ "script-src 'sha256-a'" },
+ { "script-src 'sha256-siVR8vAcqP06h2ppeNwqgjr0yZ6yned4X2VF84j4GmI='",
+ "script-src 'sha256-siVR8vAcqP06h2ppeNwqgjr0yZ6yned4X2VF84j4GmI='" },
+ { "script-src 'nonce-foo' 'unsafe-inline' ",
+ "script-src 'nonce-foo' 'unsafe-inline'" },
+ { "script-src 'nonce-foo' 'strict-dynamic' 'unsafe-inline' https: ",
+ "script-src 'nonce-foo' 'strict-dynamic' 'unsafe-inline' https:" },
+ { "script-src 'nonce-foo' 'strict-dynamic' 'unsafe-inline' 'report-sample' https: ",
+ "script-src 'nonce-foo' 'strict-dynamic' 'unsafe-inline' 'report-sample' https:" },
+ { "default-src 'sha256-siVR8' 'strict-dynamic' 'unsafe-inline' https: ",
+ "default-src 'sha256-siVR8' 'strict-dynamic' 'unsafe-inline' https:" },
+ { "worker-src https://example.com",
+ "worker-src https://example.com" },
+ { "worker-src http://worker.com; frame-src http://frame.com; child-src http://child.com",
+ "worker-src http://worker.com; frame-src http://frame.com; child-src http://child.com" },
+ { "script-src 'unsafe-allow-redirects' http://example.com",
+ "script-src http://example.com"},
+ // clang-format on
+ };
+
+ uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest);
+ ASSERT_NS_SUCCEEDED(runTestSuite(policies, policyCount, 1));
+}
+
+// ============================= TestKeywords ========================
+
+TEST(CSPParser, Keywords)
+{
+ static const PolicyTest policies[] = {
+ // clang-format off
+ { "script-src 'self'",
+ "script-src 'self'" },
+ { "script-src 'unsafe-inline'",
+ "script-src 'unsafe-inline'" },
+ { "script-src 'unsafe-eval'",
+ "script-src 'unsafe-eval'" },
+ { "script-src 'unsafe-inline' 'unsafe-eval'",
+ "script-src 'unsafe-inline' 'unsafe-eval'" },
+ { "script-src 'none'",
+ "script-src 'none'" },
+ { "script-src 'wasm-unsafe-eval'",
+ "script-src 'wasm-unsafe-eval'" },
+ { "img-src 'none'; script-src 'unsafe-eval' 'unsafe-inline'; default-src 'self'",
+ "img-src 'none'; script-src 'unsafe-eval' 'unsafe-inline'; default-src 'self'" },
+ // clang-format on
+ };
+
+ uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest);
+ ASSERT_NS_SUCCEEDED(runTestSuite(policies, policyCount, 1));
+}
+
+// =================== TestIgnoreUpperLowerCasePolicies ==============
+
+TEST(CSPParser, IgnoreUpperLowerCasePolicies)
+{
+ static const PolicyTest policies[] = {
+ // clang-format off
+ { "script-src 'SELF'",
+ "script-src 'self'" },
+ { "sCriPt-src 'Unsafe-Inline'",
+ "script-src 'unsafe-inline'" },
+ { "SCRIPT-src 'unsafe-eval'",
+ "script-src 'unsafe-eval'" },
+ { "default-SRC 'unsafe-inline' 'unsafe-eval'",
+ "default-src 'unsafe-inline' 'unsafe-eval'" },
+ { "script-src 'NoNe'",
+ "script-src 'none'" },
+ { "img-sRc 'noNe'; scrIpt-src 'unsafe-EVAL' 'UNSAFE-inline'; deFAULT-src 'Self'",
+ "img-src 'none'; script-src 'unsafe-eval' 'unsafe-inline'; default-src 'self'" },
+ { "default-src HTTP://www.example.com",
+ "default-src http://www.example.com" },
+ { "default-src HTTP://WWW.EXAMPLE.COM",
+ "default-src http://www.example.com" },
+ { "default-src HTTPS://*.example.COM",
+ "default-src https://*.example.com" },
+ { "script-src 'none' test.com;",
+ "script-src http://test.com" },
+ { "script-src 'NoNCE-correctscriptnonce'",
+ "script-src 'nonce-correctscriptnonce'" },
+ { "script-src 'NoncE-NONCENEEDSTOBEUPPERCASE'",
+ "script-src 'nonce-NONCENEEDSTOBEUPPERCASE'" },
+ { "script-src 'SHA256-siVR8vAcqP06h2ppeNwqgjr0yZ6yned4X2VF84j4GmI='",
+ "script-src 'sha256-siVR8vAcqP06h2ppeNwqgjr0yZ6yned4X2VF84j4GmI='" },
+ { "upgrade-INSECURE-requests",
+ "upgrade-insecure-requests" },
+ { "sanDBox alloW-foRMs",
+ "sandbox allow-forms"},
+ // clang-format on
+ };
+
+ uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest);
+ ASSERT_NS_SUCCEEDED(runTestSuite(policies, policyCount, 1));
+}
+
+// ========================= TestPaths ===============================
+
+TEST(CSPParser, Paths)
+{
+ static const PolicyTest policies[] = {
+ // clang-format off
+ { "script-src http://www.example.com",
+ "script-src http://www.example.com" },
+ { "script-src http://www.example.com/",
+ "script-src http://www.example.com/" },
+ { "script-src http://www.example.com/path-1",
+ "script-src http://www.example.com/path-1" },
+ { "script-src http://www.example.com/path-1/",
+ "script-src http://www.example.com/path-1/" },
+ { "script-src http://www.example.com/path-1/path_2",
+ "script-src http://www.example.com/path-1/path_2" },
+ { "script-src http://www.example.com/path-1/path_2/",
+ "script-src http://www.example.com/path-1/path_2/" },
+ { "script-src http://www.example.com/path-1/path_2/file.js",
+ "script-src http://www.example.com/path-1/path_2/file.js" },
+ { "script-src http://www.example.com/path-1/path_2/file_1.js",
+ "script-src http://www.example.com/path-1/path_2/file_1.js" },
+ { "script-src http://www.example.com/path-1/path_2/file-2.js",
+ "script-src http://www.example.com/path-1/path_2/file-2.js" },
+ { "script-src http://www.example.com/path-1/path_2/f.js",
+ "script-src http://www.example.com/path-1/path_2/f.js" },
+ { "script-src http://www.example.com:88",
+ "script-src http://www.example.com:88" },
+ { "script-src http://www.example.com:88/",
+ "script-src http://www.example.com:88/" },
+ { "script-src http://www.example.com:88/path-1",
+ "script-src http://www.example.com:88/path-1" },
+ { "script-src http://www.example.com:88/path-1/",
+ "script-src http://www.example.com:88/path-1/" },
+ { "script-src http://www.example.com:88/path-1/path_2",
+ "script-src http://www.example.com:88/path-1/path_2" },
+ { "script-src http://www.example.com:88/path-1/path_2/",
+ "script-src http://www.example.com:88/path-1/path_2/" },
+ { "script-src http://www.example.com:88/path-1/path_2/file.js",
+ "script-src http://www.example.com:88/path-1/path_2/file.js" },
+ { "script-src http://www.example.com:*",
+ "script-src http://www.example.com:*" },
+ { "script-src http://www.example.com:*/",
+ "script-src http://www.example.com:*/" },
+ { "script-src http://www.example.com:*/path-1",
+ "script-src http://www.example.com:*/path-1" },
+ { "script-src http://www.example.com:*/path-1/",
+ "script-src http://www.example.com:*/path-1/" },
+ { "script-src http://www.example.com:*/path-1/path_2",
+ "script-src http://www.example.com:*/path-1/path_2" },
+ { "script-src http://www.example.com:*/path-1/path_2/",
+ "script-src http://www.example.com:*/path-1/path_2/" },
+ { "script-src http://www.example.com:*/path-1/path_2/file.js",
+ "script-src http://www.example.com:*/path-1/path_2/file.js" },
+ { "script-src http://www.example.com#foo",
+ "script-src http://www.example.com" },
+ { "script-src http://www.example.com?foo=bar",
+ "script-src http://www.example.com" },
+ { "script-src http://www.example.com:8888#foo",
+ "script-src http://www.example.com:8888" },
+ { "script-src http://www.example.com:8888?foo",
+ "script-src http://www.example.com:8888" },
+ { "script-src http://www.example.com/#foo",
+ "script-src http://www.example.com/" },
+ { "script-src http://www.example.com/?foo",
+ "script-src http://www.example.com/" },
+ { "script-src http://www.example.com/path-1/file.js#foo",
+ "script-src http://www.example.com/path-1/file.js" },
+ { "script-src http://www.example.com/path-1/file.js?foo",
+ "script-src http://www.example.com/path-1/file.js" },
+ { "script-src http://www.example.com/path-1/file.js?foo#bar",
+ "script-src http://www.example.com/path-1/file.js" },
+ { "report-uri http://www.example.com/",
+ "report-uri http://www.example.com/" },
+ { "report-uri http://www.example.com:8888/asdf",
+ "report-uri http://www.example.com:8888/asdf" },
+ { "report-uri http://www.example.com:8888/path_1/path_2",
+ "report-uri http://www.example.com:8888/path_1/path_2" },
+ { "report-uri http://www.example.com:8888/path_1/path_2/report.sjs&301",
+ "report-uri http://www.example.com:8888/path_1/path_2/report.sjs&301" },
+ { "report-uri /examplepath",
+ "report-uri http://www.selfuri.com/examplepath" },
+ { "connect-src http://www.example.com/foo%3Bsessionid=12%2C34",
+ "connect-src http://www.example.com/foo;sessionid=12,34" },
+ { "connect-src http://www.example.com/foo%3bsessionid=12%2c34",
+ "connect-src http://www.example.com/foo;sessionid=12,34" },
+ { "connect-src http://test.com/pathIncludingAz19-._~!$&'()*+=:@",
+ "connect-src http://test.com/pathIncludingAz19-._~!$&'()*+=:@" },
+ { "script-src http://www.example.com:88/.js",
+ "script-src http://www.example.com:88/.js" },
+ { "script-src https://foo.com/_abc/abc_/_/_a_b_c_",
+ "script-src https://foo.com/_abc/abc_/_/_a_b_c_" }
+ // clang-format on
+ };
+
+ uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest);
+ ASSERT_NS_SUCCEEDED(runTestSuite(policies, policyCount, 1));
+}
+
+// ======================== TestSimplePolicies =======================
+
+TEST(CSPParser, SimplePolicies)
+{
+ static const PolicyTest policies[] = {
+ // clang-format off
+ { "frame-src intent:",
+ "frame-src intent:" },
+ { "frame-src intent://host.name",
+ "frame-src intent://host.name" },
+ { "frame-src intent://my.host.link/",
+ "frame-src intent://my.host.link/" },
+ { "default-src *",
+ "default-src *" },
+ { "default-src https:",
+ "default-src https:" },
+ { "default-src https://*",
+ "default-src https://*" },
+ { "default-src *:*",
+ "default-src http://*:*" },
+ { "default-src *:80",
+ "default-src http://*:80" },
+ { "default-src http://*:80",
+ "default-src http://*:80" },
+ { "default-src javascript:",
+ "default-src javascript:" },
+ { "default-src data:",
+ "default-src data:" },
+ { "script-src 'unsafe-eval' 'unsafe-inline' http://www.example.com",
+ "script-src 'unsafe-eval' 'unsafe-inline' http://www.example.com" },
+ { "object-src 'self'",
+ "object-src 'self'" },
+ { "style-src http://www.example.com 'self'",
+ "style-src http://www.example.com 'self'" },
+ { "media-src http://www.example.com http://www.test.com",
+ "media-src http://www.example.com http://www.test.com" },
+ { "connect-src http://www.test.com example.com *.other.com;",
+ "connect-src http://www.test.com http://example.com http://*.other.com"},
+ { "connect-src example.com *.other.com",
+ "connect-src http://example.com http://*.other.com"},
+ { "style-src *.other.com example.com",
+ "style-src http://*.other.com http://example.com"},
+ { "default-src 'self'; img-src *;",
+ "default-src 'self'; img-src *" },
+ { "object-src media1.example.com media2.example.com *.cdn.example.com;",
+ "object-src http://media1.example.com http://media2.example.com http://*.cdn.example.com" },
+ { "script-src trustedscripts.example.com",
+ "script-src http://trustedscripts.example.com" },
+ { "script-src 'self' ; default-src trustedscripts.example.com",
+ "script-src 'self'; default-src http://trustedscripts.example.com" },
+ { "default-src 'none'; report-uri http://localhost:49938/test",
+ "default-src 'none'; report-uri http://localhost:49938/test" },
+ { " ; default-src abc",
+ "default-src http://abc" },
+ { " ; ; ; ; default-src abc ; ; ; ;",
+ "default-src http://abc" },
+ { "script-src 'none' 'none' 'none';",
+ "script-src 'none'" },
+ { "script-src http://www.example.com/path-1//",
+ "script-src http://www.example.com/path-1//" },
+ { "script-src http://www.example.com/path-1//path_2",
+ "script-src http://www.example.com/path-1//path_2" },
+ { "default-src 127.0.0.1",
+ "default-src http://127.0.0.1" },
+ { "default-src 127.0.0.1:*",
+ "default-src http://127.0.0.1:*" },
+ { "default-src -; ",
+ "default-src http://-" },
+ { "script-src 1",
+ "script-src http://1" },
+ { "upgrade-insecure-requests",
+ "upgrade-insecure-requests" },
+ { "upgrade-insecure-requests https:",
+ "upgrade-insecure-requests" },
+ { "sandbox allow-scripts allow-forms ",
+ "sandbox allow-scripts allow-forms" },
+ // clang-format on
+ };
+
+ uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest);
+ ASSERT_NS_SUCCEEDED(runTestSuite(policies, policyCount, 1));
+}
+
+// =================== TestPoliciesWithInvalidSrc ====================
+
+TEST(CSPParser, PoliciesWithInvalidSrc)
+{
+ static const PolicyTest policies[] = {
+ // clang-format off
+ { "script-src 'self'; SCRIPT-SRC http://www.example.com",
+ "script-src 'self'" },
+ { "script-src 'none' test.com; script-src example.com",
+ "script-src http://test.com" },
+ { "default-src **",
+ "default-src 'none'" },
+ { "default-src 'self",
+ "default-src 'none'" },
+ { "default-src 'unsafe-inlin' ",
+ "default-src 'none'" },
+ { "default-src */",
+ "default-src 'none'" },
+ { "default-src",
+ "default-src 'none'" },
+ { "default-src 'unsafe-inlin' ",
+ "default-src 'none'" },
+ { "default-src :88",
+ "default-src 'none'" },
+ { "script-src abc::::::88",
+ "script-src 'none'" },
+ { "script-src *.*:*",
+ "script-src 'none'" },
+ { "img-src *::88",
+ "img-src 'none'" },
+ { "object-src http://localhost:",
+ "object-src 'none'" },
+ { "script-src test..com",
+ "script-src 'none'" },
+ { "script-src sub1.sub2.example+",
+ "script-src 'none'" },
+ { "script-src http://www.example.com//",
+ "script-src 'none'" },
+ { "script-src http://www.example.com:88path-1/",
+ "script-src 'none'" },
+ { "script-src http://www.example.com:88//",
+ "script-src 'none'" },
+ { "script-src http://www.example.com:88//path-1",
+ "script-src 'none'" },
+ { "script-src http://www.example.com:88//path-1",
+ "script-src 'none'" },
+ { "script-src http://www.example.com:88.js",
+ "script-src 'none'" },
+ { "script-src http://www.example.com:*.js",
+ "script-src 'none'" },
+ { "script-src http://www.example.com:*.",
+ "script-src 'none'" },
+ { "script-src 'nonce-{invalid}'",
+ "script-src 'none'" },
+ { "script-src 'sha256-{invalid}'",
+ "script-src 'none'" },
+ { "script-src 'nonce-in$valid'",
+ "script-src 'none'" },
+ { "script-src 'sha256-in$valid'",
+ "script-src 'none'" },
+ { "script-src 'nonce-invalid==='",
+ "script-src 'none'" },
+ { "script-src 'sha256-invalid==='",
+ "script-src 'none'" },
+ { "script-src 'nonce-==='",
+ "script-src 'none'" },
+ { "script-src 'sha256-==='",
+ "script-src 'none'" },
+ { "script-src 'nonce-=='",
+ "script-src 'none'" },
+ { "script-src 'sha256-=='",
+ "script-src 'none'" },
+ { "script-src 'nonce-='",
+ "script-src 'none'" },
+ { "script-src 'sha256-='",
+ "script-src 'none'" },
+ { "script-src 'nonce-'",
+ "script-src 'none'" },
+ { "script-src 'sha256-'",
+ "script-src 'none'" },
+ { "connect-src http://www.example.com/foo%zz;",
+ "connect-src 'none'" },
+ { "script-src https://foo.com/%$",
+ "script-src 'none'" },
+ { "sandbox foo",
+ "sandbox"},
+ // clang-format on
+ };
+
+ // amount of tests - 1, because the latest should be ignored.
+ uint32_t policyCount = (sizeof(policies) / sizeof(PolicyTest)) - 1;
+ ASSERT_NS_SUCCEEDED(runTestSuite(policies, policyCount, 1));
+}
+
+// ============================= TestBadPolicies =======================
+
+TEST(CSPParser, BadPolicies)
+{
+ static const PolicyTest policies[] = {
+ // clang-format off
+ { "script-sr 'self", "" },
+ { "", "" },
+ { "; ; ; ; ; ; ;", "" },
+ { "defaut-src asdf", "" },
+ { "default-src: aaa", "" },
+ { "asdf http://test.com", ""},
+ { "report-uri", ""},
+ { "report-uri http://:foo", ""},
+ { "require-sri-for", ""},
+ { "require-sri-for style", ""},
+ // clang-format on
+ };
+
+ uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest);
+ ASSERT_NS_SUCCEEDED(runTestSuite(policies, policyCount, 0));
+}
+
+// ======================= TestGoodGeneratedPolicies =================
+
+TEST(CSPParser, GoodGeneratedPolicies)
+{
+ static const PolicyTest policies[] = {
+ // clang-format off
+ { "default-src 'self'; img-src *",
+ "default-src 'self'; img-src *" },
+ { "report-uri /policy",
+ "report-uri http://www.selfuri.com/policy"},
+ { "img-src *",
+ "img-src *" },
+ { "media-src foo.bar",
+ "media-src http://foo.bar" },
+ { "frame-src *.bar",
+ "frame-src http://*.bar" },
+ { "font-src com",
+ "font-src http://com" },
+ { "connect-src f00b4r.com",
+ "connect-src http://f00b4r.com" },
+ { "script-src *.a.b.c",
+ "script-src http://*.a.b.c" },
+ { "object-src *.b.c",
+ "object-src http://*.b.c" },
+ { "style-src a.b.c",
+ "style-src http://a.b.c" },
+ { "img-src a.com",
+ "img-src http://a.com" },
+ { "media-src http://abc.com",
+ "media-src http://abc.com" },
+ { "frame-src a2-c.com",
+ "frame-src http://a2-c.com" },
+ { "font-src https://a.com",
+ "font-src https://a.com" },
+ { "connect-src *.a.com",
+ "connect-src http://*.a.com" },
+ { "default-src a.com:23",
+ "default-src http://a.com:23" },
+ { "script-src https://a.com:200",
+ "script-src https://a.com:200" },
+ { "object-src data:",
+ "object-src data:" },
+ { "style-src javascript:",
+ "style-src javascript:" },
+ { "frame-src https://foobar.com:443",
+ "frame-src https://foobar.com:443" },
+ { "font-src https://a.com:443",
+ "font-src https://a.com:443" },
+ { "connect-src http://a.com:80",
+ "connect-src http://a.com:80" },
+ { "default-src http://foobar.com",
+ "default-src http://foobar.com" },
+ { "script-src https://foobar.com",
+ "script-src https://foobar.com" },
+ { "style-src 'none'",
+ "style-src 'none'" },
+ { "img-src foo.bar:21 https://ras.bar",
+ "img-src http://foo.bar:21 https://ras.bar" },
+ { "media-src http://foo.bar:21 https://ras.bar:443",
+ "media-src http://foo.bar:21 https://ras.bar:443" },
+ { "frame-src http://self.com:80",
+ "frame-src http://self.com:80" },
+ { "font-src http://self.com",
+ "font-src http://self.com" },
+ { "connect-src https://foo.com http://bar.com:88",
+ "connect-src https://foo.com http://bar.com:88" },
+ { "default-src * https://bar.com 'none'",
+ "default-src * https://bar.com" },
+ { "script-src *.foo.com",
+ "script-src http://*.foo.com" },
+ { "object-src http://b.com",
+ "object-src http://b.com" },
+ { "style-src http://bar.com:88",
+ "style-src http://bar.com:88" },
+ { "img-src https://bar.com:88",
+ "img-src https://bar.com:88" },
+ { "media-src http://bar.com:443",
+ "media-src http://bar.com:443" },
+ { "frame-src https://foo.com:88",
+ "frame-src https://foo.com:88" },
+ { "font-src http://foo.com",
+ "font-src http://foo.com" },
+ { "connect-src http://x.com:23",
+ "connect-src http://x.com:23" },
+ { "default-src http://barbaz.com",
+ "default-src http://barbaz.com" },
+ { "script-src http://somerandom.foo.com",
+ "script-src http://somerandom.foo.com" },
+ { "default-src *",
+ "default-src *" },
+ { "style-src http://bar.com:22",
+ "style-src http://bar.com:22" },
+ { "img-src https://foo.com:443",
+ "img-src https://foo.com:443" },
+ { "script-src https://foo.com; ",
+ "script-src https://foo.com" },
+ { "img-src bar.com:*",
+ "img-src http://bar.com:*" },
+ { "font-src https://foo.com:400",
+ "font-src https://foo.com:400" },
+ { "connect-src http://bar.com:400",
+ "connect-src http://bar.com:400" },
+ { "default-src http://evil.com",
+ "default-src http://evil.com" },
+ { "script-src https://evil.com:100",
+ "script-src https://evil.com:100" },
+ { "default-src bar.com; script-src https://foo.com",
+ "default-src http://bar.com; script-src https://foo.com" },
+ { "default-src 'self'; script-src 'self' https://*:*",
+ "default-src 'self'; script-src 'self' https://*:*" },
+ { "img-src http://self.com:34",
+ "img-src http://self.com:34" },
+ { "media-src http://subd.self.com:34",
+ "media-src http://subd.self.com:34" },
+ { "default-src 'none'",
+ "default-src 'none'" },
+ { "connect-src http://self",
+ "connect-src http://self" },
+ { "default-src http://foo",
+ "default-src http://foo" },
+ { "script-src http://foo:80",
+ "script-src http://foo:80" },
+ { "object-src http://bar",
+ "object-src http://bar" },
+ { "style-src http://three:80",
+ "style-src http://three:80" },
+ { "img-src https://foo:400",
+ "img-src https://foo:400" },
+ { "media-src https://self:34",
+ "media-src https://self:34" },
+ { "frame-src https://bar",
+ "frame-src https://bar" },
+ { "font-src http://three:81",
+ "font-src http://three:81" },
+ { "connect-src https://three:81",
+ "connect-src https://three:81" },
+ { "script-src http://self.com:80/foo",
+ "script-src http://self.com:80/foo" },
+ { "object-src http://self.com/foo",
+ "object-src http://self.com/foo" },
+ { "report-uri /report.py",
+ "report-uri http://www.selfuri.com/report.py"},
+ { "img-src http://foo.org:34/report.py",
+ "img-src http://foo.org:34/report.py" },
+ { "media-src foo/bar/report.py",
+ "media-src http://foo/bar/report.py" },
+ { "report-uri /",
+ "report-uri http://www.selfuri.com/"},
+ { "font-src https://self.com/report.py",
+ "font-src https://self.com/report.py" },
+ { "connect-src https://foo.com/report.py",
+ "connect-src https://foo.com/report.py" },
+ { "default-src *; report-uri http://www.reporturi.com/",
+ "default-src *; report-uri http://www.reporturi.com/" },
+ { "default-src http://first.com",
+ "default-src http://first.com" },
+ { "script-src http://second.com",
+ "script-src http://second.com" },
+ { "object-src http://third.com",
+ "object-src http://third.com" },
+ { "style-src https://foobar.com:4443",
+ "style-src https://foobar.com:4443" },
+ { "img-src http://foobar.com:4443",
+ "img-src http://foobar.com:4443" },
+ { "media-src bar.com",
+ "media-src http://bar.com" },
+ { "frame-src http://bar.com",
+ "frame-src http://bar.com" },
+ { "font-src http://self.com/",
+ "font-src http://self.com/" },
+ { "script-src 'self'",
+ "script-src 'self'" },
+ { "default-src http://self.com/foo.png",
+ "default-src http://self.com/foo.png" },
+ { "script-src http://self.com/foo.js",
+ "script-src http://self.com/foo.js" },
+ { "object-src http://bar.com/foo.js",
+ "object-src http://bar.com/foo.js" },
+ { "style-src http://FOO.COM",
+ "style-src http://foo.com" },
+ { "img-src HTTP",
+ "img-src http://http" },
+ { "media-src http",
+ "media-src http://http" },
+ { "frame-src 'SELF'",
+ "frame-src 'self'" },
+ { "DEFAULT-src 'self';",
+ "default-src 'self'" },
+ { "default-src 'self' http://FOO.COM",
+ "default-src 'self' http://foo.com" },
+ { "default-src 'self' HTTP://foo.com",
+ "default-src 'self' http://foo.com" },
+ { "default-src 'NONE'",
+ "default-src 'none'" },
+ { "script-src policy-uri ",
+ "script-src http://policy-uri" },
+ { "img-src 'self'; ",
+ "img-src 'self'" },
+ { "frame-ancestors foo-bar.com",
+ "frame-ancestors http://foo-bar.com" },
+ { "frame-ancestors http://a.com",
+ "frame-ancestors http://a.com" },
+ { "frame-ancestors 'self'",
+ "frame-ancestors 'self'" },
+ { "frame-ancestors http://self.com:88",
+ "frame-ancestors http://self.com:88" },
+ { "frame-ancestors http://a.b.c.d.e.f.g.h.i.j.k.l.x.com",
+ "frame-ancestors http://a.b.c.d.e.f.g.h.i.j.k.l.x.com" },
+ { "frame-ancestors https://self.com:34",
+ "frame-ancestors https://self.com:34" },
+ { "frame-ancestors http://sampleuser:samplepass@example.com",
+ "frame-ancestors 'none'" },
+ { "default-src 'none'; frame-ancestors 'self'",
+ "default-src 'none'; frame-ancestors 'self'" },
+ { "frame-ancestors http://self:80",
+ "frame-ancestors http://self:80" },
+ { "frame-ancestors http://self.com/bar",
+ "frame-ancestors http://self.com/bar" },
+ { "default-src 'self'; frame-ancestors 'self'",
+ "default-src 'self'; frame-ancestors 'self'" },
+ { "frame-ancestors http://bar.com/foo.png",
+ "frame-ancestors http://bar.com/foo.png" },
+ // clang-format on
+ };
+
+ uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest);
+ ASSERT_NS_SUCCEEDED(runTestSuite(policies, policyCount, 1));
+}
+
+// ==================== TestBadGeneratedPolicies ====================
+
+TEST(CSPParser, BadGeneratedPolicies)
+{
+ static const PolicyTest policies[] = {
+ // clang-format off
+ { "foo.*.bar", ""},
+ { "foo!bar.com", ""},
+ { "x.*.a.com", ""},
+ { "a#2-c.com", ""},
+ { "http://foo.com:bar.com:23", ""},
+ { "f!oo.bar", ""},
+ { "ht!ps://f-oo.bar", ""},
+ { "https://f-oo.bar:3f", ""},
+ { "**", ""},
+ { "*a", ""},
+ { "http://username:password@self.com/foo", ""},
+ { "http://other:pass1@self.com/foo", ""},
+ { "http://user1:pass1@self.com/foo", ""},
+ { "http://username:password@self.com/bar", ""},
+ // clang-format on
+ };
+
+ uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest);
+ ASSERT_NS_SUCCEEDED(runTestSuite(policies, policyCount, 0));
+}
+
+// ============ TestGoodGeneratedPoliciesForPathHandling =============
+
+TEST(CSPParser, GoodGeneratedPoliciesForPathHandling)
+{
+ // Once bug 808292 (Implement path-level host-source matching to CSP)
+ // lands we have to update the expected output to include the parsed path
+
+ static const PolicyTest policies[] = {
+ // clang-format off
+ { "img-src http://test1.example.com",
+ "img-src http://test1.example.com" },
+ { "img-src http://test1.example.com/",
+ "img-src http://test1.example.com/" },
+ { "img-src http://test1.example.com/path-1",
+ "img-src http://test1.example.com/path-1" },
+ { "img-src http://test1.example.com/path-1/",
+ "img-src http://test1.example.com/path-1/" },
+ { "img-src http://test1.example.com/path-1/path_2/",
+ "img-src http://test1.example.com/path-1/path_2/" },
+ { "img-src http://test1.example.com/path-1/path_2/file.js",
+ "img-src http://test1.example.com/path-1/path_2/file.js" },
+ { "img-src http://test1.example.com/path-1/path_2/file_1.js",
+ "img-src http://test1.example.com/path-1/path_2/file_1.js" },
+ { "img-src http://test1.example.com/path-1/path_2/file-2.js",
+ "img-src http://test1.example.com/path-1/path_2/file-2.js" },
+ { "img-src http://test1.example.com/path-1/path_2/f.js",
+ "img-src http://test1.example.com/path-1/path_2/f.js" },
+ { "img-src http://test1.example.com/path-1/path_2/f.oo.js",
+ "img-src http://test1.example.com/path-1/path_2/f.oo.js" },
+ { "img-src test1.example.com",
+ "img-src http://test1.example.com" },
+ { "img-src test1.example.com/",
+ "img-src http://test1.example.com/" },
+ { "img-src test1.example.com/path-1",
+ "img-src http://test1.example.com/path-1" },
+ { "img-src test1.example.com/path-1/",
+ "img-src http://test1.example.com/path-1/" },
+ { "img-src test1.example.com/path-1/path_2/",
+ "img-src http://test1.example.com/path-1/path_2/" },
+ { "img-src test1.example.com/path-1/path_2/file.js",
+ "img-src http://test1.example.com/path-1/path_2/file.js" },
+ { "img-src test1.example.com/path-1/path_2/file_1.js",
+ "img-src http://test1.example.com/path-1/path_2/file_1.js" },
+ { "img-src test1.example.com/path-1/path_2/file-2.js",
+ "img-src http://test1.example.com/path-1/path_2/file-2.js" },
+ { "img-src test1.example.com/path-1/path_2/f.js",
+ "img-src http://test1.example.com/path-1/path_2/f.js" },
+ { "img-src test1.example.com/path-1/path_2/f.oo.js",
+ "img-src http://test1.example.com/path-1/path_2/f.oo.js" },
+ { "img-src *.example.com",
+ "img-src http://*.example.com" },
+ { "img-src *.example.com/",
+ "img-src http://*.example.com/" },
+ { "img-src *.example.com/path-1",
+ "img-src http://*.example.com/path-1" },
+ { "img-src *.example.com/path-1/",
+ "img-src http://*.example.com/path-1/" },
+ { "img-src *.example.com/path-1/path_2/",
+ "img-src http://*.example.com/path-1/path_2/" },
+ { "img-src *.example.com/path-1/path_2/file.js",
+ "img-src http://*.example.com/path-1/path_2/file.js" },
+ { "img-src *.example.com/path-1/path_2/file_1.js",
+ "img-src http://*.example.com/path-1/path_2/file_1.js" },
+ { "img-src *.example.com/path-1/path_2/file-2.js",
+ "img-src http://*.example.com/path-1/path_2/file-2.js" },
+ { "img-src *.example.com/path-1/path_2/f.js",
+ "img-src http://*.example.com/path-1/path_2/f.js" },
+ { "img-src *.example.com/path-1/path_2/f.oo.js",
+ "img-src http://*.example.com/path-1/path_2/f.oo.js" },
+ { "img-src test1.example.com:80",
+ "img-src http://test1.example.com:80" },
+ { "img-src test1.example.com:80/",
+ "img-src http://test1.example.com:80/" },
+ { "img-src test1.example.com:80/path-1",
+ "img-src http://test1.example.com:80/path-1" },
+ { "img-src test1.example.com:80/path-1/",
+ "img-src http://test1.example.com:80/path-1/" },
+ { "img-src test1.example.com:80/path-1/path_2",
+ "img-src http://test1.example.com:80/path-1/path_2" },
+ { "img-src test1.example.com:80/path-1/path_2/",
+ "img-src http://test1.example.com:80/path-1/path_2/" },
+ { "img-src test1.example.com:80/path-1/path_2/file.js",
+ "img-src http://test1.example.com:80/path-1/path_2/file.js" },
+ { "img-src test1.example.com:80/path-1/path_2/f.ile.js",
+ "img-src http://test1.example.com:80/path-1/path_2/f.ile.js" },
+ { "img-src test1.example.com:*",
+ "img-src http://test1.example.com:*" },
+ { "img-src test1.example.com:*/",
+ "img-src http://test1.example.com:*/" },
+ { "img-src test1.example.com:*/path-1",
+ "img-src http://test1.example.com:*/path-1" },
+ { "img-src test1.example.com:*/path-1/",
+ "img-src http://test1.example.com:*/path-1/" },
+ { "img-src test1.example.com:*/path-1/path_2",
+ "img-src http://test1.example.com:*/path-1/path_2" },
+ { "img-src test1.example.com:*/path-1/path_2/",
+ "img-src http://test1.example.com:*/path-1/path_2/" },
+ { "img-src test1.example.com:*/path-1/path_2/file.js",
+ "img-src http://test1.example.com:*/path-1/path_2/file.js" },
+ { "img-src test1.example.com:*/path-1/path_2/f.ile.js",
+ "img-src http://test1.example.com:*/path-1/path_2/f.ile.js" },
+ { "img-src http://test1.example.com/abc//",
+ "img-src http://test1.example.com/abc//" },
+ { "img-src https://test1.example.com/abc/def//",
+ "img-src https://test1.example.com/abc/def//" },
+ { "img-src https://test1.example.com/abc/def/ghi//",
+ "img-src https://test1.example.com/abc/def/ghi//" },
+ { "img-src http://test1.example.com:80/abc//",
+ "img-src http://test1.example.com:80/abc//" },
+ { "img-src https://test1.example.com:80/abc/def//",
+ "img-src https://test1.example.com:80/abc/def//" },
+ { "img-src https://test1.example.com:80/abc/def/ghi//",
+ "img-src https://test1.example.com:80/abc/def/ghi//" },
+ { "img-src https://test1.example.com/abc////////////def/",
+ "img-src https://test1.example.com/abc////////////def/" },
+ { "img-src https://test1.example.com/abc////////////",
+ "img-src https://test1.example.com/abc////////////" },
+ // clang-format on
+ };
+
+ uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest);
+ ASSERT_NS_SUCCEEDED(runTestSuite(policies, policyCount, 1));
+}
+
+// ============== TestBadGeneratedPoliciesForPathHandling ============
+
+TEST(CSPParser, BadGeneratedPoliciesForPathHandling)
+{
+ static const PolicyTest policies[] = {
+ // clang-format off
+ { "img-src test1.example.com:88path-1/",
+ "img-src 'none'" },
+ { "img-src test1.example.com:80.js",
+ "img-src 'none'" },
+ { "img-src test1.example.com:*.js",
+ "img-src 'none'" },
+ { "img-src test1.example.com:*.",
+ "img-src 'none'" },
+ { "img-src http://test1.example.com//",
+ "img-src 'none'" },
+ { "img-src http://test1.example.com:80//",
+ "img-src 'none'" },
+ { "img-src http://test1.example.com:80abc",
+ "img-src 'none'" },
+ // clang-format on
+ };
+
+ uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest);
+ ASSERT_NS_SUCCEEDED(runTestSuite(policies, policyCount, 1));
+}
+
+// ======================== TestFuzzyPolicies ========================
+
+// Use a policy, eliminate one character at a time,
+// and feed it as input to the parser.
+
+TEST(CSPParser, ShorteningPolicies)
+{
+ char pol[] =
+ "default-src http://www.sub1.sub2.example.com:88/path1/path2/ "
+ "'unsafe-inline' 'none'";
+ uint32_t len = static_cast<uint32_t>(sizeof(pol));
+
+ PolicyTest testPol[1];
+ memset(&testPol[0].policy, '\0', kMaxPolicyLength * sizeof(char));
+
+ while (--len) {
+ memset(&testPol[0].policy, '\0', kMaxPolicyLength * sizeof(char));
+ memcpy(&testPol[0].policy, &pol, len * sizeof(char));
+ ASSERT_TRUE(
+ NS_SUCCEEDED(runTestSuite(testPol, 1, kFuzzyExpectedPolicyCount)));
+ }
+}
+
+// ============================= TestFuzzyPolicies ===================
+
+// We generate kFuzzyRuns inputs by (pseudo) randomly picking from the 128
+// ASCII characters; feed them to the parser and verfy that the parser
+// handles the input gracefully.
+//
+// Please note, that by using srand(0) we get deterministic results!
+
+#if RUN_OFFLINE_TESTS
+
+TEST(CSPParser, FuzzyPolicies)
+{
+ // init srand with 0 so we get same results
+ srand(0);
+
+ PolicyTest testPol[1];
+ memset(&testPol[0].policy, '\0', kMaxPolicyLength);
+
+ for (uint32_t index = 0; index < kFuzzyRuns; index++) {
+ // randomly select the length of the next policy
+ uint32_t polLength = rand() % kMaxPolicyLength;
+ // reset memory of the policy string
+ memset(&testPol[0].policy, '\0', kMaxPolicyLength * sizeof(char));
+
+ for (uint32_t i = 0; i < polLength; i++) {
+ // fill the policy array with random ASCII chars
+ testPol[0].policy[i] = static_cast<char>(rand() % 128);
+ }
+ ASSERT_TRUE(
+ NS_SUCCEEDED(runTestSuite(testPol, 1, kFuzzyExpectedPolicyCount)));
+ }
+}
+
+#endif
+
+// ======================= TestFuzzyPoliciesIncDir ===================
+
+// In a similar fashion as in TestFuzzyPolicies, we again (pseudo) randomly
+// generate input for the parser, but this time also include a valid directive
+// followed by the random input.
+
+#if RUN_OFFLINE_TESTS
+
+TEST(CSPParser, FuzzyPoliciesIncDir)
+{
+ // init srand with 0 so we get same results
+ srand(0);
+
+ PolicyTest testPol[1];
+ memset(&testPol[0].policy, '\0', kMaxPolicyLength);
+
+ char defaultSrc[] = "default-src ";
+ int defaultSrcLen = sizeof(defaultSrc) - 1;
+ // copy default-src into the policy array
+ memcpy(&testPol[0].policy, &defaultSrc, (defaultSrcLen * sizeof(char)));
+
+ for (uint32_t index = 0; index < kFuzzyRuns; index++) {
+ // randomly select the length of the next policy
+ uint32_t polLength = rand() % (kMaxPolicyLength - defaultSrcLen);
+ // reset memory of the policy string, but leave default-src.
+ memset((&(testPol[0].policy) + (defaultSrcLen * sizeof(char))), '\0',
+ (kMaxPolicyLength - defaultSrcLen) * sizeof(char));
+
+ // do not start at index 0 so we do not overwrite 'default-src'
+ for (uint32_t i = defaultSrcLen; i < polLength; i++) {
+ // fill the policy array with random ASCII chars
+ testPol[0].policy[i] = static_cast<char>(rand() % 128);
+ }
+ ASSERT_TRUE(
+ NS_SUCCEEDED(runTestSuite(testPol, 1, kFuzzyExpectedPolicyCount)));
+ }
+}
+
+#endif
+
+// ====================== TestFuzzyPoliciesIncDirLimASCII ============
+
+// Same as TestFuzzyPoliciesIncDir() but using limited ASCII,
+// which represents more likely input.
+
+#if RUN_OFFLINE_TESTS
+
+TEST(CSPParser, FuzzyPoliciesIncDirLimASCII)
+{
+ char input[] =
+ "1234567890"
+ "abcdefghijklmnopqrstuvwxyz"
+ "ABCDEFGHIJKLMNOPQRSTUVWZYZ"
+ "!@#^&*()-+_=";
+
+ // init srand with 0 so we get same results
+ srand(0);
+
+ PolicyTest testPol[1];
+ memset(&testPol[0].policy, '\0', kMaxPolicyLength);
+
+ char defaultSrc[] = "default-src ";
+ int defaultSrcLen = sizeof(defaultSrc) - 1;
+ // copy default-src into the policy array
+ memcpy(&testPol[0].policy, &defaultSrc, (defaultSrcLen * sizeof(char)));
+
+ for (uint32_t index = 0; index < kFuzzyRuns; index++) {
+ // randomly select the length of the next policy
+ uint32_t polLength = rand() % (kMaxPolicyLength - defaultSrcLen);
+ // reset memory of the policy string, but leave default-src.
+ memset((&(testPol[0].policy) + (defaultSrcLen * sizeof(char))), '\0',
+ (kMaxPolicyLength - defaultSrcLen) * sizeof(char));
+
+ // do not start at index 0 so we do not overwrite 'default-src'
+ for (uint32_t i = defaultSrcLen; i < polLength; i++) {
+ // fill the policy array with chars from the pre-defined input
+ uint32_t inputIndex = rand() % sizeof(input);
+ testPol[0].policy[i] = input[inputIndex];
+ }
+ ASSERT_TRUE(
+ NS_SUCCEEDED(runTestSuite(testPol, 1, kFuzzyExpectedPolicyCount)));
+ }
+}
+#endif
diff --git a/dom/security/test/gtest/TestFilenameEvalParser.cpp b/dom/security/test/gtest/TestFilenameEvalParser.cpp
new file mode 100644
index 0000000000..60683007ca
--- /dev/null
+++ b/dom/security/test/gtest/TestFilenameEvalParser.cpp
@@ -0,0 +1,453 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+
+#include <string.h>
+#include <stdlib.h>
+
+#include "nsContentSecurityUtils.h"
+#include "nsStringFwd.h"
+
+#include "mozilla/ExtensionPolicyService.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/SimpleGlobalObject.h"
+#include "mozilla/extensions/WebExtensionPolicy.h"
+
+static constexpr auto kChromeURI = "chromeuri"_ns;
+static constexpr auto kResourceURI = "resourceuri"_ns;
+static constexpr auto kBlobUri = "bloburi"_ns;
+static constexpr auto kDataUri = "dataurl"_ns;
+static constexpr auto kAboutUri = "abouturi"_ns;
+static constexpr auto kSingleString = "singlestring"_ns;
+static constexpr auto kMozillaExtensionFile = "mozillaextension_file"_ns;
+static constexpr auto kExtensionURI = "extension_uri"_ns;
+static constexpr auto kSuspectedUserChromeJS = "suspectedUserChromeJS"_ns;
+#if defined(XP_WIN)
+static constexpr auto kSanitizedWindowsURL = "sanitizedWindowsURL"_ns;
+static constexpr auto kSanitizedWindowsPath = "sanitizedWindowsPath"_ns;
+#endif
+static constexpr auto kOther = "other"_ns;
+
+#define ASSERT_AND_PRINT(first, second, condition) \
+ fprintf(stderr, "First: %s\n", first.get()); \
+ fprintf(stderr, "Second: %s\n", NS_ConvertUTF16toUTF8(second).get()); \
+ ASSERT_TRUE((condition));
+// Usage: ASSERT_AND_PRINT(ret.first, ret.second.value(), ...
+
+#define ASSERT_AND_PRINT_FIRST(first, condition) \
+ fprintf(stderr, "First: %s\n", (first).get()); \
+ ASSERT_TRUE((condition));
+// Usage: ASSERT_AND_PRINT_FIRST(ret.first, ...
+
+TEST(FilenameEvalParser, ResourceChrome)
+{
+ {
+ constexpr auto str = u"chrome://firegestures/content/browser.js"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+ ASSERT_TRUE(ret.first == kChromeURI && ret.second.isSome() &&
+ ret.second.value() == str);
+ }
+ {
+ constexpr auto str = u"resource://firegestures/content/browser.js"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+ ASSERT_TRUE(ret.first == kResourceURI && ret.second.isSome() &&
+ ret.second.value() == str);
+ }
+}
+
+TEST(FilenameEvalParser, BlobData)
+{
+ {
+ constexpr auto str = u"blob://000-000"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+ ASSERT_TRUE(ret.first == kBlobUri && !ret.second.isSome());
+ }
+ {
+ constexpr auto str = u"blob:000-000"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+ ASSERT_TRUE(ret.first == kBlobUri && !ret.second.isSome());
+ }
+ {
+ constexpr auto str = u"data://blahblahblah"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+ ASSERT_TRUE(ret.first == kDataUri && !ret.second.isSome());
+ }
+ {
+ constexpr auto str = u"data:blahblahblah"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+ ASSERT_TRUE(ret.first == kDataUri && !ret.second.isSome());
+ }
+}
+
+TEST(FilenameEvalParser, MozExtension)
+{
+ { // Test shield.mozilla.org replacing
+ constexpr auto str =
+ u"jar:file:///c:/users/bob/appdata/roaming/mozilla/firefox/profiles/"
+ u"foo/"
+ "extensions/federated-learning@shield.mozilla.org.xpi!/experiments/"
+ "study/api.js"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+ ASSERT_TRUE(ret.first == kMozillaExtensionFile &&
+ ret.second.value() ==
+ u"federated-learning@s!/experiments/study/api.js"_ns);
+ }
+ { // Test mozilla.org replacing
+ constexpr auto str =
+ u"jar:file:///c:/users/bob/appdata/roaming/mozilla/firefox/profiles/"
+ u"foo/"
+ "extensions/federated-learning@shigeld.mozilla.org.xpi!/experiments/"
+ "study/api.js"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+ ASSERT_TRUE(
+ ret.first == kMozillaExtensionFile &&
+ ret.second.value() ==
+ nsLiteralString(
+ u"federated-learning@shigeld.m!/experiments/study/api.js"));
+ }
+ { // Test truncating
+ constexpr auto str =
+ u"jar:file:///c:/users/bob/appdata/roaming/mozilla/firefox/profiles/"
+ u"foo/"
+ "extensions/federated-learning@shigeld.mozilla.org.xpi!/experiments/"
+ "study/apiiiiiiiiiiiiiiiiiiiiiiiiiiiiii.js"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+ ASSERT_TRUE(ret.first == kMozillaExtensionFile &&
+ ret.second.value() ==
+ u"federated-learning@shigeld.m!/experiments/"
+ "study/apiiiiiiiiiiiiiiiiiiiiiiiiiiiiii"_ns);
+ }
+}
+
+TEST(FilenameEvalParser, UserChromeJS)
+{
+ {
+ constexpr auto str = u"firegestures/content/browser.uc.js"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+ ASSERT_TRUE(ret.first == kSuspectedUserChromeJS && !ret.second.isSome());
+ }
+ {
+ constexpr auto str = u"firegestures/content/browser.uc.js?"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+ ASSERT_TRUE(ret.first == kSuspectedUserChromeJS && !ret.second.isSome());
+ }
+ {
+ constexpr auto str = u"firegestures/content/browser.uc.js?243244224"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+ ASSERT_TRUE(ret.first == kSuspectedUserChromeJS && !ret.second.isSome());
+ }
+ {
+ constexpr auto str =
+ u"file:///b:/fxprofiles/mark/chrome/"
+ "addbookmarkherewithmiddleclick.uc.js?1558444389291"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+ ASSERT_TRUE(ret.first == kSuspectedUserChromeJS && !ret.second.isSome());
+ }
+}
+
+TEST(FilenameEvalParser, SingleFile)
+{
+ {
+ constexpr auto str = u"browser.uc.js?2456"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+ ASSERT_TRUE(ret.first == kSingleString && ret.second.isSome() &&
+ ret.second.value() == str);
+ }
+ {
+ constexpr auto str = u"debugger"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+ ASSERT_TRUE(ret.first == kSingleString && ret.second.isSome() &&
+ ret.second.value() == str);
+ }
+}
+
+TEST(FilenameEvalParser, Other)
+{
+ {
+ constexpr auto str = u"firegestures--content"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+ ASSERT_TRUE(ret.first == kOther && !ret.second.isSome());
+ }
+ {
+ constexpr auto str = u"gallop://thing/fire"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+#if defined(XP_WIN)
+ ASSERT_TRUE(ret.first == kSanitizedWindowsURL &&
+ ret.second.value() == u"gallop"_ns);
+#else
+ ASSERT_TRUE(ret.first == kOther && !ret.second.isSome());
+#endif
+ }
+ {
+ constexpr auto str = u"gallop://fire"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+#if defined(XP_WIN)
+ ASSERT_TRUE(ret.first == kSanitizedWindowsURL &&
+ ret.second.value() == u"gallop"_ns);
+#else
+ ASSERT_TRUE(ret.first == kOther && !ret.second.isSome());
+#endif
+ }
+ {
+ constexpr auto str = u"firegestures/content"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+#if defined(XP_WIN)
+ ASSERT_TRUE(ret.first == kSanitizedWindowsPath &&
+ ret.second.value() == u"content"_ns);
+#else
+ ASSERT_TRUE(ret.first == kOther && !ret.second.isSome());
+#endif
+ }
+ {
+ constexpr auto str = u"firegestures\\content"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+#if defined(XP_WIN)
+ ASSERT_TRUE(ret.first == kSanitizedWindowsPath &&
+ ret.second.value() == u"content"_ns);
+#else
+ ASSERT_TRUE(ret.first == kOther && !ret.second.isSome());
+#endif
+ }
+ {
+ constexpr auto str = u"/home/tom/files/thing"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+#if defined(XP_WIN)
+ ASSERT_TRUE(ret.first == kSanitizedWindowsPath &&
+ ret.second.value() == u"thing"_ns);
+#else
+ ASSERT_TRUE(ret.first == kOther && !ret.second.isSome());
+#endif
+ }
+ {
+ constexpr auto str = u"file://c/uers/tom/file.txt"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+#if defined(XP_WIN)
+ ASSERT_TRUE(ret.first == kSanitizedWindowsURL &&
+ ret.second.value() == u"file://.../file.txt"_ns);
+#else
+ ASSERT_TRUE(ret.first == kOther && !ret.second.isSome());
+#endif
+ }
+ {
+ constexpr auto str = u"c:/uers/tom/file.txt"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+#if defined(XP_WIN)
+ ASSERT_TRUE(ret.first == kSanitizedWindowsPath &&
+ ret.second.value() == u"file.txt"_ns);
+#else
+ ASSERT_TRUE(ret.first == kOther && !ret.second.isSome());
+#endif
+ }
+ {
+ constexpr auto str = u"http://example.com/"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+#if defined(XP_WIN)
+ ASSERT_TRUE(ret.first == kSanitizedWindowsURL &&
+ ret.second.value() == u"http"_ns);
+#else
+ ASSERT_TRUE(ret.first == kOther && !ret.second.isSome());
+#endif
+ }
+ {
+ constexpr auto str = u"http://example.com/thing.html"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+#if defined(XP_WIN)
+ ASSERT_TRUE(ret.first == kSanitizedWindowsURL &&
+ ret.second.value() == u"http"_ns);
+#else
+ ASSERT_TRUE(ret.first == kOther && !ret.second.isSome());
+#endif
+ }
+}
+
+TEST(FilenameEvalParser, WebExtensionPathParser)
+{
+ {
+ // Set up an Extension and register it so we can test against it.
+ mozilla::dom::AutoJSAPI jsAPI;
+ ASSERT_TRUE(jsAPI.Init(xpc::PrivilegedJunkScope()));
+ JSContext* cx = jsAPI.cx();
+
+ mozilla::dom::GlobalObject go(cx, xpc::PrivilegedJunkScope());
+ auto* wEI = new mozilla::extensions::WebExtensionInit();
+
+ JS::Rooted<JSObject*> func(
+ cx, (JSObject*)JS_NewFunction(cx, (JSNative)1, 0, 0, "customMethodA"));
+ JS::Rooted<JSObject*> tempGlobalRoot(cx, JS::CurrentGlobalOrNull(cx));
+ wEI->mLocalizeCallback = new mozilla::dom::WebExtensionLocalizeCallback(
+ cx, func, tempGlobalRoot, nullptr);
+
+ wEI->mAllowedOrigins =
+ mozilla::dom::OwningMatchPatternSetOrStringSequence();
+ nsString* slotPtr =
+ wEI->mAllowedOrigins.SetAsStringSequence().AppendElement(
+ mozilla::fallible);
+ ASSERT_TRUE(slotPtr != nullptr);
+ nsString& slot = *slotPtr;
+ slot.Truncate();
+ slot = u"http://example.com"_ns;
+
+ wEI->mName = u"gtest Test Extension"_ns;
+ wEI->mId = u"gtesttestextension@mozilla.org"_ns;
+ wEI->mBaseURL = u"file://foo"_ns;
+ wEI->mMozExtensionHostname = "e37c3c08-beac-a04b-8032-c4f699a1a856"_ns;
+
+ mozilla::ErrorResult eR;
+ RefPtr<mozilla::WebExtensionPolicy> w =
+ mozilla::extensions::WebExtensionPolicy::Constructor(go, *wEI, eR);
+ w->SetActive(true, eR);
+
+ constexpr auto str =
+ u"moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856/path/to/file.js"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, true);
+
+ ASSERT_TRUE(ret.first == kExtensionURI &&
+ ret.second.value() ==
+ u"moz-extension://[gtesttestextension@mozilla.org: "
+ "gtest Test Extension]P=0/path/to/file.js"_ns);
+
+ w->SetActive(false, eR);
+
+ delete wEI;
+ }
+ {
+ // Set up an Extension and register it so we can test against it.
+ mozilla::dom::AutoJSAPI jsAPI;
+ ASSERT_TRUE(jsAPI.Init(xpc::PrivilegedJunkScope()));
+ JSContext* cx = jsAPI.cx();
+
+ mozilla::dom::GlobalObject go(cx, xpc::PrivilegedJunkScope());
+ auto wEI = new mozilla::extensions::WebExtensionInit();
+
+ JS::Rooted<JSObject*> func(
+ cx, (JSObject*)JS_NewFunction(cx, (JSNative)1, 0, 0, "customMethodA"));
+ JS::Rooted<JSObject*> tempGlobalRoot(cx, JS::CurrentGlobalOrNull(cx));
+ wEI->mLocalizeCallback = new mozilla::dom::WebExtensionLocalizeCallback(
+ cx, func, tempGlobalRoot, NULL);
+
+ wEI->mAllowedOrigins =
+ mozilla::dom::OwningMatchPatternSetOrStringSequence();
+ nsString* slotPtr =
+ wEI->mAllowedOrigins.SetAsStringSequence().AppendElement(
+ mozilla::fallible);
+ nsString& slot = *slotPtr;
+ slot.Truncate();
+ slot = u"http://example.com"_ns;
+
+ wEI->mName = u"gtest Test Extension"_ns;
+ wEI->mId = u"gtesttestextension@mozilla.org"_ns;
+ wEI->mBaseURL = u"file://foo"_ns;
+ wEI->mMozExtensionHostname = "e37c3c08-beac-a04b-8032-c4f699a1a856"_ns;
+ wEI->mIsPrivileged = true;
+
+ mozilla::ErrorResult eR;
+ RefPtr<mozilla::WebExtensionPolicy> w =
+ mozilla::extensions::WebExtensionPolicy::Constructor(go, *wEI, eR);
+ w->SetActive(true, eR);
+
+ constexpr auto str =
+ u"moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856/path/to/file.js"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, true);
+
+ ASSERT_TRUE(ret.first == kExtensionURI &&
+ ret.second.value() ==
+ u"moz-extension://[gtesttestextension@mozilla.org: "
+ "gtest Test Extension]P=1/path/to/file.js"_ns);
+
+ w->SetActive(false, eR);
+
+ delete wEI;
+ }
+ {
+ constexpr auto str =
+ u"moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856/path/to/file.js"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+ ASSERT_TRUE(ret.first == kExtensionURI && !ret.second.isSome());
+ }
+ {
+ constexpr auto str =
+ u"moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856/file.js"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, true);
+ ASSERT_TRUE(
+ ret.first == kExtensionURI &&
+ ret.second.value() ==
+ nsLiteralString(
+ u"moz-extension://[failed finding addon by host]/file.js"));
+ }
+ {
+ constexpr auto str =
+ u"moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856/path/to/"
+ "file.js?querystringx=6"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, true);
+ ASSERT_TRUE(ret.first == kExtensionURI &&
+ ret.second.value() ==
+ u"moz-extension://[failed finding addon "
+ "by host]/path/to/file.js"_ns);
+ }
+}
+
+TEST(FilenameEvalParser, AboutPageParser)
+{
+ {
+ constexpr auto str = u"about:about"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+ ASSERT_TRUE(ret.first == kAboutUri &&
+ ret.second.value() == u"about:about"_ns);
+ }
+ {
+ constexpr auto str = u"about:about?hello"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+ ASSERT_TRUE(ret.first == kAboutUri &&
+ ret.second.value() == u"about:about"_ns);
+ }
+ {
+ constexpr auto str = u"about:about#mom"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+ ASSERT_TRUE(ret.first == kAboutUri &&
+ ret.second.value() == u"about:about"_ns);
+ }
+ {
+ constexpr auto str = u"about:about?hello=there#mom"_ns;
+ FilenameTypeAndDetails ret =
+ nsContentSecurityUtils::FilenameToFilenameType(str, false);
+ ASSERT_TRUE(ret.first == kAboutUri &&
+ ret.second.value() == u"about:about"_ns);
+ }
+}
diff --git a/dom/security/test/gtest/TestSecureContext.cpp b/dom/security/test/gtest/TestSecureContext.cpp
new file mode 100644
index 0000000000..dbfb4a63b6
--- /dev/null
+++ b/dom/security/test/gtest/TestSecureContext.cpp
@@ -0,0 +1,121 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+
+#include <string.h>
+#include <stdlib.h>
+
+#include "nsContentSecurityManager.h"
+#include "nsContentUtils.h"
+#include "nsIPrincipal.h"
+#include "nsScriptSecurityManager.h"
+#include "mozilla/NullPrincipal.h"
+#include "mozilla/Preferences.h"
+
+using namespace mozilla;
+
+static const uint32_t kURIMaxLength = 64;
+
+struct TestExpectations {
+ char uri[kURIMaxLength];
+ bool expectedResult;
+};
+
+class MOZ_RAII AutoRestoreBoolPref final {
+ public:
+ AutoRestoreBoolPref(const char* aPref, bool aValue) : mPref(aPref) {
+ Preferences::GetBool(mPref, &mOldValue);
+ Preferences::SetBool(mPref, aValue);
+ }
+
+ ~AutoRestoreBoolPref() { Preferences::SetBool(mPref, mOldValue); }
+
+ private:
+ const char* mPref = nullptr;
+ bool mOldValue = false;
+};
+
+// ============================= TestDirectives ========================
+
+TEST(SecureContext, IsOriginPotentiallyTrustworthyWithContentPrincipal)
+{
+ // boolean isOriginPotentiallyTrustworthy(in nsIPrincipal aPrincipal);
+
+ AutoRestoreBoolPref savedPref("network.proxy.allow_hijacking_localhost",
+ false);
+
+ static const TestExpectations uris[] = {
+ {"http://example.com/", false},
+ {"https://example.com/", true},
+ {"ws://example.com/", false},
+ {"wss://example.com/", true},
+ {"file:///xyzzy", true},
+ {"about:config", false},
+ {"http://localhost", true},
+ {"http://localhost.localhost", true},
+ {"http://a.b.c.d.e.localhost", true},
+ {"http://xyzzy.localhost", true},
+ {"http://127.0.0.1", true},
+ {"http://127.0.0.2", true},
+ {"http://127.1.0.1", true},
+ {"http://128.0.0.1", false},
+ {"http://[::1]", true},
+ {"http://[::ffff:127.0.0.1]", false},
+ {"http://[::ffff:127.0.0.2]", false},
+ {"http://[::ffff:7f00:1]", false},
+ {"http://[::ffff:7f00:2]", false},
+ {"resource://xyzzy", true},
+ {"moz-extension://xyzzy", true},
+ {"data:data:text/plain;charset=utf-8;base64,eHl6enk=", false},
+ {"blob://unique-id", false},
+ {"mailto:foo@bar.com", false},
+ {"moz-icon://example.com", false},
+ {"javascript:42", false},
+ };
+
+ uint32_t numExpectations = sizeof(uris) / sizeof(TestExpectations);
+ nsCOMPtr<nsIContentSecurityManager> csManager =
+ do_GetService(NS_CONTENTSECURITYMANAGER_CONTRACTID);
+ ASSERT_TRUE(!!csManager);
+
+ nsresult rv;
+ for (uint32_t i = 0; i < numExpectations; i++) {
+ nsCOMPtr<nsIPrincipal> prin;
+ nsAutoCString uri(uris[i].uri);
+ rv = nsScriptSecurityManager::GetScriptSecurityManager()
+ ->CreateContentPrincipalFromOrigin(uri, getter_AddRefs(prin));
+ ASSERT_EQ(rv, NS_OK);
+ bool isPotentiallyTrustworthy = prin->GetIsOriginPotentiallyTrustworthy();
+ ASSERT_EQ(isPotentiallyTrustworthy, uris[i].expectedResult)
+ << uris[i].uri << uris[i].expectedResult;
+ }
+}
+
+TEST(SecureContext, IsOriginPotentiallyTrustworthyWithSystemPrincipal)
+{
+ RefPtr<nsScriptSecurityManager> ssManager =
+ nsScriptSecurityManager::GetScriptSecurityManager();
+ ASSERT_TRUE(!!ssManager);
+ nsCOMPtr<nsIPrincipal> sysPrin = nsContentUtils::GetSystemPrincipal();
+ bool isPotentiallyTrustworthy = sysPrin->GetIsOriginPotentiallyTrustworthy();
+ ASSERT_TRUE(isPotentiallyTrustworthy);
+}
+
+TEST(SecureContext, IsOriginPotentiallyTrustworthyWithNullPrincipal)
+{
+ RefPtr<nsScriptSecurityManager> ssManager =
+ nsScriptSecurityManager::GetScriptSecurityManager();
+ ASSERT_TRUE(!!ssManager);
+
+ RefPtr<NullPrincipal> nullPrin =
+ NullPrincipal::CreateWithoutOriginAttributes();
+ bool isPotentiallyTrustworthy;
+ nsresult rv =
+ nullPrin->GetIsOriginPotentiallyTrustworthy(&isPotentiallyTrustworthy);
+ ASSERT_EQ(rv, NS_OK);
+ ASSERT_TRUE(!isPotentiallyTrustworthy);
+}
diff --git a/dom/security/test/gtest/TestSmartCrashTrimmer.cpp b/dom/security/test/gtest/TestSmartCrashTrimmer.cpp
new file mode 100644
index 0000000000..d2238c0d75
--- /dev/null
+++ b/dom/security/test/gtest/TestSmartCrashTrimmer.cpp
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "nsContentSecurityUtils.h"
+#include "nsTString.h"
+#include "nsStringFwd.h"
+#include "mozilla/Sprintf.h"
+
+#define ASSERT_STRCMP(first, second) ASSERT_TRUE(strcmp(first, second) == 0);
+
+#define ASSERT_STRCMP_AND_PRINT(first, second) \
+ fprintf(stderr, "First: %s\n", first); \
+ fprintf(stderr, "Second: %s\n", second); \
+ fprintf(stderr, "strcmp = %i\n", strcmp(first, second)); \
+ ASSERT_EQUAL(first, second);
+
+TEST(SmartCrashTrimmer, Test)
+{
+ static_assert(sPrintfCrashReasonSize == 1024);
+ {
+ auto ret = nsContentSecurityUtils::SmartFormatCrashString(
+ std::string(1025, '.').c_str());
+ ASSERT_EQ(strlen(ret), 1023ul);
+ }
+
+ {
+ auto ret = nsContentSecurityUtils::SmartFormatCrashString(
+ std::string(1025, '.').c_str(), std::string(1025, 'A').c_str(),
+ "Hello %s world %s!");
+ char expected[1025];
+ SprintfLiteral(expected, "Hello %s world AAAAAAAAAAAAAAAAAAAAAAAAA!",
+ std::string(984, '.').c_str());
+ ASSERT_STRCMP(ret.get(), expected);
+ }
+}
diff --git a/dom/security/test/gtest/TestUnexpectedPrivilegedLoads.cpp b/dom/security/test/gtest/TestUnexpectedPrivilegedLoads.cpp
new file mode 100644
index 0000000000..772e4bd353
--- /dev/null
+++ b/dom/security/test/gtest/TestUnexpectedPrivilegedLoads.cpp
@@ -0,0 +1,305 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "core/TelemetryEvent.h"
+#include "gtest/gtest.h"
+#include "js/Array.h" // JS::GetArrayLength
+#include "js/PropertyAndElement.h" // JS_GetElement, JS_GetProperty
+#include "js/TypeDecls.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/Unused.h"
+#include "TelemetryFixture.h"
+#include "TelemetryTestHelpers.h"
+
+#include <string.h>
+#include <stdlib.h>
+#include "nsContentSecurityManager.h"
+#include "nsContentSecurityUtils.h"
+#include "nsContentUtils.h"
+#include "nsIContentPolicy.h"
+#include "nsILoadInfo.h"
+#include "nsNetUtil.h"
+#include "nsStringFwd.h"
+
+using namespace mozilla;
+using namespace TelemetryTestHelpers;
+
+extern Atomic<bool, mozilla::Relaxed> sJSHacksChecked;
+extern Atomic<bool, mozilla::Relaxed> sJSHacksPresent;
+extern Atomic<bool, mozilla::Relaxed> sCSSHacksChecked;
+extern Atomic<bool, mozilla::Relaxed> sCSSHacksPresent;
+
+TEST_F(TelemetryTestFixture, UnexpectedPrivilegedLoadsTelemetryTest) {
+ // Disable JS/CSS Hacks Detection, which would consider this current profile
+ // as uninteresting for our measurements:
+ bool origJSHacksPresent = sJSHacksPresent;
+ bool origJSHacksChecked = sJSHacksChecked;
+ sJSHacksPresent = false;
+ sJSHacksChecked = true;
+ bool origCSSHacksPresent = sCSSHacksPresent;
+ bool origCSSHacksChecked = sCSSHacksChecked;
+ sCSSHacksPresent = false;
+ sCSSHacksChecked = true;
+
+ struct testResults {
+ nsCString fileinfo;
+ nsCString extraValueContenttype;
+ nsCString extraValueRemotetype;
+ nsCString extraValueFiledetails;
+ nsCString extraValueRedirects;
+ };
+
+ struct testCasesAndResults {
+ nsCString urlstring;
+ nsContentPolicyType contentType;
+ nsCString remoteType;
+ testResults expected;
+ };
+
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+ // Make sure we don't look at events from other tests.
+ Unused << mTelemetry->ClearEvents();
+
+ // required for telemetry lookups
+ constexpr auto category = "security"_ns;
+ constexpr auto method = "unexpectedload"_ns;
+ constexpr auto object = "systemprincipal"_ns;
+ constexpr auto extraKeyContenttype = "contenttype"_ns;
+ constexpr auto extraKeyRemotetype = "remotetype"_ns;
+ constexpr auto extraKeyFiledetails = "filedetails"_ns;
+ constexpr auto extraKeyRedirects = "redirects"_ns;
+
+ // some cases from TestFilenameEvalParser
+ // no need to replicate all scenarios?!
+ testCasesAndResults myTestCases[] = {
+ {"chrome://firegestures/content/browser.js"_ns,
+ nsContentPolicyType::TYPE_SCRIPT,
+ "web"_ns,
+ {"chromeuri"_ns, "TYPE_SCRIPT"_ns, "web"_ns,
+ "chrome://firegestures/content/browser.js"_ns, ""_ns}},
+ {"resource://firegestures/content/browser.js"_ns,
+ nsContentPolicyType::TYPE_SCRIPT,
+ "web"_ns,
+ {"resourceuri"_ns, "TYPE_SCRIPT"_ns, "web"_ns,
+ "resource://firegestures/content/browser.js"_ns, ""_ns}},
+ {// test that we don't report blob details
+ // ..and test that we strip of URLs from remoteTypes
+ "blob://000-000"_ns,
+ nsContentPolicyType::TYPE_SCRIPT,
+ "webIsolated=https://blob.example/"_ns,
+ {"bloburi"_ns, "TYPE_SCRIPT"_ns, "webIsolated"_ns, "unknown"_ns, ""_ns}},
+ {// test for cases where finalURI is null, due to a broken nested URI
+ // .. like malformed moz-icon URLs
+ "moz-icon:blahblah"_ns,
+ nsContentPolicyType::TYPE_DOCUMENT,
+ "web"_ns,
+ {"other"_ns, "TYPE_DOCUMENT"_ns, "web"_ns, "unknown"_ns, ""_ns}},
+ {// we dont report data urls
+ // ..and test that we strip of URLs from remoteTypes
+ "data://blahblahblah"_ns,
+ nsContentPolicyType::TYPE_SCRIPT,
+ "webCOOP+COEP=https://data.example"_ns,
+ {"dataurl"_ns, "TYPE_SCRIPT"_ns, "webCOOP+COEP"_ns, "unknown"_ns,
+ ""_ns}},
+ {// handle data URLs for webextension content scripts differently
+ // .. by noticing their annotation
+ "data:text/css;extension=style;charset=utf-8,/* some css here */"_ns,
+ nsContentPolicyType::TYPE_STYLESHEET,
+ "web"_ns,
+ {"dataurl-extension-contentstyle"_ns, "TYPE_STYLESHEET"_ns, "web"_ns,
+ "unknown"_ns, ""_ns}},
+ {// we only report file URLs on windows, where we can easily sanitize
+ "file://c/users/tom/file.txt"_ns,
+ nsContentPolicyType::TYPE_SCRIPT,
+ "web"_ns,
+ {
+#if defined(XP_WIN)
+ "sanitizedWindowsURL"_ns, "TYPE_SCRIPT"_ns, "web"_ns,
+ "file://.../file.txt"_ns, ""_ns
+
+#else
+ "other"_ns, "TYPE_SCRIPT"_ns, "web"_ns, "unknown"_ns, ""_ns
+#endif
+ }},
+ {// test for one redirect
+ "moz-extension://abcdefab-1234-4321-0000-abcdefabcdef/js/assets.js"_ns,
+ nsContentPolicyType::TYPE_SCRIPT,
+ "web"_ns,
+ {"extension_uri"_ns, "TYPE_SCRIPT"_ns, "web"_ns,
+ // the extension-id is made-up, so the extension will report failure
+ "moz-extension://[failed finding addon by host]/js/assets.js"_ns,
+ "https"_ns}},
+ {// test for cases where finalURI is empty
+ ""_ns,
+ nsContentPolicyType::TYPE_STYLESHEET,
+ "web"_ns,
+ {"other"_ns, "TYPE_STYLESHEET"_ns, "web"_ns, "unknown"_ns, ""_ns}},
+ {// test for cases where finalURI is null, due to the struct layout, we'll
+ // override the URL with nullptr in loop below.
+ "URLWillResultInNullPtr"_ns,
+ nsContentPolicyType::TYPE_SCRIPT,
+ "web"_ns,
+ {"other"_ns, "TYPE_SCRIPT"_ns, "web"_ns, "unknown"_ns, ""_ns}},
+ };
+
+ int i = 0;
+ for (auto const& currentTest : myTestCases) {
+ nsresult rv;
+ nsCOMPtr<nsIURI> uri;
+
+ // special-casing for a case where the uri is null
+ if (!currentTest.urlstring.Equals("URLWillResultInNullPtr")) {
+ NS_NewURI(getter_AddRefs(uri), currentTest.urlstring);
+ }
+
+ // We can't create channels for chrome: URLs unless they are in a chrome
+ // registry that maps them into the actual destination URL (usually
+ // file://). It seems that gtest don't have chrome manifest registered, so
+ // we'll use a mockChannel with a mockUri.
+ nsCOMPtr<nsIURI> mockUri;
+ rv = NS_NewURI(getter_AddRefs(mockUri), "http://example.com"_ns);
+ ASSERT_EQ(rv, NS_OK) << "Could not create mockUri";
+ nsCOMPtr<nsIChannel> mockChannel;
+ nsCOMPtr<nsIIOService> service = do_GetIOService();
+ if (!service) {
+ ASSERT_TRUE(false)
+ << "Couldn't initialize IOService";
+ }
+ rv = service->NewChannelFromURI(
+ mockUri, nullptr, nsContentUtils::GetSystemPrincipal(),
+ nsContentUtils::GetSystemPrincipal(), 0, currentTest.contentType,
+ getter_AddRefs(mockChannel));
+ ASSERT_EQ(rv, NS_OK) << "Could not create a mock channel";
+ nsCOMPtr<nsILoadInfo> mockLoadInfo = mockChannel->LoadInfo();
+
+ // We're adding a redirect entry for one specific test
+ if (currentTest.urlstring.EqualsASCII(
+ "moz-extension://abcdefab-1234-4321-0000-abcdefabcdef/js/"
+ "assets.js")) {
+ nsCOMPtr<nsIURI> redirUri;
+ NS_NewURI(getter_AddRefs(redirUri),
+ "https://www.analytics.example/analytics.js"_ns);
+ nsCOMPtr<nsIPrincipal> redirPrincipal =
+ BasePrincipal::CreateContentPrincipal(redirUri, OriginAttributes());
+ nsCOMPtr<nsIChannel> redirectChannel;
+ Unused << service->NewChannelFromURI(redirUri, nullptr, redirPrincipal,
+ nullptr, 0, currentTest.contentType,
+ getter_AddRefs(redirectChannel));
+
+ mockLoadInfo->AppendRedirectHistoryEntry(redirectChannel, false);
+ }
+
+ // this will record the event
+ nsContentSecurityManager::MeasureUnexpectedPrivilegedLoads(
+ mockLoadInfo, uri, currentTest.remoteType);
+
+ // let's inspect the recorded events
+
+ JS::Rooted<JS::Value> eventsSnapshot(cx.GetJSContext());
+ GetEventSnapshot(cx.GetJSContext(), &eventsSnapshot);
+
+ ASSERT_TRUE(EventPresent(cx.GetJSContext(), eventsSnapshot, category,
+ method, object))
+ << "Test event with value and extra must be present.";
+
+ // Convert eventsSnapshot into array/object
+ JSContext* aCx = cx.GetJSContext();
+ JS::Rooted<JSObject*> arrayObj(aCx, &eventsSnapshot.toObject());
+
+ JS::Rooted<JS::Value> eventRecord(aCx);
+ ASSERT_TRUE(JS_GetElement(aCx, arrayObj, i++, &eventRecord))
+ << "Must be able to get record."; // record is already undefined :-/
+
+ ASSERT_TRUE(!eventRecord.isUndefined())
+ << "eventRecord should not be undefined";
+
+ JS::Rooted<JSObject*> recordArray(aCx, &eventRecord.toObject());
+ uint32_t recordLength;
+ ASSERT_TRUE(JS::GetArrayLength(aCx, recordArray, &recordLength))
+ << "Event record array must have length.";
+ ASSERT_TRUE(recordLength == 6)
+ << "Event record must have 6 elements.";
+
+ JS::Rooted<JS::Value> str(aCx);
+ nsAutoJSString jsStr;
+ // The fileinfo string is at index 4
+ ASSERT_TRUE(JS_GetElement(aCx, recordArray, 4, &str))
+ << "Must be able to get value.";
+ ASSERT_TRUE(jsStr.init(aCx, str))
+ << "Value must be able to be init'd to a jsstring.";
+
+ ASSERT_STREQ(NS_ConvertUTF16toUTF8(jsStr).get(),
+ currentTest.expected.fileinfo.get())
+ << "Reported fileinfo '" << NS_ConvertUTF16toUTF8(jsStr).get()
+ << " 'equals expected value: " << currentTest.expected.fileinfo.get();
+
+ // Extra is at index 5
+ JS::Rooted<JS::Value> obj(aCx);
+ ASSERT_TRUE(JS_GetElement(aCx, recordArray, 5, &obj))
+ << "Must be able to get extra data";
+ JS::Rooted<JSObject*> extraObj(aCx, &obj.toObject());
+ // looking at remotetype extra for content type
+ JS::Rooted<JS::Value> extraValC(aCx);
+ ASSERT_TRUE(
+ JS_GetProperty(aCx, extraObj, extraKeyContenttype.get(), &extraValC))
+ << "Must be able to get the extra key's value for contenttype";
+ ASSERT_TRUE(jsStr.init(aCx, extraValC))
+ << "Extra value contenttype must be able to be init'd to a jsstring.";
+ ASSERT_STREQ(NS_ConvertUTF16toUTF8(jsStr).get(),
+ currentTest.expected.extraValueContenttype.get())
+ << "Reported value for extra contenttype '"
+ << NS_ConvertUTF16toUTF8(jsStr).get()
+ << "' should equals supplied value"
+ << currentTest.expected.extraValueContenttype.get();
+ // and again for remote type
+ JS::Rooted<JS::Value> extraValP(aCx);
+ ASSERT_TRUE(
+ JS_GetProperty(aCx, extraObj, extraKeyRemotetype.get(), &extraValP))
+ << "Must be able to get the extra key's value for remotetype";
+ ASSERT_TRUE(jsStr.init(aCx, extraValP))
+ << "Extra value remotetype must be able to be init'd to a jsstring.";
+ ASSERT_STREQ(NS_ConvertUTF16toUTF8(jsStr).get(),
+ currentTest.expected.extraValueRemotetype.get())
+ << "Reported value for extra remotetype '"
+ << NS_ConvertUTF16toUTF8(jsStr).get()
+ << "' should equals supplied value: "
+ << currentTest.expected.extraValueRemotetype.get();
+ // repeating the same for filedetails extra
+ JS::Rooted<JS::Value> extraValF(aCx);
+ ASSERT_TRUE(
+ JS_GetProperty(aCx, extraObj, extraKeyFiledetails.get(), &extraValF))
+ << "Must be able to get the extra key's value for filedetails";
+ ASSERT_TRUE(jsStr.init(aCx, extraValF))
+ << "Extra value filedetails must be able to be init'd to a jsstring.";
+ ASSERT_STREQ(NS_ConvertUTF16toUTF8(jsStr).get(),
+ currentTest.expected.extraValueFiledetails.get())
+ << "Reported value for extra filedetails '"
+ << NS_ConvertUTF16toUTF8(jsStr).get() << "'should equals supplied value"
+ << currentTest.expected.extraValueFiledetails.get();
+ // checking the extraKeyRedirects match
+ JS::Rooted<JS::Value> extraValRedirects(aCx);
+ ASSERT_TRUE(JS_GetProperty(aCx, extraObj, extraKeyRedirects.get(),
+ &extraValRedirects))
+ << "Must be able to get the extra value for redirects";
+ ASSERT_TRUE(jsStr.init(aCx, extraValRedirects))
+ << "Extra value redirects must be able to be init'd to a jsstring";
+ ASSERT_STREQ(NS_ConvertUTF16toUTF8(jsStr).get(),
+ currentTest.expected.extraValueRedirects.get())
+ << "Reported value for extra redirect '"
+ << NS_ConvertUTF16toUTF8(jsStr).get()
+ << "' should equals supplied value: "
+ << currentTest.expected.extraValueRedirects.get();
+ }
+
+ // Re-store JS/CSS hacks detection state
+ sJSHacksPresent = origJSHacksPresent;
+ sJSHacksChecked = origJSHacksChecked;
+ sCSSHacksPresent = origCSSHacksPresent;
+ sCSSHacksChecked = origCSSHacksChecked;
+}
diff --git a/dom/security/test/gtest/moz.build b/dom/security/test/gtest/moz.build
new file mode 100644
index 0000000000..c9ab4dcece
--- /dev/null
+++ b/dom/security/test/gtest/moz.build
@@ -0,0 +1,25 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += [
+ "TestCSPParser.cpp",
+ "TestFilenameEvalParser.cpp",
+ "TestSecureContext.cpp",
+ "TestSmartCrashTrimmer.cpp",
+]
+
+if CONFIG["OS_TARGET"] != "Android":
+ UNIFIED_SOURCES += [
+ "TestUnexpectedPrivilegedLoads.cpp",
+ ]
+
+FINAL_LIBRARY = "xul-gtest"
+
+LOCAL_INCLUDES += [
+ "/caps",
+ "/toolkit/components/telemetry/",
+ "/toolkit/components/telemetry/tests/gtest",
+]
diff --git a/dom/security/test/https-first/browser.toml b/dom/security/test/https-first/browser.toml
new file mode 100644
index 0000000000..0c63b8317d
--- /dev/null
+++ b/dom/security/test/https-first/browser.toml
@@ -0,0 +1,53 @@
+[DEFAULT]
+
+["browser_beforeunload_permit_http.js"]
+support-files = ["file_beforeunload_permit_http.html"]
+
+["browser_downgrade_mixed_content_auto_upgrade_console.js"]
+support-files = [
+ "file_mixed_content_auto_upgrade.html",
+ "pass.png",
+ "test.ogv",
+ "test.wav",
+]
+
+["browser_downgrade_view_source.js"]
+support-files = ["file_downgrade_view_source.sjs"]
+
+["browser_download_attribute.js"]
+support-files = [
+ "file_download_attribute.html",
+ "file_download_attribute.sjs",
+]
+
+["browser_httpsfirst.js"]
+support-files = ["file_httpsfirst_timeout_server.sjs"]
+
+["browser_httpsfirst_console_logging.js"]
+
+["browser_httpsfirst_speculative_connect.js"]
+support-files = ["file_httpsfirst_speculative_connect.html"]
+
+["browser_mixed_content_console.js"]
+support-files = ["file_mixed_content_console.html"]
+
+["browser_mixed_content_download.js"]
+support-files = [
+ "download_page.html",
+ "download_server.sjs",
+]
+
+["browser_navigation.js"]
+support-files = ["file_navigation.html"]
+
+["browser_schemeless.js"]
+
+["browser_slow_download.js"]
+support-files = [
+ "file_slow_download.html",
+ "file_slow_download.sjs",
+]
+
+["browser_superfluos_auth.js"]
+
+["browser_upgrade_onion.js"]
diff --git a/dom/security/test/https-first/browser_beforeunload_permit_http.js b/dom/security/test/https-first/browser_beforeunload_permit_http.js
new file mode 100644
index 0000000000..660c1a352d
--- /dev/null
+++ b/dom/security/test/https-first/browser_beforeunload_permit_http.js
@@ -0,0 +1,208 @@
+"use strict";
+
+const { PromptTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromptTestUtils.sys.mjs"
+);
+
+const TEST_PATH_HTTP = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://nocert.example.com/"
+);
+/*
+ * Description of Tests:
+ *
+ * Test load page and reload:
+ * 1. Enable HTTPS-First and the pref to trigger beforeunload by user interaction
+ * 2. Open an HTTP site. HTTPS-First will try to upgrade it to https - but since it has no cert that try will fail
+ * 3. Then simulated user interaction and reload the page with a reload flag.
+ * 4. That should lead to a beforeUnload prompt that asks for users permission to perform reload. HTTPS-First should not try to upgrade the reload again
+ *
+ * Test Navigation:
+ * 1. Enable HTTPS-First and the pref to trigger beforeunload by user interaction
+ * 2. Open an http site. HTTPS-First will try to upgrade it to https - but since it has no cert for https that try will fail
+ * 3. Then simulated user interaction and navigate to another http page. Again HTTPS-First will try to upgrade to HTTPS
+ * 4. This attempted navigation leads to a prompt which askes for permission to leave page - accept it
+ * 5. Since the site is not using a valid HTTPS cert HTTPS-First will downgrade the request back to HTTP
+ * 6. User should NOT get asked again for permission to unload
+ *
+ * Test Session History Navigation:
+ * 1. Enable HTTPS-First and the pref to trigger beforeunload by user interaction
+ * 2. Open an http site. HTTPS-First will try to upgrade it to https - but since it has no cert for https that try will fail
+ * 3. Then navigate to another http page and simulated a user interaction.
+ * 4. Trigger a session history navigation by clicking the "back button".
+ * 5. This attempted navigation leads to a prompt which askes for permission to leave page - accept it
+ */
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.security.https_first", true],
+ ["dom.require_user_interaction_for_beforeunload", true],
+ ],
+ });
+});
+const TESTS = [
+ {
+ name: "Normal Reload (No flag)",
+ reloadFlag: Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
+ },
+ {
+ name: "Bypass Cache Reload",
+ reloadFlag: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE,
+ },
+ {
+ name: "Bypass Proxy Reload",
+ reloadFlag: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY,
+ },
+ {
+ name: "Bypass Cache and Proxy Reload",
+ reloadFlag:
+ Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE |
+ Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY,
+ },
+];
+
+add_task(async function testReloadFlags() {
+ for (let index = 0; index < TESTS.length; index++) {
+ const testCase = TESTS[index];
+ // The onbeforeunload dialog should appear
+ let dialogPromise = PromptTestUtils.waitForPrompt(null, {
+ modalType: Services.prompt.MODAL_TYPE_CONTENT,
+ promptType: "confirmEx",
+ });
+ let reloadPromise = loadPageAndReload(testCase);
+ let dialog = await dialogPromise;
+ Assert.ok(true, "Showed the beforeunload dialog.");
+ await PromptTestUtils.handlePrompt(dialog, { buttonNumClick: 0 });
+ await reloadPromise;
+ }
+});
+
+add_task(async function testNavigation() {
+ // The onbeforeunload dialog should appear
+ let dialogPromise = PromptTestUtils.waitForPrompt(null, {
+ modalType: Services.prompt.MODAL_TYPE_CONTENT,
+ promptType: "confirmEx",
+ });
+
+ let openPagePromise = openPage();
+ let dialog = await dialogPromise;
+ Assert.ok(true, "Showed the beforeunload dialog.");
+ await PromptTestUtils.handlePrompt(dialog, { buttonNumClick: 0 });
+ await openPagePromise;
+});
+
+add_task(async function testSessionHistoryNavigation() {
+ // The onbeforeunload dialog should appear
+ let dialogPromise = PromptTestUtils.waitForPrompt(null, {
+ modalType: Services.prompt.MODAL_TYPE_CONTENT,
+ promptType: "confirmEx",
+ });
+
+ let openPagePromise = loadPagesAndUseBackButton();
+ let dialog = await dialogPromise;
+ Assert.ok(true, "Showed the beforeunload dialog.");
+ await PromptTestUtils.handlePrompt(dialog, { buttonNumClick: 0 });
+ await openPagePromise;
+});
+
+async function openPage() {
+ // Open about:blank in a new tab
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async function (browser) {
+ // Load http page
+ BrowserTestUtils.startLoadingURIString(
+ browser,
+ `${TEST_PATH_HTTP}file_beforeunload_permit_http.html`
+ );
+ await BrowserTestUtils.browserLoaded(browser);
+ // Interact with page such that unload permit will be necessary
+ await BrowserTestUtils.synthesizeMouse("body", 2, 2, {}, browser);
+ let hasInteractedWith = await SpecialPowers.spawn(
+ browser,
+ [""],
+ function () {
+ return content.document.userHasInteracted;
+ }
+ );
+
+ is(true, hasInteractedWith, "Simulated successfully user interaction");
+ // And then navigate away to another site which proves that user won't be asked twice to permit a reload (otherwise the test get timed out)
+ BrowserTestUtils.startLoadingURIString(
+ browser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://self-signed.example.com/"
+ );
+ await BrowserTestUtils.browserLoaded(browser);
+ Assert.ok(true, "Navigated successfully.");
+ }
+ );
+}
+
+async function loadPageAndReload(testCase) {
+ // Load initial site
+ // Open about:blank in a new tab
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async function (browser) {
+ BrowserTestUtils.startLoadingURIString(
+ browser,
+ `${TEST_PATH_HTTP}file_beforeunload_permit_http.html`
+ );
+ await BrowserTestUtils.browserLoaded(browser);
+ // Interact with page such that unload permit will be necessary
+ await BrowserTestUtils.synthesizeMouse("body", 2, 2, {}, browser);
+
+ let hasInteractedWith = await SpecialPowers.spawn(
+ browser,
+ [""],
+ function () {
+ return content.document.userHasInteracted;
+ }
+ );
+ is(true, hasInteractedWith, "Simulated successfully user interaction");
+ BrowserReloadWithFlags(testCase.reloadFlag);
+ await BrowserTestUtils.browserLoaded(browser);
+ is(true, true, `reload with flag ${testCase.name} was successful`);
+ }
+ );
+}
+
+async function loadPagesAndUseBackButton() {
+ // Load initial site
+ // Open about:blank in a new tab
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async function (browser) {
+ BrowserTestUtils.startLoadingURIString(
+ browser,
+ `${TEST_PATH_HTTP}file_beforeunload_permit_http.html`
+ );
+ await BrowserTestUtils.browserLoaded(browser);
+
+ BrowserTestUtils.startLoadingURIString(
+ browser,
+ `${TEST_PATH_HTTP}file_beforeunload_permit_http.html?getASessionHistoryEntry`
+ );
+ await BrowserTestUtils.browserLoaded(browser);
+ // Interact with page such that unload permit will be necessary
+ await BrowserTestUtils.synthesizeMouse("body", 2, 2, {}, browser);
+
+ let hasInteractedWith = await SpecialPowers.spawn(
+ browser,
+ [""],
+ function () {
+ return content.document.userHasInteracted;
+ }
+ );
+ is(true, hasInteractedWith, "Simulated successfully user interaction");
+ // Go back one site by clicking the back button
+ info("Clicking back button");
+ let backButton = document.getElementById("back-button");
+ backButton.click();
+ await BrowserTestUtils.browserLoaded(browser);
+ is(true, true, `Got back successful`);
+ }
+ );
+}
diff --git a/dom/security/test/https-first/browser_downgrade_mixed_content_auto_upgrade_console.js b/dom/security/test/https-first/browser_downgrade_mixed_content_auto_upgrade_console.js
new file mode 100644
index 0000000000..8f1778135a
--- /dev/null
+++ b/dom/security/test/https-first/browser_downgrade_mixed_content_auto_upgrade_console.js
@@ -0,0 +1,82 @@
+// Bug 1673574 - Improve Console logging for mixed content auto upgrading
+"use strict";
+
+const testPath = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://httpsfirst.com"
+);
+
+let tests = [
+ {
+ description: "Top-Level upgrade should get logged",
+ expectLogLevel: Ci.nsIConsoleMessage.warn,
+ expectIncludes: ["Upgrading insecure request", "to use", "httpsfirst.com"],
+ },
+ {
+ description: "Top-Level upgrade failure should get logged",
+ expectLogLevel: Ci.nsIConsoleMessage.warn,
+ expectIncludes: [
+ "Upgrading insecure request",
+ "failed",
+ "httpsfirst.com",
+ "Downgrading to",
+ ],
+ },
+];
+
+const kTestURI = testPath + "file_mixed_content_auto_upgrade.html";
+
+add_task(async function () {
+ // A longer timeout is necessary for this test than the plain mochitests
+ // due to opening a new tab with the web console.
+ requestLongerTimeout(4);
+
+ // Enable ML2 and HTTPS-First Mode and register console-listener
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.mixed_content.upgrade_display_content", true],
+ ["dom.security.https_first", true],
+ ],
+ });
+ Services.console.registerListener(on_new_message);
+ // 1. Upgrade page to https://
+ let promiseLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, kTestURI);
+ await promiseLoaded;
+
+ await BrowserTestUtils.waitForCondition(() => tests.length === 0);
+
+ // Clean up
+ Services.console.unregisterListener(on_new_message);
+});
+
+function on_new_message(msgObj) {
+ const message = msgObj.message;
+ const logLevel = msgObj.logLevel;
+
+ // The console message is:
+ // Should only show HTTPS-First messages
+
+ if (message.includes("Mixed Content:")) {
+ ok(
+ !message.includes("Upgrading insecure display request"),
+ "msg included a mixed content upgrade"
+ );
+ }
+ if (message.includes("HTTPS-First Mode:")) {
+ for (let i = 0; i < tests.length; i++) {
+ const testCase = tests[i];
+ // Check if log-level matches
+ if (logLevel !== testCase.expectLogLevel) {
+ continue;
+ }
+ // Check if all substrings are included
+ if (testCase.expectIncludes.some(str => !message.includes(str))) {
+ continue;
+ }
+ ok(true, testCase.description);
+ tests.splice(i, 1);
+ break;
+ }
+ }
+}
diff --git a/dom/security/test/https-first/browser_downgrade_view_source.js b/dom/security/test/https-first/browser_downgrade_view_source.js
new file mode 100644
index 0000000000..3d5552c79f
--- /dev/null
+++ b/dom/security/test/https-first/browser_downgrade_view_source.js
@@ -0,0 +1,81 @@
+// This test ensures that view-source:https falls back to view-source:http
+"use strict";
+
+const TEST_PATH_HTTP = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+
+const TEST_PATH_HTTPS = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+async function runTest(desc, url, expectedURI, excpectedContent) {
+ await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, null, true);
+ BrowserTestUtils.startLoadingURIString(browser, url);
+ await loaded;
+
+ await SpecialPowers.spawn(
+ browser,
+ [desc, expectedURI, excpectedContent],
+ async function (desc, expectedURI, excpectedContent) {
+ let loadedURI = content.document.documentURI;
+ is(loadedURI, expectedURI, desc);
+ let loadedContent = content.document.body.textContent;
+ is(loadedContent, excpectedContent, desc);
+ }
+ );
+ });
+}
+
+add_task(async function () {
+ requestLongerTimeout(2);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_first", true]],
+ });
+
+ await runTest(
+ "URL with query 'downgrade' should be http:",
+ `view-source:${TEST_PATH_HTTP}/file_downgrade_view_source.sjs?downgrade`,
+ `view-source:${TEST_PATH_HTTP}/file_downgrade_view_source.sjs?downgrade`,
+ "view-source:http://"
+ );
+
+ await runTest(
+ "URL with query 'downgrade' should be http and leave query params untouched:",
+ `view-source:${TEST_PATH_HTTP}/file_downgrade_view_source.sjs?downgrade&https://httpsfirst.com`,
+ `view-source:${TEST_PATH_HTTP}/file_downgrade_view_source.sjs?downgrade&https://httpsfirst.com`,
+ "view-source:http://"
+ );
+
+ await runTest(
+ "URL with query 'upgrade' should be https:",
+ `view-source:${TEST_PATH_HTTP}/file_downgrade_view_source.sjs?upgrade`,
+ `view-source:${TEST_PATH_HTTPS}/file_downgrade_view_source.sjs?upgrade`,
+ "view-source:https://"
+ );
+
+ await runTest(
+ "URL with query 'upgrade' should be https:",
+ `view-source:${TEST_PATH_HTTPS}/file_downgrade_view_source.sjs?upgrade`,
+ `view-source:${TEST_PATH_HTTPS}/file_downgrade_view_source.sjs?upgrade`,
+ "view-source:https://"
+ );
+
+ await runTest(
+ "URL with query 'upgrade' should be https and leave query params untouched:",
+ `view-source:${TEST_PATH_HTTP}/file_downgrade_view_source.sjs?upgrade&https://httpsfirst.com`,
+ `view-source:${TEST_PATH_HTTPS}/file_downgrade_view_source.sjs?upgrade&https://httpsfirst.com`,
+ "view-source:https://"
+ );
+
+ await runTest(
+ "URL with query 'upgrade' should be https and leave query params untouched:",
+ `view-source:${TEST_PATH_HTTPS}/file_downgrade_view_source.sjs?upgrade&https://httpsfirst.com`,
+ `view-source:${TEST_PATH_HTTPS}/file_downgrade_view_source.sjs?upgrade&https://httpsfirst.com`,
+ "view-source:https://"
+ );
+});
diff --git a/dom/security/test/https-first/browser_download_attribute.js b/dom/security/test/https-first/browser_download_attribute.js
new file mode 100644
index 0000000000..8165add998
--- /dev/null
+++ b/dom/security/test/https-first/browser_download_attribute.js
@@ -0,0 +1,125 @@
+"use strict";
+
+// Create a uri for an http site
+//(in that case a site without cert such that https-first isn't upgrading it)
+const insecureTestPath = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://nocert.example.com"
+);
+const insecureTestURI = insecureTestPath + "file_download_attribute.html";
+
+function promisePanelOpened() {
+ if (DownloadsPanel.panel && DownloadsPanel.panel.state == "open") {
+ return Promise.resolve();
+ }
+ return BrowserTestUtils.waitForEvent(DownloadsPanel.panel, "popupshown");
+}
+
+const CONSOLE_UPGRADE_TRY_MESSAGE = "Upgrading insecure request";
+const CONSOLE_ERROR_MESSAGE = "Downgrading to “http†again";
+const DOWNLOAD_PAGE_URL =
+ "nocert.example.com/browser/dom/security/test/https-first/file_download_attribute.html";
+const DOWNLOAD_LINK_URL =
+ "nocert.example.com/browser/dom/security/test/https-first/file_download_attribute.sjs";
+
+// Verifys that https-first tried to upgrade the download
+// - and that the upgrade attempt failed.
+// We will receive 4 messages. Two for upgrading and downgrading
+// the download page and another two for upgrading and downgrading
+// the download.
+let msgCounter = 0;
+function shouldConsoleTryUpgradeAndError() {
+ // Waits until CONSOLE_ERROR_MESSAGE was logged.
+ // Checks if download was tried via http://
+ return new Promise((resolve, reject) => {
+ function listener(msgObj) {
+ let text = msgObj.message;
+ // Verify upgrade messages
+ if (
+ text.includes(CONSOLE_UPGRADE_TRY_MESSAGE) &&
+ text.includes("http://")
+ ) {
+ if (msgCounter == 0) {
+ ok(
+ text.includes(DOWNLOAD_PAGE_URL),
+ "Tries to upgrade nocert example to https"
+ );
+ } else {
+ ok(
+ text.includes(DOWNLOAD_LINK_URL),
+ "Tries to upgrade download to https"
+ );
+ }
+ msgCounter++;
+ }
+ // Verify downgrade messages
+ if (text.includes(CONSOLE_ERROR_MESSAGE) && msgCounter > 0) {
+ if (msgCounter == 1) {
+ ok(
+ text.includes("https://" + DOWNLOAD_PAGE_URL),
+ "Downgrades nocert example to http"
+ );
+ msgCounter++;
+ } else {
+ ok(
+ text.includes("https://" + DOWNLOAD_LINK_URL),
+ "Downgrades download to http"
+ );
+ Services.console.unregisterListener(listener);
+ resolve();
+ }
+ }
+ }
+ Services.console.registerListener(listener);
+ });
+}
+
+// Test https-first download of an html file from an http site.
+// Test description:
+// 1. https-first tries to upgrade site to https
+// 2. upgrade fails because site has no certificate
+// 3. https-first downgrades to http and starts download via http
+// 4. Successfully completes download
+add_task(async function test_with_downloads_pref_enabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_first", true]],
+ });
+ let checkPromise = shouldConsoleTryUpgradeAndError();
+ let downloadsPanelPromise = promisePanelOpened();
+ let downloadsPromise = Downloads.getList(Downloads.PUBLIC);
+
+ BrowserTestUtils.startLoadingURIString(gBrowser, insecureTestURI);
+ // wait for downloadsPanel to open before continuing with test
+ await downloadsPanelPromise;
+ let downloadList = await downloadsPromise;
+ await checkPromise;
+ is(DownloadsPanel.isPanelShowing, true, "DownloadsPanel should be open.");
+ is(
+ downloadList._downloads.length,
+ 1,
+ "File should be successfully downloaded."
+ );
+
+ let [download] = downloadList._downloads;
+ is(download.contentType, "text/html", "File contentType should be correct.");
+ // ensure https-first didn't upgrade the scheme.
+ is(
+ download.source.url,
+ insecureTestPath + "file_download_attribute.sjs",
+ "Scheme should be http."
+ );
+
+ info("cleaning up downloads");
+ try {
+ if (Services.appinfo.OS === "WINNT") {
+ // We need to make the file writable to delete it on Windows.
+ await IOUtils.setPermissions(download.target.path, 0o600);
+ }
+ await IOUtils.remove(download.target.path);
+ } catch (error) {
+ info("The file " + download.target.path + " is not removed, " + error);
+ }
+
+ await downloadList.remove(download);
+ await download.finalize();
+});
diff --git a/dom/security/test/https-first/browser_httpsfirst.js b/dom/security/test/https-first/browser_httpsfirst.js
new file mode 100644
index 0000000000..733474dcc1
--- /dev/null
+++ b/dom/security/test/https-first/browser_httpsfirst.js
@@ -0,0 +1,74 @@
+"use strict";
+
+const TEST_PATH_HTTP = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+
+const TIMEOUT_PAGE_URI_HTTP =
+ TEST_PATH_HTTP + "file_httpsfirst_timeout_server.sjs";
+
+async function runPrefTest(aURI, aDesc, aAssertURLStartsWith) {
+ await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ const loaded = BrowserTestUtils.browserLoaded(browser, false, null, true);
+ BrowserTestUtils.startLoadingURIString(browser, aURI);
+ await loaded;
+
+ await ContentTask.spawn(
+ browser,
+ { aDesc, aAssertURLStartsWith },
+ function ({ aDesc, aAssertURLStartsWith }) {
+ ok(
+ content.document.location.href.startsWith(aAssertURLStartsWith),
+ aDesc
+ );
+ }
+ );
+ });
+}
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_first", false]],
+ });
+
+ await runPrefTest(
+ "http://example.com",
+ "HTTPS-First disabled; Should not upgrade",
+ "http://"
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_first", true]],
+ });
+
+ await runPrefTest(
+ "http://example.com",
+ "Should upgrade upgradeable website",
+ "https://"
+ );
+
+ await runPrefTest(
+ "http://httpsfirst.com",
+ "Should downgrade after error.",
+ "http://"
+ );
+
+ await runPrefTest(
+ "http://httpsfirst.com/?https://httpsfirst.com",
+ "Should downgrade after error and leave query params untouched.",
+ "http://httpsfirst.com/?https://httpsfirst.com"
+ );
+
+ await runPrefTest(
+ "http://domain.does.not.exist",
+ "Should not downgrade on dnsNotFound error.",
+ "https://"
+ );
+
+ await runPrefTest(
+ TIMEOUT_PAGE_URI_HTTP,
+ "Should downgrade after timeout.",
+ "http://"
+ );
+});
diff --git a/dom/security/test/https-first/browser_httpsfirst_console_logging.js b/dom/security/test/https-first/browser_httpsfirst_console_logging.js
new file mode 100644
index 0000000000..84a2cbbcbc
--- /dev/null
+++ b/dom/security/test/https-first/browser_httpsfirst_console_logging.js
@@ -0,0 +1,72 @@
+// Bug 1658924 - HTTPS-First Mode - Tests for console logging
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1658924
+// This test makes sure that the various console messages from the HTTPS-First
+// mode get logged to the console.
+"use strict";
+
+// Test Cases
+// description: Description of what the subtests expects.
+// expectLogLevel: Expected log-level of a message.
+// expectIncludes: Expected substrings the message should contain.
+let tests = [
+ {
+ description: "Top-Level upgrade should get logged",
+ expectLogLevel: Ci.nsIConsoleMessage.warn,
+ expectIncludes: ["Upgrading insecure request", "to use", "httpsfirst.com"],
+ },
+ {
+ description: "Top-Level upgrade failure should get logged",
+ expectLogLevel: Ci.nsIConsoleMessage.warn,
+ expectIncludes: [
+ "Upgrading insecure request",
+ "failed",
+ "httpsfirst.com",
+ "Downgrading to",
+ ],
+ },
+];
+
+add_task(async function () {
+ // A longer timeout is necessary for this test than the plain mochitests
+ // due to opening a new tab with the web console.
+ requestLongerTimeout(4);
+
+ // Enable HTTPS-First Mode and register console-listener
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_first", true]],
+ });
+ Services.console.registerListener(on_new_message);
+ // 1. Upgrade page to https://
+ let promiseLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ "http://httpsfirst.com"
+ );
+ await promiseLoaded;
+ await BrowserTestUtils.waitForCondition(() => tests.length === 0);
+
+ // Clean up
+ Services.console.unregisterListener(on_new_message);
+});
+
+function on_new_message(msgObj) {
+ const message = msgObj.message;
+ const logLevel = msgObj.logLevel;
+
+ if (message.includes("HTTPS-First Mode:")) {
+ for (let i = 0; i < tests.length; i++) {
+ const testCase = tests[i];
+ // Check if log-level matches
+ if (logLevel !== testCase.expectLogLevel) {
+ continue;
+ }
+ // Check if all substrings are included
+ if (testCase.expectIncludes.some(str => !message.includes(str))) {
+ continue;
+ }
+ ok(true, testCase.description);
+ tests.splice(i, 1);
+ break;
+ }
+ }
+}
diff --git a/dom/security/test/https-first/browser_httpsfirst_speculative_connect.js b/dom/security/test/https-first/browser_httpsfirst_speculative_connect.js
new file mode 100644
index 0000000000..f06ba0a944
--- /dev/null
+++ b/dom/security/test/https-first/browser_httpsfirst_speculative_connect.js
@@ -0,0 +1,71 @@
+"use strict";
+
+const TEST_PATH_HTTP = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+
+let console_messages = [
+ {
+ description: "Speculative Connection should get logged",
+ expectLogLevel: Ci.nsIConsoleMessage.warn,
+ expectIncludes: [
+ "Upgrading insecure speculative TCP connection",
+ "to use",
+ "example.com",
+ "file_httpsfirst_speculative_connect.html",
+ ],
+ },
+ {
+ description: "Upgrade should get logged",
+ expectLogLevel: Ci.nsIConsoleMessage.warn,
+ expectIncludes: [
+ "Upgrading insecure request",
+ "to use",
+ "example.com",
+ "file_httpsfirst_speculative_connect.html",
+ ],
+ },
+];
+
+function on_new_console_messages(msgObj) {
+ const message = msgObj.message;
+ const logLevel = msgObj.logLevel;
+
+ if (message.includes("HTTPS-First Mode:")) {
+ for (let i = 0; i < console_messages.length; i++) {
+ const testCase = console_messages[i];
+ // Check if log-level matches
+ if (logLevel !== testCase.expectLogLevel) {
+ continue;
+ }
+ // Check if all substrings are included
+ if (testCase.expectIncludes.some(str => !message.includes(str))) {
+ continue;
+ }
+ ok(true, testCase.description);
+ console_messages.splice(i, 1);
+ break;
+ }
+ }
+}
+
+add_task(async function () {
+ requestLongerTimeout(4);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_first", true]],
+ });
+ Services.console.registerListener(on_new_console_messages);
+
+ let promiseLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ `${TEST_PATH_HTTP}file_httpsfirst_speculative_connect.html`
+ );
+ await promiseLoaded;
+
+ await BrowserTestUtils.waitForCondition(() => console_messages.length === 0);
+
+ Services.console.unregisterListener(on_new_console_messages);
+});
diff --git a/dom/security/test/https-first/browser_mixed_content_console.js b/dom/security/test/https-first/browser_mixed_content_console.js
new file mode 100644
index 0000000000..c8322c036f
--- /dev/null
+++ b/dom/security/test/https-first/browser_mixed_content_console.js
@@ -0,0 +1,104 @@
+// Bug 1713593: HTTPS-First: Add test for mixed content blocker.
+"use strict";
+
+const testPath = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+
+const UPGRADE_DISPLAY_CONTENT =
+ "security.mixed_content.upgrade_display_content";
+
+let threeMessagesArrived = 0;
+let messageImageSeen = false;
+
+const kTestURI = testPath + "file_mixed_content_console.html";
+
+add_task(async function () {
+ // A longer timeout is necessary for this test than the plain mochitests
+ // due to opening a new tab with the web console.
+ requestLongerTimeout(4);
+
+ // Enable HTTPS-First Mode and register console-listener
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_first", true]],
+ });
+ Services.console.registerListener(on_console_message);
+ BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, kTestURI);
+
+ await BrowserTestUtils.waitForCondition(() => threeMessagesArrived === 3);
+
+ Services.console.unregisterListener(on_console_message);
+});
+
+function on_console_message(msgObj) {
+ const message = msgObj.message;
+
+ // The first console message is:
+ // "HTTPS-First Mode: Upgrading insecure request
+ // ‘http://example.com/browser/dom/security/test/https-first/file_mixed_content_console.html’ to use ‘https’"
+ if (message.includes("HTTPS-First Mode: Upgrading insecure request")) {
+ ok(message.includes("Upgrading insecure request"), "request got upgraded");
+ ok(
+ message.includes(
+ "“http://example.com/browser/dom/security/test/https-first/file_mixed_content_console.html†to use “httpsâ€."
+ ),
+ "correct top-level request"
+ );
+ threeMessagesArrived++;
+ }
+ // If security.mixed_content.upgrade_display_content is enabled:
+ // The second console message is about upgrading the insecure image
+ else if (
+ Services.prefs.getBoolPref(UPGRADE_DISPLAY_CONTENT) &&
+ message.includes("Mixed Content: Upgrading")
+ ) {
+ ok(
+ message.includes("insecure display request"),
+ "display content got load"
+ );
+ ok(
+ message.includes(
+ "‘http://example.com/browser/dom/security/test/https-first/auto_upgrading_identity.png’ to use ‘https’"
+ ),
+ "img loaded secure"
+ );
+ threeMessagesArrived++;
+ messageImageSeen = true;
+ }
+ // Else:
+ // The second console message is about blocking the image:
+ // Message: "Loading mixed (insecure) display content
+ // “http://example.com/browser/dom/security/test/https-first/auto_upgrading_identity.png†on a secure page".
+ // Since the message is send twice, prevent reading the image message two times
+ else if (message.includes("Loading mixed") && !messageImageSeen) {
+ ok(
+ message.includes("Loading mixed (insecure) display content"),
+ "display content got load"
+ );
+ ok(
+ message.includes(
+ "“http://example.com/browser/dom/security/test/https-first/auto_upgrading_identity.png†on a secure page"
+ ),
+ "img loaded insecure"
+ );
+ threeMessagesArrived++;
+ messageImageSeen = true;
+ }
+ // The third message is:
+ // "Blocked loading mixed active content
+ // "http://example.com/browser/dom/security/test/https-first/barfoo""
+ else if (message.includes("Blocked loading")) {
+ ok(
+ message.includes("Blocked loading mixed active content"),
+ "script got blocked"
+ );
+ ok(
+ message.includes(
+ "http://example.com/browser/dom/security/test/https-first/barfoo"
+ ),
+ "the right script got blocked"
+ );
+ threeMessagesArrived++;
+ }
+}
diff --git a/dom/security/test/https-first/browser_mixed_content_download.js b/dom/security/test/https-first/browser_mixed_content_download.js
new file mode 100644
index 0000000000..09ea64cea8
--- /dev/null
+++ b/dom/security/test/https-first/browser_mixed_content_download.js
@@ -0,0 +1,156 @@
+ChromeUtils.defineESModuleGetters(this, {
+ Downloads: "resource://gre/modules/Downloads.sys.mjs",
+ DownloadsCommon: "resource:///modules/DownloadsCommon.sys.mjs",
+});
+
+const HandlerService = Cc[
+ "@mozilla.org/uriloader/handler-service;1"
+].getService(Ci.nsIHandlerService);
+
+const MIMEService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+
+let SECURE_BASE_URL =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+ ) + "download_page.html";
+
+/**
+ * Waits until a download is triggered.
+ * It waits until a prompt is shown,
+ * saves and then accepts the dialog.
+ * @returns {Promise} Resolved once done.
+ */
+
+function shouldTriggerDownload() {
+ return new Promise((resolve, reject) => {
+ Services.wm.addListener({
+ onOpenWindow(xulWin) {
+ Services.wm.removeListener(this);
+ let win = xulWin.docShell.domWindow;
+ waitForFocus(() => {
+ if (
+ win.location ==
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml"
+ ) {
+ let dialog = win.document.getElementById("unknownContentType");
+ let button = dialog.getButton("accept");
+ let actionRadio = win.document.getElementById("save");
+ actionRadio.click();
+ button.disabled = false;
+ dialog.acceptDialog();
+ resolve();
+ } else {
+ reject();
+ }
+ }, win);
+ },
+ });
+ });
+}
+
+const CONSOLE_UPGRADE_MESSAGE = "Upgrading insecure request";
+const CONSOLE_DOWNGRADE_MESSAGE = "Downgrading to “http†again.";
+const DOWNLOAD_URL =
+ "example.com/browser/dom/security/test/https-first/download_server.sjs";
+// Verifies that https-first tries to upgrade download,
+// falls back since download is not available via https
+let msgCounter = 0;
+function shouldConsoleError() {
+ return new Promise((resolve, reject) => {
+ function listener(msgObj) {
+ let text = msgObj.message;
+ if (text.includes(CONSOLE_UPGRADE_MESSAGE) && msgCounter == 0) {
+ ok(
+ text.includes("http://" + DOWNLOAD_URL),
+ "Https-first tries to upgrade download to https"
+ );
+ msgCounter++;
+ }
+ if (text.includes(CONSOLE_DOWNGRADE_MESSAGE) && msgCounter == 1) {
+ ok(
+ text.includes("https://" + DOWNLOAD_URL),
+ "Https-first downgrades download to http."
+ );
+ resolve();
+ Services.console.unregisterListener(listener);
+ }
+ }
+ Services.console.registerListener(listener);
+ });
+}
+
+async function resetDownloads() {
+ // Removes all downloads from the download List
+ const types = new Set();
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ let downloads = await publicList.getAll();
+ for (let download of downloads) {
+ if (download.contentType) {
+ types.add(download.contentType);
+ }
+ publicList.remove(download);
+ await download.finalize(true);
+ }
+
+ if (types.size) {
+ // reset handlers for the contentTypes of any files previously downloaded
+ for (let type of types) {
+ const mimeInfo = MIMEService.getFromTypeAndExtension(type, "");
+ info("resetting handler for type: " + type);
+ HandlerService.remove(mimeInfo);
+ }
+ }
+}
+
+async function runTest(url, link, checkFunction, description) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.security.https_first", true],
+ ["browser.download.always_ask_before_handling_new_types", true],
+ ],
+ });
+ requestLongerTimeout(2);
+ await resetDownloads();
+
+ let tab = BrowserTestUtils.addTab(gBrowser, url);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+ is(
+ gBrowser.currentURI.schemeIs("https"),
+ true,
+ "Scheme of opened tab should be https"
+ );
+ info("Checking: " + description);
+
+ let checkPromise = checkFunction();
+ // Click the Link to trigger the download
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [link], contentLink => {
+ content.document.getElementById(contentLink).click();
+ });
+ await checkPromise;
+ ok(true, description);
+ BrowserTestUtils.removeTab(tab);
+}
+
+//Test description:
+// 1. Open "https://example.com"
+// 2. From "https://example.com" download something, but that download is only available via http.
+// 3. Https-first tries to upgrade the download.
+// 4. Upgrading fails - so http-first downgrade download to http.
+
+add_task(async function test_mixed_download() {
+ await runTest(
+ SECURE_BASE_URL,
+ "insecure",
+ () => Promise.all([shouldTriggerDownload(), shouldConsoleError()]),
+ "Secure -> Insecure should Error"
+ );
+ // remove downloaded file
+ let downloadsPromise = Downloads.getList(Downloads.PUBLIC);
+ let downloadList = await downloadsPromise;
+ let [download] = downloadList._downloads;
+ await downloadList.remove(download);
+});
diff --git a/dom/security/test/https-first/browser_navigation.js b/dom/security/test/https-first/browser_navigation.js
new file mode 100644
index 0000000000..dba83d5645
--- /dev/null
+++ b/dom/security/test/https-first/browser_navigation.js
@@ -0,0 +1,97 @@
+"use strict";
+
+const REQUEST_URL =
+ "http://httpsfirst.com/browser/dom/security/test/https-first/";
+
+async function promiseGetHistoryIndex(browser) {
+ if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ return SpecialPowers.spawn(browser, [], function () {
+ let shistory =
+ docShell.browsingContext.childSessionHistory.legacySHistory;
+ return shistory.index;
+ });
+ }
+
+ let shistory = browser.browsingContext.sessionHistory;
+ return shistory.index;
+}
+
+async function testNavigations() {
+ // Load initial site
+
+ let url1 = REQUEST_URL + "file_navigation.html?foo1";
+ let url2 = REQUEST_URL + "file_navigation.html?foo2";
+ let url3 = REQUEST_URL + "file_navigation.html?foo3";
+
+ let loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ BrowserTestUtils.startLoadingURIString(gBrowser, url1);
+ await loaded;
+
+ // Load another site
+ loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [url2],
+ async url => (content.location.href = url)
+ );
+ await loaded;
+
+ // Load another site
+ loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [url3],
+ async url => (content.location.href = url)
+ );
+ await loaded;
+ is(
+ await promiseGetHistoryIndex(gBrowser.selectedBrowser),
+ 2,
+ "correct session history index after load 3"
+ );
+
+ // Go back one site by clicking the back button
+ info("Clicking back button");
+ loaded = BrowserTestUtils.waitForLocationChange(gBrowser, url2);
+ let backButton = document.getElementById("back-button");
+ backButton.click();
+ await loaded;
+ is(
+ await promiseGetHistoryIndex(gBrowser.selectedBrowser),
+ 1,
+ "correct session history index after going back for the first time"
+ );
+
+ // Go back again
+ info("Clicking back button again");
+ loaded = BrowserTestUtils.waitForLocationChange(gBrowser, url1);
+ backButton.click();
+ await loaded;
+ is(
+ await promiseGetHistoryIndex(gBrowser.selectedBrowser),
+ 0,
+ "correct session history index after going back for the second time"
+ );
+}
+
+add_task(async function () {
+ waitForExplicitFinish();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_first", true]],
+ });
+
+ await testNavigations();
+
+ // If fission is enabled we also want to test the navigations with the bfcache
+ // in the parent.
+ if (SpecialPowers.getBoolPref("fission.autostart")) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["fission.bfcacheInParent", true]],
+ });
+
+ await testNavigations();
+ }
+
+ finish();
+});
diff --git a/dom/security/test/https-first/browser_schemeless.js b/dom/security/test/https-first/browser_schemeless.js
new file mode 100644
index 0000000000..9687f15072
--- /dev/null
+++ b/dom/security/test/https-first/browser_schemeless.js
@@ -0,0 +1,191 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// We explicitly need HTTP URLs in this test
+/* eslint-disable @microsoft/sdl/no-insecure-url */
+
+ChromeUtils.defineLazyGetter(this, "UrlbarTestUtils", () => {
+ const { UrlbarTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlbarTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper"
+);
+
+/** Type aInput into the address bar and press enter */
+async function runMainTest(aInput, aDesc, aExpectedScheme) {
+ await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ const loaded = BrowserTestUtils.browserLoaded(browser, false, null, true);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: aInput,
+ });
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loaded;
+
+ is(browser.currentURI.scheme, aExpectedScheme, "Main test: " + aDesc);
+ });
+}
+
+/**
+ * Type aInput into the address bar and press ctrl+enter,
+ * resulting in the input being canonized first.
+ * This should not change schemeless HTTPS behaviour. */
+async function runCanonizedTest(aInput, aDesc, aExpectedScheme) {
+ await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ const loaded = BrowserTestUtils.browserLoaded(browser, false, null, true);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: aInput,
+ });
+ EventUtils.synthesizeKey("KEY_Enter", { ctrlKey: true });
+ await loaded;
+
+ is(browser.currentURI.scheme, aExpectedScheme, "Canonized test: " + aDesc);
+ });
+}
+
+/**
+ * Type aInput into the address bar and press alt+enter,
+ * resulting in the input being loaded in a new tab.
+ * This should not change schemeless HTTPS behaviour. */
+async function runNewTabTest(aInput, aDesc, aExpectedScheme) {
+ await BrowserTestUtils.withNewTab(
+ "about:about", // For alt+enter to do anything, we need to be on a page other than about:blank.
+ async function () {
+ const newTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ null,
+ true
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: aInput,
+ });
+ EventUtils.synthesizeKey("KEY_Enter", { altKey: true });
+ const newTab = await newTabPromise;
+
+ is(
+ newTab.linkedBrowser.currentURI.scheme,
+ aExpectedScheme,
+ "New tab test: " + aDesc
+ );
+
+ BrowserTestUtils.removeTab(newTab);
+ }
+ );
+}
+
+/**
+ * Type aInput into the address bar and press shift+enter,
+ * resulting in the input being loaded in a new window.
+ * This should not change schemeless HTTPS behaviour. */
+async function runNewWindowTest(aInput, aDesc, aExpectedScheme) {
+ await BrowserTestUtils.withNewTab("about:about", async function () {
+ const newWindowPromise = BrowserTestUtils.waitForNewWindow({
+ waitForAnyURLLoaded: true,
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: aInput,
+ });
+ EventUtils.synthesizeKey("KEY_Enter", { shiftKey: true });
+ const newWindow = await newWindowPromise;
+
+ is(
+ newWindow.gBrowser.selectedBrowser.currentURI.scheme,
+ aExpectedScheme,
+ "New window test: " + aDesc
+ );
+
+ await BrowserTestUtils.closeWindow(newWindow);
+ });
+}
+
+/**
+ * Instead of typing aInput into the address bar, copy it
+ * to the clipboard and use the "Paste and Go" menu entry.
+ * This should not change schemeless HTTPS behaviour. */
+async function runPasteAndGoTest(aInput, aDesc, aExpectedScheme) {
+ await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ gURLBar.focus();
+ await SimpleTest.promiseClipboardChange(aInput, () => {
+ clipboardHelper.copyString(aInput);
+ });
+
+ const loaded = BrowserTestUtils.browserLoaded(browser, false, null, true);
+ const textBox = gURLBar.querySelector("moz-input-box");
+ const cxmenu = textBox.menupopup;
+ const cxmenuPromise = BrowserTestUtils.waitForEvent(cxmenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await cxmenuPromise;
+ const menuitem = textBox.getMenuItem("paste-and-go");
+ menuitem.closest("menupopup").activateItem(menuitem);
+ await loaded;
+
+ is(
+ browser.currentURI.scheme,
+ aExpectedScheme,
+ "Paste and go test: " + aDesc
+ );
+ });
+}
+
+async function runTest(aInput, aDesc, aExpectedScheme) {
+ await runMainTest(aInput, aDesc, aExpectedScheme);
+ await runCanonizedTest(aInput, aDesc, aExpectedScheme);
+ await runNewTabTest(aInput, aDesc, aExpectedScheme);
+ await runNewWindowTest(aInput, aDesc, aExpectedScheme);
+ await runPasteAndGoTest(aInput, aDesc, aExpectedScheme);
+}
+
+add_task(async function () {
+ requestLongerTimeout(10);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.security.https_first", false],
+ ["dom.security.https_first_schemeless", false],
+ ],
+ });
+
+ await runTest(
+ "http://example.com",
+ "Should not upgrade upgradeable website with explicit scheme",
+ "http"
+ );
+
+ await runTest(
+ "example.com",
+ "Should not upgrade upgradeable website without explicit scheme",
+ "http"
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_first_schemeless", true]],
+ });
+
+ await runTest(
+ "http://example.com",
+ "Should not upgrade upgradeable website with explicit scheme",
+ "http"
+ );
+
+ await runTest(
+ "example.com",
+ "Should upgrade upgradeable website without explicit scheme",
+ "https"
+ );
+});
diff --git a/dom/security/test/https-first/browser_slow_download.js b/dom/security/test/https-first/browser_slow_download.js
new file mode 100644
index 0000000000..1583c6d361
--- /dev/null
+++ b/dom/security/test/https-first/browser_slow_download.js
@@ -0,0 +1,158 @@
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
+});
+// Create a uri for an https site
+const testPath = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const TEST_URI = testPath + "file_slow_download.html";
+const EXPECTED_DOWNLOAD_URL =
+ "example.com/browser/dom/security/test/https-first/file_slow_download.sjs";
+
+// Since the server send the complete download file after 3 seconds we need an longer timeout
+requestLongerTimeout(4);
+
+function promisePanelOpened() {
+ if (DownloadsPanel.panel && DownloadsPanel.panel.state == "open") {
+ return Promise.resolve();
+ }
+ return BrowserTestUtils.waitForEvent(DownloadsPanel.panel, "popupshown");
+}
+
+/**
+ * Waits for a download to finish, in case it has not finished already.
+ *
+ * @param aDownload
+ * The Download object to wait upon.
+ *
+ * @return {Promise}
+ * @resolves When the download has finished successfully.
+ * @rejects JavaScript exception if the download failed.
+ */
+function promiseDownloadStopped(aDownload) {
+ if (!aDownload.stopped) {
+ // The download is in progress, wait for the current attempt to finish and
+ // report any errors that may occur.
+ return aDownload.start();
+ }
+
+ if (aDownload.succeeded) {
+ return Promise.resolve();
+ }
+
+ // The download failed or was canceled.
+ return Promise.reject(aDownload.error || new Error("Download canceled."));
+}
+
+// Verifys that no background request was send
+let requestCounter = 0;
+function examiner() {
+ SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
+}
+
+examiner.prototype = {
+ observe(subject, topic, data) {
+ if (topic !== "specialpowers-http-notify-request") {
+ return;
+ }
+ // On Android we have other requests appear here as well. Let's make
+ // sure we only evaluate requests triggered by the test.
+ if (
+ !data.startsWith("http://example.com") &&
+ !data.startsWith("https://example.com")
+ ) {
+ return;
+ }
+ ++requestCounter;
+ if (requestCounter == 1) {
+ is(data, TEST_URI, "Download start page is https");
+ return;
+ }
+ if (requestCounter == 2) {
+ // The specialpowers-http-notify-request fires before the internal redirect( /upgrade) to
+ // https happens.
+ is(
+ data,
+ "http://" + EXPECTED_DOWNLOAD_URL,
+ "First download request is http (internal)"
+ );
+ return;
+ }
+ if (requestCounter == 3) {
+ is(
+ data,
+ "https://" + EXPECTED_DOWNLOAD_URL,
+ "Download got upgraded to https"
+ );
+ return;
+ }
+ ok(false, "we should never get here, but just in case");
+ },
+ remove() {
+ SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
+ },
+};
+
+// Test description:
+// 1. Open https://example.com
+// 2. Start download - location of download is http
+// 3. https-first upgrades to https
+// 4. Server send first part of download and after 3 seconds the rest
+// 5. Complete download of text file
+add_task(async function test_slow_download() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_first", true]],
+ });
+
+ // remove all previous downloads
+ let downloadsList = await Downloads.getList(Downloads.PUBLIC);
+ await downloadsList.removeFinished();
+
+ // add observer to ensure that the background request gets canceled for the upgraded Download
+ this.examiner = new examiner();
+
+ let downloadsPanelPromise = promisePanelOpened();
+ let downloadsPromise = Downloads.getList(Downloads.PUBLIC);
+ BrowserTestUtils.startLoadingURIString(gBrowser, TEST_URI);
+ // wait for downloadsPanel to open before continuing with test
+ await downloadsPanelPromise;
+ let downloadList = await downloadsPromise;
+ is(DownloadsPanel.isPanelShowing, true, "DownloadsPanel should be open.");
+ is(downloadList._downloads.length, 1, "File should be downloaded.");
+ let [download] = downloadList._downloads;
+ // wait for download to finish (with success or error)
+ await promiseDownloadStopped(download);
+ is(download.contentType, "text/plain", "File contentType should be correct.");
+ // ensure https-first did upgrade the scheme.
+ is(
+ download.source.url,
+ "https://" + EXPECTED_DOWNLOAD_URL,
+ "Scheme should be https."
+ );
+ // ensure that no background request was send
+ is(
+ requestCounter,
+ 3,
+ "three requests total (download page, download http, download https/ upgraded)"
+ );
+ // ensure that downloaded is complete
+ is(download.target.size, 25, "Download size is correct");
+ //clean up
+ this.examiner.remove();
+ info("cleaning up downloads");
+ try {
+ if (Services.appinfo.OS === "WINNT") {
+ // We need to make the file writable to delete it on Windows.
+ await IOUtils.setPermissions(download.target.path, 0o600);
+ }
+ await IOUtils.remove(download.target.path);
+ } catch (error) {
+ info("The file " + download.target.path + " is not removed, " + error);
+ }
+
+ await downloadList.remove(download);
+ await download.finalize();
+});
diff --git a/dom/security/test/https-first/browser_superfluos_auth.js b/dom/security/test/https-first/browser_superfluos_auth.js
new file mode 100644
index 0000000000..961430a444
--- /dev/null
+++ b/dom/security/test/https-first/browser_superfluos_auth.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This test checks the superfluos auth prompt when HTTPS-First is enabled (Bug 1858565).
+
+const TEST_URI = "https://www.mozilla.org@example.com/";
+
+const { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+let respondMockPromptWithYes = false;
+
+const gMockPromptService = {
+ firstTimeCalled: false,
+ confirmExBC() {
+ return respondMockPromptWithYes ? 0 : 1;
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIPromptService"]),
+};
+
+var gMockPromptServiceCID = MockRegistrar.register(
+ "@mozilla.org/prompter;1",
+ gMockPromptService
+);
+
+registerCleanupFunction(() => {
+ MockRegistrar.unregister(gMockPromptServiceCID);
+});
+
+function checkBrowserLoad(browser) {
+ return new Promise(resolve => {
+ BrowserTestUtils.browserLoaded(browser, false, null, true).then(() => {
+ resolve(true);
+ });
+ BrowserTestUtils.browserStopped(browser, false, null, true).then(() => {
+ resolve(false);
+ });
+ });
+}
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_first", true]],
+ });
+
+ respondMockPromptWithYes = false;
+ let didBrowserLoadPromise = checkBrowserLoad(gBrowser.selectedBrowser);
+ BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, TEST_URI);
+ let didBrowserLoad = await didBrowserLoadPromise;
+ ok(
+ !didBrowserLoad,
+ "The browser should stop the load when the user refuses to load a page with superfluos authentication"
+ );
+
+ respondMockPromptWithYes = true;
+ didBrowserLoadPromise = checkBrowserLoad(gBrowser.selectedBrowser);
+ BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, TEST_URI);
+ didBrowserLoad = await didBrowserLoadPromise;
+ ok(
+ didBrowserLoad,
+ "The browser should load when the user agrees to load a page with superfluos authentication"
+ );
+});
diff --git a/dom/security/test/https-first/browser_upgrade_onion.js b/dom/security/test/https-first/browser_upgrade_onion.js
new file mode 100644
index 0000000000..5987eda580
--- /dev/null
+++ b/dom/security/test/https-first/browser_upgrade_onion.js
@@ -0,0 +1,60 @@
+// This test ensures that various configurable upgrade exceptions work
+"use strict";
+
+async function runTest(desc, url, expectedURI) {
+ await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, null, true);
+ BrowserTestUtils.startLoadingURIString(browser, url);
+ await loaded;
+
+ await SpecialPowers.spawn(
+ browser,
+ [desc, expectedURI],
+ async function (desc, expectedURI) {
+ // XXX ckerschb: generally we use the documentURI, but our test infra
+ // can not handle .onion, hence we use the URI of the failed channel
+ // stored on the docshell to see if the scheme was upgraded to https.
+ let loadedURI = content.document.documentURI;
+ if (loadedURI.startsWith("about:neterror")) {
+ loadedURI = content.docShell.failedChannel.URI.spec;
+ }
+ is(loadedURI, expectedURI, desc);
+ }
+ );
+ });
+}
+
+// by default local addresses and .onion should *not* get upgraded
+add_task(async function () {
+ requestLongerTimeout(2);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.security.https_first", true],
+ ["dom.security.https_only_mode", false],
+ ["dom.security.https_only_mode.upgrade_local", false],
+ ["dom.security.https_only_mode.upgrade_onion", false],
+ ],
+ });
+
+ await runTest(
+ "Hosts ending with .onion should be be exempt from HTTPS-First upgrades by default",
+ "http://grocery.shopping.for.one.onion/",
+ "http://grocery.shopping.for.one.onion/"
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.security.https_first", true],
+ ["dom.security.https_only_mode", false],
+ ["dom.security.https_only_mode.upgrade_local", false],
+ ["dom.security.https_only_mode.upgrade_onion", true],
+ ],
+ });
+
+ await runTest(
+ "Hosts ending with .onion should get upgraded when 'dom.security.https_only_mode.upgrade_onion' is set to true",
+ "http://grocery.shopping.for.one.onion/",
+ "https://grocery.shopping.for.one.onion/"
+ );
+});
diff --git a/dom/security/test/https-first/download_page.html b/dom/security/test/https-first/download_page.html
new file mode 100644
index 0000000000..a828ee07db
--- /dev/null
+++ b/dom/security/test/https-first/download_page.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Test mixed content download by https-first</title>
+ </head>
+ <body>
+ hi
+
+ <script>
+ const host = window.location.host;
+ const path = location.pathname.replace("download_page.html","download_server.sjs");
+
+ const insecureLink = document.createElement("a");
+ insecureLink.href=`http://${host}${path}`;
+ insecureLink.download="true";
+ insecureLink.id="insecure";
+ insecureLink.textContent="Not secure Link";
+
+ document.body.append(insecureLink);
+ </script>
+ </body>
+</html>
diff --git a/dom/security/test/https-first/download_server.sjs b/dom/security/test/https-first/download_server.sjs
new file mode 100644
index 0000000000..7af5722e7b
--- /dev/null
+++ b/dom/security/test/https-first/download_server.sjs
@@ -0,0 +1,16 @@
+function handleRequest(request, response) {
+ // Only answer to http, in case request is in https let the reply time out.
+ if (request.scheme === "https") {
+ response.processAsync();
+ return;
+ }
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ // Send some file, e.g. an image
+ response.setHeader(
+ "Content-Disposition",
+ "attachment; filename=some-file.png"
+ );
+ response.setHeader("Content-Type", "image/png");
+ response.write("Success!");
+}
diff --git a/dom/security/test/https-first/file_bad_cert.sjs b/dom/security/test/https-first/file_bad_cert.sjs
new file mode 100644
index 0000000000..1a8ae08a86
--- /dev/null
+++ b/dom/security/test/https-first/file_bad_cert.sjs
@@ -0,0 +1,34 @@
+const RESPONSE_SUCCESS = `
+ <html>
+ <body>
+ send message, downgraded
+ <script type="application/javascript">
+ let scheme = document.location.protocol;
+ window.opener.postMessage({result: 'downgraded', scheme: scheme}, '*');
+ </script>
+ </body>
+ </html>`;
+
+const RESPONSE_UNEXPECTED = `
+ <html>
+ <body>
+ send message, error
+ <script type="application/javascript">
+ let scheme = document.location.protocol;
+ window.opener.postMessage({result: 'Error', scheme: scheme}, '*');
+ </script>
+ </body>
+ </html>`;
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ // if the received request is not http send an error
+ if (request.scheme === "http") {
+ response.write(RESPONSE_SUCCESS);
+ return;
+ }
+ // we should never get here; just in case, return something unexpected
+ response.write(RESPONSE_UNEXPECTED);
+}
diff --git a/dom/security/test/https-first/file_beforeunload_permit_http.html b/dom/security/test/https-first/file_beforeunload_permit_http.html
new file mode 100644
index 0000000000..50459d6006
--- /dev/null
+++ b/dom/security/test/https-first/file_beforeunload_permit_http.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html><html>
+<body>
+ <script>
+ window.onbeforeunload = function() {
+ return "stop";
+ }
+ </script>
+</body>
+</html>
diff --git a/dom/security/test/https-first/file_break_endless_upgrade_downgrade_loop.sjs b/dom/security/test/https-first/file_break_endless_upgrade_downgrade_loop.sjs
new file mode 100644
index 0000000000..eb64c59e97
--- /dev/null
+++ b/dom/security/test/https-first/file_break_endless_upgrade_downgrade_loop.sjs
@@ -0,0 +1,61 @@
+"use strict";
+
+const REDIRECT_URI =
+ "http://example.com/tests/dom/security/test/https-first/file_break_endless_upgrade_downgrade_loop.sjs?verify";
+const DOWNGRADE_URI =
+ "http://example.com/tests/dom/security/test/https-first/file_downgrade_with_different_path.sjs";
+const RESPONSE_ERROR = "unexpected-query";
+
+// An onload postmessage to window opener
+const RESPONSE_HTTPS_SCHEME = `
+ <html>
+ <body>
+ <script type="application/javascript">
+ window.opener.postMessage({result: 'scheme-https'}, '*');
+ </script>
+ </body>
+ </html>`;
+
+const RESPONSE_HTTP_SCHEME = `
+ <html>
+ <body>
+ <script type="application/javascript">
+ window.opener.postMessage({result: 'scheme-http'}, '*');
+ </script>
+ </body>
+ </html>`;
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+ const query = request.queryString;
+
+ if (query == "downgrade") {
+ // send same-origin downgrade from https: to http: with a different path.
+ // we don't consider it's an endless upgrade downgrade loop in this case.
+ response.setStatusLine(request.httpVersion, 302, "Found");
+ response.setHeader("Location", DOWNGRADE_URI, false);
+ return;
+ }
+
+ // handle the redirect case
+ if ((query >= 301 && query <= 303) || query == 307) {
+ // send same-origin downgrade from https: to http: again simluating
+ // and endless upgrade downgrade loop.
+ response.setStatusLine(request.httpVersion, query, "Found");
+ response.setHeader("Location", REDIRECT_URI, false);
+ return;
+ }
+
+ // Check if scheme is http:// or https://
+ if (query == "verify") {
+ let response_content =
+ request.scheme === "https" ? RESPONSE_HTTPS_SCHEME : RESPONSE_HTTP_SCHEME;
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(response_content);
+ return;
+ }
+
+ // We should never get here, but just in case ...
+ response.setStatusLine(request.httpVersion, 500, "OK");
+ response.write("unexepcted query");
+}
diff --git a/dom/security/test/https-first/file_data_uri.html b/dom/security/test/https-first/file_data_uri.html
new file mode 100644
index 0000000000..69133e5079
--- /dev/null
+++ b/dom/security/test/https-first/file_data_uri.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1709069: Test that Data URI which makes a top-level request gets updated in https-first</title>
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+ window.onload = (event) => {
+ let myLoc = window.location.href;
+ window.opener.parent.postMessage({location: myLoc}, "*");
+ window.close();
+};
+</script>
+</body>
+</html>
diff --git a/dom/security/test/https-first/file_downgrade_500_responses.sjs b/dom/security/test/https-first/file_downgrade_500_responses.sjs
new file mode 100644
index 0000000000..b3cfbd79dd
--- /dev/null
+++ b/dom/security/test/https-first/file_downgrade_500_responses.sjs
@@ -0,0 +1,70 @@
+// Custom *.sjs file specifically for the needs of Bug 1709552
+"use strict";
+
+const RESPONSE_SUCCESS = `
+ <html>
+ <body>
+ send message, downgraded
+ <script type="application/javascript">
+ let scheme = document.location.protocol;
+ window.opener.postMessage({result: 'downgraded', scheme: scheme}, '*');
+ </script>
+ </body>
+ </html>`;
+
+const RESPONSE_UNEXPECTED = `
+ <html>
+ <body>
+ send message, error
+ <script type="application/javascript">
+ let scheme = document.location.protocol;
+ window.opener.postMessage({result: 'Error', scheme: scheme}, '*');
+ </script>
+ </body>
+ </html>`;
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviour
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+
+ let query = request.queryString;
+ // if the scheme is not https and it is the initial request
+ // then we rather fall through and display unexpected content
+ if (request.scheme === "https") {
+ if (query === "test1a") {
+ response.setStatusLine("1.1", 501, "Not Implemented");
+ response.write("Not Implemented\n");
+ return;
+ }
+
+ if (query === "test2a") {
+ response.setStatusLine("1.1", 504, "Gateway Timeout");
+ response.write("Gateway Timeout\n");
+ return;
+ }
+
+ if (query === "test3a") {
+ response.setStatusLine("1.1", 521, "Web Server Is Down");
+ response.write("Web Server Is Down\n");
+ return;
+ }
+ if (query === "test4a") {
+ response.setStatusLine("1.1", 530, "Railgun Error");
+ response.write("Railgun Error\n");
+ return;
+ }
+ if (query === "test5a") {
+ response.setStatusLine("1.1", 560, "Unauthorized");
+ response.write("Unauthorized\n");
+ return;
+ }
+
+ // We should never arrive here, just in case send something unexpected
+ response.write(RESPONSE_UNEXPECTED);
+ return;
+ }
+
+ // We should arrive here when the redirection was downraded successful
+ response.write(RESPONSE_SUCCESS);
+}
diff --git a/dom/security/test/https-first/file_downgrade_bad_responses.sjs b/dom/security/test/https-first/file_downgrade_bad_responses.sjs
new file mode 100644
index 0000000000..1a6eea2dd1
--- /dev/null
+++ b/dom/security/test/https-first/file_downgrade_bad_responses.sjs
@@ -0,0 +1,73 @@
+// Custom *.sjs file specifically for the needs of Bug 1709552
+"use strict";
+
+const RESPONSE_SUCCESS = `
+ <html>
+ <body>
+ send message, downgraded
+ <script type="application/javascript">
+ window.opener.postMessage({result: 'downgraded', scheme: 'http'}, '*');
+ </script>
+ </body>
+ </html>`;
+
+const RESPONSE_UNEXPECTED = `
+ <html>
+ <body>
+ send message, error
+ <script type="application/javascript">
+ window.opener.postMessage({result: 'Error', scheme: 'http'}, '*');
+ </script>
+ </body>
+ </html>`;
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviour
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+
+ let query = request.queryString;
+ // if the scheme is not https and it is the initial request
+ // then we rather fall through and display unexpected content
+ if (request.scheme === "https") {
+ if (query === "test1a") {
+ response.setStatusLine("1.1", 400, "Bad Request");
+ response.write("Bad Request\n");
+ return;
+ }
+
+ if (query === "test2a") {
+ response.setStatusLine("1.1", 403, "Forbidden");
+ response.write("Forbidden\n");
+ return;
+ }
+
+ if (query === "test3a") {
+ response.setStatusLine("1.1", 404, "Not Found");
+ response.write("Not Found\n");
+ return;
+ }
+ if (query === "test4a") {
+ response.setStatusLine("1.1", 416, "Requested Range Not Satisfiable");
+ response.write("Requested Range Not Satisfiable\n");
+ return;
+ }
+ if (query === "test5a") {
+ response.setStatusLine("1.1", 418, "I'm a teapot");
+ response.write("I'm a teapot\n");
+ return;
+ }
+ if (query == "test6a") {
+ // Simulating a timeout by processing the https request
+ response.processAsync();
+ return;
+ }
+
+ // We should never arrive here, just in case send something unexpected
+ response.write(RESPONSE_UNEXPECTED);
+ return;
+ }
+
+ // We should arrive here when the redirection was downraded successful
+ response.write(RESPONSE_SUCCESS);
+}
diff --git a/dom/security/test/https-first/file_downgrade_request_upgrade_request.sjs b/dom/security/test/https-first/file_downgrade_request_upgrade_request.sjs
new file mode 100644
index 0000000000..6004d57eaf
--- /dev/null
+++ b/dom/security/test/https-first/file_downgrade_request_upgrade_request.sjs
@@ -0,0 +1,52 @@
+// Custom *.sjs file specifically for the needs of Bug 1706126
+"use strict";
+// subdomain of example.com
+const REDIRECT_302 =
+ "http://www.redirect-example.com/tests/dom/security/test/https-first/file_downgrade_request_upgrade_request.sjs";
+
+const RESPONSE_SUCCESS = `
+ <html>
+ <body>
+ send message, upgraded
+ <script type="application/javascript">
+ let scheme = document.location.protocol;
+ window.opener.postMessage({result: 'upgraded', scheme: scheme}, '*');
+ </script>
+ </body>
+ </html>`;
+
+const RESPONSE_UNEXPECTED = `
+ <html>
+ <body>
+ send message, error
+ <script type="application/javascript">
+ let scheme = document.location.protocol;
+ window.opener.postMessage({result: 'error', scheme: scheme}, '*');
+ </script>
+ </body>
+ </html>`;
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviour
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+
+ // if the scheme is https and it is the initial request time it out
+ if (request.scheme === "https" && request.host === "redirect-example.com") {
+ // Simulating a timeout by processing the https request
+ response.processAsync();
+ return;
+ }
+ if (request.scheme === "http" && request.host === "redirect-example.com") {
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", REDIRECT_302, false);
+ return;
+ }
+ // if the request was sent to subdomain
+ if (request.host.startsWith("www.")) {
+ response.write(RESPONSE_SUCCESS);
+ return;
+ }
+ // We should never arrive here, just in case send 'error'
+ response.write(RESPONSE_UNEXPECTED);
+}
diff --git a/dom/security/test/https-first/file_downgrade_view_source.sjs b/dom/security/test/https-first/file_downgrade_view_source.sjs
new file mode 100644
index 0000000000..c57dd0deb8
--- /dev/null
+++ b/dom/security/test/https-first/file_downgrade_view_source.sjs
@@ -0,0 +1,30 @@
+"use strict";
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviour
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+
+ let query = request.queryString.split("&");
+ let scheme = request.scheme;
+
+ if (scheme === "https") {
+ if (query.includes("downgrade")) {
+ response.setStatusLine("1.1", 400, "Bad Request");
+ response.write("Bad Request\n");
+ return;
+ }
+ if (query.includes("upgrade")) {
+ response.write("view-source:https://");
+ return;
+ }
+ }
+
+ if (scheme === "http" && query.includes("downgrade")) {
+ response.write("view-source:http://");
+ return;
+ }
+
+ // We should arrive here when the redirection was downraded successful
+ response.write("unexpected scheme and query given");
+}
diff --git a/dom/security/test/https-first/file_downgrade_with_different_path.sjs b/dom/security/test/https-first/file_downgrade_with_different_path.sjs
new file mode 100644
index 0000000000..7450313d98
--- /dev/null
+++ b/dom/security/test/https-first/file_downgrade_with_different_path.sjs
@@ -0,0 +1,27 @@
+"use strict";
+
+// An onload postmessage to window opener
+const RESPONSE_HTTPS_SCHEME = `
+ <html>
+ <body>
+ <script type="application/javascript">
+ window.opener.postMessage({result: 'scheme-https'}, '*');
+ </script>
+ </body>
+ </html>`;
+
+const RESPONSE_HTTP_SCHEME = `
+ <html>
+ <body>
+ <script type="application/javascript">
+ window.opener.postMessage({result: 'scheme-http'}, '*');
+ </script>
+ </body>
+ </html>`;
+
+function handleRequest(request, response) {
+ let response_content =
+ request.scheme === "https" ? RESPONSE_HTTPS_SCHEME : RESPONSE_HTTP_SCHEME;
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(response_content);
+}
diff --git a/dom/security/test/https-first/file_download_attribute.html b/dom/security/test/https-first/file_download_attribute.html
new file mode 100644
index 0000000000..453bf408b3
--- /dev/null
+++ b/dom/security/test/https-first/file_download_attribute.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test download attribute for http site</title>
+</head>
+<body>
+ <a href="http://nocert.example.com/browser/dom/security/test/https-first/file_download_attribute.sjs" download="some.html" id="testlink">download by attribute</a>
+ <script>
+ // click the link to start download
+ let testlink = document.getElementById("testlink");
+ testlink.click();
+ </script>
+ </body>
+</html>
diff --git a/dom/security/test/https-first/file_download_attribute.sjs b/dom/security/test/https-first/file_download_attribute.sjs
new file mode 100644
index 0000000000..8941da1a41
--- /dev/null
+++ b/dom/security/test/https-first/file_download_attribute.sjs
@@ -0,0 +1,12 @@
+function handleRequest(request, response) {
+ // Only answer to http, in case request is in https let the reply time out.
+ if (request.scheme === "https") {
+ response.processAsync();
+ return;
+ }
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Disposition", "attachment; filename=some.html");
+ response.setHeader("Content-Type", "text/html");
+ response.write("success!");
+}
diff --git a/dom/security/test/https-first/file_form_submission.sjs b/dom/security/test/https-first/file_form_submission.sjs
new file mode 100644
index 0000000000..63b248d773
--- /dev/null
+++ b/dom/security/test/https-first/file_form_submission.sjs
@@ -0,0 +1,84 @@
+const CC = Components.Constructor;
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+const RESPONSE_SUCCESS = `
+ <html>
+ <body>
+ send message, downgraded
+ <script type="application/javascript">
+ let scheme = document.location.protocol;
+ const loc = document.location.href;
+ window.opener.postMessage({location: loc, scheme: scheme, form:"test=success" }, '*');
+ </script>
+ </body>
+ </html>`;
+
+const POST_FORMULAR = `
+<html>
+ <body>
+ <form action="http://example.com/tests/dom/security/test/https-first/file_form_submission.sjs?" method="POST" id="POSTForm">
+ <div>
+ <label id="submit">Submit</label>
+ <input name="test" id="form" value="success">
+ </div>
+ </form>
+ <script class="testbody" type="text/javascript">
+ document.getElementById("POSTForm").submit();
+ </script>
+ </body>
+</html>
+`;
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+ let queryString = request.queryString;
+ if (request.scheme === "https" && queryString === "test=1") {
+ response.write(RESPONSE_SUCCESS);
+ return;
+ }
+ if (
+ request.scheme === "https" &&
+ (queryString === "test=2" || queryString === "test=4")
+ ) {
+ // time out request
+ response.processAsync();
+ return;
+ }
+ if (request.scheme === "http" && queryString === "test=2") {
+ response.write(RESPONSE_SUCCESS);
+ return;
+ }
+ if (queryString === "test=3" || queryString === "test=4") {
+ // send post form
+ response.write(POST_FORMULAR);
+ return;
+ }
+ if (request.method == "POST") {
+ // extract form parameters
+ let body = new BinaryInputStream(request.bodyInputStream);
+ let avail;
+ let bytes = [];
+ while ((avail = body.available()) > 0) {
+ Array.prototype.push.apply(bytes, body.readByteArray(avail));
+ }
+ let requestBodyContents = String.fromCharCode.apply(null, bytes);
+
+ response.write(`
+ <html>
+ <script type="application/javascript">
+ let scheme = document.location.protocol;
+ const loc = document.location.href;
+ window.opener.postMessage({location: loc, scheme: scheme, form: '${requestBodyContents}'}, '*');
+ </script>
+ </html>`);
+
+ return;
+ }
+ // we should never get here; just in case, return something unexpected
+ response.write("do'h");
+}
diff --git a/dom/security/test/https-first/file_fragment.html b/dom/security/test/https-first/file_fragment.html
new file mode 100644
index 0000000000..5846d6d977
--- /dev/null
+++ b/dom/security/test/https-first/file_fragment.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<script>
+
+function beforeunload(){
+ window.opener.postMessage({
+ info: "before-unload",
+ result: window.location.hash,
+ button: false,
+ }, "*");
+}
+
+window.onload = function (){
+ let button = window.document.getElementById("clickMeButton");
+ let buttonExist = button !== null;
+ window.opener.postMessage({
+ info: "onload",
+ result: window.location.href,
+ button: buttonExist,
+ }, "*");
+ button.click();
+
+}
+
+// after button clicked and paged scrolled sends URL of current window
+window.onscroll = function(){
+ window.opener.postMessage({
+ info: "scrolled-to-foo",
+ result: window.location.href,
+ button: true,
+ documentURI: document.documentURI,
+ }, "*");
+ }
+
+
+</script>
+<body onbeforeunload="/*just to notify if we load a new page*/ beforeunload()";>
+ <a id="clickMeButton" href="http://example.com/tests/dom/security/test/https-first/file_fragment.html#foo">Click me</a>
+ <div style="height: 1000px; border: 1px solid black;"> space</div>
+ <a name="foo" href="http://example.com/tests/dom/security/test/https-first/file_fragment.html">foo</a>
+ <div style="height: 1000px; border: 1px solid black;">space</div>
+</body>
+</html>
diff --git a/dom/security/test/https-first/file_httpsfirst_speculative_connect.html b/dom/security/test/https-first/file_httpsfirst_speculative_connect.html
new file mode 100644
index 0000000000..6542884191
--- /dev/null
+++ b/dom/security/test/https-first/file_httpsfirst_speculative_connect.html
@@ -0,0 +1 @@
+<html><body>dummy file for speculative https-first upgrade test</body></html>
diff --git a/dom/security/test/https-first/file_httpsfirst_timeout_server.sjs b/dom/security/test/https-first/file_httpsfirst_timeout_server.sjs
new file mode 100644
index 0000000000..81c4c0328b
--- /dev/null
+++ b/dom/security/test/https-first/file_httpsfirst_timeout_server.sjs
@@ -0,0 +1,13 @@
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ if (request.scheme === "https") {
+ // Simulating a timeout by processing the https request
+ // async and *never* return anything!
+ response.processAsync();
+ return;
+ }
+ // we should never get here; just in case, return something unexpected
+ response.write("do'h");
+}
diff --git a/dom/security/test/https-first/file_mixed_content_auto_upgrade.html b/dom/security/test/https-first/file_mixed_content_auto_upgrade.html
new file mode 100644
index 0000000000..7dda8909a5
--- /dev/null
+++ b/dom/security/test/https-first/file_mixed_content_auto_upgrade.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1673574 - Improve Console logging for mixed content auto upgrading</title>
+</head>
+<body>
+ <!--upgradeable resources--->
+ <img src="http://example.com/browser/dom/security/test/https-first/pass.png">
+ <video src="http://example.com/browser/dom/security/test/https-first/test.ogv">
+ <audio src="http://example.com/browser/dom/security/test/https-first/test.wav">
+</body>
+</html>
diff --git a/dom/security/test/https-first/file_mixed_content_console.html b/dom/security/test/https-first/file_mixed_content_console.html
new file mode 100644
index 0000000000..631ac0b40f
--- /dev/null
+++ b/dom/security/test/https-first/file_mixed_content_console.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1713593: HTTPS-First: Add test for mixed content blocker.</title>
+</head>
+<body>
+ <!-- Test that image gets loaded (insecure) - the *.png does not actually exist because we only care for the console message to appear-->
+ <img type="image/png" id="testimage" src="http://example.com/browser/dom/security/test/https-first/auto_upgrading_identity.png" />
+ <!-- Test that the script gets blocked. The script does not actually exist because we only care for the console message to appear-->
+ <script type="text/javascript" src="http://example.com/browser/dom/security/test/https-first/barfoo" >
+</script>
+</body>
+</html>
diff --git a/dom/security/test/https-first/file_multiple_redirection.sjs b/dom/security/test/https-first/file_multiple_redirection.sjs
new file mode 100644
index 0000000000..49098ccdb7
--- /dev/null
+++ b/dom/security/test/https-first/file_multiple_redirection.sjs
@@ -0,0 +1,87 @@
+"use strict";
+
+// redirection uri
+const REDIRECT_URI =
+ "https://example.com/tests/dom/security/test/https-first/file_multiple_redirection.sjs?redirect";
+const REDIRECT_URI_HTTP =
+ "http://example.com/tests/dom/security/test/https-first/file_multiple_redirection.sjs?verify";
+const REDIRECT_URI_HTTPS =
+ "https://example.com/tests/dom/security/test/https-first/file_multiple_redirection.sjs?verify";
+
+const RESPONSE_ERROR = "unexpected-query";
+
+// An onload postmessage to window opener
+const RESPONSE_HTTPS_SCHEME = `
+ <html>
+ <body>
+ <script type="application/javascript">
+ window.opener.postMessage({result: 'scheme-https'}, '*');
+ </script>
+ </body>
+ </html>`;
+
+const RESPONSE_HTTP_SCHEME = `
+ <html>
+ <body>
+ <script type="application/javascript">
+ window.opener.postMessage({result: 'scheme-http'}, '*');
+ </script>
+ </body>
+ </html>`;
+
+function sendRedirection(query, response) {
+ // send a redirection to an http uri
+ if (query.includes("test1")) {
+ response.setHeader("Location", REDIRECT_URI_HTTP, false);
+ return;
+ }
+ // send a redirection to an https uri
+ if (query.includes("test2")) {
+ response.setHeader("Location", REDIRECT_URI_HTTPS, false);
+ return;
+ }
+ // send a redirection to an http uri with hsts header
+ if (query.includes("test3")) {
+ response.setHeader("Strict-Transport-Security", "max-age=60");
+ response.setHeader("Location", REDIRECT_URI_HTTP, false);
+ }
+}
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+ const query = request.queryString;
+
+ // if the query contains a test query start first test
+ if (query.startsWith("test")) {
+ // send a 302 redirection
+ response.setStatusLine(request.httpVersion, 302, "Found");
+ response.setHeader("Location", REDIRECT_URI + query, false);
+ return;
+ }
+ // Send a redirection
+ if (query.includes("redirect")) {
+ response.setStatusLine(request.httpVersion, 302, "Found");
+ sendRedirection(query, response);
+ return;
+ }
+ // Reset the HSTS policy, prevent influencing other tests
+ if (request.queryString === "reset") {
+ response.setHeader("Strict-Transport-Security", "max-age=0");
+ let response_content =
+ request.scheme === "https" ? RESPONSE_HTTPS_SCHEME : RESPONSE_HTTP_SCHEME;
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(response_content);
+ }
+ // Check if scheme is http:// or https://
+ if (query == "verify") {
+ let response_content =
+ request.scheme === "https" ? RESPONSE_HTTPS_SCHEME : RESPONSE_HTTP_SCHEME;
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(response_content);
+ return;
+ }
+
+ // We should never get here, but just in case ...
+ response.setStatusLine(request.httpVersion, 500, "OK");
+ response.write("unexepcted query");
+}
diff --git a/dom/security/test/https-first/file_navigation.html b/dom/security/test/https-first/file_navigation.html
new file mode 100644
index 0000000000..02d366291b
--- /dev/null
+++ b/dom/security/test/https-first/file_navigation.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <p>Blank page</p>
+ <body>
+</html>
diff --git a/dom/security/test/https-first/file_redirect.sjs b/dom/security/test/https-first/file_redirect.sjs
new file mode 100644
index 0000000000..2042bcbc88
--- /dev/null
+++ b/dom/security/test/https-first/file_redirect.sjs
@@ -0,0 +1,58 @@
+//https://bugzilla.mozilla.org/show_bug.cgi?id=1706351
+
+// Step 1. Send request with redirect queryString (eg. file_redirect.sjs?302)
+// Step 2. Server responds with corresponding redirect code to http://example.com/../file_redirect.sjs?check
+// Step 3. Response from ?check indicates whether the redirected request was secure or not.
+
+const RESPONSE_ERROR = "unexpected-query";
+
+// An onload postmessage to window opener
+const RESPONSE_SECURE = `
+ <html>
+ <body>
+ send onload message...
+ <script type="application/javascript">
+ window.opener.postMessage({result: 'secure'}, '*');
+ </script>
+ </body>
+ </html>`;
+
+const RESPONSE_INSECURE = `
+ <html>
+ <body>
+ send onload message...
+ <script type="application/javascript">
+ window.opener.postMessage({result: 'insecure'}, '*');
+ </script>
+ </body>
+ </html>`;
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ const query = request.queryString;
+
+ // Send redirect header
+ if ((query >= 301 && query <= 303) || query == 307) {
+ // needs to be a cross site redirect to http://example.com otherwise
+ // our upgrade downgrade endless loop break mechanism kicks in
+ const loc =
+ "http://test1.example.com/tests/dom/security/test/https-first/file_redirect.sjs?check";
+ response.setStatusLine(request.httpVersion, query, "Found");
+ response.setHeader("Location", loc, false);
+ return;
+ }
+
+ // Check if scheme is http:// or https://
+ if (query == "check") {
+ const secure =
+ request.scheme == "https" ? RESPONSE_SECURE : RESPONSE_INSECURE;
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(secure);
+ return;
+ }
+
+ // This should not happen
+ response.setStatusLine(request.httpVersion, 500, "OK");
+ response.write(RESPONSE_ERROR);
+}
diff --git a/dom/security/test/https-first/file_redirect_downgrade.sjs b/dom/security/test/https-first/file_redirect_downgrade.sjs
new file mode 100644
index 0000000000..a31c8cb99b
--- /dev/null
+++ b/dom/security/test/https-first/file_redirect_downgrade.sjs
@@ -0,0 +1,87 @@
+// Custom *.sjs file specifically for the needs of Bug 1707856
+"use strict";
+
+const REDIRECT_META = `
+ <html>
+ <head>
+ <meta http-equiv="refresh" content="0; url='http://example.com/tests/dom/security/test/https-first/file_redirect_downgrade.sjs?testEnd'">
+ </head>
+ <body>
+ META REDIRECT
+ </body>
+ </html>`;
+
+const REDIRECT_JS = `
+ <html>
+ <body>
+ JS REDIRECT
+ <script>
+ let url= "http://example.com/tests/dom/security/test/https-first/file_redirect_downgrade.sjs?testEnd";
+ window.location = url;
+ </script>
+ </body>
+ </html>`;
+
+const REDIRECT_302 =
+ "http://example.com/tests/dom/security/test/https-first/file_redirect_downgrade.sjs?testEnd";
+
+const RESPONSE_SUCCESS = `
+ <html>
+ <body>
+ send message, downgraded
+ <script type="application/javascript">
+ let scheme = document.location.protocol;
+ window.opener.postMessage({result: 'downgraded', scheme: scheme}, '*');
+ </script>
+ </body>
+ </html>`;
+
+const RESPONSE_UNEXPECTED = `
+ <html>
+ <body>
+ send message, error
+ <script type="application/javascript">
+ let scheme = document.location.protocol;
+ window.opener.postMessage({result: 'error', scheme: scheme}, '*');
+ </script>
+ </body>
+ </html>`;
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviour
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+
+ let query = request.queryString;
+ // if the scheme is not https and it is the initial request
+ // then we rather fall through and display unexpected content
+ if (request.scheme === "https") {
+ if (query === "test1a") {
+ response.write(REDIRECT_META);
+ return;
+ }
+
+ if (query === "test2a") {
+ response.write(REDIRECT_JS);
+ return;
+ }
+
+ if (query === "test3a") {
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", REDIRECT_302, false);
+ return;
+ }
+
+ // Simulating a timeout by processing the https request
+ response.processAsync();
+ return;
+ }
+
+ // We should arrive here when the redirection was downraded successful
+ if (query == "testEnd") {
+ response.write(RESPONSE_SUCCESS);
+ return;
+ }
+ // We should never arrive here, just in case send 'error'
+ response.write(RESPONSE_UNEXPECTED);
+}
diff --git a/dom/security/test/https-first/file_referrer_policy.sjs b/dom/security/test/https-first/file_referrer_policy.sjs
new file mode 100644
index 0000000000..ea2d8fb04b
--- /dev/null
+++ b/dom/security/test/https-first/file_referrer_policy.sjs
@@ -0,0 +1,102 @@
+const RESPONSE_ERROR = `
+ <html>
+ <body>
+ Error occurred...
+ <script type="application/javascript">
+ window.opener.postMessage({result: 'ERROR'}, '*');
+ </script>
+ </body>
+ </html>`;
+const RESPONSE_POLICY = `
+<html>
+<body>
+Send policy onload...
+<script type="application/javascript">
+ const loc = document.location.href;
+ window.opener.postMessage({result: document.referrer, location: loc}, "*");
+</script>
+</body>
+</html>`;
+
+const expectedQueries = [
+ "no-referrer",
+ "no-referrer-when-downgrade",
+ "origin",
+ "origin-when-cross-origin",
+ "same-origin",
+ "strict-origin",
+ "strict-origin-when-cross-origin",
+ "unsafe-url",
+];
+function readQuery(testCase) {
+ let twoValues = testCase.split("-");
+ let upgradeRequest = twoValues[0] === "https" ? 1 : 0;
+ let httpsResponse = twoValues[1] === "https" ? 1 : 0;
+ return [upgradeRequest, httpsResponse];
+}
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ let query = new URLSearchParams(request.queryString);
+ // Downgrade to test http/https -> HTTP referrer policy
+ if (query.has("sendMe2") && request.scheme === "https") {
+ // Simulating a timeout by processing the https request
+ response.processAsync();
+ return;
+ }
+ if (query.has("sendMe") || query.has("sendMe2")) {
+ response.write(RESPONSE_POLICY);
+ return;
+ }
+ // Get the referrer policy that we want to set
+ let referrerPolicy = query.get("rp");
+ //If the query contained one of the expected referrer policies send a request with the given policy,
+ // else send error
+ if (expectedQueries.includes(referrerPolicy)) {
+ // Determine the test case, e.g. don't upgrade request but send response in https
+ let testCase = readQuery(query.get("upgrade"));
+ let httpsRequest = testCase[0];
+ let httpsResponse = testCase[1];
+ // Downgrade to http if upgrade equals 0
+ if (httpsRequest === 0 && request.scheme === "https") {
+ // Simulating a timeout by processing the https request
+ response.processAsync();
+ return;
+ }
+ // create js redirection that request with the given (related to the query) referrer policy
+ const SEND_REQUEST_HTTPS = `
+ <html>
+ <head>
+ <meta name="referrer" content=${referrerPolicy}>
+ </head>
+ <body>
+ JS REDIRECT
+ <script>
+ let url = 'https://example.com/tests/dom/security/test/https-first/file_referrer_policy.sjs?sendMe';
+ window.location = url;
+ </script>
+ </body>
+ </html>`;
+ const SEND_REQUEST_HTTP = `
+ <html>
+ <head>
+ <meta name="referrer" content=${referrerPolicy}>
+ </head>
+ <body>
+ JS REDIRECT
+ <script>
+ let url = 'http://example.com/tests/dom/security/test/https-first/file_referrer_policy.sjs?sendMe2';
+ window.location = url;
+ </script>
+ </body>
+ </html>`;
+ let respond = httpsResponse === 1 ? SEND_REQUEST_HTTPS : SEND_REQUEST_HTTP;
+ response.write(respond);
+ return;
+ }
+
+ // We should never get here but in case we send an error
+ response.setStatusLine(request.httpVersion, 500, "OK");
+ response.write(RESPONSE_ERROR);
+}
diff --git a/dom/security/test/https-first/file_slow_download.html b/dom/security/test/https-first/file_slow_download.html
new file mode 100644
index 0000000000..084977607d
--- /dev/null
+++ b/dom/security/test/https-first/file_slow_download.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test slow download from an http site that gets upgraded to https</title>
+</head>
+<body>
+ <a href="http://example.com/browser/dom/security/test/https-first/file_slow_download.sjs" download="large-dummy-file.txt" id="testlink">download by attribute</a>
+ <script>
+ // click the link to start download
+ let testlink = document.getElementById("testlink");
+ testlink.click();
+ </script>
+ </body>
+</html>
diff --git a/dom/security/test/https-first/file_slow_download.sjs b/dom/security/test/https-first/file_slow_download.sjs
new file mode 100644
index 0000000000..6e4f109068
--- /dev/null
+++ b/dom/security/test/https-first/file_slow_download.sjs
@@ -0,0 +1,26 @@
+"use strict";
+let timer;
+
+// Send a part of the file then wait for 3 second before sending the rest.
+// If download isn't exempt from background timer of https-only/-first then the download
+// gets cancelled before it completed.
+const DELAY_MS = 3500;
+function handleRequest(request, response) {
+ response.processAsync();
+ timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader(
+ "Content-Disposition",
+ "attachment; filename=large-dummy-file.txt"
+ );
+ response.setHeader("Content-Type", "text/plain");
+ response.write("Begin the file");
+ timer.init(
+ () => {
+ response.write("End of file");
+ response.finish();
+ },
+ DELAY_MS,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+}
diff --git a/dom/security/test/https-first/file_toplevel_cookies.sjs b/dom/security/test/https-first/file_toplevel_cookies.sjs
new file mode 100644
index 0000000000..dd9f7c0909
--- /dev/null
+++ b/dom/security/test/https-first/file_toplevel_cookies.sjs
@@ -0,0 +1,233 @@
+// Custom *.sjs file specifically for the needs of Bug 1711453
+"use strict";
+
+// small red image
+const IMG_BYTES = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
+ "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="
+);
+
+const IFRAME_INC = `<iframe id="testframeinc"></iframe>`;
+
+// Sets an image sends cookie and location after loading
+const SET_COOKIE_IMG = `
+<html>
+<body>
+<img id="cookieImage">
+<script class="testbody" type="text/javascript">
+ var cookieImage = document.getElementById("cookieImage");
+ cookieImage.onload = function() {
+ let myLocation = window.location.href;
+ let myCookie = document.cookie;
+ window.opener.postMessage({result: 'upgraded', loc: myLocation, cookie: myCookie}, '*');
+ }
+ cookieImage.onerror = function() {
+ window.opener.postMessage({result: 'error'}, '*');
+ }
+ // Add the last number of the old query to the new query to set cookie properly
+ cookieImage.src = window.location.origin + "/tests/dom/security/test/https-first/file_toplevel_cookies.sjs?setSameSiteCookie"
+ + window.location.href.charAt(window.location.href.length -1);
+</script>
+</body>
+</html>
+`;
+
+// Load blank frame navigation sends cookie and location after loading
+const LOAD_BLANK_FRAME_NAV = `
+<html>
+<body>
+<iframe id="testframe"></iframe>
+<script>
+ let testframe = document.getElementById("testframe");
+ testframe.onload = function() {
+ let myLocation = window.location.href;
+ let myCookie = document.cookie;
+ window.opener.postMessage({result: 'upgraded', loc: myLocation, cookie: myCookie}, '*');
+ }
+ testframe.onerror = function() {
+ window.opener.postMessage({result: 'error', loc: 'error', cookie: ''}, '*');
+ }
+ testframe.src = window.location.origin + "/tests/dom/security/test/https-first/file_toplevel_cookies.sjs?loadblankframeNav";
+</script>
+</body>
+</html>
+`;
+
+// Load frame navigation sends cookie and location after loading
+const LOAD_FRAME_NAV = `
+<html>
+<body>
+<iframe id="testframe"></iframe>
+<script>
+ let testframe = document.getElementById("testframe");
+ testframe.onload = function() {
+ let myLocation = window.location.href;
+ let myCookie = document.cookie;
+ window.opener.postMessage({result: 'upgraded', loc: myLocation, cookie: myCookie}, '*');
+ }
+ testframe.onerror = function() {
+ window.opener.postMessage({result: 'error', loc: 'error', cookie: ''}, '*');
+ }
+ testframe.src = window.location.origin + "/tests/dom/security/test/https-first/file_toplevel_cookies.sjs?loadsrcdocframeNav";
+</script>
+</body>
+</html>
+
+`;
+// blank frame sends cookie and location after loading
+const LOAD_BLANK_FRAME = `
+<html>
+<body>
+<iframe id="testframe"></iframe>
+<script>
+ let testframe = document.getElementById("testframe");
+ testframe.onload = function() {
+ let myLocation = window.location.href;
+ let myCookie = document.cookie;
+ window.opener.postMessage({result: 'upgraded', loc: myLocation, cookie: myCookie}, '*');
+ }
+ testframe.onerror = function() {
+ window.opener.postMessage({result: 'error', loc: 'error', cookie: ''}, '*');
+ }
+ testframe.src = window.location.origin + "/tests/dom/security/test/https-first/file_toplevel_cookies.sjs?loadblankframeInc";
+</script>
+</body>
+</html>
+`;
+// frame sends cookie and location after loading
+const LOAD_FRAME = `
+<html>
+<body>
+<iframe id="testframe"></iframe>
+<script>
+ let testframe = document.getElementById("testframe");
+ testframe.onload = function() {
+ let myLocation = window.location.href;
+ let myCookie = document.cookie;
+ window.opener.postMessage({result: 'upgraded', loc: myLocation, cookie: myCookie}, '*');
+ }
+ testframe.onerror = function() {
+ window.opener.postMessage({result: 'error', loc: 'error', cookie: ''}, '*');
+ }
+ testframe.src = window.location.origin + "/tests/dom/security/test/https-first/file_toplevel_cookies.sjs?loadsrcdocframeInc";
+</script>
+</body>
+</html>
+`;
+
+const RESPONSE_UNEXPECTED = `
+ <html>
+ <body>
+ send message, error
+ <script type="application/javascript">
+ let myLocation = document.location.href;
+ window.opener.postMessage({result: 'error', loc: myLocation}, '*');
+ </script>
+ </body>
+ </html>`;
+
+function setCookie(name, query) {
+ let cookie = name + "=";
+ if (query.includes("0")) {
+ cookie += "0;Domain=.example.com;sameSite=none";
+ return cookie;
+ }
+ if (query.includes("1")) {
+ cookie += "1;Domain=.example.com;sameSite=strict";
+ return cookie;
+ }
+ if (query.includes("2")) {
+ cookie += "2;Domain=.example.com;sameSite=none;secure";
+ return cookie;
+ }
+ if (query.includes("3")) {
+ cookie += "3;Domain=.example.com;sameSite=strict;secure";
+ return cookie;
+ }
+ return cookie + "error";
+}
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+ let query = request.queryString;
+ if (query.includes("setImage")) {
+ response.write(SET_COOKIE_IMG);
+ return;
+ }
+ // using startsWith and discard the math random
+ if (query.includes("setSameSiteCookie")) {
+ response.setHeader("Set-Cookie", setCookie("setImage", query), true);
+ response.setHeader("Content-Type", "image/png");
+ response.write(IMG_BYTES);
+ return;
+ }
+
+ // navigation tests
+ if (query.includes("loadNavBlank")) {
+ response.setHeader("Set-Cookie", setCookie("loadNavBlank", query), true);
+ response.write(LOAD_BLANK_FRAME_NAV);
+ return;
+ }
+
+ if (request.queryString === "loadblankframeNav") {
+ let FRAME = `
+ <iframe src="about:blank"
+ // nothing happens here
+ </iframe>`;
+ response.write(FRAME);
+ return;
+ }
+
+ if (query.includes("loadNav")) {
+ response.setHeader("Set-Cookie", setCookie("loadNav", query), true);
+ response.write(LOAD_FRAME_NAV);
+ return;
+ }
+
+ if (query === "loadsrcdocframeNav") {
+ let FRAME = `
+ <iframe srcdoc="foo"
+ // nothing happens here
+ </iframe>`;
+ response.write(FRAME);
+ return;
+ }
+
+ // inclusion tests
+ if (query.includes("loadframeIncBlank")) {
+ response.setHeader(
+ "Set-Cookie",
+ setCookie("loadframeIncBlank", query),
+ true
+ );
+ response.write(LOAD_BLANK_FRAME);
+ return;
+ }
+
+ if (request.queryString === "loadblankframeInc") {
+ let FRAME =
+ ` <iframe id="blankframe" src="about:blank"></iframe>
+ <script>
+ document.getElementById("blankframe").contentDocument.write("` +
+ IFRAME_INC +
+ `");
+ <\script>`;
+ response.write(FRAME);
+ return;
+ }
+
+ if (query.includes("loadframeInc")) {
+ response.setHeader("Set-Cookie", setCookie("loadframeInc", query), true);
+ response.write(LOAD_FRAME);
+ return;
+ }
+
+ if (request.queryString === "loadsrcdocframeInc") {
+ response.write('<iframe srcdoc="' + IFRAME_INC + '"></iframe>');
+ return;
+ }
+
+ // We should never arrive here, just in case send 'error'
+ response.write(RESPONSE_UNEXPECTED);
+}
diff --git a/dom/security/test/https-first/file_upgrade_insecure.html b/dom/security/test/https-first/file_upgrade_insecure.html
new file mode 100644
index 0000000000..af306d2a16
--- /dev/null
+++ b/dom/security/test/https-first/file_upgrade_insecure.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1704454 - HTTPS FIRST Mode</title>
+ <!-- style -->
+ <link rel='stylesheet' type='text/css' href='http://example.com/tests/dom/security/test/https-first/file_upgrade_insecure_server.sjs?style' media='screen' />
+
+ <!-- font -->
+ <style>
+ @font-face {
+ font-family: "foofont";
+ src: url('http://example.com/tests/dom/security/test/https-first/file_upgrade_insecure_server.sjs?font');
+ }
+ .div_foo { font-family: "foofont"; }
+ </style>
+</head>
+<body>
+
+ <!-- images: -->
+ <img src="http://example.com/tests/dom/security/test/https-first/file_upgrade_insecure_server.sjs?img"></img>
+
+ <!-- redirects: upgrade http:// to https:// redirect to http:// and then upgrade to https:// again -->
+ <img src="http://example.com/tests/dom/security/test/https-first/file_upgrade_insecure_server.sjs?redirect-image"></img>
+
+ <!-- script: -->
+ <script src="http://example.com/tests/dom/security/test/https-first/file_upgrade_insecure_server.sjs?script"></script>
+
+ <!-- media: -->
+ <audio src="http://example.com/tests/dom/security/test/https-first/file_upgrade_insecure_server.sjs?media"></audio>
+
+ <!-- objects: -->
+ <object width="10" height="10" data="http://example.com/tests/dom/security/test/https-first/file_upgrade_insecure_server.sjs?object"></object>
+
+ <!-- font: (apply font loaded in header to div) -->
+ <div class="div_foo">foo</div>
+
+ <!-- iframe: (same origin) -->
+ <iframe src="http://example.com/tests/dom/security/test/https-first/file_upgrade_insecure_server.sjs?iframe">
+ <!-- within that iframe we load an image over http and make sure the requested gets upgraded to https -->
+ </iframe>
+
+ <!-- toplevel: -->
+ <script type="application/javascript">
+ let myWin = window.open("http://example.com/tests/dom/security/test/https-first/file_upgrade_insecure_server.sjs?top-level");
+ //close right after opening
+ myWin.onunload = function(){
+ myWin.close();
+ }
+ </script>
+
+ <!-- xhr: -->
+ <script type="application/javascript">
+ var myXHR = new XMLHttpRequest();
+ myXHR.open("GET", "http://example.com/tests/dom/security/test/https-first/file_upgrade_insecure_server.sjs?xhr");
+ myXHR.send(null);
+ </script>
+
+
+ <!-- form action: (upgrade POST from http:// to https://) -->
+ <iframe name='formFrame' id='formFrame'></iframe>
+ <form target="formFrame" action="http://example.com/tests/dom/security/test/https-first/file_upgrade_insecure_server.sjs?form" method="POST">
+ <input name="foo" value="foo">
+ <input type="submit" id="submitButton" formenctype='multipart/form-data' value="Submit form">
+ </form>
+ <script type="text/javascript">
+ var submitButton = document.getElementById('submitButton');
+ submitButton.click();
+ </script>
+
+</body>
+</html>
diff --git a/dom/security/test/https-first/file_upgrade_insecure_server.sjs b/dom/security/test/https-first/file_upgrade_insecure_server.sjs
new file mode 100644
index 0000000000..a8f4d66659
--- /dev/null
+++ b/dom/security/test/https-first/file_upgrade_insecure_server.sjs
@@ -0,0 +1,114 @@
+// SJS file for https-first Mode mochitests
+// Bug 1704454 - HTTPS First Mode
+
+const TOTAL_EXPECTED_REQUESTS = 12;
+
+const IFRAME_CONTENT =
+ "<!DOCTYPE HTML>" +
+ "<html>" +
+ "<head><meta charset='utf-8'>" +
+ "<title>Bug 1704454 - Test HTTPS First Mode</title>" +
+ "</head>" +
+ "<body>" +
+ "<img src='http://example.com/tests/dom/security/test/https-first/file_upgrade_insecure_server.sjs?nested-img'></img>" +
+ "</body>" +
+ "</html>";
+
+const expectedQueries = [
+ "script",
+ "style",
+ "img",
+ "iframe",
+ "form",
+ "xhr",
+ "media",
+ "object",
+ "font",
+ "img-redir",
+ "nested-img",
+ "top-level",
+];
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+ var queryString = request.queryString;
+
+ // initialize server variables and save the object state
+ // of the initial request, which returns async once the
+ // server has processed all requests.
+ if (queryString == "queryresult") {
+ setState("totaltests", TOTAL_EXPECTED_REQUESTS.toString());
+ setState("receivedQueries", "");
+ response.processAsync();
+ setObjectState("queryResult", response);
+ return;
+ }
+
+ // handle img redirect (https->http)
+ if (queryString == "redirect-image") {
+ var newLocation =
+ "http://example.com/tests/dom/security/test/https-first/file_upgrade_insecure_server.sjs?img-redir";
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", newLocation, false);
+ return;
+ }
+
+ // just in case error handling for unexpected queries
+ if (!expectedQueries.includes(queryString)) {
+ response.write("unexpected-response");
+ return;
+ }
+
+ // make sure all the requested queries aren't upgraded to https
+ // except of toplevel requests
+ if (queryString === "top-level") {
+ queryString += request.scheme === "https" ? "-ok" : "-error";
+ } else {
+ queryString += request.scheme === "http" ? "-ok" : "-error";
+ }
+ var receivedQueries = getState("receivedQueries");
+
+ // images, scripts, etc. get queried twice, do not
+ // confuse the server by storing the preload as
+ // well as the actual load. If either the preload
+ // or the actual load is not https, then we would
+ // append "-error" in the array and the test would
+ // fail at the end.
+
+ // append the result to the total query string array
+ if (receivedQueries != "") {
+ receivedQueries += ",";
+ }
+ receivedQueries += queryString;
+ setState("receivedQueries", receivedQueries);
+
+ // keep track of how many more requests the server
+ // is expecting
+ var totaltests = parseInt(getState("totaltests"));
+ totaltests -= 1;
+ setState("totaltests", totaltests.toString());
+
+ // return content (img) for the nested iframe to test
+ // that subresource requests within nested contexts
+ // get upgraded as well. We also have to return
+ // the iframe context in case of an error so we
+ // can test both, using upgrade-insecure as well
+ // as the base case of not using upgrade-insecure.
+ if (queryString == "iframe-ok" || queryString == "iframe-error") {
+ response.write(IFRAME_CONTENT);
+ }
+
+ // if we have received all the requests, we return
+ // the result back.
+ if (totaltests == 0) {
+ getObjectState("queryResult", function (queryResponse) {
+ if (!queryResponse) {
+ return;
+ }
+ var receivedQueries = getState("receivedQueries");
+ queryResponse.write(receivedQueries);
+ queryResponse.finish();
+ });
+ }
+}
diff --git a/dom/security/test/https-first/mochitest.toml b/dom/security/test/https-first/mochitest.toml
new file mode 100644
index 0000000000..a6c7364ae6
--- /dev/null
+++ b/dom/security/test/https-first/mochitest.toml
@@ -0,0 +1,56 @@
+[DEFAULT]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bad_cert.html"]
+support-files = ["file_bad_cert.sjs"]
+
+["test_break_endless_upgrade_downgrade_loop.html"]
+support-files = [
+ "file_break_endless_upgrade_downgrade_loop.sjs",
+ "file_downgrade_with_different_path.sjs",
+]
+
+["test_data_uri.html"]
+support-files = ["file_data_uri.html"]
+
+["test_downgrade_500_responses.html"]
+support-files = ["file_downgrade_500_responses.sjs"]
+
+["test_downgrade_bad_responses.html"]
+support-files = ["file_downgrade_bad_responses.sjs"]
+
+["test_downgrade_request_upgrade_request.html"]
+support-files = ["file_downgrade_request_upgrade_request.sjs"]
+
+["test_form_submission.html"]
+support-files = ["file_form_submission.sjs"]
+
+["test_fragment.html"]
+support-files = ["file_fragment.html"]
+
+["test_multiple_redirection.html"]
+support-files = ["file_multiple_redirection.sjs"]
+
+["test_redirect_downgrade.html"]
+support-files = ["file_redirect_downgrade.sjs"]
+
+["test_redirect_upgrade.html"]
+scheme = "https"
+support-files = ["file_redirect.sjs"]
+
+["test_referrer_policy.html"]
+support-files = ["file_referrer_policy.sjs"]
+
+["test_resource_upgrade.html"]
+scheme = "https"
+support-files = [
+ "file_upgrade_insecure.html",
+ "file_upgrade_insecure_server.sjs",
+]
+skip-if = ["true"] # Bug 1727101, Bug 1727925
+
+["test_toplevel_cookies.html"]
+support-files = ["file_toplevel_cookies.sjs"]
diff --git a/dom/security/test/https-first/pass.png b/dom/security/test/https-first/pass.png
new file mode 100644
index 0000000000..2fa1e0ac06
--- /dev/null
+++ b/dom/security/test/https-first/pass.png
Binary files differ
diff --git a/dom/security/test/https-first/test.ogv b/dom/security/test/https-first/test.ogv
new file mode 100644
index 0000000000..0f83996e5d
--- /dev/null
+++ b/dom/security/test/https-first/test.ogv
Binary files differ
diff --git a/dom/security/test/https-first/test.wav b/dom/security/test/https-first/test.wav
new file mode 100644
index 0000000000..85dc1ea904
--- /dev/null
+++ b/dom/security/test/https-first/test.wav
Binary files differ
diff --git a/dom/security/test/https-first/test_bad_cert.html b/dom/security/test/https-first/test_bad_cert.html
new file mode 100644
index 0000000000..d7e9296d97
--- /dev/null
+++ b/dom/security/test/https-first/test_bad_cert.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1719309
+Test that bad cert sites won't get upgraded by https-first
+-->
+
+<head>
+ <title>HTTPS-FirstMode - Bad Certificates</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+ <h1>HTTPS-First Mode</h1>
+ <p>Test: Downgrade bad certificates without warning page </p>
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1706351">Bug 1719309</a>
+
+ <script class="testbody" type="text/javascript">
+ "use strict";
+ /*
+ * We perform the following tests:
+ * 1. Request nocert.example.com which is a site without a certificate
+ * 2. Request a site with self-signed cert (self-signed.example.com)
+ * 3. Request a site with an untrusted cert (untrusted.example.com)
+ * 4. Request a site with an expired cert
+ * 5. Request a site with an untrusted and expired cert
+ * 6. Request a site with no subject alternative dns name matching
+ *
+ * Expected result: Https-first tries to upgrade each request. Receives for each one an SSL_ERROR_*
+ * and downgrades back to http.
+ */
+ const badCertificates = ["nocert","self-signed", "untrusted","expired","untrusted-expired", "no-subject-alt-name"];
+ let currentTest = 0;
+ let testWin;
+ window.addEventListener("message", receiveMessage);
+
+ // Receive message and verify that it is from an http site.
+ // Verify that we got the correct message and an http scheme
+ async function receiveMessage(event) {
+ let data = event.data;
+ let currentBadCert = badCertificates[currentTest];
+ ok(data.result === "downgraded", "Downgraded request " + currentBadCert);
+ ok(data.scheme === "http:", "Received 'http' for " + currentBadCert);
+ testWin.close();
+ if (++currentTest < badCertificates.length) {
+ startTest();
+ return;
+ }
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+ }
+
+ async function startTest() {
+ const currentCode = badCertificates[currentTest];
+ // make a request to a subdomain of example.com with a bad certificate
+ testWin = window.open(`http://${currentCode}.example.com/tests/dom/security/test/https-first/file_bad_cert.sjs`);
+ }
+
+ // Set preference and start test
+ SpecialPowers.pushPrefEnv({ set: [
+ ["dom.security.https_first", true],
+ ]}, startTest);
+ SimpleTest.waitForExplicitFinish();
+ </script>
+</body>
+</html>
diff --git a/dom/security/test/https-first/test_break_endless_upgrade_downgrade_loop.html b/dom/security/test/https-first/test_break_endless_upgrade_downgrade_loop.html
new file mode 100644
index 0000000000..7d239350a1
--- /dev/null
+++ b/dom/security/test/https-first/test_break_endless_upgrade_downgrade_loop.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1715253
+Test that same origin redirect does not cause endless loop with https-first enabled
+-->
+
+<head>
+ <title>HTTPS-First-Mode - Break endless upgrade downgrade redirect loop</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+ <h1>HTTPS-First Mode</h1>
+ <p>Upgrade Test for insecure redirects.</p>
+
+ <script class="testbody" type="text/javascript">
+ "use strict";
+
+ SimpleTest.waitForExplicitFinish();
+
+ const redirectCodes = ["301", "302","303","307"];
+ let currentTest = 0;
+ let testWin;
+ window.addEventListener("message", receiveMessage);
+
+ // receive message from loaded site verifying the scheme of
+ // the loaded document.
+ async function receiveMessage(event) {
+ let currentRedirectCode = redirectCodes[currentTest];
+ is(event.data.result,
+ "scheme-http",
+ "same-origin redirect results in 'http' for " + currentRedirectCode
+ );
+ testWin.close();
+ if (++currentTest < redirectCodes.length) {
+ startTest();
+ return;
+ }
+ window.removeEventListener("message", receiveMessage);
+ window.addEventListener("message", receiveMessageForDifferentPathTest);
+ testDifferentPath();
+ }
+
+ async function receiveMessageForDifferentPathTest(event) {
+ is(event.data.result,
+ "scheme-https",
+ "scheme should be https when the path is different"
+ );
+ testWin.close();
+ window.removeEventListener("message", receiveMessageForDifferentPathTest);
+ SimpleTest.finish();
+ }
+
+ async function startTest() {
+ const currentCode = redirectCodes[currentTest];
+ // Load an http:// window which gets upgraded to https://
+ let uri =
+ `http://example.com/tests/dom/security/test/https-first/file_break_endless_upgrade_downgrade_loop.sjs?${currentCode}`;
+ testWin = window.open(uri);
+ }
+
+ async function testDifferentPath() {
+ // Load an https:// window which gets downgraded to http://
+ let uri =
+ `https://example.com/tests/dom/security/test/https-first/file_break_endless_upgrade_downgrade_loop.sjs?downgrade`;
+ testWin = window.open(uri);
+ }
+
+ // Set preference and start test
+ SpecialPowers.pushPrefEnv({ set: [
+ ["dom.security.https_first", true],
+ ["security.mixed_content.block_active_content", false],
+ ["security.mixed_content.block_display_content", false],
+ ["dom.security.https_only_check_path_upgrade_downgrade_endless_loop", true],
+ ]}, startTest);
+ </script>
+</body>
+</html>
diff --git a/dom/security/test/https-first/test_data_uri.html b/dom/security/test/https-first/test_data_uri.html
new file mode 100644
index 0000000000..b9891260db
--- /dev/null
+++ b/dom/security/test/https-first/test_data_uri.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1709069: Test that Data URI which makes a top-level request gets updated in https-first</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+window.addEventListener("message", receiveMessage);
+
+// HTML site which makes a top-level http request
+const HTML = `
+<html>
+<body>
+ DATA HTML
+<script>
+ window.open("http://example.com/tests/dom/security/test/https-first/file_data_uri.html");
+<\/script>
+<\/body>
+<\/html>
+`;
+
+const DATA_HTML = "data:text/html, " + HTML;
+
+// Verify that data uri top-level request got upgraded to https and
+// the reached location is correct
+async function receiveMessage(event){
+ let data = event.data;
+ is(data.location, "https://example.com/tests/dom/security/test/https-first/file_data_uri.html",
+ "Reached the correct location");
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+function test_toplevel_https() {
+ document.getElementById("testframe").src = DATA_HTML;
+}
+
+SpecialPowers.pushPrefEnv({ set: [
+ ["dom.security.https_first", true],
+ ]}, test_toplevel_https);
+
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/https-first/test_downgrade_500_responses.html b/dom/security/test/https-first/test_downgrade_500_responses.html
new file mode 100644
index 0000000000..3943c9095c
--- /dev/null
+++ b/dom/security/test/https-first/test_downgrade_500_responses.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Bug 1747673 : HTTPS First fallback to http for non-standard 5xx status code responses</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+/*
+ * Description of the test:
+ * Perform five tests where https-first receives an
+ * 5xx status code (standard and non-standard 5xx status) if request is send to site by https.
+ * Expected behaviour: https-first fallbacks to http after receiving 5xx error.
+ * Test 1: 501 Response
+ * Test 2: 504 Response
+ * Test 3: 521 Response
+ * Test 4: 530 Response
+ * Test 5: 560 Response
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+const REQUEST_URL =
+ "http://example.com/tests/dom/security/test/https-first/file_downgrade_500_responses.sjs";
+
+const redirectQueries = ["?test1a", "?test2a","?test3a", "?test4a", "?test5a"];
+let currentTest = 0;
+let testWin;
+let currentQuery;
+window.addEventListener("message", receiveMessage);
+
+// Receive message and verify that it is from an http site.
+// When the message is 'downgraded' then it was send by an http site
+// and the redirection worked.
+async function receiveMessage(event) {
+ let data = event.data;
+ currentQuery = redirectQueries[currentTest];
+ ok(data.result === "downgraded", "Redirected successful to 'http' for " + currentQuery);
+ is(data.scheme, "http:", "scheme is 'http' for " + currentQuery );
+ testWin.close();
+ if (++currentTest < redirectQueries.length) {
+ runTest();
+ return;
+ }
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+async function runTest() {
+ currentQuery = redirectQueries[currentTest];
+ testWin = window.open(REQUEST_URL + currentQuery, "_blank");
+}
+
+SpecialPowers.pushPrefEnv({ set: [
+ ["dom.security.https_first", true]
+ ]}, runTest);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/https-first/test_downgrade_bad_responses.html b/dom/security/test/https-first/test_downgrade_bad_responses.html
new file mode 100644
index 0000000000..39cef7f26a
--- /dev/null
+++ b/dom/security/test/https-first/test_downgrade_bad_responses.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Bug 1709552 : HTTPS-First: Add downgrade tests for bad responses to https request </title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+/*
+ * Description of the test:
+ * We perform five tests where we expect https-first to detect
+ * that the target site only supports http
+ * Test 1: 400 Response
+ * Test 2: 401 Response
+ * Test 3: 403 Response
+ * Test 4: 416 Response
+ * Test 5: 418 Response
+ * Test 6: Timeout
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+const REQUEST_URL =
+ "http://example.com/tests/dom/security/test/https-first/file_downgrade_bad_responses.sjs";
+
+const redirectQueries = ["?test1a", "?test2a","?test3a", "?test4a", "?test5a", "?test6a"];
+let currentTest = 0;
+let testWin;
+let currentQuery;
+window.addEventListener("message", receiveMessage);
+
+// Receive message and verify that it is from an http site.
+// When the message is 'downgraded' then it was send by an http site
+// and the redirection worked.
+async function receiveMessage(event) {
+ let data = event.data;
+ currentQuery = redirectQueries[currentTest];
+ ok(data.result === "downgraded", "Redirected successful to 'http' for " + currentQuery);
+ ok(data.scheme === "http", "scheme is 'http' for " + currentQuery );
+ testWin.close();
+ if (++currentTest < redirectQueries.length) {
+ runTest();
+ return;
+ }
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+async function runTest() {
+ currentQuery = redirectQueries[currentTest];
+ testWin = window.open(REQUEST_URL + currentQuery, "_blank");
+}
+
+SpecialPowers.pushPrefEnv({ set: [
+ ["dom.security.https_first", true]
+ ]}, runTest);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/https-first/test_downgrade_request_upgrade_request.html b/dom/security/test/https-first/test_downgrade_request_upgrade_request.html
new file mode 100644
index 0000000000..b659636ace
--- /dev/null
+++ b/dom/security/test/https-first/test_downgrade_request_upgrade_request.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title> Bug 1706126: Test https-first, downgrade first request and then upgrade redirection to subdomain</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+/*
+ * Description of the test:
+ * First we request http://redirect-example.com which HTTPS-First upgrades to https://redirect-example.com.
+ * The request https://redirect-example.com doesn't receive an answer (timeout), so we send a background
+ * request.
+ * The background request receives an answer. So the request https://redirect-example.com gets downgraded
+ * to http://redirect-example.com by the exempt flag.
+ * The request http://redirect-example.com gets redirected to http://wwww.redirect-example.com. At that stage
+ * HTTPS-First should clear the exempt flag and upgrade the redirection to https://wwww.redirect-example.com.
+ *
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+const REQUEST_URL =
+ "http://redirect-example.com/tests/dom/security/test/https-first/file_downgrade_request_upgrade_request.sjs";
+
+let testWin;
+window.addEventListener("message", receiveMessage);
+
+// Receive message and verify that it is from an https site.
+async function receiveMessage(event) {
+ let data = event.data;
+ ok(data.result === "upgraded", "Redirected successful to 'https' for subdomain ");
+ is(data.scheme,"https:", "scheme is 'https' for subdomain");
+ testWin.close();
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+async function runTest() {
+ testWin = window.open(REQUEST_URL, "_blank");
+}
+
+SpecialPowers.pushPrefEnv({ set: [
+ ["dom.security.https_first", true]
+ ]}, runTest);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/https-first/test_form_submission.html b/dom/security/test/https-first/test_form_submission.html
new file mode 100644
index 0000000000..a68c3501c6
--- /dev/null
+++ b/dom/security/test/https-first/test_form_submission.html
@@ -0,0 +1,122 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1720103 - Https-first: Do not upgrade form submissions (for now)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+<script class="testbody" type="text/javascript">
+/*
+ * Description of the test:
+ * We test https-first behaviour with forms.
+ * We perform each test with once with same origin and the second time
+ * with a cross origin. We perform two GET form requests and two POST
+ * form requests.
+ * In more detail:
+ *
+ * 1. Test: Request that gets upgraded to https, GET form submission.
+ *
+ * 2. Test: Request that gets upgraded to https, that upgraded request
+ * gets timed out, so https-first send an http request, GET form submission.
+ *
+ * 3. Test: request that gets upgraded to https, and sends a POST form
+ * to http://example.com.
+ *
+ * 4. Test: Request where the https upgrade get timed out -> http, and sends a POST form
+ * to http://example.com,
+ *
+ */
+SimpleTest.waitForExplicitFinish();
+window.addEventListener("message", receiveMessage);
+
+const SAME_ORIGIN = "http://example.com/tests/dom/security/test/https-first/file_form_submission.sjs";
+const CROSS_ORIGIN = SAME_ORIGIN.replace(".com", ".org");
+const Tests = [{
+ // 1. Test GET, gets upgraded
+ query: "?test=1",
+ scheme: "https:",
+ method: "GET",
+ value: "test=success",
+},
+{
+ // 2. Test GET, initial request will be downgraded
+ query:"?test=2",
+ scheme: "http:",
+ method: "GET",
+ value: "test=success"
+},
+{ // 3. Test POST formular, gets upgraded
+ query: "?test=3",
+ scheme: "http:",
+ method: "POST",
+ value: "test=success"
+},
+{ // 4. Test POST formular, request will be downgraded
+ query: "?test=4",
+ scheme: "http:",
+ method: "POST",
+ value: "test=success"
+},
+];
+let currentTest;
+let counter = 0;
+let testWin;
+let sameOrigin = true;
+
+// Verify that top-level request got the expected scheme and reached the correct location.
+async function receiveMessage(event){
+ let data = event.data;
+ let origin = sameOrigin? SAME_ORIGIN : CROSS_ORIGIN
+ const expectedLocation = origin.replace("http:", currentTest.scheme);
+ // If GET request check that form was transfered by url
+ if (currentTest.method === "GET") {
+ is(data.location, expectedLocation + currentTest.query,
+ "Reached the correct location for " + currentTest.query );
+ } else {
+ // Since the form is always send to example.com we expect it here as location
+ is(data.location.includes(SAME_ORIGIN.replace("http:", currentTest.scheme)), true,
+ "Reached the correct location for " + currentTest.query );
+ }
+ is(data.scheme, currentTest.scheme,`${currentTest.query} upgraded or downgraded to ` + currentTest.scheme);
+ // Check that the form value is correct
+ is(data.form, currentTest.value, "Form was transfered");
+ testWin.close();
+ // Flip origin flag
+ sameOrigin ^= true;
+ // Only go to next test if already sent same and cross origin request for current test
+ if (sameOrigin) {
+ counter++;
+ }
+ // Check if we have test left, if not finish the testing
+ if (counter >= Tests.length) {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+ return;
+ }
+ // If we didn't reached the end yet, run next test
+ runTest();
+}
+
+function runTest() {
+ currentTest = Tests[counter];
+ // If sameOrigin flag is set make a origin request, else a cross origin request
+ if (sameOrigin) {
+ testWin= window.open(SAME_ORIGIN + currentTest.query, "_blank");
+ } else {
+ testWin= window.open(CROSS_ORIGIN + currentTest.query, "_blank");
+ }
+}
+
+// Set prefs and start test
+SpecialPowers.pushPrefEnv({ set: [
+ ["dom.security.https_first", true],
+ ["security.warn_submit_secure_to_insecure", false]
+ ]}, runTest);
+
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/https-first/test_fragment.html b/dom/security/test/https-first/test_fragment.html
new file mode 100644
index 0000000000..4a27f198e1
--- /dev/null
+++ b/dom/security/test/https-first/test_fragment.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Bug 1706577: Have https-first mode account for fragment navigations</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+/*
+ * Description of the test:
+ * Have https-first detect a fragment navigation rather than navigating away
+ * from the page.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+const REQUEST_URL = "http://example.com/tests/dom/security/test/https-first/file_fragment.html";
+const EXPECT_URL = REQUEST_URL.replace("http://", "https://");
+
+let winTest = null;
+let checkButtonClicked = false;
+
+async function receiveMessage(event) {
+ let data = event.data;
+ if (!checkButtonClicked) {
+ ok(data.result == EXPECT_URL, "location is correct");
+ ok(data.button, "button is clicked");
+ ok(data.info == "onload", "Onloading worked");
+ checkButtonClicked = true;
+ return;
+ }
+
+ // Once the button was clicked we know the tast has finished
+ ok(data.button, "button is clicked");
+ is(data.result, EXPECT_URL + "#foo", "location (hash) is correct");
+ ok(data.info == "scrolled-to-foo","Scrolled successfully without reloading!");
+ is(data.documentURI, EXPECT_URL + "#foo", "Document URI is correct");
+ window.removeEventListener("message",receiveMessage);
+ winTest.close();
+ SimpleTest.finish();
+}
+
+async function runTest() {
+ await SpecialPowers.pushPrefEnv({ set: [
+ ["dom.security.https_first", true],
+ ]});
+ winTest = window.open(REQUEST_URL);
+}
+
+window.addEventListener("message", receiveMessage);
+
+runTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/https-first/test_multiple_redirection.html b/dom/security/test/https-first/test_multiple_redirection.html
new file mode 100644
index 0000000000..d631f140e6
--- /dev/null
+++ b/dom/security/test/https-first/test_multiple_redirection.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1721410
+Test multiple redirects using https-first and ensure the entire redirect chain is using https
+-->
+
+<head>
+ <title>HTTPS-First-Mode - Test for multiple redirections</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+
+ <script class="testbody" type="text/javascript">
+ "use strict";
+
+ SimpleTest.waitForExplicitFinish();
+
+ const testCase = [
+ // test 1: https-first upgrades http://example.com/test1 -> https://example.com/test1
+ // that's redirect to https://example.com/.../redirect which then redirects
+ // to http://example.com/../verify. Since the last redirect is http, and the
+ // the redirection chain contains already example.com we expect https-first
+ // to downgrade the request.
+ {name: "test last redirect HTTP", result: "scheme-http", query: "test1" },
+ // test 2: https-first upgrades http://example.com/test2 -> https://example.com/test2
+ // that's redirect to https://example.com/.../redirect which then redirects
+ // to https://example.com/../verify. Since the last redirect is https, we
+ // expect to reach an https website.
+ {name: "test last redirect HTTPS", result: "scheme-https", query: "test2"},
+ // test 3: https-first upgrades http://example.com/test3 -> https://example.com/test3
+ // that's redirect to https://example.com/.../hsts which then sets an hsts header
+ // and redirects to http://example.com/../verify. Since an hsts header was set
+ // we expect that to reach an https site
+ {name: "test last redirect HSTS", result: "scheme-https", query: "test3"},
+ // reset: reset hsts header for example.com
+ {name: "reset HSTS header", result: "scheme-https", query: "reset"},
+ ]
+ let currentTest = 0;
+ let testWin;
+ window.addEventListener("message", receiveMessage);
+
+ // receive message from loaded site verifying the scheme of
+ // the loaded document.
+ async function receiveMessage(event) {
+ let test = testCase[currentTest];
+ is(event.data.result,
+ test.result,
+ "same-origin redirect results in " + test.name
+ );
+ testWin.close();
+ if (++currentTest < testCase.length) {
+ startTest();
+ return;
+ }
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+ }
+
+ async function startTest() {
+ const test = testCase[currentTest];
+ // Load an http:// window which gets upgraded to https://
+ let uri =
+ `http://example.com/tests/dom/security/test/https-first/file_multiple_redirection.sjs?${test.query}`;
+ testWin = window.open(uri);
+ }
+
+ // Set preference and start test
+ SpecialPowers.pushPrefEnv({ set: [
+ ["dom.security.https_first", true],
+ ]}, startTest);
+ </script>
+</body>
+</html>
diff --git a/dom/security/test/https-first/test_redirect_downgrade.html b/dom/security/test/https-first/test_redirect_downgrade.html
new file mode 100644
index 0000000000..07f998c085
--- /dev/null
+++ b/dom/security/test/https-first/test_redirect_downgrade.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Bug 1707856: Test redirect downgrades with https-first</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+/*
+ * Description of the test:
+ * We perform three tests where we expect https-first to detect
+ * that the target site only supports http
+ * Test 1: Meta Refresh
+ * Test 2: JS Redirect
+ * Test 3: 302 redirect
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+const REQUEST_URL =
+ "http://example.com/tests/dom/security/test/https-first/file_redirect_downgrade.sjs";
+
+const redirectQueries = ["?test1a", "?test2a","?test3a"];
+let currentTest = 0;
+let testWin;
+let currentQuery;
+window.addEventListener("message", receiveMessage);
+
+// Receive message and verify that it is from an https site.
+// When the message is 'downgraded' then it was send by an http site
+// and the redirection worked.
+async function receiveMessage(event) {
+ let data = event.data;
+ ok(data.result === "downgraded", "Redirected successful to 'http' for " + currentQuery);
+ ok(data.scheme === "http:", "scheme is 'http' for " + currentQuery );
+ testWin.close();
+ if (++currentTest < redirectQueries.length) {
+ runTest();
+ return;
+ }
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+async function runTest() {
+ currentQuery = redirectQueries[currentTest];
+ testWin = window.open(REQUEST_URL + currentQuery, "_blank");
+}
+
+SpecialPowers.pushPrefEnv({ set: [
+ ["dom.security.https_first", true]
+ ]}, runTest);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/https-first/test_redirect_upgrade.html b/dom/security/test/https-first/test_redirect_upgrade.html
new file mode 100644
index 0000000000..6cccf6af67
--- /dev/null
+++ b/dom/security/test/https-first/test_redirect_upgrade.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1706351
+Test that 302 redirect requests get upgraded to https:// with HTTPS-First Mode enabled
+-->
+
+<head>
+ <title>HTTPS-FirstMode - Redirect Upgrade</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+ <h1>HTTPS-First Mode</h1>
+ <p>Upgrade Test for insecure redirects.</p>
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1706351">Bug 1706351</a>
+
+ <script class="testbody" type="text/javascript">
+ "use strict";
+
+ const redirectCodes = ["301", "302","303","307"];
+ let currentTest = 0;
+ let testWin;
+ window.addEventListener("message", receiveMessage);
+
+ // Receive message and verify that it is from an https site.
+ // When the message is 'secure' then it was send by an https site.
+ async function receiveMessage(event) {
+ let data = event.data;
+ let currentRedirectCode = redirectCodes[currentTest];
+ ok(data.result === "secure", "Received 'https' for " + currentRedirectCode);
+ testWin.close();
+ if (++currentTest < redirectCodes.length) {
+ startTest();
+ return;
+ }
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+ }
+
+ async function startTest() {
+ const currentCode = redirectCodes[currentTest];
+ // Make a request to a site (eg. https://file_redirect.sjs?301), which will redirect to http://file_redirect.sjs?check.
+ // The response will either be secure-ok, if the request has been upgraded to https:// or secure-error if it didn't.
+ testWin = window.open(`https://example.com/tests/dom/security/test/https-first/file_redirect.sjs?${currentCode}`);
+ }
+
+ // Set preference and start test
+ SpecialPowers.pushPrefEnv({ set: [
+ ["dom.security.https_first", true],
+ ["security.mixed_content.block_active_content", false],
+ ["security.mixed_content.block_display_content", false],
+ ]}, startTest);
+ SimpleTest.waitForExplicitFinish();
+ </script>
+</body>
+</html>
diff --git a/dom/security/test/https-first/test_referrer_policy.html b/dom/security/test/https-first/test_referrer_policy.html
new file mode 100644
index 0000000000..61521e2351
--- /dev/null
+++ b/dom/security/test/https-first/test_referrer_policy.html
@@ -0,0 +1,237 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Bug 1716706 : Write referrer-policy tests for https-first </title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+/*
+ * Description of the test:
+ * We perform each test with 8 different settings.
+ * The first is a same origin request from an http site to an https site.
+ * The second is a same origin request from an https -> https.
+ * The third is a cross-origin request from an http -> https.
+ * The fourth is a cross-origin request from an https -> https.
+ * The fifth is a same origin request from an http -> http site.
+ * The sixth is a same origin request from an https -> http.
+ * The seventh is a cross-origin request from an http -> http.
+ * The last is a cross-origin request from an https -> http.
+ */
+
+SimpleTest.waitForExplicitFinish();
+// This test performs a lot of requests and checks (64 requests).
+// So to prevent to get a timeout before executing all test request longer timeout.
+SimpleTest.requestLongerTimeout(2);
+const SAME_ORIGIN =
+ "http://example.com/tests/dom/security/test/https-first/file_referrer_policy.sjs?";
+// SAME ORIGIN with "https" instead of "http"
+const SAME_ORIGIN_HTTPS = SAME_ORIGIN.replace("http", "https");
+
+const CROSS_ORIGIN =
+ "http://example.org/tests/dom/security/test/https-first/file_referrer_policy.sjs?";
+// CROSS ORIGIN with "https" instead of "http"
+const CROSS_ORIGIN_HTTPS = CROSS_ORIGIN.replace("http", "https");
+
+// Define test cases. Query equals the test case referrer policy.
+// We will set in the final request the url parameters such that 'rp=' equals the referrer policy
+//and 'upgrade=' equals '1' if the request should be https.
+// For a 'upgrade=0' url parameter the server lead to a timeout such that https-first downgrades
+// the request to http.
+const testCases = [
+ {
+ query: "no-referrer",
+ expectedResultSameOriginDownUp: "",
+ expectedResultSameOriginUpUp: "",
+ expectedResultCrossOriginDownUp:"",
+ expectedResultCrossOriginUpUp:"",
+ expectedResultSameOriginDownDown: "",
+ expectedResultSameOriginUpDown: "",
+ expectedResultCrossOriginDownDown:"",
+ expectedResultCrossOriginUpDown: "",
+ },
+ {
+ query: "no-referrer-when-downgrade",
+ expectedResultSameOriginDownUp: SAME_ORIGIN + "rp=no-referrer-when-downgrade&upgrade=http-https",
+ expectedResultSameOriginUpUp: SAME_ORIGIN_HTTPS + "rp=no-referrer-when-downgrade&upgrade=https-https",
+ expectedResultCrossOriginDownUp: CROSS_ORIGIN + "rp=no-referrer-when-downgrade&upgrade=http-https",
+ expectedResultCrossOriginUpUp: CROSS_ORIGIN_HTTPS + "rp=no-referrer-when-downgrade&upgrade=https-https",
+ expectedResultSameOriginDownDown: SAME_ORIGIN + "rp=no-referrer-when-downgrade&upgrade=http-http",
+ expectedResultSameOriginUpDown: "",
+ expectedResultCrossOriginDownDown: CROSS_ORIGIN + "rp=no-referrer-when-downgrade&upgrade=http-http",
+ expectedResultCrossOriginUpDown:"",
+ },
+ {
+ query: "origin",
+ expectedResultSameOriginDownUp: "http://example.com/",
+ expectedResultSameOriginUpUp: "https://example.com/",
+ expectedResultCrossOriginDownUp:"http://example.org/",
+ expectedResultCrossOriginUpUp:"https://example.org/",
+ expectedResultSameOriginDownDown: "http://example.com/",
+ expectedResultSameOriginUpDown: "https://example.com/",
+ expectedResultCrossOriginDownDown:"http://example.org/",
+ expectedResultCrossOriginUpDown:"https://example.org/",
+ },
+ {
+ query: "origin-when-cross-origin",
+ expectedResultSameOriginDownUp: "http://example.com/",
+ expectedResultSameOriginUpUp: SAME_ORIGIN_HTTPS + "rp=origin-when-cross-origin&upgrade=https-https",
+ expectedResultCrossOriginDownUp:"http://example.org/",
+ expectedResultCrossOriginUpUp:"https://example.org/",
+ expectedResultSameOriginDownDown: SAME_ORIGIN + "rp=origin-when-cross-origin&upgrade=http-http",
+ expectedResultSameOriginUpDown: "https://example.com/",
+ expectedResultCrossOriginDownDown:"http://example.org/",
+ expectedResultCrossOriginUpDown:"https://example.org/",
+ },
+ {
+ query: "same-origin",
+ expectedResultSameOriginDownUp: "",
+ expectedResultSameOriginUpUp: SAME_ORIGIN_HTTPS + "rp=same-origin&upgrade=https-https",
+ expectedResultCrossOriginDownUp:"",
+ expectedResultCrossOriginUpUp:"",
+ expectedResultSameOriginDownDown: SAME_ORIGIN + "rp=same-origin&upgrade=http-http",
+ expectedResultSameOriginUpDown: "",
+ expectedResultCrossOriginDownDown: "",
+ expectedResultCrossOriginUpDown:"",
+ },
+ {
+ query: "strict-origin",
+ expectedResultSameOriginDownUp: "http://example.com/",
+ expectedResultSameOriginUpUp: "https://example.com/",
+ expectedResultCrossOriginDownUp:"http://example.org/",
+ expectedResultCrossOriginUpUp:"https://example.org/",
+ expectedResultSameOriginDownDown: "http://example.com/",
+ expectedResultSameOriginUpDown: "",
+ expectedResultCrossOriginDownDown:"http://example.org/",
+ expectedResultCrossOriginUpDown:"",
+ },
+ {
+ query: "strict-origin-when-cross-origin",
+ expectedResultSameOriginDownUp: "http://example.com/",
+ expectedResultSameOriginUpUp: SAME_ORIGIN_HTTPS + "rp=strict-origin-when-cross-origin&upgrade=https-https",
+ expectedResultCrossOriginDownUp:"http://example.org/",
+ expectedResultCrossOriginUpUp:"https://example.org/",
+ expectedResultSameOriginDownDown: SAME_ORIGIN + "rp=strict-origin-when-cross-origin&upgrade=http-http",
+ expectedResultSameOriginUpDown: "",
+ expectedResultCrossOriginDownDown:"http://example.org/",
+ expectedResultCrossOriginUpDown:"",
+ },
+ {
+ query: "unsafe-url",
+ expectedResultSameOriginDownUp: SAME_ORIGIN + "rp=unsafe-url&upgrade=http-https",
+ expectedResultSameOriginUpUp: SAME_ORIGIN_HTTPS + "rp=unsafe-url&upgrade=https-https",
+ expectedResultCrossOriginDownUp: CROSS_ORIGIN + "rp=unsafe-url&upgrade=http-https",
+ expectedResultCrossOriginUpUp: CROSS_ORIGIN_HTTPS + "rp=unsafe-url&upgrade=https-https",
+ expectedResultSameOriginDownDown: SAME_ORIGIN + "rp=unsafe-url&upgrade=http-http",
+ expectedResultSameOriginUpDown: SAME_ORIGIN_HTTPS + "rp=unsafe-url&upgrade=https-http",
+ expectedResultCrossOriginDownDown:CROSS_ORIGIN + "rp=unsafe-url&upgrade=http-http",
+ expectedResultCrossOriginUpDown:CROSS_ORIGIN_HTTPS + "rp=unsafe-url&upgrade=https-http",
+ },
+];
+
+
+let currentTest = 0;
+let sameOriginRequest = true;
+let testWin;
+let currentQuery;
+window.addEventListener("message", receiveMessage);
+let currentRun = 0;
+// All combinations, HTTP -> HTTPS, HTTPS -> HTTPS, HTTP -> HTTP, HTTPS -> HTTP
+const ALL_COMB = ["http-https", "https-https" ,"http-http", "https-http"];
+
+// Receive message and verify that we receive the expected referrer header
+async function receiveMessage(event) {
+ let data = event.data;
+ currentQuery = testCases[currentTest].query;
+ let currentComb = ALL_COMB[currentRun];
+ // if request was http -> https
+ if (currentComb === "http-https") {
+ if (sameOriginRequest){
+ is(data.result, testCases[currentTest].expectedResultSameOriginDownUp ,
+ "We received for the downgraded same site request with referrer policy: " + currentQuery + " the correct referrer");
+ is(data.location, SAME_ORIGIN_HTTPS + "sendMe","Opened correct location");
+ } else {
+ is(data.result, testCases[currentTest].expectedResultCrossOriginDownUp ,
+ "We received for the downgraded cross site request with referrer policy: " + currentQuery + " the correct referrer");
+ is(data.location, SAME_ORIGIN_HTTPS + "sendMe", "Opened correct location");
+ }
+ // if request was https -> https
+ } else if (currentComb === "https-https") {
+ if (sameOriginRequest){
+ is(data.result, testCases[currentTest].expectedResultSameOriginUpUp ,
+ "We received for the upgraded same site request with referrer policy: " + currentQuery + " the correct referrer");
+ is(data.location, SAME_ORIGIN_HTTPS + "sendMe", "Opened correct location");
+ } else {
+ is(data.result, testCases[currentTest].expectedResultCrossOriginUpUp,
+ "We received for the upgraded cross site request with referrer policy: " + currentQuery + " the correct referrer");
+ is(data.location, SAME_ORIGIN_HTTPS + "sendMe", "Opened correct location");
+ }
+ } else if (currentComb === "http-http") {
+ if (sameOriginRequest){
+ is(data.result, testCases[currentTest].expectedResultSameOriginDownDown ,
+ "We received for the upgraded same site request with referrer policy: " + currentQuery + " the correct referrer");
+ is(data.location, SAME_ORIGIN + "sendMe2","Opened correct location for" + currentQuery + currentComb);
+ } else {
+ is(data.result, testCases[currentTest].expectedResultCrossOriginDownDown,
+ "We received for the upgraded cross site request with referrer policy: " + currentQuery + " the correct referrer");
+ is(data.location, SAME_ORIGIN + "sendMe2", "Opened correct location " + currentQuery + currentComb);
+ }
+ } else if (currentComb === "https-http") {
+ if (sameOriginRequest){
+ is(data.result, testCases[currentTest].expectedResultSameOriginUpDown ,
+ "We received for the upgraded same site request with referrer policy: " + currentQuery + " the correct referrer");
+ is(data.location, SAME_ORIGIN + "sendMe2","Opened correct location " + currentQuery + currentComb);
+ } else {
+ is(data.result, testCases[currentTest].expectedResultCrossOriginUpDown,
+ "We received for the upgraded cross site request with referrer policy: " + currentQuery + " the correct referrer");
+ is(data.location, SAME_ORIGIN + "sendMe2", "Opened correct location " + currentQuery + currentComb);
+ }
+ }
+ testWin.close();
+ currentRun++;
+ if (currentTest >= testCases.length -1 && currentRun === ALL_COMB.length && !sameOriginRequest) {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+ return;
+ }
+ runTest();
+}
+
+async function runTest() {
+ currentQuery = testCases[currentTest].query;
+ // send same origin request
+ if (sameOriginRequest && currentRun < ALL_COMB.length) {
+ // if upgrade = 0 downgrade request, else upgrade
+ testWin = window.open(SAME_ORIGIN + "rp=" +currentQuery + "&upgrade=" + ALL_COMB[currentRun], "_blank");
+ } else {
+ // if same origin isn't set, check if we need to send cross origin requests
+ // eslint-disable-next-line no-lonely-if
+ if (!sameOriginRequest && currentRun < ALL_COMB.length ) {
+ // if upgrade = 0 downgrade request, else upgrade
+ testWin = window.open(CROSS_ORIGIN + "rp=" +currentQuery + "&upgrade=" + ALL_COMB[currentRun], "_blank");
+ } // else we completed all test case of the current query for the current origin. Prepare and call next test
+ else {
+ // reset currentRun and go to next query
+ currentRun = 0;
+ if(!sameOriginRequest){
+ currentTest++;
+ }
+ // run same test again for crossOrigin or start new test with sameOrigin
+ sameOriginRequest = !sameOriginRequest;
+ currentQuery = testCases[currentTest].query;
+ runTest();
+ }
+ }
+}
+
+SpecialPowers.pushPrefEnv({ set: [
+ ["dom.security.https_first", true],
+ ["network.http.referer.disallowCrossSiteRelaxingDefault", false],
+ ]}, runTest);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/https-first/test_resource_upgrade.html b/dom/security/test/https-first/test_resource_upgrade.html
new file mode 100644
index 0000000000..66f65d9a04
--- /dev/null
+++ b/dom/security/test/https-first/test_resource_upgrade.html
@@ -0,0 +1,115 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>HTTPS-First Mode - Resource Upgrade</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+ <h1>HTTPS-First Mode</h1>
+ <p>Upgrade Test for various resources</p>
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1704454">Bug 1704454/a>
+ <iframe style="width:100%;" id="testframe"></iframe>
+
+ <script class="testbody" type="text/javascript">
+ /* Description of the test:
+ * We load resources (img, script, sytle, etc) over *http* and
+ * make sure they do not get upgraded to *https* because
+ * https-first only applies to top-level requests.
+ *
+ * In detail:
+ * We perform an XHR request to the *.sjs file which is processed async on
+ * the server and waits till all the requests were processed by the server.
+ * Once the server received all the different requests, the server responds
+ * to the initial XHR request with an array of results which must match
+ * the expected results from each test, making sure that all requests
+ * received by the server (*.sjs) were actually *http* requests.
+ */
+
+ const splitRegex = /^(.*)-(.*)$/
+ const testConfig = {
+ topLevelScheme: "http://",
+ results: [
+ "iframe", "script", "img", "img-redir", "font", "xhr", "style",
+ "media", "object", "form", "nested-img","top-level"
+ ]
+ }
+
+
+ function runTest() {
+ // sends an xhr request to the server which is processed async, which only
+ // returns after the server has received all the expected requests.
+ var myXHR = new XMLHttpRequest();
+ myXHR.open("GET", "file_upgrade_insecure_server.sjs?queryresult");
+ myXHR.onload = function (e) {
+ var results = myXHR.responseText.split(",");
+ for (var index in results) {
+ checkResult(results[index]);
+ }
+ }
+ myXHR.onerror = function (e) {
+ ok(false, "Could not query results from server (" + e.message + ")");
+ finishTest();
+ }
+ myXHR.send();
+
+ // give it some time and run the testpage
+ SimpleTest.executeSoon(() => {
+ var src = testConfig.topLevelScheme + "example.com/tests/dom/security/test/https-first/file_upgrade_insecure.html";
+ document.getElementById("testframe").src = src;
+ });
+ }
+
+ // a postMessage handler that is used by sandboxed iframes without
+ // 'allow-same-origin' to bubble up results back to this main page.
+ window.addEventListener("message", receiveMessage);
+ function receiveMessage(event) {
+ checkResult(event.data.result);
+ }
+
+ function finishTest() {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+ }
+
+ function checkResult(response) {
+ // A response looks either like this "iframe-ok" or "[key]-[result]"
+ const [, key, result] = splitRegex.exec(response)
+ // try to find the expected result within the results array
+ var index = testConfig.results.indexOf(key);
+
+ // If the response is not even part of the results array, something is super wrong
+ if (index == -1) {
+ ok(false, `Unexpected response from server (${response})`);
+ finishTest();
+ }
+
+ // take the element out the array and continue till the results array is empty
+ if (index != -1) {
+ testConfig.results.splice(index, 1);
+ }
+
+ // Check if the result was okay or had an error
+ is(result, 'ok', `Upgrade all requests on toplevel http for '${key}' came back with: '${result}'`)
+
+ // If we're not expecting any more resulsts, finish the test
+ if (!testConfig.results.length) {
+ finishTest();
+ }
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ // Set preference and start test
+ SpecialPowers.pushPrefEnv({ set: [
+ ["dom.security.https_first", true],
+ ["security.mixed_content.block_active_content", false],
+ ["security.mixed_content.block_display_content", false]
+ ] }, runTest);
+
+ </script>
+</body>
+
+</html>
diff --git a/dom/security/test/https-first/test_toplevel_cookies.html b/dom/security/test/https-first/test_toplevel_cookies.html
new file mode 100644
index 0000000000..2c0c64db46
--- /dev/null
+++ b/dom/security/test/https-first/test_toplevel_cookies.html
@@ -0,0 +1,116 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Bug 1711453 : HTTPS-First: Add test for cookies </title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+/*
+ * Description of the test:
+ * We perform each test with 4 different cookie settings and
+ * expect https-first to detect which cookie is same origin and
+ * which is cross origin. The cookies are in an image or in a frame.
+ * The 4 cookie settings differ in two flags which are set or not.
+ * The first call is always with secure flag not set and sameSite=none
+ * In the second call we don't set the secure flag but sameSite=strict
+ * In the third call we set the secure flag and sameSite=none
+ * In the forth call we set the secure flag and sameSite=strict
+ * More detailed:
+ * We run the tests in the following order.
+ * Test 1a: Image is loaded with cookie same-origin, not secure and sameSite=none
+ * Test 1b: Image is loaded with cookie same-origin, not secure and sameSite=strict
+ * Test 1c: Image is loaded with cookie same-origin, secure and sameSite=none
+ * Test 1d: Image is loaded with cookie same-origin, secure and sameSite=strict
+ * Test 1e: Image is loaded with cookie cross-origin, not secure and sameSite=none
+ * Test 1f: Image is loaded with cookie cross-origin, not secure and sameSite=strict
+ * Test 2a: Load frame navigation with cookie same-origin, not secure and sameSite=none
+ * ...
+ * Test 3a: Load frame navigation blank with cookie same-origin, not secure and sameSite=none
+ * ...
+ * Test 4a: Load frame Inc with cookie same-origin, not secure and sameSite=none
+ * ...
+ * Test 5a: Load frame Inc Blank with cookie same-origin, not secure and sameSite=none
+ * ...
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+const SAME_ORIGIN =
+ "http://example.com/tests/dom/security/test/https-first/file_toplevel_cookies.sjs?";
+
+const CROSS_ORIGIN =
+ "http://example.org/tests/dom/security/test/https-first/file_toplevel_cookies.sjs?";
+
+const redirectQueries = ["setImage", "loadNav", "loadNavBlank","loadframeInc", "loadframeIncBlank"];
+let currentTest = 0;
+let sameOriginRequest = true;
+let testWin;
+let currentQuery;
+window.addEventListener("message", receiveMessage);
+let currentRun = 0;
+// All possible cookie attribute combinations
+// cookie attributes are secure=set/not set and sameSite= none/ strict
+const ALL_COOKIE_COMB = ["notSecure,none", "notSecure,strict", "secure,none", "secure,strict"]
+
+// Receive message and verify that it is from an https site.
+// When the message is 'upgraded' then it was send by an https site
+// and validate that we received the right cookie. Verify that for a cross
+//origin request we didn't receive a cookie.
+async function receiveMessage(event) {
+ let data = event.data;
+ currentQuery = redirectQueries[currentTest];
+ ok(data.result === "upgraded", "Upgraded successful to https for " + currentQuery);
+ ok(data.loc.includes("https"), "scheme is 'https' for " + currentQuery );
+ if (!sameOriginRequest) {
+ ok(data.cookie === "", "Cookie from cross-Origin site shouldn't be accepted " + currentQuery + " " + ALL_COOKIE_COMB[currentRun]);
+ } else {
+ is(data.cookie.includes(currentQuery + "=" + currentRun), true, "Cookie successfully arrived for " + currentQuery + " " + ALL_COOKIE_COMB[currentRun]);
+ }
+ testWin.close();
+ currentRun++;
+ if (currentTest >= redirectQueries.length -1 && currentRun === ALL_COOKIE_COMB.length && !sameOriginRequest) {
+ window.removeEventListener("message", receiveMessage);
+ SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault");
+ SimpleTest.finish();
+ return;
+ }
+ runTest();
+}
+
+async function runTest() {
+ currentQuery = redirectQueries[currentTest];
+ // send same origin request
+ if (sameOriginRequest && currentRun < ALL_COOKIE_COMB.length) {
+ testWin = window.open(SAME_ORIGIN + currentQuery + currentRun, "_blank");
+ } else {
+ // if same origin isn't set, check if we need to send cross origin requests
+ // eslint-disable-next-line no-lonely-if
+ if (!sameOriginRequest && currentRun < ALL_COOKIE_COMB.length ) {
+ testWin = window.open(CROSS_ORIGIN + currentQuery + currentRun, "_blank");
+ } // else we completed all test case of the current query for the current origin. Prepare and call next test
+ else {
+ // reset currentRun and go to next query
+ currentRun = 0;
+ if(!sameOriginRequest){
+ currentTest++;
+ }
+ // run same test again for crossOrigin or start new test with sameOrigin
+ sameOriginRequest = !sameOriginRequest;
+ currentQuery = redirectQueries[currentTest];
+ runTest();
+ }
+ }
+}
+
+SpecialPowers.pushPrefEnv({ set: [
+ ["dom.security.https_first", true],
+ ["network.cookie.sameSite.noneRequiresSecure", false],
+ ]}, runTest);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/https-only/browser.toml b/dom/security/test/https-only/browser.toml
new file mode 100644
index 0000000000..2cba418aff
--- /dev/null
+++ b/dom/security/test/https-only/browser.toml
@@ -0,0 +1,62 @@
+[DEFAULT]
+prefs = ["dom.security.https_first=false"]
+
+["browser_background_redirect.js"]
+support-files = ["file_background_redirect.sjs"]
+
+["browser_bug1874801.js"]
+support-files = [
+ "file_bug1874801.sjs",
+ "file_bug1874801.html",
+]
+
+["browser_console_logging.js"]
+support-files = ["file_console_logging.html"]
+
+["browser_continue_button_delay.js"]
+
+["browser_cors_mixedcontent.js"]
+support-files = ["file_cors_mixedcontent.html"]
+
+["browser_hsts_host.js"]
+support-files = [
+ "hsts_headers.sjs",
+ "file_fragment_noscript.html",
+]
+
+["browser_httpsonly_prefs.js"]
+
+["browser_httpsonly_speculative_connect.js"]
+support-files = ["file_httpsonly_speculative_connect.html"]
+
+["browser_iframe_test.js"]
+skip-if = [
+ "os == 'linux' && bits == 64", # Bug 1735565
+ "os == 'win' && bits == 64", # Bug 1735565
+]
+support-files = ["file_iframe_test.sjs"]
+
+["browser_navigation.js"]
+support-files = ["file_redirect_to_insecure.sjs"]
+
+["browser_redirect_tainting.js"]
+support-files = ["file_redirect_tainting.sjs"]
+
+["browser_save_as.js"]
+support-files = ["file_save_as.html"]
+
+["browser_triggering_principal_exemption.js"]
+
+["browser_upgrade_exceptions.js"]
+
+["browser_upgrade_exemption.js"]
+
+["browser_user_gesture.js"]
+support-files = ["file_user_gesture.html"]
+
+["browser_websocket_exceptions.js"]
+skip-if = ["os == 'android'"] # WebSocket tests are not supported on Android Yet. Bug 1566168.
+support-files = [
+ "file_websocket_exceptions.html",
+ "file_websocket_exceptions_iframe.html",
+]
diff --git a/dom/security/test/https-only/browser_background_redirect.js b/dom/security/test/https-only/browser_background_redirect.js
new file mode 100644
index 0000000000..2907278943
--- /dev/null
+++ b/dom/security/test/https-only/browser_background_redirect.js
@@ -0,0 +1,64 @@
+"use strict";
+/* Description of the test:
+ * We load a page which gets upgraded to https. HTTPS-Only Mode then
+ * sends an 'http' background request which we redirect (using the
+ * web extension API) to 'same-origin' https. We ensure the HTTPS-Only
+ * Error page does not occur, but we https page gets loaded.
+ */
+
+let extension = null;
+
+add_task(async function test_https_only_background_request_redirect() {
+ // A longer timeout is necessary for this test since we have to wait
+ // at least 3 seconds for the https-only background request to happen.
+ requestLongerTimeout(10);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_only_mode", true]],
+ });
+
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["webRequest", "webRequestBlocking", "http://example.com/*"],
+ },
+ background() {
+ let { browser } = this;
+ browser.webRequest.onBeforeRequest.addListener(
+ details => {
+ if (details.url === "http://example.com/") {
+ browser.test.sendMessage("redir-handled");
+ let redirectUrl = "https://example.com/";
+ return { redirectUrl };
+ }
+ return undefined;
+ },
+ { urls: ["http://example.com/*"] },
+ ["blocking"]
+ );
+ },
+ });
+
+ await extension.startup();
+
+ await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, null, true);
+ BrowserTestUtils.startLoadingURIString(
+ browser,
+ "http://example.com/browser/dom/security/test/https-only/file_background_redirect.sjs?start"
+ );
+
+ await extension.awaitMessage("redir-handled");
+
+ await loaded;
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let innerHTML = content.document.body.innerHTML;
+ ok(
+ innerHTML.includes("Test Page for Bug 1683015 loaded"),
+ "No https-only error page"
+ );
+ });
+ });
+
+ await extension.unload();
+});
diff --git a/dom/security/test/https-only/browser_bug1874801.js b/dom/security/test/https-only/browser_bug1874801.js
new file mode 100644
index 0000000000..280a072736
--- /dev/null
+++ b/dom/security/test/https-only/browser_bug1874801.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Specifically test https://bugzilla.mozilla.org/show_bug.cgi?id=1874801
+
+const TAB_URL =
+ "https://example.com/browser/dom/security/test/https-only/file_bug1874801.html";
+
+function assertImageLoaded(tab) {
+ return ContentTask.spawn(tab.linkedBrowser, {}, () => {
+ const img = content.document.getElementsByTagName("img")[0];
+
+ ok(!!img, "Image tag should exist");
+ ok(img.complete && img.naturalWidth > 0, "Image should have loaded ");
+ });
+}
+
+add_task(async function test_bug1874801() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.mixed_content.upgrade_display_content", false],
+ ["dom.security.https_first", true],
+ ["dom.security.https_only_mode", true],
+ ],
+ });
+
+ // Open Tab
+ const tabToClose = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TAB_URL,
+ true
+ );
+
+ // Make sure the image was loaded via HTTPS
+ await assertImageLoaded(tabToClose);
+
+ // Close Tab
+ const tabClosePromise =
+ BrowserTestUtils.waitForSessionStoreUpdate(tabToClose);
+ BrowserTestUtils.removeTab(tabToClose);
+ await tabClosePromise;
+
+ // Restore Tab
+ const restoredTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ TAB_URL,
+ true
+ );
+ undoCloseTab();
+ const restoredTab = await restoredTabPromise;
+
+ // Make sure the image was loaded via HTTPS
+ await assertImageLoaded(restoredTab);
+});
diff --git a/dom/security/test/https-only/browser_console_logging.js b/dom/security/test/https-only/browser_console_logging.js
new file mode 100644
index 0000000000..d648c27289
--- /dev/null
+++ b/dom/security/test/https-only/browser_console_logging.js
@@ -0,0 +1,153 @@
+// Bug 1625448 - HTTPS Only Mode - Tests for console logging
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1625448
+// This test makes sure that the various console messages from the HTTPS-Only
+// mode get logged to the console.
+"use strict";
+
+// Test Cases
+// description: Description of what the subtests expects.
+// expectLogLevel: Expected log-level of a message.
+// expectIncludes: Expected substrings the message should contain.
+let tests = [
+ {
+ description: "Top-Level upgrade should get logged",
+ expectLogLevel: Ci.nsIConsoleMessage.warn,
+ expectIncludes: [
+ "HTTPS-Only Mode: Upgrading insecure request",
+ "to use",
+ "file_console_logging.html",
+ ],
+ },
+ {
+ description: "iFrame upgrade failure should get logged",
+ expectLogLevel: Ci.nsIConsoleMessage.error,
+ expectIncludes: [
+ "HTTPS-Only Mode: Upgrading insecure request",
+ "failed",
+ "file_console_logging.html",
+ ],
+ },
+ {
+ description: "WebSocket upgrade should get logged",
+ expectLogLevel: Ci.nsIConsoleMessage.warn,
+ expectIncludes: [
+ "HTTPS-Only Mode: Upgrading insecure request",
+ "to use",
+ "ws://does.not.exist",
+ ],
+ },
+ {
+ description: "Sub-Resource upgrade for file_1 should get logged",
+ expectLogLevel: Ci.nsIConsoleMessage.warn,
+ expectIncludes: ["Upgrading insecure", "request", "file_1.jpg"],
+ },
+ {
+ description: "Sub-Resource upgrade for file_2 should get logged",
+ expectLogLevel: Ci.nsIConsoleMessage.warn,
+ expectIncludes: ["Upgrading insecure", "request", "to use", "file_2.jpg"],
+ },
+ {
+ description: "Exempt request for file_exempt should get logged",
+ expectLogLevel: Ci.nsIConsoleMessage.info,
+ expectIncludes: [
+ "Not upgrading insecure request",
+ "because it is exempt",
+ "file_exempt.jpg",
+ ],
+ },
+ {
+ description: "Sub-Resource upgrade failure for file_2 should get logged",
+ expectLogLevel: Ci.nsIConsoleMessage.error,
+ expectIncludes: ["Upgrading insecure request", "failed", "file_2.jpg"],
+ },
+];
+
+const testPathUpgradeable = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+// DNS errors are not logged as HTTPS-Only Mode upgrade failures, so we have to
+// upgrade to a domain that exists but fails.
+const testPathNotUpgradeable = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://self-signed.example.com"
+);
+const kTestURISuccess = testPathUpgradeable + "file_console_logging.html";
+const kTestURIFail = testPathNotUpgradeable + "file_console_logging.html";
+const kTestURIExempt = testPathUpgradeable + "file_exempt.jpg";
+
+const UPGRADE_DISPLAY_CONTENT =
+ "security.mixed_content.upgrade_display_content";
+
+add_task(async function () {
+ // A longer timeout is necessary for this test than the plain mochitests
+ // due to opening a new tab with the web console.
+ requestLongerTimeout(4);
+
+ // Enable HTTPS-Only Mode and register console-listener
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_only_mode", true]],
+ });
+ Services.console.registerListener(on_new_message);
+ // 1. Upgrade page to https://
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ kTestURISuccess
+ );
+ // 2. Make an exempt http:// request
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", kTestURIExempt, true);
+ xhr.channel.loadInfo.httpsOnlyStatus |= Ci.nsILoadInfo.HTTPS_ONLY_EXEMPT;
+ xhr.send();
+ // 3. Make Websocket request
+ new WebSocket("ws://does.not.exist");
+
+ await BrowserTestUtils.waitForCondition(() => tests.length === 0);
+
+ // Clean up
+ Services.console.unregisterListener(on_new_message);
+});
+
+function on_new_message(msgObj) {
+ const message = msgObj.message;
+ const logLevel = msgObj.logLevel;
+
+ // Bools about message and pref
+ const isMCL2Enabled = Services.prefs.getBoolPref(UPGRADE_DISPLAY_CONTENT);
+ const isHTTPSOnlyModeLog = message.includes("HTTPS-Only Mode:");
+ const isMCLog = message.includes("Mixed Content:");
+
+ // Check for messages about HTTPS-only upgrades (those should be unrelated to mixed content upgrades)
+ // or for mixed content upgrades which should only occur if security.mixed_content.upgrade_display_content is enabled
+ // (unrelated to https-only logs).
+ if (
+ (isHTTPSOnlyModeLog && !isMCLog) ||
+ (isMCLog && isMCL2Enabled && !isHTTPSOnlyModeLog)
+ ) {
+ for (let i = 0; i < tests.length; i++) {
+ const testCase = tests[i];
+ // If security.mixed_content.upgrade_display_content is enabled, the mixed content control mechanism is upgrading file2.jpg
+ // and HTTPS-Only mode is not failing upgrading file2.jpg, so it won't be logged.
+ // so skip last test case
+ if (
+ testCase.description ==
+ "Sub-Resource upgrade failure for file_2 should get logged" &&
+ isMCL2Enabled
+ ) {
+ tests.splice(i, 1);
+ continue;
+ }
+ // Check if log-level matches
+ if (logLevel !== testCase.expectLogLevel) {
+ continue;
+ }
+ // Check if all substrings are included
+ if (testCase.expectIncludes.some(str => !message.includes(str))) {
+ continue;
+ }
+ ok(true, testCase.description);
+ tests.splice(i, 1);
+ break;
+ }
+ }
+}
diff --git a/dom/security/test/https-only/browser_continue_button_delay.js b/dom/security/test/https-only/browser_continue_button_delay.js
new file mode 100644
index 0000000000..6bdee1610e
--- /dev/null
+++ b/dom/security/test/https-only/browser_continue_button_delay.js
@@ -0,0 +1,59 @@
+"use strict";
+
+function waitForEnabledButton() {
+ return new Promise(resolve => {
+ const button = content.document.getElementById("openInsecure");
+ const observer = new content.MutationObserver(mutations => {
+ for (const mutation of mutations) {
+ if (
+ mutation.type === "attributes" &&
+ mutation.attributeName === "inert" &&
+ !mutation.target.inert
+ ) {
+ resolve();
+ }
+ }
+ });
+ observer.observe(button, { attributeFilter: ["inert"] });
+ ok(
+ button.inert,
+ "The 'Continue to HTTP Site' button should be inert right after the error page is loaded."
+ );
+ });
+}
+
+add_task(async function () {
+ waitForExplicitFinish();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_only_mode", true]],
+ });
+
+ const specifiedDelay = Services.prefs.getIntPref(
+ "security.dialog_enable_delay",
+ 1000
+ );
+
+ let loaded = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
+ info("Loading insecure page");
+ const startTime = Date.now();
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser,
+ // We specifically want a insecure url here that will fail to upgrade
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://untrusted.example.com:80"
+ );
+ await loaded;
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], waitForEnabledButton);
+ const endTime = Date.now();
+
+ const observedDelay = endTime - startTime;
+
+ Assert.greater(
+ observedDelay,
+ specifiedDelay - 100,
+ `The observed delay (${observedDelay}ms) should be roughly the same or greater than the delay specified in "security.dialog_enable_delay" (${specifiedDelay}ms)`
+ );
+
+ finish();
+});
diff --git a/dom/security/test/https-only/browser_cors_mixedcontent.js b/dom/security/test/https-only/browser_cors_mixedcontent.js
new file mode 100644
index 0000000000..fb78d66979
--- /dev/null
+++ b/dom/security/test/https-only/browser_cors_mixedcontent.js
@@ -0,0 +1,127 @@
+// Bug 1659505 - Https-Only: CORS and MixedContent tests
+// https://bugzilla.mozilla.org/bug/1659505
+"use strict";
+
+// > How does this test work?
+// We open a page, that makes two fetch-requests to example.com (same-origin)
+// and example.org (cross-origin). When both fetch-calls have either failed or
+// succeeded, the site dispatches an event with the results.
+
+add_task(async function () {
+ // HTTPS-Only Mode disabled
+ await runTest({
+ description: "Load site with HTTP and HOM disabled",
+ topLevelScheme: "http",
+
+ expectedSameOrigin: "success", // ok
+ expectedCrossOrigin: "error", // CORS
+ });
+ await runTest({
+ description: "Load site with HTTPS and HOM disabled",
+ topLevelScheme: "https",
+
+ expectedSameOrigin: "error", // Mixed Content
+ expectedCrossOrigin: "error", // Mixed Content
+ });
+
+ // HTTPS-Only Mode disabled and MixedContent blocker disabled
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.mixed_content.block_active_content", false]],
+ });
+ await runTest({
+ description: "Load site with HTTPS; HOM and MixedContent blocker disabled",
+ topLevelScheme: "https",
+
+ expectedSameOrigin: "error", // CORS
+ expectedCrossOrigin: "error", // CORS
+ });
+ await SpecialPowers.popPrefEnv();
+
+ // HTTPS-Only Mode enabled, no exception
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_only_mode", true]],
+ });
+ await runTest({
+ description: "Load site with HTTP and HOM enabled",
+ topLevelScheme: "http",
+
+ expectedSameOrigin: "success", // ok
+ expectedCrossOrigin: "error", // CORS
+ });
+
+ // HTTPS-Only enabled, with exception
+ await SpecialPowers.pushPermissions([
+ {
+ type: "https-only-load-insecure",
+ allow: true,
+ context: "http://example.com",
+ },
+ ]);
+
+ await runTest({
+ description: "Load site with HTTP, HOM enabled but site exempt",
+ topLevelScheme: "http",
+
+ expectedSameOrigin: "success", // ok
+ expectedCrossOrigin: "error", // CORS
+ });
+
+ await SpecialPowers.popPermissions();
+ await SpecialPowers.pushPermissions([
+ {
+ type: "https-only-load-insecure",
+ allow: true,
+ context: "https://example.com",
+ },
+ ]);
+ await runTest({
+ description: "Load site with HTTPS, HOM enabled but site exempt",
+ topLevelScheme: "https",
+
+ expectedSameOrigin: "error", // Mixed Content
+ expectedCrossOrigin: "error", // Mixed Content
+ });
+
+ // Remove permission again (has to be done manually for some reason?)
+ await SpecialPowers.popPermissions();
+});
+
+const SERVER_URL = scheme =>
+ `${scheme}://example.com/browser/dom/security/test/https-only/file_cors_mixedcontent.html`;
+
+async function runTest(test) {
+ await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ let loaded = BrowserTestUtils.browserLoaded(browser);
+
+ BrowserTestUtils.startLoadingURIString(
+ browser,
+ SERVER_URL(test.topLevelScheme)
+ );
+
+ await loaded;
+
+ // eslint-disable-next-line no-shadow
+ await SpecialPowers.spawn(browser, [test], async function (test) {
+ const promise = new Promise(resolve => {
+ content.addEventListener("FetchEnded", resolve, {
+ once: true,
+ });
+ });
+
+ content.dispatchEvent(new content.Event("StartFetch"));
+
+ const { detail } = await promise;
+
+ is(
+ detail.comResult,
+ test.expectedSameOrigin,
+ `${test.description} (same-origin)`
+ );
+ is(
+ detail.orgResult,
+ test.expectedCrossOrigin,
+ `${test.description} (cross-origin)`
+ );
+ });
+ });
+}
diff --git a/dom/security/test/https-only/browser_hsts_host.js b/dom/security/test/https-only/browser_hsts_host.js
new file mode 100644
index 0000000000..858c19865c
--- /dev/null
+++ b/dom/security/test/https-only/browser_hsts_host.js
@@ -0,0 +1,203 @@
+// Bug 1722489 - HTTPS-Only Mode - Tests evaluation order
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1722489
+// This test ensures that an http request to an hsts host
+// gets upgraded by hsts and not by https-only.
+"use strict";
+
+// Set bools to track that tests ended.
+let readMessage = false;
+let testFinished = false;
+// Visit a secure site that sends an HSTS header to set up the rest of the
+// test.
+add_task(async function see_hsts_header() {
+ let setHstsUrl =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "hsts_headers.sjs";
+ Services.obs.addObserver(observer, "http-on-examine-response");
+
+ let promiseLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ setHstsUrl
+ );
+ BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, setHstsUrl);
+ await promiseLoaded;
+
+ await BrowserTestUtils.waitForCondition(() => readMessage);
+ // Clean up
+ Services.obs.removeObserver(observer, "http-on-examine-response");
+});
+
+// Test that HTTPS_Only is not performed if HSTS host is visited.
+add_task(async function () {
+ // A longer timeout is necessary for this test than the plain mochitests
+ // due to opening a new tab with the web console.
+ requestLongerTimeout(4);
+
+ // Enable HTTPS-Only Mode and register console-listener
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_only_mode", true]],
+ });
+
+ Services.console.registerListener(onNewMessage);
+ const RESOURCE_LINK =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+ ) + "hsts_headers.sjs";
+
+ // 1. Upgrade page to https://
+ let promiseLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ RESOURCE_LINK
+ );
+ await promiseLoaded;
+
+ await BrowserTestUtils.waitForCondition(() => testFinished);
+
+ // Clean up
+ Services.console.unregisterListener(onNewMessage);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+// Test that when clicking on #fragment with a different scheme (http vs https)
+// DOES cause an actual navigation with HSTS, even though https-only mode is
+// enabled.
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.security.https_only_mode", true],
+ [
+ "dom.security.https_only_mode_break_upgrade_downgrade_endless_loop",
+ false,
+ ],
+ ],
+ });
+
+ const TEST_PAGE =
+ "http://example.com/browser/dom/security/test/https-only/file_fragment_noscript.html";
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ waitForLoad: true,
+ },
+ async function (browser) {
+ const UPGRADED_URL = TEST_PAGE.replace("http:", "https:");
+
+ await SpecialPowers.spawn(browser, [UPGRADED_URL], async function (url) {
+ is(content.window.location.href, url);
+
+ content.window.addEventListener("scroll", () => {
+ ok(false, "scroll event should not trigger");
+ });
+
+ let beforeUnload = new Promise(resolve => {
+ content.window.addEventListener("beforeunload", resolve, {
+ once: true,
+ });
+ });
+
+ content.window.document.querySelector("#clickMeButton").click();
+
+ // Wait for unload event.
+ await beforeUnload;
+ });
+
+ await BrowserTestUtils.browserLoaded(browser);
+
+ await SpecialPowers.spawn(browser, [UPGRADED_URL], async function (url) {
+ is(content.window.location.href, url + "#foo");
+ });
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function () {
+ // Reset HSTS header
+ readMessage = false;
+ let clearHstsUrl =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "hsts_headers.sjs?reset";
+
+ Services.obs.addObserver(observer, "http-on-examine-response");
+ // reset hsts header
+ let promiseLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ clearHstsUrl
+ );
+ await BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ clearHstsUrl
+ );
+ await promiseLoaded;
+ await BrowserTestUtils.waitForCondition(() => readMessage);
+ // Clean up
+ Services.obs.removeObserver(observer, "http-on-examine-response");
+});
+
+function observer(subject, topic, state) {
+ info("observer called with " + topic);
+ if (topic == "http-on-examine-response") {
+ onExamineResponse(subject);
+ }
+}
+
+function onExamineResponse(subject) {
+ let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+ // If message was already read or is not related to "example.com",
+ // don't examine it.
+ if (!channel.URI.spec.includes("example.com") || readMessage) {
+ return;
+ }
+ info("onExamineResponse with " + channel.URI.spec);
+ if (channel.URI.spec.includes("reset")) {
+ try {
+ let hsts = channel.getResponseHeader("Strict-Transport-Security");
+ is(hsts, "max-age=0", "HSTS header is not set");
+ } catch (e) {
+ ok(false, "HSTS header still set");
+ }
+ readMessage = true;
+ return;
+ }
+ try {
+ let hsts = channel.getResponseHeader("Strict-Transport-Security");
+ let csp = channel.getResponseHeader("Content-Security-Policy");
+ // Check that HSTS and CSP upgrade headers are set
+ is(hsts, "max-age=60", "HSTS header is set");
+ is(csp, "upgrade-insecure-requests", "CSP header is set");
+ } catch (e) {
+ ok(false, "No header set");
+ }
+ readMessage = true;
+}
+
+function onNewMessage(msgObj) {
+ const message = msgObj.message;
+ // ensure that request is not upgraded HTTPS-Only.
+ if (message.includes("Upgrading insecure request")) {
+ ok(false, "Top-Level upgrade shouldn't get logged");
+ testFinished = true;
+ } else if (
+ message.includes("Upgrading insecure speculative TCP connection")
+ ) {
+ // TODO: Check assertion
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1735683
+ ok(true, "Top-Level upgrade shouldn't get logged");
+ testFinished = true;
+ } else if (gBrowser.selectedBrowser.currentURI.scheme === "https") {
+ ok(true, "Top-Level upgrade shouldn't get logged");
+ testFinished = true;
+ }
+}
diff --git a/dom/security/test/https-only/browser_httpsonly_prefs.js b/dom/security/test/https-only/browser_httpsonly_prefs.js
new file mode 100644
index 0000000000..467ab490e7
--- /dev/null
+++ b/dom/security/test/https-only/browser_httpsonly_prefs.js
@@ -0,0 +1,118 @@
+"use strict";
+
+async function runPrefTest(
+ aHTTPSOnlyPref,
+ aHTTPSOnlyPrefPBM,
+ aExecuteFromPBM,
+ aDesc,
+ aAssertURLStartsWith
+) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.security.https_only_mode", aHTTPSOnlyPref],
+ ["dom.security.https_only_mode_pbm", aHTTPSOnlyPrefPBM],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ await ContentTask.spawn(
+ browser,
+ { aExecuteFromPBM, aDesc, aAssertURLStartsWith },
+ // eslint-disable-next-line no-shadow
+ async function ({ aExecuteFromPBM, aDesc, aAssertURLStartsWith }) {
+ const responseURL = await new Promise(resolve => {
+ let xhr = new XMLHttpRequest();
+ xhr.timeout = 1200;
+ xhr.open("GET", "http://example.com");
+ if (aExecuteFromPBM) {
+ xhr.channel.loadInfo.originAttributes = {
+ privateBrowsingId: 1,
+ };
+ }
+ xhr.onreadystatechange = () => {
+ // We don't care about the result and it's possible that
+ // the requests might even succeed in some testing environments
+ if (
+ xhr.readyState !== XMLHttpRequest.OPENED ||
+ xhr.readyState !== XMLHttpRequest.UNSENT
+ ) {
+ // Let's make sure this function does not get called anymore
+ xhr.onreadystatechange = undefined;
+ resolve(xhr.responseURL);
+ }
+ };
+ xhr.send();
+ });
+ ok(responseURL.startsWith(aAssertURLStartsWith), aDesc);
+ }
+ );
+ });
+}
+
+add_task(async function () {
+ requestLongerTimeout(2);
+
+ await runPrefTest(
+ false,
+ false,
+ false,
+ "Setting no prefs should not upgrade",
+ "http://"
+ );
+
+ await runPrefTest(
+ true,
+ false,
+ false,
+ "Setting aHTTPSOnlyPref should upgrade",
+ "https://"
+ );
+
+ await runPrefTest(
+ false,
+ true,
+ false,
+ "Setting aHTTPSOnlyPrefPBM should not upgrade",
+ "http://"
+ );
+
+ await runPrefTest(
+ false,
+ false,
+ true,
+ "Setting aPBM should not upgrade",
+ "http://"
+ );
+
+ await runPrefTest(
+ true,
+ true,
+ false,
+ "Setting aHTTPSOnlyPref and aHTTPSOnlyPrefPBM should should upgrade",
+ "https://"
+ );
+
+ await runPrefTest(
+ true,
+ false,
+ true,
+ "Setting aHTTPSOnlyPref and aPBM should upgrade",
+ "https://"
+ );
+
+ await runPrefTest(
+ false,
+ true,
+ true,
+ "Setting aHTTPSOnlyPrefPBM and aPBM should upgrade",
+ "https://"
+ );
+
+ await runPrefTest(
+ true,
+ true,
+ true,
+ "Setting aHTTPSOnlyPref and aHTTPSOnlyPrefPBM and aPBM should upgrade",
+ "https://"
+ );
+});
diff --git a/dom/security/test/https-only/browser_httpsonly_speculative_connect.js b/dom/security/test/https-only/browser_httpsonly_speculative_connect.js
new file mode 100644
index 0000000000..d48f27b1a9
--- /dev/null
+++ b/dom/security/test/https-only/browser_httpsonly_speculative_connect.js
@@ -0,0 +1,71 @@
+"use strict";
+
+const TEST_PATH_HTTP = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.org"
+);
+
+let console_messages = [
+ {
+ description: "Speculative Connection should get logged",
+ expectLogLevel: Ci.nsIConsoleMessage.warn,
+ expectIncludes: [
+ "Upgrading insecure speculative TCP connection",
+ "to use",
+ "example.org",
+ "file_httpsonly_speculative_connect.html",
+ ],
+ },
+ {
+ description: "Upgrade should get logged",
+ expectLogLevel: Ci.nsIConsoleMessage.warn,
+ expectIncludes: [
+ "Upgrading insecure request",
+ "to use",
+ "example.org",
+ "file_httpsonly_speculative_connect.html",
+ ],
+ },
+];
+
+function on_new_console_messages(msgObj) {
+ const message = msgObj.message;
+ const logLevel = msgObj.logLevel;
+
+ if (message.includes("HTTPS-Only Mode:")) {
+ for (let i = 0; i < console_messages.length; i++) {
+ const testCase = console_messages[i];
+ // Check if log-level matches
+ if (logLevel !== testCase.expectLogLevel) {
+ continue;
+ }
+ // Check if all substrings are included
+ if (testCase.expectIncludes.some(str => !message.includes(str))) {
+ continue;
+ }
+ ok(true, testCase.description);
+ console_messages.splice(i, 1);
+ break;
+ }
+ }
+}
+
+add_task(async function () {
+ requestLongerTimeout(4);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_only_mode", true]],
+ });
+ Services.console.registerListener(on_new_console_messages);
+
+ let promiseLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ `${TEST_PATH_HTTP}file_httpsonly_speculative_connect.html`
+ );
+ await promiseLoaded;
+
+ await BrowserTestUtils.waitForCondition(() => console_messages.length === 0);
+
+ Services.console.unregisterListener(on_new_console_messages);
+});
diff --git a/dom/security/test/https-only/browser_iframe_test.js b/dom/security/test/https-only/browser_iframe_test.js
new file mode 100644
index 0000000000..eb4c1a97c3
--- /dev/null
+++ b/dom/security/test/https-only/browser_iframe_test.js
@@ -0,0 +1,223 @@
+// Bug 1658264 - Https-Only: HTTPS-Only and iFrames
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1658264
+"use strict";
+
+// > How does this test work?
+// We're sending a request to file_iframe_test.sjs with various
+// browser-configurations. The sjs-file returns a website with two iFrames
+// loading the same sjs-file again. One iFrame is same origin (example.com) and
+// the other cross-origin (example.org) Each request gets saved in a semicolon
+// seperated list of strings. The sjs-file gets initialized with the
+// query-string "setup" and the result string can be polled with "results". Each
+// string has this format: {top/com/org}-{queryString}-{scheme}. In the end
+// we're just checking if all expected requests were recorded and had the
+// correct scheme. Requests that are meant to fail should explicitly not be
+// contained in the list of results.
+
+// The test loads all tabs and evaluates when all have finished loading
+// it may take quite a long time.
+// This requires more twice as much as the default 45 seconds per test:
+requestLongerTimeout(2);
+SimpleTest.requestCompleteLog();
+
+add_task(async function () {
+ await setup();
+
+ // Using this variable to parallelize and collect tests
+ let testSet = [];
+
+ /*
+ * HTTPS-Only Mode disabled
+ */
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_only_mode", false]],
+ });
+
+ // Top-Level scheme: HTTP
+ // NOTE(freddyb): Test case temporarily disabled. See bug 1735565
+ /*testSet.push(
+ runTest({
+ queryString: "test1.1",
+ topLevelScheme: "http",
+
+ expectedTopLevel: "http",
+ expectedSameOrigin: "http",
+ expectedCrossOrigin: "http",
+ })
+ );*/
+ // Top-Level scheme: HTTPS
+ testSet.push(
+ runTest({
+ queryString: "test1.2",
+ topLevelScheme: "https",
+
+ expectedTopLevel: "https",
+ expectedSameOrigin: "fail",
+ expectedCrossOrigin: "fail",
+ })
+ );
+
+ await Promise.all(testSet);
+ testSet = [];
+ /*
+ * HTTPS-Only Mode enabled, no exception
+ */
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_only_mode", true]],
+ });
+
+ // Top-Level scheme: HTTP
+ testSet.push(
+ runTest({
+ queryString: "test2.1",
+ topLevelScheme: "http",
+
+ expectedTopLevel: "https",
+ expectedSameOrigin: "https",
+ expectedCrossOrigin: "https",
+ })
+ );
+ // Top-Level scheme: HTTPS
+ testSet.push(
+ runTest({
+ queryString: "test2.2",
+ topLevelScheme: "https",
+
+ expectedTopLevel: "https",
+ expectedSameOrigin: "https",
+ expectedCrossOrigin: "https",
+ })
+ );
+
+ await Promise.all(testSet);
+ testSet = [];
+
+ /*
+ * HTTPS-Only enabled, with exceptions
+ * for http://example.org and http://example.com
+ */
+ // Exempting example.org (cross-site) should not affect anything
+ await SpecialPowers.pushPermissions([
+ {
+ type: "https-only-load-insecure",
+ allow: true,
+ context: "http://example.org",
+ },
+ ]);
+ await SpecialPowers.pushPermissions([
+ {
+ type: "https-only-load-insecure",
+ allow: true,
+ context: "http://example.com",
+ },
+ ]);
+
+ // Top-Level scheme: HTTP
+ await runTest({
+ queryString: "test3.1",
+ topLevelScheme: "http",
+
+ expectedTopLevel: "http",
+ expectedSameOrigin: "http",
+ expectedCrossOrigin: "http",
+ });
+
+ await SpecialPowers.popPermissions();
+ await SpecialPowers.pushPermissions([
+ {
+ type: "https-only-load-insecure",
+ allow: true,
+ context: "https://example.com",
+ },
+ ]);
+ // Top-Level scheme: HTTPS
+ await runTest({
+ queryString: "test3.2",
+ topLevelScheme: "https",
+
+ expectedTopLevel: "https",
+ expectedSameOrigin: "fail",
+ expectedCrossOrigin: "fail",
+ });
+
+ // Remove permissions again (has to be done manually for some reason?)
+ await SpecialPowers.popPermissions();
+ await SpecialPowers.popPermissions();
+
+ await evaluate();
+});
+
+const SERVER_URL = scheme =>
+ `${scheme}://example.com/browser/dom/security/test/https-only/file_iframe_test.sjs?`;
+let shouldContain = [];
+let shouldNotContain = [];
+
+async function setup() {
+ info(`TEST-CASE-setup - A`);
+ const response = await fetch(SERVER_URL("https") + "setup");
+ info(`TEST-CASE-setup - B`);
+ const txt = await response.text();
+ info(`TEST-CASE-setup - C`);
+ if (txt != "ok") {
+ ok(false, "Failed to setup test server.");
+ finish();
+ }
+}
+
+async function evaluate() {
+ info(`TEST-CASE-evaluate - A`);
+ const response = await fetch(SERVER_URL("https") + "results");
+ info(`TEST-CASE-evaluate - B`);
+ const requestResults = (await response.text()).split(";");
+ info(`TEST-CASE-evaluate - C`);
+
+ shouldContain.map(str =>
+ ok(requestResults.includes(str), `Results should contain '${str}'.`)
+ );
+ shouldNotContain.map(str =>
+ ok(!requestResults.includes(str), `Results shouldn't contain '${str}'.`)
+ );
+}
+
+async function runTest(test) {
+ const queryString = test.queryString;
+ info(`TEST-CASE-${test.queryString} - runTest BEGIN`);
+ await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ browser,
+ false, // includeSubFrames
+ SERVER_URL(test.expectedTopLevel) + queryString,
+ false // maybeErrorPage
+ );
+ BrowserTestUtils.startLoadingURIString(
+ browser,
+ SERVER_URL(test.topLevelScheme) + queryString
+ );
+ info(`TEST-CASE-${test.queryString} - Before 'await loaded'`);
+ await loaded;
+ info(`TEST-CASE-${test.queryString} - After 'await loaded'`);
+ });
+ info(`TEST-CASE-${test.queryString} - After 'await withNewTab'`);
+
+ if (test.expectedTopLevel !== "fail") {
+ shouldContain.push(`top-${queryString}-${test.expectedTopLevel}`);
+ } else {
+ shouldNotContain.push(`top-${queryString}-http`);
+ shouldNotContain.push(`top-${queryString}-https`);
+ }
+
+ if (test.expectedSameOrigin !== "fail") {
+ shouldContain.push(`com-${queryString}-${test.expectedSameOrigin}`);
+ } else {
+ shouldNotContain.push(`com-${queryString}-http`);
+ shouldNotContain.push(`com-${queryString}-https`);
+ }
+
+ if (test.expectedCrossOrigin !== "fail") {
+ shouldContain.push(`org-${queryString}-${test.expectedCrossOrigin}`);
+ } else {
+ shouldNotContain.push(`org-${queryString}-http`);
+ shouldNotContain.push(`org-${queryString}-https`);
+ }
+ info(`TEST-CASE-${test.queryString} - runTest END`);
+}
diff --git a/dom/security/test/https-only/browser_navigation.js b/dom/security/test/https-only/browser_navigation.js
new file mode 100644
index 0000000000..8c4609a57a
--- /dev/null
+++ b/dom/security/test/https-only/browser_navigation.js
@@ -0,0 +1,94 @@
+"use strict";
+
+// For each FIRST_URL_* this test does the following:
+// 1. Navigate to FIRST_URL_*
+// 2. Check if we are on a HTTPS-Only error page
+// 3. Navigate to SECOND_URL
+// 4. Navigate back
+// 5. Check if we are on a HTTPS-Only error page
+
+const FIRST_URL_SECURE = "https://example.com";
+const FIRST_URL_INSECURE_REDIRECT =
+ "http://example.com/browser/dom/security/test/https-only/file_redirect_to_insecure.sjs";
+const FIRST_URL_INSECURE_NOCERT = "http://nocert.example.com";
+const SECOND_URL = "https://example.org";
+
+function waitForPage() {
+ return new Promise(resolve => {
+ BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser).then(resolve);
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(resolve);
+ });
+}
+
+async function verifyErrorPage(expectErrorPage = true) {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [expectErrorPage],
+ async function (_expectErrorPage) {
+ let doc = content.document;
+ let innerHTML = doc.body.innerHTML;
+ let errorPageL10nId = "about-httpsonly-title-alert";
+
+ is(
+ innerHTML.includes(errorPageL10nId) &&
+ doc.documentURI.startsWith("about:httpsonlyerror"),
+ _expectErrorPage,
+ "we should be on the https-only error page"
+ );
+ }
+ );
+}
+
+async function runTest(
+ firstUrl,
+ expectErrorPageOnFirstVisit,
+ expectErrorPageOnSecondVisit
+) {
+ let loaded = waitForPage();
+ info("Loading first page");
+ BrowserTestUtils.startLoadingURIString(gBrowser, firstUrl);
+ await loaded;
+ await verifyErrorPage(expectErrorPageOnFirstVisit);
+
+ loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ info("Navigating to second page");
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [SECOND_URL],
+ async url => (content.location.href = url)
+ );
+ await loaded;
+
+ // Go back one site by clicking the back button
+ loaded = BrowserTestUtils.waitForLocationChange(gBrowser);
+ info("Clicking back button");
+ let backButton = document.getElementById("back-button");
+ backButton.click();
+ await loaded;
+ await verifyErrorPage(expectErrorPageOnSecondVisit);
+}
+
+add_task(async function () {
+ waitForExplicitFinish();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_only_mode", true]],
+ });
+
+ // We don't expect any HTTPS-Only error pages, on the first and second visit of this URL,
+ // since the URL is reachable via https.
+ await runTest(FIRST_URL_SECURE, false, false);
+
+ // Since trying to upgrade this url will result in being redirected again to the insecure
+ // site, we are not able to upgrade it and a HTTPS-Only error page is shown.
+ // This is happening both on the first and second visit.
+ await runTest(FIRST_URL_INSECURE_REDIRECT, true, true);
+
+ // Similar to the previous case, we can not upgrade this URL, since this time it has a
+ // invalid certificate. We would expect a HTTPS-Only error page on both vists, but it is only
+ // shown on the first one, on the second one we get an errror page about the invalid
+ // certificate instead (Bug 1848117).
+ await runTest(FIRST_URL_INSECURE_NOCERT, true, false);
+
+ finish();
+});
diff --git a/dom/security/test/https-only/browser_redirect_tainting.js b/dom/security/test/https-only/browser_redirect_tainting.js
new file mode 100644
index 0000000000..0823ec4658
--- /dev/null
+++ b/dom/security/test/https-only/browser_redirect_tainting.js
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test steps:
+// 1. Load file_redirect_tainting.sjs?html.
+// 2. The server returns an html which loads an image at http://example.net.
+// 3. The image request will be upgraded to HTTPS since HTTPS-only mode is on.
+// 4. In file_redirect_tainting.sjs, we set "Access-Control-Allow-Origin" to
+// the value of the Origin header.
+// 5. If the vlaue does not match, the image won't be loaded.
+async function do_test() {
+ let requestUrl = `https://example.com/browser/dom/security/test/https-only/file_redirect_tainting.sjs?html`;
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: requestUrl,
+ waitForLoad: true,
+ },
+ async function (browser) {
+ let imageLoaded = await SpecialPowers.spawn(browser, [], function () {
+ let image = content.document.getElementById("test_image");
+ return image && image.complete && image.naturalHeight !== 0;
+ });
+ await Assert.ok(imageLoaded, "test_image should be loaded");
+ }
+ );
+}
+
+add_task(async function test_https_only_redirect_tainting() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_only_mode", true]],
+ });
+
+ await do_test();
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/dom/security/test/https-only/browser_save_as.js b/dom/security/test/https-only/browser_save_as.js
new file mode 100644
index 0000000000..309dd69c79
--- /dev/null
+++ b/dom/security/test/https-only/browser_save_as.js
@@ -0,0 +1,187 @@
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js",
+ this
+);
+
+// Using insecure HTTP URL for a test cases around HTTP/HTTPS download interaction
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const HTTP_LINK = `http://example.org/`;
+const HTTPS_LINK = `https://example.org/`;
+const TEST_PATH =
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/browser/dom/security/test/https-only/file_save_as.html";
+
+let MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+const tempDir = createTemporarySaveDirectory();
+MockFilePicker.displayDirectory = tempDir;
+
+add_setup(async function () {
+ info("Setting MockFilePicker.");
+ mockTransferRegisterer.register();
+
+ registerCleanupFunction(function () {
+ mockTransferRegisterer.unregister();
+ MockFilePicker.cleanup();
+ tempDir.remove(true);
+ });
+});
+
+function createTemporarySaveDirectory() {
+ let saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ saveDir.append("testsavedir");
+ saveDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ return saveDir;
+}
+
+function createPromiseForObservingChannel(expectedUrl) {
+ return new Promise(resolve => {
+ let observer = (aSubject, aTopic) => {
+ if (aTopic === "http-on-modify-request") {
+ let httpChannel = aSubject.QueryInterface(Ci.nsIHttpChannel);
+
+ if (httpChannel.URI.spec != expectedUrl) {
+ return;
+ }
+
+ Services.obs.removeObserver(observer, "http-on-modify-request");
+ resolve();
+ }
+ };
+
+ Services.obs.addObserver(observer, "http-on-modify-request");
+ });
+}
+
+function createPromiseForTransferComplete() {
+ return new Promise(resolve => {
+ MockFilePicker.showCallback = fp => {
+ info("MockFilePicker showCallback");
+
+ let fileName = fp.defaultString;
+ let destFile = tempDir.clone();
+ destFile.append(fileName);
+
+ MockFilePicker.setFiles([destFile]);
+ MockFilePicker.filterIndex = 0; // kSaveAsType_Complete
+
+ MockFilePicker.showCallback = null;
+ mockTransferCallback = function (downloadSuccess) {
+ ok(downloadSuccess, "File should have been downloaded successfully");
+ mockTransferCallback = () => {};
+ resolve();
+ };
+ };
+ });
+}
+
+function createPromiseForConsoleError(message) {
+ return new Promise((resolve, reject) => {
+ function listener(msgObj) {
+ let text = msgObj.message;
+ if (text.includes(message)) {
+ info(`Found occurence of '${message}'`);
+ Services.console.unregisterListener(listener);
+ resolve();
+ }
+ }
+ Services.console.registerListener(listener);
+ });
+}
+
+async function runTest(selector, expectedUrl, expectedError) {
+ info(`Open a new tab for testing "Save link as" in context menu.`);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PATH);
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(document, "popupshown");
+
+ let browser = gBrowser.selectedBrowser;
+
+ info("Open the context menu.");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ selector,
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ browser
+ );
+
+ await popupShownPromise;
+
+ let downloadEndPromise = expectedError
+ ? createPromiseForConsoleError(expectedError)
+ : createPromiseForTransferComplete();
+ let observerPromise = createPromiseForObservingChannel(expectedUrl);
+
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+
+ // Select "Save As" option from context menu.
+ let saveElement = document.getElementById(`context-savelink`);
+ info("Triggering the save process.");
+ contextMenu.activateItem(saveElement);
+
+ info("Waiting for the channel.");
+ await observerPromise;
+
+ info(
+ expectedError
+ ? "Waiting for error in console."
+ : "Wait until the save is finished."
+ );
+ await downloadEndPromise;
+
+ info("Wait until the menu is closed.");
+ await popupHiddenPromise;
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+async function setHttpsFirstAndOnlyPrefs(httpsFirst, httpsOnly) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.security.https_first", httpsFirst],
+ ["dom.security.https_only_mode", httpsOnly],
+ ],
+ });
+}
+
+add_task(async function testBaseline() {
+ // Run with HTTPS-First and HTTPS-Only disabled
+ await setHttpsFirstAndOnlyPrefs(false, false);
+ await runTest("#insecure-link", HTTP_LINK, undefined);
+ await runTest("#secure-link", HTTPS_LINK, undefined);
+});
+
+add_task(async function testHttpsFirst() {
+ // Run with HTTPS-First enabled
+ // The the user will get a warning about really wanting to download
+ // from a insecure site, because we upgraded the top level document,
+ // but the download is still insecure. In the future we also want to
+ // upgrade these Save-As downloads.
+ await setHttpsFirstAndOnlyPrefs(true, false);
+ await runTest(
+ "#insecure-link",
+ HTTP_LINK,
+ "Blocked downloading insecure content “http://example.org/â€."
+ );
+ await runTest("#secure-link", HTTPS_LINK, undefined);
+});
+
+add_task(async function testHttpsOnly() {
+ // Run with HTTPS-Only enabled
+ // Should have same behaviour as HTTPS-First
+ await setHttpsFirstAndOnlyPrefs(false, true);
+ await runTest(
+ "#insecure-link",
+ HTTP_LINK,
+ "Blocked downloading insecure content “http://example.org/â€."
+ );
+ await runTest("#secure-link", HTTPS_LINK, undefined);
+});
diff --git a/dom/security/test/https-only/browser_triggering_principal_exemption.js b/dom/security/test/https-only/browser_triggering_principal_exemption.js
new file mode 100644
index 0000000000..09a867a63b
--- /dev/null
+++ b/dom/security/test/https-only/browser_triggering_principal_exemption.js
@@ -0,0 +1,72 @@
+// Bug 1662359 - Don't upgrade subresources whose triggering principal is exempt from HTTPS-Only mode.
+// https://bugzilla.mozilla.org/bug/1662359
+"use strict";
+
+const TRIGGERING_PAGE = "http://example.org";
+const LOADED_RESOURCE = "http://example.com";
+
+add_task(async function () {
+ // Enable HTTPS-Only Mode
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_only_mode", true]],
+ });
+
+ await runTest(
+ "Request with not exempt triggering principal should get upgraded.",
+ "https://"
+ );
+
+ // Now exempt the triggering page
+ await SpecialPowers.pushPermissions([
+ {
+ type: "https-only-load-insecure",
+ allow: true,
+ context: TRIGGERING_PAGE,
+ },
+ ]);
+
+ await runTest(
+ "Request with exempt triggering principal should not get upgraded.",
+ "http://"
+ );
+
+ await SpecialPowers.popPermissions();
+});
+
+async function runTest(desc, startsWith) {
+ const responseURL = await new Promise(resolve => {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", LOADED_RESOURCE);
+
+ // Replace loadinfo with one whose triggeringPrincipal is a content
+ // principal for TRIGGERING_PAGE.
+ const triggeringPrincipal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ TRIGGERING_PAGE
+ );
+ let dummyURI = Services.io.newURI(LOADED_RESOURCE);
+ let dummyChannel = NetUtil.newChannel({
+ uri: dummyURI,
+ triggeringPrincipal,
+ loadingPrincipal: xhr.channel.loadInfo.loadingPrincipal,
+ securityFlags: xhr.channel.loadInfo.securityFlags,
+ contentPolicyType: xhr.channel.loadInfo.externalContentPolicyType,
+ });
+ xhr.channel.loadInfo = dummyChannel.loadInfo;
+
+ xhr.onreadystatechange = () => {
+ // We don't care about the result, just if Firefox upgraded the URL
+ // internally.
+ if (
+ xhr.readyState !== XMLHttpRequest.OPENED ||
+ xhr.readyState !== XMLHttpRequest.UNSENT
+ ) {
+ // Let's make sure this function doesn't get called anymore
+ xhr.onreadystatechange = undefined;
+ resolve(xhr.responseURL);
+ }
+ };
+ xhr.send();
+ });
+ ok(responseURL.startsWith(startsWith), desc);
+}
diff --git a/dom/security/test/https-only/browser_upgrade_exceptions.js b/dom/security/test/https-only/browser_upgrade_exceptions.js
new file mode 100644
index 0000000000..8611b32a0f
--- /dev/null
+++ b/dom/security/test/https-only/browser_upgrade_exceptions.js
@@ -0,0 +1,86 @@
+// Bug 1625448 - HTTPS Only Mode - Exceptions for loopback and local IP addresses
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1631384
+// This test ensures that various configurable upgrade exceptions work
+"use strict";
+
+add_task(async function () {
+ requestLongerTimeout(2);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_only_mode", true]],
+ });
+
+ // Loopback test
+ await runTest(
+ "Loopback IP addresses should always be exempt from upgrades (localhost)",
+ "http://localhost",
+ "http://"
+ );
+ await runTest(
+ "Loopback IP addresses should always be exempt from upgrades (127.0.0.1)",
+ "http://127.0.0.1",
+ "http://"
+ );
+ // Default local-IP and onion tests
+ await runTest(
+ "Local IP addresses should be exempt from upgrades by default",
+ "http://10.0.250.250",
+ "http://"
+ );
+ await runTest(
+ "Hosts ending with .onion should be be exempt from HTTPS-Only upgrades by default",
+ "http://grocery.shopping.for.one.onion",
+ "http://"
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.security.https_only_mode.upgrade_local", true],
+ ["dom.security.https_only_mode.upgrade_onion", true],
+ ],
+ });
+
+ // Local-IP and onion tests with upgrade enabled
+ await runTest(
+ "Local IP addresses should get upgraded when 'dom.security.https_only_mode.upgrade_local' is set to true",
+ "http://10.0.250.250",
+ "https://"
+ );
+ await runTest(
+ "Hosts ending with .onion should get upgraded when 'dom.security.https_only_mode.upgrade_onion' is set to true",
+ "http://grocery.shopping.for.one.onion",
+ "https://"
+ );
+ // Local-IP request with HTTPS_ONLY_EXEMPT flag
+ await runTest(
+ "The HTTPS_ONLY_EXEMPT flag should overrule upgrade-prefs",
+ "http://10.0.250.250",
+ "http://",
+ true
+ );
+});
+
+async function runTest(desc, url, startsWith, exempt = false) {
+ const responseURL = await new Promise(resolve => {
+ let xhr = new XMLHttpRequest();
+ xhr.timeout = 1200;
+ xhr.open("GET", url);
+ if (exempt) {
+ xhr.channel.loadInfo.httpsOnlyStatus |= Ci.nsILoadInfo.HTTPS_ONLY_EXEMPT;
+ }
+ xhr.onreadystatechange = () => {
+ // We don't care about the result and it's possible that
+ // the requests might even succeed in some testing environments
+ if (
+ xhr.readyState !== XMLHttpRequest.OPENED ||
+ xhr.readyState !== XMLHttpRequest.UNSENT
+ ) {
+ // Let's make sure this function doesn't get caled anymore
+ xhr.onreadystatechange = undefined;
+ resolve(xhr.responseURL);
+ }
+ };
+ xhr.send();
+ });
+ ok(responseURL.startsWith(startsWith), desc);
+}
diff --git a/dom/security/test/https-only/browser_upgrade_exemption.js b/dom/security/test/https-only/browser_upgrade_exemption.js
new file mode 100644
index 0000000000..23d857b511
--- /dev/null
+++ b/dom/security/test/https-only/browser_upgrade_exemption.js
@@ -0,0 +1,80 @@
+"use strict";
+
+const PAGE_WITHOUT_SCHEME = "://example.com";
+
+add_task(async function () {
+ // Load a insecure page with HTTPS-Only and HTTPS-First disabled
+ await runTest({
+ loadScheme: "http",
+ expectScheme: "http",
+ });
+
+ // Load a secure page with HTTPS-Only and HTTPS-First disabled
+ await runTest({
+ loadScheme: "https",
+ expectScheme: "https",
+ });
+
+ // Load a exempted insecure page with HTTPS-Only and HTTPS-First disabled
+ await runTest({
+ exempt: true,
+ loadScheme: "http",
+ expectScheme: "http",
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_only_mode", true]],
+ });
+
+ // Load a insecure page with HTTPS-Only enabled
+ await runTest({
+ loadScheme: "http",
+ expectScheme: "https",
+ });
+
+ // Load a exempted insecure page with HTTPS-Only enabled
+ await runTest({
+ exempt: true,
+ loadScheme: "http",
+ expectScheme: "http",
+ });
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_first", true]],
+ });
+
+ // Load a insecure page with HTTPS-First enabled
+ await runTest({
+ loadScheme: "http",
+ expectScheme: "https",
+ });
+
+ // Load a exempted insecure page with HTTPS-First enabled
+ await runTest({
+ exempt: true,
+ loadScheme: "http",
+ expectScheme: "http",
+ });
+});
+
+async function runTest(options) {
+ const { exempt = false, loadScheme, expectScheme } = options;
+ const page = loadScheme + PAGE_WITHOUT_SCHEME;
+
+ if (exempt) {
+ await SpecialPowers.pushPermissions([
+ {
+ type: "https-only-load-insecure",
+ allow: true,
+ context: page,
+ },
+ ]);
+ }
+
+ await BrowserTestUtils.withNewTab(page, async function (browser) {
+ is(browser.currentURI.scheme, expectScheme, "Unexpected scheme");
+ await SpecialPowers.popPermissions();
+ await SpecialPowers.popPrefEnv();
+ });
+}
diff --git a/dom/security/test/https-only/browser_user_gesture.js b/dom/security/test/https-only/browser_user_gesture.js
new file mode 100644
index 0000000000..e7d6a20318
--- /dev/null
+++ b/dom/security/test/https-only/browser_user_gesture.js
@@ -0,0 +1,55 @@
+// Bug 1725026 - HTTPS Only Mode - Test if a load triggered by a user gesture
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1725026
+// Test if a load triggered by a user gesture can be upgraded to HTTPS
+// successfully.
+
+"use strict";
+
+const testPathUpgradeable = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+
+const kTestURI = testPathUpgradeable + "file_user_gesture.html";
+
+add_task(async function () {
+ // Enable HTTPS-Only Mode and register console-listener
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_only_mode", true]],
+ });
+
+ await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ const loaded = BrowserTestUtils.browserLoaded(browser, false, null, true);
+ // 1. Upgrade a page to https://
+ BrowserTestUtils.startLoadingURIString(browser, kTestURI);
+ await loaded;
+ await ContentTask.spawn(browser, {}, async args => {
+ ok(
+ content.document.location.href.startsWith("https://"),
+ "Should be https"
+ );
+
+ // 2. Trigger a load by clicking button.
+ // The scheme of the link url is `http` and the load should be able to
+ // upgraded to `https` because of HTTPS-only mode.
+ let button = content.document.getElementById("httpLinkButton");
+ await EventUtils.synthesizeMouseAtCenter(
+ button,
+ { type: "mousedown" },
+ content
+ );
+ await EventUtils.synthesizeMouseAtCenter(
+ button,
+ { type: "mouseup" },
+ content
+ );
+ await ContentTaskUtils.waitForCondition(() => {
+ return content.document.location.href.startsWith("https://");
+ });
+ ok(
+ content.document.location.href.startsWith("https://"),
+ "Should be https"
+ );
+ });
+ });
+});
diff --git a/dom/security/test/https-only/browser_websocket_exceptions.js b/dom/security/test/https-only/browser_websocket_exceptions.js
new file mode 100644
index 0000000000..a6e5336c63
--- /dev/null
+++ b/dom/security/test/https-only/browser_websocket_exceptions.js
@@ -0,0 +1,68 @@
+"use strict";
+
+const TEST_PATH_HTTP = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://localhost:9898"
+);
+
+let WEBSOCKET_DOC_URL = `${TEST_PATH_HTTP}file_websocket_exceptions.html`;
+
+add_task(async function () {
+ // Here is a sequence of how this test works:
+ // 1. Dynamically inject a localhost iframe
+ // 2. Add an exemption for localhost
+ // 3. Fire up Websocket
+ // Generally local IP addresses are exempt from https-only, but if we do not add
+ // an exemption for localhost, then the TriggeringPrincipal of the WebSocket is
+ // `not` exempt and we would upgrade ws to wss.
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.security.https_only_mode", true],
+ ["network.proxy.allow_hijacking_localhost", true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ let loaded = BrowserTestUtils.browserLoaded(browser);
+
+ BrowserTestUtils.startLoadingURIString(browser, WEBSOCKET_DOC_URL);
+ await loaded;
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ // Part 1:
+ let myIframe = content.document.createElement("iframe");
+ content.document.body.appendChild(myIframe);
+ myIframe.src =
+ "http://localhost:9898/browser/dom/security/test/https-only/file_websocket_exceptions_iframe.html";
+
+ myIframe.onload = async function () {
+ // Part 2:
+ await SpecialPowers.pushPermissions([
+ {
+ type: "https-only-load-insecure",
+ allow: true,
+ context: "http://localhost:9898",
+ },
+ ]);
+ // Part 3.
+ myIframe.contentWindow.postMessage({ myMessage: "runWebSocket" }, "*");
+ };
+
+ const promise = new Promise(resolve => {
+ content.addEventListener("WebSocketEnded", resolve, {
+ once: true,
+ });
+ });
+
+ const { detail } = await promise;
+
+ is(detail.state, "onopen", "sanity: websocket loaded");
+ ok(
+ detail.url.startsWith("ws://example.com/tests"),
+ "exempt websocket should not be upgraded to wss://"
+ );
+ });
+ });
+ await SpecialPowers.popPermissions();
+});
diff --git a/dom/security/test/https-only/file_background_redirect.sjs b/dom/security/test/https-only/file_background_redirect.sjs
new file mode 100644
index 0000000000..45e01797e3
--- /dev/null
+++ b/dom/security/test/https-only/file_background_redirect.sjs
@@ -0,0 +1,32 @@
+// Custom *.sjs file specifically for the needs of Bug 1683015
+"use strict";
+
+let { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+async function handleRequest(request, response) {
+ // avoid confusing cache behaviour
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+
+ let query = request.queryString;
+ if (request.scheme === "https" && query === "start") {
+ // Simulating long repsonse time by processing the https request
+ // using a 5 seconds delay. Note that the http background request
+ // gets send after 3 seconds
+ response.processAsync();
+ setTimeout(() => {
+ response.setStatusLine("1.1", 200, "OK");
+ response.write(
+ "<html><body>Test Page for Bug 1683015 loaded</body></html>"
+ );
+ response.finish();
+ }, 5000); /* wait 5 seconds */
+ return;
+ }
+
+ // we should never get here, but just in case return something unexpected
+ response.setStatusLine("1.1", 404, "Not Found");
+ response.write("<html><body>SHOULDN'T DISPLAY THIS PAGE</body></html>");
+}
diff --git a/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs b/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs
new file mode 100644
index 0000000000..a8a9083ef3
--- /dev/null
+++ b/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs
@@ -0,0 +1,67 @@
+// Custom *.sjs file specifically for the needs of Bug 1691888
+"use strict";
+
+const REDIRECT_META = `
+ <html>
+ <head>
+ <meta http-equiv="refresh" content="0; url='http://example.com/tests/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs?test1b'">
+ </head>
+ <body>
+ META REDIRECT
+ </body>
+ </html>`;
+
+const REDIRECT_JS = `
+ <html>
+ <body>
+ JS REDIRECT
+ <script>
+ let url= "http://example.com/tests/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs?test2b";
+ window.location = url;
+ </script>
+ </body>
+ </html>`;
+
+const REDIRECT_302 =
+ "http://example.com/tests/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs?test3b";
+
+const REDIRECT_302_DIFFERENT_PATH =
+ "http://example.com/tests/dom/security/test/https-only/file_user_gesture.html";
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviour
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+
+ // if the scheme is not https, meaning that the initial request did not
+ // get upgraded, then we rather fall through and display unexpected content.
+ if (request.scheme === "https") {
+ let query = request.queryString;
+
+ if (query === "test1a") {
+ response.write(REDIRECT_META);
+ return;
+ }
+
+ if (query === "test2a") {
+ response.write(REDIRECT_JS);
+ return;
+ }
+
+ if (query === "test3a") {
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", REDIRECT_302, false);
+ return;
+ }
+
+ if (query === "test4a") {
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", REDIRECT_302_DIFFERENT_PATH, false);
+ return;
+ }
+ }
+
+ // we should never get here, just in case,
+ // let's return something unexpected
+ response.write("<html><body>DO NOT DISPLAY THIS</body></html>");
+}
diff --git a/dom/security/test/https-only/file_bug1874801.html b/dom/security/test/https-only/file_bug1874801.html
new file mode 100644
index 0000000000..58c2f03c81
--- /dev/null
+++ b/dom/security/test/https-only/file_bug1874801.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Bug 1874801</title>
+</head>
+<body>
+ <img src="http://example.com/browser/dom/security/test/https-only/file_bug1874801.sjs">
+</body>
+</html>
diff --git a/dom/security/test/https-only/file_bug1874801.sjs b/dom/security/test/https-only/file_bug1874801.sjs
new file mode 100644
index 0000000000..ce84af1d5f
--- /dev/null
+++ b/dom/security/test/https-only/file_bug1874801.sjs
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+ if (request.scheme === "https") {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "image/svg+xml");
+ response.write(
+ `<svg version="1.1" width="100" height="40" xmlns="http://www.w3.org/2000/svg"><text x="20" y="20">HTTPS</text></svg>`
+ );
+ return;
+ }
+ if (request.scheme === "http") {
+ response.setStatusLine(request.httpVersion, 400, "Bad Request");
+ }
+}
diff --git a/dom/security/test/https-only/file_console_logging.html b/dom/security/test/https-only/file_console_logging.html
new file mode 100644
index 0000000000..94e07cddd9
--- /dev/null
+++ b/dom/security/test/https-only/file_console_logging.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1625448 - HTTPS Only Mode - Tests for console logging</title>
+</head>
+<body>
+ <!-- These files don't exist since we only care if the webserver can respond. -->
+ <!-- This request can get upgraded. -->
+ <img src="http://example.com/file_1.jpg">
+ <!-- This request can't get upgraded -->
+ <img src="http://self-signed.example.com/file_2.jpg">
+
+ <iframe src="http://self-signed.example.com/browser/dom/security/test/https-only/file_console_logging.html"></iframe>
+</body>
+</html>
diff --git a/dom/security/test/https-only/file_cors_mixedcontent.html b/dom/security/test/https-only/file_cors_mixedcontent.html
new file mode 100644
index 0000000000..50d32954ef
--- /dev/null
+++ b/dom/security/test/https-only/file_cors_mixedcontent.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+
+ <script>
+ addEventListener("StartFetch", async function() {
+ let comResult;
+ let orgResult;
+
+ await Promise.all([
+ fetch("http://example.com/")
+ .then(() => {
+ comResult = "success";
+ })
+ .catch(() => {
+ comResult = "error";
+ }),
+ fetch("http://example.org/")
+ .then(() => {
+ orgResult = "success";
+ })
+ .catch(() => {
+ orgResult = "error";
+ }),
+ ]);
+
+ window.dispatchEvent(new CustomEvent("FetchEnded", {
+ detail: { comResult, orgResult }
+ }));
+
+ })
+ </script>
+</head>
+
+<body>
+ <h2>Https-Only: CORS and MixedContent tests</h2>
+ <p><a href="https://bugzilla.mozilla.org/bug/1659505">Bug 1659505</a></p>
+</body>
+
+</html>
diff --git a/dom/security/test/https-only/file_fragment.html b/dom/security/test/https-only/file_fragment.html
new file mode 100644
index 0000000000..49cff76f9a
--- /dev/null
+++ b/dom/security/test/https-only/file_fragment.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<script>
+// sends url of current window and onbeforeunloadMessage
+// when we enter here test failed.
+function beforeunload(){
+ window.opener.postMessage({
+ info: "before-unload",
+ result: window.location.hash,
+ button: false,
+ }, "*");
+}
+
+// sends url of current window and then click on button
+window.onload = function (){
+ // get button by ID
+ let button = window.document.getElementById("clickMeButton");
+ // if getting button was successful, buttonExist = true
+ let buttonExist = button !== null;
+ // send loading message
+ window.opener.postMessage({
+ info: "onload",
+ result: window.location.href,
+ button: buttonExist,
+ }, "*");
+ // click button
+ button.click();
+}
+// after button clicked and paged scrolled sends URL of current window
+window.onscroll = function(){
+ window.opener.postMessage({
+ info: "scrolled-to-foo",
+ result: window.location.href,
+ button: true,
+ documentURI: document.documentURI,
+ }, "*");
+ }
+
+
+</script>
+<body onbeforeunload="/*just to notify if we load a new page*/ beforeunload()";>
+ <a id="clickMeButton" href="http://example.com/tests/dom/security/test/https-only/file_fragment.html#foo">Click me</a>
+ <div style="height: 1000px; border: 1px solid black;"> space</div>
+ <a name="foo" href="http://example.com/tests/dom/security/test/https-only/file_fragment.html">foo</a>
+ <div style="height: 1000px; border: 1px solid black;">space</div>
+</body>
+</html>
diff --git a/dom/security/test/https-only/file_fragment_noscript.html b/dom/security/test/https-only/file_fragment_noscript.html
new file mode 100644
index 0000000000..609961fd98
--- /dev/null
+++ b/dom/security/test/https-only/file_fragment_noscript.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+ <a id="clickMeButton" href="http://example.com/browser/dom/security/test/https-only/file_fragment_noscript.html#foo">Click me</a>
+ <div style="height: 1000px; border: 1px solid black;"> space</div>
+ <a name="foo" href="http://example.com/browser/dom/security/test/https-only/file_fragment_noscript.html">foo</a>
+ <div style="height: 1000px; border: 1px solid black;">space</div>
+</body>
+</html>
diff --git a/dom/security/test/https-only/file_http_background_auth_request.sjs b/dom/security/test/https-only/file_http_background_auth_request.sjs
new file mode 100644
index 0000000000..80fc8ffcf7
--- /dev/null
+++ b/dom/security/test/https-only/file_http_background_auth_request.sjs
@@ -0,0 +1,16 @@
+// Custom *.sjs file specifically for the needs of Bug 1665062
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ if (request.scheme === "https") {
+ response.setHeader("Content-Type", "text/html;charset=utf-8", false);
+ response.setStatusLine(request.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", 'Basic realm="bug1665062"');
+ return;
+ }
+
+ // we should never get here; just in case, return something unexpected
+ response.write("do'h");
+}
diff --git a/dom/security/test/https-only/file_http_background_request.sjs b/dom/security/test/https-only/file_http_background_request.sjs
new file mode 100644
index 0000000000..ef0e2ce5bd
--- /dev/null
+++ b/dom/security/test/https-only/file_http_background_request.sjs
@@ -0,0 +1,15 @@
+// Custom *.sjs file specifically for the needs of Bug 1663396
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ if (request.scheme === "https") {
+ // Simulating a timeout by processing the https request
+ // async and *never* return anything!
+ response.processAsync();
+ return;
+ }
+ // we should never get here; just in case, return something unexpected
+ response.write("do'h");
+}
diff --git a/dom/security/test/https-only/file_httpsonly_speculative_connect.html b/dom/security/test/https-only/file_httpsonly_speculative_connect.html
new file mode 100644
index 0000000000..46a10401f9
--- /dev/null
+++ b/dom/security/test/https-only/file_httpsonly_speculative_connect.html
@@ -0,0 +1 @@
+<html><body>dummy file for speculative https-only upgrade test</body></html>
diff --git a/dom/security/test/https-only/file_iframe_test.sjs b/dom/security/test/https-only/file_iframe_test.sjs
new file mode 100644
index 0000000000..611870c87c
--- /dev/null
+++ b/dom/security/test/https-only/file_iframe_test.sjs
@@ -0,0 +1,58 @@
+// Bug 1658264 - HTTPS-Only and iFrames
+// see browser_iframe_test.js
+
+const IFRAME_CONTENT = `
+<!DOCTYPE HTML>
+<html>
+ <head><meta charset="utf-8"></head>
+ <body>Helo Friend!</body>
+</html>`;
+const DOCUMENT_CONTENT = q => `
+<!DOCTYPE HTML>
+<html>
+ <head><meta charset="utf-8"></head>
+ <body>
+ <iframe src="http://example.com/browser/dom/security/test/https-only/file_iframe_test.sjs?com-${q}"></iframe>
+ <iframe src="http://example.org/browser/dom/security/test/https-only/file_iframe_test.sjs?org-${q}"></iframe>
+ </body>
+</html>`;
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+ let queryString = request.queryString;
+ const queryScheme = request.scheme;
+
+ // Setup the state with an empty string and return "ok"
+ if (queryString == "setup") {
+ setState("receivedQueries", "");
+ response.write("ok");
+ return;
+ }
+
+ let receivedQueries = getState("receivedQueries");
+
+ // Return result-string
+ if (queryString == "results") {
+ response.write(receivedQueries);
+ return;
+ }
+
+ // Add semicolon to seperate strings
+ if (receivedQueries !== "") {
+ receivedQueries += ";";
+ }
+
+ // Requests from iFrames start with com or org
+ if (queryString.startsWith("com-") || queryString.startsWith("org-")) {
+ receivedQueries += queryString;
+ setState("receivedQueries", `${receivedQueries}-${queryScheme}`);
+ response.write(IFRAME_CONTENT);
+ return;
+ }
+
+ // Everything else has to be a top-level request
+ receivedQueries += `top-${queryString}`;
+ setState("receivedQueries", `${receivedQueries}-${queryScheme}`);
+ response.write(DOCUMENT_CONTENT(queryString));
+}
diff --git a/dom/security/test/https-only/file_insecure_reload.sjs b/dom/security/test/https-only/file_insecure_reload.sjs
new file mode 100644
index 0000000000..a48ec4e20f
--- /dev/null
+++ b/dom/security/test/https-only/file_insecure_reload.sjs
@@ -0,0 +1,27 @@
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1702001
+
+// An onload postmessage to window opener
+const ON_LOAD = `
+ <html>
+ <body>
+ send onload message...
+ <script type="application/javascript">
+ window.opener.postMessage({result: 'you entered the http page', historyLength: history.length}, '*');
+ </script>
+ </body>
+ </html>`;
+
+// When an https request is sent, cause a timeout so that the https-only error
+// page is displayed.
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+ if (request.scheme === "https") {
+ // Simulating a timeout by processing the https request
+ // async and *never* return anything!
+ response.processAsync();
+ return;
+ }
+ if (request.scheme === "http") {
+ response.write(ON_LOAD);
+ }
+}
diff --git a/dom/security/test/https-only/file_redirect.sjs b/dom/security/test/https-only/file_redirect.sjs
new file mode 100644
index 0000000000..c66cbaa226
--- /dev/null
+++ b/dom/security/test/https-only/file_redirect.sjs
@@ -0,0 +1,37 @@
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1613063
+
+// Step 1. Send request with redirect queryString (eg. file_redirect.sjs?302)
+// Step 2. Server responds with corresponding redirect code to http://example.com/../file_redirect.sjs?check
+// Step 3. Response from ?check indicates whether the redirected request was secure or not.
+
+const RESPONSE_SECURE = "secure-ok";
+const RESPONSE_INSECURE = "secure-error";
+const RESPONSE_ERROR = "unexpected-query";
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ const query = request.queryString;
+
+ // Send redirect header
+ if ((query >= 301 && query <= 303) || query == 307) {
+ const loc =
+ "http://example.com/tests/dom/security/test/https-only/file_redirect.sjs?check";
+ response.setStatusLine(request.httpVersion, query, "Moved");
+ response.setHeader("Location", loc, false);
+ return;
+ }
+
+ // Check if scheme is http:// oder https://
+ if (query == "check") {
+ const secure =
+ request.scheme == "https" ? RESPONSE_SECURE : RESPONSE_INSECURE;
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(secure);
+ return;
+ }
+
+ // This should not happen
+ response.setStatusLine(request.httpVersion, 500, "OK");
+ response.write(RESPONSE_ERROR);
+}
diff --git a/dom/security/test/https-only/file_redirect_tainting.sjs b/dom/security/test/https-only/file_redirect_tainting.sjs
new file mode 100644
index 0000000000..9ecca88d6d
--- /dev/null
+++ b/dom/security/test/https-only/file_redirect_tainting.sjs
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// small red image
+const IMG_BYTES = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
+ "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="
+);
+
+const body = `<!DOCTYPE html>
+<html lang="en">
+ <body>
+ <script>
+ let image = new Image();
+ image.crossOrigin = "anonymous";
+ image.src = "http://example.net/browser/dom/security/test/https-only/file_redirect_tainting.sjs?img";
+ image.id = "test_image";
+ document.body.appendChild(image);
+ </script>
+ </body>
+</html>`;
+
+function handleRequest(request, response) {
+ if (request.queryString === "html") {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html");
+ response.write(body);
+ return;
+ }
+
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "image/png");
+ let origin = request.getHeader("Origin");
+ response.setHeader("Access-Control-Allow-Origin", origin);
+ response.write(IMG_BYTES);
+}
diff --git a/dom/security/test/https-only/file_redirect_to_insecure.sjs b/dom/security/test/https-only/file_redirect_to_insecure.sjs
new file mode 100644
index 0000000000..ea88223926
--- /dev/null
+++ b/dom/security/test/https-only/file_redirect_to_insecure.sjs
@@ -0,0 +1,16 @@
+// Redirect back to http if visited via https. This way we can simulate
+// a site which can not be upgraded by HTTPS-Only.
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+ if (request.scheme === "https") {
+ response.setStatusLine(request.httpVersion, "302", "Found");
+ response.setHeader(
+ "Location",
+ // We explicitly want a insecure URL here, so disable eslint
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ `http://${request.host}${request.path}`,
+ false
+ );
+ }
+}
diff --git a/dom/security/test/https-only/file_save_as.html b/dom/security/test/https-only/file_save_as.html
new file mode 100644
index 0000000000..44232b16ec
--- /dev/null
+++ b/dom/security/test/https-only/file_save_as.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+</head>
+<body>
+ <a href="http://example.org" id="insecure-link">Insecure Link</a>
+ <a href="https://example.org" id="secure-link">Secure Link</a>
+</body>
+</html> \ No newline at end of file
diff --git a/dom/security/test/https-only/file_upgrade_insecure.html b/dom/security/test/https-only/file_upgrade_insecure.html
new file mode 100644
index 0000000000..346cfbeb9c
--- /dev/null
+++ b/dom/security/test/https-only/file_upgrade_insecure.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1613063 - HTTPS Only Mode</title>
+ <!-- style -->
+ <link rel='stylesheet' type='text/css' href='http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?style' media='screen' />
+
+ <!-- font -->
+ <style>
+ @font-face {
+ font-family: "foofont";
+ src: url('http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?font');
+ }
+ .div_foo { font-family: "foofont"; }
+ </style>
+</head>
+<body>
+
+ <!-- images: -->
+ <img src="http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?img"></img>
+
+ <!-- redirects: upgrade http:// to https:// redirect to http:// and then upgrade to https:// again -->
+ <img src="http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?redirect-image"></img>
+
+ <!-- script: -->
+ <script src="http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?script"></script>
+
+ <!-- media: -->
+ <audio src="http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?media"></audio>
+
+ <!-- objects: -->
+ <object width="10" height="10" data="http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?object"></object>
+
+ <!-- font: (apply font loaded in header to div) -->
+ <div class="div_foo">foo</div>
+
+ <!-- iframe: (same origin) -->
+ <iframe src="http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?iframe">
+ <!-- within that iframe we load an image over http and make sure the requested gets upgraded to https -->
+ </iframe>
+
+ <!-- xhr: -->
+ <script type="application/javascript">
+ var myXHR = new XMLHttpRequest();
+ myXHR.open("GET", "http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?xhr");
+ myXHR.send(null);
+ </script>
+
+ <!-- websockets: upgrade ws:// to wss://-->
+ <script type="application/javascript">
+ // WebSocket tests are not supported on Android yet. Bug 1566168
+ const { AppConstants } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+ );
+ if (AppConstants.platform !== "android") {
+ var mySocket = new WebSocket("ws://example.com/tests/dom/security/test/https-only/file_upgrade_insecure");
+ mySocket.onopen = function(e) {
+ if (mySocket.url.includes("wss://")) {
+ window.parent.postMessage({result: "websocket-ok"}, "*");
+ }
+ else {
+ window.parent.postMessage({result: "websocket-error"}, "*");
+ }
+ mySocket.close();
+ };
+ mySocket.onerror = function(e) {
+ // debug information for Bug 1316305
+ dump(" xxx mySocket.onerror: (mySocket): " + mySocket + "\n");
+ dump(" xxx mySocket.onerror: (mySocket.url): " + mySocket.url + "\n");
+ dump(" xxx mySocket.onerror: (e): " + e + "\n");
+ dump(" xxx mySocket.onerror: (e.message): " + e.message + "\n");
+ dump(" xxx mySocket.onerror: This might be related to Bug 1316305!\n");
+ window.parent.postMessage({result: "websocket-unexpected-error"}, "*");
+ };
+ }
+ </script>
+
+ <!-- form action: (upgrade POST from http:// to https://) -->
+ <iframe name='formFrame' id='formFrame'></iframe>
+ <form target="formFrame" action="http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?form" method="POST">
+ <input name="foo" value="foo">
+ <input type="submit" id="submitButton" formenctype='multipart/form-data' value="Submit form">
+ </form>
+ <script type="text/javascript">
+ var submitButton = document.getElementById('submitButton');
+ submitButton.click();
+ </script>
+
+</body>
+</html>
diff --git a/dom/security/test/https-only/file_upgrade_insecure_server.sjs b/dom/security/test/https-only/file_upgrade_insecure_server.sjs
new file mode 100644
index 0000000000..7cf590141c
--- /dev/null
+++ b/dom/security/test/https-only/file_upgrade_insecure_server.sjs
@@ -0,0 +1,112 @@
+// SJS file for HTTPS-Only Mode mochitests
+// Bug 1613063 - HTTPS Only Mode
+
+const TOTAL_EXPECTED_REQUESTS = 11;
+
+const IFRAME_CONTENT =
+ "<!DOCTYPE HTML>" +
+ "<html>" +
+ "<head><meta charset='utf-8'>" +
+ "<title>Bug 1613063 - HTTPS Only Mode</title>" +
+ "</head>" +
+ "<body>" +
+ "<img src='http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?nested-img'></img>" +
+ "</body>" +
+ "</html>";
+
+const expectedQueries = [
+ "script",
+ "style",
+ "img",
+ "iframe",
+ "form",
+ "xhr",
+ "media",
+ "object",
+ "font",
+ "img-redir",
+ "nested-img",
+];
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+ var queryString = request.queryString;
+
+ // initialize server variables and save the object state
+ // of the initial request, which returns async once the
+ // server has processed all requests.
+ if (queryString == "queryresult") {
+ setState("totaltests", TOTAL_EXPECTED_REQUESTS.toString());
+ setState("receivedQueries", "");
+ response.processAsync();
+ setObjectState("queryResult", response);
+ return;
+ }
+
+ // handle img redirect (https->http)
+ if (queryString == "redirect-image") {
+ var newLocation =
+ "http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?img-redir";
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", newLocation, false);
+ return;
+ }
+
+ // just in case error handling for unexpected queries
+ if (!expectedQueries.includes(queryString)) {
+ response.write("unexpected-response");
+ return;
+ }
+
+ // make sure all the requested queries are indeed https
+ queryString += request.scheme == "https" ? "-ok" : "-error";
+
+ var receivedQueries = getState("receivedQueries");
+
+ // images, scripts, etc. get queried twice, do not
+ // confuse the server by storing the preload as
+ // well as the actual load. If either the preload
+ // or the actual load is not https, then we would
+ // append "-error" in the array and the test would
+ // fail at the end.
+ if (receivedQueries.includes(queryString)) {
+ return;
+ }
+
+ // append the result to the total query string array
+ if (receivedQueries != "") {
+ receivedQueries += ",";
+ }
+ receivedQueries += queryString;
+ setState("receivedQueries", receivedQueries);
+
+ // keep track of how many more requests the server
+ // is expecting
+ var totaltests = parseInt(getState("totaltests"));
+ totaltests -= 1;
+ setState("totaltests", totaltests.toString());
+
+ // return content (img) for the nested iframe to test
+ // that subresource requests within nested contexts
+ // get upgraded as well. We also have to return
+ // the iframe context in case of an error so we
+ // can test both, using upgrade-insecure as well
+ // as the base case of not using upgrade-insecure.
+ if (queryString == "iframe-ok" || queryString == "iframe-error") {
+ response.write(IFRAME_CONTENT);
+ }
+
+ // if we have received all the requests, we return
+ // the result back.
+ if (totaltests == 0) {
+ getObjectState("queryResult", function (queryResponse) {
+ if (!queryResponse) {
+ return;
+ }
+ receivedQueries = getState("receivedQueries");
+ queryResponse.write(receivedQueries);
+ queryResponse.finish();
+ });
+ }
+}
diff --git a/dom/security/test/https-only/file_upgrade_insecure_wsh.py b/dom/security/test/https-only/file_upgrade_insecure_wsh.py
new file mode 100644
index 0000000000..b7159c742b
--- /dev/null
+++ b/dom/security/test/https-only/file_upgrade_insecure_wsh.py
@@ -0,0 +1,6 @@
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ pass
diff --git a/dom/security/test/https-only/file_user_gesture.html b/dom/security/test/https-only/file_user_gesture.html
new file mode 100644
index 0000000000..ac67064bf0
--- /dev/null
+++ b/dom/security/test/https-only/file_user_gesture.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1725026 - HTTPS Only Mode - Test if a load triggered by a user gesture can be upgraded to HTTPS</title>
+</head>
+<body>
+ <button id="httpLinkButton" onclick="location.href='http://example.com/tests/dom/security/test/https-only/file_console_logging.html'" type="button">
+ button</button>
+</body>
+</html>
diff --git a/dom/security/test/https-only/file_websocket_exceptions.html b/dom/security/test/https-only/file_websocket_exceptions.html
new file mode 100644
index 0000000000..6c6ba07480
--- /dev/null
+++ b/dom/security/test/https-only/file_websocket_exceptions.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+ Dummy file which gets iframe injected<br/>
+</body>
+</html>
diff --git a/dom/security/test/https-only/file_websocket_exceptions_iframe.html b/dom/security/test/https-only/file_websocket_exceptions_iframe.html
new file mode 100644
index 0000000000..23c6af2d45
--- /dev/null
+++ b/dom/security/test/https-only/file_websocket_exceptions_iframe.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<script>
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ window.removeEventListener("message", receiveMessage);
+
+ var mySocket = new WebSocket("ws://example.com/tests/dom/security/test/https-only/file_upgrade_insecure");
+ mySocket.onopen = function(e) {
+ parent.dispatchEvent(new CustomEvent("WebSocketEnded", {
+ detail: { url: mySocket.url, state: "onopen" }
+ }));
+ mySocket.close();
+ };
+ mySocket.onerror = function(e) {
+ parent.dispatchEvent(new CustomEvent("WebSocketEnded", {
+ detail: { url: mySocket.url, state: "onerror" }
+ }));
+ mySocket.close();
+ };
+}
+</script>
+</head>
+<body>
+ Https-Only: WebSocket exemption test in iframe</br>
+</body>
+</html>
diff --git a/dom/security/test/https-only/hsts_headers.sjs b/dom/security/test/https-only/hsts_headers.sjs
new file mode 100644
index 0000000000..72e82caaf3
--- /dev/null
+++ b/dom/security/test/https-only/hsts_headers.sjs
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+ if (request.queryString === "reset") {
+ // Reset the HSTS policy, prevent influencing other tests
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Strict-Transport-Security", "max-age=0");
+ response.write("Resetting HSTS");
+ return;
+ }
+ let hstsHeader = "max-age=60";
+ response.setHeader("Strict-Transport-Security", hstsHeader);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+ // Set header for csp upgrade
+ response.setHeader(
+ "Content-Security-Policy",
+ "upgrade-insecure-requests",
+ false
+ );
+ response.setStatusLine(request.httpVersion, 200);
+ response.write("<!DOCTYPE html><html><body><h1>Ok!</h1></body></html>");
+}
diff --git a/dom/security/test/https-only/mochitest.toml b/dom/security/test/https-only/mochitest.toml
new file mode 100644
index 0000000000..e7ecd439c4
--- /dev/null
+++ b/dom/security/test/https-only/mochitest.toml
@@ -0,0 +1,65 @@
+[DEFAULT]
+support-files = [
+ "file_redirect.sjs",
+ "file_upgrade_insecure.html",
+ "file_upgrade_insecure_server.sjs",
+ "file_upgrade_insecure_wsh.py",
+]
+prefs = [
+ "dom.security.https_first=false",
+ "security.mixed_content.upgrade_display_content=false",
+]
+
+["test_break_endless_upgrade_downgrade_loop.html"]
+skip-if = [
+ "os == 'android'", # no support for error pages, Bug 1697866
+ "http3",
+ "http2",
+]
+support-files = [
+ "file_break_endless_upgrade_downgrade_loop.sjs",
+ "file_user_gesture.html",
+]
+
+["test_fragment.html"]
+support-files = ["file_fragment.html"]
+
+["test_http_background_auth_request.html"]
+support-files = ["file_http_background_auth_request.sjs"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_http_background_request.html"]
+support-files = ["file_http_background_request.sjs"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_insecure_reload.html"]
+support-files = ["file_insecure_reload.sjs"]
+skip-if = ["os == 'android'"] # no https-only errorpage support in android
+
+["test_redirect_upgrade.html"]
+scheme = "https"
+fail-if = ["xorigin"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_resource_upgrade.html"]
+scheme = "https"
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_user_suggestion_box.html"]
+skip-if = [
+ "os == 'android'", # no https-only errorpage support in android
+ "http3",
+ "http2",
+]
diff --git a/dom/security/test/https-only/test_break_endless_upgrade_downgrade_loop.html b/dom/security/test/https-only/test_break_endless_upgrade_downgrade_loop.html
new file mode 100644
index 0000000000..847a71f378
--- /dev/null
+++ b/dom/security/test/https-only/test_break_endless_upgrade_downgrade_loop.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Bug 1691888: Break endless upgrade downgrade loops when using https-only</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+/*
+ * Description of the test:
+ * We perform three tests where our upgrade/downgrade redirect loop detector should break the
+ * endless loop:
+ * Test 1: Meta Refresh
+ * Test 2: JS Redirect
+ * Test 3: 302 redirect
+ */
+
+SimpleTest.requestFlakyTimeout("We need to wait for the HTTPS-Only error page to appear");
+SimpleTest.requestLongerTimeout(10);
+SimpleTest.waitForExplicitFinish();
+
+const REQUEST_URL =
+ "http://example.com/tests/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs";
+
+function resolveAfter6Seconds() {
+ return new Promise(resolve => {
+ setTimeout(() => {
+ resolve();
+ }, 6000);
+ });
+}
+
+async function verifyResult(aTestName) {
+ let errorPageL10nId = "about-httpsonly-title-alert";
+ let innerHTML = content.document.body.innerHTML;
+ ok(innerHTML.includes(errorPageL10nId), "the error page should be shown for " + aTestName);
+}
+
+async function verifyTest4Result() {
+ let pathname = content.document.location.pathname;
+ ok(
+ pathname === "/tests/dom/security/test/https-only/file_user_gesture.html",
+ "the http:// page should be loaded"
+ );
+}
+
+async function runTests() {
+ await SpecialPowers.pushPrefEnv({ set: [
+ ["dom.security.https_only_mode", true],
+ ["dom.security.https_only_mode_break_upgrade_downgrade_endless_loop", true],
+ ["dom.security.https_only_check_path_upgrade_downgrade_endless_loop", true],
+ ]});
+
+ // Test 1: Meta Refresh Redirect
+ let winTest1 = window.open(REQUEST_URL + "?test1a", "_blank");
+ // Test 2: JS win.location Redirect
+ let winTest2 = window.open(REQUEST_URL + "?test2a", "_blank");
+ // Test 3: 302 Redirect
+ let winTest3 = window.open(REQUEST_URL + "?test3a", "_blank");
+ // Test 4: 302 Redirect with a different path
+ let winTest4 = window.open(REQUEST_URL + "?test4a", "_blank");
+
+ // provide enough time for:
+ // the redirects to occur, and the error page to be displayed
+ await resolveAfter6Seconds();
+
+ await SpecialPowers.spawn(winTest1, ["test1"], verifyResult);
+ winTest1.close();
+
+ await SpecialPowers.spawn(winTest2, ["test2"], verifyResult);
+ winTest2.close();
+
+ await SpecialPowers.spawn(winTest3, ["test3"], verifyResult);
+ winTest3.close();
+
+ await SpecialPowers.spawn(winTest4, ["test4"], verifyTest4Result);
+ winTest4.close();
+
+ SimpleTest.finish();
+}
+
+runTests();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/https-only/test_fragment.html b/dom/security/test/https-only/test_fragment.html
new file mode 100644
index 0000000000..52a63764fb
--- /dev/null
+++ b/dom/security/test/https-only/test_fragment.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Bug 1694932: Https-only mode reloads the page in certain cases when there should be just a fragment navigation </title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+/*
+ * Description of the test:
+ * Perform a test where a button click leads to scroll the page. Test if https-only detects
+ * that the redirection address of the button is on the same page. Instead of a reload https-only
+ * should only scroll.
+ *
+ * Test:
+ * Enable https-only and load the url
+ * Load: http://mozilla.pettay.fi/moztests/fragment.html
+ * Click "Click me"
+ * The page should be scrolled down to 'foo' without a reload
+ * It shouldn't receive a message 'before unload' because the on before unload
+ * function in file_fragment.html should not be called
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+const REQUEST_URL = "http://example.com/tests/dom/security/test/https-only/file_fragment.html";
+const EXPECT_URL = REQUEST_URL.replace("http://", "https://");
+let winTest = null;
+let checkButtonClicked = false;
+
+async function receiveMessage(event) {
+ let data = event.data;
+ // checks if click was successful
+ if (!checkButtonClicked){
+ // expected window location ( expected URL)
+ ok(data.result == EXPECT_URL, "location is correct");
+ ok(data.button, "button is clicked");
+ // checks if loading was successful
+ ok(data.info == "onload", "Onloading worked");
+ // button was clicked
+ checkButtonClicked = true;
+ return;
+ }
+ // if Button was clicked once -> test finished
+ // check if hash exist and if hash of location is correct
+ ok(data.button, "button is clicked");
+ ok(data.result == EXPECT_URL + "#foo", "location (hash) is correct");
+ // check that page is scrolled not reloaded
+ ok(data.info == "scrolled-to-foo","Scrolled successfully without reloading!");
+ is(data.documentURI, EXPECT_URL + "#foo", "Document URI is correct");
+ // complete test and close window
+ window.removeEventListener("message",receiveMessage);
+ winTest.close();
+ SimpleTest.finish();
+}
+
+async function runTest() {
+ //Test: With https-only mode activated
+ await SpecialPowers.pushPrefEnv({ set: [
+ ["dom.security.https_only_mode", true],
+ ]});
+ winTest = window.open(REQUEST_URL);
+}
+window.addEventListener("message", receiveMessage);
+runTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/https-only/test_http_background_auth_request.html b/dom/security/test/https-only/test_http_background_auth_request.html
new file mode 100644
index 0000000000..5a7bf665b9
--- /dev/null
+++ b/dom/security/test/https-only/test_http_background_auth_request.html
@@ -0,0 +1,113 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1665062 - HTTPS-Only: Do not cancel channel if auth is in progress</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script class="testbody" type="text/javascript">
+
+/*
+ * Description of the test:
+ * We send a top-level request which results in a '401 - Unauthorized' and ensure that the
+ * http background request does not accidentally treat that request as a potential timeout.
+ * We make sure that ther HTTPS-Only Mode Error Page does *NOT* show up.
+ */
+
+const { AppConstants } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("When Auth is in progress, HTTPS-Only page should not show up");
+SimpleTest.requestLongerTimeout(10);
+
+const EXPECTED_KICK_OFF_REQUEST =
+ "http://test1.example.com/tests/dom/security/test/https-only/file_http_background_auth_request.sjs?foo";
+const EXPECTED_UPGRADE_REQUEST = EXPECTED_KICK_OFF_REQUEST.replace("http://", "https://");
+let EXPECTED_BG_REQUEST = "http://test1.example.com/";
+let requestCounter = 0;
+
+function examiner() {
+ SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
+}
+examiner.prototype = {
+ observe(subject, topic, data) {
+ if (topic !== "specialpowers-http-notify-request") {
+ return;
+ }
+
+ // On Android we have other requests appear here as well. Let's make
+ // sure we only evaluate requests triggered by the test.
+ if (!data.startsWith("http://test1.example.com") &&
+ !data.startsWith("https://test1.example.com")) {
+ return;
+ }
+ ++requestCounter;
+ if (requestCounter == 1) {
+ is(data, EXPECTED_KICK_OFF_REQUEST, "kick off request needs to be http");
+ return;
+ }
+ if (requestCounter == 2) {
+ is(data, EXPECTED_UPGRADE_REQUEST, "upgraded request needs to be https");
+ return;
+ }
+ if (requestCounter == 3) {
+ is(data, EXPECTED_BG_REQUEST, "background request needs to be http and no sensitive info");
+ return;
+ }
+ ok(false, "we should never get here, but just in case");
+ },
+ remove() {
+ SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
+ }
+}
+window.AuthBackgroundRequestExaminer = new examiner();
+
+// https-only top-level background request occurs after 3 seconds, hence
+// we use 4 seconds to make sure the background request did not happen.
+function resolveAfter4Seconds() {
+ return new Promise(resolve => {
+ setTimeout(() => {
+ resolve();
+ }, 4000);
+ });
+}
+
+async function runTests() {
+ await SpecialPowers.pushPrefEnv({ set: [
+ ["dom.security.https_only_mode", true],
+ ["dom.security.https_only_mode_send_http_background_request", true],
+ ]});
+
+ let testWin = window.open(EXPECTED_KICK_OFF_REQUEST, "_blank");
+
+ // Give the Auth Process and background request some time before moving on.
+ await resolveAfter4Seconds();
+
+ if (AppConstants.platform !== "android") {
+ is(requestCounter, 3, "three requests total (kickoff, upgraded, background)");
+ } else {
+ // On Android, the auth request resolves and hence the background request
+ // is not even kicked off - nevertheless, the error page should not appear!
+ is(requestCounter, 2, "two requests total (kickoff, upgraded)");
+ }
+
+ await SpecialPowers.spawn(testWin, [], () => {
+ let innerHTML = content.document.body.innerHTML;
+ is(innerHTML, "", "expection page should not be displayed");
+ });
+
+ testWin.close();
+
+ window.AuthBackgroundRequestExaminer.remove();
+ SimpleTest.finish();
+}
+
+runTests();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/https-only/test_http_background_request.html b/dom/security/test/https-only/test_http_background_request.html
new file mode 100644
index 0000000000..5dff0a5fdb
--- /dev/null
+++ b/dom/security/test/https-only/test_http_background_request.html
@@ -0,0 +1,125 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1663396: Test HTTPS-Only-Mode top-level background request not leaking sensitive info</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script class="testbody" type="text/javascript">
+
+/*
+ * Description of the test:
+ * Send a top-level request and make sure that the the top-level https-only background request
+ * (a) does only use pre-path information
+ * (b) does not happen if the pref is set to false
+ */
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("have to test that https-only mode background request does not happen");
+SimpleTest.requestLongerTimeout(8);
+
+const SJS_PATH = "tests/dom/security/test/https-only/file_http_background_request.sjs?sensitive";
+
+const EXPECTED_KICK_OFF_REQUEST = "http://example.com/" + SJS_PATH;
+const EXPECTED_KICK_OFF_REQUEST_LOCAL = "http://localhost:8/" + SJS_PATH;
+const EXPECTED_UPGRADE_REQUEST = EXPECTED_KICK_OFF_REQUEST.replace("http://", "https://");
+let expectedBackgroundRequest = "";
+let requestCounter = 0;
+
+function examiner() {
+ SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
+}
+examiner.prototype = {
+ observe(subject, topic, data) {
+ if (topic !== "specialpowers-http-notify-request") {
+ return;
+ }
+ // On Android we have other requests appear here as well. Let's make
+ // sure we only evaluate requests triggered by the test.
+ if (!data.startsWith("http://example.com") &&
+ !data.startsWith("https://example.com") &&
+ !data.startsWith("http://localhost:8") &&
+ !data.startsWith("https://localhost:8")) {
+ return;
+ }
+ ++requestCounter;
+ if (requestCounter == 1) {
+ ok(
+ data === EXPECTED_KICK_OFF_REQUEST || data === EXPECTED_KICK_OFF_REQUEST_LOCAL,
+ "kick off request needs to be http"
+ );
+ return;
+ }
+ if (requestCounter == 2) {
+ is(data, EXPECTED_UPGRADE_REQUEST, "upgraded request needs to be https");
+ return;
+ }
+ if (requestCounter == 3) {
+ is(data, expectedBackgroundRequest, "background request needs to be http and no sensitive info like path");
+ return;
+ }
+ ok(false, "we should never get here, but just in case");
+ },
+ remove() {
+ SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
+ }
+}
+window.BackgroundRequestExaminer = new examiner();
+
+// https-only top-level background request occurs after 3 seconds, hence
+// we use 4 seconds to make sure the background request did not happen.
+function resolveAfter4Seconds() {
+ return new Promise(resolve => {
+ setTimeout(() => {
+ resolve();
+ }, 4000);
+ });
+}
+
+async function runTests() {
+ // (a) Test http background request to only use prePath information
+ expectedBackgroundRequest = "http://example.com/";
+ requestCounter = 0;
+ await SpecialPowers.pushPrefEnv({ set: [
+ ["dom.security.https_only_mode", true],
+ ["dom.security.https_only_mode_send_http_background_request", true],
+ ["dom.security.https_only_mode_error_page_user_suggestions", false],
+ ]});
+ let testWin = window.open(EXPECTED_KICK_OFF_REQUEST, "_blank");
+ await resolveAfter4Seconds();
+ is(requestCounter, 3, "three requests total (kickoff, upgraded, background)");
+ testWin.close();
+
+ // (x) Test no http background request happens when localhost
+ expectedBackgroundRequest = "";
+ requestCounter = 0;
+ testWin = window.open(EXPECTED_KICK_OFF_REQUEST_LOCAL, "_blank");
+ await resolveAfter4Seconds();
+ is(requestCounter, 1, "one requests total (kickoff, no upgraded, no background)");
+ testWin.close();
+
+ // (b) Test no http background request happens if pref is set to false
+ expectedBackgroundRequest = "";
+ requestCounter = 0;
+ await SpecialPowers.pushPrefEnv({ set: [
+ ["dom.security.https_only_mode", true],
+ ["dom.security.https_only_mode_send_http_background_request", false],
+ ["dom.security.https_only_mode_error_page_user_suggestions", false],
+ ]});
+ testWin = window.open(EXPECTED_KICK_OFF_REQUEST, "_blank");
+ await resolveAfter4Seconds();
+ is(requestCounter, 2, "two requests total (kickoff, upgraded, no background)");
+ testWin.close();
+
+ // clean up and finish tests
+ window.BackgroundRequestExaminer.remove();
+ SimpleTest.finish();
+}
+
+runTests();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/https-only/test_insecure_reload.html b/dom/security/test/https-only/test_insecure_reload.html
new file mode 100644
index 0000000000..d143c9080b
--- /dev/null
+++ b/dom/security/test/https-only/test_insecure_reload.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Bug 1702001: Https-only mode does not reload pages after clicking "Continue to HTTP Site", when url contains navigation </title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+/*
+ * Description of the test:
+ *
+ * Load a page including a fragment portion which does not support https and make
+ * sure that exempting the page from https-only-mode does not result in a fragment
+ * navigation.
+ */
+
+
+ function resolveAfter6Seconds() {
+ return new Promise(resolve => {
+ setTimeout(() => {
+ resolve();
+ }, 6000);
+ });
+}
+
+SimpleTest.requestFlakyTimeout("We need to wait for the HTTPS-Only error page to appear");
+SimpleTest.waitForExplicitFinish();
+
+let winTest = null;
+let TEST_URL = "http://example.com/tests/dom/security/test/https-only/file_insecure_reload.sjs#nav";
+
+// verify that https-only page appeared
+async function verifyErrorPage() {
+ let errorPageL10nId = "about-httpsonly-title-alert";
+ let innerHTML = content.document.body.innerHTML;
+ ok(innerHTML.includes(errorPageL10nId), "the error page should be shown for ");
+ let button = content.document.getElementById("openInsecure");
+ // Click "Continue to HTTP Site"
+ ok(button, "button exist");
+ if(button) {
+ button.click();
+ }
+}
+// verify that you entered the page and are not still displaying
+// the https-only error page
+async function receiveMessage(event) {
+ // read event
+ let { result, historyLength } = event.data;
+ is(result, "you entered the http page", "The requested page should be shown");
+ is(historyLength, 1, "History should contain one item");
+ window.removeEventListener("message",receiveMessage);
+ winTest.close();
+ SimpleTest.finish();
+}
+
+
+async function runTest() {
+ //Test: With https-only mode activated
+ await SpecialPowers.pushPrefEnv({ set: [
+ ["dom.security.https_only_mode", true],
+ ]});
+ winTest = window.open(TEST_URL);
+ await resolveAfter6Seconds();
+ await SpecialPowers.spawn(winTest,[],verifyErrorPage);
+}
+window.addEventListener("message", receiveMessage);
+runTest();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/https-only/test_redirect_upgrade.html b/dom/security/test/https-only/test_redirect_upgrade.html
new file mode 100644
index 0000000000..59f02f96d0
--- /dev/null
+++ b/dom/security/test/https-only/test_redirect_upgrade.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1613063
+Test that 302 redirect requests get upgraded to https:// with HTTPS-Only Mode enabled
+-->
+
+<head>
+ <title>HTTPS-Only Mode - XHR Redirect Upgrade</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+ <h1>HTTPS-Only Mode</h1>
+ <p>Upgrade Test for insecure XHR redirects.</p>
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1613063">Bug 1613063</a>
+
+ <script type="application/javascript">
+
+ const redirectCodes = ["301", "302", "303", "307"]
+ let currentTest = 0
+
+ function startTest() {
+ const currentCode = redirectCodes[currentTest];
+
+ const myXHR = new XMLHttpRequest();
+ // Make a request to a site (eg. https://file_redirect.sjs?301), which will redirect to http://file_redirect.sjs?check.
+ // The response will either be secure-ok, if the request has been upgraded to https:// or secure-error if it didn't.
+ myXHR.open("GET", `https://example.com/tests/dom/security/test/https-only/file_redirect.sjs?${currentCode}`);
+ myXHR.onload = (e) => {
+ is(myXHR.responseText, "secure-ok", `a ${currentCode} redirect when posting violation report should be blocked`)
+ testDone();
+ }
+ // This should not happen
+ myXHR.onerror = (e) => {
+ ok(false, `Could not query results from server for ${currentCode}-redirect test (" + e.message + ")`);
+ testDone();
+ }
+ myXHR.send();
+ }
+
+ function testDone() {
+ // Check if there are remaining tests
+ if (++currentTest < redirectCodes.length) {
+ startTest()
+ } else {
+ SimpleTest.finish();
+ }
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ // Set preference and start test
+ SpecialPowers.pushPrefEnv({ set: [["dom.security.https_only_mode", true]] }, startTest);
+
+ </script>
+</body>
+</html>
diff --git a/dom/security/test/https-only/test_resource_upgrade.html b/dom/security/test/https-only/test_resource_upgrade.html
new file mode 100644
index 0000000000..6584bad020
--- /dev/null
+++ b/dom/security/test/https-only/test_resource_upgrade.html
@@ -0,0 +1,122 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>HTTPS-Only Mode - Resource Upgrade</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+ <h1>HTTPS-Only Mode</h1>
+ <p>Upgrade Test for various resources</p>
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1613063">Bug 1613063</a>
+ <iframe style="width:100%;" id="testframe"></iframe>
+
+ <script class="testbody" type="text/javascript">
+ /* Description of the test:
+ * We load resources (img, script, sytle, etc) over *http* and make sure
+ * that all the resources get upgraded to use >> https << when the
+ * preference "dom.security.https_only_mode" is set to true. We further
+ * test that subresources within nested contexts (iframes) get upgraded
+ * and also test the handling of server side redirects.
+ *
+ * In detail:
+ * We perform an XHR request to the *.sjs file which is processed async on
+ * the server and waits till all the requests were processed by the server.
+ * Once the server received all the different requests, the server responds
+ * to the initial XHR request with an array of results which must match
+ * the expected results from each test, making sure that all requests
+ * received by the server (*.sjs) were actually *https* requests.
+ */
+
+ const { AppConstants } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+ );
+ const splitRegex = /^(.*)-(.*)$/
+ const testConfig = {
+ topLevelScheme: "http://",
+ results: [
+ "iframe", "script", "img", "img-redir", "font", "xhr", "style",
+ "media", "object", "form", "nested-img"
+ ]
+ }
+ // TODO: WebSocket tests are not supported on Android Yet. Bug 1566168.
+ if (AppConstants.platform !== "android") {
+ testConfig.results.push("websocket");
+ }
+
+
+ function runTest() {
+ // sends an xhr request to the server which is processed async, which only
+ // returns after the server has received all the expected requests.
+ var myXHR = new XMLHttpRequest();
+ myXHR.open("GET", "file_upgrade_insecure_server.sjs?queryresult");
+ myXHR.onload = function (e) {
+ var results = myXHR.responseText.split(",");
+ for (var index in results) {
+ checkResult(results[index]);
+ }
+ }
+ myXHR.onerror = function (e) {
+ ok(false, "Could not query results from server (" + e.message + ")");
+ finishTest();
+ }
+ myXHR.send();
+
+ // give it some time and run the testpage
+ SimpleTest.executeSoon(() => {
+ var src = testConfig.topLevelScheme + "example.com/tests/dom/security/test/https-only/file_upgrade_insecure.html";
+ document.getElementById("testframe").src = src;
+ });
+ }
+
+ // a postMessage handler that is used by sandboxed iframes without
+ // 'allow-same-origin' to bubble up results back to this main page.
+ window.addEventListener("message", receiveMessage);
+ function receiveMessage(event) {
+ checkResult(event.data.result);
+ }
+
+ function finishTest() {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+ }
+
+ function checkResult(response) {
+ // A response looks either like this "iframe-ok" or "[key]-[result]"
+ const [, key, result] = splitRegex.exec(response)
+ // try to find the expected result within the results array
+ var index = testConfig.results.indexOf(key);
+
+ // If the response is not even part of the results array, something is super wrong
+ if (index == -1) {
+ ok(false, `Unexpected response from server (${response})`);
+ finishTest();
+ }
+
+ // take the element out the array and continue till the results array is empty
+ if (index != -1) {
+ testConfig.results.splice(index, 1);
+ }
+
+ // Check if the result was okay or had an error
+ is(result, 'ok', `Upgrade all requests on toplevel http for '${key}' came back with: '${result}'`)
+
+ // If we're not expecting any more resulsts, finish the test
+ if (!testConfig.results.length) {
+ finishTest();
+ }
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ // Set preference and start test
+ SpecialPowers.pushPrefEnv({ set: [["dom.security.https_only_mode", true]] }, runTest);
+
+ </script>
+</body>
+
+</html>
diff --git a/dom/security/test/https-only/test_user_suggestion_box.html b/dom/security/test/https-only/test_user_suggestion_box.html
new file mode 100644
index 0000000000..1feabcd003
--- /dev/null
+++ b/dom/security/test/https-only/test_user_suggestion_box.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Bug 1665057 - Add www button on https-only error page</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+"use strict";
+
+/*
+ * Description of the test:
+ * We send a top-level request to a http-page in https-only mode
+ * The page has a bad certificate and can't be updated so the error page appears
+ * If there is a secure connection possible to www the suggestion-box on the error page
+ * should appear and have a link to that secure www-page
+ * if the original-pagerequest already has a www or there is no secure www connection
+ * the suggestion box should not appear
+ */
+
+SimpleTest.requestFlakyTimeout("We need to wait for the HTTPS-Only error page to appear and for the additional 'www' request to provide a suggestion.");
+SimpleTest.requestLongerTimeout(10);
+
+// urls of server locations with bad cert -> https-only should display error page
+const KICK_OFF_REQUEST_WITH_SUGGESTION = "http://suggestion-example.com";
+const KICK_OFF_REQUEST_WITHOUT_SUGGESTION = "http://no-suggestion-example.com";
+
+// l10n ids to compare html to
+const ERROR_PAGE_L10N_ID = "about-httpsonly-title-alert";
+const SUGGESTION_BOX_L10N_ID = "about-httpsonly-suggestion-box-www-text";
+
+// the error page needs to be build and a background https://www request needs to happen
+// we use 4 seconds to make sure these requests did happen.
+function resolveAfter4Seconds() {
+ return new Promise(resolve => {
+ setTimeout(() => {
+ resolve();
+ }, 4000);
+ });
+}
+
+async function runTests(aMessage) {
+ let errorPageL10nId = "about-httpsonly-title-alert";
+ let suggestionBoxL10nId = "about-httpsonly-suggestion-box-www-text";
+
+ let innerHTML = content.document.body.innerHTML;
+
+ if (aMessage === "with_suggestion") {
+ // test if the page with suggestion shows the error page and the suggestion box
+ ok(innerHTML.includes(errorPageL10nId), "the error page should be shown.");
+ ok(innerHTML.includes(suggestionBoxL10nId), "the suggestion box should be shown.");
+ } else if (aMessage === "without_suggestion") {
+ // test if the page without suggestion shows the error page but not the suggestion box
+ ok(innerHTML.includes(errorPageL10nId), "the error page should be shown.");
+ ok(!innerHTML.includes(suggestionBoxL10nId), "the suggestion box should not be shown.");
+ } else {
+ ok(false, "we should never get here");
+ }
+}
+
+add_task(async function() {
+ await SpecialPowers.pushPrefEnv({ set: [
+ ["dom.security.https_only_mode", true],
+ ["dom.security.https_only_mode_send_http_background_request", false],
+ ["dom.security.https_only_mode_error_page_user_suggestions", true],
+ ]});
+ let testWinSuggestion = window.open(KICK_OFF_REQUEST_WITH_SUGGESTION, "_blank");
+ let testWinWithoutSuggestion = window.open(KICK_OFF_REQUEST_WITHOUT_SUGGESTION, "_blank");
+
+ await resolveAfter4Seconds();
+
+ await SpecialPowers.spawn(testWinSuggestion, ["with_suggestion"], runTests);
+ await SpecialPowers.spawn(testWinWithoutSuggestion, ["without_suggestion"], runTests);
+
+ testWinSuggestion.close();
+ testWinWithoutSuggestion.close();
+});
+</script>
+</body>
+</html>
diff --git a/dom/security/test/mixedcontentblocker/auto_upgrading_identity.html b/dom/security/test/mixedcontentblocker/auto_upgrading_identity.html
new file mode 100644
index 0000000000..d843b7fae1
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/auto_upgrading_identity.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset='utf-8'>
+ <title>Bug 1674341: Test SiteIdentity when auto-upgrading mixed content</title>
+</head>
+<body>
+ <!-- needs to be http: image for mixed content and auto-upgrading -->
+ <img type="image/png" id="testimage" src="http://example.com/browser/dom/security/test/mixedcontentblocker/auto_upgrading_identity.png" />
+</body>
+</html>
diff --git a/dom/security/test/mixedcontentblocker/auto_upgrading_identity.png b/dom/security/test/mixedcontentblocker/auto_upgrading_identity.png
new file mode 100644
index 0000000000..52c591798e
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/auto_upgrading_identity.png
Binary files differ
diff --git a/dom/security/test/mixedcontentblocker/browser.toml b/dom/security/test/mixedcontentblocker/browser.toml
new file mode 100644
index 0000000000..5b0b85cb0b
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/browser.toml
@@ -0,0 +1,35 @@
+[DEFAULT]
+prefs = ["dom.security.https_first=false"]
+support-files = [
+ "download_page.html",
+ "download_server.sjs",
+]
+
+["browser_auto_upgrading_identity.js"]
+support-files = [
+ "auto_upgrading_identity.html",
+ "auto_upgrading_identity.png",
+]
+
+["browser_csp_block_all_mixedcontent_and_mixed_content_display_upgrade.js"]
+support-files = [
+ "file_csp_block_all_mixedcontent_and_mixed_content_display_upgrade.html",
+ "pass.png",
+ "test.ogv",
+ "test.wav",
+]
+
+["browser_mixed_content_auth_download.js"]
+support-files = [
+ "file_auth_download_page.html",
+ "file_auth_download_server.sjs",
+]
+
+["browser_mixed_content_auto_upgrade_display_console.js"]
+support-files = ["file_mixed_content_auto_upgrade_display_console.html"]
+
+["browser_test_mixed_content_download.js"]
+skip-if = [
+ "win11_2009", # Bug 1784764
+ "os == 'linux' && !debug", # Bug 1784764
+]
diff --git a/dom/security/test/mixedcontentblocker/browser_auto_upgrading_identity.js b/dom/security/test/mixedcontentblocker/browser_auto_upgrading_identity.js
new file mode 100644
index 0000000000..6e467b8d30
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/browser_auto_upgrading_identity.js
@@ -0,0 +1,58 @@
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const TEST_TOPLEVEL_URI = TEST_PATH + "auto_upgrading_identity.html";
+
+// auto upgrading mixed content should not indicate passive mixed content loaded
+add_task(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.mixed_content.upgrade_display_content", true],
+ ["security.mixed_content.upgrade_display_content.image", true],
+ ],
+ });
+ await BrowserTestUtils.withNewTab(
+ TEST_TOPLEVEL_URI,
+ async function (browser) {
+ await ContentTask.spawn(browser, {}, async function () {
+ let testImg = content.document.getElementById("testimage");
+ ok(
+ testImg.src.includes("auto_upgrading_identity.png"),
+ "sanity: correct image is loaded"
+ );
+ });
+ // Ensure the identiy handler does not show mixed content!
+ ok(
+ !gIdentityHandler._isMixedPassiveContentLoaded,
+ "Auto-Upgrading Mixed Content: Identity should note indicate mixed content"
+ );
+ }
+ );
+});
+
+// regular mixed content test should indicate passive mixed content loaded
+add_task(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.mixed_content.upgrade_display_content", false]],
+ });
+ await BrowserTestUtils.withNewTab(
+ TEST_TOPLEVEL_URI,
+ async function (browser) {
+ await ContentTask.spawn(browser, {}, async function () {
+ let testImg = content.document.getElementById("testimage");
+ ok(
+ testImg.src.includes("auto_upgrading_identity.png"),
+ "sanity: correct image is loaded"
+ );
+ });
+ // Ensure the identiy handler does show mixed content!
+ ok(
+ gIdentityHandler._isMixedPassiveContentLoaded,
+ "Regular Mixed Content: Identity should indicate mixed content"
+ );
+ }
+ );
+});
diff --git a/dom/security/test/mixedcontentblocker/browser_csp_block_all_mixedcontent_and_mixed_content_display_upgrade.js b/dom/security/test/mixedcontentblocker/browser_csp_block_all_mixedcontent_and_mixed_content_display_upgrade.js
new file mode 100644
index 0000000000..6e130d16e0
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/browser_csp_block_all_mixedcontent_and_mixed_content_display_upgrade.js
@@ -0,0 +1,171 @@
+/*
+ * Description of the Test:
+ * We load an https page which uses a CSP including block-all-mixed-content.
+ * The page embedded an audio, img and video. ML2 should upgrade them and
+ * CSP should not be triggered.
+ */
+
+const PRE_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+let gExpectedMessages = 0;
+let gExpectBlockAllMsg = false;
+
+function onConsoleMessage({ message }) {
+ // Check if csp warns about block-all-mixed content being obsolete
+ if (message.includes("Content-Security-Policy")) {
+ if (gExpectBlockAllMsg) {
+ ok(
+ message.includes("block-all-mixed-content obsolete"),
+ "CSP warns about block-all-mixed content being obsolete"
+ );
+ } else {
+ ok(
+ message.includes("Blocking insecure request"),
+ "CSP error about blocking insecure request"
+ );
+ }
+ }
+ if (message.includes("Mixed Content:")) {
+ ok(
+ message.includes("Upgrading insecure display request"),
+ "msg included a mixed content upgrade"
+ );
+ gExpectedMessages--;
+ }
+}
+
+async function checkLoadedElements() {
+ let url =
+ PRE_PATH +
+ "file_csp_block_all_mixedcontent_and_mixed_content_display_upgrade.html";
+ return BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ waitForLoad: true,
+ },
+ async function (browser) {
+ return ContentTask.spawn(browser, [], async function () {
+ console.log(content.document.innerHTML);
+
+ // Check image loaded
+ let image = content.document.getElementById("some-img");
+ let imageLoaded = image && image.complete && image.naturalHeight !== 0;
+ // Check audio loaded
+ let audio = content.document.getElementById("some-audio");
+ let audioLoaded = audio && audio.readyState >= 2;
+ // Check video loaded
+ let video = content.document.getElementById("some-video");
+ //let videoPlayable = await once(video, "loadeddata").then(_ => true);
+ let videoLoaded = video && video.readyState === 4;
+ return { audio: audioLoaded, img: imageLoaded, video: videoLoaded };
+ });
+ }
+ );
+}
+
+add_task(async function test_upgrade_all() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.mixed_content.upgrade_display_content", true],
+ // Not enabled by default outside Nightly.
+ ["security.mixed_content.upgrade_display_content.image", true],
+ ],
+ });
+ Services.console.registerListener(onConsoleMessage);
+
+ // Starting the test
+ gExpectedMessages = 3;
+ gExpectBlockAllMsg = true;
+
+ let loadedElements = await checkLoadedElements();
+ is(loadedElements.img, true, "Image loaded and was upgraded");
+ is(loadedElements.video, true, "Video loaded and was upgraded");
+ is(loadedElements.audio, true, "Audio loaded and was upgraded");
+
+ await BrowserTestUtils.waitForCondition(() => gExpectedMessages === 0);
+
+ // Clean up
+ Services.console.unregisterListener(onConsoleMessage);
+ SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_dont_upgrade_image() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.mixed_content.upgrade_display_content", true],
+ ["security.mixed_content.upgrade_display_content.image", false],
+ ],
+ });
+ Services.console.registerListener(onConsoleMessage);
+
+ // Starting the test
+ gExpectedMessages = 2;
+ gExpectBlockAllMsg = false;
+
+ let loadedElements = await checkLoadedElements();
+ is(loadedElements.img, false, "Image was not loaded");
+ is(loadedElements.video, true, "Video loaded and was upgraded");
+ is(loadedElements.audio, true, "Audio loaded and was upgraded");
+
+ await BrowserTestUtils.waitForCondition(() => gExpectedMessages === 0);
+
+ // Clean up
+ Services.console.unregisterListener(onConsoleMessage);
+ SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_dont_upgrade_audio() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.mixed_content.upgrade_display_content", true],
+ ["security.mixed_content.upgrade_display_content.image", true],
+ ["security.mixed_content.upgrade_display_content.audio", false],
+ ],
+ });
+ Services.console.registerListener(onConsoleMessage);
+
+ // Starting the test
+ gExpectedMessages = 2;
+ gExpectBlockAllMsg = false;
+
+ let loadedElements = await checkLoadedElements();
+ is(loadedElements.img, true, "Image loaded and was upgraded");
+ is(loadedElements.video, true, "Video loaded and was upgraded");
+ is(loadedElements.audio, false, "Audio was not loaded");
+
+ await BrowserTestUtils.waitForCondition(() => gExpectedMessages === 0);
+
+ // Clean up
+ Services.console.unregisterListener(onConsoleMessage);
+ SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_dont_upgrade_video() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.mixed_content.upgrade_display_content", true],
+ ["security.mixed_content.upgrade_display_content.image", true],
+ ["security.mixed_content.upgrade_display_content.video", false],
+ ],
+ });
+ Services.console.registerListener(onConsoleMessage);
+
+ // Starting the test
+ gExpectedMessages = 2;
+ gExpectBlockAllMsg = false;
+
+ let loadedElements = await checkLoadedElements();
+ is(loadedElements.img, true, "Image loaded and was upgraded");
+ is(loadedElements.video, false, "Video was not loaded");
+ is(loadedElements.audio, true, "Audio loaded and was upgraded");
+
+ await BrowserTestUtils.waitForCondition(() => gExpectedMessages === 0);
+
+ // Clean up
+ Services.console.unregisterListener(onConsoleMessage);
+ SpecialPowers.popPrefEnv();
+});
diff --git a/dom/security/test/mixedcontentblocker/browser_mixed_content_auth_download.js b/dom/security/test/mixedcontentblocker/browser_mixed_content_auth_download.js
new file mode 100644
index 0000000000..25fee8de3c
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/browser_mixed_content_auth_download.js
@@ -0,0 +1,169 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ Downloads: "resource://gre/modules/Downloads.sys.mjs",
+ DownloadsCommon: "resource:///modules/DownloadsCommon.sys.mjs",
+});
+
+const { PromptTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromptTestUtils.sys.mjs"
+);
+
+let authPromptModalType = Services.prefs.getIntPref(
+ "prompts.modalType.httpAuth"
+);
+
+const downloadMonitoringView = {
+ _listeners: [],
+ onDownloadAdded(download) {
+ for (let listener of this._listeners) {
+ listener(download);
+ }
+ this._listeners = [];
+ },
+ waitForDownload(listener) {
+ this._listeners.push(listener);
+ },
+};
+
+let SECURE_BASE_URL =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+ ) + "file_auth_download_page.html";
+
+/**
+ * Waits until a download is triggered.
+ * It waits until a prompt is shown,
+ * saves and then accepts the dialog.
+ * @returns {Promise} Resolved once done.
+ */
+
+function shouldTriggerDownload() {
+ return new Promise(res => {
+ downloadMonitoringView.waitForDownload(res);
+ });
+}
+function shouldNotifyDownloadUI() {
+ return new Promise(res => {
+ downloadMonitoringView.waitForDownload(async aDownload => {
+ let { error } = aDownload;
+ if (
+ error.becauseBlockedByReputationCheck &&
+ error.reputationCheckVerdict == Downloads.Error.BLOCK_VERDICT_INSECURE
+ ) {
+ // It's an insecure Download, now Check that it has been cleaned up properly
+ if ((await IOUtils.stat(aDownload.target.path)).size != 0) {
+ throw new Error(`Download target is not empty!`);
+ }
+ if ((await IOUtils.stat(aDownload.target.path)).size != 0) {
+ throw new Error(`Download partFile was not cleaned up properly`);
+ }
+ // Assert that the Referrer is present
+ if (!aDownload.source.referrerInfo) {
+ throw new Error("The Blocked download is missing the ReferrerInfo");
+ }
+
+ res(aDownload);
+ } else {
+ ok(false, "No error for download that was expected to error!");
+ }
+ });
+ });
+}
+
+async function resetDownloads() {
+ // Removes all downloads from the download List
+ const types = new Set();
+ let publicList = await Downloads.getList(Downloads.ALL);
+ let downloads = await publicList.getAll();
+ for (let download of downloads) {
+ if (download.contentType) {
+ types.add(download.contentType);
+ }
+ publicList.remove(download);
+ await download.finalize(true);
+ }
+}
+
+async function runTest(url, link, checkFunction, description) {
+ await resetDownloads();
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ is(
+ gBrowser.currentURI.schemeIs("https"),
+ true,
+ "Scheme of opened tab should be https"
+ );
+ info("Checking: " + description);
+
+ let checkPromise = checkFunction();
+ // Click the Link to trigger the download
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [link], contentLink => {
+ content.document.getElementById(contentLink).click();
+ });
+ // Wait for the auth prompt, enter the login details and close the prompt
+ await PromptTestUtils.handleNextPrompt(
+ gBrowser.selectedBrowser,
+ { modalType: authPromptModalType, promptType: "promptUserAndPass" },
+ { buttonNumClick: 0, loginInput: "user", passwordInput: "pass" }
+ );
+ await checkPromise;
+ ok(true, description);
+ // Close download panel
+ DownloadsPanel.hidePanel();
+ is(DownloadsPanel.panel.state, "closed", "Panel should be closed");
+ await BrowserTestUtils.removeTab(tab);
+}
+
+add_setup(async function () {
+ let list = await Downloads.getList(Downloads.ALL);
+ list.addView(downloadMonitoringView);
+ registerCleanupFunction(() => list.removeView(downloadMonitoringView));
+ // Ensure to delete all cached credentials before running test
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve);
+ });
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.block_download_insecure", true]],
+ });
+});
+//Test description:
+// 1. Open "https://example.com".
+// 2. From "https://example.com" download something, but that download is only available via http
+// and with authentication.
+// 3. Login and start download.
+// 4. Mixed-content blocker blocks download.
+// 5. Unblock download and verify the downloaded file.
+add_task(async function test_auth_download() {
+ await runTest(
+ SECURE_BASE_URL,
+ "insecure",
+ async () => {
+ let [, download] = await Promise.all([
+ shouldTriggerDownload(),
+ shouldNotifyDownloadUI(),
+ ]);
+ await download.unblock();
+ Assert.equal(
+ download.error,
+ null,
+ "There should be no error after unblocking"
+ );
+ info(
+ "Start download to be able to validate the size and the success of the download"
+ );
+ await download.start();
+ is(
+ download.contentType,
+ "text/html",
+ "File contentType should be correct."
+ );
+ ok(download.succeeded, "Download succeeded!");
+ is(download.target.size, 27, "Download has correct size");
+ },
+ "A locked Download from an auth server should succeeded to Download after a Manual unblock"
+ );
+});
diff --git a/dom/security/test/mixedcontentblocker/browser_mixed_content_auto_upgrade_display_console.js b/dom/security/test/mixedcontentblocker/browser_mixed_content_auto_upgrade_display_console.js
new file mode 100644
index 0000000000..033467dd1c
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/browser_mixed_content_auto_upgrade_display_console.js
@@ -0,0 +1,54 @@
+// Bug 1673574 - Improve Console logging for mixed content auto upgrading
+"use strict";
+
+const testPath = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+let seenAutoUpgradeMessage = false;
+
+const kTestURI =
+ testPath + "file_mixed_content_auto_upgrade_display_console.html";
+
+add_task(async function () {
+ // A longer timeout is necessary for this test than the plain mochitests
+ // due to opening a new tab with the web console.
+ requestLongerTimeout(4);
+
+ // Enable HTTPS-Only Mode and register console-listener
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.mixed_content.upgrade_display_content", true],
+ ["security.mixed_content.upgrade_display_content.image", true],
+ ],
+ });
+ Services.console.registerListener(on_auto_upgrade_message);
+
+ BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, kTestURI);
+
+ await BrowserTestUtils.waitForCondition(() => seenAutoUpgradeMessage);
+
+ Services.console.unregisterListener(on_auto_upgrade_message);
+});
+
+function on_auto_upgrade_message(msgObj) {
+ const message = msgObj.message;
+
+ // The console message is:
+ // "Mixed Content: Upgrading insecure display request
+ // ‘http://example.com/file_mixed_content_auto_upgrade_display_console.jpg’ to use ‘https’"
+
+ if (!message.includes("Mixed Content:")) {
+ return;
+ }
+ ok(
+ message.includes("Upgrading insecure display request"),
+ "msg includes info"
+ );
+ ok(
+ message.includes("file_mixed_content_auto_upgrade_display_console.jpg"),
+ "msg includes file"
+ );
+ seenAutoUpgradeMessage = true;
+}
diff --git a/dom/security/test/mixedcontentblocker/browser_test_mixed_content_download.js b/dom/security/test/mixedcontentblocker/browser_test_mixed_content_download.js
new file mode 100644
index 0000000000..ee350008aa
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/browser_test_mixed_content_download.js
@@ -0,0 +1,341 @@
+ChromeUtils.defineESModuleGetters(this, {
+ Downloads: "resource://gre/modules/Downloads.sys.mjs",
+ DownloadsCommon: "resource:///modules/DownloadsCommon.sys.mjs",
+});
+
+const HandlerService = Cc[
+ "@mozilla.org/uriloader/handler-service;1"
+].getService(Ci.nsIHandlerService);
+
+const MIMEService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+
+let INSECURE_BASE_URL =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "http://example.com/"
+ ) + "download_page.html";
+let SECURE_BASE_URL =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+ ) + "download_page.html";
+
+function promiseFocus() {
+ return new Promise(resolve => {
+ waitForFocus(resolve);
+ });
+}
+
+function promisePanelOpened() {
+ if (DownloadsPanel.panel && DownloadsPanel.panel.state == "open") {
+ return Promise.resolve();
+ }
+ return BrowserTestUtils.waitForEvent(DownloadsPanel.panel, "popupshown");
+}
+
+async function task_openPanel() {
+ await promiseFocus();
+
+ let promise = promisePanelOpened();
+ DownloadsPanel.showPanel();
+ await promise;
+}
+
+const downloadMonitoringView = {
+ _listeners: [],
+ onDownloadAdded(download) {
+ for (let listener of this._listeners) {
+ listener(download);
+ }
+ this._listeners = [];
+ },
+ waitForDownload(listener) {
+ this._listeners.push(listener);
+ },
+};
+
+/**
+ * Waits until a download is triggered.
+ * Unless the always_ask_before_handling_new_types pref is true, the download
+ * will simply be saved, so resolve when the view is notified of the new
+ * download. Otherwise, it waits until a prompt is shown, selects the choosen
+ * <action>, then accepts the dialog
+ * @param [action] Which action to select, either:
+ * "handleInternally", "save" or "open".
+ * @returns {Promise} Resolved once done.
+ */
+
+function shouldTriggerDownload(action = "save") {
+ if (
+ Services.prefs.getBoolPref(
+ "browser.download.always_ask_before_handling_new_types"
+ )
+ ) {
+ return new Promise((resolve, reject) => {
+ Services.wm.addListener({
+ onOpenWindow(xulWin) {
+ Services.wm.removeListener(this);
+ let win = xulWin.docShell.domWindow;
+ waitForFocus(() => {
+ if (
+ win.location ==
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml"
+ ) {
+ let dialog = win.document.getElementById("unknownContentType");
+ let button = dialog.getButton("accept");
+ let actionRadio = win.document.getElementById(action);
+ actionRadio.click();
+ button.disabled = false;
+ dialog.acceptDialog();
+ resolve();
+ } else {
+ reject();
+ }
+ }, win);
+ },
+ });
+ });
+ }
+ return new Promise(res => {
+ downloadMonitoringView.waitForDownload(res);
+ });
+}
+
+const CONSOLE_ERROR_MESSAGE = "Blocked downloading insecure content";
+
+function shouldConsoleError() {
+ // Waits until CONSOLE_ERROR_MESSAGE was logged
+ return new Promise((resolve, reject) => {
+ function listener(msgObj) {
+ let text = msgObj.message;
+ if (text.includes(CONSOLE_ERROR_MESSAGE)) {
+ Services.console.unregisterListener(listener);
+ resolve();
+ }
+ }
+ Services.console.registerListener(listener);
+ });
+}
+
+async function resetDownloads() {
+ // Removes all downloads from the download List
+ const types = new Set();
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ let downloads = await publicList.getAll();
+ for (let download of downloads) {
+ if (download.contentType) {
+ types.add(download.contentType);
+ }
+ publicList.remove(download);
+ await download.finalize(true);
+ }
+
+ if (types.size) {
+ // reset handlers for the contentTypes of any files previously downloaded
+ for (let type of types) {
+ const mimeInfo = MIMEService.getFromTypeAndExtension(type, "");
+ info("resetting handler for type: " + type);
+ HandlerService.remove(mimeInfo);
+ }
+ }
+}
+
+function shouldNotifyDownloadUI() {
+ return new Promise(res => {
+ downloadMonitoringView.waitForDownload(async aDownload => {
+ let { error } = aDownload;
+ if (
+ error.becauseBlockedByReputationCheck &&
+ error.reputationCheckVerdict == Downloads.Error.BLOCK_VERDICT_INSECURE
+ ) {
+ // It's an insecure Download, now Check that it has been cleaned up properly
+ if ((await IOUtils.stat(aDownload.target.path)).size != 0) {
+ throw new Error(`Download target is not empty!`);
+ }
+ if ((await IOUtils.stat(aDownload.target.path)).size != 0) {
+ throw new Error(`Download partFile was not cleaned up properly`);
+ }
+ // Assert that the Referrer is presnt
+ if (!aDownload.source.referrerInfo) {
+ throw new Error("The Blocked download is missing the ReferrerInfo");
+ }
+
+ res(aDownload);
+ } else {
+ ok(false, "No error for download that was expected to error!");
+ }
+ });
+ });
+}
+
+async function runTest(url, link, checkFunction, description) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.block_download_insecure", true]],
+ });
+ await resetDownloads();
+
+ let tab = BrowserTestUtils.addTab(gBrowser, url);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Checking: " + description);
+
+ let checkPromise = checkFunction();
+ // Click the Link to trigger the download
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [link], contentLink => {
+ content.document.getElementById(contentLink).click();
+ });
+
+ await checkPromise;
+
+ ok(true, description);
+ BrowserTestUtils.removeTab(tab);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_setup(async () => {
+ let list = await Downloads.getList(Downloads.ALL);
+ list.addView(downloadMonitoringView);
+ registerCleanupFunction(() => list.removeView(downloadMonitoringView));
+});
+
+// Test Blocking
+add_task(async function test_blocking() {
+ for (let prefVal of [true, false]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.always_ask_before_handling_new_types", prefVal]],
+ });
+ await runTest(
+ INSECURE_BASE_URL,
+ "insecure",
+ shouldTriggerDownload,
+ "Insecure -> Insecure should download"
+ );
+ await runTest(
+ INSECURE_BASE_URL,
+ "secure",
+ shouldTriggerDownload,
+ "Insecure -> Secure should download"
+ );
+ await runTest(
+ SECURE_BASE_URL,
+ "insecure",
+ () =>
+ Promise.all([
+ shouldTriggerDownload(),
+ shouldNotifyDownloadUI(),
+ shouldConsoleError(),
+ ]),
+ "Secure -> Insecure should Error"
+ );
+ await runTest(
+ SECURE_BASE_URL,
+ "secure",
+ shouldTriggerDownload,
+ "Secure -> Secure should Download"
+ );
+ await SpecialPowers.popPrefEnv();
+ }
+});
+
+// Test Manual Unblocking
+add_task(async function test_manual_unblocking() {
+ for (let prefVal of [true, false]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.always_ask_before_handling_new_types", prefVal]],
+ });
+ await runTest(
+ SECURE_BASE_URL,
+ "insecure",
+ async () => {
+ let [, download] = await Promise.all([
+ shouldTriggerDownload(),
+ shouldNotifyDownloadUI(),
+ ]);
+ await download.unblock();
+ Assert.equal(
+ download.error,
+ null,
+ "There should be no error after unblocking"
+ );
+ },
+ "A Blocked Download Should succeeded to Download after a Manual unblock"
+ );
+ await SpecialPowers.popPrefEnv();
+ }
+});
+
+// Test Unblock Download Visible
+add_task(async function test_unblock_download_visible() {
+ for (let prefVal of [true, false]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.always_ask_before_handling_new_types", prefVal]],
+ });
+ // Focus, open and close the panel once
+ // to make sure the panel is loaded and ready
+ await promiseFocus();
+ await runTest(
+ SECURE_BASE_URL,
+ "insecure",
+ async () => {
+ let panelHasOpened = promisePanelOpened();
+ info("awaiting that the download is triggered and added to the list");
+ await Promise.all([shouldTriggerDownload(), shouldNotifyDownloadUI()]);
+ info("awaiting that the Download list shows itself");
+ await panelHasOpened;
+ DownloadsPanel.hidePanel();
+ ok(true, "The Download Panel should have opened on blocked download");
+ },
+ "A Blocked Download Should open the Download Panel"
+ );
+ await SpecialPowers.popPrefEnv();
+ }
+});
+
+// Test Download an insecure svg and choose "Open with Firefox"
+add_task(async function download_open_insecure_SVG() {
+ const mimeInfo = MIMEService.getFromTypeAndExtension("image/svg+xml", "svg");
+ mimeInfo.alwaysAskBeforeHandling = false;
+ mimeInfo.preferredAction = mimeInfo.handleInternally;
+ HandlerService.store(mimeInfo);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.always_ask_before_handling_new_types", false]],
+ });
+ await promiseFocus();
+ await runTest(
+ SECURE_BASE_URL,
+ "insecureSVG",
+ async () => {
+ info("awaiting that the download is triggered and added to the list");
+ let [_, download] = await Promise.all([
+ shouldTriggerDownload("handleInternally"),
+ shouldNotifyDownloadUI(),
+ ]);
+
+ let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+ await download.unblock();
+ Assert.equal(
+ download.error,
+ null,
+ "There should be no error after unblocking"
+ );
+
+ let tab = await newTabPromise;
+
+ ok(
+ tab.linkedBrowser._documentURI.filePath.includes(".svg"),
+ "The download target was opened"
+ );
+ BrowserTestUtils.removeTab(tab);
+ ok(true, "The Content was opened in a new tab");
+ await SpecialPowers.popPrefEnv();
+ },
+ "A Blocked SVG can be opened internally"
+ );
+
+ HandlerService.remove(mimeInfo);
+});
diff --git a/dom/security/test/mixedcontentblocker/download_page.html b/dom/security/test/mixedcontentblocker/download_page.html
new file mode 100644
index 0000000000..86f605c478
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/download_page.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=676619
+-->
+ <head>
+
+ <title>Test for the download attribute</title>
+
+ </head>
+ <body>
+ hi
+
+ <script>
+ const host = window.location.host;
+ const path = location.pathname.replace("download_page.html","download_server.sjs");
+
+ const secureLink = document.createElement("a");
+ secureLink.href=`https://${host}/${path}`;
+ secureLink.download="true";
+ secureLink.textContent="Secure Link";
+ secureLink.id="secure";
+
+ const insecureLink = document.createElement("a");
+ insecureLink.href=`http://${host}/${path}`;
+ insecureLink.download="true";
+ insecureLink.id="insecure";
+ insecureLink.textContent="Not secure Link";
+
+ const insecureSVG = document.createElement("a");
+ insecureSVG.href=`http://${host}/${path}?type=image/svg+xml&name=example.svg`;
+ insecureSVG.download="true";
+ insecureSVG.id="insecureSVG";
+ insecureSVG.textContent="Not secure Link to SVG";
+
+ document.body.append(insecureSVG);
+ document.body.append(secureLink);
+ document.body.append(insecureLink);
+ </script>
+ </body>
+</html>
diff --git a/dom/security/test/mixedcontentblocker/download_server.sjs b/dom/security/test/mixedcontentblocker/download_server.sjs
new file mode 100644
index 0000000000..e659df2f40
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/download_server.sjs
@@ -0,0 +1,20 @@
+// force the Browser to Show a Download Prompt
+
+function handleRequest(request, response) {
+ let type = "image/png";
+ let filename = "hello.png";
+ request.queryString.split("&").forEach(val => {
+ var [key, value] = val.split("=");
+ if (key == "type") {
+ type = value;
+ }
+ if (key == "name") {
+ filename = value;
+ }
+ });
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Disposition", `attachment; filename=${filename}`);
+ response.setHeader("Content-Type", type);
+ response.write("🙈🙊ðŸµðŸ™Š");
+}
diff --git a/dom/security/test/mixedcontentblocker/file_auth_download_page.html b/dom/security/test/mixedcontentblocker/file_auth_download_page.html
new file mode 100644
index 0000000000..931d9d00ad
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/file_auth_download_page.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Test mixed content download with auth header by https-first</title>
+ </head>
+ <body>
+ hi
+
+ <script>
+ const host = window.location.host;
+ const path = location.pathname.replace("file_auth_download_page.html","file_auth_download_server.sjs");
+
+ const insecureLink = document.createElement("a");
+ insecureLink.href = `http://${host}${path}?user=user&pass=pass`;
+ insecureLink.download = "true";
+ insecureLink.id = "insecure";
+ insecureLink.textContent = "Not secure Link";
+
+ document.body.append(insecureLink);
+ </script>
+ </body>
+</html>
diff --git a/dom/security/test/mixedcontentblocker/file_auth_download_server.sjs b/dom/security/test/mixedcontentblocker/file_auth_download_server.sjs
new file mode 100644
index 0000000000..1600d4aa12
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/file_auth_download_server.sjs
@@ -0,0 +1,61 @@
+"use strict";
+
+function handleRequest(request, response) {
+ let match;
+
+ // Allow the caller to drive how authentication is processed via the query.
+ // Eg, http://localhost:8888/authenticate.sjs?user=foo&realm=bar
+ // The extra ? allows the user/pass/realm checks to succeed if the name is
+ // at the beginning of the query string.
+ let query = new URLSearchParams(request.queryString);
+
+ let expected_user = query.get("user");
+ let expected_pass = query.get("pass");
+ let realm = query.get("realm");
+
+ // Look for an authentication header, if any, in the request.
+ //
+ // EG: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
+ //
+ // This test only supports Basic auth. The value sent by the client is
+ // "username:password", obscured with base64 encoding.
+
+ let actual_user = "",
+ actual_pass = "",
+ authHeader;
+ if (request.hasHeader("Authorization")) {
+ authHeader = request.getHeader("Authorization");
+ match = /Basic (.+)/.exec(authHeader);
+ if (match.length != 2) {
+ throw new Error("Couldn't parse auth header: " + authHeader);
+ }
+ // Decode base64 to string
+ let userpass = atob(match[1]);
+ match = /(.*):(.*)/.exec(userpass);
+ if (match.length != 3) {
+ throw new Error("Couldn't decode auth header: " + userpass);
+ }
+ actual_user = match[1];
+ actual_pass = match[2];
+ }
+
+ // Don't request authentication if the credentials we got were what we
+ // expected.
+ let requestAuth =
+ expected_user != actual_user || expected_pass != actual_pass;
+
+ if (requestAuth) {
+ response.setStatusLine("1.0", 401, "Authentication required");
+ response.setHeader("WWW-Authenticate", 'basic realm="' + realm + '"', true);
+ response.write("Authentication required");
+ } else {
+ response.setStatusLine("1.0", 200, "OK");
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader(
+ "Content-Disposition",
+ "attachment; filename=dummy-file.html"
+ );
+ response.setHeader("Content-Type", "text/html");
+ response.write("<p id='success'>SUCCESS</p>");
+ }
+}
diff --git a/dom/security/test/mixedcontentblocker/file_bug1550792.html b/dom/security/test/mixedcontentblocker/file_bug1550792.html
new file mode 100644
index 0000000000..30bdc5d5c5
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/file_bug1550792.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+ <iframe id="nestframe"></iframe>
+<script>
+ let nestframe = document.getElementById("nestframe");
+
+ window.addEventListener("message", (event) => {
+ parent.postMessage(event.data, "*");
+
+ // Only create second iframe once
+ if(event.data.type === "https") {
+ return;
+ }
+
+ nestframe.contentDocument.body.innerHTML = `
+ <iframe id="frametwo"
+ src=\"https://example.com\"
+ onload=\"parent.postMessage({status:'loaded', type: 'https'}, '*')\"
+ onerror=\"parent.postMessage({status:'blocked', type: 'https'}, '*')\"
+ ></iframe>`;
+ });
+
+ nestframe.onload = (event) => {
+ nestframe.contentDocument.body.innerHTML = `
+ <iframe id="frameone"
+ src=\"http://example.com\"
+ onload=\"parent.postMessage({status:'loaded', type: 'http'}, '*')\"
+ onerror=\"parent.postMessage({status:'blocked', type: 'http'}, '*')\"
+ ></iframe>`;
+ }
+
+ nestframe.src = "about:blank";
+</script>
+</body>
+</html>
diff --git a/dom/security/test/mixedcontentblocker/file_bug1551886.html b/dom/security/test/mixedcontentblocker/file_bug1551886.html
new file mode 100644
index 0000000000..41c46b5273
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/file_bug1551886.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+</head>
+
+<body>
+<script>
+ let f = document.createElement("iframe");
+ f.src = "data:text/html,<iframe src='http://example.com' onload=\"parent.postMessage({status:'loaded', type: 'http'}, 'https://example.com')\" onerror=\"parent.postMessage({status:'blocked', type: 'http'}, 'https://example.com')\"></iframe>";
+ window.addEventListener("message", (event) => {
+ parent.postMessage(event.data, "http://mochi.test:8888");
+
+ // Only create second iframe once
+ if(event.data.type === "https") {
+ return;
+ }
+
+ let f2 = document.createElement("iframe");
+ f2.src = "data:text/html,<iframe src='https://example.com' onload=\"parent.postMessage({status:'loaded', type: 'https'}, 'https://example.com')\" onerror=\"parent.postMessage({status:'blocked', type: 'https'}, 'https://example.com')\"></iframe>";
+ document.body.appendChild(f2);
+ });
+ document.body.appendChild(f);
+</script>
+</body>
+</html>
diff --git a/dom/security/test/mixedcontentblocker/file_bug803225_test_mailto.html b/dom/security/test/mixedcontentblocker/file_bug803225_test_mailto.html
new file mode 100644
index 0000000000..f1459d3667
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/file_bug803225_test_mailto.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Tests for Mixed Content Blocker - Mailto Protocol Compose Page
+https://bugzilla.mozilla.org/show_bug.cgi?id=803225
+-->
+<head> <meta charset="utf-8">
+</head>
+<body>
+Hello
+<script>window.close();</script>
+</body>
+</html>
diff --git a/dom/security/test/mixedcontentblocker/file_csp_block_all_mixedcontent_and_mixed_content_display_upgrade.html b/dom/security/test/mixedcontentblocker/file_csp_block_all_mixedcontent_and_mixed_content_display_upgrade.html
new file mode 100644
index 0000000000..80e97443ed
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/file_csp_block_all_mixedcontent_and_mixed_content_display_upgrade.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1806080 - ML2 with CSP block-all-mixed-content </title>
+ <meta http-equiv="Content-Security-Policy" content="block-all-mixed-content">
+</head>
+<body>
+ <!--upgradeable resources--->
+ <img id="some-img" src="http://test1.example.com/browser/dom/security/test/mixedcontentblocker/pass.png" width="100px">
+ <video id="some-video" src="http://test1.example.com/browser/dom/security/test/mixedcontentblocker/test.ogv" width="100px">
+ <audio id="some-audio" src="http://test1.example.com/browser/dom/security/test/mixedcontentblocker/test.wav" width="100px">
+</body>
+</html>
diff --git a/dom/security/test/mixedcontentblocker/file_frameNavigation.html b/dom/security/test/mixedcontentblocker/file_frameNavigation.html
new file mode 100644
index 0000000000..ae76ec806f
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/file_frameNavigation.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Tests for Mixed Content Blocker related to navigating children, grandchildren, etc
+https://bugzilla.mozilla.org/show_bug.cgi?id=840388
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tests for Mixed Content Frame Navigation</title>
+</head>
+<body>
+<div id="testContent"></div>
+
+<script>
+ var baseUrlHttps = "https://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html";
+
+ // For tests that require setTimeout, set the maximum polling time to 50 x 100ms = 5 seconds.
+ var MAX_COUNT = 50;
+ var TIMEOUT_INTERVAL = 100;
+
+ var testContent = document.getElementById("testContent");
+
+ // Test 1: Navigate secure iframe to insecure iframe on an insecure page
+ var iframe_test1 = document.createElement("iframe");
+ var counter_test1 = 0;
+ iframe_test1.src = baseUrlHttps + "?insecurePage_navigate_child";
+ iframe_test1.setAttribute("id", "test1");
+ iframe_test1.onerror = function() {
+ parent.postMessage({"test": "insecurePage_navigate_child", "msg": "got an onerror alert when loading or navigating testing iframe"}, "http://mochi.test:8888");
+ };
+ testContent.appendChild(iframe_test1);
+
+ function navigationStatus(iframe_test1)
+ {
+ // When the page is navigating, it goes through about:blank and we will get a permission denied for loc.
+ // Catch that specific exception and return
+ try {
+ var loc = document.getElementById("test1").contentDocument.location;
+ } catch(e) {
+ if (e.name === "SecurityError") {
+ // We received an exception we didn't expect.
+ throw e;
+ }
+ counter_test1++;
+ return;
+ }
+ if (loc == "http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html?insecurePage_navigate_child_response") {
+ return;
+ }
+ if (counter_test1 < MAX_COUNT) {
+ counter_test1++;
+ setTimeout(navigationStatus, TIMEOUT_INTERVAL, iframe_test1);
+ }
+ else {
+ // After we have called setTimeout the maximum number of times, assume navigating the iframe is blocked
+ parent.postMessage({"test": "insecurePage_navigate_child", "msg": "navigating to insecure iframe blocked on insecure page"}, "http://mochi.test:8888");
+ }
+ }
+
+ setTimeout(navigationStatus, TIMEOUT_INTERVAL, iframe_test1);
+
+ // Test 2: Navigate secure grandchild iframe to insecure grandchild iframe on a page that has no secure parents
+ var iframe_test2 = document.createElement("iframe");
+ iframe_test2.src = "http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_grandchild.html"
+ iframe_test2.onerror = function() {
+ parent.postMessage({"test": "insecurePage_navigate_grandchild", "msg": "got an on error alert when loading or navigating testing iframe"}, "http://mochi.test:8888");
+ };
+ testContent.appendChild(iframe_test2);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/mixedcontentblocker/file_frameNavigation_blankTarget.html b/dom/security/test/mixedcontentblocker/file_frameNavigation_blankTarget.html
new file mode 100644
index 0000000000..c4502ce8d2
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/file_frameNavigation_blankTarget.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Tests for Mixed Content Blocker - Opening link with _blank target in an https iframe.
+https://bugzilla.mozilla.org/show_bug.cgi?id=841850
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tests for Mixed Content Frame Navigation</title>
+</head>
+<body>
+<a href="http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html?blankTarget" id="blankTarget" target="_blank" rel="opener">Go to http site</a>
+
+<script>
+ var blankTarget = document.getElementById("blankTarget");
+ blankTarget.click();
+
+ var observer = {
+ observe(subject, topic, data) {
+ if (topic === "specialpowers-http-notify-request" &&
+ data === "http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html?blankTarget") {
+ parent.parent.postMessage({"test": "blankTarget", "msg": "opened an http link with target=_blank from a secure page"}, "http://mochi.test:8888");
+ SpecialPowers.removeObserver(observer, "specialpowers-http-notify-request");
+ }
+ }
+ }
+
+ // This is a special observer topic that is proxied from http-on-modify-request
+ // in the parent process to inform us when a URI is loaded
+ SpecialPowers.addObserver(observer, "specialpowers-http-notify-request");
+</script>
+</body>
+</html>
diff --git a/dom/security/test/mixedcontentblocker/file_frameNavigation_grandchild.html b/dom/security/test/mixedcontentblocker/file_frameNavigation_grandchild.html
new file mode 100644
index 0000000000..e88033dae5
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/file_frameNavigation_grandchild.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Tests for Mixed Content Blocker - Navigating Grandchild frames when a secure parent doesn't exist
+https://bugzilla.mozilla.org/show_bug.cgi?id=840388
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tests for Mixed Content Frame Navigation</title>
+</head>
+<body>
+<iframe src="https://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html?insecurePage_navigate_grandchild" id="child"></iframe>
+
+<script>
+ // For tests that require setTimeout, set the maximum polling time to 100 x 100ms = 10 seconds.
+ var MAX_COUNT = 50;
+ var TIMEOUT_INTERVAL = 100;
+ var counter = 0;
+
+ var child = document.getElementById("child");
+ function navigationStatus(child)
+ {
+ // When the page is navigating, it goes through about:blank and we will get a permission denied for loc.
+ // Catch that specific exception and return
+ try {
+ var loc;
+ if (child.contentDocument) {
+ loc = child.contentDocument.location;
+ }
+ } catch(e) {
+ if (e.message && !e.message.includes("Permission denied to access property")) {
+ // We received an exception we didn't expect.
+ throw e;
+ }
+ counter++;
+ return;
+ }
+ if (loc == "http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html?insecurePage_navigate_grandchild_response") {
+ return;
+ }
+ if(counter < MAX_COUNT) {
+ counter++;
+ setTimeout(navigationStatus, TIMEOUT_INTERVAL, child);
+ }
+ else {
+ // After we have called setTimeout the maximum number of times, assume navigating the iframe is blocked
+ parent.parent.postMessage({"test": "insecurePage_navigate_grandchild", "msg": "navigating to insecure grandchild iframe blocked on insecure page"}, "http://mochi.test:8888");
+ }
+ }
+
+ setTimeout(navigationStatus, TIMEOUT_INTERVAL, child);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html b/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html
new file mode 100644
index 0000000000..251bb73e33
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+<div id="content"></div>
+<script>
+ // get the case from the query string
+ var type = location.search.substring(1);
+
+ function clickLink() {
+ // If we don't reflow before clicking the link, the test will fail intermittently. The reason is still unknown. We'll track this issue in bug 1259715.
+ requestAnimationFrame(function() {
+ setTimeout(function() {
+ document.getElementById("link").click();
+ }, 0);
+ });
+ }
+
+ switch (type) {
+ case "insecurePage_navigate_child":
+ document.getElementById("content").innerHTML =
+ '<a href="http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html?insecurePage_navigate_child_response" id="link">Testing\<\/a>';
+ clickLink();
+ break;
+
+ case "insecurePage_navigate_child_response":
+ parent.parent.postMessage({"test": "insecurePage_navigate_child", "msg": "navigated to insecure iframe on insecure page"}, "http://mochi.test:8888");
+ document.getElementById("content").innerHTML = "Navigated from secure to insecure frame on an insecure page";
+ break;
+
+ case "insecurePage_navigate_grandchild":
+ document.getElementById("content").innerHTML =
+ '<a href="http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html?insecurePage_navigate_grandchild_response" id="link">Testing\<\/a>';
+ clickLink();
+ break;
+
+ case "insecurePage_navigate_grandchild_response":
+ parent.parent.parent.postMessage({"test": "insecurePage_navigate_grandchild", "msg": "navigated to insecure grandchild iframe on insecure page"}, "http://mochi.test:8888");
+ document.getElementById("content").innerHTML = "Navigated from secure to insecure grandchild frame on an insecure page";
+ break;
+
+ case "securePage_navigate_child":
+ document.getElementById("content").innerHTML =
+ '<a href="http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html?securePage_navigate_child_response" id="link">Testing\<\/a>';
+ clickLink();
+ break;
+
+ case "securePage_navigate_child_response":
+ document.getElementById("content").innerHTML = "<p>Navigated from secure to insecure frame on a secure page</p>";
+ parent.parent.postMessage({"test": "securePage_navigate_child", "msg": "navigated to insecure iframe on secure page"}, "http://mochi.test:8888");
+ break;
+
+ case "securePage_navigate_grandchild":
+ document.getElementById("content").innerHTML=
+ '<a href="http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html?securePage_navigate_grandchild_response" id="link">Testing\<\/a>';
+ clickLink();
+ break;
+
+ case "securePage_navigate_grandchild_response":
+ parent.parent.parent.postMessage({"test": "securePage_navigate_grandchild", "msg": "navigated to insecure grandchild iframe on secure page"}, "http://mochi.test:8888");
+ document.getElementById("content").innerHTML = "<p>Navigated from secure to insecure grandchild frame on a secure page</p>";
+ break;
+
+ case "blankTarget":
+ window.close();
+ break;
+
+ default:
+ document.getElementById("content").innerHTML = "Hello";
+ break;
+ }
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/mixedcontentblocker/file_frameNavigation_secure.html b/dom/security/test/mixedcontentblocker/file_frameNavigation_secure.html
new file mode 100644
index 0000000000..0502c0c9dd
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/file_frameNavigation_secure.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Tests for Mixed Content Blocker related to navigating children, grandchildren, etc
+https://bugzilla.mozilla.org/show_bug.cgi?id=840388
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tests for Mixed Content Frame Navigation</title>
+</head>
+<body>
+<div id="testContent"></div>
+
+<script>
+ var baseUrl = "https://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html";
+
+ // For tests that require setTimeout, set the maximum polling time to 50 x 100ms = 5 seconds.
+ var MAX_COUNT = 50;
+ var TIMEOUT_INTERVAL = 100;
+
+ var testContent = document.getElementById("testContent");
+
+ // Test 1: Navigate secure iframe to insecure iframe on a secure page
+ var iframe_test1 = document.createElement("iframe");
+ var counter_test1 = 0;
+ iframe_test1.setAttribute("id", "test1");
+ iframe_test1.src = baseUrl + "?securePage_navigate_child";
+ iframe_test1.onerror = function() {
+ parent.postMessage({"test": "securePage_navigate_child", "msg": "navigating to insecure iframe blocked on secure page"}, "http://mochi.test:8888");
+ };
+ testContent.appendChild(iframe_test1);
+
+ function navigationStatus(iframe_test1)
+ {
+ // When the page is navigating, it goes through about:blank and we will get a permission denied for loc.
+ // Catch that specific exception and return
+ try {
+ var loc = document.getElementById("test1").contentDocument.location;
+ } catch(e) {
+ if (e.name === "SecurityError") {
+ // We received an exception we didn't expect.
+ throw e;
+ }
+ counter_test1++;
+ return;
+ }
+ if (loc == "http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html?insecurePage_navigate_child_response") {
+ return;
+ }
+ if (counter_test1 < MAX_COUNT) {
+ counter_test1++;
+ setTimeout(navigationStatus, TIMEOUT_INTERVAL, iframe_test1);
+ }
+ else {
+ // After we have called setTimeout the maximum number of times, assume navigating the iframe is blocked
+ parent.postMessage({"test": "securePage_navigate_child", "msg": "navigating to insecure iframe blocked on secure page"}, "http://mochi.test:8888");
+ }
+ }
+
+ setTimeout(navigationStatus, TIMEOUT_INTERVAL, iframe_test1);
+
+ // Test 2: Open an http page in a new tab from a link click with target=_blank.
+ var iframe_test3 = document.createElement("iframe");
+ iframe_test3.src = "https://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_blankTarget.html";
+ iframe_test3.onerror = function() {
+ parent.postMessage({"test": "blankTarget", "msg": "got an onerror event when loading or navigating testing iframe"}, "http://mochi.test:8888");
+ };
+ testContent.appendChild(iframe_test3);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/mixedcontentblocker/file_frameNavigation_secure_grandchild.html b/dom/security/test/mixedcontentblocker/file_frameNavigation_secure_grandchild.html
new file mode 100644
index 0000000000..bbdfe2beab
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/file_frameNavigation_secure_grandchild.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Tests for Mixed Content Blocker - Navigating Grandchild Frames when a secure parent exists
+https://bugzilla.mozilla.org/show_bug.cgi?id=840388
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tests for Mixed Content Frame Navigation</title>
+</head>
+<body>
+
+<iframe src="https://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html?securePage_navigate_grandchild" id="child"></iframe>
+<script>
+ // For tests that require setTimeout, set the maximum polling time to 50 x 100ms = 5 seconds.
+ var MAX_COUNT = 50;
+ var TIMEOUT_INTERVAL = 100;
+ var counter = 0;
+
+ var child = document.getElementById("child");
+ function navigationStatus(child)
+ {
+ var loc;
+ // When the page is navigating, it goes through about:blank and we will get a permission denied for loc.
+ // Catch that specific exception and return
+ try {
+ loc = document.getElementById("child").contentDocument.location;
+ } catch(e) {
+ if (e.message && !e.message.includes("Permission denied to access property")) {
+ // We received an exception we didn't expect.
+ throw e;
+ }
+ counter++;
+ return;
+ }
+ if (loc == "http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html?securePage_navigate_grandchild_response") {
+ return;
+ }
+ if (counter < MAX_COUNT) {
+ counter++;
+ setTimeout(navigationStatus, TIMEOUT_INTERVAL, child);
+ }
+ else {
+ // After we have called setTimeout the maximum number of times, assume navigating the iframe is blocked
+ dump("\nThe current location of the grandchild iframe is: "+loc+".\n");
+ dump("\nWe have past the maximum timeout. Navigating a grandchild iframe from an https location to an http location on a secure page failed. We are about to post message to the top level page\n");
+ parent.parent.postMessage({"test": "securePage_navigate_grandchild", "msg": "navigating to insecure grandchild iframe blocked on secure page"}, "http://mochi.test:8888");
+ dump("\nAttempted postMessage\n");
+ }
+ }
+
+ setTimeout(navigationStatus, TIMEOUT_INTERVAL, child);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/mixedcontentblocker/file_main.html b/dom/security/test/mixedcontentblocker/file_main.html
new file mode 100644
index 0000000000..5bbbf0ec3a
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/file_main.html
@@ -0,0 +1,336 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Tests for Mixed Content Blocker
+https://bugzilla.mozilla.org/show_bug.cgi?id=62178
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tests for Bug 62178</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<div id="testContent"></div>
+
+<!-- types the Mixed Content Blocker can block
+ /*
+ switch (aContentType) {
+ case nsIContentPolicy::TYPE_OBJECT:
+ case nsIContentPolicy::TYPE_SCRIPT:
+ case nsIContentPolicy::TYPE_STYLESHEET:
+ case nsIContentPolicy::TYPE_SUBDOCUMENT:
+ case nsIContentPolicy::TYPE_XMLHTTPREQUEST:
+
+ case nsIContentPolicy::TYPE_FONT: - NO TEST:
+ Load events for external fonts are not detectable by javascript.
+ case nsIContentPolicy::TYPE_WEBSOCKET: - NO TEST:
+ websocket connections over https require an encrypted websocket protocol (wss:)
+
+ case nsIContentPolicy::TYPE_IMAGE:
+ case nsIContentPolicy::TYPE_IMAGESET:
+ case nsIContentPolicy::TYPE_MEDIA:
+ case nsIContentPolicy::TYPE_PING:
+ our ping implementation is off by default and does not comply with the current spec (bug 786347)
+ case nsIContentPolicy::TYPE_BEACON:
+
+ }
+ */
+-->
+
+<script>
+ function ok(a, msg) {
+ parent.postMessage({msg, check: true, status: !!a}, "http://mochi.test:8888");
+ }
+
+ function is(a, b, msg) {
+ ok(a == b, msg);
+ }
+
+ function report(type, msg) {
+ parent.postMessage({test: type, msg}, "http://mochi.test:8888");
+ }
+
+ function uniqueID() {
+ return Math.floor(Math.random() * 100000)
+ }
+ function uniqueIDParam(id) {
+ return "&uniqueID=" + id;
+ }
+
+ async function init() {
+ var baseUrl = "http://example.com/tests/dom/security/test/mixedcontentblocker/file_server.sjs";
+ var checkLastRequestUrl = "https://example.com/tests/dom/security/test/mixedcontentblocker/file_server.sjs?lastRequest=true";
+
+ //For tests that require setTimeout, set the maximum polling time to 100 x 100ms = 10 seconds.
+ var MAX_COUNT = 100;
+ var TIMEOUT_INTERVAL = 100;
+
+ var testContent = document.getElementById("testContent");
+
+ async function checkLastRequest() {
+ const response = await fetch(checkLastRequestUrl);
+ return response.json();
+ }
+
+ /* Part 1: Mixed Script tests */
+
+ // Test 1a: insecure object
+ var object = document.createElement("object");
+ var objectId = uniqueID();
+ object.data = baseUrl + "?type=object" + uniqueIDParam(objectId);
+ object.type = "image/png";
+ object.width = "200";
+ object.height = "200";
+
+ testContent.appendChild(object);
+
+ var objectCount = 0;
+
+ function objectStatus(object) {
+ // Expose our privileged bits on the object. We will match the MIME type to the one
+ // provided by file_server.sjs
+ object = SpecialPowers.wrap(object);
+ var typeIsSet = object.actualType && (object.actualType !== '');
+ var isLoaded = typeIsSet && object.actualType === 'application/x-test-match';
+ if (isLoaded) {
+ //object loaded
+ report("object", "insecure object loaded");
+ }
+ else {
+ if(!typeIsSet && objectCount < MAX_COUNT) {
+ objectCount++;
+ setTimeout(objectStatus, TIMEOUT_INTERVAL, object);
+ }
+ else {
+ //After we have called setTimeout the maximum number of times, assume object is blocked
+ report("object", "insecure object blocked");
+ }
+ }
+ }
+
+ // object does not have onload and onerror events. Hence we need a setTimeout to check the object's status
+ setTimeout(objectStatus, TIMEOUT_INTERVAL, object);
+
+ // Test 1b: insecure script
+ var script = document.createElement("script");
+ var scriptId = uniqueID();
+ var scriptLoad = false;
+ var scriptCount = 0;
+ script.src = baseUrl + "?type=script" + uniqueIDParam(scriptId);
+ script.onload = function(e) {
+ report("script", "insecure script loaded");
+ scriptLoad = true;
+ }
+ testContent.appendChild(script);
+
+ function scriptStatus(script)
+ {
+ if(scriptLoad) {
+ return;
+ }
+ if (scriptCount < MAX_COUNT) {
+ scriptCount++;
+ setTimeout(scriptStatus, TIMEOUT_INTERVAL, script);
+ }
+ else {
+ //After we have called setTimeout the maximum number of times, assume script is blocked
+ report("script", "insecure script blocked");
+ }
+ }
+
+ // scripts blocked by Content Policy's do not have onerror events (see bug 789856). Hence we need a setTimeout to check the script's status
+ setTimeout(scriptStatus, TIMEOUT_INTERVAL, script);
+
+
+ // Test 1c: insecure stylesheet
+ var cssStyleSheet = document.createElement("link");
+ var cssStyleSheetId = uniqueID();
+ cssStyleSheet.rel = "stylesheet";
+ cssStyleSheet.href = baseUrl + "?type=stylesheet" + uniqueIDParam(cssStyleSheetId);
+ cssStyleSheet.type = "text/css";
+ testContent.appendChild(cssStyleSheet);
+
+ var styleCount = 0;
+
+ function styleStatus(cssStyleSheet) {
+ if( cssStyleSheet.sheet || cssStyleSheet.styleSheet || cssStyleSheet.innerHTML ) {
+ report("stylesheet", "insecure stylesheet loaded");
+ }
+ else {
+ if(styleCount < MAX_COUNT) {
+ styleCount++;
+ setTimeout(styleStatus, TIMEOUT_INTERVAL, cssStyleSheet);
+ }
+ else {
+ //After we have called setTimeout the maximum number of times, assume stylesheet is blocked
+ report("stylesheet", "insecure stylesheet blocked");
+ }
+ }
+ }
+
+ // link does not have onload and onerror events. Hence we need a setTimeout to check the link's status
+ window.setTimeout(styleStatus, TIMEOUT_INTERVAL, cssStyleSheet);
+
+ // Test 1d: insecure iframe
+ var iframe = document.createElement("iframe");
+ var iframeId = uniqueID();
+ iframe.src = baseUrl + "?type=iframe" + uniqueIDParam(iframeId);
+ iframe.onload = function() {
+ report("iframe", "insecure iframe loaded");
+ }
+ iframe.onerror = function() {
+ report("iframe", "insecure iframe blocked");
+ };
+ testContent.appendChild(iframe);
+
+
+ // Test 1e: insecure xhr
+ await new Promise((resolve) => {
+ var xhr = new XMLHttpRequest;
+ var xhrId = uniqueID();
+ try {
+ xhr.open("GET", baseUrl + "?type=xhr" + uniqueIDParam(xhrId), true);
+ xhr.send();
+ xhr.onloadend = function (oEvent) {
+ if (xhr.status == 200) {
+ report("xhr", "insecure xhr loaded");
+ resolve();
+ }
+ else {
+ report("xhr", "insecure xhr blocked");
+ resolve();
+ }
+ }
+ } catch(ex) {
+ report("xhr", "insecure xhr blocked");
+ resolve();
+ }
+ });
+
+ /* Part 2: Mixed Display tests */
+
+ // Shorthand for all image test variants
+ async function imgHandlers(img, test, uniqueID) {
+ await new Promise((resolve) => {
+ img.onload = async () => {
+ const lastRequest = await checkLastRequest();
+ is(lastRequest.uniqueID, uniqueID, "UniqueID for the last request matches");
+ let message = "insecure image loaded";
+ if (lastRequest.scheme == "https") {
+ message = "secure image loaded after upgrade";
+ }
+ report(test, message);
+ resolve();
+ }
+ img.onerror = async () => {
+ let message = "insecure image blocked";
+ report(test, message);
+ resolve();
+ }
+ });
+ }
+
+ // Test 2a: insecure image
+ var img = document.createElement("img");
+ var imgId = uniqueID();
+ img.src = baseUrl + "?type=img" + uniqueIDParam(imgId);
+ await imgHandlers(img, "image", imgId);
+ // We don't need to append the image to the document. Doing so causes the image test to run twice.
+
+ // Test 2b: insecure media
+ var media = document.createElement("video");
+ var mediaId = uniqueID();
+ media.src = baseUrl + "?type=media" + uniqueIDParam(mediaId);
+ media.width = "320";
+ media.height = "200";
+ media.type = "video/ogg";
+ await new Promise(res => {
+ media.onloadeddata = async () => {
+ const lastRequest = await checkLastRequest();
+ is(lastRequest.uniqueID, mediaId, "UniqueID for the last request matches");
+ let message = "insecure media loaded";
+ if (lastRequest.scheme == "https") {
+ message = "secure media loaded after upgrade";
+ }
+ report("media", message);
+ res();
+ }
+ media.onerror = function() {
+ report("media", "insecure media blocked");
+ res();
+ }
+ });
+ // We don't need to append the video to the document. Doing so causes the image test to run twice.
+
+ /* Part 3: Mixed Active Tests for Image srcset */
+
+ // Test 3a: image with srcset
+ var imgA = document.createElement("img");
+ var imgAId = uniqueID();
+ imgA.srcset = baseUrl + "?type=img&subtype=imageSrcset" + uniqueIDParam(imgAId);
+ await imgHandlers(imgA, "imageSrcset", imgAId);
+
+ // Test 3b: image with srcset, using fallback from src, should still use imageset policy
+ var imgB = document.createElement("img");
+ var imgBId = uniqueID();
+ imgB.srcset = " ";
+ imgB.src = baseUrl + "?type=img&subtype=imageSrcSetFallback" + uniqueIDParam(imgBId);
+ await imgHandlers(imgB, "imageSrcsetFallback", imgBId);
+
+ // Test 3c: image in <picture>
+ var imgC = document.createElement("img");
+ var pictureC = document.createElement("picture");
+ var sourceC = document.createElement("source");
+ var sourceCId = uniqueID();
+ sourceC.srcset = baseUrl + "?type=img&subtype=imagePicture" + uniqueIDParam(sourceCId);
+ pictureC.appendChild(sourceC);
+ pictureC.appendChild(imgC);
+ await imgHandlers(imgC, "imagePicture", sourceCId);
+
+ // Test 3d: Loaded basic image switching to a <picture>, loading
+ // same source, should still redo the request with new
+ // policy.
+ var imgD = document.createElement("img");
+ var imgDId = uniqueID();
+ var sourceDId = uniqueID();
+ imgD.src = baseUrl + "?type=img&subtype=imageJoinPicture" + uniqueIDParam(imgDId);
+ await new Promise(res => {
+ imgD.onload = imgD.onerror = function() {
+ // Whether or not it loads, we want to now append it to a picture and observe
+ var pictureD = document.createElement("picture");
+ var sourceD = document.createElement("source");
+ sourceD.srcset = baseUrl + "?type=img&subtype=imageJoinPicture2" + uniqueIDParam(sourceDId);
+ pictureD.appendChild(sourceD);
+ pictureD.appendChild(imgD);
+ res();
+ }
+ });
+ await imgHandlers(imgD, "imageJoinPicture", sourceDId);
+
+ // Test 3e: img load from <picture> source reverts to img.src as it
+ // is removed -- the new request should revert to mixed
+ // display policy
+ var imgE = document.createElement("img");
+ var pictureE = document.createElement("picture");
+ var pictureEId = uniqueID();
+ var sourceE = document.createElement("source");
+ var sourceEId = uniqueID();
+ sourceE.srcset = baseUrl + "?type=img&subtype=imageLeavePicture" + uniqueIDParam(sourceEId);
+ pictureE.appendChild(sourceE);
+ pictureE.appendChild(imgE);
+ imgE.src = baseUrl + "?type=img&subtype=imageLeavePicture2" + uniqueIDParam(pictureEId);
+ await new Promise(res => {
+ imgE.onload = imgE.onerror = function() {
+ // Whether or not it loads, remove it from the picture and observe
+ pictureE.removeChild(imgE)
+ res();
+ }
+ });
+ await imgHandlers(imgE, "imageLeavePicture", pictureEId);
+ }
+
+ init();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/mixedcontentblocker/file_main_bug803225.html b/dom/security/test/mixedcontentblocker/file_main_bug803225.html
new file mode 100644
index 0000000000..1363a68886
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/file_main_bug803225.html
@@ -0,0 +1,175 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Tests for Mixed Content Blocker - Allowed Protocols
+https://bugzilla.mozilla.org/show_bug.cgi?id=803225
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tests for Bug 62178</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<div id="testContent"></div>
+
+<!-- Test additional schemes the Mixed Content Blocker should not block
+ "about" protocol URIs that are URI_SAFE_FOR_UNTRUSTED_CONTENT (moz-safe-about; see nsAboutProtocolHandler::NewURI
+ "data",
+ "javascript",
+ "mailto",
+ "resource",
+ "wss"
+-->
+
+<script>
+ const { AppConstants } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+ );
+
+ //For tests that require setTimeout, set the timeout interval
+ var TIMEOUT_INTERVAL = 100;
+
+ var testContent = document.getElementById("testContent");
+
+ // Test 1 & 2: about and javascript protcols within an iframe
+ var data = Array(2,2);
+ var protocols = [
+ ["about", ""], //When no source is specified, the frame gets a source of about:blank
+ ["javascript", "javascript:document.open();document.write='<h1>SUCCESS</h1>';document.close();"],
+ ];
+ for(var i=0; i < protocols.length; i++)
+ {
+ var generic_frame = document.createElement("iframe");
+ generic_frame.src = protocols[i][1];
+ generic_frame.name="generic_protocol";
+
+ generic_frame.onload = function(i) {
+ data = {"test": protocols[i][0], "msg": "resource with " + protocols[i][0] + " protocol loaded"};
+ parent.postMessage(data, "http://mochi.test:8888");
+ }.bind(generic_frame, i)
+
+ generic_frame.onerror = function(i) {
+ data = {"test": protocols[i][0], "msg": "resource with " + protocols[i][0] + " protocol did not load"};
+ parent.postMessage(data, "http://mochi.test:8888");
+ }.bind(generic_frame, i);
+
+ testContent.appendChild(generic_frame, i);
+ }
+
+ // Test 3: for resource within a script tag
+ // Note: the script we load throws an exception, but the script element's
+ // onload listener is called after we successfully fetch the script,
+ // independently of whether it throws an exception.
+ var resource_script=document.createElement("script");
+ resource_script.src = "resource://gre/modules/XPCOMUtils.sys.mjs";
+ resource_script.name = "resource_protocol";
+ resource_script.onload = function() {
+ parent.postMessage({"test": "resource", "msg": "resource with resource protocol loaded"}, "http://mochi.test:8888");
+ }
+ resource_script.onerror = function() {
+ parent.postMessage({"test": "resource", "msg": "resource with resource protocol did not load"}, "http://mochi.test:8888");
+ }
+
+ testContent.appendChild(resource_script);
+
+ // Test 4: about unsafe protocol within an iframe
+ var unsafe_about_frame = document.createElement("iframe");
+ unsafe_about_frame.src = "about:config";
+ unsafe_about_frame.name = "unsafe_about_protocol";
+ unsafe_about_frame.onload = function() {
+ parent.postMessage({"test": "unsafe_about", "msg": "resource with unsafe about protocol loaded"}, "http://mochi.test:8888");
+ }
+ unsafe_about_frame.onerror = function() {
+ parent.postMessage({"test": "unsafe_about", "msg": "resource with unsafe about protocol did not load"}, "http://mochi.test:8888");
+ }
+ testContent.appendChild(unsafe_about_frame);
+
+ // Test 5: data protocol within a script tag
+ var x = 2;
+ var newscript = document.createElement("script");
+ newscript.src= "data:text/javascript,var x = 4;";
+ newscript.onload = function() {
+ parent.postMessage({"test": "data_protocol", "msg": "resource with data protocol loaded"}, "http://mochi.test:8888");
+ }
+ newscript.onerror = function() {
+ parent.postMessage({"test": "data_protocol", "msg": "resource with data protocol did not load"}, "http://mochi.test:8888");
+ }
+ testContent.appendChild(newscript);
+
+ // Test 6: mailto protocol
+ let mm = SpecialPowers.loadChromeScript(function launchHandler() {
+ /* eslint-env mozilla/chrome-script */
+ var ioService = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService);
+
+ var webHandler = Cc["@mozilla.org/uriloader/web-handler-app;1"].
+ createInstance(Ci.nsIWebHandlerApp);
+ webHandler.name = "Web Handler";
+ webHandler.uriTemplate = "http://example.com/tests/dom/security/test/mixedcontentblocker/file_bug803225_test_mailto.html?s=%";
+
+ Services.ppmm.addMessageListener("Test:content-ready", function contentReadyListener() {
+ Services.ppmm.removeMessageListener("Test:content-ready", contentReadyListener);
+ sendAsyncMessage("Test:content-ready-forward");
+ Services.ppmm.removeDelayedProcessScript(pScript);
+ })
+
+ var pScript = "data:,new " + function () {
+ /* eslint-env mozilla/process-script */
+ var os = Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService);
+ var observer = {
+ observe(subject, topic, data) {
+ if (topic == "content-document-global-created" && data == "http://example.com") {
+ sendAsyncMessage("Test:content-ready");
+ os.removeObserver(observer, "content-document-global-created");
+ }
+ }
+ };
+ os.addObserver(observer, "content-document-global-created");
+ }
+
+ Services.ppmm.loadProcessScript(pScript, true);
+
+ var uri = ioService.newURI("mailto:foo@bar.com");
+ webHandler.launchWithURI(uri);
+ });
+
+ var mailto = false;
+
+ mm.addMessageListener("Test:content-ready-forward", function contentReadyListener() {
+ mm.removeMessageListener("Test:content-ready-forward", contentReadyListener);
+ mailto = true;
+ parent.postMessage({"test": "mailto", "msg": "resource with mailto protocol loaded"}, "http://mochi.test:8888");
+ });
+
+ function mailtoProtocolStatus() {
+ if(!mailto) {
+ //There is no onerror event associated with the WebHandler, and hence we need a setTimeout to check the status
+ setTimeout(mailtoProtocolStatus, TIMEOUT_INTERVAL);
+ }
+ }
+
+ mailtoProtocolStatus();
+
+ // Test 7: wss protocol
+ // WebSocket tests are not supported on Android Yet. Bug 1566168.
+ if (AppConstants.platform !== "android") {
+ var wss;
+ wss = new WebSocket("wss://example.com/tests/dom/security/test/mixedcontentblocker/file_main_bug803225_websocket");
+
+ var status_wss = "started";
+ wss.onopen = function(e) {
+ status_wss = "opened";
+ wss.close();
+ }
+ wss.onclose = function(e) {
+ if(status_wss == "opened") {
+ parent.postMessage({"test": "wss", "msg": "resource with wss protocol loaded"}, "http://mochi.test:8888");
+ } else {
+ parent.postMessage({"test": "wss", "msg": "resource with wss protocol did not load"}, "http://mochi.test:8888");
+ }
+ }
+ }
+</script>
+</body>
+</html>
diff --git a/dom/security/test/mixedcontentblocker/file_main_bug803225_websocket_wsh.py b/dom/security/test/mixedcontentblocker/file_main_bug803225_websocket_wsh.py
new file mode 100644
index 0000000000..b7159c742b
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/file_main_bug803225_websocket_wsh.py
@@ -0,0 +1,6 @@
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ pass
diff --git a/dom/security/test/mixedcontentblocker/file_mixed_content_auto_upgrade_display_console.html b/dom/security/test/mixedcontentblocker/file_mixed_content_auto_upgrade_display_console.html
new file mode 100644
index 0000000000..b86fbc9cbc
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/file_mixed_content_auto_upgrade_display_console.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1673574 - Improve Console logging for mixed content auto upgrading</title>
+</head>
+<body>
+ <!-- The following file does in fact not exist because we only care if it shows up in the console -->
+ <img src="http://example.com/file_mixed_content_auto_upgrade_display_console.jpg">
+</body>
+</html>
diff --git a/dom/security/test/mixedcontentblocker/file_redirect.html b/dom/security/test/mixedcontentblocker/file_redirect.html
new file mode 100644
index 0000000000..99e1873791
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/file_redirect.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug1402363: Test mixed content redirects</title>
+</head>
+<body>
+
+<script type="text/javascript">
+ const PATH = "https://example.com/tests/dom/security/test/mixedcontentblocker/";
+
+ // check a fetch redirect from https to https (should be allowed)
+ fetch(PATH + "file_redirect_handler.sjs?https-to-https-redirect", {
+ method: 'get'
+ }).then(function(response) {
+ window.parent.postMessage("https-to-https-loaded", "*");
+ }).catch(function(err) {
+ window.parent.postMessage("https-to-https-blocked", "*");
+ });
+
+ // check a fetch redirect from https to http (should be blocked)
+ fetch(PATH + "file_redirect_handler.sjs?https-to-http-redirect", {
+ method: 'get'
+ }).then(function(response) {
+ window.parent.postMessage("https-to-http-loaded", "*");
+ }).catch(function(err) {
+ window.parent.postMessage("https-to-http-blocked", "*");
+ });
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/mixedcontentblocker/file_redirect_handler.sjs b/dom/security/test/mixedcontentblocker/file_redirect_handler.sjs
new file mode 100644
index 0000000000..f4ed278675
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/file_redirect_handler.sjs
@@ -0,0 +1,34 @@
+// custom *.sjs file for
+// Bug 1402363: Test Mixed Content Redirect Blocking.
+
+const URL_PATH = "example.com/tests/dom/security/test/mixedcontentblocker/";
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+ let queryStr = request.queryString;
+
+ if (queryStr === "https-to-https-redirect") {
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader(
+ "Location",
+ "https://" + URL_PATH + "file_redirect_handler.sjs?load",
+ false
+ );
+ return;
+ }
+
+ if (queryStr === "https-to-http-redirect") {
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader(
+ "Location",
+ "http://" + URL_PATH + "file_redirect_handler.sjs?load",
+ false
+ );
+ return;
+ }
+
+ if (queryStr === "load") {
+ response.setHeader("Content-Type", "text/html", false);
+ response.write("foo");
+ }
+}
diff --git a/dom/security/test/mixedcontentblocker/file_server.sjs b/dom/security/test/mixedcontentblocker/file_server.sjs
new file mode 100644
index 0000000000..4f86c282ee
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/file_server.sjs
@@ -0,0 +1,131 @@
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+
+function ERR(response, msg) {
+ dump("ERROR: " + msg + "\n");
+ response.write("HTTP/1.1 400 Bad Request\r\n");
+ response.write("Content-Type: text/html; charset=UTF-8\r\n");
+ response.write("Content-Length: " + msg.length + "\r\n");
+ response.write("\r\n");
+ response.write(msg);
+}
+
+function loadContentFromFile(path) {
+ // Load the content to return in the response from file.
+ // Since it's relative to the cwd of the test runner, we start there and
+ // append to get to the actual path of the file.
+ var testContentFile = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+ var dirs = path.split("/");
+ for (var i = 0; i < dirs.length; i++) {
+ testContentFile.append(dirs[i]);
+ }
+ var testContentFileStream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ testContentFileStream.init(testContentFile, -1, 0, 0);
+ var testContent = NetUtil.readInputStreamToString(
+ testContentFileStream,
+ testContentFileStream.available()
+ );
+ return testContent;
+}
+
+function handleRequest(request, response) {
+ const { scheme, host, path } = request;
+ // get the Content-Type to serve from the query string
+ var contentType = null;
+ var uniqueID = null;
+ var showLastRequest = false;
+ request.queryString.split("&").forEach(function (val) {
+ var [name, value] = val.split("=");
+ if (name == "type") {
+ contentType = unescape(value);
+ }
+ if (name == "uniqueID") {
+ uniqueID = unescape(value);
+ }
+ if (name == "lastRequest") {
+ showLastRequest = true;
+ }
+ });
+
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ if (showLastRequest) {
+ response.setHeader("Content-Type", "text/html", false);
+
+ // We don't want to expose the same lastRequest multiple times.
+ var state = getState("lastRequest");
+ setState("lastRequest", "");
+
+ if (state == "") {
+ ERR(response, "No last request!");
+ return;
+ }
+
+ response.write(state);
+ return;
+ }
+
+ if (!uniqueID) {
+ ERR(response, "No uniqueID?!?");
+ return;
+ }
+
+ setState(
+ "lastRequest",
+ JSON.stringify({
+ scheme,
+ host,
+ path,
+ uniqueID,
+ contentType: contentType || "other",
+ })
+ );
+
+ switch (contentType) {
+ case "img":
+ response.setHeader("Content-Type", "image/png", false);
+ response.write(
+ loadContentFromFile("tests/image/test/mochitest/blue.png")
+ );
+ break;
+
+ case "media":
+ response.setHeader("Content-Type", "video/ogg", false);
+ response.write(loadContentFromFile("tests/dom/media/test/320x240.ogv"));
+ break;
+
+ case "iframe":
+ response.setHeader("Content-Type", "text/html", false);
+ response.write("frame content");
+ break;
+
+ case "script":
+ response.setHeader("Content-Type", "application/javascript", false);
+ break;
+
+ case "stylesheet":
+ response.setHeader("Content-Type", "text/css", false);
+ break;
+
+ case "object":
+ response.setHeader("Content-Type", "application/x-test-match", false);
+ break;
+
+ case "xhr":
+ response.setHeader("Content-Type", "text/xml", false);
+ response.setHeader("Access-Control-Allow-Origin", "https://example.com");
+ response.write('<?xml version="1.0" encoding="UTF-8" ?><test></test>');
+ break;
+
+ default:
+ response.setHeader("Content-Type", "text/html", false);
+ response.write("<html><body>Hello World</body></html>");
+ break;
+ }
+}
diff --git a/dom/security/test/mixedcontentblocker/file_windowOpen.html b/dom/security/test/mixedcontentblocker/file_windowOpen.html
new file mode 100644
index 0000000000..508c1d17db
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/file_windowOpen.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Test mixed content load in iframe via window.open</title>
+ </head>
+ <body>
+ I'm in an iframe!
+ </body>
+</html>
diff --git a/dom/security/test/mixedcontentblocker/mochitest.toml b/dom/security/test/mixedcontentblocker/mochitest.toml
new file mode 100644
index 0000000000..17d8cb4608
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/mochitest.toml
@@ -0,0 +1,66 @@
+[DEFAULT]
+tags = "mcb"
+prefs = [
+ "security.mixed_content.upgrade_display_content=false",
+ "dom.security.https_first=false",
+]
+support-files = [
+ "file_bug803225_test_mailto.html",
+ "file_frameNavigation.html",
+ "file_frameNavigation_blankTarget.html",
+ "file_frameNavigation_grandchild.html",
+ "file_frameNavigation_innermost.html",
+ "file_frameNavigation_secure.html",
+ "file_frameNavigation_secure_grandchild.html",
+ "file_main.html",
+ "file_main_bug803225.html",
+ "file_main_bug803225_websocket_wsh.py",
+ "file_server.sjs",
+ "!/dom/media/test/320x240.ogv",
+ "!/image/test/mochitest/blue.png",
+ "file_redirect.html",
+ "file_redirect_handler.sjs",
+ "file_bug1550792.html",
+ "file_bug1551886.html",
+ "file_windowOpen.html",
+]
+
+["test_bug803225.html"]
+skip-if = [
+ "os=='linux' && bits==32", # Linux32:bug 1324870
+ "headless", # Headless:bug 1405870
+ "tsan", # tsan:bug 1612707
+ "http3",
+ "http2",
+]
+
+["test_bug1550792.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug1551886.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_frameNavigation.html"]
+skip-if = ["true"] # Bug 1424752
+
+["test_main.html"]
+skip-if = [
+ "true",
+ "os == 'android'",
+ "verify && !debug && os == 'linux'", # Android: TIMED_OUT; bug 1402554
+ "tsan", # Times out / Memory consumption, bug 1612707
+]
+
+["test_redirect.html"]
+
+["test_windowOpen.html"]
+scheme = "https"
+skip-if = [
+ "!debug" # Bug 1855588
+]
diff --git a/dom/security/test/mixedcontentblocker/pass.png b/dom/security/test/mixedcontentblocker/pass.png
new file mode 100644
index 0000000000..2fa1e0ac06
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/pass.png
Binary files differ
diff --git a/dom/security/test/mixedcontentblocker/test.ogv b/dom/security/test/mixedcontentblocker/test.ogv
new file mode 100644
index 0000000000..0f83996e5d
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/test.ogv
Binary files differ
diff --git a/dom/security/test/mixedcontentblocker/test.wav b/dom/security/test/mixedcontentblocker/test.wav
new file mode 100644
index 0000000000..85dc1ea904
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/test.wav
Binary files differ
diff --git a/dom/security/test/mixedcontentblocker/test_bug1550792.html b/dom/security/test/mixedcontentblocker/test_bug1550792.html
new file mode 100644
index 0000000000..4f15f9489a
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/test_bug1550792.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1550792: Block insecure subresource with non-https secure context parent</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+let f = document.createElement("iframe");
+f.src = "https://example.com/tests/dom/security/test/mixedcontentblocker/file_bug1550792.html";
+
+window.addEventListener("message", (event) => {
+ switch(event.data.type) {
+ case 'http':
+ is(event.data.status, "blocked", "nested load of http should be blocked.");
+ break
+ case 'https':
+ is(event.data.status, "loaded", "nested load of https should not be blocked.");
+ SimpleTest.finish();
+ break;
+ }
+});
+
+document.body.appendChild(f);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/mixedcontentblocker/test_bug1551886.html b/dom/security/test/mixedcontentblocker/test_bug1551886.html
new file mode 100644
index 0000000000..bf128256a4
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/test_bug1551886.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Bug 1551886: Opaque documents aren't considered in the mixed content blocker</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+let f = document.createElement("iframe");
+f.src = "https://example.com/tests/dom/security/test/mixedcontentblocker/file_bug1551886.html";
+
+window.addEventListener("message", (event) => {
+ switch(event.data.type) {
+ case 'http':
+ is(event.data.status, "blocked", "nested load of http://example should get blocked by the MCB");
+ break
+ case 'https':
+ is(event.data.status, "loaded", "nested load of https://example should not get blocked by the MCB");
+ SimpleTest.finish();
+ break;
+ }
+});
+
+document.body.appendChild(f);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/mixedcontentblocker/test_bug803225.html b/dom/security/test/mixedcontentblocker/test_bug803225.html
new file mode 100644
index 0000000000..87b372adf7
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/test_bug803225.html
@@ -0,0 +1,157 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Testing Allowlist of Resource Scheme for Mixed Content Blocker
+https://bugzilla.mozilla.org/show_bug.cgi?id=803225
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tests for Bug 803225</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+ <script>
+ const { AppConstants } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+ );
+
+ var counter = 0;
+ var settings = [ [true, true], [true, false], [false, true], [false, false] ];
+
+ var blockActive;
+ var blockDisplay;
+
+ //Cycle through 4 different preference settings.
+ function changePrefs(callback) {
+ let newPrefs = [
+ ["security.all_resource_uri_content_accessible", true], // Temporarily allow content to access all resource:// URIs.
+ ["security.mixed_content.block_display_content", settings[counter][0]],
+ ["security.mixed_content.block_active_content", settings[counter][1]]
+ ];
+
+ SpecialPowers.pushPrefEnv({"set": newPrefs}, function () {
+ blockDisplay = SpecialPowers.getBoolPref("security.mixed_content.block_display_content");
+ blockActive = SpecialPowers.getBoolPref("security.mixed_content.block_active_content");
+ counter++;
+ callback();
+ });
+ }
+
+ var testsToRun = {
+ /* https - Tests already run as part of bug 62178. */
+ about: false,
+ resource: false,
+ unsafe_about: false,
+ data_protocol: false,
+ javascript: false,
+ };
+
+ if (AppConstants.platform !== "android") {
+ // WebSocket tests are not supported on Android Yet. Bug 1566168.
+ testsToRun.wss = false;
+ testsToRun.mailto = false;
+ }
+
+ function log(msg) {
+ document.getElementById("log").textContent += "\n" + msg;
+ }
+
+ function reloadFrame() {
+ document.getElementById('framediv').innerHTML = '<iframe id="testHarness" src="https://example.com/tests/dom/security/test/mixedcontentblocker/file_main_bug803225.html"></iframe>';
+ }
+
+ function checkTestsCompleted() {
+ for (var prop in testsToRun) {
+ // some test hasn't run yet so we're not done
+ if (!testsToRun[prop])
+ return;
+ }
+ //if the testsToRun are all completed, change the pref and run the tests again until we have cycled through all the prefs.
+ if(counter < 4) {
+ for (var prop in testsToRun) {
+ testsToRun[prop] = false;
+ }
+ //call to change the preferences
+ changePrefs(function() {
+ log("\nblockDisplay set to "+blockDisplay+", blockActive set to "+blockActive+".");
+ reloadFrame();
+ });
+ }
+ else {
+ SimpleTest.finish();
+ }
+ }
+
+ var firstTest = true;
+
+ function receiveMessage(event) {
+ if(firstTest) {
+ log("blockDisplay set to "+blockDisplay+", blockActive set to "+blockActive+".");
+ firstTest = false;
+ }
+
+ log("test: "+event.data.test+", msg: "+event.data.msg + " logging message.");
+ // test that the load type matches the pref for this type of content
+ // (i.e. active vs. display)
+
+ switch(event.data.test) {
+
+ /* Mixed Script tests */
+ case "about":
+ ok(event.data.msg == "resource with about protocol loaded", "resource with about protocol did not load");
+ testsToRun.about = true;
+ break;
+
+ case "resource":
+ ok(event.data.msg == "resource with resource protocol loaded", "resource with resource protocol did not load");
+ testsToRun.resource = true;
+ break;
+
+ case "unsafe_about":
+ // This one should not load
+ ok(event.data.msg == "resource with unsafe about protocol did not load", "resource with unsafe about protocol loaded");
+ testsToRun.unsafe_about = true;
+ break;
+
+ case "data_protocol":
+ ok(event.data.msg == "resource with data protocol loaded", "resource with data protocol did not load");
+ testsToRun.data_protocol = true;
+ break;
+
+ case "javascript":
+ ok(event.data.msg == "resource with javascript protocol loaded", "resource with javascript protocol did not load");
+ testsToRun.javascript = true;
+ break;
+
+ case "wss":
+ ok(event.data.msg == "resource with wss protocol loaded", "resource with wss protocol did not load");
+ testsToRun.wss = true;
+ break;
+
+ case "mailto":
+ ok(event.data.msg == "resource with mailto protocol loaded", "resource with mailto protocol did not load");
+ testsToRun.mailto = true;
+ break;
+ }
+ checkTestsCompleted();
+ }
+
+ function startTest() {
+ //Set the first set of settings (true, true) and increment the counter.
+ changePrefs(function() {
+ // listen for a messages from the mixed content test harness
+ window.addEventListener("message", receiveMessage);
+
+ reloadFrame();
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ </script>
+</head>
+
+<body onload='startTest()'>
+ <div id="framediv"></div>
+ <pre id="log"></pre>
+</body>
+</html>
diff --git a/dom/security/test/mixedcontentblocker/test_frameNavigation.html b/dom/security/test/mixedcontentblocker/test_frameNavigation.html
new file mode 100644
index 0000000000..82e3e715d2
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/test_frameNavigation.html
@@ -0,0 +1,127 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Tests for Mixed Content Blocker
+https://bugzilla.mozilla.org/show_bug.cgi?id=840388
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tests for Bug 840388</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+ <script>
+ var counter = 0;
+ var origBlockActive = SpecialPowers.getBoolPref("security.mixed_content.block_active_content");
+
+ SpecialPowers.setBoolPref("security.mixed_content.block_active_content", true);
+ var blockActive = SpecialPowers.getBoolPref("security.mixed_content.block_active_content");
+
+
+ var testsToRunInsecure = {
+ insecurePage_navigate_child: false,
+ insecurePage_navigate_grandchild: false,
+ };
+
+ var testsToRunSecure = {
+ securePage_navigate_child: false,
+ blankTarget: false,
+ };
+
+ function log(msg) {
+ document.getElementById("log").textContent += "\n" + msg;
+ }
+
+ var secureTestsStarted = false;
+ async function checkTestsCompleted() {
+ for (var prop in testsToRunInsecure) {
+ // some test hasn't run yet so we're not done
+ if (!testsToRunInsecure[prop])
+ return;
+ }
+ // If we are here, all the insecure tests have run.
+ // If we haven't changed the iframe to run the secure tests, change it now.
+ if (!secureTestsStarted) {
+ document.getElementById('testing_frame').src = "https://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_secure.html";
+ secureTestsStarted = true;
+ }
+ for (var prop in testsToRunSecure) {
+ // some test hasn't run yet so we're not done
+ if (!testsToRunSecure[prop])
+ return;
+ }
+ //if the secure and insecure testsToRun are all completed, change the block mixed active content pref and run the tests again.
+ if(counter < 1) {
+ for (var prop in testsToRunSecure) {
+ testsToRunSecure[prop] = false;
+ }
+ for (var prop in testsToRunInsecure) {
+ testsToRunInsecure[prop] = false;
+ }
+ //call to change the preferences
+ counter++;
+ await SpecialPowers.setBoolPref("security.mixed_content.block_active_content", false);
+ blockActive = SpecialPowers.getBoolPref("security.mixed_content.block_active_content");
+ log("blockActive set to "+blockActive+".");
+ secureTestsStarted = false;
+ document.getElementById('framediv').innerHTML = '<iframe src="http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation.html" id="testing_frame"></iframe>';
+ }
+ else {
+ //set the prefs back to what they were set to originally
+ SpecialPowers.setBoolPref("security.mixed_content.block_active_content", origBlockActive);
+ SimpleTest.finish();
+ }
+ }
+
+ var firstTestDebugMessage = true;
+
+ // listen for a messages from the mixed content test harness
+ window.addEventListener("message", receiveMessage);
+ function receiveMessage(event) {
+ if(firstTestDebugMessage) {
+ log("blockActive set to "+blockActive);
+ firstTestDebugMessage = false;
+ }
+
+ log("test: "+event.data.test+", msg: "+event.data.msg + ".");
+ // test that the load type matches the pref for this type of content
+ // (i.e. active vs. display)
+
+ switch(event.data.test) {
+
+ case "insecurePage_navigate_child":
+ is(event.data.msg, "navigated to insecure iframe on insecure page", "navigating to insecure iframe blocked on insecure page");
+ testsToRunInsecure.insecurePage_navigate_child = true;
+ break;
+
+ case "insecurePage_navigate_grandchild":
+ is(event.data.msg, "navigated to insecure grandchild iframe on insecure page", "navigating to insecure grandchild iframe blocked on insecure page");
+ testsToRunInsecure.insecurePage_navigate_grandchild = true;
+ break;
+
+ case "securePage_navigate_child":
+ ok(blockActive == (event.data.msg == "navigating to insecure iframe blocked on secure page"), "navigated to insecure iframe on secure page");
+ testsToRunSecure.securePage_navigate_child = true;
+ break;
+
+ case "blankTarget":
+ is(event.data.msg, "opened an http link with target=_blank from a secure page", "couldn't open an http link in a new window from a secure page");
+ testsToRunSecure.blankTarget = true;
+ break;
+
+ }
+ checkTestsCompleted();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ </script>
+</head>
+
+<body>
+ <div id="framediv">
+ <iframe src="http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation.html" id="testing_frame"></iframe>
+ </div>
+
+ <pre id="log"></pre>
+</body>
+</html>
diff --git a/dom/security/test/mixedcontentblocker/test_main.html b/dom/security/test/mixedcontentblocker/test_main.html
new file mode 100644
index 0000000000..9a13a0853f
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/test_main.html
@@ -0,0 +1,231 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Tests for Mixed Content Blocker
+https://bugzilla.mozilla.org/show_bug.cgi?id=62178
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tests for Bug 62178</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+ <script>
+ let counter = 0;
+ // blockDisplay blockActive upgradeDisplay
+ const settings = [
+ [true, true, true],
+ [true, false, true],
+ [false, true, true],
+ [false, false, true],
+ [true, true, false],
+ [true, false, false],
+ [false, true, false],
+ [false, false, false],
+ ];
+
+ let blockActive;
+ let blockDisplay;
+ let upgradeDisplay;
+
+ //Cycle through 8 different preference settings.
+ function changePrefs(otherPrefs, callback) {
+ let basePrefs = [["security.mixed_content.block_display_content", settings[counter][0]],
+ ["security.mixed_content.block_active_content", settings[counter][1]],
+ ["security.mixed_content.upgrade_display_content", settings[counter][2]]];
+ let newPrefs = basePrefs.concat(otherPrefs);
+
+ SpecialPowers.pushPrefEnv({"set": newPrefs}, function () {
+ blockDisplay = SpecialPowers.getBoolPref("security.mixed_content.block_display_content");
+ blockActive = SpecialPowers.getBoolPref("security.mixed_content.block_active_content");
+ upgradeDisplay = SpecialPowers.getBoolPref("security.mixed_content.upgrade_display_content");
+ counter++;
+ callback();
+ });
+ }
+
+ let testsToRun = {
+ iframe: false,
+ image: false,
+ imageSrcset: false,
+ imageSrcsetFallback: false,
+ imagePicture: false,
+ imageJoinPicture: false,
+ imageLeavePicture: false,
+ script: false,
+ stylesheet: false,
+ object: false,
+ media: false,
+ xhr: false,
+ };
+
+ function log(msg) {
+ document.getElementById("log").textContent += "\n" + msg;
+ }
+
+ function reloadFrame() {
+ document.getElementById('framediv').innerHTML = '<iframe id="testHarness" src="https://example.com/tests/dom/security/test/mixedcontentblocker/file_main.html"></iframe>';
+ }
+
+ function checkTestsCompleted() {
+ for (var prop in testsToRun) {
+ // some test hasn't run yet so we're not done
+ if (!testsToRun[prop])
+ return;
+ }
+ //if the testsToRun are all completed, chnage the pref and run the tests again until we have cycled through all the prefs.
+ if(counter < 8) {
+ for (var prop in testsToRun) {
+ testsToRun[prop] = false;
+ }
+ //call to change the preferences
+ changePrefs([], function() {
+ log(`\nblockDisplay set to ${blockDisplay}, blockActive set to ${blockActive}, upgradeDisplay set to ${upgradeDisplay}`);
+ reloadFrame();
+ });
+ }
+ else {
+ SimpleTest.finish();
+ }
+ }
+
+ var firstTest = true;
+
+ function receiveMessage(event) {
+ if(firstTest) {
+ log(`blockActive set to ${blockActive}, blockDisplay set to ${blockDisplay}, upgradeDisplay set to ${upgradeDisplay}.`);
+ firstTest = false;
+ }
+
+ // Simple check from the iframe.
+ if (event.data.check) {
+ ok(event.data.status, event.data.msg);
+ return;
+ }
+
+ log("test: "+event.data.test+", msg: "+event.data.msg + " logging message.");
+ // test that the load type matches the pref for this type of content
+ // (i.e. active vs. display)
+
+ switch(event.data.test) {
+
+ /* Mixed Script tests */
+ case "iframe":
+ ok(blockActive == (event.data.msg == "insecure iframe blocked"), "iframe did not follow block_active_content pref");
+ testsToRun.iframe = true;
+ break;
+
+ case "object":
+ ok(blockActive == (event.data.msg == "insecure object blocked"), "object did not follow block_active_content pref");
+ testsToRun.object = true;
+ break;
+
+ case "script":
+ ok(blockActive == (event.data.msg == "insecure script blocked"), "script did not follow block_active_content pref");
+ testsToRun.script = true;
+ break;
+
+ case "stylesheet":
+ ok(blockActive == (event.data.msg == "insecure stylesheet blocked"), "stylesheet did not follow block_active_content pref");
+ testsToRun.stylesheet = true;
+ break;
+
+ case "xhr":
+ ok(blockActive == (event.data.msg == "insecure xhr blocked"), "xhr did not follow block_active_content pref");
+ testsToRun.xhr = true;
+ break;
+
+ /* Mixed Display tests */
+ case "image":
+ //test that the image load matches the pref for display content
+ if (upgradeDisplay) {
+ ok(event.data.msg == "secure image loaded after upgrade", "image did not follow upgrade_display_content pref");
+ } else {
+ ok(blockDisplay == (event.data.msg == "insecure image blocked"), "image did not follow block_display_content pref");
+ }
+ testsToRun.image = true;
+ break;
+
+ case "media":
+ if (upgradeDisplay) {
+ ok(event.data.msg == "secure media loaded after upgrade", "media did not follow upgrade_display_content pref");
+ } else {
+ ok(blockDisplay == (event.data.msg == "insecure media blocked"), "media did not follow block_display_content pref");
+ }
+ testsToRun.media = true;
+ break;
+
+ /* Images using the "imageset" policy, from <img srcset> and <picture>, do not get the mixed display exception */
+ case "imageSrcset":
+ // When blockDisplay && blockActive && upgradeDisplay are all true the request is blocked
+ // This appears to be a side effect of blockDisplay taking precedence here.
+ if (event.data.msg == "secure image loaded after upgrade") {
+ ok(upgradeDisplay, "imageSrcset did not follow upgrade_display_content pref");
+ } else {
+ ok(blockActive == (event.data.msg == "insecure image blocked"), "imageSrcset did not follow block_active_content pref");
+ }
+ testsToRun.imageSrcset = true;
+ break;
+
+ case "imageSrcsetFallback":
+ if (event.data.msg == "secure image loaded after upgrade") {
+ ok(upgradeDisplay, "imageSrcsetFallback did not follow upgrade_display_content pref");
+ } else {
+ ok(blockActive == (event.data.msg == "insecure image blocked"), "imageSrcsetFallback did not follow block_active_content pref");
+ }
+ testsToRun.imageSrcsetFallback = true;
+ break;
+
+ case "imagePicture":
+ if (event.data.msg == "secure image loaded after upgrade") {
+ ok(upgradeDisplay, "imagePicture did not follow upgrade_display_content pref");
+ } else {
+ ok(blockActive == (event.data.msg == "insecure image blocked"), "imagePicture did not follow block_active_content pref");
+ }
+ testsToRun.imagePicture = true;
+ break;
+
+ case "imageJoinPicture":
+ if (event.data.msg == "secure image loaded after upgrade") {
+ ok(upgradeDisplay, "imageJoinPicture did not follow upgrade_display_content pref");
+ } else {
+ ok(blockActive == (event.data.msg == "insecure image blocked"), "imageJoinPicture did not follow block_active_content pref");
+ }
+ testsToRun.imageJoinPicture = true;
+ break;
+
+ // Should return to mixed display mode
+ case "imageLeavePicture":
+ if (event.data.msg == "secure image loaded after upgrade") {
+ ok(upgradeDisplay, "imageLeavePicture did not follow upgrade_display_content pref");
+ } else {
+ ok(blockDisplay == (event.data.msg == "insecure image blocked"), "imageLeavePicture did not follow block_display_content pref");
+ }
+ testsToRun.imageLeavePicture = true;
+ break;
+
+ }
+ checkTestsCompleted();
+ }
+
+ function startTest() {
+ //Set the first set of mixed content settings and increment the counter.
+ changePrefs([], function() {
+ //listen for a messages from the mixed content test harness
+ window.addEventListener("message", receiveMessage);
+
+ //Kick off test
+ reloadFrame();
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ </script>
+</head>
+
+<body onload='startTest()'>
+ <div id="framediv"></div>
+ <pre id="log"></pre>
+</body>
+</html>
diff --git a/dom/security/test/mixedcontentblocker/test_redirect.html b/dom/security/test/mixedcontentblocker/test_redirect.html
new file mode 100644
index 0000000000..3fdd4e2e7b
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/test_redirect.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug1402363: Test mixed content redirects</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body onload='startTest()'>
+<iframe style="width:100%;height:300px;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+const PATH = "https://example.com/tests/dom/security/test/mixedcontentblocker/";
+let testcounter = 0;
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ if (event.data === "https-to-https-loaded") {
+ ok(true, "https to https fetch redirect should be allowed");
+ }
+ else if (event.data === "https-to-http-blocked") {
+ ok(true, "https to http fetch redirect should be blocked");
+ }
+ else {
+ ok(false, "sanity: we should never enter that branch (" + event.data + ")");
+ }
+ testcounter++;
+ if (testcounter < 2) {
+ return;
+ }
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+function startTest() {
+ let testframe = document.getElementById("testframe");
+ testframe.src = PATH + "file_redirect.html";
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/mixedcontentblocker/test_windowOpen.html b/dom/security/test/mixedcontentblocker/test_windowOpen.html
new file mode 100644
index 0000000000..ae286c38f8
--- /dev/null
+++ b/dom/security/test/mixedcontentblocker/test_windowOpen.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests for Mixed Content Navigation with window.open</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+let testsCompleted = 0;
+const numberOfTestCases = 2;
+
+function markTestCaseComplete() {
+ testsCompleted++;
+
+ if (testsCompleted == numberOfTestCases) {
+ SimpleTest.finish();
+ }
+}
+
+window.onmessage = function(event) {
+ if (event.data.src.includes("test1")) {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ is(event.data.target, "http://test1.example.com/tests/dom/security/test/mixedcontentblocker/file_windowOpen.html", "error thrown for failed iframe load should be from test1's iframe.");
+ is(event.data.outcome, "blocked", "http iframe should be blocked from loading in child https window.");
+ is(event.data.method, "http", "messages from test1 iframe should be http.");
+ markTestCaseComplete();
+ }
+ else if (event.data.src.includes("test2")) {
+ if (event.data.outcome != 'csp-error') {
+ is(event.data.target, "https://test2.example.com/tests/dom/security/test/mixedcontentblocker/file_windowOpen.html", "event message received for successful iframe load should be from test2's iframe.");
+ is(event.data.triggeringPrincipal, "https://example.com/tests/dom/security/test/mixedcontentblocker/test_windowOpen.html", "triggeringPrincipal for successfully loaded https iframe should be the original test file.");
+ is(event.data.outcome, "loaded", "https iframe should be allowed to load in child https window.");
+ is(event.data.method, "https", "messages from test2 iframe should be https");
+ }
+ markTestCaseComplete();
+ }
+};
+
+function testURLInOpenedWindow(testURL) {
+ let openedWindow = window.open("javascript:''","_blank");
+ openedWindow.onload = function() {
+ openedWindow.document.body.innerHTML = '<iframe id="testframe">'
+
+ let testframe = openedWindow.document.getElementById("testframe");
+ testframe.onload = function(event) {
+ try {
+ let triggeringPrincipal = SpecialPowers.wrap(this.contentWindow).docShell.currentDocumentChannel.loadInfo.triggeringPrincipal.asciiSpec;
+ openedWindow.opener.postMessage({outcome: 'loaded', method: this.src.split(":")[0], src: this.src, target: event.target.src, triggeringPrincipal}, '*');
+ }
+ catch (error) {
+ // If we can't get the docShell due to CSP blocking access to the iframe's docShell then skip this test case
+ if (error.name === "SecurityError" && error.message === 'Permission denied to access property "docShell" on cross-origin object') {
+ openedWindow.opener.postMessage({outcome: 'csp-error', method: this.src.split(":")[0], src: this.src}, '*');
+ }
+ else throw error;
+ }
+ openedWindow.close();
+ }
+ testframe.onerror = function(error) {
+ openedWindow.opener.postMessage({outcome: 'blocked', method: this.src.split(":")[0], src: this.src, target: error.target.src}, '*');
+ openedWindow.close();
+ }
+
+ testframe.src = testURL;
+ };
+};
+
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+testURLInOpenedWindow("http://test1.example.com/tests/dom/security/test/mixedcontentblocker/file_windowOpen.html");
+testURLInOpenedWindow("https://test2.example.com/tests/dom/security/test/mixedcontentblocker/file_windowOpen.html");
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/moz.build b/dom/security/test/moz.build
new file mode 100644
index 0000000000..d1b6cdf317
--- /dev/null
+++ b/dom/security/test/moz.build
@@ -0,0 +1,43 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("cors/**"):
+ BUG_COMPONENT = ("Core", "Networking")
+
+XPCSHELL_TESTS_MANIFESTS += [
+ "unit/xpcshell.toml",
+]
+
+TEST_DIRS += [
+ "gtest",
+]
+
+MOCHITEST_MANIFESTS += [
+ "cors/mochitest.toml",
+ "csp/mochitest.toml",
+ "general/mochitest.toml",
+ "https-first/mochitest.toml",
+ "https-only/mochitest.toml",
+ "mixedcontentblocker/mochitest.toml",
+ "referrer-policy/mochitest.toml",
+ "sec-fetch/mochitest.toml",
+ "sri/mochitest.toml",
+]
+
+MOCHITEST_CHROME_MANIFESTS += [
+ "general/chrome.toml",
+]
+
+BROWSER_CHROME_MANIFESTS += [
+ "cors/browser.toml",
+ "csp/browser.toml",
+ "general/browser.toml",
+ "https-first/browser.toml",
+ "https-only/browser.toml",
+ "mixedcontentblocker/browser.toml",
+ "referrer-policy/browser.toml",
+ "sec-fetch/browser.toml",
+]
diff --git a/dom/security/test/referrer-policy/browser.toml b/dom/security/test/referrer-policy/browser.toml
new file mode 100644
index 0000000000..325b6a3f49
--- /dev/null
+++ b/dom/security/test/referrer-policy/browser.toml
@@ -0,0 +1,9 @@
+[DEFAULT]
+support-files = ["referrer_page.sjs"]
+
+["browser_fragment_navigation.js"]
+support-files = ["file_fragment_navigation.sjs"]
+
+["browser_referrer_disallow_cross_site_relaxing.js"]
+
+["browser_referrer_telemetry.js"]
diff --git a/dom/security/test/referrer-policy/browser_fragment_navigation.js b/dom/security/test/referrer-policy/browser_fragment_navigation.js
new file mode 100644
index 0000000000..c3d5e62854
--- /dev/null
+++ b/dom/security/test/referrer-policy/browser_fragment_navigation.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_FILE =
+ "https://example.com/browser/dom/security/test/referrer-policy/file_fragment_navigation.sjs";
+
+add_task(async function test_browser_navigation() {
+ await BrowserTestUtils.withNewTab(TEST_FILE, async browser => {
+ let loadPromise = BrowserTestUtils.browserLoaded(browser);
+ await SpecialPowers.spawn(browser, [], () => {
+ ok(content.document.getElementById("ok"), "Initial page should load");
+
+ info("Clicking on link to check referrer");
+ content.document.getElementById("check_referrer").click();
+ });
+ await loadPromise;
+
+ await SpecialPowers.spawn(browser, [], () => {
+ ok(
+ content.document.getElementById("ok"),
+ "Page should load when checking referrer"
+ );
+
+ info("Clicking on fragment link");
+ content.document.getElementById("fragment").click();
+ });
+
+ info("Reloading tab");
+ loadPromise = BrowserTestUtils.browserLoaded(browser);
+ await BrowserTestUtils.reloadTab(gBrowser.selectedTab);
+ await loadPromise;
+
+ await SpecialPowers.spawn(browser, [], () => {
+ ok(
+ content.document.getElementById("ok"),
+ "Page should load when checking referrer after fragment navigation and reload"
+ );
+ });
+ });
+});
diff --git a/dom/security/test/referrer-policy/browser_referrer_disallow_cross_site_relaxing.js b/dom/security/test/referrer-policy/browser_referrer_disallow_cross_site_relaxing.js
new file mode 100644
index 0000000000..7f8df7b34b
--- /dev/null
+++ b/dom/security/test/referrer-policy/browser_referrer_disallow_cross_site_relaxing.js
@@ -0,0 +1,458 @@
+/**
+ * Bug 1720294 - Testing disallow relaxing default referrer policy for
+ * cross-site requests.
+ */
+
+"use strict";
+
+requestLongerTimeout(6);
+
+const TEST_DOMAIN = "https://example.com/";
+const TEST_SAME_SITE_DOMAIN = "https://test1.example.com/";
+const TEST_SAME_SITE_DOMAIN_HTTP = "http://test1.example.com/";
+const TEST_CROSS_SITE_DOMAIN = "https://test1.example.org/";
+const TEST_CROSS_SITE_DOMAIN_HTTP = "http://test1.example.org/";
+
+const TEST_PATH = "browser/dom/security/test/referrer-policy/";
+
+const TEST_PAGE = `${TEST_DOMAIN}${TEST_PATH}referrer_page.sjs`;
+const TEST_SAME_SITE_PAGE = `${TEST_SAME_SITE_DOMAIN}${TEST_PATH}referrer_page.sjs`;
+const TEST_SAME_SITE_PAGE_HTTP = `${TEST_SAME_SITE_DOMAIN_HTTP}${TEST_PATH}referrer_page.sjs`;
+const TEST_CROSS_SITE_PAGE = `${TEST_CROSS_SITE_DOMAIN}${TEST_PATH}referrer_page.sjs`;
+const TEST_CROSS_SITE_PAGE_HTTP = `${TEST_CROSS_SITE_DOMAIN_HTTP}${TEST_PATH}referrer_page.sjs`;
+
+const REFERRER_FULL = 0;
+const REFERRER_ORIGIN = 1;
+const REFERRER_NONE = 2;
+
+function getExpectedReferrer(referrer, type) {
+ let res;
+
+ switch (type) {
+ case REFERRER_FULL:
+ res = referrer;
+ break;
+ case REFERRER_ORIGIN:
+ let url = new URL(referrer);
+ res = `${url.origin}/`;
+ break;
+ case REFERRER_NONE:
+ res = "";
+ break;
+ default:
+ ok(false, "unknown type");
+ }
+
+ return res;
+}
+
+async function verifyResultInPage(browser, expected) {
+ await SpecialPowers.spawn(browser, [expected], value => {
+ is(content.document.referrer, value, "The document.referrer is correct.");
+
+ let result = content.document.getElementById("result");
+ is(result.textContent, value, "The referer header is correct");
+ });
+}
+
+function getExpectedConsoleMessage(expected, isPrefOn, url) {
+ let msg;
+
+ if (isPrefOn) {
+ msg =
+ "Referrer Policy: Ignoring the less restricted referrer policy “" +
+ expected +
+ "†for the cross-site request: " +
+ url;
+ } else {
+ msg =
+ "Referrer Policy: Less restricted policies, including " +
+ "‘no-referrer-when-downgrade’, ‘origin-when-cross-origin’ and " +
+ "‘unsafe-url’, will be ignored soon for the cross-site request: " +
+ url;
+ }
+
+ return msg;
+}
+
+function createConsoleMessageVerificationPromise(expected, isPrefOn, url) {
+ if (!expected) {
+ return Promise.resolve();
+ }
+
+ return new Promise(resolve => {
+ let listener = {
+ observe(msg) {
+ let message = msg.QueryInterface(Ci.nsIScriptError);
+
+ if (message.category.startsWith("Security")) {
+ is(
+ message.errorMessage,
+ getExpectedConsoleMessage(expected, isPrefOn, url),
+ "The console message is correct."
+ );
+ Services.console.unregisterListener(listener);
+ resolve();
+ }
+ },
+ };
+
+ Services.console.registerListener(listener);
+ });
+}
+
+function verifyNoConsoleMessage() {
+ // Verify that there is no referrer policy console message.
+ let allMessages = Services.console.getMessageArray();
+
+ for (let msg of allMessages) {
+ let message = msg.QueryInterface(Ci.nsIScriptError);
+ if (
+ message.category.startsWith("Security") &&
+ message.errorMessage.startsWith("Referrer Policy:")
+ ) {
+ ok(false, "There should be no console message for referrer policy.");
+ }
+ }
+}
+
+const TEST_CASES = [
+ // Testing that the referrer policy can be overridden with less restricted
+ // policy in the same-origin scenario.
+ {
+ policy: "unsafe-url",
+ referrer: TEST_PAGE,
+ test_url: TEST_PAGE,
+ expect: REFERRER_FULL,
+ original: REFERRER_FULL,
+ },
+ // Testing that the referrer policy can be overridden with less restricted
+ // policy in the same-site scenario.
+ {
+ policy: "unsafe-url",
+ referrer: TEST_PAGE,
+ test_url: TEST_SAME_SITE_PAGE,
+ expect: REFERRER_FULL,
+ original: REFERRER_FULL,
+ },
+ {
+ policy: "no-referrer-when-downgrade",
+ referrer: TEST_PAGE,
+ test_url: TEST_SAME_SITE_PAGE,
+ expect: REFERRER_FULL,
+ original: REFERRER_FULL,
+ },
+ {
+ policy: "origin-when-cross-origin",
+ referrer: TEST_PAGE,
+ test_url: TEST_SAME_SITE_PAGE_HTTP,
+ expect: REFERRER_ORIGIN,
+ original: REFERRER_ORIGIN,
+ },
+ // Testing that the referrer policy cannot be overridden with less restricted
+ // policy in the cross-site scenario.
+ {
+ policy: "unsafe-url",
+ referrer: TEST_PAGE,
+ test_url: TEST_CROSS_SITE_PAGE,
+ expect: REFERRER_ORIGIN,
+ expect_console: "unsafe-url",
+ original: REFERRER_FULL,
+ },
+ {
+ policy: "no-referrer-when-downgrade",
+ referrer: TEST_PAGE,
+ test_url: TEST_CROSS_SITE_PAGE,
+ expect: REFERRER_ORIGIN,
+ expect_console: "no-referrer-when-downgrade",
+ original: REFERRER_FULL,
+ },
+ {
+ policy: "origin-when-cross-origin",
+ referrer: TEST_PAGE,
+ test_url: TEST_CROSS_SITE_PAGE_HTTP,
+ expect: REFERRER_NONE,
+ expect_console: "origin-when-cross-origin",
+ original: REFERRER_ORIGIN,
+ },
+ // Testing that the referrer policy can still be overridden with more
+ // restricted policy in the cross-site scenario.
+ {
+ policy: "no-referrer",
+ referrer: TEST_PAGE,
+ test_url: TEST_CROSS_SITE_PAGE,
+ expect: REFERRER_NONE,
+ original: REFERRER_NONE,
+ },
+];
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Disable mixed content blocking to be able to test downgrade scenario.
+ ["security.mixed_content.block_active_content", false],
+ ],
+ });
+});
+
+async function runTestIniFrame(gBrowser, enabled, expectNoConsole) {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async browser => {
+ for (let type of ["meta", "header"]) {
+ for (let test of TEST_CASES) {
+ info(`Test iframe: ${test.toSource()}`);
+ let referrerURL = `${test.referrer}?${type}=${test.policy}`;
+ let expected = enabled
+ ? getExpectedReferrer(referrerURL, test.expect)
+ : getExpectedReferrer(referrerURL, test.original);
+
+ let expected_console = expectNoConsole
+ ? undefined
+ : test.expect_console;
+
+ Services.console.reset();
+
+ BrowserTestUtils.startLoadingURIString(browser, referrerURL);
+ await BrowserTestUtils.browserLoaded(browser, false, referrerURL);
+
+ let iframeURL = test.test_url + "?show";
+
+ let consolePromise = createConsoleMessageVerificationPromise(
+ expected_console,
+ enabled,
+ iframeURL
+ );
+ // Create an iframe and load the url.
+ let bc = await SpecialPowers.spawn(
+ browser,
+ [iframeURL],
+ async url => {
+ let iframe = content.document.createElement("iframe");
+ let loadPromise = ContentTaskUtils.waitForEvent(iframe, "load");
+ iframe.src = url;
+ content.document.body.appendChild(iframe);
+
+ await loadPromise;
+
+ return iframe.browsingContext;
+ }
+ );
+
+ await verifyResultInPage(bc, expected);
+ await consolePromise;
+ if (!expected_console) {
+ verifyNoConsoleMessage();
+ }
+ }
+ }
+ }
+ );
+}
+
+async function runTestForLinkClick(gBrowser, enabled, expectNoConsole) {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async browser => {
+ for (let type of ["meta", "header"]) {
+ for (let test of TEST_CASES) {
+ info(`Test link click: ${test.toSource()}`);
+ let referrerURL = `${test.referrer}?${type}=${test.policy}`;
+ let expected = enabled
+ ? getExpectedReferrer(referrerURL, test.expect)
+ : getExpectedReferrer(referrerURL, test.original);
+
+ let expected_console = expectNoConsole
+ ? undefined
+ : test.expect_console;
+
+ Services.console.reset();
+
+ BrowserTestUtils.startLoadingURIString(browser, referrerURL);
+ await BrowserTestUtils.browserLoaded(browser, false, referrerURL);
+
+ let linkURL = test.test_url + "?show";
+
+ let consolePromise = createConsoleMessageVerificationPromise(
+ expected_console,
+ enabled,
+ linkURL
+ );
+
+ // Create the promise to wait for the navigation finishes.
+ let loadedPromise = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ linkURL
+ );
+
+ // Generate the link and click it to navigate.
+ await SpecialPowers.spawn(browser, [linkURL], async url => {
+ let link = content.document.createElement("a");
+ link.textContent = "Link";
+ link.setAttribute("href", url);
+
+ content.document.body.appendChild(link);
+ link.click();
+ });
+
+ await loadedPromise;
+
+ await verifyResultInPage(browser, expected);
+ await consolePromise;
+ if (!expected_console) {
+ verifyNoConsoleMessage();
+ }
+ }
+ }
+ }
+ );
+}
+
+async function toggleETPForPage(gBrowser, url, toggle) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ // First, Toggle ETP off for the test page.
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ url
+ );
+
+ if (toggle) {
+ gProtectionsHandler.enableForCurrentPage();
+ } else {
+ gProtectionsHandler.disableForCurrentPage();
+ }
+
+ await browserLoadedPromise;
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function test_iframe() {
+ for (let enabled of [true, false]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.http.referer.disallowCrossSiteRelaxingDefault", enabled]],
+ });
+
+ await runTestIniFrame(gBrowser, enabled);
+ }
+});
+
+add_task(async function test_iframe_pbmode() {
+ let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+
+ for (let enabled of [true, false]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "network.http.referer.disallowCrossSiteRelaxingDefault.pbmode",
+ enabled,
+ ],
+ ],
+ });
+
+ await runTestIniFrame(win.gBrowser, enabled);
+ }
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_link_click() {
+ for (let enabled of [true, false]) {
+ for (let enabled_top of [true, false]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["network.http.referer.disallowCrossSiteRelaxingDefault", enabled],
+ [
+ "network.http.referer.disallowCrossSiteRelaxingDefault.top_navigation",
+ enabled_top,
+ ],
+ ],
+ });
+
+ // We won't show the console message if the strict rule is disabled for
+ // the top navigation.
+ await runTestForLinkClick(gBrowser, enabled && enabled_top, !enabled_top);
+ }
+ }
+});
+
+add_task(async function test_link_click_pbmode() {
+ let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+
+ for (let enabled of [true, false]) {
+ for (let enabled_top of [true, false]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "network.http.referer.disallowCrossSiteRelaxingDefault.pbmode",
+ enabled,
+ ],
+ [
+ "network.http.referer.disallowCrossSiteRelaxingDefault.pbmode.top_navigation",
+ enabled_top,
+ ],
+ // Disable https first mode for private browsing mode to test downgrade
+ // cases.
+ ["dom.security.https_first_pbm", false],
+ ],
+ });
+
+ // We won't show the console message if the strict rule is disabled for
+ // the top navigation in the private browsing window.
+ await runTestForLinkClick(
+ win.gBrowser,
+ enabled && enabled_top,
+ !enabled_top
+ );
+ }
+ }
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_iframe_etp_toggle_off() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.http.referer.disallowCrossSiteRelaxingDefault", true]],
+ });
+
+ // Open a new tab for the test page and toggle ETP off.
+ await toggleETPForPage(gBrowser, TEST_PAGE, false);
+
+ // Run the test to see if the protection is disabled. We won't send console
+ // message if the protection was disabled by the ETP toggle.
+ await runTestIniFrame(gBrowser, false, true);
+
+ // toggle ETP on again.
+ await toggleETPForPage(gBrowser, TEST_PAGE, true);
+
+ // Run the test again to see if the protection is enabled.
+ await runTestIniFrame(gBrowser, true);
+});
+
+add_task(async function test_link_click_etp_toggle_off() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["network.http.referer.disallowCrossSiteRelaxingDefault", true],
+ [
+ "network.http.referer.disallowCrossSiteRelaxingDefault.top_navigation",
+ true,
+ ],
+ ],
+ });
+
+ // Toggle ETP off for the cross site. Note that the cross site is the place
+ // where we test against the ETP permission for top navigation.
+ await toggleETPForPage(gBrowser, TEST_CROSS_SITE_PAGE, false);
+
+ // Run the test to see if the protection is disabled. We won't send console
+ // message if the protection was disabled by the ETP toggle.
+ await runTestForLinkClick(gBrowser, false, true);
+
+ // toggle ETP on again.
+ await toggleETPForPage(gBrowser, TEST_CROSS_SITE_PAGE, true);
+
+ // Run the test again to see if the protection is enabled.
+ await runTestForLinkClick(gBrowser, true);
+});
diff --git a/dom/security/test/referrer-policy/browser_referrer_telemetry.js b/dom/security/test/referrer-policy/browser_referrer_telemetry.js
new file mode 100644
index 0000000000..7542dd9338
--- /dev/null
+++ b/dom/security/test/referrer-policy/browser_referrer_telemetry.js
@@ -0,0 +1,126 @@
+/**
+ * Bug 1720869 - Testing the referrer policy telemetry.
+ */
+
+"use strict";
+
+const TEST_DOMAIN = "https://example.com/";
+const TEST_CROSS_SITE_DOMAIN = "https://test1.example.org/";
+
+const TEST_PATH = "browser/dom/security/test/referrer-policy/";
+
+const TEST_PAGE = `${TEST_DOMAIN}${TEST_PATH}referrer_page.sjs`;
+const TEST_CROSS_SITE_PAGE = `${TEST_CROSS_SITE_DOMAIN}${TEST_PATH}referrer_page.sjs`;
+
+// This matches to the order in ReferrerPolicy.webidl
+const REFERRER_POLICY_INDEX = {
+ empty: 0,
+ "no-referrer": 1,
+ "no-referrer-when-downgrade": 2,
+ origin: 3,
+ "origin-when-cross-origin": 4,
+ "unsafe-url": 5,
+ "same-origin": 6,
+ "strict-origin": 7,
+ "strict-origin-when-cross-origin": 8,
+};
+
+const TEST_CASES = [
+ {
+ policy: "",
+ expected: REFERRER_POLICY_INDEX.empty,
+ },
+ {
+ policy: "no-referrer",
+ expected: REFERRER_POLICY_INDEX["no-referrer"],
+ },
+ {
+ policy: "no-referrer-when-downgrade",
+ expected: REFERRER_POLICY_INDEX["no-referrer-when-downgrade"],
+ },
+ {
+ policy: "origin",
+ expected: REFERRER_POLICY_INDEX.origin,
+ },
+ {
+ policy: "origin-when-cross-origin",
+ expected: REFERRER_POLICY_INDEX["origin-when-cross-origin"],
+ },
+ {
+ policy: "same-origin",
+ expected: REFERRER_POLICY_INDEX["same-origin"],
+ },
+ {
+ policy: "strict-origin",
+ expected: REFERRER_POLICY_INDEX["strict-origin"],
+ },
+ {
+ policy: "strict-origin-when-cross-origin",
+ expected: REFERRER_POLICY_INDEX["strict-origin-when-cross-origin"],
+ },
+ {
+ policy: "unsafe-url",
+ expected: REFERRER_POLICY_INDEX["unsafe-url"],
+ },
+];
+
+function clearTelemetry() {
+ Services.telemetry.getSnapshotForHistograms("main", true /* clear */);
+ Services.telemetry.getHistogramById("REFERRER_POLICY_COUNT").clear();
+}
+
+add_setup(async function () {
+ // Clear Telemetry probes before testing.
+ clearTelemetry();
+});
+
+function verifyTelemetry(expected, isSameSite) {
+ // The record of cross-site loads is placed in the second half of the
+ // telemetry.
+ const offset = isSameSite ? 0 : Object.keys(REFERRER_POLICY_INDEX).length;
+
+ let histograms = Services.telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+
+ let referrerPolicyCountProbe = histograms.REFERRER_POLICY_COUNT;
+
+ ok(referrerPolicyCountProbe, "The telemetry probe has been recorded.");
+ is(
+ referrerPolicyCountProbe.values[expected + offset],
+ 1,
+ "The telemetry is added correctly."
+ );
+}
+
+add_task(async function run_tests() {
+ for (let test of TEST_CASES) {
+ for (let sameSite of [true, false]) {
+ clearTelemetry();
+ let referrerURL = `${TEST_PAGE}?header=${test.policy}`;
+
+ await BrowserTestUtils.withNewTab(referrerURL, async browser => {
+ let iframeURL = sameSite
+ ? TEST_PAGE + "?show"
+ : TEST_CROSS_SITE_PAGE + "?show";
+
+ // Create an iframe and load the url.
+ await SpecialPowers.spawn(browser, [iframeURL], async url => {
+ let iframe = content.document.createElement("iframe");
+ iframe.src = url;
+
+ await new content.Promise(resolve => {
+ iframe.onload = () => {
+ resolve();
+ };
+
+ content.document.body.appendChild(iframe);
+ });
+ });
+
+ verifyTelemetry(test.expected, sameSite);
+ });
+ }
+ }
+});
diff --git a/dom/security/test/referrer-policy/file_fragment_navigation.sjs b/dom/security/test/referrer-policy/file_fragment_navigation.sjs
new file mode 100644
index 0000000000..5fb6f0d826
--- /dev/null
+++ b/dom/security/test/referrer-policy/file_fragment_navigation.sjs
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+ if (
+ request.queryString === "check_referrer" &&
+ (!request.hasHeader("referer") ||
+ request.getHeader("referer") !==
+ "https://example.com/browser/dom/security/test/referrer-policy/file_fragment_navigation.sjs")
+ ) {
+ response.setStatusLine(request.httpVersion, 400, "Bad Request");
+ response.write("Did not receive referrer");
+ } else {
+ response.setHeader("Content-Type", "text/html");
+ response.write(
+ `<span id="ok">OK</span>
+<a id="check_referrer" href="?check_referrer">check_referrer</a>
+<a id="fragment" href="#fragment">fragment</a>`
+ );
+ }
+}
diff --git a/dom/security/test/referrer-policy/img_referrer_testserver.sjs b/dom/security/test/referrer-policy/img_referrer_testserver.sjs
new file mode 100644
index 0000000000..7fcc8d4914
--- /dev/null
+++ b/dom/security/test/referrer-policy/img_referrer_testserver.sjs
@@ -0,0 +1,337 @@
+var BASE_URL =
+ "example.com/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs";
+const IMG_BYTES = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
+ "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="
+);
+
+function createTestUrl(aPolicy, aAction, aName, aContent) {
+ var content = aContent || "text";
+ return (
+ "http://" +
+ BASE_URL +
+ "?" +
+ "action=" +
+ aAction +
+ "&" +
+ "policy=" +
+ aPolicy +
+ "&" +
+ "name=" +
+ aName +
+ "&" +
+ "content=" +
+ content
+ );
+}
+
+function createTestPage(aHead, aImgPolicy, aName) {
+ var _createTestUrl = createTestUrl.bind(null, aImgPolicy, "test", aName);
+
+ return (
+ "<!DOCTYPE HTML>\n\
+ <html>" +
+ aHead +
+ '<body>\n\
+ <img src="' +
+ _createTestUrl("img") +
+ '" referrerpolicy="' +
+ aImgPolicy +
+ '" id="image"></img>\n\
+ <script>' +
+ // LOAD EVENT (of the test)
+ // fires when the img resource for the page is loaded
+ 'window.addEventListener("load", function() {\n\
+ parent.postMessage("childLoadComplete", "http://mochi.test:8888");\n\
+ }.bind(window), false);' +
+ "</script>\n\
+ </body>\n\
+ </html>"
+ );
+}
+
+// Creates the following test cases for the specified referrer
+// policy combination:
+// <img> with referrer
+function createTest(aPolicy, aImgPolicy, aName) {
+ var headString = "<head>";
+ if (aPolicy) {
+ headString += '<meta name="referrer" content="' + aPolicy + '">';
+ }
+
+ headString += "<script></script>";
+
+ return createTestPage(headString, aImgPolicy, aName);
+}
+
+// testing regular load img with referrer policy
+// speculative parser should not kick in here
+function createTest2(aImgPolicy, name) {
+ return createTestPage("", aImgPolicy, name);
+}
+
+function createTest3(aImgPolicy1, aImgPolicy2, aImgPolicy3, aName) {
+ return (
+ '<!DOCTYPE HTML>\n\
+ <html>\n\
+ <body>\n\
+ <img src="' +
+ createTestUrl(aImgPolicy1, "test", aName + aImgPolicy1) +
+ '" referrerpolicy="' +
+ aImgPolicy1 +
+ '" id="image"></img>\n\
+ <img src="' +
+ createTestUrl(aImgPolicy2, "test", aName + aImgPolicy2) +
+ '" referrerpolicy="' +
+ aImgPolicy2 +
+ '" id="image"></img>\n\
+ <img src="' +
+ createTestUrl(aImgPolicy3, "test", aName + aImgPolicy3) +
+ '" referrerpolicy="' +
+ aImgPolicy3 +
+ '" id="image"></img>\n\
+ <script>\n\
+ var _numLoads = 0;' +
+ // LOAD EVENT (of the test)
+ // fires when the img resource for the page is loaded
+ 'window.addEventListener("load", function() {\n\
+ parent.postMessage("childLoadComplete", "http://mochi.test:8888");\n\
+ }.bind(window), false);' +
+ "</script>\n\
+ </body>\n\
+ </html>"
+ );
+}
+
+function createTestPage2(aHead, aPolicy, aName) {
+ return (
+ "<!DOCTYPE HTML>\n\
+ <html>" +
+ aHead +
+ '<body>\n\
+ <img src="' +
+ createTestUrl(aPolicy, "test", aName) +
+ '" id="image"></img>\n\
+ <script>' +
+ // LOAD EVENT (of the test)
+ // fires when the img resource for the page is loaded
+ 'window.addEventListener("load", function() {\n\
+ parent.postMessage("childLoadComplete", "http://mochi.test:8888");\n\
+ }.bind(window), false);' +
+ "</script>\n\
+ </body>\n\
+ </html>"
+ );
+}
+
+function createTestPage3(aHead, aPolicy, aName) {
+ return (
+ "<!DOCTYPE HTML>\n\
+ <html>" +
+ aHead +
+ "<body>\n\
+ <script>" +
+ 'var image = new Image();\n\
+ image.src = "' +
+ createTestUrl(aPolicy, "test", aName, "image") +
+ '";\n\
+ image.referrerPolicy = "' +
+ aPolicy +
+ '";\n\
+ image.onload = function() {\n\
+ window.parent.postMessage("childLoadComplete", "http://mochi.test:8888");\n\
+ }\n\
+ document.body.appendChild(image);' +
+ "</script>\n\
+ </body>\n\
+ </html>"
+ );
+}
+
+function createTestPage4(aHead, aPolicy, aName) {
+ return (
+ "<!DOCTYPE HTML>\n\
+ <html>" +
+ aHead +
+ "<body>\n\
+ <script>" +
+ 'var image = new Image();\n\
+ image.referrerPolicy = "' +
+ aPolicy +
+ '";\n\
+ image.src = "' +
+ createTestUrl(aPolicy, "test", aName, "image") +
+ '";\n\
+ image.onload = function() {\n\
+ window.parent.postMessage("childLoadComplete", "http://mochi.test:8888");\n\
+ }\n\
+ document.body.appendChild(image);' +
+ "</script>\n\
+ </body>\n\
+ </html>"
+ );
+}
+
+function createSetAttributeTest1(aPolicy, aImgPolicy, aName) {
+ var headString = "<head>";
+ headString += '<meta name="referrer" content="' + aPolicy + '">';
+ headString += "<script></script>";
+
+ return createTestPage3(headString, aImgPolicy, aName);
+}
+
+function createSetAttributeTest2(aPolicy, aImgPolicy, aName) {
+ var headString = "<head>";
+ headString += '<meta name="referrer" content="' + aPolicy + '">';
+ headString += "<script></script>";
+
+ return createTestPage4(headString, aImgPolicy, aName);
+}
+
+function createTest4(aPolicy, aName) {
+ var headString = "<head>";
+ headString += '<meta name="referrer" content="' + aPolicy + '">';
+ headString += "<script></script>";
+
+ return createTestPage2(headString, aPolicy, aName);
+}
+
+function createTest5(aPolicy, aName) {
+ var headString = "<head>";
+ headString += '<meta name="referrer" content="' + aPolicy + '">';
+
+ return createTestPage2(headString, aPolicy, aName);
+}
+
+function handleRequest(request, response) {
+ var sharedKey = "img_referrer_testserver.sjs";
+ var params = request.queryString.split("&");
+ var action = params[0].split("=")[1];
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+
+ if (action === "resetState") {
+ let state = getSharedState(sharedKey);
+ state = {};
+ setSharedState(sharedKey, JSON.stringify(state));
+ response.write("");
+ return;
+ }
+ if (action === "test") {
+ // ?action=test&policy=origin&name=name&content=content
+ let policy = params[1].split("=")[1];
+ let name = params[2].split("=")[1];
+ let content = params[3].split("=")[1];
+ let result = getSharedState(sharedKey);
+
+ if (result === "") {
+ result = {};
+ } else {
+ result = JSON.parse(result);
+ }
+
+ if (!result.tests) {
+ result.tests = {};
+ }
+
+ var referrerLevel = "none";
+ var test = {};
+ if (request.hasHeader("Referer")) {
+ let referrer = request.getHeader("Referer");
+ if (referrer.indexOf("img_referrer_testserver") > 0) {
+ referrerLevel = "full";
+ } else if (referrer == "http://mochi.test:8888/") {
+ referrerLevel = "origin";
+ }
+ test.referrer = request.getHeader("Referer");
+ } else {
+ test.referrer = "";
+ }
+ test.policy = referrerLevel;
+ test.expected = policy;
+
+ result.tests[name] = test;
+
+ setSharedState(sharedKey, JSON.stringify(result));
+
+ if (content === "image") {
+ response.setHeader("Content-Type", "image/png");
+ response.write(IMG_BYTES);
+ }
+ return;
+ }
+ if (action === "get-test-results") {
+ // ?action=get-result
+ response.write(getSharedState(sharedKey));
+ return;
+ }
+ if (action === "generate-img-policy-test") {
+ // ?action=generate-img-policy-test&imgPolicy=b64-encoded-string&name=name&policy=b64-encoded-string
+ let imgPolicy = unescape(params[1].split("=")[1]);
+ let name = unescape(params[2].split("=")[1]);
+ let metaPolicy = "";
+ if (params[3]) {
+ metaPolicy = params[3].split("=")[1];
+ }
+
+ response.write(createTest(metaPolicy, imgPolicy, name));
+ return;
+ }
+ if (action === "generate-img-policy-test2") {
+ // ?action=generate-img-policy-test2&imgPolicy=b64-encoded-string&name=name
+ let imgPolicy = unescape(params[1].split("=")[1]);
+ let name = unescape(params[2].split("=")[1]);
+
+ response.write(createTest2(imgPolicy, name));
+ return;
+ }
+ if (action === "generate-img-policy-test3") {
+ // ?action=generate-img-policy-test3&imgPolicy1=b64-encoded-string&imgPolicy2=b64-encoded-string&imgPolicy3=b64-encoded-string&name=name
+ let imgPolicy1 = unescape(params[1].split("=")[1]);
+ let imgPolicy2 = unescape(params[2].split("=")[1]);
+ let imgPolicy3 = unescape(params[3].split("=")[1]);
+ let name = unescape(params[4].split("=")[1]);
+
+ response.write(createTest3(imgPolicy1, imgPolicy2, imgPolicy3, name));
+ return;
+ }
+ if (action === "generate-img-policy-test4") {
+ // ?action=generate-img-policy-test4&imgPolicy=b64-encoded-string&name=name
+ let policy = unescape(params[1].split("=")[1]);
+ let name = unescape(params[2].split("=")[1]);
+
+ response.write(createTest4(policy, name));
+ return;
+ }
+ if (action === "generate-img-policy-test5") {
+ // ?action=generate-img-policy-test5&policy=b64-encoded-string&name=name
+ let policy = unescape(params[1].split("=")[1]);
+ let name = unescape(params[2].split("=")[1]);
+
+ response.write(createTest5(policy, name));
+ return;
+ }
+
+ if (action === "generate-setAttribute-test1") {
+ // ?action=generate-setAttribute-test1&policy=b64-encoded-string&name=name
+ let imgPolicy = unescape(params[1].split("=")[1]);
+ let policy = unescape(params[2].split("=")[1]);
+ let name = unescape(params[3].split("=")[1]);
+
+ response.write(createSetAttributeTest1(policy, imgPolicy, name));
+ return;
+ }
+
+ if (action === "generate-setAttribute-test2") {
+ // ?action=generate-setAttribute-test2&policy=b64-encoded-string&name=name
+ let imgPolicy = unescape(params[1].split("=")[1]);
+ let policy = unescape(params[2].split("=")[1]);
+ let name = unescape(params[3].split("=")[1]);
+
+ response.write(createSetAttributeTest2(policy, imgPolicy, name));
+ return;
+ }
+
+ response.write("I don't know action " + action);
+}
diff --git a/dom/security/test/referrer-policy/mochitest.toml b/dom/security/test/referrer-policy/mochitest.toml
new file mode 100644
index 0000000000..89a54ad554
--- /dev/null
+++ b/dom/security/test/referrer-policy/mochitest.toml
@@ -0,0 +1,28 @@
+[DEFAULT]
+support-files = [
+ "img_referrer_testserver.sjs",
+ "referrer_header.sjs",
+ "referrer_header_current_document_iframe.html",
+ "referrer_helper.js",
+ "referrer_testserver.sjs",
+]
+
+["test_img_referrer.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_referrer_header_current_document.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_referrer_redirect.html"]
+# Please keep alphabetical order.
+skip-if = [
+ "http3",
+ "http2",
+]
+
diff --git a/dom/security/test/referrer-policy/referrer_header.sjs b/dom/security/test/referrer-policy/referrer_header.sjs
new file mode 100644
index 0000000000..29c324b8f6
--- /dev/null
+++ b/dom/security/test/referrer-policy/referrer_header.sjs
@@ -0,0 +1,6 @@
+function handleRequest(request, response) {
+ response.setHeader("Referrer-Policy", "same-origin");
+ response.write(
+ '<!DOCTYPE HTML><html><body>Loaded</body><script>parent.postMessage(document.referrer, "*");</script></html>'
+ );
+}
diff --git a/dom/security/test/referrer-policy/referrer_header_current_document_iframe.html b/dom/security/test/referrer-policy/referrer_header_current_document_iframe.html
new file mode 100644
index 0000000000..5996c8ba8a
--- /dev/null
+++ b/dom/security/test/referrer-policy/referrer_header_current_document_iframe.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script>
+ window.addEventListener("load", function() {
+ document.getElementById("link").click();
+ });
+ </script>
+</head>
+<body>
+ <a id="link" href="http://example.org/tests/dom/security/test/referrer-policy/referrer_header.sjs">Navigate</a>
+</body>
diff --git a/dom/security/test/referrer-policy/referrer_helper.js b/dom/security/test/referrer-policy/referrer_helper.js
new file mode 100644
index 0000000000..b892017eef
--- /dev/null
+++ b/dom/security/test/referrer-policy/referrer_helper.js
@@ -0,0 +1,129 @@
+// This helper expects these globals to be defined.
+/* global PARAMS, SJS, testCases */
+
+/*
+ * common functionality for iframe, anchor, and area referrer attribute tests
+ */
+const GET_RESULT = SJS + "ACTION=get-test-results";
+const RESET_STATE = SJS + "ACTION=resetState";
+
+SimpleTest.waitForExplicitFinish();
+var advance = function () {
+ tests.next();
+};
+
+/**
+ * Listen for notifications from the child.
+ * These are sent in case of error, or when the loads we await have completed.
+ */
+window.addEventListener("message", function (event) {
+ if (event.data == "childLoadComplete") {
+ // all loads happen, continue the test.
+ advance();
+ }
+});
+
+/**
+ * helper to perform an XHR
+ * to do checkIndividualResults and resetState
+ */
+function doXHR(aUrl, onSuccess, onFail) {
+ // The server is at http[s]://example.com so we need cross-origin XHR.
+ var xhr = new XMLHttpRequest({ mozSystem: true });
+ xhr.responseType = "json";
+ xhr.onload = function () {
+ onSuccess(xhr);
+ };
+ xhr.onerror = function () {
+ onFail(xhr);
+ };
+ xhr.open("GET", "http" + aUrl, true);
+ xhr.send(null);
+}
+
+/**
+ * Grabs the results via XHR and passes to checker.
+ */
+function checkIndividualResults(aTestname, aExpectedReferrer, aName) {
+ var onload = xhr => {
+ var results = xhr.response;
+ info(JSON.stringify(xhr.response));
+ ok(aName in results, aName + " tests have to be performed.");
+ is(
+ results[aName].policy,
+ aExpectedReferrer,
+ aTestname +
+ " --- " +
+ results[aName].policy +
+ " (" +
+ results[aName].referrer +
+ ")"
+ );
+ advance();
+ };
+ var onerror = xhr => {
+ ok(false, "Can't get results from the counter server.");
+ SimpleTest.finish();
+ };
+ doXHR(GET_RESULT, onload, onerror);
+}
+
+function resetState() {
+ doXHR(RESET_STATE, advance, function (xhr) {
+ ok(false, "error in reset state");
+ SimpleTest.finish();
+ });
+}
+
+/**
+ * testing if referrer header is sent correctly
+ */
+var tests = (function* () {
+ yield SpecialPowers.pushPrefEnv(
+ { set: [["security.mixed_content.block_active_content", false]] },
+ advance
+ );
+ yield SpecialPowers.pushPrefEnv(
+ { set: [["network.http.referer.disallowCrossSiteRelaxingDefault", false]] },
+ advance
+ );
+ yield SpecialPowers.pushPermissions(
+ [{ type: "systemXHR", allow: true, context: document }],
+ advance
+ );
+
+ var iframe = document.getElementById("testframe");
+
+ for (var j = 0; j < testCases.length; j++) {
+ if (testCases[j].PREFS) {
+ yield SpecialPowers.pushPrefEnv({ set: testCases[j].PREFS }, advance);
+ }
+
+ var actions = testCases[j].ACTION;
+ var subTests = testCases[j].TESTS;
+ for (var k = 0; k < actions.length; k++) {
+ var actionString = actions[k];
+ for (var i = 0; i < subTests.length; i++) {
+ yield resetState();
+ var searchParams = new URLSearchParams();
+ searchParams.append("ACTION", actionString);
+ searchParams.append("NAME", subTests[i].NAME);
+ for (var l of PARAMS) {
+ if (subTests[i][l]) {
+ searchParams.append(l, subTests[i][l]);
+ }
+ }
+ var schemeFrom = subTests[i].SCHEME_FROM || "http";
+ yield (iframe.src = schemeFrom + SJS + searchParams.toString());
+ yield checkIndividualResults(
+ subTests[i].DESC,
+ subTests[i].RESULT,
+ subTests[i].NAME
+ );
+ }
+ }
+ }
+
+ // complete.
+ SimpleTest.finish();
+})();
diff --git a/dom/security/test/referrer-policy/referrer_page.sjs b/dom/security/test/referrer-policy/referrer_page.sjs
new file mode 100644
index 0000000000..2cfae0d398
--- /dev/null
+++ b/dom/security/test/referrer-policy/referrer_page.sjs
@@ -0,0 +1,39 @@
+function handleRequest(request, response) {
+ let params = new URLSearchParams(request.queryString);
+ let referrerPolicyHeader = params.get("header") || "";
+ let metaReferrerPolicy = params.get("meta") || "";
+ let showReferrer = params.has("show");
+
+ if (referrerPolicyHeader) {
+ response.setHeader("Referrer-Policy", referrerPolicyHeader, false);
+ }
+
+ let metaString = "";
+ let resultString = "";
+
+ if (metaReferrerPolicy) {
+ metaString = `<meta name="referrer" content="${metaReferrerPolicy}">`;
+ }
+
+ if (showReferrer) {
+ if (request.hasHeader("Referer")) {
+ resultString = `Referer Header: <a id="result">${request.getHeader(
+ "Referer"
+ )}</a>`;
+ } else {
+ resultString = `Referer Header: <a id="result"></a>`;
+ }
+ }
+
+ response.write(
+ `<!DOCTYPE HTML>
+ <html>
+ <head>
+ ${metaString}
+ </head>
+ <body>
+ ${resultString}
+ </body>
+ </html>`
+ );
+}
diff --git a/dom/security/test/referrer-policy/referrer_testserver.sjs b/dom/security/test/referrer-policy/referrer_testserver.sjs
new file mode 100644
index 0000000000..9f112e88dc
--- /dev/null
+++ b/dom/security/test/referrer-policy/referrer_testserver.sjs
@@ -0,0 +1,704 @@
+/*
+ * Test server for iframe, anchor, and area referrer attributes.
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1175736
+ * Also server for further referrer tests such as redirecting tests
+ * bug 1174913, bug 1175736, bug 1184781
+ */
+
+const SJS = "referrer_testserver.sjs?";
+const SJS_PATH = "/tests/dom/security/test/referrer-policy/";
+const BASE_ORIGIN = "example.com";
+const BASE_URL = BASE_ORIGIN + SJS_PATH + SJS;
+const SHARED_KEY = SJS;
+const SAME_ORIGIN = "mochi.test:8888" + SJS_PATH + SJS;
+const CROSS_ORIGIN_URL = "test1.example.com" + SJS_PATH + SJS;
+const HSTS_URL = "includesubdomains.preloaded.test" + SJS_PATH + SJS;
+
+const IMG_BYTES = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
+ "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="
+);
+
+function createTestUrl(
+ aPolicy,
+ aAction,
+ aName,
+ aType,
+ aSchemeFrom,
+ aSchemeTo,
+ crossOrigin,
+ referrerPolicyHeader
+) {
+ var schemeTo = aSchemeTo || "http";
+ var schemeFrom = aSchemeFrom || "http";
+ var rpHeader = referrerPolicyHeader || "";
+ var url = schemeTo + "://";
+ url += crossOrigin ? CROSS_ORIGIN_URL : BASE_URL;
+ url +=
+ "ACTION=" +
+ aAction +
+ "&" +
+ "policy=" +
+ aPolicy +
+ "&" +
+ "NAME=" +
+ aName +
+ "&" +
+ "type=" +
+ aType +
+ "&" +
+ "RP_HEADER=" +
+ rpHeader +
+ "&" +
+ "SCHEME_FROM=" +
+ schemeFrom;
+ return url;
+}
+
+// test page using iframe referrer attribute
+// if aParams are set this creates a test where the iframe url is a redirect
+function createIframeTestPageUsingRefferer(
+ aMetaPolicy,
+ aAttributePolicy,
+ aNewAttributePolicy,
+ aName,
+ aParams,
+ aSchemeFrom,
+ aSchemeTo,
+ aChangingMethod
+) {
+ var metaString = "";
+ if (aMetaPolicy) {
+ metaString = `<meta name="referrer" content="${aMetaPolicy}">`;
+ }
+ var changeString = "";
+ if (aChangingMethod === "setAttribute") {
+ changeString = `document.getElementById("myframe").setAttribute("referrerpolicy", "${aNewAttributePolicy}")`;
+ } else if (aChangingMethod === "property") {
+ changeString = `document.getElementById("myframe").referrerPolicy = "${aNewAttributePolicy}"`;
+ }
+ var iFrameString = `<iframe src="" id="myframe" ${
+ aAttributePolicy ? ` referrerpolicy="${aAttributePolicy}"` : ""
+ }>iframe</iframe>`;
+ var iframeUrl = "";
+ if (aParams) {
+ aParams.delete("ACTION");
+ aParams.append("ACTION", "redirectIframe");
+ iframeUrl = "http://" + CROSS_ORIGIN_URL + aParams.toString();
+ } else {
+ iframeUrl = createTestUrl(
+ aAttributePolicy,
+ "test",
+ aName,
+ "iframe",
+ aSchemeFrom,
+ aSchemeTo
+ );
+ }
+
+ return `<!DOCTYPE HTML>
+ <html>
+ <head>
+ ${metaString}
+ </head>
+ <body>
+ ${iFrameString}
+ <script>
+ window.addEventListener("load", function() {
+ ${changeString}
+ document.getElementById("myframe").onload = function(){
+ parent.postMessage("childLoadComplete", "http://mochi.test:8888");
+ };
+ document.getElementById("myframe").src = "${iframeUrl}";
+ }.bind(window), false);
+ </script>
+ </body>
+ </html>`;
+}
+
+function buildAnchorString(
+ aMetaPolicy,
+ aReferrerPolicy,
+ aName,
+ aRelString,
+ aSchemeFrom,
+ aSchemeTo
+) {
+ if (aReferrerPolicy) {
+ return `<a href="${createTestUrl(
+ aReferrerPolicy,
+ "test",
+ aName,
+ "link",
+ aSchemeFrom,
+ aSchemeTo
+ )}" referrerpolicy="${aReferrerPolicy}" id="link" ${aRelString}>${aReferrerPolicy}</a>`;
+ }
+ return `<a href="${createTestUrl(
+ aMetaPolicy,
+ "test",
+ aName,
+ "link",
+ aSchemeFrom,
+ aSchemeTo
+ )}" id="link" ${aRelString}>link</a>`;
+}
+
+function buildAreaString(
+ aMetaPolicy,
+ aReferrerPolicy,
+ aName,
+ aRelString,
+ aSchemeFrom,
+ aSchemeTo
+) {
+ var result = `<img src="file_mozfiledataurl_img.jpg" alt="image" usemap="#imageMap">`;
+ result += `<map name="imageMap">`;
+ if (aReferrerPolicy) {
+ result += `<area shape="circle" coords="1,1,1" href="${createTestUrl(
+ aReferrerPolicy,
+ "test",
+ aName,
+ "link",
+ aSchemeFrom,
+ aSchemeTo
+ )}" alt="theArea" referrerpolicy="${aReferrerPolicy}" id="link" ${aRelString}>`;
+ } else {
+ result += `<area shape="circle" coords="1,1,1" href="${createTestUrl(
+ aMetaPolicy,
+ "test",
+ aName,
+ "link",
+ aSchemeFrom,
+ aSchemeTo
+ )}" alt="theArea" id="link" ${aRelString}>`;
+ }
+ result += `</map>`;
+
+ return result;
+}
+
+// test page using anchor or area referrer attribute
+function createAETestPageUsingRefferer(
+ aMetaPolicy,
+ aAttributePolicy,
+ aNewAttributePolicy,
+ aName,
+ aRel,
+ aStringBuilder,
+ aSchemeFrom,
+ aSchemeTo,
+ aChangingMethod
+) {
+ var metaString = "";
+ if (aMetaPolicy) {
+ metaString = `<head><meta name="referrer" content="${aMetaPolicy}"></head>`;
+ }
+ var changeString = "";
+ if (aChangingMethod === "setAttribute") {
+ changeString = `document.getElementById("link").setAttribute("referrerpolicy", "${aNewAttributePolicy}")`;
+ } else if (aChangingMethod === "property") {
+ changeString = `document.getElementById("link").referrerPolicy = "${aNewAttributePolicy}"`;
+ }
+ var relString = "";
+ if (aRel) {
+ relString = `rel="noreferrer"`;
+ }
+ var elementString = aStringBuilder(
+ aMetaPolicy,
+ aAttributePolicy,
+ aName,
+ relString,
+ aSchemeFrom,
+ aSchemeTo
+ );
+
+ return `<!DOCTYPE HTML>
+ <html>
+ ${metaString}
+ <body>
+ ${elementString}
+ <script>
+ window.addEventListener("load", function() {
+ ${changeString}
+ document.getElementById("link").click();
+ }.bind(window), false);
+ </script>
+ </body>
+ </html>`;
+}
+
+// test page using anchor target=_blank rel=noopener
+function createTargetBlankRefferer(
+ aMetaPolicy,
+ aName,
+ aSchemeFrom,
+ aSchemeTo,
+ aRpHeader
+) {
+ var metaString = "";
+ if (aMetaPolicy) {
+ metaString = `<head><meta name="referrer" content="${aMetaPolicy}"></head>`;
+ }
+ var elementString = `<a href="${createTestUrl(
+ aMetaPolicy,
+ "test",
+ aName,
+ "link",
+ aSchemeFrom,
+ aSchemeTo,
+ aRpHeader
+ )}" target=_blank rel="noopener" id="link">link</a>`;
+
+ return `<!DOCTYPE HTML>
+ <html>
+ ${metaString}
+ <body>
+ ${elementString}
+ <script>
+ window.addEventListener("load", function() {
+ let link = document.getElementById("link");
+ SpecialPowers.wrap(window).parent.postMessage("childLoadReady", "*");
+ link.click();
+ }.bind(window), false);
+ </script>
+ </body>
+ </html>`;
+}
+
+// creates test page with img that is a redirect
+function createImgTestCase(aParams, aAttributePolicy, aRedirect) {
+ var metaString = "";
+ if (aParams.has("META_POLICY")) {
+ metaString = `<meta name="referrer" content="${aParams.get(
+ "META_POLICY"
+ )}">`;
+ }
+ aParams.delete("ACTION");
+ if (aRedirect) {
+ aParams.append("ACTION", "redirectImg");
+ } else {
+ aParams.append("ACTION", "test");
+ aParams.append("type", "img");
+ }
+ var imgUrl =
+ "http://" +
+ (aParams.get("HSTS") ? HSTS_URL : CROSS_ORIGIN_URL) +
+ aParams.toString();
+
+ return `<!DOCTYPE HTML>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ ${metaString}
+ <title>Test referrer policies on redirect (img)</title>
+ </head>
+ <body>
+ <img id="testImg" src="${imgUrl}" ${
+ aAttributePolicy ? ` referrerpolicy="${aAttributePolicy}"` : ""
+ }>
+ <script>
+ window.addEventListener("load", function() {
+ parent.postMessage("childLoadComplete", "http://mochi.test:8888");
+ }.bind(window), false);
+ </script>
+ </body>
+ </html>`;
+}
+
+// test page using link referrer attribute
+function createLinkPageUsingRefferer(
+ aMetaPolicy,
+ aAttributePolicy,
+ aNewAttributePolicy,
+ aName,
+ aRel,
+ aStringBuilder,
+ aSchemeFrom,
+ aSchemeTo,
+ aTestType
+) {
+ var metaString = "";
+ if (aMetaPolicy) {
+ metaString = `<meta name="referrer" content="${aMetaPolicy}">`;
+ }
+
+ var changeString = "";
+ var policy = aAttributePolicy ? aAttributePolicy : aMetaPolicy;
+ var elementString = aStringBuilder(
+ policy,
+ aName,
+ aRel,
+ aSchemeFrom,
+ aSchemeTo,
+ aTestType
+ );
+
+ if (aTestType === "setAttribute") {
+ changeString = `var link = document.getElementById("test_link");
+ link.setAttribute("referrerpolicy", "${aNewAttributePolicy}");
+ link.href = "${createTestUrl(
+ policy,
+ "test",
+ aName,
+ "link_element_" + aRel,
+ aSchemeFrom,
+ aSchemeTo
+ )}";`;
+ } else if (aTestType === "property") {
+ changeString = `var link = document.getElementById("test_link");
+ link.referrerPolicy = "${aNewAttributePolicy}";
+ link.href = "${createTestUrl(
+ policy,
+ "test",
+ aName,
+ "link_element_" + aRel,
+ aSchemeFrom,
+ aSchemeTo
+ )}";`;
+ }
+
+ return `<!DOCTYPE HTML>
+ <html>
+ <head>
+ ${metaString}
+ </head>
+ <body>
+ ${elementString}
+ <script>
+ ${changeString}
+ </script>
+ </body>
+ </html>`;
+}
+
+function createFetchUserControlRPTestCase(
+ aName,
+ aSchemeFrom,
+ aSchemeTo,
+ crossOrigin
+) {
+ var srcUrl = createTestUrl(
+ "",
+ "test",
+ aName,
+ "fetch",
+ aSchemeFrom,
+ aSchemeTo,
+ crossOrigin
+ );
+
+ return `<!DOCTYPE HTML>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <title>Test user control referrer policies</title>
+ </head>
+ <body>
+ <script>
+ fetch("${srcUrl}", {referrerPolicy: ""}).then(function (response) {
+ window.parent.postMessage("childLoadComplete", "http://mochi.test:8888");
+ });
+ </script>
+ </body>
+ </html>`;
+}
+
+function buildLinkString(
+ aPolicy,
+ aName,
+ aRel,
+ aSchemeFrom,
+ aSchemeTo,
+ aTestType
+) {
+ var href = "";
+ var onChildComplete = `window.parent.postMessage("childLoadComplete", "http://mochi.test:8888");`;
+ var policy = "";
+ var asString = "";
+ var relString = "";
+
+ if (aRel) {
+ relString = `rel="${aRel}"`;
+ }
+
+ if (aPolicy) {
+ policy = `referrerpolicy=${aPolicy}`;
+ }
+
+ if (aRel == "preload") {
+ asString = 'as="image"';
+ }
+
+ if (!aTestType) {
+ href = `href=${createTestUrl(
+ aPolicy,
+ "test",
+ aName,
+ "link_element_" + aRel,
+ aSchemeFrom,
+ aSchemeTo
+ )}`;
+ }
+
+ return `<link ${relString} ${href} ${policy} ${asString} id="test_link" onload='${onChildComplete}' onerror='${onChildComplete}'>`;
+}
+// eslint-disable-next-line complexity
+function handleRequest(request, response) {
+ var params = new URLSearchParams(request.queryString);
+ var action = params.get("ACTION");
+ var schemeFrom = params.get("SCHEME_FROM") || "http";
+ var schemeTo = params.get("SCHEME_TO") || "http";
+ var crossOrigin = params.get("CROSS_ORIGIN") || false;
+ var referrerPolicyHeader = params.get("RP_HEADER") || "";
+
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ if (referrerPolicyHeader) {
+ response.setHeader("Referrer-Policy", referrerPolicyHeader, false);
+ }
+
+ if (action === "resetState") {
+ setSharedState(SHARED_KEY, "{}");
+ response.write("");
+ return;
+ }
+ if (action === "get-test-results") {
+ // ?action=get-result
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/plain", false);
+ response.write(getSharedState(SHARED_KEY));
+ return;
+ }
+ if (action === "redirect") {
+ response.write(
+ '<script>parent.postMessage("childLoadComplete", "http://mochi.test:8888");</script>'
+ );
+ return;
+ }
+ if (action === "redirectImg") {
+ params.delete("ACTION");
+ params.append("ACTION", "test");
+ params.append("type", "img");
+ // 302 found, 301 Moved Permanently, 303 See Other, 307 Temporary Redirect
+ response.setStatusLine("1.1", 302, "found");
+ response.setHeader(
+ "Location",
+ "http://" + CROSS_ORIGIN_URL + params.toString(),
+ false
+ );
+ return;
+ }
+ if (action === "redirectIframe") {
+ params.delete("ACTION");
+ params.append("ACTION", "test");
+ params.append("type", "iframe");
+ // 302 found, 301 Moved Permanently, 303 See Other, 307 Temporary Redirect
+ response.setStatusLine("1.1", 302, "found");
+ response.setHeader(
+ "Location",
+ "http://" + CROSS_ORIGIN_URL + params.toString(),
+ false
+ );
+ return;
+ }
+ if (action === "test") {
+ // ?action=test&policy=origin&name=name
+ let policy = params.get("policy");
+ let name = params.get("NAME");
+ let type = params.get("type");
+ let result = getSharedState(SHARED_KEY);
+
+ result = result ? JSON.parse(result) : {};
+
+ var referrerLevel = "none";
+ var test = {};
+ if (request.hasHeader("Referer")) {
+ var referrer = request.getHeader("Referer");
+ if (referrer.indexOf("referrer_testserver") > 0) {
+ referrerLevel = "full";
+ } else if (referrer.indexOf(schemeFrom + "://example.com") == 0) {
+ referrerLevel = "origin";
+ } else {
+ // this is never supposed to happen
+ referrerLevel = "other-origin";
+ }
+ test.referrer = referrer;
+ } else {
+ test.referrer = "";
+ }
+ test.policy = referrerLevel;
+ test.expected = policy;
+
+ result[name] = test;
+
+ setSharedState(SHARED_KEY, JSON.stringify(result));
+
+ if (type === "img" || type == "link_element_preload") {
+ // return image
+ response.setHeader("Content-Type", "image/png");
+ response.write(IMG_BYTES);
+ return;
+ }
+ if (type === "iframe") {
+ // return iframe page
+ response.write("<html><body>I am the iframe</body></html>");
+ return;
+ }
+ if (type === "link") {
+ // forward link click to redirect URL to finish test
+ var loc = "http://" + BASE_URL + "ACTION=redirect";
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", loc, false);
+ }
+ return;
+ }
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+
+ // parse test arguments and start test
+ var attributePolicy = params.get("ATTRIBUTE_POLICY") || "";
+ var newAttributePolicy = params.get("NEW_ATTRIBUTE_POLICY") || "";
+ var metaPolicy = params.get("META_POLICY") || "";
+ var rel = params.get("REL") || "";
+ var name = params.get("NAME");
+
+ // anchor & area
+ var _getPage = createAETestPageUsingRefferer.bind(
+ null,
+ metaPolicy,
+ attributePolicy,
+ newAttributePolicy,
+ name,
+ rel
+ );
+ var _getAnchorPage = _getPage.bind(
+ null,
+ buildAnchorString,
+ schemeFrom,
+ schemeTo
+ );
+ var _getAreaPage = _getPage.bind(null, buildAreaString, schemeFrom, schemeTo);
+
+ // aMetaPolicy, aAttributePolicy, aNewAttributePolicy, aName, aChangingMethod, aStringBuilder
+ if (action === "generate-anchor-policy-test") {
+ response.write(_getAnchorPage());
+ return;
+ }
+ if (action === "generate-anchor-changing-policy-test-set-attribute") {
+ response.write(_getAnchorPage("setAttribute"));
+ return;
+ }
+ if (action === "generate-anchor-changing-policy-test-property") {
+ response.write(_getAnchorPage("property"));
+ return;
+ }
+ if (action === "generate-area-policy-test") {
+ response.write(_getAreaPage());
+ return;
+ }
+ if (action === "generate-area-changing-policy-test-set-attribute") {
+ response.write(_getAreaPage("setAttribute"));
+ return;
+ }
+ if (action === "generate-area-changing-policy-test-property") {
+ response.write(_getAreaPage("property"));
+ return;
+ }
+ if (action === "generate-anchor-target-blank-policy-test") {
+ response.write(
+ createTargetBlankRefferer(
+ metaPolicy,
+ name,
+ schemeFrom,
+ schemeTo,
+ referrerPolicyHeader
+ )
+ );
+ return;
+ }
+
+ // iframe
+ _getPage = createIframeTestPageUsingRefferer.bind(
+ null,
+ metaPolicy,
+ attributePolicy,
+ newAttributePolicy,
+ name,
+ "",
+ schemeFrom,
+ schemeTo
+ );
+
+ // aMetaPolicy, aAttributePolicy, aNewAttributePolicy, aName, aChangingMethod
+ if (action === "generate-iframe-policy-test") {
+ response.write(_getPage());
+ return;
+ }
+ if (action === "generate-iframe-changing-policy-test-set-attribute") {
+ response.write(_getPage("setAttribute"));
+ return;
+ }
+ if (action === "generate-iframe-changing-policy-test-property") {
+ response.write(_getPage("property"));
+ return;
+ }
+
+ // redirect tests with img and iframe
+ if (action === "generate-img-redirect-policy-test") {
+ response.write(createImgTestCase(params, attributePolicy, true));
+ return;
+ }
+ if (action === "generate-iframe-redirect-policy-test") {
+ response.write(
+ createIframeTestPageUsingRefferer(
+ metaPolicy,
+ attributePolicy,
+ newAttributePolicy,
+ name,
+ params,
+ schemeFrom,
+ schemeTo
+ )
+ );
+ return;
+ }
+
+ if (action === "generate-img-policy-test") {
+ response.write(createImgTestCase(params, attributePolicy, false));
+ return;
+ }
+
+ _getPage = createLinkPageUsingRefferer.bind(
+ null,
+ metaPolicy,
+ attributePolicy,
+ newAttributePolicy,
+ name,
+ rel
+ );
+ var _getLinkPage = _getPage.bind(null, buildLinkString, schemeFrom, schemeTo);
+
+ // link
+ if (action === "generate-link-policy-test") {
+ response.write(_getLinkPage());
+ return;
+ }
+ if (action === "generate-link-policy-test-set-attribute") {
+ response.write(_getLinkPage("setAttribute"));
+ return;
+ }
+ if (action === "generate-link-policy-test-property") {
+ response.write(_getLinkPage("property"));
+ return;
+ }
+
+ if (action === "generate-fetch-user-control-policy-test") {
+ response.write(
+ createFetchUserControlRPTestCase(name, schemeFrom, schemeTo, crossOrigin)
+ );
+ return;
+ }
+
+ response.write("I don't know action " + action);
+}
diff --git a/dom/security/test/referrer-policy/test_img_referrer.html b/dom/security/test/referrer-policy/test_img_referrer.html
new file mode 100644
index 0000000000..fcc80929d2
--- /dev/null
+++ b/dom/security/test/referrer-policy/test_img_referrer.html
@@ -0,0 +1,190 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test img policy attribute for Bug 1166910</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+<!--
+Testing that img referrer attribute is honoured correctly
+* Speculative parser loads (generate-img-policy-test)
+* regular loads (generate-img-policy-test2)
+* loading a single image multiple times with different policies (generate-img-policy-test3)
+* testing setAttribute and .referrer (generate-setAttribute-test)
+* regression tests that meta referrer is still working even if attribute referrers are enabled
+https://bugzilla.mozilla.org/show_bug.cgi?id=1166910
+-->
+
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+var advance = function() { tests.next(); };
+
+/**
+ * Listen for notifications from the child.
+ * These are sent in case of error, or when the loads we await have completed.
+ */
+window.addEventListener("message", function(event) {
+ if (event.data == "childLoadComplete" ||
+ event.data.contains("childLoadComplete")) {
+ advance();
+ }
+});
+
+/**
+ * helper to perform an XHR.
+ */
+function doXHR(aUrl, onSuccess, onFail) {
+ var xhr = new XMLHttpRequest();
+ xhr.responseType = "json";
+ xhr.onload = function () {
+ onSuccess(xhr);
+ };
+ xhr.onerror = function () {
+ onFail(xhr);
+ };
+ xhr.open('GET', aUrl, true);
+ xhr.send(null);
+}
+
+/**
+ * Grabs the results via XHR and passes to checker.
+ */
+function checkIndividualResults(aTestname, aExpectedImg, aName) {
+ doXHR('/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=get-test-results',
+ function(xhr) {
+ var results = xhr.response;
+ info(JSON.stringify(xhr.response));
+
+ for (let i in aName) {
+ ok(aName[i] in results.tests, aName[i] + " tests have to be performed.");
+ is(results.tests[aName[i]].policy, aExpectedImg[i], aTestname + ' --- ' + results.tests[aName[i]].policy + ' (' + results.tests[aName[i]].referrer + ')');
+ }
+
+ advance();
+ },
+ function(xhr) {
+ ok(false, "Can't get results from the counter server.");
+ SimpleTest.finish();
+ });
+}
+
+function resetState() {
+ doXHR('/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=resetState',
+ advance,
+ function(xhr) {
+ ok(false, "error in reset state");
+ SimpleTest.finish();
+ });
+}
+
+/**
+ * testing if img referrer attribute is honoured (1165501)
+ */
+var tests = (function*() {
+
+ yield SpecialPowers.pushPrefEnv(
+ { set: [["network.http.referer.disallowCrossSiteRelaxingDefault", false]] },
+ advance
+ );
+
+ var iframe = document.getElementById("testframe");
+ var sjs = "/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=generate-img-policy-test";
+
+ // setting img unsafe-url and meta origin - unsafe-url shall prevail (should use speculative load)
+ yield resetState();
+ var name = 'unsaf-url-with-meta-in-origin';
+ yield iframe.src = sjs + "&imgPolicy=" + escape('unsafe-url') + "&name=" + name + "&policy=" + escape('origin');
+ yield checkIndividualResults("unsafe-url (img) with origin in meta", ["full"], [name]);
+
+ // setting img no-referrer and meta default - no-referrer shall prevail (should use speculative load)
+ yield resetState();
+ name = 'no-referrer-with-meta-in-origin';
+ yield iframe.src = sjs + "&imgPolicy=" + escape('no-referrer')+ "&name=" + name + "&policy=" + escape('origin');
+ yield checkIndividualResults("no-referrer (img) with default in meta", ["none"], [name]);
+
+ // test referrer policy in regular load
+ yield resetState();
+ sjs = "/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=generate-img-policy-test2";
+ name = 'regular-load-unsafe-url';
+ yield iframe.src = sjs + "&imgPolicy=" + escape('unsafe-url') + "&name=" + name;
+ yield checkIndividualResults("unsafe-url in img", ["full"], [name]);
+
+ // test referrer policy in regular load with multiple images
+ var policies = ['unsafe-url', 'origin', 'no-referrer'];
+ var expected = ["full", "origin", "none"];
+ yield resetState();
+ sjs = "/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=generate-img-policy-test3";
+ name = 'multiple-images-'+policies[0]+'-'+policies[1]+'-'+policies[2];
+ yield iframe.src = sjs + "&imgPolicy1=" + escape(policies[0]) + "&imgPolicy2=" + escape(policies[1]) + "&imgPolicy3=" + escape(policies[2]) + "&name=" + name;
+ yield checkIndividualResults(policies[0]+", "+policies[1]+" and "+policies[2]+" in img", expected, [name+policies[0], name+policies[1], name+policies[2]]);
+
+ policies = ['origin', 'no-referrer', 'unsafe-url'];
+ expected = ["origin", "none", "full"];
+ yield resetState();
+ sjs = "/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=generate-img-policy-test3";
+ name = 'multiple-images-'+policies[0]+'-'+policies[1]+'-'+policies[2];
+ yield iframe.src = sjs + "&imgPolicy1=" + escape(policies[0]) + "&imgPolicy2=" + escape(policies[1]) + "&imgPolicy3=" + escape(policies[2]) + "&name=" + name;
+ yield checkIndividualResults(policies[0]+", "+policies[1]+" and "+policies[2]+" in img", expected, [name+policies[0], name+policies[1], name+policies[2]]);
+
+ policies = ['no-referrer', 'origin', 'unsafe-url'];
+ expected = ["none", "origin", "full"];
+ yield resetState();
+ sjs = "/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=generate-img-policy-test3";
+ name = 'multiple-images-'+policies[0]+'-'+policies[1]+'-'+policies[2];
+ yield iframe.src = sjs + "&imgPolicy1=" + escape(policies[0]) + "&imgPolicy2=" + escape(policies[1]) + "&imgPolicy3=" + escape(policies[2]) + "&name=" + name;
+ yield checkIndividualResults(policies[0]+", "+policies[1]+" and "+policies[2]+" in img", expected, [name+policies[0], name+policies[1], name+policies[2]]);
+
+ // regression tests that meta referrer is still working even if attribute referrers are enabled
+ yield resetState();
+ sjs = "/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=generate-img-policy-test4";
+ name = 'regular-load-no-referrer-meta';
+ yield iframe.src = sjs + "&policy=" + escape('no-referrer') + "&name=" + name;
+ yield checkIndividualResults("no-referrer in meta (no img referrer policy), speculative load", ["none"], [name]);
+
+ yield resetState();
+ sjs = "/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=generate-img-policy-test5";
+ name = 'regular-load-no-referrer-meta';
+ yield iframe.src = sjs + "&policy=" + escape('no-referrer') + "&name=" + name;
+ yield checkIndividualResults("no-referrer in meta (no img referrer policy), regular load", ["none"], [name]);
+
+ //test setAttribute
+ yield resetState();
+ sjs = "/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=generate-setAttribute-test1";
+ name = 'set-referrer-policy-attribute-before-src';
+ yield iframe.src = sjs + "&imgPolicy=" + escape('no-referrer') + "&policy=" + escape('unsafe-url') + "&name=" + name;
+ yield checkIndividualResults("no-referrer in img", ["none"], [name]);
+
+ yield resetState();
+ sjs = "/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=generate-setAttribute-test2";
+ name = 'set-referrer-policy-attribute-after-src';
+ yield iframe.src = sjs + "&imgPolicy=" + escape('no-referrer') + "&policy=" + escape('unsafe-url') + "&name=" + name;
+ yield checkIndividualResults("no-referrer in img", ["none"], [name]);
+
+ yield resetState();
+ sjs =
+ "/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=generate-setAttribute-test2";
+ name = 'set-invalid-referrer-policy-attribute-before-src-invalid';
+ yield iframe.src = sjs + "&imgPolicy=" + escape('invalid') + "&policy=" + escape('unsafe-url') + "&name=" + name;
+ yield checkIndividualResults("unsafe-url in meta, invalid in img", ["full"], [name]);
+
+ yield resetState();
+ sjs =
+ "/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=generate-setAttribute-test2";
+ name = 'set-invalid-referrer-policy-attribute-before-src-invalid';
+ yield iframe.src = sjs + "&imgPolicy=" + escape('default') + "&policy=" + escape('unsafe-url') + "&name=" + name;
+ yield checkIndividualResults("unsafe-url in meta, default in img", ["full"], [name]);
+
+ // complete.
+ SimpleTest.finish();
+})();
+
+</script>
+</head>
+
+<body onload="tests.next();">
+ <iframe id="testframe"></iframe>
+
+</body>
+</html>
diff --git a/dom/security/test/referrer-policy/test_referrer_header_current_document.html b/dom/security/test/referrer-policy/test_referrer_header_current_document.html
new file mode 100644
index 0000000000..27dc54a21f
--- /dev/null
+++ b/dom/security/test/referrer-policy/test_referrer_header_current_document.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test referrer header not affecting document.referrer for current document for Bug 1601743</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+
+ <!--
+ Testing that navigating to a document with Referrer-Policy:same-origin doesn't affect
+ the value of document.referrer for that document.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1601743
+ -->
+
+ <script type="application/javascript">
+ function getExpectedReferrer(referrer) {
+ let defaultPolicy = SpecialPowers.getIntPref("network.http.referer.defaultPolicy");
+ SimpleTest.ok([2, 3].indexOf(defaultPolicy) > -1, "default referrer policy should be either strict-origin-when-cross-origin(2) or no-referrer-when-downgrade(3)");
+ if (defaultPolicy == 2) {
+ return referrer.match(/https?:\/\/[^\/]+\/?/i)[0];
+ }
+ return referrer;
+ }
+ const IFRAME_URL = `${location.origin}/tests/dom/security/test/referrer-policy/referrer_header_current_document_iframe.html`;
+
+ SimpleTest.waitForExplicitFinish();
+ window.addEventListener("message", (event) => {
+ SimpleTest.is(event.data, getExpectedReferrer(IFRAME_URL), "Must have the original iframe as the referrer!");
+ SimpleTest.finish();
+ }, { once: true });
+ </script>
+</head>
+
+<body>
+<iframe src="referrer_header_current_document_iframe.html"></iframe>
+</body>
diff --git a/dom/security/test/referrer-policy/test_referrer_redirect.html b/dom/security/test/referrer-policy/test_referrer_redirect.html
new file mode 100644
index 0000000000..df7a75a19c
--- /dev/null
+++ b/dom/security/test/referrer-policy/test_referrer_redirect.html
@@ -0,0 +1,171 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test anchor and area policy attribute for Bug 1184781</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+ <!--
+ Testing referrer headers after redirects.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1184781
+ -->
+
+ <script type="application/javascript">
+
+ const SJS = "://example.com/tests/dom/security/test/referrer-policy/referrer_testserver.sjs?";
+ const PARAMS = ["ATTRIBUTE_POLICY", "NEW_ATTRIBUTE_POLICY", "META_POLICY", "RP_HEADER", "HSTS"];
+
+ const testCases = [
+ {ACTION: ["generate-img-redirect-policy-test", "generate-iframe-redirect-policy-test"],
+ TESTS: [
+ {
+ ATTRIBUTE_POLICY: "no-referrer",
+ NAME: "no-referrer-with-no-meta",
+ DESC: "no-referrer (img/iframe) with no meta",
+ RESULT: "none"
+ },
+ {
+ ATTRIBUTE_POLICY: "origin",
+ NAME: "origin-with-no-meta",
+ DESC: "origin (img/iframe) with no meta",
+ RESULT: "origin"
+ },
+ {
+ ATTRIBUTE_POLICY: "unsafe-url",
+ NAME: "unsafe-url-with-no-meta",
+ DESC: "unsafe-url (img/iframe) with no meta",
+ RESULT: "full"
+ },
+ {
+ META_POLICY: "unsafe-url",
+ NAME: "unsafe-url-in-meta",
+ DESC: "unsafe-url in meta",
+ RESULT: "full"
+ },
+ {
+ META_POLICY: "origin",
+ NAME: "origin-in-meta",
+ DESC: "origin in meta",
+ RESULT: "origin"
+ },
+ {
+ META_POLICY: "no-referrer",
+ NAME: "no-referrer-in-meta",
+ DESC: "no-referrer in meta",
+ RESULT: "none"
+ },
+ {
+ META_POLICY: "origin-when-cross-origin",
+ NAME: "origin-when-cross-origin-in-meta",
+ DESC: "origin-when-cross-origin in meta",
+ RESULT: "origin"
+ },
+ {
+ ATTRIBUTE_POLICY: "no-referrer",
+ RP_HEADER: "origin",
+ NAME: "no-referrer-with-no-meta-origin-RP-header",
+ DESC: "no-referrer (img/iframe) with no meta, origin Referrer-Policy redirect header",
+ RESULT: "none"
+ },
+ {
+ ATTRIBUTE_POLICY: "origin",
+ RP_HEADER: "no-referrer",
+ NAME: "origin-with-no-meta-no-referrer-RP-header",
+ DESC: "origin (img/iframe) with no meta, no-referrer Referrer-Policy redirect header",
+ RESULT: "none"
+ },
+ {
+ ATTRIBUTE_POLICY: "unsafe-url",
+ RP_HEADER: "origin",
+ NAME: "unsafe-url-with-no-meta-origin-RP-header",
+ DESC: "unsafe-url (img/iframe) with no meta, origin Referrer-Policy redirect header",
+ RESULT: "origin"
+ },
+ {
+ META_POLICY: "unsafe-url",
+ RP_HEADER: "origin",
+ NAME: "unsafe-url-in-meta-origin-RP-header",
+ DESC: "unsafe-url in meta, origin Referrer-Policy redirect header",
+ RESULT: "origin"
+ },
+ {
+ META_POLICY: "origin",
+ RP_HEADER: "no-referrer",
+ NAME: "origin-in-meta-no-referrer-RP-header",
+ DESC: "origin in meta, no-referrer Referrer-Policy redirect header",
+ RESULT: "none"
+ },
+ {
+ META_POLICY: "no-referrer",
+ RP_HEADER: "origin",
+ NAME: "no-referrer-in-meta-origin-RP-header",
+ DESC: "no-referrer in meta, origin Referrer-Policy redirect header",
+ RESULT: "none"
+ },
+ {
+ META_POLICY: "origin-when-cross-origin",
+ RP_HEADER: "unsafe-url",
+ NAME: "origin-when-cross-origin-in-meta-unsafe-url-RP-header",
+ DESC: "origin-when-cross-origin in meta, unsafe-url Referrer-Policy redirect header",
+ RESULT: "origin"
+ }
+ ]
+ },
+ // Check that "internal" redirects for mixed content upgrading
+ // are invisible, but not for HSTS upgrades (Bug 1857894).
+ {
+ ACTION: ["generate-img-policy-test"],
+ PREFS: [
+ ["security.mixed_content.upgrade_display_content", true],
+ ["security.mixed_content.upgrade_display_content.image", true],
+ ],
+ TESTS: [
+ {
+ META_POLICY: "strict-origin",
+ NAME: "img-strict-origin-mixed-content-upgrade",
+ DESC: "img-strict-origin-mixed-content-upgrade",
+ SCHEME_FROM: "https",
+ RESULT: "other-origin",
+ },
+ ]
+ },
+ {
+ ACTION: ["generate-img-policy-test"],
+ PREFS: [["security.mixed_content.upgrade_display_content", false]],
+ TESTS: [
+ {
+ META_POLICY: "strict-origin",
+ NAME: "img-strict-origin-mixed-content-no-upgrade",
+ DESC: "img-strict-origin-mixed-content-no-upgrade",
+ SCHEME_FROM: "https",
+ RESULT: "none",
+ },
+ ]
+ },
+ {
+ ACTION: ["generate-img-policy-test"],
+ PREFS: [
+ ["security.mixed_content.upgrade_display_content", false],
+ ["network.stricttransportsecurity.preloadlist", true],
+ ],
+ TESTS: [
+ {
+ META_POLICY: "strict-origin",
+ NAME: "img-strict-origin-hsts-upgrade",
+ DESC: "img-strict-origin-hsts-upgrade",
+ SCHEME_FROM: "https",
+ RESULT: "none",
+ HSTS: true,
+ },
+ ]
+ }
+ ];
+ </script>
+ <script type="application/javascript" src="/tests/dom/security/test/referrer-policy/referrer_helper.js"></script>
+</head>
+<body onload="tests.next();">
+ <iframe id="testframe"></iframe>
+</body>
+</html>
+
diff --git a/dom/security/test/sec-fetch/browser.toml b/dom/security/test/sec-fetch/browser.toml
new file mode 100644
index 0000000000..a21bf0e966
--- /dev/null
+++ b/dom/security/test/sec-fetch/browser.toml
@@ -0,0 +1,10 @@
+[DEFAULT]
+support-files = ["file_no_cache.sjs"]
+
+["browser_external_loads.js"]
+support-files = [
+ "file_dummy_link.html",
+ "file_dummy_link_location.html",
+]
+
+["browser_navigation.js"]
diff --git a/dom/security/test/sec-fetch/browser_external_loads.js b/dom/security/test/sec-fetch/browser_external_loads.js
new file mode 100644
index 0000000000..0340b46899
--- /dev/null
+++ b/dom/security/test/sec-fetch/browser_external_loads.js
@@ -0,0 +1,176 @@
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+var gExpectedHeader = {};
+
+function checkSecFetchUser(subject, topic, data) {
+ let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+ if (!channel.URI.spec.startsWith("https://example.com")) {
+ return;
+ }
+
+ info(`testing headers for load of ${channel.URI.spec}`);
+
+ const secFetchHeaders = [
+ "sec-fetch-mode",
+ "sec-fetch-dest",
+ "sec-fetch-user",
+ "sec-fetch-site",
+ ];
+
+ secFetchHeaders.forEach(header => {
+ const expectedValue = gExpectedHeader[header];
+ try {
+ is(
+ channel.getRequestHeader(header),
+ expectedValue,
+ `${header} is set to ${expectedValue}`
+ );
+ } catch (e) {
+ if (expectedValue) {
+ ok(false, `${header} should be set`);
+ } else {
+ ok(true, `${header} should not be set`);
+ }
+ }
+ });
+}
+
+add_task(async function external_load() {
+ waitForExplicitFinish();
+ Services.obs.addObserver(checkSecFetchUser, "http-on-stop-request");
+
+ let headersChecked = new Promise(resolve => {
+ let reqStopped = async (subject, topic, data) => {
+ Services.obs.removeObserver(reqStopped, "http-on-stop-request");
+ resolve();
+ };
+ Services.obs.addObserver(reqStopped, "http-on-stop-request");
+ });
+
+ // System fetch. Shouldn't use Sec- headers for that.
+ gExpectedHeader = {
+ "sec-fetch-site": null,
+ "sec-fetch-mode": null,
+ "sec-fetch-dest": null,
+ "sec-fetch-user": null,
+ };
+ await window.fetch(`${TEST_PATH}file_dummy_link.html?sysfetch`);
+ await headersChecked;
+
+ // Simulate an external load in the *current* window with
+ // Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL and the system principal.
+ gExpectedHeader = {
+ "sec-fetch-site": "none",
+ "sec-fetch-mode": "navigate",
+ "sec-fetch-dest": "document",
+ "sec-fetch-user": "?1",
+ };
+
+ let loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ window.browserDOMWindow.openURI(
+ makeURI(`${TEST_PATH}file_dummy_link.html`),
+ null,
+ Ci.nsIBrowserDOMWindow.OPEN_CURRENTWINDOW,
+ Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL,
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ await loaded;
+
+ // Open a link in a *new* window through the context menu.
+ gExpectedHeader = {
+ "sec-fetch-site": "same-origin",
+ "sec-fetch-mode": "navigate",
+ "sec-fetch-dest": "document",
+ "sec-fetch-user": "?1",
+ };
+
+ loaded = BrowserTestUtils.waitForNewWindow({
+ url: `${TEST_PATH}file_dummy_link_location.html`,
+ });
+ BrowserTestUtils.waitForEvent(document, "popupshown", false, event => {
+ document.getElementById("context-openlink").doCommand();
+ event.target.hidePopup();
+ return true;
+ });
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#dummylink",
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+
+ let win = await loaded;
+ win.close();
+
+ // Simulate an external load in a *new* window with
+ // Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL and the system principal.
+ gExpectedHeader = {
+ "sec-fetch-site": "none",
+ "sec-fetch-mode": "navigate",
+ "sec-fetch-dest": "document",
+ "sec-fetch-user": "?1",
+ };
+
+ loaded = BrowserTestUtils.waitForNewWindow({
+ url: "https://example.com/newwindow",
+ });
+ window.browserDOMWindow.openURI(
+ makeURI("https://example.com/newwindow"),
+ null,
+ Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW,
+ Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL,
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ win = await loaded;
+ win.close();
+
+ // Open a *new* window through window.open without user activation.
+ gExpectedHeader = {
+ "sec-fetch-site": "same-origin",
+ "sec-fetch-mode": "navigate",
+ "sec-fetch-dest": "document",
+ };
+
+ loaded = BrowserTestUtils.waitForNewWindow({
+ url: "https://example.com/windowopen",
+ });
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.window.open(
+ "https://example.com/windowopen",
+ "_blank",
+ "height=500,width=500"
+ );
+ });
+ win = await loaded;
+ win.close();
+
+ // Open a *new* window through window.open with user activation.
+ gExpectedHeader = {
+ "sec-fetch-site": "same-origin",
+ "sec-fetch-mode": "navigate",
+ "sec-fetch-dest": "document",
+ "sec-fetch-user": "?1",
+ };
+
+ loaded = BrowserTestUtils.waitForNewWindow({
+ url: "https://example.com/windowopen_withactivation",
+ });
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.document.notifyUserGestureActivation();
+ content.window.open(
+ "https://example.com/windowopen_withactivation",
+ "_blank",
+ "height=500,width=500"
+ );
+ content.document.clearUserGestureActivation();
+ });
+ win = await loaded;
+ win.close();
+
+ Services.obs.removeObserver(checkSecFetchUser, "http-on-stop-request");
+ finish();
+});
diff --git a/dom/security/test/sec-fetch/browser_navigation.js b/dom/security/test/sec-fetch/browser_navigation.js
new file mode 100644
index 0000000000..d203391356
--- /dev/null
+++ b/dom/security/test/sec-fetch/browser_navigation.js
@@ -0,0 +1,182 @@
+"use strict";
+
+const REQUEST_URL =
+ "https://example.com/browser/dom/security/test/sec-fetch/file_no_cache.sjs";
+
+let gTestCounter = 0;
+let gExpectedHeader = {};
+
+async function setup() {
+ waitForExplicitFinish();
+}
+
+function checkSecFetchUser(subject, topic, data) {
+ let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+ if (!channel.URI.spec.startsWith("https://example.com/")) {
+ return;
+ }
+
+ info(`testing headers for load of ${channel.URI.spec}`);
+
+ const secFetchHeaders = [
+ "sec-fetch-mode",
+ "sec-fetch-dest",
+ "sec-fetch-user",
+ "sec-fetch-site",
+ ];
+
+ secFetchHeaders.forEach(header => {
+ const expectedValue = gExpectedHeader[header];
+ try {
+ is(
+ channel.getRequestHeader(header),
+ expectedValue,
+ `${header} is set to ${expectedValue}`
+ );
+ } catch (e) {
+ if (expectedValue) {
+ ok(false, "required headers are set");
+ } else {
+ ok(true, `${header} should not be set`);
+ }
+ }
+ });
+
+ gTestCounter++;
+}
+
+async function testNavigations() {
+ gTestCounter = 0;
+
+ // Load initial site
+ let loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ BrowserTestUtils.startLoadingURIString(gBrowser, REQUEST_URL + "?test1");
+ await loaded;
+
+ // Load another site
+ loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ content.document.notifyUserGestureActivation(); // simulate user activation
+ let test2Button = content.document.getElementById("test2_button");
+ test2Button.click();
+ content.document.clearUserGestureActivation();
+ });
+ await loaded;
+ // Load another site
+ loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ content.document.notifyUserGestureActivation(); // simulate user activation
+ let test3Button = content.document.getElementById("test3_button");
+ test3Button.click();
+ content.document.clearUserGestureActivation();
+ });
+ await loaded;
+
+ gExpectedHeader = {
+ "sec-fetch-mode": "navigate",
+ "sec-fetch-dest": "document",
+ "sec-fetch-site": "same-origin",
+ "sec-fetch-user": "?1",
+ };
+
+ // Register the http request observer.
+ // All following actions should cause requests with the sec-fetch-user header
+ // set.
+ Services.obs.addObserver(checkSecFetchUser, "http-on-stop-request");
+
+ // Go back one site by clicking the back button
+ info("Clicking back button");
+ loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ document.notifyUserGestureActivation(); // simulate user activation
+ let backButton = document.getElementById("back-button");
+ backButton.click();
+ document.clearUserGestureActivation();
+ await loaded;
+
+ // Reload the site by clicking the reload button
+ info("Clicking reload button");
+ loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ document.notifyUserGestureActivation(); // simulate user activation
+ let reloadButton = document.getElementById("reload-button");
+ await TestUtils.waitForCondition(() => {
+ return !reloadButton.disabled;
+ });
+ reloadButton.click();
+ document.clearUserGestureActivation();
+ await loaded;
+
+ // Go forward one site by clicking the forward button
+ info("Clicking forward button");
+ loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ document.notifyUserGestureActivation(); // simulate user activation
+ let forwardButton = document.getElementById("forward-button");
+ forwardButton.click();
+ document.clearUserGestureActivation();
+ await loaded;
+
+ // Testing history.back/forward...
+
+ info("going back with history.back");
+ loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ content.document.notifyUserGestureActivation(); // simulate user activation
+ content.history.back();
+ content.document.clearUserGestureActivation();
+ });
+ await loaded;
+
+ info("going forward with history.forward");
+ loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ content.document.notifyUserGestureActivation(); // simulate user activation
+ content.history.forward();
+ content.document.clearUserGestureActivation();
+ });
+ await loaded;
+
+ gExpectedHeader = {
+ "sec-fetch-mode": "navigate",
+ "sec-fetch-dest": "document",
+ "sec-fetch-site": "same-origin",
+ };
+
+ info("going back with history.back without user activation");
+ loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ content.history.back();
+ });
+ await loaded;
+
+ info("going forward with history.forward without user activation");
+ loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ content.history.forward();
+ });
+ await loaded;
+
+ Assert.strictEqual(
+ gTestCounter,
+ 7,
+ "testing that all five actions have been tested."
+ );
+
+ Services.obs.removeObserver(checkSecFetchUser, "http-on-stop-request");
+}
+
+add_task(async function () {
+ waitForExplicitFinish();
+
+ await testNavigations();
+
+ // If fission is enabled we also want to test the navigations with the bfcache
+ // in the parent.
+ if (SpecialPowers.getBoolPref("fission.autostart")) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["fission.bfcacheInParent", true]],
+ });
+
+ await testNavigations();
+ }
+
+ finish();
+});
diff --git a/dom/security/test/sec-fetch/file_dummy_link.html b/dom/security/test/sec-fetch/file_dummy_link.html
new file mode 100644
index 0000000000..2150054226
--- /dev/null
+++ b/dom/security/test/sec-fetch/file_dummy_link.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1738694 - Sec-Fetch-User header is missing when opening a link in a new window</title>
+</head>
+<body>
+ <a id="dummylink" href="file_dummy_link_location.html">Open</a>
+</body>
+</html>
diff --git a/dom/security/test/sec-fetch/file_dummy_link_location.html b/dom/security/test/sec-fetch/file_dummy_link_location.html
new file mode 100644
index 0000000000..9f9400e1c3
--- /dev/null
+++ b/dom/security/test/sec-fetch/file_dummy_link_location.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1738694 - Sec-Fetch-User header is missing when opening a link in a new window</title>
+</head>
+<body>
+ <h1>file_dummy_link_location.html</h1>
+</body>
+</html>
diff --git a/dom/security/test/sec-fetch/file_no_cache.sjs b/dom/security/test/sec-fetch/file_no_cache.sjs
new file mode 100644
index 0000000000..9e75209e44
--- /dev/null
+++ b/dom/security/test/sec-fetch/file_no_cache.sjs
@@ -0,0 +1,28 @@
+const MESSAGE_PAGE = function (msg) {
+ return `
+<html>
+<script type="text/javascript">
+window.parent.postMessage({test : "${msg}"},"*");
+</script>
+<script>
+ addEventListener("back", () => {
+ history.back();
+ });
+ addEventListener("forward", () => {
+ history.forward();
+ });
+</script>
+<body>
+ <a id="test2_button" href="https://example.com/browser/dom/security/test/sec-fetch/file_no_cache.sjs?test2">Click me</a>
+ <a id="test3_button" href="https://example.com/browser/dom/security/test/sec-fetch/file_no_cache.sjs?test3">Click me</a>
+<body>
+</html>
+`;
+};
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-store");
+ response.setHeader("Content-Type", "text/html");
+
+ response.write(MESSAGE_PAGE(request.queryString));
+}
diff --git a/dom/security/test/sec-fetch/file_redirect.sjs b/dom/security/test/sec-fetch/file_redirect.sjs
new file mode 100644
index 0000000000..84c2c39913
--- /dev/null
+++ b/dom/security/test/sec-fetch/file_redirect.sjs
@@ -0,0 +1,34 @@
+const SITE_META_REDIRECT = `
+<html>
+ <head>
+ <meta http-equiv="refresh" content="0; url='https://example.com/tests/dom/security/test/sec-fetch/file_redirect.sjs?redirect302'">
+ </head>
+ <body>
+ META REDIRECT
+ </body>
+</html>
+`;
+
+const REDIRECT_302 =
+ "https://example.com/tests/dom/security/test/sec-fetch/file_redirect.sjs?pageC";
+
+function handleRequest(req, res) {
+ // avoid confusing cache behaviour
+ res.setHeader("Cache-Control", "no-cache", false);
+ res.setHeader("Content-Type", "text/html", false);
+
+ switch (req.queryString) {
+ case "meta":
+ res.write(SITE_META_REDIRECT);
+ return;
+ case "redirect302":
+ res.setStatusLine("1.1", 302, "Found");
+ res.setHeader("Location", REDIRECT_302, false);
+ return;
+ case "pageC":
+ res.write("<html><body>PAGE C</body></html>");
+ return;
+ }
+
+ res.write(`<html><body>THIS SHOULD NEVER BE DISPLAYED</body></html>`);
+}
diff --git a/dom/security/test/sec-fetch/file_trustworthy_loopback.html b/dom/security/test/sec-fetch/file_trustworthy_loopback.html
new file mode 100644
index 0000000000..88f9242650
--- /dev/null
+++ b/dom/security/test/sec-fetch/file_trustworthy_loopback.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1732069: Sec-Fetch-Site inconsistent on localhost/IPs</title>
+</head>
+<body>
+ <iframe src="http://localhost:9898/foo"></iframe>
+ <iframe src="http://localhost:9899/foo"></iframe>
+ <iframe src="http://sub.localhost/foo"></iframe>
+</body>
+</html>
diff --git a/dom/security/test/sec-fetch/file_websocket_wsh.py b/dom/security/test/sec-fetch/file_websocket_wsh.py
new file mode 100644
index 0000000000..b7159c742b
--- /dev/null
+++ b/dom/security/test/sec-fetch/file_websocket_wsh.py
@@ -0,0 +1,6 @@
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ pass
diff --git a/dom/security/test/sec-fetch/mochitest.toml b/dom/security/test/sec-fetch/mochitest.toml
new file mode 100644
index 0000000000..1b3db1772e
--- /dev/null
+++ b/dom/security/test/sec-fetch/mochitest.toml
@@ -0,0 +1,29 @@
+[DEFAULT]
+support-files = [
+ "file_no_cache.sjs",
+ "file_redirect.sjs",
+]
+
+["test_iframe_history_manipulation.html"]
+
+["test_iframe_src_metaRedirect.html"]
+
+["test_iframe_srcdoc_metaRedirect.html"]
+
+["test_iframe_window_open_metaRedirect.html"]
+
+["test_trustworthy_loopback.html"]
+skip-if = [
+ "os == 'linux' && !fission", # Bug 1805760
+ "http3",
+ "http2",
+]
+support-files = ["file_trustworthy_loopback.html"]
+
+["test_websocket.html"]
+skip-if = [
+ "os == 'android'", # no websocket support Bug 982828
+ "http3",
+ "http2",
+]
+support-files = ["file_websocket_wsh.py"]
diff --git a/dom/security/test/sec-fetch/test_iframe_history_manipulation.html b/dom/security/test/sec-fetch/test_iframe_history_manipulation.html
new file mode 100644
index 0000000000..5ec749bf4d
--- /dev/null
+++ b/dom/security/test/sec-fetch/test_iframe_history_manipulation.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1648825 - Fetch Metadata Headers contain invalid value for Sec-Fetch-Site for history manipulation</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+
+<script class="testbody" type="text/javascript">
+
+const REQUEST_PATH = 'tests/dom/security/test/sec-fetch/file_no_cache.sjs'
+let sendHome = true;
+let testCounter = 0;
+let testFrame;
+
+var script = SpecialPowers.loadChromeScript(() => {
+ /* eslint-env mozilla/chrome-script */
+ Services.obs.addObserver(function onExamResp(subject, topic, data) {
+ let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+ info("request observed: " + channel.URI.spec);
+ if (!channel.URI.spec.startsWith("https://example.org")) {
+ return;
+ }
+ let headerPresent = false;
+ try {
+ is(channel.getRequestHeader("Sec-Fetch-Site"), "cross-site", "testing sec-fetch-site is cross-site");
+
+ // This should fail and cause the catch clause to be executed.
+ channel.getRequestHeader("Sec-Fetch-User");
+ headerPresent = true;
+ } catch (e) {
+ headerPresent = false;
+ }
+
+ ok(!headerPresent, "testing sec-fetch-user header is not set");
+
+ sendAsyncMessage("test-pass");
+ }, "http-on-stop-request");
+});
+
+script.addMessageListener("test-pass", () => {
+ testCounter++;
+ if(testCounter == 2) {
+ SimpleTest.finish();
+ }
+});
+
+window.addEventListener("message", function (event) {
+ iframeAction(event.data.test);
+});
+
+function iframeAction(test) {
+ info("received message " + test);
+
+ switch (test) {
+ case 'test':
+ testFrame.contentWindow.location = `https://example.org/${REQUEST_PATH}?test#bypass`;
+ if(sendHome) {
+ // We need to send the message manually here because there is no request send to the server.
+ window.postMessage({test: "home"}, "*");
+ sendHome = false;
+ }
+
+ break;
+ case 'home':
+ testFrame.contentWindow.location = `/${REQUEST_PATH}?back`;
+ break;
+ case 'back':
+ testFrame.contentWindow.history.back();
+ break;
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+
+testFrame = document.createElement('iframe');
+testFrame.src = `https://example.org/${REQUEST_PATH}?test`;
+document.body.appendChild(testFrame);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/sec-fetch/test_iframe_src_metaRedirect.html b/dom/security/test/sec-fetch/test_iframe_src_metaRedirect.html
new file mode 100644
index 0000000000..28eae80226
--- /dev/null
+++ b/dom/security/test/sec-fetch/test_iframe_src_metaRedirect.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1647128 - Fetch Metadata Headers contain invalid value for Sec-Fetch-Site for meta redirects</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+
+<script class="testbody" type="text/javascript">
+/*
+ * Description of the test:
+ * We load site A that redirects to site B using a meta refresh,
+ * finally site B redirects to site C via a 302 redirect.
+ * The first load of site A is made by an iframe: frame.src = "...".
+ * We check that all requests have the Sec-Fetch-* headers set appropriately.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+const REQUEST_URL = "https://example.com/tests/dom/security/test/sec-fetch/file_redirect.sjs";
+let testPassCounter = 0;
+
+var script = SpecialPowers.loadChromeScript(() => {
+ /* eslint-env mozilla/chrome-script */
+ Services.obs.addObserver(function onExamResp(subject, topic, data) {
+ let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+ if (!channel.URI.spec.startsWith("https://example.com/tests/dom/security/test/sec-fetch/file_redirect.sjs")) {
+ return;
+ }
+
+ // The redirection flow is the following:
+ // http://mochi.test:8888 -> https://example.com?meta -> https://example.com?redirect302 -> https://example.com?pageC
+ // So the Sec-Fetch-* headers for each request should be:
+ const expectedHeaders = {
+ "?meta": {
+ "Sec-Fetch-Site": "cross-site",
+ "Sec-Fetch-Mode": "navigate",
+ "Sec-Fetch-Dest": "iframe",
+ "Sec-Fetch-User": null,
+ },
+ "?redirect302": {
+ "Sec-Fetch-Site": "same-origin",
+ "Sec-Fetch-Mode": "navigate",
+ "Sec-Fetch-Dest": "iframe",
+ "Sec-Fetch-User": null,
+ },
+ "?pageC": {
+ "Sec-Fetch-Site": "same-origin",
+ "Sec-Fetch-Mode": "navigate",
+ "Sec-Fetch-Dest": "iframe",
+ "Sec-Fetch-User": null,
+ },
+ };
+
+ let matchedOne = false;
+ for (const [query, headers] of Object.entries(expectedHeaders)) {
+ if (!channel.URI.spec.endsWith(query)) {
+ continue;
+ }
+ matchedOne = true;
+
+ for (const [header, value] of Object.entries(headers)) {
+ try {
+ is(channel.getRequestHeader(header), value, `testing ${header} for the ${query} query`);
+ } catch (e) {
+ is(header, "Sec-Fetch-User", "testing Sec-Fetch-User");
+ }
+ }
+ }
+ ok(matchedOne, "testing expectedHeaders");
+
+ sendAsyncMessage("test-pass");
+ }, "http-on-stop-request");
+});
+
+script.addMessageListener("test-pass", async () => {
+ testPassCounter++;
+ if (testPassCounter < 3) {
+ return;
+ }
+
+ // If we received "test-pass" 3 times we know that all loads had Sec-Fetch-* headers set appropriately.
+ SimpleTest.finish();
+});
+
+let frame = document.createElement("iframe");
+frame.src = REQUEST_URL + "?meta";
+document.body.appendChild(frame);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/sec-fetch/test_iframe_srcdoc_metaRedirect.html b/dom/security/test/sec-fetch/test_iframe_srcdoc_metaRedirect.html
new file mode 100644
index 0000000000..adee5afe84
--- /dev/null
+++ b/dom/security/test/sec-fetch/test_iframe_srcdoc_metaRedirect.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1647128 - Fetch Metadata Headers contain invalid value for Sec-Fetch-Site for meta redirects</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+
+<script class="testbody" type="text/javascript">
+/*
+ * Description of the test:
+ * We load site A that redirects to site B using a meta refresh,
+ * finally site B redirects to site C via a 302 redirect.
+ * The first load of site A is made by an iframe: frame.srcdoc = "<meta ...".
+ * We check that all requests have the Sec-Fetch-* headers set appropriately.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+const REQUEST_URL = "https://example.com/tests/dom/security/test/sec-fetch/file_redirect.sjs";
+let testPassCounter = 0;
+
+var script = SpecialPowers.loadChromeScript(() => {
+ /* eslint-env mozilla/chrome-script */
+ Services.obs.addObserver(function onExamResp(subject, topic, data) {
+ let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+ if (!channel.URI.spec.startsWith("https://example.com/tests/dom/security/test/sec-fetch/file_redirect.sjs")) {
+ return;
+ }
+
+ // The redirection flow is the following:
+ // http://mochi.test:8888 -> https://example.com?meta -> https://example.com?redirect302 -> https://example.com?pageC
+ // So the Sec-Fetch-* headers for each request should be:
+ const expectedHeaders = {
+ "?meta": {
+ "Sec-Fetch-Site": "cross-site",
+ "Sec-Fetch-Mode": "navigate",
+ "Sec-Fetch-Dest": "iframe",
+ "Sec-Fetch-User": null,
+ },
+ "?redirect302": {
+ "Sec-Fetch-Site": "same-origin",
+ "Sec-Fetch-Mode": "navigate",
+ "Sec-Fetch-Dest": "iframe",
+ "Sec-Fetch-User": null,
+ },
+ "?pageC": {
+ "Sec-Fetch-Site": "same-origin",
+ "Sec-Fetch-Mode": "navigate",
+ "Sec-Fetch-Dest": "iframe",
+ "Sec-Fetch-User": null,
+ },
+ };
+
+ let matchedOne = false;
+ for (const [query, headers] of Object.entries(expectedHeaders)) {
+ if (!channel.URI.spec.endsWith(query)) {
+ continue;
+ }
+ matchedOne = true;
+
+ for (const [header, value] of Object.entries(headers)) {
+ try {
+ is(channel.getRequestHeader(header), value, `testing ${header} for the ${query} query`);
+ } catch (e) {
+ is(header, "Sec-Fetch-User", "testing Sec-Fetch-User");
+ }
+ }
+ }
+ ok(matchedOne, "testing expectedHeaders");
+
+ sendAsyncMessage("test-pass");
+ }, "http-on-stop-request");
+});
+
+script.addMessageListener("test-pass", async () => {
+ testPassCounter++;
+ if (testPassCounter < 3) {
+ return;
+ }
+
+ // If we received "test-pass" 3 times we know that all loads had Sec-Fetch-* headers set appropriately.
+ SimpleTest.finish();
+});
+
+let frame = document.createElement("iframe");
+frame.srcdoc = `<meta http-equiv="refresh" content="0; url='${REQUEST_URL}?meta'">`;
+document.body.appendChild(frame);
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/sec-fetch/test_iframe_window_open_metaRedirect.html b/dom/security/test/sec-fetch/test_iframe_window_open_metaRedirect.html
new file mode 100644
index 0000000000..b532baeb5e
--- /dev/null
+++ b/dom/security/test/sec-fetch/test_iframe_window_open_metaRedirect.html
@@ -0,0 +1,98 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1647128 - Fetch Metadata Headers contain invalid value for Sec-Fetch-Site for meta redirects</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+
+<script class="testbody" type="text/javascript">
+/*
+ * Description of the test:
+ * We load site A that redirects to site B using a meta refresh,
+ * finally site B redirects to site C via a 302 redirect.
+ * The first load of site A is made through window.open.
+ * We check that all requests have the Sec-Fetch-* headers set appropriately.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+const REQUEST_URL = "https://example.com/tests/dom/security/test/sec-fetch/file_redirect.sjs";
+let testPassCounter = 0;
+let testWindow;
+
+var script = SpecialPowers.loadChromeScript(() => {
+ /* eslint-env mozilla/chrome-script */
+ Services.obs.addObserver(function onExamResp(subject, topic, data) {
+ let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+ if (!channel.URI.spec.startsWith("https://example.com/tests/dom/security/test/sec-fetch/file_redirect.sjs")) {
+ return;
+ }
+
+ // The redirection flow is the following:
+ // http://mochi.test:8888 -> https://example.com?meta -> https://example.com?redirect302 -> https://example.com?pageC
+ // So the Sec-Fetch-* headers for each request should be:
+ const expectedHeaders = {
+ "?meta": {
+ "Sec-Fetch-Site": "cross-site",
+ "Sec-Fetch-Mode": "navigate",
+ "Sec-Fetch-Dest": "document",
+ "Sec-Fetch-User": null,
+ },
+ "?redirect302": {
+ "Sec-Fetch-Site": "same-origin",
+ "Sec-Fetch-Mode": "navigate",
+ "Sec-Fetch-Dest": "document",
+ "Sec-Fetch-User": null,
+ },
+ "?pageC": {
+ "Sec-Fetch-Site": "same-origin",
+ "Sec-Fetch-Mode": "navigate",
+ "Sec-Fetch-Dest": "document",
+ "Sec-Fetch-User": null,
+ },
+ };
+
+ let matchedOne = false;
+ for (const [query, headers] of Object.entries(expectedHeaders)) {
+ if (!channel.URI.spec.endsWith(query)) {
+ continue;
+ }
+ matchedOne = true;
+
+ for (const [header, value] of Object.entries(headers)) {
+ try {
+ is(channel.getRequestHeader(header), value, `testing ${header} for the ${query} query`);
+ } catch (e) {
+ is(header, "Sec-Fetch-User", "testing Sec-Fetch-User");
+ }
+ }
+ }
+ ok(matchedOne, "testing expectedHeaders");
+
+ sendAsyncMessage("test-pass");
+ }, "http-on-stop-request");
+});
+
+script.addMessageListener("test-pass", async () => {
+ testPassCounter++;
+ if (testPassCounter < 3) {
+ return;
+ }
+
+ if (testWindow) {
+ testWindow.close();
+ }
+
+ // If we received "test-pass" 3 times we know that all loads had Sec-Fetch-* headers set appropriately.
+ SimpleTest.finish();
+});
+
+testWindow = window.open(REQUEST_URL + "?meta");
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/sec-fetch/test_trustworthy_loopback.html b/dom/security/test/sec-fetch/test_trustworthy_loopback.html
new file mode 100644
index 0000000000..95ecac17ed
--- /dev/null
+++ b/dom/security/test/sec-fetch/test_trustworthy_loopback.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1732069: Sec-Fetch-Site inconsistent on localhost/IPs</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+let testsSucceeded = 0;
+
+let win;
+function checkTestsDone() {
+ testsSucceeded++;
+ if (testsSucceeded == 3) {
+ win.close();
+ SimpleTest.finish();
+ }
+}
+
+var script = SpecialPowers.loadChromeScript(() => {
+ /* eslint-env mozilla/chrome-script */
+ Services.obs.addObserver(function onExamResp(subject, topic, data) {
+ let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+ if (!channel.URI.spec.includes("localhost") ||
+ channel.URI.spec.startsWith("http://localhost:9898/tests/dom/security/test/sec-fetch/file_trustworthy_loopback.html")) {
+ return;
+ }
+
+ const expectedHeaders = {
+ "localhost:9898": {
+ "sec-fetch-site": "same-origin",
+ "sec-fetch-mode": "navigate",
+ "sec-fetch-dest": "iframe",
+ },
+ "sub.localhost:-1": {
+ "sec-fetch-site": "cross-site",
+ "sec-fetch-mode": "navigate",
+ "sec-fetch-dest": "iframe",
+ },
+ "localhost:9899": {
+ "sec-fetch-site": "same-site",
+ "sec-fetch-mode": "navigate",
+ "sec-fetch-dest": "iframe",
+ },
+ };
+
+ info(`checking headers for request to ${channel.URI.spec}`);
+ const expected = expectedHeaders[channel.URI.host + ":" + channel.URI.port];
+ for (let key in expected) {
+ try {
+ is(channel.getRequestHeader(key), expected[key], `${key} header matches`);
+ } catch (e) {
+ ok(false, "failed to check headers");
+ }
+ }
+ sendAsyncMessage("test-end");
+ }, "http-on-stop-request");
+});
+
+script.addMessageListener("test-end", () => {
+ checkTestsDone();
+});
+
+SpecialPowers.pushPrefEnv({set: [
+ ["network.proxy.allow_hijacking_localhost", true],
+ ["network.proxy.testing_localhost_is_secure_when_hijacked", true],
+]}).then(function() {
+ win = window.open("http://localhost:9898/tests/dom/security/test/sec-fetch/file_trustworthy_loopback.html");
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/sec-fetch/test_websocket.html b/dom/security/test/sec-fetch/test_websocket.html
new file mode 100644
index 0000000000..5df0553a4f
--- /dev/null
+++ b/dom/security/test/sec-fetch/test_websocket.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1628605: Test Sec-Fetch-* header for websockets</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+let testsSucceeded = 0;
+
+function checkTestsDone() {
+ testsSucceeded++;
+ if (testsSucceeded == 2) {
+ SimpleTest.finish();
+ }
+}
+
+var script = SpecialPowers.loadChromeScript(() => {
+ /* eslint-env mozilla/chrome-script */
+ Services.obs.addObserver(function onExamResp(subject, topic, data) {
+ let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+ if (!channel.URI.spec.startsWith("https://example.com/tests/dom/security/test/sec-fetch/file_websocket")) {
+ return;
+ }
+
+ // Sec-Fetch-* Headers should be present for Dest, Mode, Site
+ try {
+ let secFetchDest = channel.getRequestHeader("Sec-Fetch-Dest");
+ is(secFetchDest, "empty", "testing sec-fetch-dest");
+
+ let secFetchMode = channel.getRequestHeader("Sec-Fetch-Mode");
+ is(secFetchMode, "websocket", "testing sec-fetch-mode");
+
+ let secFetchSite = channel.getRequestHeader("Sec-Fetch-Site");
+ is(secFetchSite, "cross-site", "testing sec-fetch-site");
+ }
+ catch (e) {
+ ok(false, "testing sec-fetch-*");
+ }
+
+ // Sec-Fetch-User should not be present
+ try {
+ channel.getRequestHeader("Sec-Fetch-User");
+ ok(false, "testing sec-fetch-user");
+ }
+ catch (e) {
+ ok(true, "testing sec-fetch-user");
+ }
+ Services.obs.removeObserver(onExamResp, "http-on-stop-request");
+
+ sendAsyncMessage("test-end");
+ }, "http-on-stop-request");
+});
+
+script.addMessageListener("test-end", () => {
+ checkTestsDone();
+});
+
+var wssSocket = new WebSocket("wss://example.com/tests/dom/security/test/sec-fetch/file_websocket");
+wssSocket.onopen = function(e) {
+ ok(true, "sanity: wssSocket onopen");
+ checkTestsDone();
+};
+wssSocket.onerror = function(e) {
+ ok(false, "sanity: wssSocket onerror");
+};
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/sri/file_bug_1271796.css b/dom/security/test/sri/file_bug_1271796.css
new file mode 100644
index 0000000000..c0928f2cf0
--- /dev/null
+++ b/dom/security/test/sri/file_bug_1271796.css
@@ -0,0 +1,2 @@
+/*! Simple test for bug 1271796 */
+p::before { content: "\2014"; }
diff --git a/dom/security/test/sri/iframe_script_crossdomain.html b/dom/security/test/sri/iframe_script_crossdomain.html
new file mode 100644
index 0000000000..fe91834db5
--- /dev/null
+++ b/dom/security/test/sri/iframe_script_crossdomain.html
@@ -0,0 +1,135 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+
+<script type="application/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ window.hasCORSLoaded = false;
+ window.hasNonCORSLoaded = false;
+
+ function good_nonsriLoaded() {
+ ok(true, "Non-eligible non-SRI resource was loaded correctly.");
+ }
+ function bad_nonsriBlocked() {
+ ok(false, "Non-eligible non-SRI resources should be loaded!");
+ }
+
+ function good_nonCORSInvalidBlocked() {
+ ok(true, "A non-CORS resource with invalid metadata was correctly blocked.");
+ }
+ function bad_nonCORSInvalidLoaded() {
+ ok(false, "Non-CORS resources with invalid metadata should be blocked!");
+ }
+
+ window.onerrorCalled = false;
+ window.onloadCalled = false;
+
+ function bad_onloadCalled() {
+ window.onloadCalled = true;
+ }
+
+ function good_onerrorCalled() {
+ window.onerrorCalled = true;
+ }
+
+ function good_incorrect301Blocked() {
+ ok(true, "A non-CORS load with incorrect hash redirected to a different origin was blocked correctly.");
+ }
+ function bad_incorrect301Loaded() {
+ ok(false, "Non-CORS loads with incorrect hashes redirecting to a different origin should be blocked!");
+ }
+
+ function good_correct301Blocked() {
+ ok(true, "A non-CORS load with correct hash redirected to a different origin was blocked correctly.");
+ }
+ function bad_correct301Loaded() {
+ ok(false, "Non-CORS loads with correct hashes redirecting to a different origin should be blocked!");
+ }
+
+ function good_correctDataLoaded() {
+ ok(true, "Since data: URLs are same-origin, they should be loaded.");
+ }
+ function bad_correctDataBlocked() {
+ todo(false, "We should not block scripts in data: URIs!");
+ }
+ function good_correctDataCORSLoaded() {
+ ok(true, "A data: URL with a CORS load was loaded correctly.");
+ }
+ function bad_correctDataCORSBlocked() {
+ ok(false, "We should not BLOCK scripts!");
+ }
+
+ window.onload = function() {
+ SimpleTest.finish()
+ }
+</script>
+
+<!-- cors-enabled. should be loaded -->
+<script src="http://example.com/tests/dom/security/test/sri/script_crossdomain1.js"
+ crossorigin=""
+ integrity="sha512-9Tv2DL1fHvmPQa1RviwKleE/jq72jgxj8XGLyWn3H6Xp/qbtfK/jZINoPFAv2mf0Nn1TxhZYMFULAbzJNGkl4Q=="></script>
+
+<!-- not cors-enabled. should be blocked -->
+<script src="http://example.com/tests/dom/security/test/sri/script_crossdomain2.js"
+ crossorigin="anonymous"
+ integrity="sha256-ntgU2U1xv7HfK1XWMTSWz6vJkyVtGzMrIAxQkux1I94="
+ onload="bad_onloadCalled()"
+ onerror="good_onerrorCalled()"></script>
+
+<!-- non-cors but not actually using SRI. should trigger onload -->
+<script src="http://example.com/tests/dom/security/test/sri/script_crossdomain3.js"
+ integrity=" "
+ onload="good_nonsriLoaded()"
+ onerror="bad_nonsriBlocked()"></script>
+
+<!-- non-cors with invalid metadata -->
+<script src="http://example.com/tests/dom/security/test/sri/script_crossdomain4.js"
+ integrity="sha256-bogus"
+ onload="bad_nonCORSInvalidLoaded()"
+ onerror="good_nonCORSInvalidBlocked()"></script>
+
+<!-- non-cors that's same-origin initially but redirected to another origin -->
+<script src="script_301.js"
+ integrity="sha384-invalid"
+ onerror="good_incorrect301Blocked()"
+ onload="bad_incorrect301Loaded()"></script>
+
+<!-- non-cors that's same-origin initially but redirected to another origin -->
+<script src="script_301.js"
+ integrity="sha384-1NpiDI6decClMaTWSCAfUjTdx1BiOffsCPgH4lW5hCLwmHk0VyV/g6B9Sw2kD2K3"
+ onerror="good_correct301Blocked()"
+ onload="bad_correct301Loaded()"></script>
+
+<!-- data: URLs are same-origin -->
+<script src="data:,console.log('data:valid');"
+ integrity="sha256-W5I4VIN+mCwOfR9kDbvWoY1UOVRXIh4mKRN0Nz0ookg="
+ onerror="bad_correctDataBlocked()"
+ onload="good_correctDataLoaded()"></script>
+
+<!-- not cors-enabled with data: URLs. should trigger onload -->
+<script src="data:,console.log('data:valid');"
+ crossorigin="anonymous"
+ integrity="sha256-W5I4VIN+mCwOfR9kDbvWoY1UOVRXIh4mKRN0Nz0ookg="
+ onerror="bad_correctDataCORSBlocked()"
+ onload="good_correctDataCORSLoaded()"></script>
+
+<script>
+ ok(window.hasCORSLoaded, "CORS-enabled resource with a correct hash");
+ ok(!window.hasNonCORSLoaded, "Correct hash, but non-CORS, should be blocked");
+ ok(!window.onloadCalled, "Failed loads should not call onload when they're cross-domain");
+ ok(window.onerrorCalled, "Failed loads should call onerror when they're cross-domain");
+</script>
+</body>
+</html>
diff --git a/dom/security/test/sri/iframe_script_sameorigin.html b/dom/security/test/sri/iframe_script_sameorigin.html
new file mode 100644
index 0000000000..8c1994fec4
--- /dev/null
+++ b/dom/security/test/sri/iframe_script_sameorigin.html
@@ -0,0 +1,249 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ SimpleTest.finish();
+ }
+ </script>
+ <script>
+ function good_correctHashLoaded() {
+ ok(true, "A script was correctly loaded when integrity matched")
+ }
+ function bad_correctHashBlocked() {
+ ok(false, "We should load scripts with hashes that match!");
+ }
+
+ function good_correctHashArrayLoaded() {
+ ok(true, "A script was correctly loaded when one of the hashes in the integrity attribute matched")
+ }
+ function bad_correctHashArrayBlocked() {
+ ok(false, "We should load scripts with at least one hash that match!");
+ }
+
+ function good_emptyIntegrityLoaded() {
+ ok(true, "A script was correctly loaded when the integrity attribute was empty")
+ }
+ function bad_emptyIntegrityBlocked() {
+ ok(false, "We should load scripts with empty integrity attributes!");
+ }
+
+ function good_whitespaceIntegrityLoaded() {
+ ok(true, "A script was correctly loaded when the integrity attribute only contained whitespace")
+ }
+ function bad_whitespaceIntegrityBlocked() {
+ ok(false, "We should load scripts with integrity attributes containing only whitespace!");
+ }
+
+ function good_incorrectHashBlocked() {
+ ok(true, "A script was correctly blocked, because the hash digest was wrong");
+ }
+ function bad_incorrectHashLoaded() {
+ ok(false, "We should not load scripts with hashes that do not match the content!");
+ }
+
+ function good_incorrectHashArrayBlocked() {
+ ok(true, "A script was correctly blocked, because all the hashes were wrong");
+ }
+ function bad_incorrectHashArrayLoaded() {
+ ok(false, "We should not load scripts when none of the hashes match the content!");
+ }
+
+ function good_incorrectHashLengthBlocked() {
+ ok(true, "A script was correctly blocked, because the hash length was wrong");
+ }
+ function bad_incorrectHashLengthLoaded() {
+ ok(false, "We should not load scripts with hashes that don't have the right length!");
+ }
+
+ function bad_incorrectHashFunctionBlocked() {
+ ok(false, "We should load scripts with invalid/unsupported hash functions!");
+ }
+ function good_incorrectHashFunctionLoaded() {
+ ok(true, "A script was correctly loaded, despite the hash function being invalid/unsupported.");
+ }
+
+ function bad_missingHashFunctionBlocked() {
+ ok(false, "We should load scripts with missing hash functions!");
+ }
+ function good_missingHashFunctionLoaded() {
+ ok(true, "A script was correctly loaded, despite a missing hash function.");
+ }
+
+ function bad_missingHashValueBlocked() {
+ ok(false, "We should load scripts with missing hash digests!");
+ }
+ function good_missingHashValueLoaded() {
+ ok(true, "A script was correctly loaded, despite the missing hash digest.");
+ }
+
+ function good_401Blocked() {
+ ok(true, "A script was not loaded because of 401 response.");
+ }
+ function bad_401Loaded() {
+ ok(false, "We should nt load scripts with a 401 response!");
+ }
+
+ function good_valid302Loaded() {
+ ok(true, "A script was loaded successfully despite a 302 response.");
+ }
+ function bad_valid302Blocked() {
+ ok(false, "We should load scripts with a 302 response and the right hash!");
+ }
+
+ function good_invalid302Blocked() {
+ ok(true, "A script was blocked successfully after a 302 response.");
+ }
+ function bad_invalid302Loaded() {
+ ok(false, "We should not load scripts with a 302 response and the wrong hash!");
+ }
+
+ function good_validBlobLoaded() {
+ ok(true, "A script was loaded successfully from a blob: URL.");
+ }
+ function bad_validBlobBlocked() {
+ ok(false, "We should load scripts using blob: URLs with the right hash!");
+ }
+
+ function good_invalidBlobBlocked() {
+ ok(true, "A script was blocked successfully from a blob: URL.");
+ }
+ function bad_invalidBlobLoaded() {
+ ok(false, "We should not load scripts using blob: URLs with the wrong hash!");
+ }
+</script>
+</head>
+<body>
+ <!-- valid hash. should trigger onload -->
+ <!-- the hash value comes from running this command:
+ cat script.js | openssl dgst -sha256 -binary | openssl enc -base64 -A
+ -->
+ <script src="script.js"
+ integrity="sha256-RkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA="
+ onerror="bad_correctHashBlocked()"
+ onload="good_correctHashLoaded()"></script>
+
+ <!-- valid sha512 hash. should trigger onload -->
+ <script src="script.js"
+ integrity="sha512-mzSqH+vC6qrXX46JX2WEZ0FtY/lGj/5+5yYCBlk0jfYHLm0vP6XgsURbq83mwMApsnwbDLXdgjp5J8E93GT6Mw==?ignore=this"
+ onerror="bad_correctHashBlocked()"
+ onload="good_correctHashLoaded()"></script>
+
+ <!-- one valid sha256 hash. should trigger onload -->
+ <script src="script.js"
+ integrity="sha256-rkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA= sha256-RkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA= sha256-rkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA="
+ onerror="bad_correctHashArrayBlocked()"
+ onload="good_correctHashArrayLoaded()"></script>
+
+ <!-- empty integrity. should trigger onload -->
+ <script src="script.js"
+ integrity=""
+ onerror="bad_emptyIntegrityBlocked()"
+ onload="good_emptyIntegrityLoaded()"></script>
+
+ <!-- whitespace integrity. should trigger onload -->
+ <script src="script.js"
+ integrity="
+
+"
+ onerror="bad_whitespaceIntegrityBlocked()"
+ onload="good_whitespaceIntegrityLoaded()"></script>
+
+ <!-- invalid sha256 hash but valid sha384 hash. should trigger onload -->
+ <script src="script.js"
+ integrity="sha256-bogus sha384-zDCkvKOHXk8mM6Nk07oOGXGME17PA4+ydFw+hq0r9kgF6ZDYFWK3fLGPEy7FoOAo?"
+ onerror="bad_correctHashBlocked()"
+ onload="good_correctHashLoaded()"></script>
+
+ <!-- valid sha256 and invalid sha384. should trigger onerror -->
+ <script src="script.js"
+ integrity="sha256-RkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA= sha384-RkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA="
+ onerror="good_incorrectHashLengthBlocked()"
+ onload="bad_incorrectHashLengthLoaded()"></script>
+
+ <!-- invalid hash. should trigger onerror -->
+ <script src="script.js"
+ integrity="sha256-rkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA="
+ onerror="good_incorrectHashBlocked()"
+ onload="bad_incorrectHashLoaded()"></script>
+
+ <!-- invalid hashes. should trigger onerror -->
+ <script src="script.js"
+ integrity="sha256-rkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA= sha256-ZkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA= sha256-zkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA="
+ onerror="good_incorrectHashBlocked()"
+ onload="bad_incorrectHashLoaded()"></script>
+
+ <!-- invalid hash function. should trigger onload -->
+ <script src="script.js"
+ integrity="rot13-RkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA="
+ onerror="bad_incorrectHashFunctionBlocked()"
+ onload="good_incorrectHashFunctionLoaded()"></script>
+
+ <!-- missing hash function. should trigger onload -->
+ <script src="script.js"
+ integrity="RkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA="
+ onerror="bad_missingHashFunctionBlocked()"
+ onload="good_missingHashFunctionLoaded()"></script>
+
+ <!-- missing hash value. should trigger onload -->
+ <script src="script.js"
+ integrity="sha512-"
+ onerror="bad_missingHashValueBlocked()"
+ onload="good_missingHashValueLoaded()"></script>
+
+ <!-- 401 response. should trigger onerror -->
+ <script src="script_401.js"
+ integrity="sha256-RkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA="
+ onerror="good_401Blocked()"
+ onload="bad_401Loaded()"></script>
+
+ <!-- valid sha256 after a redirection. should trigger onload -->
+ <script src="script_302.js"
+ integrity="sha256-RkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA="
+ onerror="bad_valid302Blocked()"
+ onload="good_valid302Loaded()"></script>
+
+ <!-- invalid sha256 after a redirection. should trigger onerror -->
+ <script src="script_302.js"
+ integrity="sha256-JSi74NSN8WQNr9syBGmNg2APJp9PnHUO5ioZo5hmIiQ="
+ onerror="good_invalid302Blocked()"
+ onload="bad_invalid302Loaded()"></script>
+
+ <!-- valid sha256 for a blob: URL -->
+ <script>
+ var blob = new Blob(["console.log('blob:valid');"],
+ {type:"application/javascript"});
+ var script = document.createElement('script');
+ script.setAttribute('src', URL.createObjectURL(blob));
+ script.setAttribute('integrity', 'sha256-AwLdXiGfCqOxOXDPUim73G8NVEL34jT0IcQR/tqv/GQ=');
+ script.onerror = bad_validBlobBlocked;
+ script.onload = good_validBlobLoaded;
+ var head = document.getElementsByTagName('head').item(0);
+ head.appendChild(script);
+ </script>
+
+ <!-- invalid sha256 for a blob: URL -->
+ <script>
+ var blob = new Blob(["console.log('blob:invalid');"],
+ {type:"application/javascript"});
+ var script = document.createElement('script');
+ script.setAttribute('src', URL.createObjectURL(blob));
+ script.setAttribute('integrity', 'sha256-AwLdXiGfCqOxOXDPUim73G8NVEL34jT0IcQR/tqv/GQ=');
+ script.onerror = good_invalidBlobBlocked;
+ script.onload = bad_invalidBlobLoaded;
+ var head = document.getElementsByTagName('head').item(0);
+ head.appendChild(script);
+ </script>
+
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/sri/iframe_style_crossdomain.html b/dom/security/test/sri/iframe_style_crossdomain.html
new file mode 100644
index 0000000000..f5eb57cbe7
--- /dev/null
+++ b/dom/security/test/sri/iframe_style_crossdomain.html
@@ -0,0 +1,117 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+ function check_styles() {
+ var redText = document.getElementById('red-text');
+ var greenText = document.getElementById('green-text');
+ var blueText = document.getElementById('blue-text');
+ var redTextColor = window.getComputedStyle(redText).getPropertyValue('color');
+ var greenTextColor = window.getComputedStyle(greenText).getPropertyValue('color');
+ var blueTextColor = window.getComputedStyle(blueText).getPropertyValue('color');
+ ok(redTextColor == 'rgb(255, 0, 0)', "The first part should be red.");
+ ok(greenTextColor == 'rgb(0, 255, 0)', "The second part should be green.");
+ ok(blueTextColor == 'rgb(0, 0, 255)', "The third part should be blue.");
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ check_styles();
+ SimpleTest.finish();
+ }
+ </script>
+ <script>
+ function good_correctHashCORSLoaded() {
+ ok(true, "A CORS cross-domain stylesheet with correct hash was correctly loaded.");
+ }
+ function bad_correctHashCORSBlocked() {
+ ok(false, "We should load CORS cross-domain stylesheets with hashes that match!");
+ }
+ function good_correctHashBlocked() {
+ ok(true, "A non-CORS cross-domain stylesheet with correct hash was correctly blocked.");
+ }
+ function bad_correctHashLoaded() {
+ ok(false, "We should block non-CORS cross-domain stylesheets with hashes that match!");
+ }
+
+ function good_incorrectHashBlocked() {
+ ok(true, "A non-CORS cross-domain stylesheet with incorrect hash was correctly blocked.");
+ }
+ function bad_incorrectHashLoaded() {
+ ok(false, "We should load non-CORS cross-domain stylesheets with incorrect hashes!");
+ }
+
+ function bad_correctDataBlocked() {
+ ok(false, "We should not block non-CORS cross-domain stylesheets in data: URI!");
+ }
+ function good_correctDataLoaded() {
+ ok(true, "A non-CORS cross-domain stylesheet with data: URI was correctly loaded.");
+ }
+ function bad_correctDataCORSBlocked() {
+ ok(false, "We should not block CORS stylesheets in data: URI!");
+ }
+ function good_correctDataCORSLoaded() {
+ ok(true, "A CORS stylesheet with data: URI was correctly loaded.");
+ }
+
+ function good_correctHashOpaqueBlocked() {
+ ok(true, "A non-CORS(Opaque) cross-domain stylesheet with correct hash was correctly blocked.");
+ }
+ function bad_correctHashOpaqueLoaded() {
+ ok(false, "We should not load non-CORS(Opaque) cross-domain stylesheets with correct hashes!");
+ }
+ </script>
+
+ <!-- valid CORS sha256 hash -->
+ <link rel="stylesheet" href="http://example.com/tests/dom/security/test/sri/style1.css"
+ crossorigin="anonymous"
+ integrity="sha256-qs8lnkunWoVldk5d5E+652yth4VTSHohlBKQvvgGwa8="
+ onerror="bad_correctHashCORSBlocked()"
+ onload="good_correctHashCORSLoaded()">
+
+ <!-- valid non-CORS sha256 hash -->
+ <link rel="stylesheet" href="style_301.css"
+ integrity="sha256-qs8lnkunWoVldk5d5E+652yth4VTSHohlBKQvvgGwa8="
+ onerror="good_correctHashBlocked()"
+ onload="bad_correctHashLoaded()">
+
+ <!-- invalid non-CORS sha256 hash -->
+ <link rel="stylesheet" href="style_301.css?again"
+ integrity="sha256-bogus"
+ onerror="good_incorrectHashBlocked()"
+ onload="bad_incorrectHashLoaded()">
+
+ <!-- valid non-CORS sha256 hash in a data: URL -->
+ <link rel="stylesheet" href="data:text/css,.green-text{color:rgb(0, 255, 0)}"
+ integrity="sha256-EhVtGGyovvffvYdhyqJxUJ/ekam7zlxxo46iM13cwP0="
+ onerror="bad_correctDataBlocked()"
+ onload="good_correctDataLoaded()">
+
+ <!-- valid CORS sha256 hash in a data: URL -->
+ <link rel="stylesheet" href="data:text/css,.blue-text{color:rgb(0, 0, 255)}"
+ crossorigin="anonymous"
+ integrity="sha256-m0Fs2hNSyPOn1030Dp+c8pJFHNmwpeTbB+8J/DcqLss="
+ onerror="bad_correctDataCORSBlocked()"
+ onload="good_correctDataCORSLoaded()">
+
+ <!-- valid non-CORS sha256 hash -->
+ <link rel="stylesheet" href="http://example.com/tests/dom/security/test/sri/style1.css"
+ integrity="sha256-qs8lnkunWoVldk5d5E+652yth4VTSHohlBKQvvgGwa8="
+ onerror="good_correctHashOpaqueBlocked()"
+ onload="bad_correctHashOpaqueLoaded()">
+</head>
+<body>
+<p><span id="red-text">This should be red</span> but
+ <span id="green-text" class="green-text">this should be green</span> and
+ <span id="blue-text" class="blue-text">this should be blue</span></p>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/sri/iframe_style_sameorigin.html b/dom/security/test/sri/iframe_style_sameorigin.html
new file mode 100644
index 0000000000..52ebd10d9b
--- /dev/null
+++ b/dom/security/test/sri/iframe_style_sameorigin.html
@@ -0,0 +1,164 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+ function check_styles() {
+ var redText = document.getElementById('red-text');
+ var blueText = document.getElementById('blue-text-element');
+ var blackText1 = document.getElementById('black-text');
+ var blackText2 = document.getElementById('black-text-2');
+ var redTextColor = window.getComputedStyle(redText).getPropertyValue('color');
+ var blueTextColor = window.getComputedStyle(blueText).getPropertyValue('color');
+ var blackTextColor1 = window.getComputedStyle(blackText1).getPropertyValue('color');
+ var blackTextColor2 = window.getComputedStyle(blackText2).getPropertyValue('color');
+ ok(redTextColor == 'rgb(255, 0, 0)', "The first part should be red.");
+ ok(blueTextColor == 'rgb(0, 0, 255)', "The second part should be blue.");
+ ok(blackTextColor1 == 'rgb(0, 0, 0)', "The second last part should still be black.");
+ ok(blackTextColor2 == 'rgb(0, 0, 0)', "The last part should still be black.");
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ check_styles();
+ SimpleTest.finish();
+ }
+ </script>
+ <script>
+ function good_correctHashLoaded() {
+ ok(true, "A stylesheet was correctly loaded when integrity matched");
+ }
+ function bad_correctHashBlocked() {
+ ok(false, "We should load stylesheets with hashes that match!");
+ }
+
+ function good_emptyIntegrityLoaded() {
+ ok(true, "A stylesheet was correctly loaded when the integrity attribute was empty");
+ }
+ function bad_emptyIntegrityBlocked() {
+ ok(false, "We should load stylesheets with empty integrity attributes!");
+ }
+
+ function good_incorrectHashBlocked() {
+ ok(true, "A stylesheet was correctly blocked, because the hash digest was wrong");
+ }
+ function bad_incorrectHashLoaded() {
+ ok(false, "We should not load stylesheets with hashes that do not match the content!");
+ }
+
+ function good_validBlobLoaded() {
+ ok(true, "A stylesheet was loaded successfully from a blob: URL with the right hash.");
+ }
+ function bad_validBlobBlocked() {
+ ok(false, "We should load stylesheets using blob: URLs with the right hash!");
+ }
+ function good_invalidBlobBlocked() {
+ ok(true, "A stylesheet was blocked successfully from a blob: URL with an invalid hash.");
+ }
+ function bad_invalidBlobLoaded() {
+ ok(false, "We should not load stylesheets using blob: URLs when they have the wrong hash!");
+ }
+
+ function good_correctUTF8HashLoaded() {
+ ok(true, "A UTF8 stylesheet was correctly loaded when integrity matched");
+ }
+ function bad_correctUTF8HashBlocked() {
+ ok(false, "We should load UTF8 stylesheets with hashes that match!");
+ }
+ function good_correctUTF8BOMHashLoaded() {
+ ok(true, "A UTF8 stylesheet (with BOM) was correctly loaded when integrity matched");
+ }
+ function bad_correctUTF8BOMHashBlocked() {
+ ok(false, "We should load UTF8 (with BOM) stylesheets with hashes that match!");
+ }
+ function good_correctUTF8ishHashLoaded() {
+ ok(true, "A UTF8ish stylesheet was correctly loaded when integrity matched");
+ }
+ function bad_correctUTF8ishHashBlocked() {
+ ok(false, "We should load UTF8ish stylesheets with hashes that match!");
+ }
+ </script>
+
+ <!-- valid sha256 hash. should trigger onload -->
+ <link rel="stylesheet" href="style1.css"
+ integrity="sha256-qs8lnkunWoVldk5d5E+652yth4VTSHohlBKQvvgGwa8="
+ onerror="bad_correctHashBlocked()"
+ onload="good_correctHashLoaded()">
+
+ <!-- empty metadata. should trigger onload -->
+ <link rel="stylesheet" href="style2.css"
+ integrity=""
+ onerror="bad_emptyIntegrityBlocked()"
+ onload="good_emptyIntegrityLoaded()">
+
+ <!-- invalid sha256 hash. should trigger onerror -->
+ <link rel="stylesheet" href="style3.css"
+ integrity="sha256-bogus"
+ onerror="good_incorrectHashBlocked()"
+ onload="bad_incorrectHashLoaded()">
+
+ <!-- valid sha384 hash of a utf8 file. should trigger onload -->
+ <link rel="stylesheet" href="style4.css"
+ integrity="sha384-13rt+j7xMDLhohLukb7AZx8lDGS3hkahp0IoeuyvxSNVPyc1QQmTDcwXGhQZjoMH"
+ onerror="bad_correctUTF8HashBlocked()"
+ onload="good_correctUTF8HashLoaded()">
+
+ <!-- valid sha384 hash of a utf8 file with a BOM. should trigger onload -->
+ <link rel="stylesheet" href="style5.css"
+ integrity="sha384-udAqVKPIHf/OD1isAYKrgzsog/3Q6lSEL2nKhtLSTmHryiae0+y6x1akeTzEF446"
+ onerror="bad_correctUTF8BOMHashBlocked()"
+ onload="good_correctUTF8BOMHashLoaded()">
+
+ <!-- valid sha384 hash of a utf8 file with the wrong charset. should trigger onload -->
+ <link rel="stylesheet" href="style6.css"
+ integrity="sha384-Xli4ROFoVGCiRgXyl7y8jv5Vm2yuqj+8tkNL3cUI7AHaCocna75JLs5xID437W6C"
+ onerror="bad_correctUTF8ishHashBlocked()"
+ onload="good_correctUTF8ishHashLoaded()">
+</head>
+<body>
+
+<!-- valid sha256 for a blob: URL -->
+<script>
+ var blob = new Blob(['.blue-text{color:blue}'],
+ {type: 'text/css'});
+ var link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.href = window.URL.createObjectURL(blob);
+ link.setAttribute('integrity', 'sha256-/F+EMVnTWYJOAzN5n7/21idiydu6nRi33LZOISZtwOM=');
+ link.onerror = bad_validBlobBlocked;
+ link.onload = good_validBlobLoaded;
+ document.body.appendChild(link);
+</script>
+
+<!-- invalid sha256 for a blob: URL -->
+<script>
+ var blob = new Blob(['.black-text{color:blue}'],
+ {type: 'text/css'});
+ var link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.href = window.URL.createObjectURL(blob);
+ link.setAttribute('integrity', 'sha256-/F+EMVnTWYJOAzN5n7/21idiydu6nRi33LZOISZtwOM=');
+ link.onerror = good_invalidBlobBlocked;
+ link.onload = bad_invalidBlobLoaded;
+ document.body.appendChild(link);
+</script>
+
+<p><span id="red-text">This should be red </span>,
+ <span id="purple-text">this should be purple</span>,
+ <span id="brown-text">this should be brown</span>,
+ <span id="orange-text">this should be orange</span>, and
+ <span class="blue-text" id="blue-text-element">this should be blue.</span>
+ However, <span id="black-text">this should stay black</span> and
+ <span class="black-text" id="black-text-2">this should also stay black.</span>
+</p>
+
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/security/test/sri/mochitest.toml b/dom/security/test/sri/mochitest.toml
new file mode 100644
index 0000000000..e40ffa6cbc
--- /dev/null
+++ b/dom/security/test/sri/mochitest.toml
@@ -0,0 +1,56 @@
+[DEFAULT]
+support-files = [
+ "file_bug_1271796.css",
+ "iframe_script_crossdomain.html",
+ "iframe_script_sameorigin.html",
+ "iframe_style_crossdomain.html",
+ "iframe_style_sameorigin.html",
+ "script_crossdomain1.js",
+ "script_crossdomain1.js^headers^",
+ "script_crossdomain2.js",
+ "script_crossdomain3.js",
+ "script_crossdomain3.js^headers^",
+ "script_crossdomain4.js",
+ "script_crossdomain4.js^headers^",
+ "script_crossdomain5.js",
+ "script_crossdomain5.js^headers^",
+ "script.js",
+ "script.js^headers^",
+ "script_301.js",
+ "script_301.js^headers^",
+ "script_302.js",
+ "script_302.js^headers^",
+ "script_401.js",
+ "script_401.js^headers^",
+ "style1.css",
+ "style1.css^headers^",
+ "style2.css",
+ "style3.css",
+ "style4.css",
+ "style4.css^headers^",
+ "style5.css",
+ "style6.css",
+ "style6.css^headers^",
+ "style_301.css",
+ "style_301.css^headers^",
+]
+
+["test_bug_1271796.html"]
+
+["test_bug_1364262.html"]
+
+["test_script_crossdomain.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_script_sameorigin.html"]
+
+["test_style_crossdomain.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_style_sameorigin.html"]
diff --git a/dom/security/test/sri/script.js b/dom/security/test/sri/script.js
new file mode 100644
index 0000000000..8fd8f96b2f
--- /dev/null
+++ b/dom/security/test/sri/script.js
@@ -0,0 +1 @@
+var load=true;
diff --git a/dom/security/test/sri/script.js^headers^ b/dom/security/test/sri/script.js^headers^
new file mode 100644
index 0000000000..b77232d81d
--- /dev/null
+++ b/dom/security/test/sri/script.js^headers^
@@ -0,0 +1 @@
+Cache-control: public
diff --git a/dom/security/test/sri/script_301.js b/dom/security/test/sri/script_301.js
new file mode 100644
index 0000000000..9a95de77cf
--- /dev/null
+++ b/dom/security/test/sri/script_301.js
@@ -0,0 +1 @@
+var load=false;
diff --git a/dom/security/test/sri/script_301.js^headers^ b/dom/security/test/sri/script_301.js^headers^
new file mode 100644
index 0000000000..efbfb73346
--- /dev/null
+++ b/dom/security/test/sri/script_301.js^headers^
@@ -0,0 +1,2 @@
+HTTP 301 Moved Permanently
+Location: http://example.com/tests/dom/security/test/sri/script_crossdomain5.js
diff --git a/dom/security/test/sri/script_302.js b/dom/security/test/sri/script_302.js
new file mode 100644
index 0000000000..9a95de77cf
--- /dev/null
+++ b/dom/security/test/sri/script_302.js
@@ -0,0 +1 @@
+var load=false;
diff --git a/dom/security/test/sri/script_302.js^headers^ b/dom/security/test/sri/script_302.js^headers^
new file mode 100644
index 0000000000..05a545a6a1
--- /dev/null
+++ b/dom/security/test/sri/script_302.js^headers^
@@ -0,0 +1,2 @@
+HTTP 302 Found
+Location: /tests/dom/security/test/sri/script.js
diff --git a/dom/security/test/sri/script_401.js b/dom/security/test/sri/script_401.js
new file mode 100644
index 0000000000..8fd8f96b2f
--- /dev/null
+++ b/dom/security/test/sri/script_401.js
@@ -0,0 +1 @@
+var load=true;
diff --git a/dom/security/test/sri/script_401.js^headers^ b/dom/security/test/sri/script_401.js^headers^
new file mode 100644
index 0000000000..889fbe081a
--- /dev/null
+++ b/dom/security/test/sri/script_401.js^headers^
@@ -0,0 +1,2 @@
+HTTP 401 Authorization Required
+Cache-control: public
diff --git a/dom/security/test/sri/script_crossdomain1.js b/dom/security/test/sri/script_crossdomain1.js
new file mode 100644
index 0000000000..1f17a6db24
--- /dev/null
+++ b/dom/security/test/sri/script_crossdomain1.js
@@ -0,0 +1,4 @@
+/*
+ * this file should be loaded, because it has CORS enabled.
+*/
+window.hasCORSLoaded = true;
diff --git a/dom/security/test/sri/script_crossdomain1.js^headers^ b/dom/security/test/sri/script_crossdomain1.js^headers^
new file mode 100644
index 0000000000..3a6a85d894
--- /dev/null
+++ b/dom/security/test/sri/script_crossdomain1.js^headers^
@@ -0,0 +1 @@
+Access-Control-Allow-Origin: http://mochi.test:8888
diff --git a/dom/security/test/sri/script_crossdomain2.js b/dom/security/test/sri/script_crossdomain2.js
new file mode 100644
index 0000000000..4b0208ab34
--- /dev/null
+++ b/dom/security/test/sri/script_crossdomain2.js
@@ -0,0 +1,5 @@
+/*
+ * this file should not be loaded, because it does not have CORS
+ * enabled.
+ */
+window.hasNonCORSLoaded = true;
diff --git a/dom/security/test/sri/script_crossdomain3.js b/dom/security/test/sri/script_crossdomain3.js
new file mode 100644
index 0000000000..eed05d59b7
--- /dev/null
+++ b/dom/security/test/sri/script_crossdomain3.js
@@ -0,0 +1 @@
+// This script intentionally left blank
diff --git a/dom/security/test/sri/script_crossdomain3.js^headers^ b/dom/security/test/sri/script_crossdomain3.js^headers^
new file mode 100644
index 0000000000..3a6a85d894
--- /dev/null
+++ b/dom/security/test/sri/script_crossdomain3.js^headers^
@@ -0,0 +1 @@
+Access-Control-Allow-Origin: http://mochi.test:8888
diff --git a/dom/security/test/sri/script_crossdomain4.js b/dom/security/test/sri/script_crossdomain4.js
new file mode 100644
index 0000000000..eed05d59b7
--- /dev/null
+++ b/dom/security/test/sri/script_crossdomain4.js
@@ -0,0 +1 @@
+// This script intentionally left blank
diff --git a/dom/security/test/sri/script_crossdomain4.js^headers^ b/dom/security/test/sri/script_crossdomain4.js^headers^
new file mode 100644
index 0000000000..3a6a85d894
--- /dev/null
+++ b/dom/security/test/sri/script_crossdomain4.js^headers^
@@ -0,0 +1 @@
+Access-Control-Allow-Origin: http://mochi.test:8888
diff --git a/dom/security/test/sri/script_crossdomain5.js b/dom/security/test/sri/script_crossdomain5.js
new file mode 100644
index 0000000000..eed05d59b7
--- /dev/null
+++ b/dom/security/test/sri/script_crossdomain5.js
@@ -0,0 +1 @@
+// This script intentionally left blank
diff --git a/dom/security/test/sri/script_crossdomain5.js^headers^ b/dom/security/test/sri/script_crossdomain5.js^headers^
new file mode 100644
index 0000000000..cb762eff80
--- /dev/null
+++ b/dom/security/test/sri/script_crossdomain5.js^headers^
@@ -0,0 +1 @@
+Access-Control-Allow-Origin: *
diff --git a/dom/security/test/sri/style1.css b/dom/security/test/sri/style1.css
new file mode 100644
index 0000000000..c7ab9ecffa
--- /dev/null
+++ b/dom/security/test/sri/style1.css
@@ -0,0 +1,3 @@
+#red-text {
+ color: red;
+}
diff --git a/dom/security/test/sri/style1.css^headers^ b/dom/security/test/sri/style1.css^headers^
new file mode 100644
index 0000000000..3a6a85d894
--- /dev/null
+++ b/dom/security/test/sri/style1.css^headers^
@@ -0,0 +1 @@
+Access-Control-Allow-Origin: http://mochi.test:8888
diff --git a/dom/security/test/sri/style2.css b/dom/security/test/sri/style2.css
new file mode 100644
index 0000000000..9eece75e5b
--- /dev/null
+++ b/dom/security/test/sri/style2.css
@@ -0,0 +1 @@
+; A valid but somewhat uninteresting stylesheet
diff --git a/dom/security/test/sri/style3.css b/dom/security/test/sri/style3.css
new file mode 100644
index 0000000000..b64fa3b749
--- /dev/null
+++ b/dom/security/test/sri/style3.css
@@ -0,0 +1,3 @@
+#black-text {
+ color: green;
+}
diff --git a/dom/security/test/sri/style4.css b/dom/security/test/sri/style4.css
new file mode 100644
index 0000000000..eab83656ed
--- /dev/null
+++ b/dom/security/test/sri/style4.css
@@ -0,0 +1,4 @@
+/* François was here. */
+#purple-text {
+ color: purple;
+}
diff --git a/dom/security/test/sri/style4.css^headers^ b/dom/security/test/sri/style4.css^headers^
new file mode 100644
index 0000000000..e13897f157
--- /dev/null
+++ b/dom/security/test/sri/style4.css^headers^
@@ -0,0 +1 @@
+Content-Type: text/css; charset=utf-8
diff --git a/dom/security/test/sri/style5.css b/dom/security/test/sri/style5.css
new file mode 100644
index 0000000000..5d59134cc6
--- /dev/null
+++ b/dom/security/test/sri/style5.css
@@ -0,0 +1,4 @@
+/* François was here. */
+#orange-text {
+ color: orange;
+}
diff --git a/dom/security/test/sri/style6.css b/dom/security/test/sri/style6.css
new file mode 100644
index 0000000000..569557694d
--- /dev/null
+++ b/dom/security/test/sri/style6.css
@@ -0,0 +1,4 @@
+/* François was here. */
+#brown-text {
+ color: brown;
+}
diff --git a/dom/security/test/sri/style6.css^headers^ b/dom/security/test/sri/style6.css^headers^
new file mode 100644
index 0000000000..d866aa5224
--- /dev/null
+++ b/dom/security/test/sri/style6.css^headers^
@@ -0,0 +1 @@
+Content-Type: text/css; charset=iso-8859-8
diff --git a/dom/security/test/sri/style_301.css b/dom/security/test/sri/style_301.css
new file mode 100644
index 0000000000..c7ab9ecffa
--- /dev/null
+++ b/dom/security/test/sri/style_301.css
@@ -0,0 +1,3 @@
+#red-text {
+ color: red;
+}
diff --git a/dom/security/test/sri/style_301.css^headers^ b/dom/security/test/sri/style_301.css^headers^
new file mode 100644
index 0000000000..c5b78ee04b
--- /dev/null
+++ b/dom/security/test/sri/style_301.css^headers^
@@ -0,0 +1,2 @@
+HTTP 301 Moved Permanently
+Location: http://example.com/tests/dom/security/test/sri/style1.css
diff --git a/dom/security/test/sri/test_bug_1271796.html b/dom/security/test/sri/test_bug_1271796.html
new file mode 100644
index 0000000000..9c74cc64ea
--- /dev/null
+++ b/dom/security/test/sri/test_bug_1271796.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ function good_shouldLoadEncodingProblem() {
+ ok(true, "Problematically encoded file correctly loaded.")
+ };
+ function bad_shouldntEncounterBug1271796() {
+ ok(false, "Problematically encoded should load!")
+ }
+ window.onload = function() {
+ SimpleTest.finish();
+ }
+ </script>
+ <link rel="stylesheet" href="file_bug_1271796.css" crossorigin="anonymous"
+ integrity="sha384-8Xl0mTN4S2QZ5xeliG1sd4Ar9o1xMw6JoJy9RNjyHGQDha7GiLxo8l1llwLVgTNG"
+ onload="good_shouldLoadEncodingProblem();"
+ onerror="bad_shouldntEncounterBug1271796();">
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1271796">Bug 1271796</a><br>
+<p>This text is prepended by emdash if css has loaded</p>
+</body>
+</html>
diff --git a/dom/security/test/sri/test_bug_1364262.html b/dom/security/test/sri/test_bug_1364262.html
new file mode 100644
index 0000000000..cf77c7dac1
--- /dev/null
+++ b/dom/security/test/sri/test_bug_1364262.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.setExpected(["pass", 1]);
+
+ function good_correctlyBlockedStylesheet() {
+ ok(true, "Non-base64 hash blocked the load.")
+ };
+ function bad_shouldNotLoadStylesheet() {
+ ok(false, "Non-base64 hashes should not load!")
+ }
+ window.onload = function() {
+ SimpleTest.finish();
+ }
+
+ let link = document.createElement('link');
+ document.head.appendChild(link);
+ link.setAttribute('rel', 'stylesheet');
+ link.onerror = good_correctlyBlockedStylesheet;
+ link.onload = bad_shouldNotLoadStylesheet;
+ link.integrity = 'sha512-\uD89D\uDF05\uD89D\uDEE6';
+ link.setAttribute('href', 'data:text/css;small[contenteditable^="false"], summary { }');
+ </script>
+</head>
+<body>
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1364262">Bug 1364262</a>
+</body>
+</html>
diff --git a/dom/security/test/sri/test_script_crossdomain.html b/dom/security/test/sri/test_script_crossdomain.html
new file mode 100644
index 0000000000..2f9b27bfa4
--- /dev/null
+++ b/dom/security/test/sri/test_script_crossdomain.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Cross-domain script tests for Bug 992096</title>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=992096">Mozilla Bug 992096</a>
+<div>
+ <iframe src="iframe_script_crossdomain.html" height="100%" width="90%" frameborder="0"></iframe>
+</div>
+</body>
+</html>
diff --git a/dom/security/test/sri/test_script_sameorigin.html b/dom/security/test/sri/test_script_sameorigin.html
new file mode 100644
index 0000000000..d975132a2e
--- /dev/null
+++ b/dom/security/test/sri/test_script_sameorigin.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Same-origin script tests for Bug 992096</title>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=992096">Mozilla Bug 992096</a>
+<div>
+ <iframe src="iframe_script_sameorigin.html" height="100%" width="90%" frameborder="0"></iframe>
+</div>
+</body>
+</html>
diff --git a/dom/security/test/sri/test_style_crossdomain.html b/dom/security/test/sri/test_style_crossdomain.html
new file mode 100644
index 0000000000..eb4dac4cc4
--- /dev/null
+++ b/dom/security/test/sri/test_style_crossdomain.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Cross-domain stylesheet tests for Bug 1196740</title>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1196740">Mozilla Bug 1196740</a>
+<div>
+ <iframe src="iframe_style_crossdomain.html" height="100%" width="90%" frameborder="0"></iframe>
+</div>
+</body>
+</html>
diff --git a/dom/security/test/sri/test_style_sameorigin.html b/dom/security/test/sri/test_style_sameorigin.html
new file mode 100644
index 0000000000..9b85eaf71b
--- /dev/null
+++ b/dom/security/test/sri/test_style_sameorigin.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Same-origin stylesheet tests for Bug 992096</title>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=992096">Mozilla Bug 992096</a>
+<div>
+ <iframe src="iframe_style_sameorigin.html" height="100%" width="90%" frameborder="0"></iframe>
+</div>
+</body>
+</html>
diff --git a/dom/security/test/unit/test_csp_reports.js b/dom/security/test/unit/test_csp_reports.js
new file mode 100644
index 0000000000..36da1a13e5
--- /dev/null
+++ b/dom/security/test/unit/test_csp_reports.js
@@ -0,0 +1,299 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpServer = new HttpServer();
+httpServer.start(-1);
+var testsToFinish = 0;
+
+var principal;
+
+const REPORT_SERVER_PORT = httpServer.identity.primaryPort;
+const REPORT_SERVER_URI = "http://localhost";
+
+/**
+ * Construct a callback that listens to a report submission and either passes
+ * or fails a test based on what it gets.
+ */
+function makeReportHandler(testpath, message, expectedJSON) {
+ return function (request, response) {
+ // we only like "POST" submissions for reports!
+ if (request.method !== "POST") {
+ do_throw("violation report should be a POST request");
+ return;
+ }
+
+ // check content-type of report is "application/csp-report"
+ var contentType = request.hasHeader("Content-Type")
+ ? request.getHeader("Content-Type")
+ : undefined;
+ if (contentType !== "application/csp-report") {
+ do_throw(
+ "violation report should have the 'application/csp-report' " +
+ "content-type, when in fact it is " +
+ contentType.toString()
+ );
+ }
+
+ // obtain violation report
+ var reportObj = JSON.parse(
+ NetUtil.readInputStreamToString(
+ request.bodyInputStream,
+ request.bodyInputStream.available()
+ )
+ );
+
+ // dump("GOT REPORT:\n" + JSON.stringify(reportObj) + "\n");
+ // dump("TESTPATH: " + testpath + "\n");
+ // dump("EXPECTED: \n" + JSON.stringify(expectedJSON) + "\n\n");
+
+ for (var i in expectedJSON) {
+ Assert.equal(expectedJSON[i], reportObj["csp-report"][i]);
+ }
+
+ testsToFinish--;
+ httpServer.registerPathHandler(testpath, null);
+ if (testsToFinish < 1) {
+ httpServer.stop(do_test_finished);
+ } else {
+ do_test_finished();
+ }
+ };
+}
+
+/**
+ * Everything created by this assumes it will cause a report. If you want to
+ * add a test here that will *not* cause a report to go out, you're gonna have
+ * to make sure the test cleans up after itself.
+ */
+function makeTest(id, expectedJSON, useReportOnlyPolicy, callback) {
+ testsToFinish++;
+ do_test_pending();
+
+ // set up a new CSP instance for each test.
+ var csp = Cc["@mozilla.org/cspcontext;1"].createInstance(
+ Ci.nsIContentSecurityPolicy
+ );
+ var policy =
+ "default-src 'none' 'report-sample'; " +
+ "report-uri " +
+ REPORT_SERVER_URI +
+ ":" +
+ REPORT_SERVER_PORT +
+ "/test" +
+ id;
+ var selfuri = NetUtil.newURI(
+ REPORT_SERVER_URI + ":" + REPORT_SERVER_PORT + "/foo/self"
+ );
+
+ dump("Created test " + id + " : " + policy + "\n\n");
+
+ principal = Services.scriptSecurityManager.createContentPrincipal(
+ selfuri,
+ {}
+ );
+ csp.setRequestContextWithPrincipal(principal, selfuri, "", 0);
+
+ // Load up the policy
+ // set as report-only if that's the case
+ csp.appendPolicy(policy, useReportOnlyPolicy, false);
+
+ // prime the report server
+ var handler = makeReportHandler("/test" + id, "Test " + id, expectedJSON);
+ httpServer.registerPathHandler("/test" + id, handler);
+
+ // trigger the violation
+ callback(csp);
+}
+
+function run_test() {
+ do_get_profile();
+
+ var selfuri = NetUtil.newURI(
+ REPORT_SERVER_URI + ":" + REPORT_SERVER_PORT + "/foo/self"
+ );
+
+ // test that inline script violations cause a report.
+ makeTest(
+ 0,
+ {
+ "blocked-uri": "inline",
+ "effective-directive": "script-src-elem",
+ disposition: "enforce",
+ },
+ false,
+ function (csp) {
+ let inlineOK = true;
+ inlineOK = csp.getAllowsInline(
+ Ci.nsIContentSecurityPolicy.SCRIPT_SRC_ELEM_DIRECTIVE,
+ false, // aHasUnsafeHash
+ "", // aNonce
+ false, // aParserCreated
+ null, // aTriggeringElement
+ null, // nsICSPEventListener
+ "", // aContentOfPseudoScript
+ 0, // aLineNumber
+ 1 // aColumnNumber
+ );
+
+ // this is not a report only policy, so it better block inline scripts
+ Assert.ok(!inlineOK);
+ }
+ );
+
+ // test that eval violations cause a report.
+ makeTest(
+ 1,
+ {
+ "blocked-uri": "eval",
+ // JSON script-sample is UTF8 encoded
+ "script-sample": "\xc2\xa3\xc2\xa5\xc2\xb5\xe5\x8c\x97\xf0\xa0\x9d\xb9",
+ "line-number": 1,
+ "column-number": 2,
+ },
+ false,
+ function (csp) {
+ let evalOK = true,
+ oReportViolation = { value: false };
+ evalOK = csp.getAllowsEval(oReportViolation);
+
+ // this is not a report only policy, so it better block eval
+ Assert.ok(!evalOK);
+ // ... and cause reports to go out
+ Assert.ok(oReportViolation.value);
+
+ if (oReportViolation.value) {
+ // force the logging, since the getter doesn't.
+ csp.logViolationDetails(
+ Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_EVAL,
+ null, // aTriggeringElement
+ null, // nsICSPEventListener
+ selfuri.asciiSpec,
+ // sending UTF-16 script sample to make sure
+ // csp report in JSON is not cut-off, please
+ // note that JSON is UTF8 encoded.
+ "\u00a3\u00a5\u00b5\u5317\ud841\udf79",
+ 1, // line number
+ 2 // column number
+ );
+ }
+ }
+ );
+
+ makeTest(
+ 2,
+ { "blocked-uri": "http://blocked.test/foo.js" },
+ false,
+ function (csp) {
+ // shouldLoad creates and sends out the report here.
+ csp.shouldLoad(
+ Ci.nsIContentPolicy.TYPE_SCRIPT,
+ null, // nsICSPEventListener
+ null, // aLoadInfo
+ NetUtil.newURI("http://blocked.test/foo.js"),
+ null,
+ true
+ );
+ }
+ );
+
+ // test that inline script violations cause a report in report-only policy
+ makeTest(
+ 3,
+ { "blocked-uri": "inline", disposition: "report" },
+ true,
+ function (csp) {
+ let inlineOK = true;
+ inlineOK = csp.getAllowsInline(
+ Ci.nsIContentSecurityPolicy.SCRIPT_SRC_ELEM_DIRECTIVE,
+ false, // aHasUnsafeHash
+ "", // aNonce
+ false, // aParserCreated
+ null, // aTriggeringElement
+ null, // nsICSPEventListener
+ "", // aContentOfPseudoScript
+ 0, // aLineNumber
+ 1 // aColumnNumber
+ );
+
+ // this is a report only policy, so it better allow inline scripts
+ Assert.ok(inlineOK);
+ }
+ );
+
+ // test that eval violations cause a report in report-only policy
+ makeTest(4, { "blocked-uri": "eval" }, true, function (csp) {
+ let evalOK = true,
+ oReportViolation = { value: false };
+ evalOK = csp.getAllowsEval(oReportViolation);
+
+ // this is a report only policy, so it better allow eval
+ Assert.ok(evalOK);
+ // ... but still cause reports to go out
+ Assert.ok(oReportViolation.value);
+
+ if (oReportViolation.value) {
+ // force the logging, since the getter doesn't.
+ csp.logViolationDetails(
+ Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_EVAL,
+ null, // aTriggeringElement
+ null, // nsICSPEventListener
+ selfuri.asciiSpec,
+ "script sample",
+ 4, // line number
+ 5 // column number
+ );
+ }
+ });
+
+ // test that only the uri's scheme is reported for globally unique identifiers
+ makeTest(5, { "blocked-uri": "data" }, false, function (csp) {
+ var base64data =
+ "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
+ "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+ // shouldLoad creates and sends out the report here.
+ csp.shouldLoad(
+ Ci.nsIContentPolicy.TYPE_IMAGE,
+ null, // nsICSPEventListener
+ null, // nsILoadInfo
+ NetUtil.newURI("data:image/png;base64," + base64data),
+ null,
+ true
+ );
+ });
+
+ // test that only the uri's scheme is reported for globally unique identifiers
+ makeTest(6, { "blocked-uri": "intent" }, false, function (csp) {
+ // shouldLoad creates and sends out the report here.
+ csp.shouldLoad(
+ Ci.nsIContentPolicy.TYPE_SUBDOCUMENT,
+ null, // nsICSPEventListener
+ null, // nsILoadInfo
+ NetUtil.newURI("intent://mymaps.com/maps?um=1&ie=UTF-8&fb=1&sll"),
+ null,
+ true
+ );
+ });
+
+ // test fragment removal
+ var selfSpec =
+ REPORT_SERVER_URI + ":" + REPORT_SERVER_PORT + "/foo/self/foo.js";
+ makeTest(7, { "blocked-uri": selfSpec }, false, function (csp) {
+ // shouldLoad creates and sends out the report here.
+ csp.shouldLoad(
+ Ci.nsIContentPolicy.TYPE_SCRIPT,
+ null, // nsICSPEventListener
+ null, // nsILoadInfo
+ NetUtil.newURI(selfSpec + "#bar"),
+ null,
+ true
+ );
+ });
+}
diff --git a/dom/security/test/unit/test_csp_upgrade_insecure_request_header.js b/dom/security/test/unit/test_csp_upgrade_insecure_request_header.js
new file mode 100644
index 0000000000..26758d261d
--- /dev/null
+++ b/dom/security/test/unit/test_csp_upgrade_insecure_request_header.js
@@ -0,0 +1,103 @@
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+
+// Since this test creates a TYPE_DOCUMENT channel via javascript, it will
+// end up using the wrong LoadInfo constructor. Setting this pref will disable
+// the ContentPolicyType assertion in the constructor.
+Services.prefs.setBoolPref("network.loadinfo.skip_type_assertion", true);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = null;
+var channel = null;
+var curTest = null;
+var testpath = "/footpath";
+
+var tests = [
+ {
+ description: "should not set request header for TYPE_OTHER",
+ expectingHeader: false,
+ contentType: Ci.nsIContentPolicy.TYPE_OTHER,
+ },
+ {
+ description: "should set request header for TYPE_DOCUMENT",
+ expectingHeader: true,
+ contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ },
+ {
+ description: "should set request header for TYPE_SUBDOCUMENT",
+ expectingHeader: true,
+ contentType: Ci.nsIContentPolicy.TYPE_SUBDOCUMENT,
+ },
+ {
+ description: "should not set request header for TYPE_IMAGE",
+ expectingHeader: false,
+ contentType: Ci.nsIContentPolicy.TYPE_IMAGE,
+ },
+];
+
+function ChannelListener() {}
+
+ChannelListener.prototype = {
+ onStartRequest(request) {},
+ onDataAvailable(request, stream, offset, count) {
+ do_throw("Should not get any data!");
+ },
+ onStopRequest(request, status) {
+ var upgrade_insecure_header = false;
+ try {
+ if (request.getRequestHeader("Upgrade-Insecure-Requests")) {
+ upgrade_insecure_header = true;
+ }
+ } catch (e) {
+ // exception is thrown if header is not available on the request
+ }
+ // debug
+ // dump("executing test: " + curTest.description);
+ Assert.equal(upgrade_insecure_header, curTest.expectingHeader);
+ run_next_test();
+ },
+};
+
+function setupChannel(aContentType) {
+ var chan = NetUtil.newChannel({
+ uri: URL + testpath,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: aContentType,
+ });
+ chan.QueryInterface(Ci.nsIHttpChannel);
+ chan.requestMethod = "GET";
+ return chan;
+}
+
+function serverHandler(metadata, response) {
+ // no need to perform anything here
+}
+
+function run_next_test() {
+ curTest = tests.shift();
+ if (!curTest) {
+ httpserver.stop(do_test_finished);
+ return;
+ }
+ channel = setupChannel(curTest.contentType);
+ channel.asyncOpen(new ChannelListener());
+}
+
+function run_test() {
+ do_get_profile();
+
+ // set up the test environment
+ httpserver = new HttpServer();
+ httpserver.registerPathHandler(testpath, serverHandler);
+ httpserver.start(-1);
+
+ run_next_test();
+ do_test_pending();
+}
diff --git a/dom/security/test/unit/test_deserialization_format_before_100.js b/dom/security/test/unit/test_deserialization_format_before_100.js
new file mode 100644
index 0000000000..a21ae4838a
--- /dev/null
+++ b/dom/security/test/unit/test_deserialization_format_before_100.js
@@ -0,0 +1,244 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const ReferrerInfo = Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+);
+
+async function runTest(setupFunc, expected) {
+ let objectOutStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
+ Ci.nsIObjectOutputStream
+ );
+ let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
+ pipe.init(
+ false /* non-blocking input */,
+ false /* non-blocking output */,
+ 0 /* segment size */,
+ 0 /* max segments */
+ );
+ objectOutStream.setOutputStream(pipe.outputStream);
+
+ setupFunc(objectOutStream);
+
+ let objectInStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIObjectInputStream
+ );
+ objectInStream.setInputStream(pipe.inputStream);
+
+ let referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY);
+ try {
+ referrerInfo.read(objectInStream);
+ } catch (e) {
+ Assert.ok(false, "Shouldn't fail when deserializing.");
+ return;
+ }
+
+ Assert.ok(true, "Successfully deserialize the referrerInfo.");
+
+ let { referrerPolicy, sendReferrer, computedReferrerSpec } = expected;
+ Assert.equal(
+ referrerInfo.referrerPolicy,
+ referrerPolicy,
+ "The referrerInfo has the expected referrer policy."
+ );
+
+ Assert.equal(
+ referrerInfo.sendReferrer,
+ sendReferrer,
+ "The referrerInfo has the expected sendReferrer value."
+ );
+
+ if (computedReferrerSpec) {
+ Assert.equal(
+ referrerInfo.computedReferrerSpec,
+ computedReferrerSpec,
+ "The referrerInfo has the expected computedReferrerSpec value."
+ );
+ }
+}
+
+// Test deserializing referrer info with the old format.
+add_task(async function test_deserializeOldReferrerInfo() {
+ // Test with a regular old format.
+ await runTest(
+ stream => {
+ // Write to the output stream with the old format.
+ stream.writeBoolean(true); // nonNull
+ stream.writeStringZ("https://example.com/"); // spec
+ stream.write32(Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN); // policy
+ stream.writeBoolean(false); // sendReferrer
+ stream.writeBoolean(false); // isComputed
+ stream.writeBoolean(true); // initialized
+ stream.writeBoolean(false); // overridePolicyByDefault
+ },
+ {
+ referrerPolicy: Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
+ sendReferrer: false,
+ }
+ );
+
+ // Test with an old format with `sendReferrer` is true.
+ await runTest(
+ stream => {
+ // Write to the output stream with the old format.
+ stream.writeBoolean(true); // nonNull
+ stream.writeStringZ("https://example.com/"); // spec
+ stream.write32(Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN); // policy
+ stream.writeBoolean(true); // sendReferrer
+ stream.writeBoolean(false); // isComputed
+ stream.writeBoolean(true); // initialized
+ stream.writeBoolean(false); // overridePolicyByDefault
+ },
+ {
+ referrerPolicy: Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
+ sendReferrer: true,
+ }
+ );
+
+ // Test with an old format with a computed Referrer.
+ await runTest(
+ stream => {
+ // Write to the output stream with the old format with a string.
+ stream.writeBoolean(true); // nonNull
+ stream.writeStringZ("https://example.com/"); // spec
+ stream.write32(Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN); // policy
+ stream.writeBoolean(false); // sendReferrer
+ stream.writeBoolean(true); // isComputed
+ stream.writeStringZ("https://example.com/"); // computedReferrer
+ stream.writeBoolean(true); // initialized
+ stream.writeBoolean(false); // overridePolicyByDefault
+ },
+ {
+ referrerPolicy: Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
+ sendReferrer: false,
+ computedReferrerSpec: "https://example.com/",
+ }
+ );
+
+ // Test with an old format with a computed Referrer and sendReferrer as true.
+ await runTest(
+ stream => {
+ // Write to the output stream with the old format with a string.
+ stream.writeBoolean(true); // nonNull
+ stream.writeStringZ("https://example.com/"); // spec
+ stream.write32(Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN); // policy
+ stream.writeBoolean(true); // sendReferrer
+ stream.writeBoolean(true); // isComputed
+ stream.writeStringZ("https://example.com/"); // computedReferrer
+ stream.writeBoolean(true); // initialized
+ stream.writeBoolean(false); // overridePolicyByDefault
+ },
+ {
+ referrerPolicy: Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
+ sendReferrer: true,
+ computedReferrerSpec: "https://example.com/",
+ }
+ );
+});
+
+// Test deserializing referrer info with the current format.
+add_task(async function test_deserializeReferrerInfo() {
+ // Test with a current format.
+ await runTest(
+ stream => {
+ // Write to the output stream with the new format.
+ stream.writeBoolean(true); // nonNull
+ stream.writeStringZ("https://example.com/"); // spec
+ stream.write32(Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN); // policy
+ stream.write32(Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN); // original policy
+ stream.writeBoolean(false); // sendReferrer
+ stream.writeBoolean(false); // isComputed
+ stream.writeBoolean(true); // initialized
+ stream.writeBoolean(false); // overridePolicyByDefault
+ },
+ {
+ referrerPolicy: Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
+ sendReferrer: false,
+ }
+ );
+
+ // Test with a current format with sendReferrer as true.
+ await runTest(
+ stream => {
+ // Write to the output stream with the new format.
+ stream.writeBoolean(true); // nonNull
+ stream.writeStringZ("https://example.com/"); // spec
+ stream.write32(Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN); // policy
+ stream.write32(Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN); // original policy
+ stream.writeBoolean(true); // sendReferrer
+ stream.writeBoolean(false); // isComputed
+ stream.writeBoolean(true); // initialized
+ stream.writeBoolean(false); // overridePolicyByDefault
+ },
+ {
+ referrerPolicy: Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
+ sendReferrer: true,
+ }
+ );
+
+ // Test with a current format with a computedReferrer.
+ await runTest(
+ stream => {
+ // Write to the output stream with the new format with a string.
+ stream.writeBoolean(true); // nonNull
+ stream.writeStringZ("https://example.com/"); // spec
+ stream.write32(Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN); // policy
+ stream.write32(Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN); // original policy
+ stream.writeBoolean(false); // sendReferrer
+ stream.writeBoolean(true); // isComputed
+ stream.writeStringZ("https://example.com/"); // computedReferrer
+ stream.writeBoolean(true); // initialized
+ stream.writeBoolean(false); // overridePolicyByDefault
+ },
+ {
+ referrerPolicy: Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
+ sendReferrer: false,
+ computedReferrerSpec: "https://example.com/",
+ }
+ );
+
+ // Test with a current format with a computedReferrer and sendReferrer as true.
+ await runTest(
+ stream => {
+ // Write to the output stream with the new format with a string.
+ stream.writeBoolean(true); // nonNull
+ stream.writeStringZ("https://example.com/"); // spec
+ stream.write32(Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN); // policy
+ stream.write32(Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN); // original policy
+ stream.writeBoolean(true); // sendReferrer
+ stream.writeBoolean(true); // isComputed
+ stream.writeStringZ("https://example.com/"); // computedReferrer
+ stream.writeBoolean(true); // initialized
+ stream.writeBoolean(false); // overridePolicyByDefault
+ },
+ {
+ referrerPolicy: Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
+ sendReferrer: true,
+ computedReferrerSpec: "https://example.com/",
+ }
+ );
+
+ // Test with a current format that the tailing bytes are all zero.
+ await runTest(
+ stream => {
+ // Write to the output stream with the new format with a string.
+ stream.writeBoolean(true); // nonNull
+ stream.writeStringZ("https://example.com/"); // spec
+ stream.write32(Ci.nsIReferrerInfo.EMPTY); // policy
+ stream.write32(Ci.nsIReferrerInfo.EMPTY); // original policy
+ stream.writeBoolean(false); // sendReferrer
+ stream.writeBoolean(false); // isComputed
+ stream.writeBoolean(false); // initialized
+ stream.writeBoolean(false); // overridePolicyByDefault
+ },
+ {
+ referrerPolicy: Ci.nsIReferrerInfo.EMPTY,
+ sendReferrer: false,
+ }
+ );
+});
diff --git a/dom/security/test/unit/test_https_only_https_first_default_port.js b/dom/security/test/unit/test_https_only_https_first_default_port.js
new file mode 100644
index 0000000000..bd4d6717eb
--- /dev/null
+++ b/dom/security/test/unit/test_https_only_https_first_default_port.js
@@ -0,0 +1,111 @@
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+
+const TEST_PATH = "/https_only_https_first_port";
+var httpserver = null;
+var channel = null;
+var curTest = null;
+
+const TESTS = [
+ {
+ description: "Test 1 - Default Port (scheme: http, port: default)",
+ url: "http://test1.example.com",
+ expectedScheme: "https",
+ expectedPort: -1, // -1 == default
+ },
+ {
+ description: "Test 2 - Explicit Default Port (scheme: http, port: 80)",
+ url: "http://test1.example.com:80",
+ expectedScheme: "https",
+ expectedPort: -1, // -1 == default
+ },
+ {
+ description: "Test 3 - Explicit Custom Port (scheme: http, port: 8888)",
+ url: "http://test1.example.com:8888",
+ expectedScheme: "http",
+ expectedPort: 8888,
+ },
+ {
+ description:
+ "Test 4 - Explicit Default Port for https (scheme: https, port: 443)",
+ url: "https://test1.example.com:443",
+ expectedScheme: "https",
+ expectedPort: -1, // -1 == default
+ },
+];
+
+function ChannelListener() {}
+
+ChannelListener.prototype = {
+ onStartRequest(request) {
+ // dummy implementation
+ },
+ onDataAvailable(request, stream, offset, count) {
+ do_throw("Should not get any data!");
+ },
+ onStopRequest(request, status) {
+ var chan = request.QueryInterface(Ci.nsIChannel);
+ let requestURL = chan.URI;
+ Assert.equal(
+ requestURL.scheme,
+ curTest.expectedScheme,
+ curTest.description
+ );
+ Assert.equal(requestURL.port, curTest.expectedPort, curTest.description);
+ Assert.equal(requestURL.host, "test1.example.com", curTest.description);
+ run_next_test();
+ },
+};
+
+function setUpPrefs() {
+ // set up the required prefs
+ Services.prefs.setBoolPref("dom.security.https_first", true);
+ Services.prefs.setBoolPref("dom.security.https_only_mode", false);
+}
+
+function setUpChannel() {
+ var chan = NetUtil.newChannel({
+ uri: curTest.url + TEST_PATH,
+ loadingPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ });
+ chan.QueryInterface(Ci.nsIHttpChannel);
+ chan.requestMethod = "GET";
+ return chan;
+}
+
+function serverHandler(metadata, response) {
+ // dummy implementation
+}
+
+function run_next_test() {
+ curTest = TESTS.shift();
+ if (!curTest) {
+ httpserver.stop(do_test_finished);
+ return;
+ }
+
+ channel = setUpChannel();
+ channel.asyncOpen(new ChannelListener());
+}
+
+function run_test() {
+ do_get_profile();
+ do_test_pending();
+
+ // set up the test environment
+ httpserver = new HttpServer();
+ httpserver.registerPathHandler(TEST_PATH, serverHandler);
+ httpserver.start(-1);
+
+ // set up prefs
+ setUpPrefs();
+
+ // run the tests
+ run_next_test();
+}
diff --git a/dom/security/test/unit/test_https_only_https_first_prefs.js b/dom/security/test/unit/test_https_only_https_first_prefs.js
new file mode 100644
index 0000000000..9c6ced1fcb
--- /dev/null
+++ b/dom/security/test/unit/test_https_only_https_first_prefs.js
@@ -0,0 +1,361 @@
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "HTTP_TEST_URL", function () {
+ return "http://test1.example.com";
+});
+
+const TEST_PATH = "/https_only_https_first_path";
+var httpserver = null;
+var channel = null;
+var curTest = null;
+
+const TESTS = [
+ {
+ // Test 1: all prefs to false
+ description: "Test 1 - top-level",
+ contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ https_only: false,
+ https_only_pbm: false,
+ https_first: false,
+ https_first_pbm: false,
+ pbm: false,
+ expectedScheme: "http",
+ },
+ {
+ description: "Test 1 - top-level - pbm",
+ contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ https_only: false,
+ https_only_pbm: false,
+ https_first: false,
+ https_first_pbm: false,
+ pbm: true,
+ expectedScheme: "http",
+ },
+ {
+ description: "Test 1 - sub-resource",
+ contentType: Ci.nsIContentPolicy.TYPE_IMAGE,
+ https_only: false,
+ https_only_pbm: false,
+ https_first: false,
+ https_first_pbm: false,
+ pbm: false,
+ expectedScheme: "http",
+ },
+ {
+ description: "Test 1 - sub-resource - pbm",
+ contentType: Ci.nsIContentPolicy.TYPE_IMAGE,
+ https_only: false,
+ https_only_pbm: false,
+ https_first: false,
+ https_first_pbm: false,
+ pbm: true,
+ expectedScheme: "http",
+ },
+ // Test 2: https_only true
+ {
+ description: "Test 2 - top-level",
+ contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ https_only: true,
+ https_only_pbm: false,
+ https_first: false,
+ https_first_pbm: false,
+ pbm: false,
+ expectedScheme: "https",
+ },
+ {
+ description: "Test 2 - top-level - pbm",
+ contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ https_only: true,
+ https_only_pbm: false,
+ https_first: false,
+ https_first_pbm: false,
+ pbm: true,
+ expectedScheme: "https",
+ },
+ {
+ description: "Test 2 - sub-resource",
+ contentType: Ci.nsIContentPolicy.TYPE_IMAGE,
+ https_only: true,
+ https_only_pbm: false,
+ https_first: false,
+ https_first_pbm: false,
+ pbm: false,
+ expectedScheme: "https",
+ },
+ {
+ description: "Test 2 - sub-resource - pbm",
+ contentType: Ci.nsIContentPolicy.TYPE_IMAGE,
+ https_only: true,
+ https_only_pbm: false,
+ https_first: false,
+ https_first_pbm: false,
+ pbm: true,
+ expectedScheme: "https",
+ },
+ // Test 3: https_only_pbm true
+ {
+ description: "Test 3 - top-level",
+ contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ https_only: false,
+ https_only_pbm: true,
+ https_first: false,
+ https_first_pbm: false,
+ pbm: false,
+ expectedScheme: "http",
+ },
+ {
+ description: "Test 3 - top-level - pbm",
+ contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ https_only: false,
+ https_only_pbm: true,
+ https_first: false,
+ https_first_pbm: false,
+ pbm: true,
+ expectedScheme: "https",
+ },
+ {
+ description: "Test 3 - sub-resource",
+ contentType: Ci.nsIContentPolicy.TYPE_IMAGE,
+ https_only: false,
+ https_only_pbm: true,
+ https_first: false,
+ https_first_pbm: false,
+ pbm: false,
+ expectedScheme: "http",
+ },
+ {
+ description: "Test 3 - sub-resource - pbm",
+ contentType: Ci.nsIContentPolicy.TYPE_IMAGE,
+ https_only: false,
+ https_only_pbm: true,
+ https_first: false,
+ https_first_pbm: false,
+ pbm: true,
+ expectedScheme: "https",
+ },
+ // Test 4: https_first true
+ {
+ description: "Test 4 - top-level",
+ contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ https_only: false,
+ https_only_pbm: false,
+ https_first: true,
+ https_first_pbm: false,
+ pbm: false,
+ expectedScheme: "https",
+ },
+ {
+ description: "Test 4 - top-level - pbm",
+ contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ https_only: false,
+ https_only_pbm: false,
+ https_first: true,
+ https_first_pbm: false,
+ pbm: true,
+ expectedScheme: "https",
+ },
+ {
+ description: "Test 4 - sub-resource",
+ contentType: Ci.nsIContentPolicy.TYPE_IMAGE,
+ https_only: false,
+ https_only_pbm: false,
+ https_first: true,
+ https_first_pbm: false,
+ pbm: false,
+ expectedScheme: "http",
+ },
+ {
+ description: "Test 4 - sub-resource - pbm",
+ contentType: Ci.nsIContentPolicy.TYPE_IMAGE,
+ https_only: false,
+ https_only_pbm: false,
+ https_first: true,
+ https_first_pbm: false,
+ pbm: true,
+ expectedScheme: "http",
+ },
+ // Test 5: https_first_pbm true
+ {
+ description: "Test 5 - top-level",
+ contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ https_only: false,
+ https_only_pbm: false,
+ https_first: false,
+ https_first_pbm: true,
+ pbm: false,
+ expectedScheme: "http",
+ },
+ {
+ description: "Test 5 - top-level - pbm",
+ contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ https_only: false,
+ https_only_pbm: false,
+ https_first: false,
+ https_first_pbm: true,
+ pbm: true,
+ expectedScheme: "https",
+ },
+ {
+ description: "Test 5 - sub-resource",
+ contentType: Ci.nsIContentPolicy.TYPE_IMAGE,
+ https_only: false,
+ https_only_pbm: false,
+ https_first: false,
+ https_first_pbm: true,
+ pbm: false,
+ expectedScheme: "http",
+ },
+ {
+ description: "Test 5 - sub-resource - pbm",
+ contentType: Ci.nsIContentPolicy.TYPE_IMAGE,
+ https_only: false,
+ https_only_pbm: false,
+ https_first: false,
+ https_first_pbm: true,
+ pbm: true,
+ expectedScheme: "http",
+ },
+ // Test 6: https_only overrules https_first
+ {
+ description: "Test 6 - top-level",
+ contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ https_only: true,
+ https_only_pbm: false,
+ https_first: true,
+ https_first_pbm: false,
+ pbm: false,
+ expectedScheme: "https",
+ },
+ {
+ description: "Test 6 - top-level - pbm",
+ contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ https_only: true,
+ https_only_pbm: false,
+ https_first: true,
+ https_first_pbm: false,
+ pbm: true,
+ expectedScheme: "https",
+ },
+ {
+ description: "Test 6 - sub-resource",
+ contentType: Ci.nsIContentPolicy.TYPE_IMAGE,
+ https_only: true,
+ https_only_pbm: false,
+ https_first: true,
+ https_first_pbm: false,
+ pbm: false,
+ expectedScheme: "https",
+ },
+ {
+ description: "Test 6 - sub-resource - pbm",
+ contentType: Ci.nsIContentPolicy.TYPE_IMAGE,
+ https_only: true,
+ https_only_pbm: false,
+ https_first: true,
+ https_first_pbm: false,
+ pbm: true,
+ expectedScheme: "https",
+ },
+];
+
+function ChannelListener() {}
+
+ChannelListener.prototype = {
+ onStartRequest(request) {
+ var chan = request.QueryInterface(Ci.nsIChannel);
+ var httpChan = chan.QueryInterface(Ci.nsIHttpChannel);
+ var authHeader = httpChan.getRequestHeader("Authorization");
+ Assert.equal(authHeader, "Basic user:pass", curTest.description);
+ },
+ onDataAvailable(request, stream, offset, count) {
+ do_throw("Should not get any data!");
+ },
+ onStopRequest(request, status) {
+ var chan = request.QueryInterface(Ci.nsIChannel);
+ let requestURL = chan.URI;
+ Assert.equal(
+ requestURL.scheme,
+ curTest.expectedScheme,
+ curTest.description
+ );
+ Assert.equal(requestURL.host, "test1.example.com", curTest.description);
+ Assert.equal(requestURL.filePath, TEST_PATH, curTest.description);
+ run_next_test();
+ },
+};
+
+function setUpPrefs() {
+ // set up the required prefs
+ Services.prefs.setBoolPref(
+ "dom.security.https_only_mode",
+ curTest.https_only
+ );
+ Services.prefs.setBoolPref(
+ "dom.security.https_only_mode_pbm",
+ curTest.https_only_pbm
+ );
+ Services.prefs.setBoolPref("dom.security.https_first", curTest.https_first);
+ Services.prefs.setBoolPref(
+ "dom.security.https_first_pbm",
+ curTest.https_first_pbm
+ );
+}
+
+function setUpChannel() {
+ // 1) Set up Principal using OA in case of Private Browsing
+ let attr = {};
+ if (curTest.pbm) {
+ attr.privateBrowsingId = 1;
+ }
+ let uri = Services.io.newURI("http://test1.example.com");
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ attr
+ );
+
+ // 2) Set up Channel
+ var chan = NetUtil.newChannel({
+ uri: HTTP_TEST_URL + TEST_PATH,
+ loadingPrincipal: principal,
+ contentPolicyType: curTest.contentType,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ });
+ chan.QueryInterface(Ci.nsIHttpChannel);
+ chan.requestMethod = "GET";
+ chan.setRequestHeader("Authorization", "Basic user:pass", false);
+ return chan;
+}
+
+function serverHandler(metadata, response) {
+ // dummy implementation
+}
+
+function run_next_test() {
+ curTest = TESTS.shift();
+ if (!curTest) {
+ httpserver.stop(do_test_finished);
+ return;
+ }
+
+ setUpPrefs();
+
+ channel = setUpChannel();
+ channel.asyncOpen(new ChannelListener());
+}
+
+function run_test() {
+ do_get_profile();
+
+ // set up the test environment
+ httpserver = new HttpServer();
+ httpserver.registerPathHandler(TEST_PATH, serverHandler);
+ httpserver.start(-1);
+
+ run_next_test();
+ do_test_pending();
+}
diff --git a/dom/security/test/unit/test_isOriginPotentiallyTrustworthy.js b/dom/security/test/unit/test_isOriginPotentiallyTrustworthy.js
new file mode 100644
index 0000000000..acada7e956
--- /dev/null
+++ b/dom/security/test/unit/test_isOriginPotentiallyTrustworthy.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests the "Is origin potentially trustworthy?" logic from
+ * <https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy>.
+ */
+
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+
+Services.prefs.setCharPref(
+ "dom.securecontext.allowlist",
+ "example.net,example.org"
+);
+
+Services.prefs.setBoolPref("dom.securecontext.allowlist_onions", false);
+
+add_task(async function test_isOriginPotentiallyTrustworthy() {
+ for (let [uriSpec, expectedResult] of [
+ ["http://example.com/", false],
+ ["https://example.com/", true],
+ ["http://localhost/", true],
+ ["http://localhost.localhost/", true],
+ ["http://127.0.0.1/", true],
+ ["file:///", true],
+ ["resource:///", true],
+ ["moz-extension://", true],
+ ["wss://example.com/", true],
+ ["about:config", false],
+ ["http://example.net/", true],
+ ["ws://example.org/", true],
+ ["chrome://example.net/content/messenger.xul", false],
+ ["http://1234567890abcdef.onion/", false],
+ ]) {
+ let uri = NetUtil.newURI(uriSpec);
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+ Assert.equal(principal.isOriginPotentiallyTrustworthy, expectedResult);
+ }
+ // And now let's test whether .onion sites are properly treated when
+ // allowlisted, see bug 1382359.
+ Services.prefs.setBoolPref("dom.securecontext.allowlist_onions", true);
+ let uri = NetUtil.newURI("http://1234567890abcdef.onion/");
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+ Assert.equal(principal.isOriginPotentiallyTrustworthy, true);
+});
diff --git a/dom/security/test/unit/xpcshell.toml b/dom/security/test/unit/xpcshell.toml
new file mode 100644
index 0000000000..345e70e75f
--- /dev/null
+++ b/dom/security/test/unit/xpcshell.toml
@@ -0,0 +1,16 @@
+[DEFAULT]
+head = ""
+
+["test_csp_reports.js"]
+
+["test_csp_upgrade_insecure_request_header.js"]
+
+["test_deserialization_format_before_100.js"]
+
+["test_https_only_https_first_default_port.js"]
+skip-if = ["debug"] # assertions in loadinfo; loadinfo is just not xpcshell test friendly
+
+["test_https_only_https_first_prefs.js"]
+skip-if = ["debug"] # assertions in loadinfo; loadinfo is just not xpcshell test friendly
+
+["test_isOriginPotentiallyTrustworthy.js"]