From 3715b157ac02792e97b5c947ee4fffbf207e3f05 Mon Sep 17 00:00:00 2001 From: molant Date: Fri, 9 Mar 2018 09:02:08 -0800 Subject: [PATCH 1/3] New: Rule SRI Close #26 --- packages/rule-sri/CHANGELOG.md | 0 packages/rule-sri/LICENSE.txt | 201 +++++++++++++ packages/rule-sri/README.md | 145 +++++++++ packages/rule-sri/package.json | 69 +++++ packages/rule-sri/src/index.ts | 7 + packages/rule-sri/src/rule.ts | 313 ++++++++++++++++++++ packages/rule-sri/tests/fixtures/scripts.js | 1 + packages/rule-sri/tests/fixtures/styles.css | 3 + packages/rule-sri/tests/tests-http.ts | 22 ++ packages/rule-sri/tests/tests-https.ts | 249 ++++++++++++++++ packages/rule-sri/tsconfig.json | 14 + yarn.lock | 2 +- 12 files changed, 1025 insertions(+), 1 deletion(-) create mode 100644 packages/rule-sri/CHANGELOG.md create mode 100644 packages/rule-sri/LICENSE.txt create mode 100644 packages/rule-sri/README.md create mode 100644 packages/rule-sri/package.json create mode 100644 packages/rule-sri/src/index.ts create mode 100644 packages/rule-sri/src/rule.ts create mode 100644 packages/rule-sri/tests/fixtures/scripts.js create mode 100644 packages/rule-sri/tests/fixtures/styles.css create mode 100644 packages/rule-sri/tests/tests-http.ts create mode 100644 packages/rule-sri/tests/tests-https.ts create mode 100644 packages/rule-sri/tsconfig.json diff --git a/packages/rule-sri/CHANGELOG.md b/packages/rule-sri/CHANGELOG.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/rule-sri/LICENSE.txt b/packages/rule-sri/LICENSE.txt new file mode 100644 index 00000000000..540e41dcbd4 --- /dev/null +++ b/packages/rule-sri/LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright JS Foundation and other contributors, https://js.foundation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/rule-sri/README.md b/packages/rule-sri/README.md new file mode 100644 index 00000000000..757732b988a --- /dev/null +++ b/packages/rule-sri/README.md @@ -0,0 +1,145 @@ +# Require scripts and styles to use subresource integrity (`sri`) + +`sri` warns about requesting scripts or styles without using Subresource +integrity. + +## Why is this important? + +Nowadays it's very common to use third party resources from CDNs or different +services (analytics, ads, etc.), and thus, increasing the risk surface of your +web application. + +While there are techniques to verify the agent is talking with the right server +(TLS, HSTS, etc.), an attacker (or administrator) with access to the server can +manipulate the content with impunity. + +> If you want to load a crypto miner on 1,000+ websites you don't attack 1,000+ +websites, you attack the 1 website that they all load content from. +([Scott Helme][weak link]) + +Subresource integrity [is a standard][sri spec] that mitigates this by ensuring +that an exact representation of a resource, and only that representation, loads +and executes. + +## What does the rule check? + +This rule checks that a website uses correctly SRI, more especifically: + +* All the downloaded resources by an ` +``` + +Cross-origin resource with no `crossorigin` attribute: + +```html + +``` + +Cross-origin resource with invalid `crossorigin` attribute: + +```html + +``` + +Cross-origin resource loaded over `HTTP`: + +```html + +``` + +### Examples that **pass** the rule + +Same-origin resource with `sha384` or better: + +```html + +``` + +Same-origin resource with multiple hashes and `sha384` is one of them: + +```html + +``` + +Cross-origin resource with valid `crossorigin` attribute: + +```html + +``` + +## Can the rule be configured? + +Yes, by default the baseline algorithm is `sha384` but you can change it to +`sha256`, or `sha512`: + +```json +"sri": ["warning", { + "baseline": "sha512" +}] +``` + +The above will validate that the `integrity` of all scripts and styles use +`sha512`. + +## Further Reading + +* [Using Subresource Integrity - by Frederik Braun][using sri] +* [Subresource Integrity specification][sri spec] +* [Protect your site from Cryptojacking with CSP + SRI - by Scott Helme][prevent cryptojacking] +* [SRI Hash Generator][srihash generator] + + + +[collisions]: https://w3c.github.io/webappsec-subresource-integrity/#hash-collision-attacks +[crossorigin]: https://w3c.github.io/webappsec-subresource-integrity/#is-response-eligible +[prevent cryptojacking]: https://scotthelme.co.uk/protect-site-from-cryptojacking-csp-sri/ +[secure context]: https://w3c.github.io/webappsec-subresource-integrity/#non-secure-contexts +[sri format]: https://w3c.github.io/webappsec-subresource-integrity/#resource-integrity +[sri spec]: https://w3c.github.io/webappsec-subresource-integrity/ +[srihash generator]: https://www.srihash.org/ +[using sri]: https://frederik-braun.com/using-subresource-integrity.html +[weak link]: https://scotthelme.co.uk/protect-site-from-cryptojacking-csp-sri/#theweaklink diff --git a/packages/rule-sri/package.json b/packages/rule-sri/package.json new file mode 100644 index 00000000000..079226764ae --- /dev/null +++ b/packages/rule-sri/package.json @@ -0,0 +1,69 @@ +{ + "ava": { + "failFast": false, + "files": [ + "dist/tests/**/*.js" + ], + "timeout": "1m" + }, + "dependencies": { + "async": "^2.6.0" + }, + "description": "Require scripts and styles to use Subresource Integrity", + "devDependencies": { + "ava": "^0.25.0", + "cpx": "^1.5.0", + "eslint": "^4.18.2", + "eslint-plugin-markdown": "^1.0.0-beta.7", + "eslint-plugin-typescript": "^0.10.0", + "markdownlint-cli": "^0.7.0", + "npm-run-all": "^4.1.2", + "nyc": "^11.4.1", + "rimraf": "^2.6.2", + "sonarwhal": "^1.0.3", + "typescript": "2.7.2", + "typescript-eslint-parser": "^14.0.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "files": [ + "dist/src", + "npm-shrinkwrap.json" + ], + "homepage": "https://sonarwhal.com/", + "keywords": [ + "rule", + "sonarwhal", + "sri", + "sri-rule" + ], + "license": "Apache-2.0", + "main": "dist/src/index.js", + "name": "@sonarwhal/rule-sri", + "nyc": { + "extends": "../../../../.nycrc" + }, + "peerDependencies": { + "sonarwhal": "^1.0.3" + }, + "private": true, + "repository": "sonarwhal/sonarwhal", + "scripts": { + "build": "npm run clean && npm-run-all build:*", + "build-release": "npm run clean && npm run build:assets && tsc --inlineSourceMap false --removeComments true", + "build:assets": "cpx \"./{src,tests}/**/{!(*.ts),.!(ts)}\" dist", + "build:ts": "tsc", + "clean": "rimraf dist", + "lint": "npm-run-all lint:*", + "lint:js": "eslint . --cache --ext js --ext md --ext ts --ignore-path ../../.eslintignore --report-unused-disable-directives", + "lint:md": "markdownlint *.md", + "sonarwhal": "node node_modules/sonarwhal/dist/src/bin/sonarwhal.js", + "test": "npm run lint && npm run build && nyc ava", + "watch": "npm run build && npm-run-all --parallel -c watch:*", + "watch:assets": "npm run build:assets -- -w --no-initial", + "watch:test": "ava --watch", + "watch:ts": "npm run build:ts -- --watch" + }, + "version": "1.0.0" +} diff --git a/packages/rule-sri/src/index.ts b/packages/rule-sri/src/index.ts new file mode 100644 index 00000000000..1fecdd1935a --- /dev/null +++ b/packages/rule-sri/src/index.ts @@ -0,0 +1,7 @@ +/** + * @fileoverview Require scripts and styles to use Subresource Integrity + */ + +import * as sri from './rule'; + +module.exports = { sri }; diff --git a/packages/rule-sri/src/rule.ts b/packages/rule-sri/src/rule.ts new file mode 100644 index 00000000000..bb3d74e8601 --- /dev/null +++ b/packages/rule-sri/src/rule.ts @@ -0,0 +1,313 @@ +/** + * @fileoverview Require scripts and styles to use Subresource Integrity + */ + +import * as crypto from 'crypto'; +import { URL } from 'url'; +import { promisify } from 'util'; + +import * as async from 'async'; + +import { Category } from 'sonarwhal/dist/src/lib/enums/category'; +import { RuleContext } from 'sonarwhal/dist/src/lib/rule-context'; +import { IRule, RuleMetadata, FetchEnd } from 'sonarwhal/dist/src/lib/types'; +import { debug as d } from 'sonarwhal/dist/src/lib/utils/debug'; +import { normalizeString } from 'sonarwhal/dist/src/lib/utils/misc'; +import { RuleScope } from 'sonarwhal/dist/src/lib/enums/rulescope'; + +const debug: debug.IDebugger = d(__filename); +const everySeries = promisify(async.everySeries); + +/* + * ------------------------------------------------------------------------------ + * Public + * ------------------------------------------------------------------------------ + */ + +// We don't do a `const enum` because of this: https://stackoverflow.com/questions/18111657/how-does-one-get-the-names-of-typescript-enum-entries#comment52596297_18112157 +enum algorithms { + sha256 = 1, + sha384 = 2, + sha512 = 3 +} + +export default class SRIRule implements IRule { + + public static readonly meta: RuleMetadata = { + docs: { + category: Category.security, + description: `Require scripts and link elements to use Subresource Integrity` + }, + id: 'sri', + schema: [{ + additionalProperties: false, + properties: { + baseline: { + oneOf: [Object.keys(algorithms)], + type: 'string' + } + } + }], + scope: RuleScope.any + } + + private resources: Map; + private context: RuleContext; + private origin: string; + private baseline: string = 'sha384'; + + /** + * Returns the hash of the content for the given `sha` strengh in a format + * valid with SRI: + * * base64 + * * `sha384-hash` + */ + private calculateHash(content: string, sha): string { + const hash = crypto + .createHash(sha) + .update(content) + .digest('base64'); + + return hash; + } + + /** + * Checks if the element that originated the request/response is a script or a stylesheet. + * There could be other downloads from a `link` element that are not stylesheets and should + * be ignored. + */ + private isScriptOrLink(evt: FetchEnd): Promise { + debug('Is ` + * * `` + * + * https://w3c.github.io/webappsec-subresource-integrity/#agility + */ + private async isIntegrityFormatValid(evt: FetchEnd): Promise { + debug('Is integrity attribute valid?'); + const { element, resource } = evt; + const integrity = element.getAttribute('integrity'); + const integrityRegExp = /^sha(256|384|512)-/; + const integrityValues = integrity.split(/\s+/); + let highestAlgorithmPriority = 0; + const that = this; + + const areFormatsValid = await everySeries(integrityValues, async (integrityValue) => { + const results = integrityRegExp.exec(integrityValue); + const isValid = Array.isArray(results); + + if (!isValid) { + await that.context.report(resource, element, `The format of the "integrity" attribute should be "sha(256|384|512)-HASH": ${integrity.substr(0, 10)}…`); + + return false; + } + + const algorithm = `sha${results[1]}`; + const algorithmPriority = algorithms[algorithm]; + + highestAlgorithmPriority = Math.max(algorithmPriority, highestAlgorithmPriority); + + return true; + }); + + if (!areFormatsValid) { + return false; + } + + const baseline = algorithms[this.baseline]; + const meetsBaseline = highestAlgorithmPriority >= baseline; + + if (!meetsBaseline) { + await this.context.report(resource, element, `The hash algorithm "${algorithms[highestAlgorithmPriority]}" doesn't meet the baseline "${this.baseline}"`); + } + + return meetsBaseline; + } + + /** + * Checks if the resources is being delivered via HTTPS. + * + * More info: https://w3c.github.io/webappsec-subresource-integrity/#non-secure-contexts + */ + private async isSecureContext(evt: FetchEnd): Promise { + debug('Is delivered on a secure context?'); + const { element, resource } = evt; + const protocol = new URL(resource).protocol; + const isSecure = protocol === 'https:'; + + if (!isSecure) { + await this.context.report(resource, element, `The resource is not delivered via a secure context`); + } + + return isSecure; + } + + /** + * Calculates if the hash is the right one for the downloaded resource. + * + * An `integrity` attribute can have multiple hashes for the same algorithm and it will + * pass as long as one validates. + * + * More info: https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist + */ + private async hasRightHash(evt: FetchEnd): Promise { + debug('Does it have the right hash?'); + const { element, resource, response } = evt; + const integrity = element.getAttribute('integrity'); + const integrities = integrity.split(/\s+/); + const calculatedHashes: Map = new Map(); + // const that = this; + + const isOK = integrities.some((integrityValue) => { + const integrityRegExp = /^sha(256|384|512)-(.*)$/; + const [, bits, hash] = integrityRegExp.exec(integrityValue); + const calculatedHash = calculatedHashes.has(bits) ? + calculatedHashes.get(bits) : + this.calculateHash(response.body.content, `sha${bits}`); + + calculatedHashes.set(bits, calculatedHash); + + return hash === calculatedHash; + }); + + if (!isOK) { + const hashes: Array = []; + + calculatedHashes.forEach((value, key) => { + hashes.push(`sha${key}-${value}`); + }); + + await this.context.report(resource, element, `The hash in the "integrity" attribute doesn't match the received payload. +Expected: ${integrities.join(', ')} +Actual: ${hashes.join(', ')}`); + } + + return isOK; + } + + /** Validation entry point. */ + private async validateResource(evt: FetchEnd) { + + const validations = [ + this.isScriptOrLink, + this.isEligibleForIntegrityValidation, + this.hasIntegrityAttribute, + this.isIntegrityFormatValid, + this.isSecureContext, + this.hasRightHash + ].map((fn) => { + // Otherwise `this` will be undefined when we call to the fn inside `every` + return fn.bind(this); + }); + + debug(`Validating integrity of: ${evt.resource}`); + + await everySeries(validations, async (validation) => { + return await validation(evt); + }); + } + + /** Sets the `origin` property using the initial request. */ + private setOrigin(evt: FetchEnd): void { + const { resource } = evt; + + this.origin = new URL(resource).origin; // Our @types/node doesn't have it + } + + public constructor(context: RuleContext) { + this.context = context; + this.resources = new Map(); + this.baseline = context.ruleOptions ? + context.ruleOptions.baseline : + this.baseline; + + context.on('fetch::end::script', this.validateResource.bind(this)); + context.on('fetch::end::css', this.validateResource.bind(this)); + context.on('fetch::end::html', this.setOrigin.bind(this)); + } +} diff --git a/packages/rule-sri/tests/fixtures/scripts.js b/packages/rule-sri/tests/fixtures/scripts.js new file mode 100644 index 00000000000..a8a3f974b6b --- /dev/null +++ b/packages/rule-sri/tests/fixtures/scripts.js @@ -0,0 +1 @@ +console.log('alert!'); diff --git a/packages/rule-sri/tests/fixtures/styles.css b/packages/rule-sri/tests/fixtures/styles.css new file mode 100644 index 00000000000..f4b24d8a6d9 --- /dev/null +++ b/packages/rule-sri/tests/fixtures/styles.css @@ -0,0 +1,3 @@ +body { + background: #000; +} diff --git a/packages/rule-sri/tests/tests-http.ts b/packages/rule-sri/tests/tests-http.ts new file mode 100644 index 00000000000..f55eabe05ea --- /dev/null +++ b/packages/rule-sri/tests/tests-http.ts @@ -0,0 +1,22 @@ +import { generateHTMLPage } from 'sonarwhal/dist/tests/helpers/misc'; +import { getRuleName } from 'sonarwhal/dist/src/lib/utils/rule-helpers'; +import { readFile } from 'sonarwhal/dist/src/lib/utils/misc'; +import { RuleTest } from 'sonarwhal/dist/tests/helpers/rule-test-type'; +import * as ruleRunner from 'sonarwhal/dist/tests/helpers/rule-runner'; + +const ruleName = getRuleName(__dirname); + +const styles = readFile(`${__dirname}/fixtures/styles.css`); + +const defaultTestsHttp: Array = [ + { + name: `Page with a same-origin resource and SRI sha384 fails if content is delivered via http`, + reports: [{ message: 'The resource is not delivered via a secure context' }], + serverConfig: { + '/': generateHTMLPage(''), + '/styles.css': styles + } + } +]; + +ruleRunner.testRule(ruleName, defaultTestsHttp); diff --git a/packages/rule-sri/tests/tests-https.ts b/packages/rule-sri/tests/tests-https.ts new file mode 100644 index 00000000000..628fab17931 --- /dev/null +++ b/packages/rule-sri/tests/tests-https.ts @@ -0,0 +1,249 @@ +import { generateHTMLPage } from 'sonarwhal/dist/tests/helpers/misc'; +import { getRuleName } from 'sonarwhal/dist/src/lib/utils/rule-helpers'; +import { readFile } from 'sonarwhal/dist/src/lib/utils/misc'; +import { RuleTest } from 'sonarwhal/dist/tests/helpers/rule-test-type'; +import * as ruleRunner from 'sonarwhal/dist/tests/helpers/rule-runner'; + +const ruleName = getRuleName(__dirname); + +const styles = readFile(`${__dirname}/fixtures/styles.css`); +const scripts = readFile(`${__dirname}/fixtures/scripts.js`); + +const defaultTestsHttps: Array = [ + { + name: 'Page with no resources passes', + serverConfig: generateHTMLPage() + }, + { + name: `Page with a same-origin resource and no SRI fails`, + reports: [{ message: 'Resource https://localhost/styles.css requested without the "integrity" attribute' }], + serverConfig: { + '/': generateHTMLPage(''), + '/styles.css': styles + } + }, + { + name: `Page with a same-origin resource and SRI sha256 fails`, + reports: [{ message: `The hash algorithm "sha256" doesn't meet the baseline "sha384"` }], + serverConfig: { + '/': generateHTMLPage(''), + '/styles.css': styles + } + }, + { + name: `Page with a same-origin and SRI sha384 passes`, + serverConfig: { + '/': generateHTMLPage(''), + '/styles.css': styles + } + }, + { + name: `Page with a same-origin and SRI sha512 passes`, + serverConfig: { + '/': generateHTMLPage(''), + '/styles.css': styles + } + }, + { + name: `Page with a same-origin and invalid SRI sha384 fails`, + reports: [{ + message: `The hash in the "integrity" attribute doesn't match the received payload. +Expected: sha384-thisIsAnInvalidHash +Actual: sha384-lai7vFxeX5cfA6yRNCr/WHChPKVsaaYLX1IC1j+GOyS6RWj/BqI8bHH8AP2HPwv4` + }], + serverConfig: { + '/': generateHTMLPage(''), + '/styles.css': styles + } + }, + { + name: `Page with a same-origin and invalid SRI sha512 fails`, + reports: [{ + message: `The hash in the "integrity" attribute doesn't match the received payload. +Expected: sha512-thisIsAnInvalidHash +Actual: sha512-qC6bbhWZ7Rr0ACjhjfJpavLUm3oAUCbcheJUYNSb4DKASapgeWGLZBGXLTsoaASFg1VeCzTKs1QIMkWaL1ewsA==` + }], + serverConfig: { + '/': generateHTMLPage(''), + '/styles.css': styles + } + }, + { + name: `Page with a same-origin and SRI md5 fails`, + reports: [{ message: `The format of the "integrity" attribute should be "sha(256|384|512)-HASH": md5-KN0EFM…` }], + serverConfig: { + '/': generateHTMLPage(''), + '/styles.css': styles + } + }, + { + name: `Page with multiple same-origin resources and one without SRI fails`, + reports: [{ message: 'Resource https://localhost/scripts.js requested without the "integrity" attribute' }], + serverConfig: { + '/': generateHTMLPage(` + `), + '/scripts.js': scripts, + '/styles.css': styles + } + }, + { + name: `Page with multiple same origin resources with SRI passes`, + serverConfig: { + '/': generateHTMLPage(` + `), + '/scripts.js': scripts, + '/styles.css': styles + } + }, + { + name: `Page with cross-origin script with SRI and not "crossorigin" fails`, + reports: [{ message: 'Cross-origin scripts need a "crossorigin" attribute to be elegible for integrity validation' }], + serverConfig: { + '/': generateHTMLPage(` + `), + '/styles.css': styles + } + }, + { + name: `Page with cross-origin script with SRI and 'crossorigin="anonymous"' passes`, + serverConfig: { + '/': generateHTMLPage(` + `), + '/styles.css': styles + } + }, + { + name: `Page with cross-origin script with SRI and 'crossorigin="use-credentials"' passes`, + serverConfig: { + '/': generateHTMLPage(` + `), + '/styles.css': styles + } + }, + { + name: `Page with cross-origin script with SRI and 'crossorigin="invalid"' fails`, + reports: [{ message: `Attribute "crossorigin" doesn't have a valid value, should "anonymous" or "use-credentials": crossorigin="invalid"` }], + serverConfig: { + '/': generateHTMLPage(` + `), + '/styles.css': styles + } + }, + { + name: `Page with same-origin resource and multiple algorithms passes if highest >= 384`, + serverConfig: { + '/': generateHTMLPage(``), + '/styles.css': styles + } + }, + { + name: `Page with same-origin resource and multiple algorithms passes if highest >= 384 regardless of the order`, + serverConfig: { + '/': generateHTMLPage(``), + '/styles.css': styles + } + }, + { + name: `Page with same-origin resource and different hashes for the same algorithm passes if one matches`, + serverConfig: { + '/': generateHTMLPage(``), + '/styles.css': styles + } + }, + { + name: `Page with same-origin resource and different hashes for the same algorithm fails if none match`, + reports: [{ + message: `The hash in the "integrity" attribute doesn't match the received payload. +Expected: sha384-randomHash1, sha384-randomHash2 +Actual: sha384-lai7vFxeX5cfA6yRNCr/WHChPKVsaaYLX1IC1j+GOyS6RWj/BqI8bHH8AP2HPwv4` + }], + serverConfig: { + '/': generateHTMLPage(``), + '/styles.css': styles + } + }, + { + name: `Page with same-origin resource and multiple "integrity" attributes and the first one is valid, passes`, + serverConfig: { + '/': generateHTMLPage(``), + '/styles.css': styles + } + } + // Chrome downloads the file twice if 2 integrity attributes are present and the first one is invalid (only the first integrity is used in both cases) + /* + * { + * name: `Page with same-origin resource and multiple "integrity" attributes and the first one is invalid, fails`, + * reports: [{ message: `The hash in the "integrity" attribute doesn't match the received payload` }], + * serverConfig: { + * '/': generateHTMLPage(``), + * '/styles.css': styles + * } + * } + */ +]; + +const configTestsHigh: Array = [ + { + name: `Page with a same-origin resource and SRI sha256 fails if baseline is 512`, + reports: [{ message: `The hash algorithm "sha256" doesn't meet the baseline "sha512"` }], + serverConfig: { + '/': generateHTMLPage(''), + '/styles.css': styles + } + }, + { + name: `Page with a same-origin resource and SRI sha384 fails if baseline is 512`, + reports: [{ message: `The hash algorithm "sha384" doesn't meet the baseline "sha512"` }], + serverConfig: { + '/': generateHTMLPage(''), + '/styles.css': styles + } + }, + { + name: `Page with a same-origin resource and SRI sha512 passes if baseline is 512`, + serverConfig: { + '/': generateHTMLPage(''), + '/styles.css': styles + } + } +]; + +const configTestsLow: Array = [ + { + name: `Page with a same-origin resource and SRI sha256 passes if baseline is 256`, + serverConfig: { + '/': generateHTMLPage(''), + '/styles.css': styles + } + }, + { + name: `Page with a same-origin resource and SRI sha384 passes if baseline is 256`, + serverConfig: { + '/': generateHTMLPage(''), + '/styles.css': styles + } + }, + { + name: `Page with a same-origin resource and SRI sha256 passes if baseline is 256`, + serverConfig: { + '/': generateHTMLPage(''), + '/styles.css': styles + } + } +]; + +ruleRunner.testRule(ruleName, defaultTestsHttps, { https: true }); +ruleRunner.testRule(ruleName, configTestsHigh, { + https: true, + ruleOptions: { baseline: 'sha512' } +}); +ruleRunner.testRule(ruleName, configTestsLow, { + https: true, + ruleOptions: { baseline: 'sha256' } +}); diff --git a/packages/rule-sri/tsconfig.json b/packages/rule-sri/tsconfig.json new file mode 100644 index 00000000000..22f41dbc76c --- /dev/null +++ b/packages/rule-sri/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "outDir": "dist" + }, + "exclude": [ + "dist", + "node_modules" + ], + "extends": "../../tsconfig.json", + "include": [ + "src/**/*.ts", + "tests/**/*.ts" + ] +} diff --git a/yarn.lock b/yarn.lock index e27ff51c078..8571003bf69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -655,7 +655,7 @@ async@^1.4.0: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" -async@^2.0.0, async@^2.1.4: +async@^2.0.0, async@^2.1.4, async@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" dependencies: From bcb6b6d2565e51bf036c953a6a4587dfadfe2c69 Mon Sep 17 00:00:00 2001 From: molant Date: Tue, 13 Mar 2018 09:53:57 -0700 Subject: [PATCH 2/3] New: Add `sri` to `configuration-web-recommended` --- packages/configuration-web-recommended/index.json | 1 + packages/configuration-web-recommended/package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/configuration-web-recommended/index.json b/packages/configuration-web-recommended/index.json index b2da6c7823d..9fbad8d5774 100644 --- a/packages/configuration-web-recommended/index.json +++ b/packages/configuration-web-recommended/index.json @@ -29,6 +29,7 @@ "no-http-redirects": "error", "no-protocol-relative-urls": "error", "no-vulnerable-javascript-libraries": "error", + "sri": "error", "ssllabs": "error", "strict-transport-security": "error", "validate-set-cookie-header": "error", diff --git a/packages/configuration-web-recommended/package.json b/packages/configuration-web-recommended/package.json index 0a2a7f117a8..3e278ae84ca 100644 --- a/packages/configuration-web-recommended/package.json +++ b/packages/configuration-web-recommended/package.json @@ -24,6 +24,7 @@ "@sonarwhal/rule-no-http-redirects": "^3.0.0", "@sonarwhal/rule-no-protocol-relative-urls": "^3.0.0", "@sonarwhal/rule-no-vulnerable-javascript-libraries": "^3.0.0", + "@sonarwhal/rule-sri": "^1.0.0", "@sonarwhal/rule-ssllabs": "^3.0.0", "@sonarwhal/rule-strict-transport-security": "^3.0.0", "@sonarwhal/rule-validate-set-cookie-header": "^3.0.0", From 9e7ac6dcc4ee91719e3f28422721c6493b8b4d22 Mon Sep 17 00:00:00 2001 From: molant Date: Tue, 13 Mar 2018 11:38:03 -0700 Subject: [PATCH 3/3] Fix: PR Feedback --- packages/rule-sri/README.md | 16 ++++++++-------- packages/rule-sri/src/rule.ts | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/rule-sri/README.md b/packages/rule-sri/README.md index 757732b988a..5c5e1b8a121 100644 --- a/packages/rule-sri/README.md +++ b/packages/rule-sri/README.md @@ -1,13 +1,13 @@ -# Require scripts and styles to use subresource integrity (`sri`) +# Require scripts and styles to use subresource integrity (`@sonarwhal/rule-sri`) -`sri` warns about requesting scripts or styles without using Subresource +`sri` warns about requesting scripts or stylesheets without using subresource integrity. ## Why is this important? Nowadays it's very common to use third party resources from CDNs or different services (analytics, ads, etc.), and thus, increasing the risk surface of your -web application. +web site/app. While there are techniques to verify the agent is talking with the right server (TLS, HSTS, etc.), an attacker (or administrator) with access to the server can @@ -29,11 +29,11 @@ This rule checks that a website uses correctly SRI, more especifically: have an `integrity` attribute. * [The `integrity` attribute has to be valid][sri format]. I.e.: it should contain something in the form of `sha(256|384|512)-HASH`, where `HASH` is - the hashed value of the downlaoded body's response using the previous - algorithm (`sha256`, `sha384`, or `sha512`). -* The minium cryptographic hash function used has to be [`sha384`][collisions]. - If multiple are provided, the highest one will be used to determine if the - baseline is met. + the hashed value of the downlaoded body's response using the previously + specified algorithm (`sha256`, `sha384`, or `sha512`). +* The minium cryptographic hash function used is [`sha384`][collisions]. + If multiple ones are provided, the highest one will be used to determine if + the baseline is met. * When using a cross-origin resource (e.g.: using a script hosted in a third party CDN), the `