├── .editorconfig
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── cypress.json
├── cypress
├── fixtures
│ ├── compromised.js
│ ├── fallback.js
│ ├── original.js
│ ├── scenario-1.html
│ ├── scenario-2.html
│ ├── scenario-3.html
│ ├── scenario-4.html
│ ├── scenario-5.html
│ └── scenario-6.html
└── integration
│ └── sri_spec.js
├── dist
└── sri.min.js
├── docs
├── CNAME
├── android-chrome-192x192.png
├── android-chrome-256x256.png
├── apple-touch-icon.png
├── browserconfig.xml
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── index.html
├── manifest.json
├── markdown.css
├── mstile-150x150.png
├── prism.js
├── safari-pinned-tab.svg
└── sri-issue-chrome.jpg
├── package-lock.json
├── package.json
└── src
└── sri.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | ; EditorConfig file: http://EditorConfig.org
2 | ; Install the "EditorConfig" plugin into your editor to use
3 |
4 | root = true
5 |
6 | [*]
7 | charset = utf-8
8 | insert_final_newline = true
9 | indent_style = space
10 | indent_size = 2
11 | trim_trailing_whitespace = true
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Dependency directories
9 | node_modules/
10 | jspm_packages/
11 |
12 | # Optional npm cache directory
13 | .npm
14 |
15 | # Optional eslint cache
16 | .eslintcache
17 |
18 | # Yarn Integrity file
19 | .yarn-integrity
20 |
21 | # dotenv environment variables file
22 | .env
23 | cypress/videos
24 | cypress/plugins
25 | cypress/support
26 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | CNAME
2 | index.html
3 | markdown.css
4 | prism.js
5 | sri-issue-chrome.jpg
6 | cypress
7 | docs
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | cache:
3 | directories:
4 | - node_modules
5 | notifications:
6 | email: false
7 | node_js:
8 | - '10'
9 | before_script:
10 | - npm prune
11 | script:
12 | - npm run serve &
13 | - npm test
14 | after_success:
15 | - npm run semantic-release
16 | branches:
17 | except:
18 | - /^v\d+\.\d+\.\d+$/
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Jakub Mikulas
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Subresource Integrity Fallback [sri.js.org](https://sri.js.org)
2 |
3 | ## What is Subresource Integrity
4 |
5 | [Subresource Integrity (SRI)][1] is a security feature that ensures that resources client browser downloads and runs (JavaScript/CSS) haven't been tampered with.
6 |
7 | [SRI Browser support][2] is on the rise and some CDNs include it in the best practices. All is sunsh[i][3]ne and lollipops until you see this error in user's console:
8 |
9 | ![Failed Subresource Integrity check in Chrome][4]Failed Subresource Integrity check in Chrome
10 |
11 | Guess what, nothing works. Nobody knows why. Rogue proxy? Optimalization proxy? Maybe your CDN is hacked or lying to you. Anyway, your resources are broken.
12 |
13 | ## Fallback
14 |
15 | If we don't want to leave user with an application in nonfunctioning state or at least provide the user some feedback on why application couldn't start, we need a fallback. So how can we handle tampered resource?
16 |
17 | ### Strategy
18 |
19 | Script is downloaded, but before its execution, SRI hash is checked against the downloaded resource. If hash check fails, [error event is fired][5] and resource is not executed. This error is not propagated to the `window.onerror` and _afaik_ can't be distinguished from other errors. So we need to catch all of them.
20 |
21 | To catch `script/link` errors, we can use [`onerror`][6] handler. But we need to hook it right as the resource is added to the DOM. For that we can use [MutationObserver][7].
22 |
23 | ### Fallback flow
24 |
25 | 1. Monitor DOM for a new `script/link` elements with `integrity` attribute
26 | 2. Add `onerror` handler to those
27 | 3. If `onerror` is fired, try to load fallback resource specified on the element
28 | 4. When downloading fallback resource fails (or fallback is not available) script fires an event to notify user that something went wrong
29 |
30 | ## How to use
31 |
32 | Because we are trying to deal with unreliable CDN/network, **all of the following code must be placed in the document itself** and shouldn't be served from any remote location.
33 |
34 | ### 1) Add [SRI fallback code][8] to the header
35 |
36 | Code must be placed before any resource using `integrity` attribute - ideally in `
`, since CSS `link` can also utilize integrity check. Its minified version _(~0,4kb gzipped)_ is also on [`npm`][9]:
37 |
38 | **`npm i subresource-integrity-fallback -S`**
39 |
40 | ### 2) Add resource fallback data attributes
41 |
42 | Then you need to supply `data-sri-fallback` attribute on any resource with `integrity` check. Example use:
43 |
44 | ```html
45 |
50 | ```
51 |
52 | It might be a good idea to either serve fallback resources from same server that serves the actual page or setup a secondary CDN as a fallback.
53 |
54 | ### 3) Define a behavior on failure
55 |
56 | Last missing piece is the `window.resourceLoadError` function, that will be called when resource with `integrity` check fails to load - as explained above, this reason can come either from normal network problems or resource tampering. Function will get error itself as an argument and boolean indicating whether fallback resource also failed. Function will be called for each failing resource.
57 |
58 | ```js
59 | // In the
60 | window.resourceLoadError = function(err, isRetry) {
61 | if (isRetry) {
62 | return letUserKnowAboutPossibleTampering(err);
63 | }
64 | return letUserKnowSomethingFailedToLoad(err);
65 | }
66 | // SRI Fallback code below this...
67 | ```
68 |
69 | ## Other considerations
70 |
71 | * Have a reason to use it. SRI is not a free lunch, be sure you need it and are ready to spend some resources on it.
72 | * You should [send `no-transform` header][10] from your CDN.
73 | * Using this together with [CSP][11] is a good idea.
74 |
75 | [1]: https://mdn.io/SubresourceIntegrity
76 | [2]: http://caniuse.com/#feat=subresource-integrity
77 | [3]: https://www.youtube.com/watch?v=XQmBXEZEYtg
78 | [4]: https://sri.js.org/sri-issue-chrome.jpg
79 | [5]: https://www.w3.org/TR/SRI/#handling-integrity-violations
80 | [6]: https://developer.mozilla.org/cs/docs/Web/API/GlobalEventHandlers/onerror
81 | [7]: https://developer.mozilla.org/docs/Web/API/MutationObserver
82 | [8]: https://github.com/JackuB/subresource-integrity-fallback/tree/master/dist
83 | [9]: https://www.npmjs.com/package/subresource-integrity-fallback
84 | [10]: https://www.w3.org/TR/SRI/#proxies
85 | [11]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
86 |
87 | ## Contributing/developing
88 |
89 | - Install dependencies: `npm install`
90 | - Build the `/dist` and serve the static files: `npm run serve`
91 | - Test/play with Cypress: `npm run cypress:open`
92 |
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseUrl": "http://localhost:8080/cypress/fixtures"
3 | }
4 |
--------------------------------------------------------------------------------
/cypress/fixtures/compromised.js:
--------------------------------------------------------------------------------
1 | document.body.innerText = 'Compromised script loaded';
--------------------------------------------------------------------------------
/cypress/fixtures/fallback.js:
--------------------------------------------------------------------------------
1 | document.body.innerText = 'Fallback script loaded';
--------------------------------------------------------------------------------
/cypress/fixtures/original.js:
--------------------------------------------------------------------------------
1 | document.body.innerText = 'First script loaded';
--------------------------------------------------------------------------------
/cypress/fixtures/scenario-1.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | SRI test page
8 |
9 |
10 |
11 | Hello world
12 |
13 |
14 |
--------------------------------------------------------------------------------
/cypress/fixtures/scenario-2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | SRI test page
8 |
16 |
17 |
18 |
19 | Hello world
20 |
21 |
25 |
--------------------------------------------------------------------------------
/cypress/fixtures/scenario-3.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | SRI test page
8 |
16 |
17 |
18 |
19 | Hello world
20 |
21 |
25 |
--------------------------------------------------------------------------------
/cypress/fixtures/scenario-4.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | SRI test page
8 |
16 |
17 |
18 |
19 | Hello world
20 |
21 |
25 |
--------------------------------------------------------------------------------
/cypress/fixtures/scenario-5.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | SRI test page
8 |
16 |
17 |
18 |
19 | Hello world
20 |
21 |
24 |
--------------------------------------------------------------------------------
/cypress/fixtures/scenario-6.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | SRI test page
8 |
16 |
17 |
18 |
19 | Hello world
20 |
21 |
24 |
27 |
--------------------------------------------------------------------------------
/cypress/integration/sri_spec.js:
--------------------------------------------------------------------------------
1 | /*
2 | Scenarios
3 | - fail SRI on first script and load fallback without SRI
4 | */
5 |
6 | describe('Plain JS', function() {
7 | it('successfully loads original script', function() {
8 | cy.visit('scenario-1.html');
9 | cy.contains('body', 'First script loaded');
10 | });
11 | });
12 |
13 | describe('JS with valid SRI', function() {
14 | it('successfully loads original script', function() {
15 | cy.visit('scenario-2.html');
16 | cy.contains('body', 'First script loaded');
17 | });
18 | });
19 |
20 | describe('JS with invalid SRI, but valid fallback', function() {
21 | it('fails to load original script', function() {
22 | cy.visit('scenario-3.html');
23 | cy.get('body').should('not.contain', 'First script loaded');
24 | });
25 |
26 | it('loads the fallback script', function() {
27 | cy.contains('body', 'Fallback script loaded');
28 | });
29 |
30 | it('does not execute the resourceLoadError callback', function() {
31 | cy.get('body').should('not.contain', 'resourceLoadError');
32 | });
33 | });
34 |
35 | describe('JS with invalid SRI and fallback (invalid hash)', function() {
36 | it('fails to load original script', function() {
37 | cy.visit('scenario-4.html');
38 | cy.get('body').should('not.contain', 'First script loaded');
39 | });
40 |
41 | it('fails to load the fallback script', function() {
42 | cy.get('body').should('not.contain', 'Fallback script loaded');
43 | });
44 |
45 | it('executes the resourceLoadError callback', function() {
46 | cy.contains('body', 'resourceLoadError');
47 | cy.contains('body', 'Loading original and fallback failed');
48 | });
49 | });
50 |
51 | describe('JS with invalid SRI (compromised file)', function() {
52 | it('fails to load original script', function() {
53 | cy.visit('scenario-5.html');
54 | cy.get('body').should('not.contain', 'First script loaded');
55 | });
56 |
57 | it('executes the resourceLoadError callback', function() {
58 | cy.contains('body', 'resourceLoadError');
59 | cy.contains('body', 'Loading original failed');
60 | });
61 | });
62 |
63 | describe('Multiple JS files with SRI, one is compromised', function() {
64 | it('fails to load original script', function() {
65 | cy.visit('scenario-5.html');
66 | cy.get('body').should('not.contain', 'First script loaded');
67 | });
68 |
69 | it('executes the resourceLoadError callback', function() {
70 | cy.contains('body', 'resourceLoadError');
71 | cy.contains('body', 'Loading original failed');
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/dist/sri.min.js:
--------------------------------------------------------------------------------
1 | try{!function(){const t=window.resourceLoadError||(()=>{}),e="data-sri-fallback",r=window.MutationObserver||window.WebKitMutationObserver;r&&new r(function(t){t.forEach(function(t){t.addedNodes.forEach(i)})}).observe(document,{childList:!0,subtree:!0});const i=function(r){const i=(r.tagName||"").toLowerCase();"link"!==i&&"script"!==i||!r.integrity||r.getAttribute("data-sri-fallback-retry")||(r.onerror=function(o){if(r.getAttribute(e)){const o=document.createElement(i),n=r.parentNode;o.setAttribute("data-sri-fallback-retry","1"),o.setAttribute("integrity",r.integrity),r.src&&o.setAttribute("src",r.getAttribute(e)),r.href&&o.setAttribute("href",r.getAttribute(e)),o.onerror=function(e){t(e,!0)},n.appendChild(o),r.remove()}else t(o,!1)})}}()}catch(t){console.error(t)}
--------------------------------------------------------------------------------
/docs/CNAME:
--------------------------------------------------------------------------------
1 | sri.js.org
2 |
--------------------------------------------------------------------------------
/docs/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackuB/subresource-integrity-fallback/966d31fe9368375fb543e1ed30ca08ad400eb6fd/docs/android-chrome-192x192.png
--------------------------------------------------------------------------------
/docs/android-chrome-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackuB/subresource-integrity-fallback/966d31fe9368375fb543e1ed30ca08ad400eb6fd/docs/android-chrome-256x256.png
--------------------------------------------------------------------------------
/docs/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackuB/subresource-integrity-fallback/966d31fe9368375fb543e1ed30ca08ad400eb6fd/docs/apple-touch-icon.png
--------------------------------------------------------------------------------
/docs/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #ffffff
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/docs/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackuB/subresource-integrity-fallback/966d31fe9368375fb543e1ed30ca08ad400eb6fd/docs/favicon-16x16.png
--------------------------------------------------------------------------------
/docs/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackuB/subresource-integrity-fallback/966d31fe9368375fb543e1ed30ca08ad400eb6fd/docs/favicon-32x32.png
--------------------------------------------------------------------------------
/docs/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackuB/subresource-integrity-fallback/966d31fe9368375fb543e1ed30ca08ad400eb6fd/docs/favicon.ico
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Subresource Integrity Fallback
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
Subresource Integrity Fallback
20 |
21 |
What is Subresource Integrity
22 |
Subresource Integrity (SRI) is a security feature that ensures that resources client browser downloads and runs (JavaScript/CSS) haven't been tampered with.
23 |
24 |
SRI Browser support is on the rise and some CDNs include it in the best practices. All is sunshine and lollipops until you see this error in user's console:
Guess what, nothing works. Nobody knows why. Rogue proxy? Optimalization proxy? Maybe your CDN is hacked or lying to you. Anyway, your resources are broken.
32 |
33 |
Fallback
34 |
If we don't want to leave user with an application in nonfunctioning state or at least provide the user some feedback on why application couldn't start, we need a fallback. So how can we handle tampered resource?
35 |
36 |
Strategy
37 |
Script is downloaded, but before its execution, SRI hash is checked against the downloaded resource. If hash check fails, error event is fired and resource is not executed. This error is not propagated to the window.onerror and afaik can't be distinguished from other errors. So we need to catch all of them.
38 |
39 |
To catch script/link errors, we can use onerror handler. But we need to hook it right as the resource is added to the DOM. For that we can use MutationObserver.
40 |
41 |
Fallback flow
42 |
43 |
Monitor DOM for a new script/link elements with integrity attribute
44 |
Add onerror handler to those
45 |
If onerror is fired, try to load fallback resource specified on the element
46 |
When downloading fallback resource fails (or fallback is not available) script fires an event to notify user that something went wrong
47 |
48 |
49 |
How to use
50 |
Because we are trying to deal with unreliable CDN/network, all of the following code must be placed in the document itself and shouldn't be served from any remote location.
Code must be placed before any resource using integrity attribute - ideally in <head>, since CSS link can also utilize integrity check. Its minified version (~0,4kb gzipped) is also on npm:
54 |
55 |
npm i subresource-integrity-fallback -S
56 |
57 |
2) Add resource fallback data attributes
58 |
Then you need to supply data-sri-fallback attribute on any resource with integrity check. Example use:
It might be a good idea to either serve fallback resources from same server that serves the actual page or setup a secondary CDN as a fallback.
67 |
68 |
3) Define a behavior on failure
69 |
Last missing piece is the window.resourceLoadError function, that will be called when resource with integrity check fails to load - as explained above, this reason can come either from normal network problems or resource tampering. Function will get error itself as an argument and boolean indicating whether fallback resource also failed. Function will be called for each failing resource.
70 |
71 |
// In the <head>
72 | window.resourceLoadError = function(err, isRetry) {
73 | if (isRetry) {
74 | return letUserKnowAboutPossibleTampering(err);
75 | }
76 | return letUserKnowSomethingFailedToLoad(err);
77 | }
78 | // SRI Fallback code below this...
79 |
80 |
Other considerations
81 |
82 |
Have a reason to use it. SRI is not a free lunch, be sure you need it and are ready to spend some resources on it.