├── .gitignore ├── .npmignore ├── AUTHORS ├── CONTRIBUTING ├── LICENSE ├── README.md ├── allowlist_bypasses ├── angular.ts ├── flash.ts ├── json │ ├── angular.json │ ├── flash.json │ └── jsonp.json └── jsonp.ts ├── checks ├── checker.ts ├── parser_checks.ts ├── parser_checks_test.ts ├── security_checks.ts ├── security_checks_test.ts ├── strictcsp_checks.ts └── strictcsp_checks_test.ts ├── csp.ts ├── csp_test.ts ├── evaluator.ts ├── evaluator_test.ts ├── finding.ts ├── finding_test.ts ├── jasmine.json ├── lighthouse ├── lighthouse_checks.ts └── lighthouse_checks_test.ts ├── package-lock.json ├── package.json ├── parser.ts ├── parser_test.ts ├── tsconfig.json ├── utils.ts └── utils_test.ts /.gitignore: -------------------------------------------------------------------------------- 1 | dest/ 2 | *.js -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Purposefully blank so NPM doesn't use the .gitignore file -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This file lists all individuals having contributed content to the repository. 2 | # This file is distinct from the CONTRIBUTING files. 3 | # See the latter for an explanation. 4 | 5 | # Names should be added to this file as: 6 | # email address (Name or Organization) 7 | # The email address is not required for organizations. 8 | 9 | lwe@google.com (Lukas Weichselbaum) 10 | ddworken@google.com (David Dworken) -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at the end). 2 | 3 | ### Before you contribute 4 | Before we can use your code, you must sign the 5 | [Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) 6 | (CLA), which you can do online. The CLA is necessary mainly because you own the 7 | copyright to your changes, even after your contribution becomes part of our 8 | codebase, so we need your permission to use and distribute your code. We also 9 | need to be sure of various other things—for instance that you'll tell us if you 10 | know that your code infringes on other people's patents. You don't have to sign 11 | the CLA until after you've submitted your code for review and a member has 12 | approved it, but you must do it before we can put your code into our codebase. 13 | Before you start working on a larger contribution, you should get in touch with 14 | us first through the issue tracker with your idea so that we can help out and 15 | possibly guide you. Coordinating up front makes it much easier to avoid 16 | frustration later on. 17 | 18 | ### Code reviews 19 | All submissions, including submissions by project members, require review. We 20 | use Github pull requests for this purpose. 21 | 22 | ### The small print 23 | Contributions made by corporations are covered by a different agreement than 24 | the one above, the 25 | [Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate). -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSP Evaluator Core Library 2 | 3 | ## Introduction 4 | 5 | -------------------------------------------------------------------------------- 6 | 7 | Please note: this is not an official Google product. 8 | 9 | CSP Evaluator allows developers and security experts to check if a Content 10 | Security Policy ([CSP](https://csp.withgoogle.com/docs/index.html)) serves as a 11 | strong mitigation against 12 | [cross-site scripting attacks](https://www.google.com/about/appsecurity/learning/xss/). 13 | It assists with the process of reviewing CSP policies, and helps identify subtle 14 | CSP bypasses which undermine the value of a policy. CSP Evaluator checks are 15 | based on a [large-scale study](https://research.google.com/pubs/pub45542.html) 16 | and are aimed to help developers to harden their CSP and improve the security of 17 | their applications. This tool is provided only for the convenience of developers 18 | and Google provides no guarantees or warranties for this tool. 19 | 20 | CSP Evaluator comes with a built-in list of common CSP allowlist bypasses which 21 | reduce the security of a policy. This list only contains popular bypasses and is 22 | by no means complete. 23 | 24 | The CSP Evaluator library + frontend is deployed here: 25 | https://csp-evaluator.withgoogle.com/ 26 | 27 | ## Installing 28 | 29 | This library is published to `https://www.npmjs.com/package/csp_evaluator`. You 30 | can install it via: 31 | 32 | ```bash 33 | npm install csp_evaluator 34 | ``` 35 | 36 | ## Building 37 | 38 | To build, run: 39 | 40 | ```bash 41 | npm install && tsc --build 42 | ``` 43 | 44 | ## Testing 45 | 46 | To run unit tests, run: 47 | 48 | ```bash 49 | npm install && npm test 50 | ``` 51 | 52 | ## Example Usage 53 | 54 | ```javascript 55 | import {CspEvaluator} from "csp_evaluator/dist/evaluator.js"; 56 | import {CspParser} from "csp_evaluator/dist/parser.js"; 57 | 58 | const parsed = new CspParser("script-src https://google.com").csp; 59 | console.log(new CspEvaluator(parsed).evaluate()); 60 | ``` 61 | -------------------------------------------------------------------------------- /allowlist_bypasses/angular.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Collection of popular sites/CDNs hosting Angular. 3 | * @author lwe@google.com (Lukas Weichselbaum) 4 | * 5 | * @license 6 | * Copyright 2016 Google Inc. All rights reserved. 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | 21 | /** 22 | * Angular libraries on commonly allowlisted origins (e.g. CDNs) that would 23 | * allow a CSP bypass. 24 | * Only most common paths are listed here. Hence there might still be other 25 | * paths on these domains that would allow a bypass. 26 | */ 27 | export const URLS: string[] = [ 28 | '//gstatic.com/fsn/angular_js-bundle1.js', 29 | '//www.gstatic.com/fsn/angular_js-bundle1.js', 30 | '//www.googleadservices.com/pageadimg/imgad', 31 | '//yandex.st/angularjs/1.2.16/angular-cookies.min.js', 32 | '//yastatic.net/angularjs/1.2.23/angular.min.js', 33 | '//yuedust.yuedu.126.net/js/components/angular/angular.js', 34 | '//art.jobs.netease.com/script/angular.js', 35 | '//csu-c45.kxcdn.com/angular/angular.js', 36 | '//elysiumwebsite.s3.amazonaws.com/uploads/blog-media/rockstar/angular.min.js', 37 | '//inno.blob.core.windows.net/new/libs/AngularJS/1.2.1/angular.min.js', 38 | '//gift-talk.kakao.com/public/javascripts/angular.min.js', 39 | '//ajax.googleapis.com/ajax/libs/angularjs/1.2.0rc1/angular-route.min.js', 40 | '//master-sumok.ru/vendors/angular/angular-cookies.js', 41 | '//ayicommon-a.akamaihd.net/static/vendor/angular-1.4.2.min.js', 42 | '//pangxiehaitao.com/framework/angular-1.3.9/angular-animate.min.js', 43 | '//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.16/angular.min.js', 44 | '//96fe3ee995e96e922b6b-d10c35bd0a0de2c718b252bc575fdb73.ssl.cf1.rackcdn.com/angular.js', 45 | '//oss.maxcdn.com/angularjs/1.2.20/angular.min.js', 46 | '//reports.zemanta.com/smedia/common/angularjs/1.2.11/angular.js', 47 | '//cdn.shopify.com/s/files/1/0225/6463/t/1/assets/angular-animate.min.js', 48 | '//parademanagement.com.s3-website-ap-southeast-1.amazonaws.com/js/angular.min.js', 49 | '//cdn.jsdelivr.net/angularjs/1.1.2/angular.min.js', 50 | '//eb2883ede55c53e09fd5-9c145fb03d93709ea57875d307e2d82e.ssl.cf3.rackcdn.com/components/angular-resource.min.js', 51 | '//andors-trail.googlecode.com/git/AndorsTrailEdit/lib/angular.min.js', 52 | '//cdn.walkme.com/General/EnvironmentTests/angular/angular.min.js', 53 | '//laundrymail.com/angular/angular.js', 54 | '//s3-eu-west-1.amazonaws.com/staticancpa/js/angular-cookies.min.js', 55 | '//collade.demo.stswp.com/js/vendor/angular.min.js', 56 | '//mrfishie.github.io/sailor/bower_components/angular/angular.min.js', 57 | '//askgithub.com/static/js/angular.min.js', 58 | '//services.amazon.com/solution-providers/assets/vendor/angular-cookies.min.js', 59 | '//raw.githubusercontent.com/angular/code.angularjs.org/master/1.0.7/angular-resource.js', 60 | '//prb-resume.appspot.com/bower_components/angular-animate/angular-animate.js', 61 | '//dl.dropboxusercontent.com/u/30877786/angular.min.js', 62 | '//static.tumblr.com/x5qdx0r/nPOnngtff/angular-resource.min_1_.js', 63 | '//storage.googleapis.com/assets-prod.urbansitter.net/us-sym/assets/vendor/angular-sanitize/angular-sanitize.min.js', 64 | '//twitter.github.io/labella.js/bower_components/angular/angular.min.js', 65 | '//cdn2-casinoroom.global.ssl.fastly.net/js/lib/angular-animate.min.js', 66 | '//www.adobe.com/devnet-apps/flashshowcase/lib/angular/angular.1.1.5.min.js', 67 | '//eternal-sunset.herokuapp.com/bower_components/angular/angular.js', 68 | '//cdn.bootcss.com/angular.js/1.2.0/angular.min.js' 69 | ]; 70 | -------------------------------------------------------------------------------- /allowlist_bypasses/flash.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Collection of popular sites/CDNs hosting flash with user 3 | * provided JS. 4 | * @author lwe@google.com (Lukas Weichselbaum) 5 | * 6 | * @license 7 | * Copyright 2016 Google Inc. All rights reserved. 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | 22 | /** 23 | * Domains that would allow a CSP bypass if allowlisted. 24 | * Only most common paths will be listed here. Hence there might still be other 25 | * paths on these domains that would allow a bypass. 26 | */ 27 | export const URLS: string[] = [ 28 | '//vk.com/swf/video.swf', 29 | '//ajax.googleapis.com/ajax/libs/yui/2.8.0r4/build/charts/assets/charts.swf' 30 | ]; 31 | -------------------------------------------------------------------------------- /allowlist_bypasses/json/angular.json: -------------------------------------------------------------------------------- 1 | {"urls": 2 | [ 3 | "//gstatic.com/fsn/angular_js-bundle1.js", 4 | "//www.gstatic.com/fsn/angular_js-bundle1.js", 5 | "//www.googleadservices.com/pageadimg/imgad", 6 | "//yandex.st/angularjs/1.2.16/angular-cookies.min.js", 7 | "//yastatic.net/angularjs/1.2.23/angular.min.js", 8 | "//yuedust.yuedu.126.net/js/components/angular/angular.js", 9 | "//art.jobs.netease.com/script/angular.js", 10 | "//csu-c45.kxcdn.com/angular/angular.js", 11 | "//elysiumwebsite.s3.amazonaws.com/uploads/blog-media/rockstar/angular.min.js", 12 | "//inno.blob.core.windows.net/new/libs/AngularJS/1.2.1/angular.min.js", 13 | "//gift-talk.kakao.com/public/javascripts/angular.min.js", 14 | "//ajax.googleapis.com/ajax/libs/angularjs/1.2.0rc1/angular-route.min.js", 15 | "//master-sumok.ru/vendors/angular/angular-cookies.js", 16 | "//ayicommon-a.akamaihd.net/static/vendor/angular-1.4.2.min.js", 17 | "//pangxiehaitao.com/framework/angular-1.3.9/angular-animate.min.js", 18 | "//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.16/angular.min.js", 19 | "//96fe3ee995e96e922b6b-d10c35bd0a0de2c718b252bc575fdb73.ssl.cf1.rackcdn.com/angular.js", 20 | "//oss.maxcdn.com/angularjs/1.2.20/angular.min.js", 21 | "//reports.zemanta.com/smedia/common/angularjs/1.2.11/angular.js", 22 | "//cdn.shopify.com/s/files/1/0225/6463/t/1/assets/angular-animate.min.js", 23 | "//parademanagement.com.s3-website-ap-southeast-1.amazonaws.com/js/angular.min.js", 24 | "//cdn.jsdelivr.net/angularjs/1.1.2/angular.min.js", 25 | "//eb2883ede55c53e09fd5-9c145fb03d93709ea57875d307e2d82e.ssl.cf3.rackcdn.com/components/angular-resource.min.js", 26 | "//andors-trail.googlecode.com/git/AndorsTrailEdit/lib/angular.min.js", 27 | "//cdn.walkme.com/General/EnvironmentTests/angular/angular.min.js", 28 | "//laundrymail.com/angular/angular.js", 29 | "//s3-eu-west-1.amazonaws.com/staticancpa/js/angular-cookies.min.js", 30 | "//collade.demo.stswp.com/js/vendor/angular.min.js", 31 | "//mrfishie.github.io/sailor/bower_components/angular/angular.min.js", 32 | "//askgithub.com/static/js/angular.min.js", 33 | "//services.amazon.com/solution-providers/assets/vendor/angular-cookies.min.js", 34 | "//raw.githubusercontent.com/angular/code.angularjs.org/master/1.0.7/angular-resource.js", 35 | "//prb-resume.appspot.com/bower_components/angular-animate/angular-animate.js", 36 | "//dl.dropboxusercontent.com/u/30877786/angular.min.js", 37 | "//static.tumblr.com/x5qdx0r/nPOnngtff/angular-resource.min_1_.js", 38 | "//storage.googleapis.com/assets-prod.urbansitter.net/us-sym/assets/vendor/angular-sanitize/angular-sanitize.min.js", 39 | "//twitter.github.io/labella.js/bower_components/angular/angular.min.js", 40 | "//cdn2-casinoroom.global.ssl.fastly.net/js/lib/angular-animate.min.js", 41 | "//www.adobe.com/devnet-apps/flashshowcase/lib/angular/angular.1.1.5.min.js", 42 | "//eternal-sunset.herokuapp.com/bower_components/angular/angular.js", 43 | "//cdn.bootcss.com/angular.js/1.2.0/angular.min.js" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /allowlist_bypasses/json/flash.json: -------------------------------------------------------------------------------- 1 | {"urls": 2 | [ 3 | "//vk.com/swf/video.swf", 4 | "//ajax.googleapis.com/ajax/libs/yui/2.8.0r4/build/charts/assets/charts.swf" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /allowlist_bypasses/json/jsonp.json: -------------------------------------------------------------------------------- 1 | {"needsEval": 2 | [ 3 | "googletagmanager.com", 4 | "www.googletagmanager.com", 5 | "www.googleadservices.com", 6 | 7 | "google-analytics.com", 8 | "ssl.google-analytics.com", 9 | "www.google-analytics.com" 10 | ], 11 | "urls": 12 | [ 13 | "//bebezoo.1688.com/fragment/index.htm", 14 | "//www.google-analytics.com/gtm/js", 15 | "//googleads.g.doubleclick.net/pagead/conversion/1036918760/wcm", 16 | "//www.googleadservices.com/pagead/conversion/1070110417/wcm", 17 | "//www.google.com/tools/feedback/escalation-options", 18 | "//pin.aliyun.com/check_audio", 19 | "//offer.alibaba.com/market/CID100002954/5/fetchKeyword.do", 20 | "//ccrprod.alipay.com/ccr/arriveTime.json", 21 | "//group.aliexpress.com/ajaxAcquireGroupbuyProduct.do", 22 | "//detector.alicdn.com/2.7.3/index.php", 23 | "//suggest.taobao.com/sug", 24 | "//translate.google.com/translate_a/l", 25 | "//count.tbcdn.cn//counter3", 26 | "//wb.amap.com/channel.php", 27 | "//translate.googleapis.com/translate_a/l", 28 | "//afpeng.alimama.com/ex", 29 | "//accounts.google.com/o/oauth2/revoke", 30 | "//pagead2.googlesyndication.com/relatedsearch", 31 | "//yandex.ru/soft/browsers/check", 32 | "//api.facebook.com/restserver.php", 33 | "//mts0.googleapis.com/maps/vt", 34 | "//syndication.twitter.com/widgets/timelines/765840589183213568", 35 | "//www.youtube.com/profile_style", 36 | "//googletagmanager.com/gtm/js", 37 | "//mc.yandex.ru/watch/24306916/1", 38 | "//share.yandex.net/counter/gpp/", 39 | "//ok.go.mail.ru/lady_on_lady_recipes_r.json", 40 | "//d1f69o4buvlrj5.cloudfront.net/__efa_15_1_ornpba.xekq.arg/optout_check", 41 | "//www.googletagmanager.com/gtm/js", 42 | "//api.vk.com/method/wall.get", 43 | "//www.sharethis.com/get-publisher-info.php", 44 | "//google.ru/maps/vt", 45 | "//pro.netrox.sc/oapi/h_checksite.ashx", 46 | "//vimeo.com/api/oembed.json/", 47 | "//de.blog.newrelic.com/wp-admin/admin-ajax.php", 48 | "//ajax.googleapis.com/ajax/services/search/news", 49 | "//ssl.google-analytics.com/gtm/js", 50 | "//pubsub.pubnub.com/subscribe/demo/hello_world/", 51 | "//pass.yandex.ua/services", 52 | "//id.rambler.ru/script/topline_info.js", 53 | "//m.addthis.com/live/red_lojson/100eng.json", 54 | "//passport.ngs.ru/ajax/check", 55 | "//catalog.api.2gis.ru/ads/search", 56 | "//gum.criteo.com/sync", 57 | "//maps.google.com/maps/vt", 58 | "//ynuf.alipay.com/service/um.json", 59 | "//securepubads.g.doubleclick.net/gampad/ads", 60 | "//c.tiles.mapbox.com/v3/texastribune.tx-congress-cvap/6/15/26.grid.json", 61 | "//rexchange.begun.ru/banners", 62 | "//an.yandex.ru/page/147484", 63 | "//links.services.disqus.com/api/ping", 64 | "//api.map.baidu.com/", 65 | "//tj.gongchang.com/api/keywordrecomm/", 66 | "//data.gongchang.com/livegrail/", 67 | "//ulogin.ru/token.php", 68 | "//beta.gismeteo.ru/api/informer/layout.js/120x240-3/ru/", 69 | "//maps.googleapis.com/maps/api/js/GeoPhotoService.GetMetadata", 70 | "//a.config.skype.com/config/v1/Skype/908_1.33.0.111/SkypePersonalization", 71 | "//maps.beeline.ru/w", 72 | "//target.ukr.net/", 73 | "//www.meteoprog.ua/data/weather/informer/Poltava.js", 74 | "//cdn.syndication.twimg.com/widgets/timelines/599200054310604802", 75 | "//wslocker.ru/client/user.chk.php", 76 | "//community.adobe.com/CommunityPod/getJSON", 77 | "//maps.google.lv/maps/vt", 78 | "//dev.virtualearth.net/REST/V1/Imagery/Metadata/AerialWithLabels/26.318581", 79 | "//awaps.yandex.ru/10/8938/02400400.", 80 | "//a248.e.akamai.net/h5.hulu.com/h5.mp4", 81 | "//nominatim.openstreetmap.org/", 82 | "//plugins.mozilla.org/en-us/plugins_list.json", 83 | "//h.cackle.me/widget/32153/bootstrap", 84 | "//graph.facebook.com/1/", 85 | "//fellowes.ugc.bazaarvoice.com/data/reviews.json", 86 | "//widgets.pinterest.com/v3/pidgets/boards/ciciwin/hedgehog-squirrel-crafts/pins/", 87 | "//se.wikipedia.org/w/api.php", 88 | "//cse.google.com/api/007627024705277327428/cse/r3vs7b0fcli/queries/js", 89 | "//relap.io/api/v2/similar_pages_jsonp.js", 90 | "//c1n3.hypercomments.com/stream/subscribe", 91 | "//maps.google.de/maps/vt", 92 | "//books.google.com/books", 93 | "//connect.mail.ru/share_count", 94 | "//tr.indeed.com/m/newjobs", 95 | "//www-onepick-opensocial.googleusercontent.com/gadgets/proxy", 96 | "//www.panoramio.com/map/get_panoramas.php", 97 | "//client.siteheart.com/streamcli/client", 98 | "//www.facebook.com/restserver.php", 99 | "//autocomplete.travelpayouts.com/avia", 100 | "//www.googleapis.com/freebase/v1/topic/m/0344_", 101 | "//mts1.googleapis.com/mapslt/ft", 102 | "//publish.twitter.com/oembed", 103 | "//fast.wistia.com/embed/medias/o75jtw7654.json", 104 | "//partner.googleadservices.com/gampad/ads", 105 | "//pass.yandex.ru/services", 106 | "//gupiao.baidu.com/stocks/stockbets", 107 | "//widget.admitad.com/widget/init", 108 | "//api.instagram.com/v1/tags/partykungen23328/media/recent", 109 | "//video.media.yql.yahoo.com/v1/video/sapi/streams/063fb76c-6c70-38c5-9bbc-04b7c384de2b", 110 | "//ib.adnxs.com/jpt", 111 | "//pass.yandex.com/services", 112 | "//www.google.de/maps/vt", 113 | "//clients1.google.com/complete/search", 114 | "//api.userlike.com/api/chat/slot/proactive/", 115 | "//www.youku.com/index_cookielist/s/jsonp", 116 | "//mt1.googleapis.com/mapslt/ft", 117 | "//api.mixpanel.com/track/", 118 | "//wpd.b.qq.com/cgi/get_sign.php", 119 | "//pipes.yahooapis.com/pipes/pipe.run", 120 | "//gdata.youtube.com/feeds/api/videos/WsJIHN1kNWc", 121 | "//9.chart.apis.google.com/chart", 122 | "//cdn.syndication.twitter.com/moments/709229296800440320", 123 | "//api.flickr.com/services/feeds/photos_friends.gne", 124 | "//cbks0.googleapis.com/cbk", 125 | "//www.blogger.com/feeds/5578653387562324002/posts/summary/4427562025302749269", 126 | "//query.yahooapis.com/v1/public/yql", 127 | "//kecngantang.blogspot.com/feeds/posts/default/-/Komik", 128 | "//www.travelpayouts.com/widgets/50f53ce9ada1b54bcc000031.json", 129 | "//i.cackle.me/widget/32586/bootstrap", 130 | "//translate.yandex.net/api/v1.5/tr.json/detect", 131 | "//a.tiles.mapbox.com/v3/zentralmedia.map-n2raeauc.jsonp", 132 | "//maps.google.ru/maps/vt", 133 | "//c1n2.hypercomments.com/stream/subscribe", 134 | "//rec.ydf.yandex.ru/cookie" 135 | ] 136 | } 137 | -------------------------------------------------------------------------------- /allowlist_bypasses/jsonp.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Collection of popular sites/CDNs hosting JSONP-like endpoints. 3 | * Endpoints don't contain necessary parameters to trigger JSONP response 4 | * because parameters are ignored in CSP allowlists. 5 | * Usually per domain only one (popular) file path is listed to allow bypasses 6 | * of the most common path based allowlists. It's not practical to ship a list 7 | * for all possible paths/domains. Therefore the jsonp bypass check usually only 8 | * works efficient for domain based allowlists. 9 | * @author lwe@google.com (Lukas Weichselbaum) 10 | * 11 | * @license 12 | * Copyright 2016 Google Inc. All rights reserved. 13 | * Licensed under the Apache License, Version 2.0 (the "License"); 14 | * you may not use this file except in compliance with the License. 15 | * You may obtain a copy of the License at 16 | * 17 | * http://www.apache.org/licenses/LICENSE-2.0 18 | * 19 | * Unless required by applicable law or agreed to in writing, software 20 | * distributed under the License is distributed on an "AS IS" BASIS, 21 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | * See the License for the specific language governing permissions and 23 | * limitations under the License. 24 | */ 25 | 26 | 27 | /** 28 | * Some JSONP-like bypasses only work if the CSP allows 'eval()'. 29 | */ 30 | export const NEEDS_EVAL: string[] = [ 31 | 'googletagmanager.com', 'www.googletagmanager.com', 32 | 33 | 'www.googleadservices.com', 'google-analytics.com', 34 | 'ssl.google-analytics.com', 'www.google-analytics.com' 35 | ]; 36 | 37 | 38 | 39 | /** 40 | * JSONP endpoints on commonly allowlisted origins (e.g. CDNs) that would allow 41 | * a CSP bypass. 42 | * Only most common paths are listed here. Hence there might still be other 43 | * paths on these domains that would allow a bypass. 44 | */ 45 | export const URLS: string[] = [ 46 | '//bebezoo.1688.com/fragment/index.htm', 47 | '//www.google-analytics.com/gtm/js', 48 | '//googleads.g.doubleclick.net/pagead/conversion/1036918760/wcm', 49 | '//www.googleadservices.com/pagead/conversion/1070110417/wcm', 50 | '//www.google.com/tools/feedback/escalation-options', 51 | '//pin.aliyun.com/check_audio', 52 | '//offer.alibaba.com/market/CID100002954/5/fetchKeyword.do', 53 | '//ccrprod.alipay.com/ccr/arriveTime.json', 54 | '//group.aliexpress.com/ajaxAcquireGroupbuyProduct.do', 55 | '//detector.alicdn.com/2.7.3/index.php', 56 | '//suggest.taobao.com/sug', 57 | '//translate.google.com/translate_a/l', 58 | '//count.tbcdn.cn//counter3', 59 | '//wb.amap.com/channel.php', 60 | '//translate.googleapis.com/translate_a/l', 61 | '//afpeng.alimama.com/ex', 62 | '//accounts.google.com/o/oauth2/revoke', 63 | '//pagead2.googlesyndication.com/relatedsearch', 64 | '//yandex.ru/soft/browsers/check', 65 | '//api.facebook.com/restserver.php', 66 | '//mts0.googleapis.com/maps/vt', 67 | '//syndication.twitter.com/widgets/timelines/765840589183213568', 68 | '//www.youtube.com/profile_style', 69 | '//googletagmanager.com/gtm/js', 70 | '//mc.yandex.ru/watch/24306916/1', 71 | '//share.yandex.net/counter/gpp/', 72 | '//ok.go.mail.ru/lady_on_lady_recipes_r.json', 73 | '//d1f69o4buvlrj5.cloudfront.net/__efa_15_1_ornpba.xekq.arg/optout_check', 74 | '//www.googletagmanager.com/gtm/js', 75 | '//api.vk.com/method/wall.get', 76 | '//www.sharethis.com/get-publisher-info.php', 77 | '//google.ru/maps/vt', 78 | '//pro.netrox.sc/oapi/h_checksite.ashx', 79 | '//vimeo.com/api/oembed.json/', 80 | '//de.blog.newrelic.com/wp-admin/admin-ajax.php', 81 | '//ajax.googleapis.com/ajax/services/search/news', 82 | '//ssl.google-analytics.com/gtm/js', 83 | '//pubsub.pubnub.com/subscribe/demo/hello_world/', 84 | '//pass.yandex.ua/services', 85 | '//id.rambler.ru/script/topline_info.js', 86 | '//m.addthis.com/live/red_lojson/100eng.json', 87 | '//passport.ngs.ru/ajax/check', 88 | '//catalog.api.2gis.ru/ads/search', 89 | '//gum.criteo.com/sync', 90 | '//maps.google.com/maps/vt', 91 | '//ynuf.alipay.com/service/um.json', 92 | '//securepubads.g.doubleclick.net/gampad/ads', 93 | '//c.tiles.mapbox.com/v3/texastribune.tx-congress-cvap/6/15/26.grid.json', 94 | '//rexchange.begun.ru/banners', 95 | '//an.yandex.ru/page/147484', 96 | '//links.services.disqus.com/api/ping', 97 | '//api.map.baidu.com/', 98 | '//tj.gongchang.com/api/keywordrecomm/', 99 | '//data.gongchang.com/livegrail/', 100 | '//ulogin.ru/token.php', 101 | '//beta.gismeteo.ru/api/informer/layout.js/120x240-3/ru/', 102 | '//maps.googleapis.com/maps/api/js/GeoPhotoService.GetMetadata', 103 | '//a.config.skype.com/config/v1/Skype/908_1.33.0.111/SkypePersonalization', 104 | '//maps.beeline.ru/w', 105 | '//target.ukr.net/', 106 | '//www.meteoprog.ua/data/weather/informer/Poltava.js', 107 | '//cdn.syndication.twimg.com/widgets/timelines/599200054310604802', 108 | '//wslocker.ru/client/user.chk.php', 109 | '//community.adobe.com/CommunityPod/getJSON', 110 | '//maps.google.lv/maps/vt', 111 | '//dev.virtualearth.net/REST/V1/Imagery/Metadata/AerialWithLabels/26.318581', 112 | '//awaps.yandex.ru/10/8938/02400400.', 113 | '//a248.e.akamai.net/h5.hulu.com/h5.mp4', 114 | '//nominatim.openstreetmap.org/', 115 | '//plugins.mozilla.org/en-us/plugins_list.json', 116 | '//h.cackle.me/widget/32153/bootstrap', 117 | '//graph.facebook.com/1/', 118 | '//fellowes.ugc.bazaarvoice.com/data/reviews.json', 119 | '//widgets.pinterest.com/v3/pidgets/boards/ciciwin/hedgehog-squirrel-crafts/pins/', 120 | '//se.wikipedia.org/w/api.php', 121 | '//cse.google.com/api/007627024705277327428/cse/r3vs7b0fcli/queries/js', 122 | '//relap.io/api/v2/similar_pages_jsonp.js', 123 | '//c1n3.hypercomments.com/stream/subscribe', 124 | '//maps.google.de/maps/vt', 125 | '//books.google.com/books', 126 | '//connect.mail.ru/share_count', 127 | '//tr.indeed.com/m/newjobs', 128 | '//www-onepick-opensocial.googleusercontent.com/gadgets/proxy', 129 | '//www.panoramio.com/map/get_panoramas.php', 130 | '//client.siteheart.com/streamcli/client', 131 | '//www.facebook.com/restserver.php', 132 | '//autocomplete.travelpayouts.com/avia', 133 | '//www.googleapis.com/freebase/v1/topic/m/0344_', 134 | '//mts1.googleapis.com/mapslt/ft', 135 | '//publish.twitter.com/oembed', 136 | '//fast.wistia.com/embed/medias/o75jtw7654.json', 137 | '//partner.googleadservices.com/gampad/ads', 138 | '//pass.yandex.ru/services', 139 | '//gupiao.baidu.com/stocks/stockbets', 140 | '//widget.admitad.com/widget/init', 141 | '//api.instagram.com/v1/tags/partykungen23328/media/recent', 142 | '//video.media.yql.yahoo.com/v1/video/sapi/streams/063fb76c-6c70-38c5-9bbc-04b7c384de2b', 143 | '//ib.adnxs.com/jpt', 144 | '//pass.yandex.com/services', 145 | '//www.google.de/maps/vt', 146 | '//clients1.google.com/complete/search', 147 | '//api.userlike.com/api/chat/slot/proactive/', 148 | '//www.youku.com/index_cookielist/s/jsonp', 149 | '//mt1.googleapis.com/mapslt/ft', 150 | '//api.mixpanel.com/track/', 151 | '//wpd.b.qq.com/cgi/get_sign.php', 152 | '//pipes.yahooapis.com/pipes/pipe.run', 153 | '//gdata.youtube.com/feeds/api/videos/WsJIHN1kNWc', 154 | '//9.chart.apis.google.com/chart', 155 | '//cdn.syndication.twitter.com/moments/709229296800440320', 156 | '//api.flickr.com/services/feeds/photos_friends.gne', 157 | '//cbks0.googleapis.com/cbk', 158 | '//www.blogger.com/feeds/5578653387562324002/posts/summary/4427562025302749269', 159 | '//query.yahooapis.com/v1/public/yql', 160 | '//kecngantang.blogspot.com/feeds/posts/default/-/Komik', 161 | '//www.travelpayouts.com/widgets/50f53ce9ada1b54bcc000031.json', 162 | '//i.cackle.me/widget/32586/bootstrap', 163 | '//translate.yandex.net/api/v1.5/tr.json/detect', 164 | '//a.tiles.mapbox.com/v3/zentralmedia.map-n2raeauc.jsonp', 165 | '//maps.google.ru/maps/vt', 166 | '//c1n2.hypercomments.com/stream/subscribe', 167 | '//rec.ydf.yandex.ru/cookie', 168 | '//cdn.jsdelivr.net' 169 | ]; 170 | -------------------------------------------------------------------------------- /checks/checker.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Shared interfaces for functions that check CSP policies. 3 | */ 4 | 5 | import {Csp} from '../csp'; 6 | import {Finding} from '../finding'; 7 | 8 | /** 9 | * A function that checks a given Csp for problems and returns an unordered 10 | * list of Findings. 11 | */ 12 | export type CheckerFunction = (csp: Csp) => Finding[]; 13 | -------------------------------------------------------------------------------- /checks/parser_checks.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Collection of CSP parser checks which can be used to find 3 | * common syntax mistakes like missing semicolons, invalid directives or 4 | * invalid keywords. 5 | * @author lwe@google.com (Lukas Weichselbaum) 6 | * 7 | * @license 8 | * Copyright 2016 Google Inc. All rights reserved. 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | */ 21 | 22 | import * as csp from '../csp'; 23 | import {Csp, Keyword} from '../csp'; 24 | 25 | import {Finding, Severity, Type} from '../finding'; 26 | 27 | 28 | /** 29 | * Checks if the csp contains invalid directives. 30 | * 31 | * Example policy where this check would trigger: 32 | * foobar-src foo.bar 33 | * 34 | * @param parsedCsp A parsed csp. 35 | */ 36 | export function checkUnknownDirective(parsedCsp: Csp): Finding[] { 37 | const findings: Finding[] = []; 38 | 39 | for (const directive of Object.keys(parsedCsp.directives)) { 40 | if (csp.isDirective(directive)) { 41 | // Directive is known. 42 | continue; 43 | } 44 | 45 | if (directive.endsWith(':')) { 46 | findings.push(new Finding( 47 | Type.UNKNOWN_DIRECTIVE, 'CSP directives don\'t end with a colon.', 48 | Severity.SYNTAX, directive)); 49 | } else { 50 | findings.push(new Finding( 51 | Type.UNKNOWN_DIRECTIVE, 52 | 'Directive "' + directive + '" is not a known CSP directive.', 53 | Severity.SYNTAX, directive)); 54 | } 55 | } 56 | 57 | return findings; 58 | } 59 | 60 | 61 | /** 62 | * Checks if semicolons are missing in the csp. 63 | * 64 | * Example policy where this check would trigger (missing semicolon before 65 | * start of object-src): 66 | * script-src foo.bar object-src 'none' 67 | * 68 | * @param parsedCsp A parsed csp. 69 | */ 70 | export function checkMissingSemicolon(parsedCsp: Csp): Finding[] { 71 | const findings: Finding[] = []; 72 | 73 | for (const [directive, directiveValues] of Object.entries( 74 | parsedCsp.directives)) { 75 | if (directiveValues === undefined) { 76 | continue; 77 | } 78 | for (const value of directiveValues) { 79 | // If we find a known directive inside a directive value, it is very 80 | // likely that a semicolon was forgoten. 81 | if (csp.isDirective(value)) { 82 | findings.push(new Finding( 83 | Type.MISSING_SEMICOLON, 84 | 'Did you forget the semicolon? ' + 85 | '"' + value + '" seems to be a directive, not a value.', 86 | Severity.SYNTAX, directive, value)); 87 | } 88 | } 89 | } 90 | 91 | return findings; 92 | } 93 | 94 | 95 | /** 96 | * Checks if csp contains invalid keywords. 97 | * 98 | * Example policy where this check would trigger: 99 | * script-src 'notAkeyword' 100 | * 101 | * @param parsedCsp A parsed csp. 102 | */ 103 | export function checkInvalidKeyword(parsedCsp: Csp): Finding[] { 104 | const findings: Finding[] = []; 105 | const keywordsNoTicks = 106 | Object.values(Keyword).map((k) => k.replace(/'/g, '')); 107 | 108 | for (const [directive, directiveValues] of Object.entries( 109 | parsedCsp.directives)) { 110 | if (directiveValues === undefined) { 111 | continue; 112 | } 113 | for (const value of directiveValues) { 114 | // Check if single ticks have been forgotten. 115 | if (keywordsNoTicks.some((k) => k === value) || 116 | value.startsWith('nonce-') || 117 | value.match(/^(sha256|sha384|sha512)-/)) { 118 | findings.push(new Finding( 119 | Type.INVALID_KEYWORD, 120 | 'Did you forget to surround "' + value + '" with single-ticks?', 121 | Severity.SYNTAX, directive, value)); 122 | continue; 123 | } 124 | 125 | // Continue, if the value doesn't start with single tick. 126 | // All CSP keywords start with a single tick. 127 | if (!value.startsWith('\'')) { 128 | continue; 129 | } 130 | 131 | if (directive === csp.Directive.REQUIRE_TRUSTED_TYPES_FOR) { 132 | // Continue, if it's an allowed Trusted Types sink. 133 | if (value === csp.TrustedTypesSink.SCRIPT) { 134 | continue; 135 | } 136 | } else if (directive === csp.Directive.TRUSTED_TYPES) { 137 | // Continue, if it's an allowed Trusted Types keyword. 138 | if (value === '\'allow-duplicates\'' || value === '\'none\'') { 139 | continue; 140 | } 141 | } else { 142 | // Continue, if it's a valid keyword. 143 | if (csp.isKeyword(value) || csp.isHash(value) || csp.isNonce(value)) { 144 | continue; 145 | } 146 | } 147 | 148 | findings.push(new Finding( 149 | Type.INVALID_KEYWORD, value + ' seems to be an invalid CSP keyword.', 150 | Severity.SYNTAX, directive, value)); 151 | } 152 | } 153 | 154 | return findings; 155 | } 156 | 157 | -------------------------------------------------------------------------------- /checks/parser_checks_test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 Google Inc. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @fileoverview Tests for CSP Parser checks. 17 | * @author lwe@google.com (Lukas Weichselbaum) 18 | */ 19 | 20 | import 'jasmine'; 21 | 22 | import {Finding, Severity} from '../finding'; 23 | import {CspParser} from '../parser'; 24 | 25 | import {CheckerFunction} from './checker'; 26 | import * as parserChecks from './parser_checks'; 27 | 28 | /** 29 | * Runs a check on a CSP string. 30 | * 31 | * @param test CSP string. 32 | * @param checkFunction check. 33 | */ 34 | function checkCsp(test: string, checkFunction: CheckerFunction): Finding[] { 35 | const parsedCsp = (new CspParser(test)).csp; 36 | return checkFunction(parsedCsp); 37 | } 38 | 39 | 40 | describe('Test parser checks', () => { 41 | /** Tests for csp.parserChecks.checkUnknownDirective */ 42 | it('CheckUnknownDirective', () => { 43 | const test = 'foobar-src http:'; 44 | 45 | const violations = checkCsp(test, parserChecks.checkUnknownDirective); 46 | expect(violations.length).toBe(1); 47 | expect(violations[0].severity).toBe(Severity.SYNTAX); 48 | expect(violations[0].directive).toBe('foobar-src'); 49 | }); 50 | 51 | /** Tests for csp.parserChecks.checkMissingSemicolon */ 52 | it('CheckMissingSemicolon', () => { 53 | const test = 'default-src foo.bar script-src \'none\''; 54 | 55 | const violations = checkCsp(test, parserChecks.checkMissingSemicolon); 56 | expect(violations.length).toBe(1); 57 | expect(violations[0].severity).toBe(Severity.SYNTAX); 58 | expect(violations[0].value).toBe('script-src'); 59 | }); 60 | 61 | /** Tests for csp.parserChecks.checkInvalidKeyword */ 62 | it('CheckInvalidKeywordForgottenSingleTicks', () => { 63 | const test = 'script-src strict-dynamic nonce-test sha256-asdf'; 64 | 65 | const violations = checkCsp(test, parserChecks.checkInvalidKeyword); 66 | expect(violations.length).toBe(3); 67 | expect(violations.every((v) => v.severity === Severity.SYNTAX)).toBeTrue(); 68 | expect(violations.every((v) => v.description.includes('single-ticks'))) 69 | .toBeTrue(); 70 | }); 71 | 72 | it('CheckInvalidKeywordUnknownKeyword', () => { 73 | const test = 'script-src \'foo-bar\''; 74 | 75 | const violations = checkCsp(test, parserChecks.checkInvalidKeyword); 76 | expect(violations.length).toBe(1); 77 | expect(violations[0].severity).toBe(Severity.SYNTAX); 78 | expect(violations[0].value).toBe('\'foo-bar\''); 79 | }); 80 | 81 | it('CheckInvalidKeywordAllowsRequireTrustedTypesForScript', () => { 82 | const test = 'require-trusted-types-for \'script\''; 83 | 84 | const violations = checkCsp(test, parserChecks.checkInvalidKeyword); 85 | expect(violations.length).toBe(0); 86 | }); 87 | 88 | it('CheckInvalidKeywordAllowsTrustedTypesAllowDuplicateKeyword', () => { 89 | const test = 'trusted-types \'allow-duplicates\' policy1'; 90 | 91 | const violations = checkCsp(test, parserChecks.checkInvalidKeyword); 92 | expect(violations.length).toBe(0); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /checks/security_checks.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Collection of CSP evaluation checks. 3 | * @author lwe@google.com (Lukas Weichselbaum) 4 | * 5 | * @license 6 | * Copyright 2016 Google Inc. All rights reserved. 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | import * as angular from '../allowlist_bypasses/angular'; 21 | import * as flash from '../allowlist_bypasses/flash'; 22 | import * as jsonp from '../allowlist_bypasses/jsonp'; 23 | import * as csp from '../csp'; 24 | import {Csp, Directive, Keyword} from '../csp'; 25 | import {Finding, Severity, Type} from '../finding'; 26 | import * as utils from '../utils'; 27 | 28 | 29 | /** 30 | * A list of CSP directives that can allow XSS vulnerabilities if they fail 31 | * validation. 32 | */ 33 | export const DIRECTIVES_CAUSING_XSS: Directive[] = [ 34 | Directive.SCRIPT_SRC, Directive.SCRIPT_SRC_ATTR, Directive.SCRIPT_SRC_ELEM, 35 | Directive.OBJECT_SRC, Directive.BASE_URI 36 | ]; 37 | 38 | /** 39 | * A list of URL schemes that can allow XSS vulnerabilities when requests to 40 | * them are made. 41 | */ 42 | export const URL_SCHEMES_CAUSING_XSS: string[] = ['data:', 'http:', 'https:']; 43 | 44 | 45 | /** 46 | * Checks if passed csp allows inline scripts. 47 | * Findings of this check are critical and FP free. 48 | * unsafe-inline is ignored in the presence of a nonce or a hash. This check 49 | * does not account for this and therefore the effectiveCsp needs to be passed. 50 | * 51 | * Example policy where this check would trigger: 52 | * script-src 'unsafe-inline' 53 | * 54 | * @param effectiveCsp A parsed csp that only contains values which 55 | * are active in a certain version of CSP (e.g. no unsafe-inline if a nonce 56 | * is present). 57 | */ 58 | export function checkScriptUnsafeInline(effectiveCsp: Csp): Finding[] { 59 | const violations: Finding[] = []; 60 | const directivesToCheck = effectiveCsp.getEffectiveDirectives([ 61 | Directive.SCRIPT_SRC, Directive.SCRIPT_SRC_ATTR, Directive.SCRIPT_SRC_ELEM 62 | ]); 63 | 64 | for (const directive of directivesToCheck) { 65 | const values = effectiveCsp.directives[directive] || []; 66 | if (values.includes(Keyword.UNSAFE_INLINE)) { 67 | violations.push(new Finding( 68 | Type.SCRIPT_UNSAFE_INLINE, 69 | `'unsafe-inline' allows the execution of unsafe in-page scripts ` + 70 | 'and event handlers.', 71 | Severity.HIGH, directive, Keyword.UNSAFE_INLINE)); 72 | } 73 | if (values.includes(Keyword.UNSAFE_HASHES)) { 74 | violations.push(new Finding( 75 | Type.SCRIPT_UNSAFE_HASHES, 76 | `'unsafe-hashes', while safer than 'unsafe-inline', allows the execution of unsafe in-page scripts and event handlers as long as their hashes appear in the CSP. Please refactor them to no longer use inline scripts if possible.`, 77 | Severity.MEDIUM_MAYBE, directive, Keyword.UNSAFE_HASHES)); 78 | } 79 | } 80 | 81 | return violations; 82 | } 83 | 84 | 85 | /** 86 | * Checks if passed csp allows eval in scripts. 87 | * Findings of this check have a medium severity and are FP free. 88 | * 89 | * Example policy where this check would trigger: 90 | * script-src 'unsafe-eval' 91 | * 92 | * @param parsedCsp Parsed CSP. 93 | */ 94 | export function checkScriptUnsafeEval(parsedCsp: Csp): Finding[] { 95 | const violations: Finding[] = []; 96 | const directivesToCheck = parsedCsp.getEffectiveDirectives([ 97 | Directive.SCRIPT_SRC, Directive.SCRIPT_SRC_ATTR, Directive.SCRIPT_SRC_ELEM 98 | ]); 99 | 100 | for (const directive of directivesToCheck) { 101 | const values = parsedCsp.directives[directive] || []; 102 | if (values.includes(Keyword.UNSAFE_EVAL)) { 103 | violations.push(new Finding( 104 | Type.SCRIPT_UNSAFE_EVAL, 105 | `'unsafe-eval' allows the execution of code injected into DOM APIs ` + 106 | 'such as eval().', 107 | Severity.MEDIUM_MAYBE, directive, Keyword.UNSAFE_EVAL)); 108 | } 109 | } 110 | 111 | return violations; 112 | } 113 | 114 | 115 | /** 116 | * Checks if plain URL schemes (e.g. http:) are allowed in sensitive directives. 117 | * Findings of this check have a high severity and are FP free. 118 | * 119 | * Example policy where this check would trigger: 120 | * script-src https: http: data: 121 | * 122 | * @param parsedCsp Parsed CSP. 123 | */ 124 | export function checkPlainUrlSchemes(parsedCsp: Csp): Finding[] { 125 | const violations: Finding[] = []; 126 | const directivesToCheck = 127 | parsedCsp.getEffectiveDirectives(DIRECTIVES_CAUSING_XSS); 128 | 129 | for (const directive of directivesToCheck) { 130 | const values = parsedCsp.directives[directive] || []; 131 | for (const value of values) { 132 | if (URL_SCHEMES_CAUSING_XSS.includes(value)) { 133 | violations.push(new Finding( 134 | Type.PLAIN_URL_SCHEMES, 135 | value + ' URI in ' + directive + ' allows the execution of ' + 136 | 'unsafe scripts.', 137 | Severity.HIGH, directive, value)); 138 | } 139 | } 140 | } 141 | 142 | return violations; 143 | } 144 | 145 | 146 | /** 147 | * Checks if csp contains wildcards in sensitive directives. 148 | * Findings of this check have a high severity and are FP free. 149 | * 150 | * Example policy where this check would trigger: 151 | * script-src * 152 | * 153 | * @param parsedCsp Parsed CSP. 154 | */ 155 | export function checkWildcards(parsedCsp: Csp): Finding[] { 156 | const violations: Finding[] = []; 157 | const directivesToCheck = 158 | parsedCsp.getEffectiveDirectives(DIRECTIVES_CAUSING_XSS); 159 | 160 | for (const directive of directivesToCheck) { 161 | const values = parsedCsp.directives[directive] || []; 162 | for (const value of values) { 163 | const url = utils.getSchemeFreeUrl(value); 164 | if (url === '*') { 165 | violations.push(new Finding( 166 | Type.PLAIN_WILDCARD, directive + ` should not allow '*' as source`, 167 | Severity.HIGH, directive, value)); 168 | continue; 169 | } 170 | } 171 | } 172 | 173 | return violations; 174 | } 175 | 176 | /** 177 | * Checks if object-src is restricted to none either directly or via a 178 | * default-src. 179 | */ 180 | export function checkMissingObjectSrcDirective(parsedCsp: Csp): Finding[] { 181 | let objectRestrictions: string[]|undefined = []; 182 | if (Directive.OBJECT_SRC in parsedCsp.directives) { 183 | objectRestrictions = parsedCsp.directives[Directive.OBJECT_SRC]; 184 | } else if (Directive.DEFAULT_SRC in parsedCsp.directives) { 185 | objectRestrictions = parsedCsp.directives[Directive.DEFAULT_SRC]; 186 | } 187 | if (objectRestrictions !== undefined && objectRestrictions.length >= 1) { 188 | return []; 189 | } 190 | return [new Finding( 191 | Type.MISSING_DIRECTIVES, 192 | `Missing object-src allows the injection of plugins which can execute JavaScript. Can you set it to 'none'?`, 193 | Severity.HIGH, Directive.OBJECT_SRC)]; 194 | } 195 | 196 | /** 197 | * Checks if script-src is restricted either directly or via a default-src. 198 | */ 199 | export function checkMissingScriptSrcDirective(parsedCsp: Csp): Finding[] { 200 | if (Directive.SCRIPT_SRC in parsedCsp.directives || 201 | Directive.DEFAULT_SRC in parsedCsp.directives) { 202 | return []; 203 | } 204 | return [new Finding( 205 | Type.MISSING_DIRECTIVES, 'script-src directive is missing.', 206 | Severity.HIGH, Directive.SCRIPT_SRC)]; 207 | } 208 | 209 | /** 210 | * Checks if the base-uri needs to be restricted and if so, whether it has been 211 | * restricted. 212 | */ 213 | export function checkMissingBaseUriDirective(parsedCsp: Csp): Finding[] { 214 | return checkMultipleMissingBaseUriDirective([parsedCsp]); 215 | } 216 | 217 | /** 218 | * Checks if the base-uri needs to be restricted and if so, whether it has been 219 | * restricted. 220 | */ 221 | export function checkMultipleMissingBaseUriDirective(parsedCsps: Csp[]): 222 | Finding[] { 223 | // base-uri can be used to bypass nonce based CSPs and hash based CSPs that 224 | // use strict dynamic 225 | const needsBaseUri = (csp: Csp) => 226 | (csp.policyHasScriptNonces() || 227 | (csp.policyHasScriptHashes() && csp.policyHasStrictDynamic())); 228 | const hasBaseUri = (csp: Csp) => Directive.BASE_URI in csp.directives; 229 | 230 | if (parsedCsps.some(needsBaseUri) && !parsedCsps.some(hasBaseUri)) { 231 | const description = 'Missing base-uri allows the injection of base tags. ' + 232 | 'They can be used to set the base URL for all relative (script) ' + 233 | 'URLs to an attacker controlled domain. ' + 234 | `Can you set it to 'none' or 'self'?`; 235 | return [new Finding( 236 | Type.MISSING_DIRECTIVES, description, Severity.HIGH, 237 | Directive.BASE_URI)]; 238 | } 239 | return []; 240 | } 241 | 242 | 243 | /** 244 | * Checks if all necessary directives for preventing XSS are set. 245 | * Findings of this check have a high severity and are FP free. 246 | * 247 | * Example policy where this check would trigger: 248 | * script-src 'none' 249 | * 250 | * @param parsedCsp Parsed CSP. 251 | */ 252 | export function checkMissingDirectives(parsedCsp: Csp): Finding[] { 253 | return [ 254 | ...checkMissingObjectSrcDirective(parsedCsp), 255 | ...checkMissingScriptSrcDirective(parsedCsp), 256 | ...checkMissingBaseUriDirective(parsedCsp), 257 | ]; 258 | } 259 | 260 | 261 | /** 262 | * Checks if allowlisted origins are bypassable by JSONP/Angular endpoints. 263 | * High severity findings of this check are FP free. 264 | * 265 | * Example policy where this check would trigger: 266 | * default-src 'none'; script-src www.google.com 267 | * 268 | * @param parsedCsp Parsed CSP. 269 | */ 270 | export function checkScriptAllowlistBypass(parsedCsp: Csp): Finding[] { 271 | const violations: Finding[] = []; 272 | parsedCsp 273 | .getEffectiveDirectives([Directive.SCRIPT_SRC, Directive.SCRIPT_SRC_ELEM]) 274 | .forEach(effectiveScriptSrcDirective => { 275 | const scriptSrcValues = 276 | parsedCsp.directives[effectiveScriptSrcDirective] || []; 277 | if (scriptSrcValues.includes(Keyword.NONE)) { 278 | return; 279 | } 280 | 281 | for (const value of scriptSrcValues) { 282 | if (value === Keyword.SELF) { 283 | violations.push(new Finding( 284 | Type.SCRIPT_ALLOWLIST_BYPASS, 285 | `'self' can be problematic if you host JSONP, AngularJS or user ` + 286 | 'uploaded files.', 287 | Severity.MEDIUM_MAYBE, effectiveScriptSrcDirective, value)); 288 | continue; 289 | } 290 | 291 | // Ignore keywords, nonces and hashes (they start with a single 292 | // quote). 293 | if (value.startsWith('\'')) { 294 | continue; 295 | } 296 | 297 | // Ignore standalone schemes and things that don't look like URLs (no 298 | // dot). 299 | if (csp.isUrlScheme(value) || value.indexOf('.') === -1) { 300 | continue; 301 | } 302 | 303 | const url = '//' + utils.getSchemeFreeUrl(value); 304 | 305 | const angularBypass = utils.matchWildcardUrls(url, angular.URLS); 306 | 307 | let jsonpBypass = utils.matchWildcardUrls(url, jsonp.URLS); 308 | 309 | // Some JSONP bypasses only work in presence of unsafe-eval. 310 | if (jsonpBypass) { 311 | const evalRequired = 312 | jsonp.NEEDS_EVAL.includes(jsonpBypass.hostname); 313 | const evalPresent = scriptSrcValues.includes(Keyword.UNSAFE_EVAL); 314 | if (evalRequired && !evalPresent) { 315 | jsonpBypass = null; 316 | } 317 | } 318 | 319 | if (jsonpBypass || angularBypass) { 320 | let bypassDomain = ''; 321 | let bypassTxt = ''; 322 | if (jsonpBypass) { 323 | bypassDomain = jsonpBypass.hostname; 324 | bypassTxt = ' JSONP endpoints'; 325 | } 326 | if (angularBypass) { 327 | bypassDomain = angularBypass.hostname; 328 | bypassTxt += (bypassTxt.trim() === '') ? '' : ' and'; 329 | bypassTxt += ' Angular libraries'; 330 | } 331 | 332 | violations.push(new Finding( 333 | Type.SCRIPT_ALLOWLIST_BYPASS, 334 | bypassDomain + ' is known to host' + bypassTxt + 335 | ' which allow to bypass this CSP.', 336 | Severity.HIGH, effectiveScriptSrcDirective, value)); 337 | } else { 338 | violations.push(new Finding( 339 | Type.SCRIPT_ALLOWLIST_BYPASS, 340 | `No bypass found; make sure that this URL doesn't serve JSONP ` + 341 | 'replies or Angular libraries.', 342 | Severity.MEDIUM_MAYBE, effectiveScriptSrcDirective, value)); 343 | } 344 | } 345 | }); 346 | 347 | return violations; 348 | } 349 | 350 | 351 | /** 352 | * Checks if allowlisted object-src origins are bypassable. 353 | * Findings of this check have a high severity and are FP free. 354 | * 355 | * Example policy where this check would trigger: 356 | * default-src 'none'; object-src ajax.googleapis.com 357 | * 358 | * @param parsedCsp Parsed CSP. 359 | */ 360 | export function checkFlashObjectAllowlistBypass(parsedCsp: Csp): Finding[] { 361 | const violations = []; 362 | const effectiveObjectSrcDirective = 363 | parsedCsp.getEffectiveDirective(Directive.OBJECT_SRC); 364 | const objectSrcValues = 365 | parsedCsp.directives[effectiveObjectSrcDirective] || []; 366 | 367 | // If flash is not allowed in plugin-types, continue. 368 | const pluginTypes = parsedCsp.directives[Directive.PLUGIN_TYPES]; 369 | if (pluginTypes && !pluginTypes.includes('application/x-shockwave-flash')) { 370 | return []; 371 | } 372 | 373 | for (const value of objectSrcValues) { 374 | // Nothing to do here if 'none'. 375 | if (value === Keyword.NONE) { 376 | return []; 377 | } 378 | 379 | const url = '//' + utils.getSchemeFreeUrl(value); 380 | const flashBypass = utils.matchWildcardUrls(url, flash.URLS); 381 | 382 | if (flashBypass) { 383 | violations.push(new Finding( 384 | Type.OBJECT_ALLOWLIST_BYPASS, 385 | flashBypass.hostname + 386 | ' is known to host Flash files which allow to bypass this CSP.', 387 | Severity.HIGH, effectiveObjectSrcDirective, value)); 388 | } else if (effectiveObjectSrcDirective === Directive.OBJECT_SRC) { 389 | violations.push(new Finding( 390 | Type.OBJECT_ALLOWLIST_BYPASS, 391 | `Can you restrict object-src to 'none' only?`, Severity.MEDIUM_MAYBE, 392 | effectiveObjectSrcDirective, value)); 393 | } 394 | } 395 | 396 | return violations; 397 | } 398 | 399 | /** 400 | * Returns whether the given string "looks" like an IP address. This function 401 | * only uses basic heuristics and does not accept all valid IPs nor reject all 402 | * invalid IPs. 403 | */ 404 | export function looksLikeIpAddress(maybeIp: string): boolean { 405 | if (maybeIp.startsWith('[') && maybeIp.endsWith(']')) { 406 | // Looks like an IPv6 address and not a hostname (though it may be some 407 | // nonsense like `[foo]`) 408 | return true; 409 | } 410 | if (/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/.test(maybeIp)) { 411 | // Looks like an IPv4 address (though it may be something like 412 | // `500.600.700.800` 413 | return true; 414 | } 415 | // Won't match IP addresses encoded in other manners (eg octal or 416 | // decimal) 417 | return false; 418 | } 419 | 420 | /** 421 | * Checks if csp contains IP addresses. 422 | * Findings of this check are informal only and are FP free. 423 | * 424 | * Example policy where this check would trigger: 425 | * script-src 127.0.0.1 426 | * 427 | * @param parsedCsp Parsed CSP. 428 | */ 429 | export function checkIpSource(parsedCsp: Csp): Finding[] { 430 | const violations: Finding[] = []; 431 | 432 | // Function for checking if directive values contain IP addresses. 433 | const checkIp = (directive: string, directiveValues: string[]) => { 434 | for (const value of directiveValues) { 435 | const host = utils.getHostname(value); 436 | if (looksLikeIpAddress(host)) { 437 | // Check if localhost. 438 | // See 4.8 in https://www.w3.org/TR/CSP2/#match-source-expression 439 | if (host === '127.0.0.1') { 440 | violations.push(new Finding( 441 | Type.IP_SOURCE, 442 | directive + ' directive allows localhost as source. ' + 443 | 'Please make sure to remove this in production environments.', 444 | Severity.INFO, directive, value)); 445 | } else { 446 | violations.push(new Finding( 447 | Type.IP_SOURCE, 448 | directive + ' directive has an IP-Address as source: ' + host + 449 | ' (will be ignored by browsers!). ', 450 | Severity.INFO, directive, value)); 451 | } 452 | } 453 | } 454 | }; 455 | 456 | // Apply check to values of all directives. 457 | utils.applyCheckFunktionToDirectives(parsedCsp, checkIp); 458 | return violations; 459 | } 460 | 461 | 462 | /** 463 | * Checks if csp contains directives that are deprecated in CSP3. 464 | * Findings of this check are informal only and are FP free. 465 | * 466 | * Example policy where this check would trigger: 467 | * report-uri foo.bar/csp 468 | * 469 | * @param parsedCsp Parsed CSP. 470 | */ 471 | export function checkDeprecatedDirective(parsedCsp: Csp): Finding[] { 472 | const violations = []; 473 | 474 | // More details: https://www.chromestatus.com/feature/5769374145183744 475 | if (Directive.REFLECTED_XSS in parsedCsp.directives) { 476 | violations.push(new Finding( 477 | Type.DEPRECATED_DIRECTIVE, 478 | 'reflected-xss is deprecated since CSP2. ' + 479 | 'Please, use the X-XSS-Protection header instead.', 480 | Severity.INFO, Directive.REFLECTED_XSS)); 481 | } 482 | 483 | // More details: https://www.chromestatus.com/feature/5680800376815616 484 | if (Directive.REFERRER in parsedCsp.directives) { 485 | violations.push(new Finding( 486 | Type.DEPRECATED_DIRECTIVE, 487 | 'referrer is deprecated since CSP2. ' + 488 | 'Please, use the Referrer-Policy header instead.', 489 | Severity.INFO, Directive.REFERRER)); 490 | } 491 | 492 | // More details: https://github.com/w3c/webappsec-csp/pull/327 493 | if (Directive.DISOWN_OPENER in parsedCsp.directives) { 494 | violations.push(new Finding( 495 | Type.DEPRECATED_DIRECTIVE, 496 | 'disown-opener is deprecated since CSP3. ' + 497 | 'Please, use the Cross Origin Opener Policy header instead.', 498 | Severity.INFO, Directive.DISOWN_OPENER)); 499 | } 500 | 501 | // More details: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/prefetch-src 502 | if (Directive.PREFETCH_SRC in parsedCsp.directives) { 503 | violations.push(new Finding( 504 | Type.DEPRECATED_DIRECTIVE, 505 | 'prefetch-src is deprecated since CSP3. ' + 506 | 'Be aware that this feature may cease to work at any time.', 507 | Severity.INFO, Directive.PREFETCH_SRC)); 508 | } 509 | return violations; 510 | } 511 | 512 | 513 | /** 514 | * Checks if csp nonce is at least 8 characters long. 515 | * Findings of this check are of medium severity and are FP free. 516 | * 517 | * Example policy where this check would trigger: 518 | * script-src 'nonce-short' 519 | * 520 | * @param parsedCsp Parsed CSP. 521 | */ 522 | export function checkNonceLength(parsedCsp: Csp): Finding[] { 523 | const noncePattern = new RegExp('^\'nonce-(.+)\'$'); 524 | const violations: Finding[] = []; 525 | 526 | utils.applyCheckFunktionToDirectives( 527 | parsedCsp, (directive, directiveValues) => { 528 | for (const value of directiveValues) { 529 | const match = value.match(noncePattern); 530 | if (!match) { 531 | continue; 532 | } 533 | // Not a nonce. 534 | 535 | const nonceValue = match[1]; 536 | if (nonceValue.length < 8) { 537 | violations.push(new Finding( 538 | Type.NONCE_LENGTH, 539 | 'Nonces should be at least 8 characters long.', Severity.MEDIUM, 540 | directive, value)); 541 | } 542 | 543 | if (!csp.isNonce(value, true)) { 544 | violations.push(new Finding( 545 | Type.NONCE_CHARSET, 546 | 'Nonces should only use the base64 charset.', Severity.INFO, 547 | directive, value)); 548 | } 549 | } 550 | }); 551 | 552 | return violations; 553 | } 554 | 555 | 556 | /** 557 | * Checks if CSP allows sourcing from http:// 558 | * Findings of this check are of medium severity and are FP free. 559 | * 560 | * Example policy where this check would trigger: 561 | * report-uri http://foo.bar/csp 562 | * 563 | * @param parsedCsp Parsed CSP. 564 | */ 565 | export function checkSrcHttp(parsedCsp: Csp): Finding[] { 566 | const violations: Finding[] = []; 567 | 568 | utils.applyCheckFunktionToDirectives( 569 | parsedCsp, (directive, directiveValues) => { 570 | for (const value of directiveValues) { 571 | const description = directive === Directive.REPORT_URI ? 572 | 'Use HTTPS to send violation reports securely.' : 573 | 'Allow only resources downloaded over HTTPS.'; 574 | if (value.startsWith('http://')) { 575 | violations.push(new Finding( 576 | Type.SRC_HTTP, description, Severity.MEDIUM, directive, value)); 577 | } 578 | } 579 | }); 580 | 581 | return violations; 582 | } 583 | 584 | /** 585 | * Checks if the policy has configured reporting in a robust manner. 586 | */ 587 | export function checkHasConfiguredReporting(parsedCsp: Csp): Finding[] { 588 | const reportUriValues: string[] = 589 | parsedCsp.directives[Directive.REPORT_URI] || []; 590 | if (reportUriValues.length > 0) { 591 | return []; 592 | } 593 | 594 | const reportToValues: string[] = 595 | parsedCsp.directives[Directive.REPORT_TO] || []; 596 | if (reportToValues.length > 0) { 597 | return [new Finding( 598 | Type.REPORT_TO_ONLY, 599 | `This CSP policy only provides a reporting destination via the 'report-to' directive. This directive is only supported in Chromium-based browsers so it is recommended to also use a 'report-uri' directive.`, 600 | Severity.INFO, Directive.REPORT_TO)]; 601 | } 602 | 603 | return [new Finding( 604 | Type.REPORTING_DESTINATION_MISSING, 605 | 'This CSP policy does not configure a reporting destination. This makes it difficult to maintain the CSP policy over time and monitor for any breakages.', 606 | Severity.INFO, Directive.REPORT_URI)]; 607 | } 608 | -------------------------------------------------------------------------------- /checks/security_checks_test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 Google Inc. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @fileoverview Tests for CSP Evaluator Checks. 17 | * @author lwe@google.com (Lukas Weichselbaum) 18 | */ 19 | 20 | 21 | import {Directive, Version} from '../csp'; 22 | import {Finding, Severity} from '../finding'; 23 | import {CspParser} from '../parser'; 24 | 25 | import {CheckerFunction} from './checker'; 26 | import * as securityChecks from './security_checks'; 27 | 28 | /** 29 | * Helper function for running a check on a CSP string. 30 | * 31 | * @param test CSP string. 32 | * @param checkFunction check. 33 | */ 34 | function checkCsp(test: string, checkFunction: CheckerFunction): Finding[] { 35 | const parsedCsp = (new CspParser(test)).csp; 36 | return checkFunction(parsedCsp); 37 | } 38 | 39 | describe('Test security checks', () => { 40 | /** Tests for csp.securityChecks.checkScriptUnsafeInline */ 41 | it('CheckScriptUnsafeInlineInScriptSrc', () => { 42 | const test = 43 | 'default-src https:; script-src \'unsafe-inline\'; script-src-elem \'unsafe-inline\';'; 44 | 45 | const violations = checkCsp(test, securityChecks.checkScriptUnsafeInline); 46 | expect(violations.length).toBe(2); 47 | expect(violations[0].severity).toBe(Severity.HIGH); 48 | }); 49 | 50 | it('CheckScriptUnsafeInlineInDefaultSrc', () => { 51 | const test = 'default-src \'unsafe-inline\''; 52 | 53 | const violations = checkCsp(test, securityChecks.checkScriptUnsafeInline); 54 | expect(violations.length).toBe(1); 55 | }); 56 | 57 | it('CheckScriptUnsafeHashesInScriptSrc', () => { 58 | const test = 59 | 'script-src \'unsafe-hashes\' \'sha256-1DCfk1NYWuHMfoobarfoobar=\''; 60 | 61 | const violations = checkCsp(test, securityChecks.checkScriptUnsafeInline); 62 | expect(violations.length).toBe(1); 63 | expect(violations[0].severity).toBe(Severity.MEDIUM_MAYBE); 64 | }); 65 | 66 | it('CheckScriptUnsafeInlineInDefaultSrcAndNotInScriptSrc', () => { 67 | const test = 68 | 'default-src \'unsafe-inline\'; script-src https:; script-src-attr https:; script-src-elem https:'; 69 | 70 | const violations = checkCsp(test, securityChecks.checkScriptUnsafeInline); 71 | expect(violations.length).toBe(0); 72 | }); 73 | 74 | it('CheckScriptUnsafeInlineWithNonce', () => { 75 | const test = 76 | 'script-src \'unsafe-inline\' \'nonce-foobar\'; script-src-elem \'unsafe-inline\' \'nonce-foobar\';'; 77 | const parsedCsp = (new CspParser(test)).csp; 78 | 79 | let effectiveCsp = parsedCsp.getEffectiveCsp(Version.CSP1); 80 | let violations = securityChecks.checkScriptUnsafeInline(effectiveCsp); 81 | // script-src-elem and script-src-attr are ignored 82 | expect(violations.length).toBe(1); 83 | 84 | effectiveCsp = parsedCsp.getEffectiveCsp(Version.CSP3); 85 | violations = securityChecks.checkScriptUnsafeInline(effectiveCsp); 86 | expect(violations.length).toBe(0); 87 | }); 88 | 89 | /** Tests for csp.securityChecks.checkScriptUnsafeEval */ 90 | it('CheckScriptUnsafeEvalInScriptSrc', () => { 91 | const test = 92 | 'default-src https:; script-src \'unsafe-eval\'; script-src-attr \'unsafe-eval\'; script-src-elem \'unsafe-eval\';'; 93 | 94 | const violations = checkCsp(test, securityChecks.checkScriptUnsafeEval); 95 | expect(violations.length).toBe(3); 96 | expect(violations.every((v) => v.severity === Severity.MEDIUM_MAYBE)) 97 | .toBeTrue(); 98 | }); 99 | 100 | it('CheckScriptUnsafeEvalInDefaultSrc', () => { 101 | const test = 'default-src \'unsafe-eval\''; 102 | 103 | const violations = checkCsp(test, securityChecks.checkScriptUnsafeEval); 104 | expect(violations.length).toBe(1); 105 | }); 106 | 107 | /** Tests for csp.securityChecks.checkPlainUrlSchemes */ 108 | it('CheckPlainUrlSchemesInScriptSrc', () => { 109 | const test = 'script-src data: http: https: sthInvalid:'; 110 | 111 | const violations = checkCsp(test, securityChecks.checkPlainUrlSchemes); 112 | expect(violations.length).toBe(3); 113 | expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); 114 | }); 115 | 116 | it('CheckPlainUrlSchemesInObjectSrc', () => { 117 | const test = 'object-src data: http: https: sthInvalid:'; 118 | 119 | const violations = checkCsp(test, securityChecks.checkPlainUrlSchemes); 120 | expect(violations.length).toBe(3); 121 | expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); 122 | }); 123 | 124 | it('CheckPlainUrlSchemesInBaseUri', () => { 125 | const test = 'base-uri data: http: https: sthInvalid:'; 126 | 127 | const violations = checkCsp(test, securityChecks.checkPlainUrlSchemes); 128 | expect(violations.length).toBe(3); 129 | expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); 130 | }); 131 | 132 | it('CheckPlainUrlSchemesMixed', () => { 133 | const test = 'default-src https:; object-src data: sthInvalid:'; 134 | 135 | const violations = checkCsp(test, securityChecks.checkPlainUrlSchemes); 136 | expect(violations.length).toBe(2); 137 | expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); 138 | expect(violations[0].directive).toBe(Directive.DEFAULT_SRC); 139 | expect(violations[1].directive).toBe(Directive.OBJECT_SRC); 140 | }); 141 | 142 | it('CheckPlainUrlSchemesDangerousDirectivesOK', () => { 143 | const test = 144 | 'default-src https:; object-src \'none\'; script-src \'none\'; ' + 145 | 'base-uri \'none\''; 146 | 147 | const violations = checkCsp(test, securityChecks.checkPlainUrlSchemes); 148 | expect(violations.length).toBe(0); 149 | }); 150 | 151 | /** Tests for csp.securityChecks.checkWildcards */ 152 | it('CheckWildcardsInScriptSrc', () => { 153 | const test = 154 | 'script-src * http://* //*; script-src-attr * http://* //*; script-src-elem * http://* //*'; 155 | const violations = checkCsp(test, securityChecks.checkWildcards); 156 | expect(violations.length).toBe(9); 157 | expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); 158 | }); 159 | 160 | it('CheckWildcardsInObjectSrc', () => { 161 | const test = 'object-src * http://* //*'; 162 | const violations = checkCsp(test, securityChecks.checkWildcards); 163 | expect(violations.length).toBe(3); 164 | expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); 165 | }); 166 | 167 | it('CheckWildcardsInBaseUri', () => { 168 | const test = 'base-uri * http://* //*'; 169 | const violations = checkCsp(test, securityChecks.checkWildcards); 170 | expect(violations.length).toBe(3); 171 | expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); 172 | }); 173 | 174 | it('CheckWildcardsSchemesMixed', () => { 175 | const test = 'default-src *; object-src * ignore.me.com'; 176 | const violations = checkCsp(test, securityChecks.checkWildcards); 177 | expect(violations.length).toBe(2); 178 | expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); 179 | expect(violations[0].directive).toBe(Directive.DEFAULT_SRC); 180 | expect(violations[1].directive).toBe(Directive.OBJECT_SRC); 181 | }); 182 | 183 | it('CheckWildcardsDangerousDirectivesOK', () => { 184 | const test = 'default-src *; object-src *.foo.bar; script-src \'none\'; ' + 185 | 'base-uri \'none\''; 186 | const violations = checkCsp(test, securityChecks.checkWildcards); 187 | expect(violations.length).toBe(0); 188 | }); 189 | 190 | /** Tests for csp.securityChecks.checkMissingDirectives */ 191 | 192 | it('CheckMissingDirectivesMissingObjectSrc', () => { 193 | const test = 'script-src \'none\''; 194 | 195 | const violations = checkCsp(test, securityChecks.checkMissingDirectives); 196 | expect(violations.length).toBe(1); 197 | expect(violations[0].severity).toBe(Severity.HIGH); 198 | }); 199 | 200 | it('CheckMissingDirectivesMissingScriptSrc', () => { 201 | const test = 'object-src \'none\''; 202 | 203 | const violations = checkCsp(test, securityChecks.checkMissingDirectives); 204 | expect(violations.length).toBe(1); 205 | expect(violations[0].severity).toBe(Severity.HIGH); 206 | }); 207 | 208 | it('CheckMissingDirectivesObjectSrcSelf', () => { 209 | const test = 'object-src \'self\''; 210 | 211 | const violations = checkCsp(test, securityChecks.checkMissingDirectives); 212 | expect(violations.length).toBe(1); 213 | expect(violations[0].severity).toBe(Severity.HIGH); 214 | }); 215 | 216 | it('CheckMissingDirectivesMissingBaseUriInNonceCsp', () => { 217 | const test = 'script-src \'nonce-123\'; object-src \'none\''; 218 | 219 | const violations = checkCsp(test, securityChecks.checkMissingDirectives); 220 | expect(violations.length).toBe(1); 221 | expect(violations[0].severity).toBe(Severity.HIGH); 222 | }); 223 | 224 | it('CheckMissingDirectivesMissingBaseUriInHashWStrictDynamicCsp', () => { 225 | const test = 226 | 'script-src \'sha256-123456\' \'strict-dynamic\'; object-src \'none\''; 227 | 228 | const violations = checkCsp(test, securityChecks.checkMissingDirectives); 229 | expect(violations.length).toBe(1); 230 | expect(violations[0].severity).toBe(Severity.HIGH); 231 | }); 232 | 233 | it('CheckMissingDirectivesMissingBaseUriInHashCsp', () => { 234 | const test = 'script-src \'sha256-123456\'; object-src \'none\''; 235 | 236 | const violations = checkCsp(test, securityChecks.checkMissingDirectives); 237 | expect(violations.length).toBe(0); 238 | }); 239 | 240 | it('CheckMissingDirectivesScriptAndObjectSrcSet', () => { 241 | const test = 'script-src \'none\'; object-src \'none\''; 242 | 243 | const violations = checkCsp(test, securityChecks.checkMissingDirectives); 244 | expect(violations.length).toBe(0); 245 | }); 246 | 247 | it('CheckMissingDirectivesDefaultSrcSet', () => { 248 | const test = 'default-src https:;'; 249 | 250 | const violations = checkCsp(test, securityChecks.checkMissingDirectives); 251 | expect(violations.length).toBe(0); 252 | }); 253 | 254 | it('CheckMissingDirectivesDefaultSrcSetToNone', () => { 255 | const test = 'default-src \'none\';'; 256 | 257 | const violations = checkCsp(test, securityChecks.checkMissingDirectives); 258 | expect(violations.length).toBe(0); 259 | }); 260 | 261 | /** Tests for csp.securityChecks.checkScriptAllowlistBypass */ 262 | 263 | 264 | it('checkScriptAllowlistBypassJSONPBypass', () => { 265 | const test = 'script-src *.google.com'; 266 | 267 | const violations = 268 | checkCsp(test, securityChecks.checkScriptAllowlistBypass); 269 | expect(violations.length).toBe(1); 270 | expect(violations[0].severity).toBe(Severity.HIGH); 271 | expect(violations[0].description.includes( 272 | 'www.google.com is known to host JSONP endpoints which')) 273 | .toBeTrue(); 274 | }); 275 | 276 | it('checkScriptAllowlistBypassWithNoneAndJSONPBypass', () => { 277 | const test = 278 | 'script-src *.google.com \'none\'; script-src-elem *.google.com \'none\''; 279 | 280 | const violations = 281 | checkCsp(test, securityChecks.checkScriptAllowlistBypass); 282 | expect(violations.length).toBe(0); 283 | }); 284 | 285 | it('checkScriptAllowlistBypassJSONPBypassEvalRequired', () => { 286 | const test = 287 | 'script-src https://googletagmanager.com \'unsafe-eval\'; script-src-elem https://googletagmanager.com \'unsafe-eval\''; 288 | 289 | const violations = 290 | checkCsp(test, securityChecks.checkScriptAllowlistBypass); 291 | expect(violations.length).toBe(2); 292 | expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); 293 | }); 294 | 295 | it('checkScriptAllowlistBypassJSONPBypassEvalRequiredNotPresent', () => { 296 | const test = 297 | 'script-src https://googletagmanager.com; script-src-elem https://googletagmanager.com;'; 298 | 299 | const violations = 300 | checkCsp(test, securityChecks.checkScriptAllowlistBypass); 301 | expect(violations.length).toBe(2); 302 | expect(violations.every((v) => v.severity === Severity.MEDIUM_MAYBE)) 303 | .toBeTrue(); 304 | }); 305 | 306 | it('checkScriptAllowlistBypassAngularBypass', () => { 307 | const test = 'script-src gstatic.com; script-src-elem gstatic.com'; 308 | 309 | const violations = 310 | checkCsp(test, securityChecks.checkScriptAllowlistBypass); 311 | expect(violations.length).toBe(2); 312 | expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); 313 | expect(violations.every( 314 | (v) => v.description.includes( 315 | 'gstatic.com is known to host Angular libraries which'))) 316 | .toBeTrue(); 317 | }); 318 | 319 | it('checkScriptAllowlistBypassNoBypassWarningOnly', () => { 320 | const test = 'script-src foo.bar; script-src-elem foo.bar'; 321 | 322 | const violations = 323 | checkCsp(test, securityChecks.checkScriptAllowlistBypass); 324 | expect(violations.length).toBe(2); 325 | expect(violations.every((v) => v.severity === Severity.MEDIUM_MAYBE)) 326 | .toBeTrue(); 327 | }); 328 | 329 | it('checkScriptAllowlistBypassNoBypassSelfWarningOnly', () => { 330 | const test = 331 | 'script-src \'self\'; script-src-attr \'self\'; script-src-elem \'self\''; 332 | 333 | const violations = 334 | checkCsp(test, securityChecks.checkScriptAllowlistBypass); 335 | expect(violations.length).toBe(2); 336 | expect(violations.every((v) => v.severity === Severity.MEDIUM_MAYBE)) 337 | .toBeTrue(); 338 | }); 339 | 340 | /** Tests for csp.securityChecks.checkFlashObjectAllowlistBypass */ 341 | 342 | 343 | it('checkFlashObjectAllowlistBypassFlashBypass', () => { 344 | const test = 'object-src https://*.googleapis.com'; 345 | const violations = 346 | checkCsp(test, securityChecks.checkFlashObjectAllowlistBypass); 347 | expect(violations.length).toBe(1); 348 | expect(violations[0].severity).toBe(Severity.HIGH); 349 | }); 350 | 351 | it('checkFlashObjectAllowlistBypassNoFlashBypass', () => { 352 | const test = 'object-src https://foo.bar'; 353 | const violations = 354 | checkCsp(test, securityChecks.checkFlashObjectAllowlistBypass); 355 | expect(violations.length).toBe(1); 356 | expect(violations[0].severity).toBe(Severity.MEDIUM_MAYBE); 357 | }); 358 | 359 | it('checkFlashObjectAllowlistBypassSelfAllowed', () => { 360 | const test = 'object-src \'self\''; 361 | const violations = 362 | checkCsp(test, securityChecks.checkFlashObjectAllowlistBypass); 363 | expect(violations.length).toBe(1); 364 | expect(violations[0].severity).toBe(Severity.MEDIUM_MAYBE); 365 | expect(violations[0].description) 366 | .toBe('Can you restrict object-src to \'none\' only?'); 367 | }); 368 | 369 | /** Tests for csp.securityChecks.checkIpSource */ 370 | it('CheckIpSource', () => { 371 | const test = 372 | 'script-src 8.8.8.8; font-src //127.0.0.1 https://[::1] not.an.ip'; 373 | 374 | const violations = checkCsp(test, securityChecks.checkIpSource); 375 | expect(violations.length).toBe(3); 376 | expect(violations.every((v) => v.severity === Severity.INFO)).toBeTrue(); 377 | }); 378 | 379 | it('LooksLikeIpAddressIPv4', () => { 380 | expect(securityChecks.looksLikeIpAddress('8.8.8.8')).toBeTrue(); 381 | }); 382 | 383 | it('LooksLikeIpAddressIPv6', () => { 384 | expect(securityChecks.looksLikeIpAddress('[::1]')).toBeTrue(); 385 | }); 386 | 387 | it('CheckDeprecatedDirectiveReportUriWithReportTo', () => { 388 | const test = 'report-uri foo.bar/csp;report-to abc'; 389 | 390 | const violations = checkCsp(test, securityChecks.checkDeprecatedDirective); 391 | expect(violations.length).toBe(0); 392 | }); 393 | 394 | it('CheckDeprecatedDirectiveWithoutReportUriButWithReportTo', () => { 395 | const test = 'report-to abc'; 396 | 397 | const violations = checkCsp(test, securityChecks.checkDeprecatedDirective); 398 | expect(violations.length).toBe(0); 399 | }); 400 | 401 | it('CheckDeprecatedDirectiveReflectedXss', () => { 402 | const test = 'reflected-xss block'; 403 | 404 | const violations = checkCsp(test, securityChecks.checkDeprecatedDirective); 405 | expect(violations.length).toBe(1); 406 | expect(violations[0].severity).toBe(Severity.INFO); 407 | }); 408 | 409 | it('CheckDeprecatedDirectiveReferrer', () => { 410 | const test = 'referrer origin'; 411 | 412 | const violations = checkCsp(test, securityChecks.checkDeprecatedDirective); 413 | expect(violations.length).toBe(1); 414 | expect(violations[0].severity).toBe(Severity.INFO); 415 | }); 416 | 417 | it('CheckDeprecatedDirectivePrefetchSrc', () => { 418 | const test = 'prefetch-src test'; 419 | 420 | const violations = checkCsp(test, securityChecks.checkDeprecatedDirective); 421 | expect(violations.length).toBe(1); 422 | expect(violations[0].severity).toBe(Severity.INFO); 423 | }); 424 | 425 | /** Tests for csp.securityChecks.checkNonceLength */ 426 | it('CheckNonceLengthWithLongNonce', () => { 427 | const test = 'script-src \'nonce-veryLongRandomNonce\''; 428 | 429 | const violations = checkCsp(test, securityChecks.checkNonceLength); 430 | expect(violations.length).toBe(0); 431 | }); 432 | 433 | it('CheckNonceLengthWithShortNonce', () => { 434 | const test = 'script-src \'nonce-short\''; 435 | 436 | const violations = checkCsp(test, securityChecks.checkNonceLength); 437 | expect(violations.length).toBe(1); 438 | expect(violations[0].severity).toBe(Severity.MEDIUM); 439 | }); 440 | 441 | it('CheckNonceLengthInvalidCharset', () => { 442 | const test = 'script-src \'nonce-***notBase64***\''; 443 | 444 | const violations = checkCsp(test, securityChecks.checkNonceLength); 445 | expect(violations.length).toBe(1); 446 | expect(violations[0].severity).toBe(Severity.INFO); 447 | }); 448 | 449 | /** Tests for csp.securityChecks.checkSrcHttp */ 450 | it('CheckSrcHttp', () => { 451 | const test = 452 | 'script-src http://foo.bar https://test.com; report-uri http://test.com'; 453 | 454 | const violations = checkCsp(test, securityChecks.checkSrcHttp); 455 | expect(violations.length).toBe(2); 456 | expect(violations.every((v) => v.severity === Severity.MEDIUM)).toBeTrue(); 457 | }); 458 | 459 | /** Tests for csp.securityChecks.checkHasConfiguredReporting */ 460 | it('CheckHasConfiguredReporting_whenNoReporting', () => { 461 | const test = 'script-src \'nonce-aaaaaaaaaa\''; 462 | 463 | const violations = 464 | checkCsp(test, securityChecks.checkHasConfiguredReporting); 465 | 466 | expect(violations.length).toBe(1); 467 | expect(violations[0].severity).toBe(Severity.INFO); 468 | expect(violations[0].directive).toBe('report-uri'); 469 | }); 470 | 471 | it('CheckHasConfiguredReporting_whenOnlyReportTo', () => { 472 | const test = 'script-src \'nonce-aaaaaaaaaa\'; report-to name'; 473 | 474 | const violations = 475 | checkCsp(test, securityChecks.checkHasConfiguredReporting); 476 | 477 | expect(violations.length).toBe(1); 478 | expect(violations[0].severity).toBe(Severity.INFO); 479 | expect(violations[0].directive).toBe('report-to'); 480 | }); 481 | 482 | it('CheckHasConfiguredReporting_whenOnlyReportUri', () => { 483 | const test = 'script-src \'nonce-aaaaaaaaaa\'; report-uri url'; 484 | 485 | const violations = 486 | checkCsp(test, securityChecks.checkHasConfiguredReporting); 487 | 488 | expect(violations.length).toBe(0); 489 | }); 490 | 491 | it('CheckHasConfiguredReporting_whenReportUriAndReportTo', () => { 492 | const test = 493 | 'script-src \'nonce-aaaaaaaaaa\'; report-uri url; report-to name'; 494 | 495 | const violations = 496 | checkCsp(test, securityChecks.checkHasConfiguredReporting); 497 | 498 | expect(violations.length).toBe(0); 499 | }); 500 | 501 | // The nonce that is present in the script-src directive should not override 502 | // the unsafe-inline in the script-src-elem directive. 503 | it('checkScriptUnsafeInline_notOverriddenForScriptSrcElem', () => { 504 | const test = 505 | 'script-src \'nonce-aaaaaaaaaa\' strict-dynamic; script-src-elem \'unsafe-inline\';'; 506 | 507 | // Make sure there are no ignored directives. 508 | const parsedCsp = (new CspParser(test)).csp; 509 | const findings: Finding[] = []; 510 | const effectiveCsp = parsedCsp.getEffectiveCsp(Version.CSP3, findings); 511 | expect(findings.length).toBe(0); 512 | 513 | // And make sure testing the normalized CSP does not have any unexpected 514 | // violations. 515 | const violations = checkCsp( 516 | effectiveCsp.convertToString(), securityChecks.checkScriptUnsafeInline); 517 | expect(violations.length).toBe(1); 518 | expect(violations[0].severity).toBe(Severity.HIGH); 519 | }); 520 | }); 521 | -------------------------------------------------------------------------------- /checks/strictcsp_checks.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Collection of "strict" CSP and backward compatibility checks. 3 | * A "strict" CSP is based on nonces or hashes and drops the allowlist. 4 | * These checks ensure that 'strict-dynamic' and a CSP nonce/hash are present. 5 | * Due to 'strict-dynamic' any allowlist will get dropped in CSP3. 6 | * The backward compatibility checks ensure that the strict nonce/hash based CSP 7 | * will be a no-op in older browsers by checking for presence of 'unsafe-inline' 8 | * (will be dropped in newer browsers if a nonce or hash is present) and for 9 | * prsensence of http: and https: url schemes (will be droped in the presence of 10 | * 'strict-dynamic' in newer browsers). 11 | * 12 | * @author lwe@google.com (Lukas Weichselbaum) 13 | * 14 | * @license 15 | * Copyright 2016 Google Inc. All rights reserved. 16 | * Licensed under the Apache License, Version 2.0 (the "License"); 17 | * you may not use this file except in compliance with the License. 18 | * You may obtain a copy of the License at 19 | * 20 | * http://www.apache.org/licenses/LICENSE-2.0 21 | * 22 | * Unless required by applicable law or agreed to in writing, software 23 | * distributed under the License is distributed on an "AS IS" BASIS, 24 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 25 | * See the License for the specific language governing permissions and 26 | * limitations under the License. 27 | */ 28 | 29 | import * as csp from '../csp'; 30 | import {Csp, Keyword} from '../csp'; 31 | 32 | import {Finding, Severity, Type} from '../finding'; 33 | 34 | 35 | /** 36 | * Checks if 'strict-dynamic' is present. 37 | * 38 | * Example policy where this check would trigger: 39 | * script-src foo.bar 40 | * 41 | * @param parsedCsp A parsed csp. 42 | */ 43 | export function checkStrictDynamic(parsedCsp: Csp): Finding[] { 44 | const directiveName = 45 | parsedCsp.getEffectiveDirective(csp.Directive.SCRIPT_SRC); 46 | const values: string[] = parsedCsp.directives[directiveName] || []; 47 | 48 | const schemeOrHostPresent = values.some((v) => !v.startsWith('\'')); 49 | 50 | // Check if strict-dynamic is present in case a host/scheme allowlist is used. 51 | if (schemeOrHostPresent && !values.includes(Keyword.STRICT_DYNAMIC)) { 52 | return [new Finding( 53 | Type.STRICT_DYNAMIC, 54 | 'Host allowlists can frequently be bypassed. Consider using ' + 55 | '\'strict-dynamic\' in combination with CSP nonces or hashes.', 56 | Severity.STRICT_CSP, directiveName)]; 57 | } 58 | 59 | return []; 60 | } 61 | 62 | 63 | /** 64 | * Checks if 'strict-dynamic' is only used together with a nonce or a hash. 65 | * 66 | * Example policy where this check would trigger: 67 | * script-src 'strict-dynamic' 68 | * 69 | * @param parsedCsp A parsed csp. 70 | */ 71 | export function checkStrictDynamicNotStandalone(parsedCsp: Csp): Finding[] { 72 | const directiveName = 73 | parsedCsp.getEffectiveDirective(csp.Directive.SCRIPT_SRC); 74 | const values: string[] = parsedCsp.directives[directiveName] || []; 75 | 76 | if (values.includes(Keyword.STRICT_DYNAMIC) && 77 | (!parsedCsp.policyHasScriptNonces() && 78 | !parsedCsp.policyHasScriptHashes())) { 79 | return [new Finding( 80 | Type.STRICT_DYNAMIC_NOT_STANDALONE, 81 | '\'strict-dynamic\' without a CSP nonce/hash will block all scripts.', 82 | Severity.INFO, directiveName)]; 83 | } 84 | 85 | return []; 86 | } 87 | 88 | 89 | /** 90 | * Checks if the policy has 'unsafe-inline' when a nonce or hash are present. 91 | * This will ensure backward compatibility to browser that don't support 92 | * CSP nonces or hasehs. 93 | * 94 | * Example policy where this check would trigger: 95 | * script-src 'nonce-test' 96 | * 97 | * @param parsedCsp A parsed csp. 98 | */ 99 | export function checkUnsafeInlineFallback(parsedCsp: Csp): Finding[] { 100 | if (!parsedCsp.policyHasScriptNonces() && 101 | !parsedCsp.policyHasScriptHashes()) { 102 | return []; 103 | } 104 | 105 | const directiveName = 106 | parsedCsp.getEffectiveDirective(csp.Directive.SCRIPT_SRC); 107 | const values: string[] = parsedCsp.directives[directiveName] || []; 108 | 109 | if (!values.includes(Keyword.UNSAFE_INLINE)) { 110 | return [new Finding( 111 | Type.UNSAFE_INLINE_FALLBACK, 112 | 'Consider adding \'unsafe-inline\' (ignored by browsers supporting ' + 113 | 'nonces/hashes) to be backward compatible with older browsers.', 114 | Severity.STRICT_CSP, directiveName)]; 115 | } 116 | 117 | return []; 118 | } 119 | 120 | 121 | /** 122 | * Checks if the policy has an allowlist fallback (* or http: and https:) when 123 | * 'strict-dynamic' is present. 124 | * This will ensure backward compatibility to browser that don't support 125 | * 'strict-dynamic'. 126 | * 127 | * Example policy where this check would trigger: 128 | * script-src 'nonce-test' 'strict-dynamic' 129 | * 130 | * @param parsedCsp A parsed csp. 131 | */ 132 | export function checkAllowlistFallback(parsedCsp: Csp): Finding[] { 133 | const directiveName = 134 | parsedCsp.getEffectiveDirective(csp.Directive.SCRIPT_SRC); 135 | const values: string[] = parsedCsp.directives[directiveName] || []; 136 | 137 | if (!values.includes(Keyword.STRICT_DYNAMIC)) { 138 | return []; 139 | } 140 | 141 | // Check if there's already an allowlist (url scheme or url) 142 | if (!values.some( 143 | (v) => ['http:', 'https:', '*'].includes(v) || v.includes('.'))) { 144 | return [new Finding( 145 | Type.ALLOWLIST_FALLBACK, 146 | 'Consider adding https: and http: url schemes (ignored by browsers ' + 147 | 'supporting \'strict-dynamic\') to be backward compatible with older ' + 148 | 'browsers.', 149 | Severity.STRICT_CSP, directiveName)]; 150 | } 151 | 152 | return []; 153 | } 154 | 155 | 156 | /** 157 | * Checks if the policy requires Trusted Types for scripts. 158 | * 159 | * I.e. the policy should have the following dirctive: 160 | * require-trusted-types-for 'script' 161 | * 162 | * @param parsedCsp A parsed csp. 163 | */ 164 | export function checkRequiresTrustedTypesForScripts(parsedCsp: Csp): Finding[] { 165 | const directiveName = 166 | parsedCsp.getEffectiveDirective(csp.Directive.REQUIRE_TRUSTED_TYPES_FOR); 167 | const values: string[] = parsedCsp.directives[directiveName] || []; 168 | 169 | if (!values.includes(csp.TrustedTypesSink.SCRIPT)) { 170 | return [new Finding( 171 | Type.REQUIRE_TRUSTED_TYPES_FOR_SCRIPTS, 172 | 'Consider requiring Trusted Types for scripts to lock down DOM XSS ' + 173 | 'injection sinks. You can do this by adding ' + 174 | '"require-trusted-types-for \'script\'" to your policy.', 175 | Severity.INFO, csp.Directive.REQUIRE_TRUSTED_TYPES_FOR)]; 176 | } 177 | 178 | return []; 179 | } 180 | -------------------------------------------------------------------------------- /checks/strictcsp_checks_test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 Google Inc. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @fileoverview Tests for strict CSP checks. 17 | * @author lwe@google.com (Lukas Weichselbaum) 18 | */ 19 | 20 | import {Finding, Severity} from '../finding'; 21 | import {CspParser} from '../parser'; 22 | 23 | import {CheckerFunction} from './checker'; 24 | import * as strictcspChecks from './strictcsp_checks'; 25 | 26 | 27 | /** 28 | * Helper function for running a check on a CSP string. 29 | * 30 | * @param test CSP string. 31 | * @param checkFunction check. 32 | */ 33 | function checkCsp(test: string, checkFunction: CheckerFunction): Finding[] { 34 | const parsedCsp = (new CspParser(test)).csp; 35 | return checkFunction(parsedCsp); 36 | } 37 | 38 | describe('Test strictcsp checks', () => { 39 | /** Tests for csp.strictcspChecks.checkStrictDynamic */ 40 | it('CheckStrictDynamic', () => { 41 | const test = 'script-src foo.bar'; 42 | 43 | const violations = checkCsp(test, strictcspChecks.checkStrictDynamic); 44 | expect(violations.length).toBe(1); 45 | expect(violations[0].severity).toBe(Severity.STRICT_CSP); 46 | }); 47 | 48 | /** Tests for csp.strictcspChecks.checkStrictDynamicNotStandalone */ 49 | it('CheckStrictDynamicNotStandalone', () => { 50 | const test = 'script-src \'strict-dynamic\''; 51 | 52 | const violations = 53 | checkCsp(test, strictcspChecks.checkStrictDynamicNotStandalone); 54 | expect(violations[0].severity).toBe(Severity.INFO); 55 | }); 56 | 57 | it('CheckStrictDynamicNotStandaloneDoesntFireIfNoncePresent', () => { 58 | const test = 'script-src \'strict-dynamic\' \'nonce-foobar\''; 59 | 60 | const violations = 61 | checkCsp(test, strictcspChecks.checkStrictDynamicNotStandalone); 62 | expect(violations.length).toBe(0); 63 | }); 64 | 65 | /** Tests for csp.strictcspChecks.checkUnsafeInlineFallback */ 66 | it('CheckUnsafeInlineFallback', () => { 67 | const test = 'script-src \'nonce-test\''; 68 | 69 | const violations = 70 | checkCsp(test, strictcspChecks.checkUnsafeInlineFallback); 71 | expect(violations.length).toBe(1); 72 | expect(violations[0].severity).toBe(Severity.STRICT_CSP); 73 | }); 74 | 75 | it('CheckUnsafeInlineFallbackDoesntFireIfFallbackPresent', () => { 76 | const test = 'script-src \'nonce-test\' \'unsafe-inline\''; 77 | 78 | const violations = 79 | checkCsp(test, strictcspChecks.checkUnsafeInlineFallback); 80 | expect(violations.length).toBe(0); 81 | }); 82 | 83 | /** Tests for csp.strictcspChecks.checkAllowlistFallback */ 84 | it('checkAllowlistFallback', () => { 85 | const test = 'script-src \'nonce-test\' \'strict-dynamic\''; 86 | 87 | const violations = checkCsp(test, strictcspChecks.checkAllowlistFallback); 88 | expect(violations.length).toBe(1); 89 | expect(violations[0].severity).toBe(Severity.STRICT_CSP); 90 | }); 91 | 92 | it('checkAllowlistFallbackDoesntFireIfSchemeFallbackPresent', () => { 93 | const test = 'script-src \'nonce-test\' \'strict-dynamic\' https:'; 94 | 95 | const violations = checkCsp(test, strictcspChecks.checkAllowlistFallback); 96 | expect(violations.length).toBe(0); 97 | }); 98 | 99 | it('checkAllowlistFallbackDoesntFireIfURLFallbackPresent', () => { 100 | const test = 'script-src \'nonce-test\' \'strict-dynamic\' foo.bar'; 101 | 102 | const violations = checkCsp(test, strictcspChecks.checkAllowlistFallback); 103 | expect(violations.length).toBe(0); 104 | }); 105 | 106 | it('checkAllowlistFallbackDoesntFireInAbsenceOfStrictDynamic', () => { 107 | const test = 'script-src \'nonce-test\''; 108 | 109 | const violations = checkCsp(test, strictcspChecks.checkAllowlistFallback); 110 | expect(violations.length).toBe(0); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /csp.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview CSP definitions and helper functions. 3 | * @author lwe@google.com (Lukas Weichselbaum) 4 | * 5 | * @license 6 | * Copyright 2016 Google Inc. All rights reserved. 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | 21 | import {Finding, Severity, Type} from './finding'; 22 | 23 | /** 24 | * Content Security Policy object. 25 | * List of valid CSP directives: 26 | * - http://www.w3.org/TR/CSP2/#directives 27 | * - https://www.w3.org/TR/upgrade-insecure-requests/ 28 | */ 29 | export class Csp { 30 | directives: Record = {}; 31 | 32 | /** 33 | * Creates a CSP object from a list of directives. 34 | * @param directives CSP directives. 35 | */ 36 | constructor(directives: Record = {}) { 37 | for (const [directive, directiveValues] of Object.entries(directives)) { 38 | if (directiveValues) { 39 | this.directives[directive] = [...directiveValues]; 40 | } 41 | } 42 | } 43 | 44 | /** 45 | * Clones a CSP object. 46 | * @return clone of parsedCsp. 47 | */ 48 | clone(): Csp { 49 | // Use the constructor that takes in directives to create a deep copy. 50 | return new Csp(this.directives); 51 | } 52 | 53 | /** 54 | * Converts this CSP back into a string. 55 | * @return CSP string. 56 | */ 57 | convertToString(): string { 58 | let cspString = ''; 59 | 60 | for (const [directive, directiveValues] of Object.entries( 61 | this.directives)) { 62 | cspString += directive; 63 | if (directiveValues !== undefined) { 64 | for (let value, i = 0; (value = directiveValues[i]); i++) { 65 | cspString += ' '; 66 | cspString += value; 67 | } 68 | } 69 | cspString += '; '; 70 | } 71 | 72 | return cspString; 73 | } 74 | 75 | /** 76 | * Returns CSP as it would be seen by a UA supporting a specific CSP version. 77 | * @param cspVersion CSP. 78 | * @param optFindings findings about ignored directive values will be added 79 | * to this array, if passed. (e.g. CSP2 ignores 'unsafe-inline' in 80 | * presence of a nonce or a hash) 81 | * @return The effective CSP. 82 | */ 83 | getEffectiveCsp(cspVersion: Version, optFindings?: Finding[]): Csp { 84 | const findings = optFindings || []; 85 | const effectiveCsp = this.clone(); 86 | [Directive.SCRIPT_SRC, Directive.SCRIPT_SRC_ATTR, Directive.SCRIPT_SRC_ELEM] 87 | .forEach(directiveToNormalize => { 88 | const directive = effectiveCsp.getEffectiveDirective( 89 | directiveToNormalize) as Directive; 90 | const values = this.directives[directive] || []; 91 | const effectiveCspValues = effectiveCsp.directives[directive]; 92 | 93 | if (effectiveCspValues && 94 | (effectiveCsp.policyHasScriptNonces(directive) || 95 | effectiveCsp.policyHasScriptHashes(directive))) { 96 | if (cspVersion >= Version.CSP2) { 97 | // Ignore 'unsafe-inline' in CSP >= v2, if a nonce or a hash is 98 | // present. 99 | if (values.includes(Keyword.UNSAFE_INLINE)) { 100 | arrayRemove(effectiveCspValues, Keyword.UNSAFE_INLINE); 101 | findings.push(new Finding( 102 | Type.IGNORED, 103 | 'unsafe-inline is ignored if a nonce or a hash is present. ' + 104 | '(CSP2 and above)', 105 | Severity.NONE, directive, Keyword.UNSAFE_INLINE)); 106 | } 107 | } else { 108 | // remove nonces and hashes (not supported in CSP < v2). 109 | for (const value of values) { 110 | if (value.startsWith('\'nonce-') || value.startsWith('\'sha')) { 111 | arrayRemove(effectiveCspValues, value); 112 | } 113 | } 114 | } 115 | } 116 | 117 | if (effectiveCspValues && this.policyHasStrictDynamic(directive)) { 118 | // Ignore allowlist in CSP >= v3 in presence of 'strict-dynamic'. 119 | if (cspVersion >= Version.CSP3) { 120 | for (const value of values) { 121 | // Because of 'strict-dynamic' all host-source and scheme-source 122 | // expressions, as well as the "'unsafe-inline'" and "'self' 123 | // keyword-sources will be ignored. 124 | // https://w3c.github.io/webappsec-csp/#strict-dynamic-usage 125 | if (!value.startsWith('\'') || value === Keyword.SELF || 126 | value === Keyword.UNSAFE_INLINE) { 127 | arrayRemove(effectiveCspValues, value); 128 | findings.push(new Finding( 129 | Type.IGNORED, 130 | 'Because of strict-dynamic this entry is ignored in CSP3 and above', 131 | Severity.NONE, directive, value)); 132 | } 133 | } 134 | } else { 135 | // strict-dynamic not supported. 136 | arrayRemove(effectiveCspValues, Keyword.STRICT_DYNAMIC); 137 | } 138 | } 139 | }); 140 | 141 | if (cspVersion < Version.CSP3) { 142 | // Remove CSP3 directives from pre-CSP3 policies. 143 | // https://w3c.github.io/webappsec-csp/#changes-from-level-2 144 | delete effectiveCsp.directives[Directive.REPORT_TO]; 145 | delete effectiveCsp.directives[Directive.WORKER_SRC]; 146 | delete effectiveCsp.directives[Directive.MANIFEST_SRC]; 147 | delete effectiveCsp.directives[Directive.TRUSTED_TYPES]; 148 | delete effectiveCsp.directives[Directive.REQUIRE_TRUSTED_TYPES_FOR]; 149 | delete effectiveCsp.directives[Directive.SCRIPT_SRC_ATTR]; 150 | delete effectiveCsp.directives[Directive.SCRIPT_SRC_ELEM]; 151 | delete effectiveCsp.directives[Directive.STYLE_SRC_ATTR]; 152 | delete effectiveCsp.directives[Directive.STYLE_SRC_ELEM]; 153 | } 154 | 155 | return effectiveCsp; 156 | } 157 | 158 | /** 159 | * Returns default-src if directive is a fetch directive and is not present in 160 | * this CSP. Otherwise the provided directive is returned. 161 | * @param directive CSP. 162 | * @return The effective directive. 163 | */ 164 | getEffectiveDirective(directive: string): string { 165 | if (directive in this.directives) { 166 | return directive; 167 | } 168 | 169 | if ((directive === Directive.SCRIPT_SRC_ATTR || 170 | directive === Directive.SCRIPT_SRC_ELEM) && 171 | Directive.SCRIPT_SRC in this.directives) { 172 | return Directive.SCRIPT_SRC; 173 | } 174 | if ((directive === Directive.STYLE_SRC_ATTR || 175 | directive === Directive.STYLE_SRC_ELEM) && 176 | Directive.STYLE_SRC in this.directives) { 177 | return Directive.STYLE_SRC; 178 | } 179 | 180 | // Only fetch directives default to default-src. 181 | if (FETCH_DIRECTIVES.includes(directive as Directive)) { 182 | return Directive.DEFAULT_SRC; 183 | } 184 | return directive; 185 | } 186 | 187 | /** 188 | * Returns the passed directives if present in this CSP or default-src 189 | * otherwise. 190 | * @param directives CSP. 191 | * @return The effective directives. 192 | */ 193 | getEffectiveDirectives(directives: string[]): string[] { 194 | const effectiveDirectives = 195 | new Set(directives.map((val) => this.getEffectiveDirective(val))); 196 | return [...effectiveDirectives]; 197 | } 198 | 199 | /** 200 | * Checks if this CSP is using nonces for scripts. 201 | * @return true, if this CSP is using script nonces. 202 | */ 203 | policyHasScriptNonces(directive?: Directive): boolean { 204 | const directiveName = 205 | this.getEffectiveDirective(directive || Directive.SCRIPT_SRC); 206 | const values = this.directives[directiveName] || []; 207 | return values.some((val) => isNonce(val)); 208 | } 209 | 210 | /** 211 | * Checks if this CSP is using hashes for scripts. 212 | * @return true, if this CSP is using script hashes. 213 | */ 214 | policyHasScriptHashes(directive?: Directive): boolean { 215 | const directiveName = 216 | this.getEffectiveDirective(directive || Directive.SCRIPT_SRC); 217 | const values = this.directives[directiveName] || []; 218 | return values.some((val) => isHash(val)); 219 | } 220 | 221 | /** 222 | * Checks if this CSP is using strict-dynamic. 223 | * @return true, if this CSP is using CSP nonces. 224 | */ 225 | policyHasStrictDynamic(directive?: Directive): boolean { 226 | const directiveName = 227 | this.getEffectiveDirective(directive || Directive.SCRIPT_SRC); 228 | const values = this.directives[directiveName] || []; 229 | return values.includes(Keyword.STRICT_DYNAMIC); 230 | } 231 | } 232 | 233 | 234 | /** 235 | * CSP directive source keywords. 236 | */ 237 | export enum Keyword { 238 | SELF = '\'self\'', 239 | NONE = '\'none\'', 240 | UNSAFE_INLINE = '\'unsafe-inline\'', 241 | UNSAFE_EVAL = '\'unsafe-eval\'', 242 | WASM_EVAL = '\'wasm-eval\'', 243 | WASM_UNSAFE_EVAL = '\'wasm-unsafe-eval\'', 244 | STRICT_DYNAMIC = '\'strict-dynamic\'', 245 | UNSAFE_HASHED_ATTRIBUTES = '\'unsafe-hashed-attributes\'', 246 | UNSAFE_HASHES = '\'unsafe-hashes\'', 247 | REPORT_SAMPLE = '\'report-sample\'', 248 | BLOCK = '\'block\'', 249 | ALLOW = '\'allow\'', 250 | INLINE_SPECULATION_RULES = '\'inline-speculation-rules\'', 251 | } 252 | 253 | 254 | /** 255 | * CSP directive source keywords. 256 | */ 257 | export enum TrustedTypesSink { 258 | SCRIPT = '\'script\'', 259 | } 260 | 261 | 262 | /** 263 | * CSP v3 directives. 264 | * List of valid CSP directives: 265 | * - http://www.w3.org/TR/CSP2/#directives 266 | * - https://www.w3.org/TR/upgrade-insecure-requests/ 267 | * 268 | */ 269 | export enum Directive { 270 | // Fetch directives 271 | CHILD_SRC = 'child-src', 272 | CONNECT_SRC = 'connect-src', 273 | DEFAULT_SRC = 'default-src', 274 | FONT_SRC = 'font-src', 275 | FRAME_SRC = 'frame-src', 276 | IMG_SRC = 'img-src', 277 | MEDIA_SRC = 'media-src', 278 | OBJECT_SRC = 'object-src', 279 | SCRIPT_SRC = 'script-src', 280 | SCRIPT_SRC_ATTR = 'script-src-attr', 281 | SCRIPT_SRC_ELEM = 'script-src-elem', 282 | STYLE_SRC = 'style-src', 283 | STYLE_SRC_ATTR = 'style-src-attr', 284 | STYLE_SRC_ELEM = 'style-src-elem', 285 | PREFETCH_SRC = 'prefetch-src', 286 | 287 | MANIFEST_SRC = 'manifest-src', 288 | WORKER_SRC = 'worker-src', 289 | 290 | // Document directives 291 | BASE_URI = 'base-uri', 292 | PLUGIN_TYPES = 'plugin-types', 293 | SANDBOX = 'sandbox', 294 | DISOWN_OPENER = 'disown-opener', 295 | 296 | // Navigation directives 297 | FORM_ACTION = 'form-action', 298 | FRAME_ANCESTORS = 'frame-ancestors', 299 | NAVIGATE_TO = 'navigate-to', 300 | 301 | // Reporting directives 302 | REPORT_TO = 'report-to', 303 | REPORT_URI = 'report-uri', 304 | 305 | // Other directives 306 | BLOCK_ALL_MIXED_CONTENT = 'block-all-mixed-content', 307 | UPGRADE_INSECURE_REQUESTS = 'upgrade-insecure-requests', 308 | REFLECTED_XSS = 'reflected-xss', 309 | REFERRER = 'referrer', 310 | REQUIRE_SRI_FOR = 'require-sri-for', 311 | TRUSTED_TYPES = 'trusted-types', 312 | // https://github.com/WICG/trusted-types 313 | REQUIRE_TRUSTED_TYPES_FOR = 'require-trusted-types-for', 314 | WEBRTC = 'webrtc', 315 | } 316 | 317 | /** 318 | * CSP v3 fetch directives. 319 | * Fetch directives control the locations from which resources may be loaded. 320 | * https://w3c.github.io/webappsec-csp/#directives-fetch 321 | * 322 | */ 323 | export const FETCH_DIRECTIVES: Directive[] = [ 324 | Directive.CHILD_SRC, Directive.CONNECT_SRC, Directive.DEFAULT_SRC, 325 | Directive.FONT_SRC, Directive.FRAME_SRC, Directive.IMG_SRC, 326 | Directive.MANIFEST_SRC, Directive.MEDIA_SRC, Directive.OBJECT_SRC, 327 | Directive.SCRIPT_SRC, Directive.SCRIPT_SRC_ATTR, Directive.SCRIPT_SRC_ELEM, 328 | Directive.STYLE_SRC, Directive.STYLE_SRC_ATTR, Directive.STYLE_SRC_ELEM, 329 | Directive.WORKER_SRC 330 | ]; 331 | 332 | /** 333 | * CSP version. 334 | */ 335 | export enum Version { 336 | CSP1 = 1, 337 | CSP2, 338 | CSP3 339 | } 340 | 341 | 342 | /** 343 | * Checks if a string is a valid CSP directive. 344 | * @param directive value to check. 345 | * @return True if directive is a valid CSP directive. 346 | */ 347 | export function isDirective(directive: string): boolean { 348 | return Object.values(Directive).includes(directive as Directive); 349 | } 350 | 351 | 352 | /** 353 | * Checks if a string is a valid CSP keyword. 354 | * @param keyword value to check. 355 | * @return True if keyword is a valid CSP keyword. 356 | */ 357 | export function isKeyword(keyword: string): boolean { 358 | return Object.values(Keyword).includes(keyword as Keyword); 359 | } 360 | 361 | 362 | /** 363 | * Checks if a string is a valid URL scheme. 364 | * Scheme part + ":" 365 | * For scheme part see https://tools.ietf.org/html/rfc3986#section-3.1 366 | * @param urlScheme value to check. 367 | * @return True if urlScheme has a valid scheme. 368 | */ 369 | export function isUrlScheme(urlScheme: string): boolean { 370 | const pattern = new RegExp('^[a-zA-Z][+a-zA-Z0-9.-]*:$'); 371 | return pattern.test(urlScheme); 372 | } 373 | 374 | 375 | /** 376 | * A regex pattern to check nonce prefix and Base64 formatting of a nonce value. 377 | */ 378 | export const STRICT_NONCE_PATTERN = 379 | new RegExp('^\'nonce-[a-zA-Z0-9+/_-]+[=]{0,2}\'$'); 380 | 381 | 382 | /** A regex pattern for checking if nonce prefix. */ 383 | export const NONCE_PATTERN = new RegExp('^\'nonce-(.+)\'$'); 384 | 385 | 386 | /** 387 | * Checks if a string is a valid CSP nonce. 388 | * See http://www.w3.org/TR/CSP2/#nonce_value 389 | * @param nonce value to check. 390 | * @param strictCheck Check if the nonce uses the base64 charset. 391 | * @return True if nonce is has a valid CSP nonce. 392 | */ 393 | export function isNonce(nonce: string, strictCheck?: boolean): boolean { 394 | const pattern = strictCheck ? STRICT_NONCE_PATTERN : NONCE_PATTERN; 395 | return pattern.test(nonce); 396 | } 397 | 398 | 399 | /** 400 | * A regex pattern to check hash prefix and Base64 formatting of a hash value. 401 | */ 402 | export const STRICT_HASH_PATTERN = 403 | new RegExp('^\'(sha256|sha384|sha512)-[a-zA-Z0-9+/]+[=]{0,2}\'$'); 404 | 405 | 406 | /** A regex pattern to check hash prefix. */ 407 | export const HASH_PATTERN = new RegExp('^\'(sha256|sha384|sha512)-(.+)\'$'); 408 | 409 | 410 | /** 411 | * Checks if a string is a valid CSP hash. 412 | * See http://www.w3.org/TR/CSP2/#hash_value 413 | * @param hash value to check. 414 | * @param strictCheck Check if the hash uses the base64 charset. 415 | * @return True if hash is has a valid CSP hash. 416 | */ 417 | export function isHash(hash: string, strictCheck?: boolean): boolean { 418 | const pattern = strictCheck ? STRICT_HASH_PATTERN : HASH_PATTERN; 419 | return pattern.test(hash); 420 | } 421 | 422 | 423 | /** 424 | * Class to represent all generic CSP errors. 425 | */ 426 | export class CspError extends Error { 427 | /** 428 | * @param message An optional error message. 429 | */ 430 | constructor(message?: string) { 431 | super(message); 432 | } 433 | } 434 | 435 | /** 436 | * Mutate the given array to remove the first instance of the given item 437 | */ 438 | function arrayRemove(arr: T[], item: T): void { 439 | if (arr.includes(item)) { 440 | const idx = arr.findIndex(elem => item === elem); 441 | arr.splice(idx, 1); 442 | } 443 | } 444 | -------------------------------------------------------------------------------- /csp_test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Tests for CSP Defintions. 3 | * @author lwe@google.com (Lukas Weichselbaum) 4 | * 5 | * @license 6 | * Copyright 2016 Google Inc. All rights reserved. 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | import 'jasmine'; 21 | 22 | import {Directive, isDirective, isHash, isKeyword, isNonce, isUrlScheme, Keyword, Version} from './csp'; 23 | import {Finding, Severity, Type} from './finding'; 24 | import {CspParser} from './parser'; 25 | 26 | describe('Test Csp', () => { 27 | it('ConvertToString', () => { 28 | const testCsp = 'default-src \'none\'; ' + 29 | 'script-src \'nonce-unsafefoobar\' \'unsafe-eval\' \'unsafe-inline\' ' + 30 | 'https://example.com/foo.js foo.bar; ' + 31 | 'img-src \'self\' https: data: blob:; '; 32 | 33 | const parsed = (new CspParser(testCsp)).csp; 34 | expect(parsed.convertToString()).toBe(testCsp); 35 | }); 36 | 37 | it('GetEffectiveCspVersion1', () => { 38 | const testCsp = 39 | 'default-src \'unsafe-inline\' \'strict-dynamic\' \'nonce-123\' ' + 40 | '\'sha256-foobar\' \'self\'; report-to foo.bar; worker-src *; manifest-src *; script-src-attr \'self\'; script-src-elem \'self\'; style-src-attr \'self\'; style-src-elem \'self\';'; 41 | const parsed = (new CspParser(testCsp)).csp; 42 | const findings: Finding[] = []; 43 | const effectiveCsp = parsed.getEffectiveCsp(Version.CSP1, findings); 44 | 45 | expect(findings.length) 46 | .toBe(0); // No warnings about unsafe-inline being ignored in CSP 1. 47 | expect(effectiveCsp.directives[Directive.DEFAULT_SRC]).toEqual([ 48 | '\'unsafe-inline\'', '\'self\'' 49 | ]); 50 | expect(effectiveCsp.directives.hasOwnProperty(Directive.REPORT_TO)) 51 | .toBeFalse(); 52 | expect(effectiveCsp.directives.hasOwnProperty(Directive.WORKER_SRC)) 53 | .toBeFalse(); 54 | expect(effectiveCsp.directives.hasOwnProperty(Directive.MANIFEST_SRC)) 55 | .toBeFalse(); 56 | }); 57 | 58 | it('GetEffectiveCspVersion2', () => { 59 | const testCsp = 60 | 'default-src \'unsafe-inline\' \'strict-dynamic\' \'nonce-123\' ' + 61 | '\'sha256-foobar\' \'self\'; report-to foo.bar; worker-src *; manifest-src *; script-src-attr \'self\'; script-src-elem \'self\'; style-src-attr \'self\'; style-src-elem \'self\';'; 62 | const parsed = (new CspParser(testCsp)).csp; 63 | const findings: Finding[] = []; 64 | const effectiveCsp = parsed.getEffectiveCsp(Version.CSP2, findings); 65 | 66 | // Ignored messages are surfaced properly. 67 | expect(findings.length).toBe(1); // Only nonces cause unsafe-inline to be 68 | // ignored in CSP2, not strict-dynamic. 69 | expect(findings[0].value).toBe('\'unsafe-inline\''); 70 | expect(findings[0].type).toBe(Type.IGNORED); 71 | expect(findings[0].severity).toBe(Severity.NONE); 72 | expect(findings[0].directive).toBe(Directive.DEFAULT_SRC); 73 | 74 | expect(effectiveCsp.directives[Directive.DEFAULT_SRC]).toEqual([ 75 | '\'nonce-123\'', '\'sha256-foobar\'', '\'self\'' 76 | ]); 77 | expect(effectiveCsp.directives.hasOwnProperty(Directive.REPORT_TO)) 78 | .toBeFalse(); 79 | expect(effectiveCsp.directives.hasOwnProperty(Directive.WORKER_SRC)) 80 | .toBeFalse(); 81 | expect(effectiveCsp.directives.hasOwnProperty(Directive.MANIFEST_SRC)) 82 | .toBeFalse(); 83 | expect(effectiveCsp.directives.hasOwnProperty(Directive.SCRIPT_SRC_ATTR)) 84 | .toBeFalse(); 85 | expect(effectiveCsp.directives.hasOwnProperty(Directive.SCRIPT_SRC_ELEM)) 86 | .toBeFalse(); 87 | expect(effectiveCsp.directives.hasOwnProperty(Directive.STYLE_SRC_ATTR)) 88 | .toBeFalse(); 89 | expect(effectiveCsp.directives.hasOwnProperty(Directive.STYLE_SRC_ELEM)) 90 | .toBeFalse(); 91 | }); 92 | 93 | it('GetEffectiveCspVersion3', () => { 94 | const testCsp = 95 | 'default-src \'unsafe-inline\' \'strict-dynamic\' \'nonce-123\' ' + 96 | '\'sha256-foobar\' \'self\'; report-to foo.bar; worker-src *; manifest-src *; script-src-attr \'self\'; script-src-elem \'self\'; style-src-attr \'self\'; style-src-elem \'self\';'; 97 | const parsed = (new CspParser(testCsp)).csp; 98 | const findings: Finding[] = []; 99 | const effectiveCsp = parsed.getEffectiveCsp(Version.CSP3, findings); 100 | 101 | // Ignored messages are only on the default-src: Ignoring self and 102 | // unsafe-inline because of strict-dynamic and the unsafe-inline because of 103 | // the nonce. 104 | expect(findings.length).toBe(3); 105 | expect(findings.every(f => f.type === Type.IGNORED)).toBeTrue(); 106 | expect(findings.every(f => f.severity === Severity.NONE)).toBeTrue(); 107 | expect(findings.every(f => f.directive === Directive.DEFAULT_SRC)) 108 | .toBeTrue(); 109 | 110 | expect(effectiveCsp.directives[Directive.DEFAULT_SRC]).toEqual([ 111 | '\'strict-dynamic\'', '\'nonce-123\'', '\'sha256-foobar\'' 112 | ]); 113 | expect(effectiveCsp.directives[Directive.REPORT_TO]).toEqual(['foo.bar']); 114 | expect(effectiveCsp.directives[Directive.WORKER_SRC]).toEqual(['*']); 115 | expect(effectiveCsp.directives[Directive.MANIFEST_SRC]).toEqual(['*']); 116 | expect(effectiveCsp.directives[Directive.SCRIPT_SRC_ATTR]).toEqual([ 117 | '\'self\'' 118 | ]); 119 | expect(effectiveCsp.directives[Directive.SCRIPT_SRC_ELEM]).toEqual([ 120 | '\'self\'' 121 | ]); 122 | expect(effectiveCsp.directives[Directive.STYLE_SRC_ATTR]).toEqual([ 123 | '\'self\'' 124 | ]); 125 | expect(effectiveCsp.directives[Directive.STYLE_SRC_ELEM]).toEqual([ 126 | '\'self\'' 127 | ]); 128 | }); 129 | 130 | 131 | it('GetEffectiveDirective', () => { 132 | const testCsp = 'default-src https:; script-src foo.bar'; 133 | const parsed = (new CspParser(testCsp)).csp; 134 | 135 | const script = parsed.getEffectiveDirective(Directive.SCRIPT_SRC); 136 | expect(script).toBe(Directive.SCRIPT_SRC); 137 | const style = parsed.getEffectiveDirective(Directive.STYLE_SRC); 138 | expect(style).toBe(Directive.DEFAULT_SRC); 139 | }); 140 | 141 | 142 | it('GetEffectiveDirectives', () => { 143 | const testCsp = 'default-src https:; script-src foo.bar'; 144 | const parsed = (new CspParser(testCsp)).csp; 145 | 146 | const directives = parsed.getEffectiveDirectives( 147 | [Directive.SCRIPT_SRC, Directive.STYLE_SRC]); 148 | expect(directives).toEqual([Directive.SCRIPT_SRC, Directive.DEFAULT_SRC]); 149 | }); 150 | 151 | it('GetEffectiveDirectives', () => { 152 | const testCsp = 'default-src https:; style-src-elem foo.bar'; 153 | const parsed = (new CspParser(testCsp)).csp; 154 | 155 | const directives = parsed.getEffectiveDirectives([ 156 | Directive.STYLE_SRC, Directive.STYLE_SRC_ATTR, Directive.STYLE_SRC_ELEM 157 | ]); 158 | // Both style-src and style-src-attr default to default-src. 159 | expect(directives).toEqual([ 160 | Directive.DEFAULT_SRC, Directive.STYLE_SRC_ELEM 161 | ]); 162 | }); 163 | 164 | it('GetEffectiveDirectives', () => { 165 | const testCsp = 166 | 'default-src https:; script-src foo.bar; script-src-elem bar.baz; style-src foo.bar; style-src-elem bar.baz'; 167 | const parsed = (new CspParser(testCsp)).csp; 168 | 169 | const directives = parsed.getEffectiveDirectives([ 170 | Directive.SCRIPT_SRC, Directive.STYLE_SRC, Directive.SCRIPT_SRC_ATTR, 171 | Directive.SCRIPT_SRC_ELEM, Directive.STYLE_SRC_ATTR, 172 | Directive.STYLE_SRC_ELEM 173 | ]); 174 | expect(directives).toEqual([ 175 | Directive.SCRIPT_SRC, // Both script-src and script-src-attr 176 | Directive.STYLE_SRC, // Both style-src and style-src-attr 177 | Directive.SCRIPT_SRC_ELEM, 178 | Directive.STYLE_SRC_ELEM, 179 | ]); 180 | }); 181 | 182 | it('PolicyHasScriptNoncesScriptSrcWithNonce', () => { 183 | const testCsp = 'default-src https:; script-src \'nonce-test123\''; 184 | const parsed = (new CspParser(testCsp)).csp; 185 | 186 | expect(parsed.policyHasScriptNonces()).toBeTrue(); 187 | }); 188 | 189 | 190 | it('PolicyHasScriptNoncesNoNonce', () => { 191 | const testCsp = 192 | 'default-src https: \'nonce-ignored\'; script-src nonce-invalid'; 193 | const parsed = (new CspParser(testCsp)).csp; 194 | 195 | expect(parsed.policyHasScriptNonces()).toBeFalse(); 196 | }); 197 | 198 | 199 | it('PolicyHasScriptHashesScriptSrcWithHash', () => { 200 | const testCsp = 'default-src https:; script-src \'sha256-asdfASDF\''; 201 | const parsed = (new CspParser(testCsp)).csp; 202 | 203 | expect(parsed.policyHasScriptHashes()).toBeTrue(); 204 | }); 205 | 206 | 207 | it('PolicyHasScriptHashesNoHash', () => { 208 | const testCsp = 209 | 'default-src https: \'nonce-ignored\'; script-src sha256-invalid'; 210 | const parsed = (new CspParser(testCsp)).csp; 211 | 212 | expect(parsed.policyHasScriptHashes()).toBeFalse(); 213 | }); 214 | 215 | 216 | it('PolicyHasStrictDynamicScriptSrcWithStrictDynamic', () => { 217 | const testCsp = 'default-src https:; script-src \'strict-dynamic\''; 218 | const parsed = (new CspParser(testCsp)).csp; 219 | 220 | expect(parsed.policyHasStrictDynamic()).toBeTrue(); 221 | }); 222 | 223 | 224 | it('PolicyHasStrictDynamicDefaultSrcWithStrictDynamic', () => { 225 | const testCsp = 'default-src https \'strict-dynamic\''; 226 | const parsed = (new CspParser(testCsp)).csp; 227 | 228 | expect(parsed.policyHasStrictDynamic()).toBeTrue(); 229 | }); 230 | 231 | 232 | it('PolicyHasStrictDynamicNoStrictDynamic', () => { 233 | const testCsp = 'default-src \'strict-dynamic\'; script-src foo.bar'; 234 | const parsed = (new CspParser(testCsp)).csp; 235 | 236 | expect(parsed.policyHasStrictDynamic()).toBeFalse(); 237 | }); 238 | 239 | 240 | it('IsDirective', () => { 241 | const directives = Object.keys(Directive).map( 242 | (name) => Directive[name as keyof typeof Directive]); 243 | 244 | expect(directives.every(isDirective)).toBeTrue(); 245 | expect(isDirective('invalid-src')).toBeFalse(); 246 | }); 247 | 248 | 249 | it('IsKeyword', () => { 250 | const keywords = Object.keys(Keyword).map( 251 | (name) => (Keyword[name as keyof typeof Keyword])); 252 | 253 | expect(keywords.every(isKeyword)).toBeTrue(); 254 | expect(isKeyword('invalid')).toBeFalse(); 255 | }); 256 | 257 | 258 | it('IsUrlScheme', () => { 259 | expect(isUrlScheme('http:')).toBeTrue(); 260 | expect(isUrlScheme('https:')).toBeTrue(); 261 | expect(isUrlScheme('data:')).toBeTrue(); 262 | expect(isUrlScheme('blob:')).toBeTrue(); 263 | expect(isUrlScheme('b+l.o-b:')).toBeTrue(); 264 | expect(isUrlScheme('filesystem:')).toBeTrue(); 265 | expect(isUrlScheme('invalid')).toBeFalse(); 266 | expect(isUrlScheme('ht_tp:')).toBeFalse(); 267 | }); 268 | 269 | 270 | it('IsNonce', () => { 271 | expect(isNonce('\'nonce-asdfASDF=\'')).toBeTrue(); 272 | expect(isNonce('\'sha256-asdfASDF=\'')).toBeFalse(); 273 | expect(isNonce('\'asdfASDF=\'')).toBeFalse(); 274 | expect(isNonce('example.com')).toBeFalse(); 275 | }); 276 | 277 | 278 | it('IsStrictNonce', () => { 279 | expect(isNonce('\'nonce-asdfASDF=\'', true)).toBeTrue(); 280 | expect(isNonce('\'nonce-as+df/A0234SDF==\'', true)).toBeTrue(); 281 | expect(isNonce('\'nonce-as_dfASDF=\'', true)).toBeTrue(); 282 | expect(isNonce('\'nonce-asdfASDF===\'', true)).toBeFalse(); 283 | expect(isNonce('\'sha256-asdfASDF=\'', true)).toBeFalse(); 284 | }); 285 | 286 | 287 | it('IsHash', () => { 288 | expect(isHash('\'sha256-asdfASDF=\'')).toBeTrue(); 289 | expect(isHash('\'sha777-asdfASDF=\'')).toBeFalse(); 290 | expect(isHash('\'asdfASDF=\'')).toBeFalse(); 291 | expect(isHash('example.com')).toBeFalse(); 292 | }); 293 | 294 | it('IsStrictHash', () => { 295 | expect(isHash('\'sha256-asdfASDF=\'', true)).toBeTrue(); 296 | expect(isHash('\'sha256-as+d/f/ASD0+4F==\'', true)).toBeTrue(); 297 | expect(isHash('\'sha256-asdfASDF===\'', true)).toBeFalse(); 298 | expect(isHash('\'sha256-asd_fASDF=\'', true)).toBeFalse(); 299 | expect(isHash('\'sha777-asdfASDF=\'', true)).toBeFalse(); 300 | expect(isHash('\'asdfASDF=\'', true)).toBeFalse(); 301 | expect(isHash('example.com', true)).toBeFalse(); 302 | }); 303 | 304 | it('ParseNavigateTo', () => { 305 | const testCsp = 'navigate-to \'self\'; script-src \'nonce-foo\''; 306 | const parsed = (new CspParser(testCsp)).csp; 307 | 308 | expect(parsed.policyHasStrictDynamic()).toBeFalse(); 309 | expect(parsed.policyHasScriptNonces()).toBeTrue(); 310 | }); 311 | 312 | it('ParseWebRtc', () => { 313 | const testCsp = 'web-rtc \'allow\'; script-src \'nonce-foo\''; 314 | const parsed = (new CspParser(testCsp)).csp; 315 | 316 | expect(parsed.policyHasStrictDynamic()).toBeFalse(); 317 | expect(parsed.policyHasScriptNonces()).toBeTrue(); 318 | }); 319 | }); 320 | -------------------------------------------------------------------------------- /evaluator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author lwe@google.com (Lukas Weichselbaum) 3 | * 4 | * @license 5 | * Copyright 2016 Google Inc. All rights reserved. 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import {CheckerFunction} from './checks/checker'; 20 | import * as parserChecks from './checks/parser_checks'; 21 | import * as securityChecks from './checks/security_checks'; 22 | import * as strictcspChecks from './checks/strictcsp_checks'; 23 | import * as csp from './csp'; 24 | import {Csp, Version} from './csp'; 25 | import {Finding} from './finding'; 26 | 27 | 28 | 29 | /** 30 | * A class to hold a CSP Evaluator. 31 | * Evaluates a parsed CSP and reports security findings. 32 | * @unrestricted 33 | */ 34 | export class CspEvaluator { 35 | version: Version; 36 | csp: Csp; 37 | 38 | /** 39 | * List of findings reported by checks. 40 | * 41 | */ 42 | findings: Finding[] = []; 43 | /** 44 | * @param parsedCsp A parsed Content Security Policy. 45 | * @param cspVersion CSP version to apply checks for. 46 | */ 47 | constructor(parsedCsp: Csp, cspVersion?: Version, findings?: Finding[]) { 48 | /** 49 | * CSP version. 50 | */ 51 | this.version = cspVersion || csp.Version.CSP3; 52 | 53 | /** 54 | * Parsed CSP. 55 | */ 56 | this.csp = parsedCsp; 57 | 58 | /** 59 | * List of findings reported by checks. 60 | */ 61 | this.findings = findings || []; 62 | } 63 | 64 | /** 65 | * Evaluates a parsed CSP against a set of checks 66 | * @param parsedCspChecks list of checks to run on the parsed CSP (i.e. 67 | * checks like backward compatibility checks, which are independent of the 68 | * actual CSP version). 69 | * @param effectiveCspChecks list of checks to run on the effective CSP. 70 | * @return List of Findings. 71 | * @export 72 | */ 73 | evaluate( 74 | parsedCspChecks?: CheckerFunction[], 75 | effectiveCspChecks?: CheckerFunction[]): Finding[] { 76 | this.findings = []; 77 | const checks = effectiveCspChecks || DEFAULT_CHECKS; 78 | 79 | // We're applying checks on the policy as it would be seen by a browser 80 | // supporting a specific version of CSP. 81 | // For example a browser supporting only CSP1 will ignore nonces and 82 | // therefore 'unsafe-inline' would not get ignored if a policy has nonces. 83 | const effectiveCsp = this.csp.getEffectiveCsp(this.version, this.findings); 84 | 85 | // Checks independent of CSP version. 86 | if (parsedCspChecks) { 87 | for (const check of parsedCspChecks) { 88 | this.findings = this.findings.concat(check(this.csp)); 89 | } 90 | } 91 | 92 | // Checks dependent on CSP version. 93 | for (const check of checks) { 94 | this.findings = this.findings.concat(check(effectiveCsp)); 95 | } 96 | 97 | return this.findings; 98 | } 99 | } 100 | 101 | 102 | /** 103 | * Set of default checks to run. 104 | */ 105 | export const DEFAULT_CHECKS: CheckerFunction[] = [ 106 | securityChecks.checkScriptUnsafeInline, securityChecks.checkScriptUnsafeEval, 107 | securityChecks.checkPlainUrlSchemes, securityChecks.checkWildcards, 108 | securityChecks.checkMissingDirectives, 109 | securityChecks.checkScriptAllowlistBypass, 110 | securityChecks.checkFlashObjectAllowlistBypass, securityChecks.checkIpSource, 111 | securityChecks.checkNonceLength, securityChecks.checkSrcHttp, 112 | securityChecks.checkDeprecatedDirective, parserChecks.checkUnknownDirective, 113 | parserChecks.checkMissingSemicolon, parserChecks.checkInvalidKeyword 114 | ]; 115 | 116 | 117 | /** 118 | * Strict CSP and backward compatibility checks. 119 | */ 120 | export const STRICTCSP_CHECKS: CheckerFunction[] = [ 121 | strictcspChecks.checkStrictDynamic, 122 | strictcspChecks.checkStrictDynamicNotStandalone, 123 | strictcspChecks.checkUnsafeInlineFallback, 124 | strictcspChecks.checkAllowlistFallback, 125 | strictcspChecks.checkRequiresTrustedTypesForScripts 126 | ]; 127 | -------------------------------------------------------------------------------- /evaluator_test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 Google Inc. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @fileoverview Tests for CSP Evaluator. 17 | * @author lwe@google.com (Lukas Weichselbaum) 18 | */ 19 | 20 | import 'jasmine'; 21 | 22 | import {Csp} from './csp'; 23 | import {CspEvaluator} from './evaluator'; 24 | import {Finding, Severity, Type} from './finding'; 25 | 26 | describe('Test evaluator', () => { 27 | it('CspEvaluator', () => { 28 | const fakeCsp = new Csp(); 29 | const evaluator = new CspEvaluator(fakeCsp); 30 | expect(evaluator.csp).toBe(fakeCsp); 31 | }); 32 | 33 | it('Evaluate', () => { 34 | const fakeCsp = new (Csp)(); 35 | const fakeFinding = new (Finding)( 36 | Type.UNKNOWN_DIRECTIVE, 'Fake description', Severity.MEDIUM, 37 | 'fake-directive', 'fake-directive-value'); 38 | const fakeVerifier = (parsedCsp: Csp) => { 39 | return [fakeFinding]; 40 | }; 41 | 42 | const evaluator = new (CspEvaluator)(fakeCsp); 43 | const findings = 44 | evaluator.evaluate([fakeVerifier, fakeVerifier], [fakeVerifier]); 45 | 46 | const expectedFindings = [fakeFinding, fakeFinding, fakeFinding]; 47 | expect(findings).toEqual(expectedFindings); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /finding.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 Google Inc. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author lwe@google.com (Lukas Weichselbaum) 17 | */ 18 | 19 | 20 | /** 21 | * A CSP Finding is returned by a CSP check and can either reference a directive 22 | * value or a directive. If a directive value is referenced opt_index must be 23 | * provided. 24 | * @unrestricted 25 | */ 26 | export class Finding { 27 | /** 28 | * @param type Type of the finding. 29 | * @param description Description of the finding. 30 | * @param severity Severity of the finding. 31 | * @param directive The CSP directive in which the finding occurred. 32 | * @param value The directive value, if exists. 33 | */ 34 | constructor( 35 | public type: Type, public description: string, public severity: Severity, 36 | public directive: string, public value?: string) {} 37 | 38 | /** 39 | * Returns the highest severity of a list of findings. 40 | * @param findings List of findings. 41 | * @return highest severity of a list of findings. 42 | */ 43 | static getHighestSeverity(findings: Finding[]): Severity { 44 | if (findings.length === 0) { 45 | return Severity.NONE; 46 | } 47 | 48 | const severities = findings.map((finding) => finding.severity); 49 | const min = (prev: Severity, cur: Severity) => prev < cur ? prev : cur; 50 | return severities.reduce(min, Severity.NONE); 51 | } 52 | 53 | equals(obj: unknown): boolean { 54 | if (!(obj instanceof Finding)) { 55 | return false; 56 | } 57 | return obj.type === this.type && obj.description === this.description && 58 | obj.severity === this.severity && obj.directive === this.directive && 59 | obj.value === this.value; 60 | } 61 | } 62 | 63 | 64 | /** 65 | * Finding severities. 66 | */ 67 | export enum Severity { 68 | HIGH = 10, 69 | SYNTAX = 20, 70 | MEDIUM = 30, 71 | HIGH_MAYBE = 40, 72 | STRICT_CSP = 45, 73 | MEDIUM_MAYBE = 50, 74 | INFO = 60, 75 | NONE = 100 76 | } 77 | 78 | 79 | /** 80 | * Finding types for evluator checks. 81 | */ 82 | export enum Type { 83 | // Parser checks 84 | MISSING_SEMICOLON = 100, 85 | UNKNOWN_DIRECTIVE, 86 | INVALID_KEYWORD, 87 | NONCE_CHARSET = 106, 88 | 89 | // Security checks 90 | MISSING_DIRECTIVES = 300, 91 | SCRIPT_UNSAFE_INLINE, 92 | SCRIPT_UNSAFE_EVAL, 93 | PLAIN_URL_SCHEMES, 94 | PLAIN_WILDCARD, 95 | SCRIPT_ALLOWLIST_BYPASS, 96 | OBJECT_ALLOWLIST_BYPASS, 97 | NONCE_LENGTH, 98 | IP_SOURCE, 99 | DEPRECATED_DIRECTIVE, 100 | SRC_HTTP, 101 | SRC_NO_PROTOCOL, 102 | EXPERIMENTAL, 103 | WILDCARD_URL, 104 | X_FRAME_OPTIONS_OBSOLETED, 105 | STYLE_UNSAFE_INLINE, 106 | STATIC_NONCE, 107 | SCRIPT_UNSAFE_HASHES, 108 | 109 | // Strict dynamic and backward compatibility checks 110 | STRICT_DYNAMIC = 400, 111 | STRICT_DYNAMIC_NOT_STANDALONE, 112 | NONCE_HASH, 113 | UNSAFE_INLINE_FALLBACK, 114 | ALLOWLIST_FALLBACK, 115 | IGNORED, 116 | 117 | // Trusted Types checks 118 | REQUIRE_TRUSTED_TYPES_FOR_SCRIPTS = 500, 119 | 120 | // Lighthouse checks 121 | REPORTING_DESTINATION_MISSING = 600, 122 | REPORT_TO_ONLY, 123 | } 124 | -------------------------------------------------------------------------------- /finding_test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Tests for CSP Finding. 3 | * @author lwe@google.com (Lukas Weichselbaum) 4 | * 5 | * @license 6 | * Copyright 2016 Google Inc. All rights reserved. 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | import 'jasmine'; 21 | 22 | import {Directive, Keyword} from './csp'; 23 | import {Finding, Severity, Type} from './finding'; 24 | 25 | 26 | describe('Test finding', () => { 27 | it('Finding', () => { 28 | const type = Type.MISSING_SEMICOLON; 29 | const description = 'description'; 30 | const severity = Severity.HIGH; 31 | const directive = Directive.SCRIPT_SRC; 32 | const value = Keyword.NONE; 33 | 34 | const finding = new Finding(type, description, severity, directive, value); 35 | 36 | expect(finding.type).toBe(type); 37 | expect(finding.description).toBe(description); 38 | expect(finding.severity).toBe(severity); 39 | expect(finding.directive).toBe(directive); 40 | expect(finding.value).toBe(value); 41 | }); 42 | 43 | it('GetHighestSeverity', () => { 44 | const finding1 = new Finding( 45 | Type.MISSING_SEMICOLON, 'description', Severity.HIGH, 46 | Directive.SCRIPT_SRC); 47 | const finding2 = new Finding( 48 | Type.MISSING_SEMICOLON, 'description', Severity.MEDIUM, 49 | Directive.SCRIPT_SRC); 50 | const finding3 = new Finding( 51 | Type.MISSING_SEMICOLON, 'description', Severity.INFO, 52 | Directive.SCRIPT_SRC); 53 | 54 | expect(Finding.getHighestSeverity([ 55 | finding1, finding3, finding2, finding1 56 | ])).toBe(Severity.HIGH); 57 | expect(Finding.getHighestSeverity([ 58 | finding3, finding2 59 | ])).toBe(Severity.MEDIUM); 60 | expect(Finding.getHighestSeverity([ 61 | finding3, finding3 62 | ])).toBe(Severity.INFO); 63 | expect(Finding.getHighestSeverity([])).toBe(Severity.NONE); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "dist", 3 | "spec_files": [ 4 | "**/*_test.js" 5 | ], 6 | "stopSpecOnExpectationFailure": false, 7 | "random": true 8 | } 9 | -------------------------------------------------------------------------------- /lighthouse/lighthouse_checks.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview CSP checks as used by Lighthouse. These checks tend to be a 3 | * stricter subset of the other checks defined in this project. 4 | */ 5 | 6 | import {CheckerFunction} from '../checks/checker'; 7 | import {checkInvalidKeyword, checkMissingSemicolon, checkUnknownDirective} from '../checks/parser_checks'; 8 | import {checkDeprecatedDirective, checkMissingObjectSrcDirective, checkMissingScriptSrcDirective, checkMultipleMissingBaseUriDirective, checkNonceLength, checkPlainUrlSchemes, checkScriptUnsafeInline, checkWildcards} from '../checks/security_checks'; 9 | import {checkAllowlistFallback, checkStrictDynamic, checkUnsafeInlineFallback} from '../checks/strictcsp_checks'; 10 | import {Csp, Directive, Version} from '../csp'; 11 | import {Finding} from '../finding'; 12 | 13 | interface Equalable { 14 | equals(a: unknown): boolean; 15 | } 16 | 17 | function arrayContains(arr: T[], elem: T) { 18 | return arr.some(e => e.equals(elem)); 19 | } 20 | 21 | /** 22 | * Computes the intersection of all of the given sets using the `equals(...)` 23 | * method to compare items. 24 | */ 25 | function setIntersection(sets: T[][]): T[] { 26 | const intersection: T[] = []; 27 | if (sets.length === 0) { 28 | return intersection; 29 | } 30 | const firstSet = sets[0]; 31 | for (const elem of firstSet) { 32 | if (sets.every(set => arrayContains(set, elem))) { 33 | intersection.push(elem); 34 | } 35 | } 36 | return intersection; 37 | } 38 | 39 | /** 40 | * Computes the union of all of the given sets using the `equals(...)` method to 41 | * compare items. 42 | */ 43 | function setUnion(sets: T[][]): T[] { 44 | const union: T[] = []; 45 | for (const set of sets) { 46 | for (const elem of set) { 47 | if (!arrayContains(union, elem)) { 48 | union.push(elem); 49 | } 50 | } 51 | } 52 | return union; 53 | } 54 | 55 | /** 56 | * Checks if *any* of the given policies pass the given checker. If at least one 57 | * passes, returns no findings. Otherwise, returns the list of findings from the 58 | * first one that had any findings. 59 | */ 60 | function atLeastOnePasses( 61 | parsedCsps: Csp[], checker: CheckerFunction): Finding[] { 62 | const findings: Finding[][] = []; 63 | for (const parsedCsp of parsedCsps) { 64 | findings.push(checker(parsedCsp)); 65 | } 66 | return setIntersection(findings); 67 | } 68 | 69 | /** 70 | * Checks if *any* of the given policies fail the given checker. Returns the 71 | * list of findings from the one that had the most findings. 72 | */ 73 | function atLeastOneFails( 74 | parsedCsps: Csp[], checker: CheckerFunction): Finding[] { 75 | const findings: Finding[][] = []; 76 | for (const parsedCsp of parsedCsps) { 77 | findings.push(checker(parsedCsp)); 78 | } 79 | return setUnion(findings); 80 | } 81 | 82 | /** 83 | * Evaluate the given list of CSPs for checks that should cause Lighthouse to 84 | * mark the CSP as failing. Returns only the first set of failures. 85 | */ 86 | export function evaluateForFailure(parsedCsps: Csp[]): Finding[] { 87 | // Check #1 88 | const targetsXssFindings = [ 89 | ...atLeastOnePasses(parsedCsps, checkMissingScriptSrcDirective), 90 | ...atLeastOnePasses(parsedCsps, checkMissingObjectSrcDirective), 91 | ...checkMultipleMissingBaseUriDirective(parsedCsps), 92 | ]; 93 | 94 | // Check #2 95 | const effectiveCsps = 96 | parsedCsps.map(csp => csp.getEffectiveCsp(Version.CSP3)); 97 | const effectiveCspsWithScript = effectiveCsps.filter(csp => { 98 | const directiveName = csp.getEffectiveDirective(Directive.SCRIPT_SRC); 99 | return csp.directives[directiveName]; 100 | }); 101 | const robust = [ 102 | ...atLeastOnePasses(effectiveCspsWithScript, checkStrictDynamic), 103 | ...atLeastOnePasses(effectiveCspsWithScript, checkScriptUnsafeInline), 104 | ...atLeastOnePasses(effectiveCsps, checkWildcards), 105 | ...atLeastOnePasses(effectiveCsps, checkPlainUrlSchemes), 106 | ]; 107 | return [...targetsXssFindings, ...robust]; 108 | } 109 | 110 | /** 111 | * Evaluate the given list of CSPs for checks that should cause Lighthouse to 112 | * mark the CSP as OK, but present a warning. Returns only the first set of 113 | * failures. 114 | */ 115 | export function evaluateForWarnings(parsedCsps: Csp[]): Finding[] { 116 | // Check #1 is implemented by Lighthouse directly 117 | // Check #2 is no longer used in Lighthouse. 118 | 119 | // Check #3 120 | return [ 121 | ...atLeastOneFails(parsedCsps, checkUnsafeInlineFallback), 122 | ...atLeastOneFails(parsedCsps, checkAllowlistFallback) 123 | ]; 124 | } 125 | 126 | /** 127 | * Evaluate the given list of CSPs for syntax errors. Returns a list of the same 128 | * length as parsedCsps where each item in the list is the findings for the 129 | * matching Csp. 130 | */ 131 | export function evaluateForSyntaxErrors(parsedCsps: Csp[]): Finding[][] { 132 | // Check #4 133 | const allFindings: Finding[][] = []; 134 | for (const csp of parsedCsps) { 135 | const findings = [ 136 | ...checkNonceLength(csp), ...checkUnknownDirective(csp), 137 | ...checkDeprecatedDirective(csp), ...checkMissingSemicolon(csp), 138 | ...checkInvalidKeyword(csp) 139 | ]; 140 | allFindings.push(findings); 141 | } 142 | return allFindings; 143 | } 144 | -------------------------------------------------------------------------------- /lighthouse/lighthouse_checks_test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Tests for CSP Parser checks. 3 | */ 4 | 5 | import 'jasmine'; 6 | 7 | import {Csp,} from '../csp'; 8 | import {Severity} from '../finding'; 9 | import {CspParser} from '../parser'; 10 | 11 | import * as lighthouseChecks from './lighthouse_checks'; 12 | 13 | function parsePolicies(policies: string[]): Csp[] { 14 | return policies.map(p => (new CspParser(p)).csp); 15 | } 16 | 17 | describe('Test evaluateForFailure', () => { 18 | it('robust nonce-based policy', () => { 19 | const test = 20 | 'script-src \'nonce-aaaaaaaaaa\'; object-src \'none\'; base-uri \'none\''; 21 | 22 | const violations = 23 | lighthouseChecks.evaluateForFailure(parsePolicies([test])); 24 | 25 | expect(violations.length).toBe(0); 26 | }); 27 | it('robust hash-based policy', () => { 28 | const test = 'script-src \'sha256-aaaaaaaaaa\'; object-src \'none\''; 29 | 30 | const violations = 31 | lighthouseChecks.evaluateForFailure(parsePolicies([test])); 32 | 33 | expect(violations.length).toBe(0); 34 | }); 35 | it('policy not attempt', () => { 36 | const test = 'block-all-mixed-content'; 37 | 38 | const violations = 39 | lighthouseChecks.evaluateForFailure(parsePolicies([test])); 40 | 41 | expect(violations.length).toBe(2); 42 | expect(violations[0].severity).toBe(Severity.HIGH); 43 | expect(violations[0].directive).toBe('script-src'); 44 | expect(violations[0].description).toBe('script-src directive is missing.'); 45 | expect(violations[1].severity).toBe(Severity.HIGH); 46 | expect(violations[1].directive).toBe('object-src'); 47 | expect(violations[1].description) 48 | .toBe( 49 | `Missing object-src allows the injection of plugins which can execute JavaScript. Can you set it to 'none'?`); 50 | }); 51 | it('policy not robust', () => { 52 | const test = 'script-src *.google.com; object-src \'none\''; 53 | 54 | const violations = 55 | lighthouseChecks.evaluateForFailure(parsePolicies([test])); 56 | 57 | expect(violations.length).toBe(1); 58 | expect(violations[0].severity).toBe(Severity.STRICT_CSP); 59 | expect(violations[0].directive).toBe('script-src'); 60 | expect(violations[0].description) 61 | .toBe( 62 | `Host allowlists can frequently be bypassed. Consider using 'strict-dynamic' in combination with CSP nonces or hashes.`); 63 | }); 64 | it('robust policy and not robust policy', () => { 65 | const policies = [ 66 | 'script-src *.google.com; object-src \'none\'', 67 | 'script-src \'nonce-aaaaaaaaaa\'; base-uri \'none\'' 68 | ]; 69 | 70 | const violations = 71 | lighthouseChecks.evaluateForFailure(parsePolicies(policies)); 72 | 73 | expect(violations.length).toBe(0); 74 | }); 75 | it('split across many policies', () => { 76 | const policies = [ 77 | 'object-src \'none\'', 'script-src \'nonce-aaaaaaaaaa\'', 78 | 'base-uri \'none\'' 79 | ]; 80 | 81 | const violations = 82 | lighthouseChecks.evaluateForFailure(parsePolicies(policies)); 83 | 84 | expect(violations.length).toBe(0); 85 | }); 86 | it('split across many policies with default-src', () => { 87 | const policies = ['default-src \'none\'', 'base-uri \'none\'']; 88 | 89 | const violations = 90 | lighthouseChecks.evaluateForFailure(parsePolicies(policies)); 91 | 92 | expect(violations.length).toBe(0); 93 | }); 94 | it('split across many policies some mixed useless policies', () => { 95 | const policies = [ 96 | 'object-src \'none\'', 'script-src \'nonce-aaaaaaaaaa\'', 97 | 'base-uri \'none\'', 'block-all-mixed-content' 98 | ]; 99 | 100 | const violations = 101 | lighthouseChecks.evaluateForFailure(parsePolicies(policies)); 102 | 103 | expect(violations.length).toBe(0); 104 | }); 105 | it('split across many policies with allowlist', () => { 106 | const policies = [ 107 | 'object-src \'none\'', 'script-src \'nonce-aaaaaaaaaa\'', 108 | 'base-uri \'none\'', 'script-src *' 109 | ]; 110 | 111 | const violations = 112 | lighthouseChecks.evaluateForFailure(parsePolicies(policies)); 113 | 114 | expect(violations.length).toBe(0); 115 | }); 116 | 117 | it('not robust and not attempt', () => { 118 | const policies = ['block-all-mixed-content', 'script-src *.google.com']; 119 | 120 | const violations = 121 | lighthouseChecks.evaluateForFailure(parsePolicies(policies)); 122 | 123 | expect(violations.length).toBe(2); 124 | expect(violations[0].severity).toBe(Severity.HIGH); 125 | expect(violations[0].directive).toBe('object-src'); 126 | expect(violations[0].description) 127 | .toBe( 128 | `Missing object-src allows the injection of plugins which can execute JavaScript. Can you set it to 'none'?`); 129 | expect(violations[1].severity).toBe(Severity.STRICT_CSP); 130 | expect(violations[1].directive).toBe('script-src'); 131 | expect(violations[1].description) 132 | .toBe( 133 | `Host allowlists can frequently be bypassed. Consider using \'strict-dynamic\' in combination with CSP nonces or hashes.`); 134 | }); 135 | it('robust check only CSPs with script-src', () => { 136 | const policies = ['script-src https://example.com', 'object-src \'none\'']; 137 | 138 | const violations = 139 | lighthouseChecks.evaluateForFailure(parsePolicies(policies)); 140 | 141 | expect(violations.length).toBe(1); 142 | expect(violations[0].severity).toBe(Severity.STRICT_CSP); 143 | expect(violations[0].directive).toBe('script-src'); 144 | expect(violations[0].description) 145 | .toBe( 146 | `Host allowlists can frequently be bypassed. Consider using \'strict-dynamic\' in combination with CSP nonces or hashes.`); 147 | }); 148 | it('two not attempt', () => { 149 | const policies = ['block-all-mixed-content', 'block-all-mixed-content']; 150 | 151 | const violations = 152 | lighthouseChecks.evaluateForFailure(parsePolicies(policies)); 153 | 154 | expect(violations.length).toBe(2); 155 | expect(violations[0].severity).toBe(Severity.HIGH); 156 | expect(violations[0].directive).toBe('script-src'); 157 | expect(violations[0].description).toBe('script-src directive is missing.'); 158 | expect(violations[1].severity).toBe(Severity.HIGH); 159 | expect(violations[1].directive).toBe('object-src'); 160 | expect(violations[1].description) 161 | .toBe( 162 | `Missing object-src allows the injection of plugins which can execute JavaScript. Can you set it to 'none'?`); 163 | }); 164 | it('two not attempt somewhat', () => { 165 | const policies = [ 166 | 'block-all-mixed-content; object-src \'none\'', 167 | 'block-all-mixed-content', 168 | ]; 169 | 170 | const violations = 171 | lighthouseChecks.evaluateForFailure(parsePolicies(policies)); 172 | 173 | expect(violations.length).toBe(1); 174 | expect(violations[0].severity).toBe(Severity.HIGH); 175 | expect(violations[0].directive).toBe('script-src'); 176 | expect(violations[0].description).toBe('script-src directive is missing.'); 177 | }); 178 | it('base-uri split across many policies', () => { 179 | const policies = [ 180 | 'script-src \'nonce-aaaaaaaaaaa\'; object-src \'none\'', 181 | 'base-uri \'none\'', 182 | ]; 183 | 184 | const violations = 185 | lighthouseChecks.evaluateForFailure(parsePolicies(policies)); 186 | 187 | expect(violations.length).toBe(0); 188 | }); 189 | it('base-uri not set', () => { 190 | const policies = [ 191 | 'script-src \'nonce-aaaaaaaaaaa\'; object-src \'none\'', 192 | ]; 193 | 194 | const violations = 195 | lighthouseChecks.evaluateForFailure(parsePolicies(policies)); 196 | 197 | expect(violations.length).toBe(1); 198 | expect(violations[0].severity).toBe(Severity.HIGH); 199 | expect(violations[0].directive).toBe('base-uri'); 200 | expect(violations[0].description) 201 | .toBe( 202 | `Missing base-uri allows the injection of base tags. They can be used to set the base URL for all relative (script) URLs to an attacker controlled domain. Can you set it to 'none' or 'self'?`); 203 | }); 204 | it('base-uri not set in either policy', () => { 205 | const policies = [ 206 | 'script-src \'nonce-aaaaaaaaaaa\'; object-src \'none\'', 207 | 'block-all-mixed-content' 208 | ]; 209 | 210 | const violations = 211 | lighthouseChecks.evaluateForFailure(parsePolicies(policies)); 212 | 213 | expect(violations.length).toBe(1); 214 | expect(violations[0].severity).toBe(Severity.HIGH); 215 | expect(violations[0].directive).toBe('base-uri'); 216 | }); 217 | it('check wildcards', () => { 218 | const policies = ['script-src \'none\'; object-src *']; 219 | 220 | const violations = 221 | lighthouseChecks.evaluateForFailure(parsePolicies(policies)); 222 | 223 | expect(violations.length).toBe(1); 224 | expect(violations[0].severity).toBe(Severity.HIGH); 225 | expect(violations[0].directive).toBe('object-src'); 226 | expect(violations[0].description) 227 | .toBe(`object-src should not allow '*' as source`); 228 | }); 229 | it('check wildcards on multiple', () => { 230 | const policies = 231 | ['script-src \'none\'; object-src *', 'object-src \'none\'']; 232 | 233 | const violations = 234 | lighthouseChecks.evaluateForFailure(parsePolicies(policies)); 235 | 236 | expect(violations.length).toBe(0); 237 | }); 238 | it('check plain url schemes', () => { 239 | const policies = [ 240 | `script-src 'strict-dynamic' 'nonce-random123' 'unsafe-inline' https:; base-uri 'none'; object-src https:` 241 | ]; 242 | 243 | const violations = 244 | lighthouseChecks.evaluateForFailure(parsePolicies(policies)); 245 | 246 | expect(violations.length).toBe(1); 247 | expect(violations[0].severity).toBe(Severity.HIGH); 248 | expect(violations[0].directive).toBe('object-src'); 249 | expect(violations[0].description) 250 | .toBe( 251 | `https: URI in object-src allows the execution of unsafe scripts.`); 252 | }); 253 | }); 254 | describe('Test evaluateForWarnings', () => { 255 | it('perfect', () => { 256 | const test = 257 | 'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:; report-uri url'; 258 | 259 | const violations = 260 | lighthouseChecks.evaluateForWarnings(parsePolicies([test])); 261 | 262 | expect(violations.length).toBe(0); 263 | }); 264 | it('perfect except some failures', () => { 265 | const policies = [ 266 | 'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:; report-uri url; object-src \'none\'', 267 | 'block-all-mixed-content' 268 | ]; 269 | 270 | const violations = 271 | lighthouseChecks.evaluateForWarnings(parsePolicies(policies)); 272 | 273 | expect(violations.length).toBe(0); 274 | }); 275 | it('a perfect policy and a policy that does not target', () => { 276 | const policies = [ 277 | 'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:; report-uri url; base-uri \'none\'; object-src \'none\'', 278 | 'block-all-mixed-content' 279 | ]; 280 | 281 | const violations = 282 | lighthouseChecks.evaluateForWarnings(parsePolicies(policies)); 283 | 284 | expect(violations.length).toBe(0); 285 | }); 286 | it('perfect policy split into two', () => { 287 | const policies = [ 288 | 'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:; report-uri url; base-uri \'none\'; ', 289 | 'block-all-mixed-content; object-src \'none\'' 290 | ]; 291 | 292 | const violations = 293 | lighthouseChecks.evaluateForWarnings(parsePolicies(policies)); 294 | 295 | expect(violations.length).toBe(0); 296 | }); 297 | it('perfect policy split into three', () => { 298 | const policies = [ 299 | 'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:; report-uri url; base-uri \'none\'; ', 300 | 'block-all-mixed-content', 'object-src \'none\'' 301 | ]; 302 | 303 | const violations = 304 | lighthouseChecks.evaluateForWarnings(parsePolicies(policies)); 305 | 306 | expect(violations.length).toBe(0); 307 | }); 308 | it('no reporting and malformed', () => { 309 | const test = 'script-src \'nonce-aaaaaaaaaa\'; unknown-directive'; 310 | 311 | const violations = 312 | lighthouseChecks.evaluateForWarnings(parsePolicies([test])); 313 | 314 | expect(violations.length).toBe(1); 315 | expect(violations[0].severity).toBe(Severity.STRICT_CSP); 316 | expect(violations[0].directive).toBe('script-src'); 317 | expect(violations[0].description) 318 | .toBe( 319 | 'Consider adding \'unsafe-inline\' (ignored by browsers supporting nonces/hashes) to be backward compatible with older browsers.'); 320 | }); 321 | it('missing unsafe-inline fallback', () => { 322 | const test = 'script-src \'nonce-aaaaaaaaaa\'; report-uri url'; 323 | 324 | const violations = 325 | lighthouseChecks.evaluateForWarnings(parsePolicies([test])); 326 | 327 | expect(violations.length).toBe(1); 328 | expect(violations[0].severity).toBe(Severity.STRICT_CSP); 329 | expect(violations[0].directive).toBe('script-src'); 330 | expect(violations[0].description) 331 | .toBe( 332 | 'Consider adding \'unsafe-inline\' (ignored by browsers supporting nonces/hashes) to be backward compatible with older browsers.'); 333 | }); 334 | it('missing allowlist fallback', () => { 335 | const test = 336 | 'script-src \'nonce-aaaaaaaaaa\' \'strict-dynamic\' \'unsafe-inline\'; report-uri url'; 337 | 338 | const violations = 339 | lighthouseChecks.evaluateForWarnings(parsePolicies([test])); 340 | 341 | expect(violations.length).toBe(1); 342 | expect(violations[0].severity).toBe(Severity.STRICT_CSP); 343 | expect(violations[0].directive).toBe('script-src'); 344 | expect(violations[0].description) 345 | .toBe( 346 | 'Consider adding https: and http: url schemes (ignored by browsers supporting \'strict-dynamic\') to be backward compatible with older browsers.'); 347 | }); 348 | it('missing semicolon', () => { 349 | const test = 350 | 'script-src \'nonce-aaaaaaaaa\' \'unsafe-inline\'; report-uri url object-src \'self\''; 351 | 352 | const violations = 353 | lighthouseChecks.evaluateForWarnings(parsePolicies([test])); 354 | 355 | expect(violations.length).toBe(0); 356 | }); 357 | it('invalid keyword', () => { 358 | const test = 359 | 'script-src \'nonce-aaaaaaaaa\' \'invalid\' \'unsafe-inline\'; report-uri url'; 360 | 361 | const violations = 362 | lighthouseChecks.evaluateForWarnings(parsePolicies([test])); 363 | 364 | expect(violations.length).toBe(0); 365 | }); 366 | it('perfect policy and invalid policy', () => { 367 | const policies = [ 368 | 'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:; report-uri url; base-uri \'none\'; object-src \'none\'', 369 | 'unknown' 370 | ]; 371 | 372 | const violations = 373 | lighthouseChecks.evaluateForWarnings(parsePolicies(policies)); 374 | 375 | expect(violations.length).toBe(0); 376 | }); 377 | it('reporting on the wrong policy', () => { 378 | const policies = [ 379 | 'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:', 380 | 'block-all-mixed-content; report-uri url' 381 | ]; 382 | 383 | const violations = 384 | lighthouseChecks.evaluateForWarnings(parsePolicies(policies)); 385 | 386 | expect(violations.length).toBe(0); 387 | }); 388 | it('missing unsafe-inline fallback split over two policies', () => { 389 | const policies = [ 390 | 'script-src \'nonce-aaaaaaaaaa\'', 391 | 'block-all-mixed-content; report-uri url' 392 | ]; 393 | 394 | const violations = 395 | lighthouseChecks.evaluateForWarnings(parsePolicies(policies)); 396 | 397 | expect(violations.length).toBe(1); 398 | expect(violations[0].severity).toBe(Severity.STRICT_CSP); 399 | expect(violations[0].directive).toBe('script-src'); 400 | expect(violations[0].description) 401 | .toBe( 402 | 'Consider adding \'unsafe-inline\' (ignored by browsers supporting nonces/hashes) to be backward compatible with older browsers.'); 403 | }); 404 | it('strict-dynamic with no fallback in any policy', () => { 405 | const policies = [ 406 | 'script-src \'nonce-aaaaaaaaaa\' \'strict-dynamic\'', 407 | 'block-all-mixed-content; report-uri url' 408 | ]; 409 | 410 | const violations = 411 | lighthouseChecks.evaluateForWarnings(parsePolicies(policies)); 412 | 413 | expect(violations.length).toBe(2); 414 | expect(violations[0].severity).toBe(Severity.STRICT_CSP); 415 | expect(violations[0].directive).toBe('script-src'); 416 | expect(violations[0].description) 417 | .toBe( 418 | 'Consider adding \'unsafe-inline\' (ignored by browsers supporting nonces/hashes) to be backward compatible with older browsers.'); 419 | expect(violations[1].severity).toBe(Severity.STRICT_CSP); 420 | expect(violations[1].directive).toBe('script-src'); 421 | expect(violations[1].description) 422 | .toBe( 423 | 'Consider adding https: and http: url schemes (ignored by browsers supporting \'strict-dynamic\') to be backward compatible with older browsers.'); 424 | }); 425 | }); 426 | describe('Test evaluateForSyntaxErrors', () => { 427 | it('whenPerfectPolicies', () => { 428 | const policies = [ 429 | 'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:', 430 | 'block-all-mixed-content; report-uri url' 431 | ]; 432 | 433 | const violations = 434 | lighthouseChecks.evaluateForSyntaxErrors(parsePolicies(policies)); 435 | 436 | expect(violations.length).toBe(2); 437 | expect(violations[0].length).toBe(0); 438 | expect(violations[1].length).toBe(0); 439 | }); 440 | it('whenShortNonce', () => { 441 | const test = 'script-src \'nonce-a\' \'unsafe-inline\'; report-uri url'; 442 | 443 | const violations = 444 | lighthouseChecks.evaluateForSyntaxErrors(parsePolicies([test])); 445 | 446 | expect(violations.length).toBe(1); 447 | expect(violations[0].length).toBe(1); 448 | expect(violations[0][0].severity).toBe(Severity.MEDIUM); 449 | expect(violations[0][0].directive).toBe('script-src'); 450 | expect(violations[0][0].description) 451 | .toBe('Nonces should be at least 8 characters long.'); 452 | }); 453 | it('whenUnknownDirective', () => { 454 | const test = 455 | 'script-src \'nonce-aaaaaaaaa\' \'unsafe-inline\'; report-uri url; unknown'; 456 | 457 | const violations = 458 | lighthouseChecks.evaluateForSyntaxErrors(parsePolicies([test])); 459 | 460 | expect(violations.length).toBe(1); 461 | expect(violations[0].length).toBe(1); 462 | expect(violations[0][0].severity).toBe(Severity.SYNTAX); 463 | expect(violations[0][0].directive).toBe('unknown'); 464 | expect(violations[0][0].description) 465 | .toBe('Directive "unknown" is not a known CSP directive.'); 466 | }); 467 | it('whenDeprecatedDirective', () => { 468 | const test = 469 | 'script-src \'nonce-aaaaaaaaa\' \'unsafe-inline\'; report-uri url; reflected-xss foo'; 470 | 471 | const violations = 472 | lighthouseChecks.evaluateForSyntaxErrors(parsePolicies([test])); 473 | 474 | expect(violations.length).toBe(1); 475 | expect(violations[0].length).toBe(1); 476 | expect(violations[0][0].severity).toBe(Severity.INFO); 477 | expect(violations[0][0].directive).toBe('reflected-xss'); 478 | expect(violations[0][0].description) 479 | .toBe( 480 | 'reflected-xss is deprecated since CSP2. Please, use the X-XSS-Protection header instead.'); 481 | }); 482 | it('whenMissingSemicolon', () => { 483 | const test = 484 | 'script-src \'nonce-aaaaaaaaa\' \'unsafe-inline\'; report-uri url object-src \'none\''; 485 | 486 | const violations = 487 | lighthouseChecks.evaluateForSyntaxErrors(parsePolicies([test])); 488 | 489 | expect(violations.length).toBe(1); 490 | expect(violations[0].length).toBe(1); 491 | expect(violations[0][0].severity).toBe(Severity.SYNTAX); 492 | expect(violations[0][0].directive).toBe('report-uri'); 493 | expect(violations[0][0].description) 494 | .toBe( 495 | 'Did you forget the semicolon? "object-src" seems to be a directive, not a value.'); 496 | }); 497 | it('whenInvalidKeyword', () => { 498 | const test = 499 | 'script-src \'nonce-aaaaaaaaa\' \'unsafe-inline\'; object-src \'invalid\''; 500 | 501 | const violations = 502 | lighthouseChecks.evaluateForSyntaxErrors(parsePolicies([test])); 503 | 504 | expect(violations.length).toBe(1); 505 | expect(violations[0].length).toBe(1); 506 | expect(violations[0][0].severity).toBe(Severity.SYNTAX); 507 | expect(violations[0][0].directive).toBe('object-src'); 508 | expect(violations[0][0].description) 509 | .toBe('\'invalid\' seems to be an invalid CSP keyword.'); 510 | }); 511 | it('manyPolicies', () => { 512 | const policies = [ 513 | 'object-src \'invalid\'', 'script-src \'none\'', 514 | 'script-src \'nonce-short\' default-src \'none\'' 515 | ]; 516 | 517 | const violations = 518 | lighthouseChecks.evaluateForSyntaxErrors(parsePolicies(policies)); 519 | 520 | expect(violations.length).toBe(3); 521 | expect(violations[0].length).toBe(1); 522 | expect(violations[0][0].severity).toBe(Severity.SYNTAX); 523 | expect(violations[0][0].directive).toBe('object-src'); 524 | expect(violations[0][0].description) 525 | .toBe('\'invalid\' seems to be an invalid CSP keyword.'); 526 | expect(violations[1].length).toBe(0); 527 | expect(violations[2].length).toBe(2); 528 | expect(violations[2][0].severity).toBe(Severity.MEDIUM); 529 | expect(violations[2][0].directive).toBe('script-src'); 530 | expect(violations[2][0].description) 531 | .toBe('Nonces should be at least 8 characters long.'); 532 | expect(violations[2][1].severity).toBe(Severity.SYNTAX); 533 | expect(violations[2][1].directive).toBe('script-src'); 534 | expect(violations[2][1].description) 535 | .toBe( 536 | 'Did you forget the semicolon? "default-src" seems to be a directive, not a value.'); 537 | }); 538 | }); 539 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "csp_evaluator", 3 | "version": "1.0.4", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/jasmine": { 8 | "version": "3.6.7", 9 | "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.6.7.tgz", 10 | "integrity": "sha512-8dtfiykrpe4Ysn6ONj0tOjmpDIh1vWxPk80eutSeWmyaJvAZXZ84219fS4gLrvz05eidhp7BP17WVQBaXHSyXQ==", 11 | "dev": true 12 | }, 13 | "balanced-match": { 14 | "version": "1.0.0", 15 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 16 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 17 | "dev": true 18 | }, 19 | "brace-expansion": { 20 | "version": "1.1.11", 21 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 22 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 23 | "dev": true, 24 | "requires": { 25 | "balanced-match": "^1.0.0", 26 | "concat-map": "0.0.1" 27 | } 28 | }, 29 | "concat-map": { 30 | "version": "0.0.1", 31 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 32 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 33 | "dev": true 34 | }, 35 | "fs.realpath": { 36 | "version": "1.0.0", 37 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 38 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 39 | "dev": true 40 | }, 41 | "glob": { 42 | "version": "7.1.6", 43 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 44 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 45 | "dev": true, 46 | "requires": { 47 | "fs.realpath": "^1.0.0", 48 | "inflight": "^1.0.4", 49 | "inherits": "2", 50 | "minimatch": "^3.0.4", 51 | "once": "^1.3.0", 52 | "path-is-absolute": "^1.0.0" 53 | } 54 | }, 55 | "inflight": { 56 | "version": "1.0.6", 57 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 58 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 59 | "dev": true, 60 | "requires": { 61 | "once": "^1.3.0", 62 | "wrappy": "1" 63 | } 64 | }, 65 | "inherits": { 66 | "version": "2.0.4", 67 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 68 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 69 | "dev": true 70 | }, 71 | "jasmine": { 72 | "version": "3.7.0", 73 | "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.7.0.tgz", 74 | "integrity": "sha512-wlzGQ+cIFzMEsI+wDqmOwvnjTvolLFwlcpYLCqSPPH0prOQaW3P+IzMhHYn934l1imNvw07oCyX+vGUv3wmtSQ==", 75 | "dev": true, 76 | "requires": { 77 | "glob": "^7.1.6", 78 | "jasmine-core": "~3.7.0" 79 | } 80 | }, 81 | "jasmine-core": { 82 | "version": "3.7.1", 83 | "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.7.1.tgz", 84 | "integrity": "sha512-DH3oYDS/AUvvr22+xUBW62m1Xoy7tUlY1tsxKEJvl5JeJ7q8zd1K5bUwiOxdH+erj6l2vAMM3hV25Xs9/WrmuQ==", 85 | "dev": true 86 | }, 87 | "minimatch": { 88 | "version": "3.1.2", 89 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 90 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 91 | "dev": true, 92 | "requires": { 93 | "brace-expansion": "^1.1.7" 94 | } 95 | }, 96 | "once": { 97 | "version": "1.4.0", 98 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 99 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 100 | "dev": true, 101 | "requires": { 102 | "wrappy": "1" 103 | } 104 | }, 105 | "path-is-absolute": { 106 | "version": "1.0.1", 107 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 108 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 109 | "dev": true 110 | }, 111 | "typescript": { 112 | "version": "4.2.3", 113 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz", 114 | "integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==", 115 | "dev": true 116 | }, 117 | "wrappy": { 118 | "version": "1.0.2", 119 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 120 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 121 | "dev": true 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "csp_evaluator", 3 | "version": "1.1.4", 4 | "description": "Evaluate Content Security Policies for a wide range of bypasses and weaknesses", 5 | "main": "dist/evaluator.js", 6 | "keywords": [ 7 | "csp", 8 | "content security policy", 9 | "content-security-policy", 10 | "csp-evaluator" 11 | ], 12 | "homepage": "https://csp-evaluator.withgoogle.com/", 13 | "author": "Lukas Weichselbaum ", 14 | "license": "Apache-2.0", 15 | "prepublish": "tsc", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/google/csp-evaluator" 19 | }, 20 | "scripts": { 21 | "test": "tsc && npx jasmine --config=jasmine.json" 22 | }, 23 | "devDependencies": { 24 | "@types/jasmine": "^3.6.7", 25 | "jasmine": "^3.7.0", 26 | "typescript": "^4.2.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /parser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 Google Inc. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author lwe@google.com (Lukas Weichselbaum) 17 | */ 18 | 19 | import * as csp from './csp'; 20 | 21 | 22 | 23 | /** 24 | * A class to hold a parser for CSP in string format. 25 | * @unrestricted 26 | */ 27 | export class CspParser { 28 | csp: csp.Csp; 29 | /** 30 | * @param unparsedCsp A Content Security Policy as string. 31 | */ 32 | constructor(unparsedCsp: string) { 33 | /** 34 | * Parsed CSP 35 | */ 36 | this.csp = new csp.Csp(); 37 | 38 | this.parse(unparsedCsp); 39 | } 40 | 41 | /** 42 | * Parses a CSP from a string. 43 | * @param unparsedCsp CSP as string. 44 | */ 45 | parse(unparsedCsp: string): csp.Csp { 46 | // Reset the internal state: 47 | this.csp = new csp.Csp(); 48 | 49 | // Split CSP into directive tokens. 50 | const directiveTokens = unparsedCsp.split(';'); 51 | for (let i = 0; i < directiveTokens.length; i++) { 52 | const directiveToken = directiveTokens[i].trim(); 53 | 54 | // Split directive tokens into directive name and directive values. 55 | const directiveParts = directiveToken.match(/\S+/g); 56 | if (Array.isArray(directiveParts)) { 57 | const directiveName = directiveParts[0].toLowerCase(); 58 | 59 | // If the set of directives already contains a directive whose name is a 60 | // case insensitive match for directive name, ignore this instance of 61 | // the directive and continue to the next token. 62 | if (directiveName in this.csp.directives) { 63 | continue; 64 | } 65 | 66 | if (!csp.isDirective(directiveName)) { 67 | } 68 | 69 | const directiveValues: string[] = []; 70 | for (let directiveValue, j = 1; (directiveValue = directiveParts[j]); 71 | j++) { 72 | directiveValue = normalizeDirectiveValue(directiveValue); 73 | if (!directiveValues.includes(directiveValue)) { 74 | directiveValues.push(directiveValue); 75 | } 76 | } 77 | this.csp.directives[directiveName] = directiveValues; 78 | } 79 | } 80 | 81 | return this.csp; 82 | } 83 | } 84 | 85 | /** 86 | * Remove whitespaces and turn to lower case if CSP keyword or protocol 87 | * handler. 88 | * @param directiveValue directive value. 89 | * @return normalized directive value. 90 | */ 91 | function normalizeDirectiveValue(directiveValue: string): string { 92 | directiveValue = directiveValue.trim(); 93 | const directiveValueLower = directiveValue.toLowerCase(); 94 | if (csp.isKeyword(directiveValueLower) || csp.isUrlScheme(directiveValue)) { 95 | return directiveValueLower; 96 | } 97 | return directiveValue; 98 | } 99 | 100 | export const TEST_ONLY = {normalizeDirectiveValue}; 101 | -------------------------------------------------------------------------------- /parser_test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Tests for CSP Parser. 3 | * @author lwe@google.com (Lukas Weichselbaum) 4 | * 5 | * @license 6 | * Copyright 2016 Google Inc. All rights reserved. 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | import 'jasmine'; 21 | 22 | import {CspParser, TEST_ONLY} from './parser'; 23 | 24 | 25 | describe('Test parser', () => { 26 | it('CspParser', () => { 27 | const validCsp = // Test policy with different features from CSP2. 28 | 'default-src \'none\';' + 29 | 'script-src \'nonce-unsafefoobar\' \'unsafe-eval\' \'unsafe-hashes\' \'unsafe-inline\' \n' + 30 | 'https://example.com/foo.js foo.bar \'sha256-1DCfk1NYWuHMfoobarfoobar=\';' + 31 | 'script-src-elem \'self\' \'unsafe-inline\' https://apis.google.com https://www.googletagmanager.com https://www.google-analytics.com https://wchat.freshchat.com;' + 32 | 'object-src \'none\';' + 33 | 'img-src \'self\' https: data: blob:;' + 34 | 'style-src \'self\' \'unsafe-inline\' \'sha256-1DCfk1NYWuHMfoobarfoobar=\';' + 35 | 'style-src-elem \'self\' \'unsafe-inline\' https://fonts.googleapis.com https://fonts.gstatic.com;' + 36 | 'font-src *;' + 37 | 'child-src *.example.com:9090;' + 38 | 'upgrade-insecure-requests;\n' + 39 | 'report-uri /csp/test'; 40 | 41 | const parser = new (CspParser)(validCsp); 42 | const parsedCsp = parser.csp; 43 | 44 | // check directives 45 | const directives = Object.keys(parsedCsp.directives); 46 | const expectedDirectives = [ 47 | 'default-src', 'script-src', 'script-src-elem', 'object-src', 'img-src', 48 | 'style-src', 'style-src-elem', 'font-src', 'child-src', 49 | 'upgrade-insecure-requests', 'report-uri' 50 | ]; 51 | expect(expectedDirectives) 52 | .toEqual(jasmine.arrayWithExactContents(directives)); 53 | 54 | // check directive values 55 | expect(['\'none\'']) 56 | .toEqual(jasmine.arrayWithExactContents( 57 | parsedCsp.directives['default-src'] as string[])); 58 | 59 | expect([ 60 | '\'nonce-unsafefoobar\'', '\'unsafe-eval\'', '\'unsafe-hashes\'', 61 | '\'unsafe-inline\'', 'https://example.com/foo.js', 'foo.bar', 62 | '\'sha256-1DCfk1NYWuHMfoobarfoobar=\'' 63 | ]) 64 | .toEqual(jasmine.arrayWithExactContents( 65 | parsedCsp.directives['script-src'] as string[])); 66 | 67 | expect([ 68 | '\'self\'', '\'unsafe-inline\'', 'https://apis.google.com', 69 | 'https://www.googletagmanager.com', 'https://www.google-analytics.com', 70 | 'https://wchat.freshchat.com' 71 | ]) 72 | .toEqual(jasmine.arrayWithExactContents( 73 | parsedCsp.directives['script-src-elem'] as string[])); 74 | 75 | expect(['\'none\'']) 76 | .toEqual(jasmine.arrayWithExactContents( 77 | parsedCsp.directives['object-src'] as string[])); 78 | 79 | expect(['\'self\'', 'https:', 'data:', 'blob:']) 80 | .toEqual(jasmine.arrayWithExactContents( 81 | parsedCsp.directives['img-src'] as string[])); 82 | expect([ 83 | '\'self\'', '\'unsafe-inline\'', '\'sha256-1DCfk1NYWuHMfoobarfoobar=\'' 84 | ]) 85 | .toEqual(jasmine.arrayWithExactContents( 86 | parsedCsp.directives['style-src'] as string[])); 87 | expect([ 88 | '\'self\'', '\'unsafe-inline\'', 'https://fonts.googleapis.com', 89 | 'https://fonts.gstatic.com' 90 | ]) 91 | .toEqual(jasmine.arrayWithExactContents( 92 | parsedCsp.directives['style-src-elem'] as string[])); 93 | expect(['*']).toEqual(jasmine.arrayWithExactContents( 94 | parsedCsp.directives['font-src'] as string[])); 95 | expect(['*.example.com:9090']) 96 | .toEqual(jasmine.arrayWithExactContents( 97 | parsedCsp.directives['child-src'] as string[])); 98 | expect([]).toEqual(jasmine.arrayWithExactContents( 99 | parsedCsp.directives['upgrade-insecure-requests'] as string[])); 100 | expect(['/csp/test']) 101 | .toEqual(jasmine.arrayWithExactContents( 102 | parsedCsp.directives['report-uri'] as string[])); 103 | }); 104 | 105 | it('CspParserDuplicateDirectives', () => { 106 | const validCsp = 'default-src \'none\';' + 107 | 'default-src foo.bar;' + 108 | 'object-src \'none\';' + 109 | 'OBJECT-src foo.bar;'; 110 | 111 | const parser = new (CspParser)(validCsp); 112 | const parsedCsp = parser.csp; 113 | 114 | // check directives 115 | const directives = Object.keys(parsedCsp.directives); 116 | const expectedDirectives = ['default-src', 'object-src']; 117 | expect(expectedDirectives) 118 | .toEqual(jasmine.arrayWithExactContents(directives)); 119 | 120 | // check directive values 121 | expect(['\'none\'']) 122 | .toEqual(jasmine.arrayWithExactContents( 123 | parsedCsp.directives['default-src'] as string[])); 124 | expect(['\'none\'']) 125 | .toEqual(jasmine.arrayWithExactContents( 126 | parsedCsp.directives['object-src'] as string[])); 127 | }); 128 | 129 | it('CspParserMixedCaseKeywords', () => { 130 | const validCsp = 'DEFAULT-src \'NONE\';' + // Keywords should be 131 | // case insensetive. 132 | 'img-src \'sElf\' HTTPS: Example.com/CaseSensitive;'; 133 | 134 | const parser = new (CspParser)(validCsp); 135 | const parsedCsp = parser.csp; 136 | 137 | // check directives 138 | const directives = Object.keys(parsedCsp.directives); 139 | const expectedDirectives = ['default-src', 'img-src']; 140 | expect(expectedDirectives) 141 | .toEqual(jasmine.arrayWithExactContents(directives)); 142 | 143 | // check directive values 144 | expect(['\'none\'']) 145 | .toEqual(jasmine.arrayWithExactContents( 146 | parsedCsp.directives['default-src'] as string[])); 147 | expect(['\'self\'', 'https:', 'Example.com/CaseSensitive']) 148 | .toEqual(jasmine.arrayWithExactContents( 149 | parsedCsp.directives['img-src'] as string[])); 150 | }); 151 | 152 | it('NormalizeDirectiveValue', () => { 153 | expect(TEST_ONLY.normalizeDirectiveValue('\'nOnE\'')).toBe('\'none\''); 154 | expect(TEST_ONLY.normalizeDirectiveValue('\'nonce-aBcD\'')) 155 | .toBe('\'nonce-aBcD\''); 156 | expect(TEST_ONLY.normalizeDirectiveValue('\'hash-XyZ==\'')) 157 | .toBe('\'hash-XyZ==\''); 158 | expect(TEST_ONLY.normalizeDirectiveValue('HTTPS:')).toBe('https:'); 159 | expect(TEST_ONLY.normalizeDirectiveValue('example.com/TEST')) 160 | .toBe('example.com/TEST'); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "outDir": "dist", 9 | "removeComments": true, 10 | "strict": true, 11 | "esModuleInterop": true, 12 | "forceConsistentCasingInFileNames": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Utils for CSP evaluator. 3 | * @author lwe@google.com (Lukas Weichselbaum) 4 | * 5 | * @license 6 | * Copyright 2016 Google Inc. All rights reserved. 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | 21 | import * as csp from './csp'; 22 | 23 | 24 | /** 25 | * Removes scheme from url. 26 | * @param url Url. 27 | * @return url without scheme. 28 | */ 29 | export function getSchemeFreeUrl(url: string): string { 30 | url = url.replace(/^\w[+\w.-]*:\/\//i, ''); 31 | // Remove URL scheme. 32 | url = url.replace(/^\/\//, ''); 33 | // Remove protocol agnostic "//" 34 | return url; 35 | } 36 | 37 | /** 38 | * Get the hostname from the given url string in a way that supports schemeless 39 | * URLs and wildcards (aka `*`) in hostnames 40 | */ 41 | export function getHostname(url: string): string { 42 | const hostname = new URL( 43 | 'https://' + 44 | getSchemeFreeUrl(url) 45 | .replace(':*', '') // Remove wildcard port 46 | .replace('*', 'wildcard_placeholder')) 47 | .hostname.replace('wildcard_placeholder', '*'); 48 | 49 | // Some browsers strip the brackets from IPv6 addresses when you access the 50 | // hostname. If the scheme free url starts with something that vaguely looks 51 | // like an IPv6 address and our parsed hostname doesn't have the brackets, 52 | // then we add them back to work around this 53 | const ipv6Regex = /^\[[\d:]+\]/; 54 | if (getSchemeFreeUrl(url).match(ipv6Regex) && !hostname.match(ipv6Regex)) { 55 | return '[' + hostname + ']'; 56 | } 57 | return hostname; 58 | } 59 | 60 | function setScheme(u: string): string { 61 | if (u.startsWith('//')) { 62 | return u.replace('//', 'https://'); 63 | } 64 | return u; 65 | } 66 | 67 | /** 68 | * Searches for allowlisted CSP origin (URL with wildcards) in list of urls. 69 | * @param cspUrlString The allowlisted CSP origin. Can contain domain and 70 | * path wildcards. 71 | * @param listOfUrlStrings List of urls to search in. 72 | * @return First match found in url list, null otherwise. 73 | */ 74 | export function matchWildcardUrls( 75 | cspUrlString: string, listOfUrlStrings: string[]): URL|null { 76 | // non-Chromium browsers don't support wildcards in domain names. We work 77 | // around this by replacing the wildcard with `wildcard_placeholder` before 78 | // parsing the domain and using that as a magic string. This magic string is 79 | // encapsulated in this function such that callers of this function do not 80 | // have to worry about this detail. 81 | const cspUrl = 82 | new URL(setScheme(cspUrlString 83 | .replace(':*', '') // Remove wildcard port 84 | .replace('*', 'wildcard_placeholder'))); 85 | const listOfUrls = listOfUrlStrings.map(u => new URL(setScheme(u))); 86 | const host = cspUrl.hostname.toLowerCase(); 87 | const hostHasWildcard = host.startsWith('wildcard_placeholder.'); 88 | const wildcardFreeHost = host.replace(/^\wildcard_placeholder/i, ''); 89 | const path = cspUrl.pathname; 90 | const hasPath = path !== '/'; 91 | 92 | for (const url of listOfUrls) { 93 | const domain = url.hostname; 94 | if (!domain.endsWith(wildcardFreeHost)) { 95 | // Domains don't match. 96 | continue; 97 | } 98 | 99 | // If the host has no subdomain wildcard and doesn't match, continue. 100 | if (!hostHasWildcard && host !== domain) { 101 | continue; 102 | } 103 | 104 | // If the allowlisted url has a path, check if one of the url paths 105 | // match. 106 | if (hasPath) { 107 | // https://www.w3.org/TR/CSP2/#source-list-path-patching 108 | if (path.endsWith('/')) { 109 | if (!url.pathname.startsWith(path)) { 110 | continue; 111 | } 112 | } else { 113 | if (url.pathname !== path) { 114 | // Path doesn't match. 115 | continue; 116 | } 117 | } 118 | } 119 | 120 | // We found a match. 121 | return url; 122 | } 123 | 124 | // No match was found. 125 | return null; 126 | } 127 | 128 | 129 | /** 130 | * Applies a check to all directive values of a csp. 131 | * @param parsedCsp Parsed CSP. 132 | * @param check The check function that 133 | * should get applied on directive values. 134 | */ 135 | export function applyCheckFunktionToDirectives( 136 | parsedCsp: csp.Csp, 137 | check: (directive: string, directiveValues: string[]) => void, 138 | ) { 139 | const directiveNames = Object.keys(parsedCsp.directives); 140 | 141 | for (const directive of directiveNames) { 142 | const directiveValues = parsedCsp.directives[directive]; 143 | if (directiveValues) { 144 | check(directive, directiveValues); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /utils_test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 Google Inc. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author lwe@google.com (Lukas Weichselbaum) 17 | */ 18 | 19 | import 'jasmine'; 20 | 21 | import {getHostname, getSchemeFreeUrl, matchWildcardUrls} from './utils'; 22 | 23 | const TEST_BYPASSES = [ 24 | 'https://googletagmanager.com/gtm/js', 'https://www.google.com/jsapi', 25 | 'https://ajax.googleapis.com/ajax/services/feed/load' 26 | ]; 27 | 28 | describe('Test Utils', () => { 29 | it('GetSchemeFreeUrl', () => { 30 | expect(getSchemeFreeUrl('https://*')).toBe('*'); 31 | expect(getSchemeFreeUrl('//*')).toBe('*'); 32 | expect(getSchemeFreeUrl('*')).toBe('*'); 33 | expect(getSchemeFreeUrl('test//*')).toBe('test//*'); 34 | }); 35 | 36 | it('MatchWildcardUrlsMatchWildcardFreeHost', () => { 37 | const wildcardFreeHost = '//www.google.com'; 38 | const match = matchWildcardUrls(wildcardFreeHost, TEST_BYPASSES); 39 | expect(match!.hostname).toBe('www.google.com'); 40 | }); 41 | 42 | it('MatchWildcardUrlsNoMatch', () => { 43 | const wildcardFreeHost = '//www.foo.bar'; 44 | const match = matchWildcardUrls(wildcardFreeHost, TEST_BYPASSES); 45 | expect(match).toBeNull(); 46 | }); 47 | 48 | it('MatchWildcardUrlsMatchWildcardHost', () => { 49 | const wildcardHost = '//*.google.com'; 50 | const match = matchWildcardUrls(wildcardHost, TEST_BYPASSES); 51 | expect(match!.hostname).toBe('www.google.com'); 52 | }); 53 | 54 | it('MatchWildcardUrlsNoMatchWildcardHost', () => { 55 | const wildcardHost = '//*.www.google.com'; 56 | const match = matchWildcardUrls(wildcardHost, TEST_BYPASSES); 57 | expect(match).toBeNull(); 58 | }); 59 | 60 | it('MatchWildcardUrlsMatchWildcardHostWithPath', () => { 61 | const wildcardHostWithPath = '//*.google.com/jsapi'; 62 | const match = matchWildcardUrls(wildcardHostWithPath, TEST_BYPASSES); 63 | expect(match!.hostname).toBe('www.google.com'); 64 | }); 65 | 66 | it('MatchWildcardUrlsNoMatchWildcardHostWithPath', () => { 67 | const wildcardHostWithPath = '//*.google.com/wrongPath'; 68 | const match = matchWildcardUrls(wildcardHostWithPath, TEST_BYPASSES); 69 | expect(match).toBeNull(); 70 | }); 71 | 72 | it('MatchWildcardUrlsMatchHostWithPathWildcard', () => { 73 | const hostWithPath = '//ajax.googleapis.com/ajax/'; 74 | const match = matchWildcardUrls(hostWithPath, TEST_BYPASSES); 75 | expect(match!.hostname).toBe('ajax.googleapis.com'); 76 | }); 77 | 78 | it('MatchWildcardUrlsNoMatchHostWithoutPathWildcard', () => { 79 | const hostWithPath = '//ajax.googleapis.com/ajax'; 80 | const match = matchWildcardUrls(hostWithPath, TEST_BYPASSES); 81 | expect(match).toBeNull(); 82 | }); 83 | 84 | it('GetHostname', () => { 85 | expect(getHostname('https://www.google.com')).toBe('www.google.com'); 86 | }); 87 | 88 | it('GetHostnamePort', () => { 89 | expect(getHostname('https://www.google.com:8080')).toBe('www.google.com'); 90 | }); 91 | 92 | it('GetHostnameWildcardPort', () => { 93 | expect(getHostname('https://www.google.com:*')).toBe('www.google.com'); 94 | }); 95 | 96 | it('GetHostnameNoProtocol', () => { 97 | expect(getHostname('www.google.com')).toBe('www.google.com'); 98 | }); 99 | 100 | it('GetHostnameDoubleSlashProtocol', () => { 101 | expect(getHostname('//www.google.com')).toBe('www.google.com'); 102 | }); 103 | 104 | it('GetHostnameWildcard', () => { 105 | expect(getHostname('//*.google.com')).toBe('*.google.com'); 106 | }); 107 | 108 | it('GetHostnameWithPath', () => { 109 | expect(getHostname('//*.google.com/any/path')).toBe('*.google.com'); 110 | }); 111 | 112 | it('GetHostnameJustWildcard', () => { 113 | expect(getHostname('*')).toBe('*'); 114 | }); 115 | 116 | it('GetHostnameWildcardWithProtocol', () => { 117 | expect(getHostname('https://*')).toBe('*'); 118 | }); 119 | 120 | it('GetHostnameNonsense', () => { 121 | expect(getHostname('unsafe-inline')).toBe('unsafe-inline'); 122 | }); 123 | 124 | it('GetHostnameIPv4', () => { 125 | expect(getHostname('1.2.3.4')).toBe('1.2.3.4'); 126 | }); 127 | 128 | it('GetHostnameIPv6', () => { 129 | expect(getHostname('[::1]')).toBe('[::1]'); 130 | }); 131 | 132 | it('GetHostnameIPv4WithFullProtocol', () => { 133 | expect(getHostname('https://1.2.3.4')).toBe('1.2.3.4'); 134 | }); 135 | 136 | it('GetHostnameIPv6WithFullProtocol', () => { 137 | expect(getHostname('http://[::1]')).toBe('[::1]'); 138 | }); 139 | 140 | it('GetHostnameIPv4WithPartialProtocol', () => { 141 | expect(getHostname('//1.2.3.4')).toBe('1.2.3.4'); 142 | }); 143 | 144 | it('GetHostnameIPv6WithPartialProtocol', () => { 145 | expect(getHostname('//[::1]')).toBe('[::1]'); 146 | }); 147 | }); 148 | --------------------------------------------------------------------------------