Why Third-Party Resources Deserve Scrutiny
When your site pulls a file from another server, you are implicitly trusting that server's security posture. You do not control that infrastructure, so any compromise there becomes your compromise too. That is not a reason to avoid third-party resources outright — CDNs and other external hosts offer real performance and bandwidth benefits — but it is a reason to verify what you are actually loading.
This is not a hypothetical concern. In June 2019, Malwarebytes reported that attackers had modified a JavaScript library served via an Amazon S3 bucket to steal credit card data from NBA.com visitors. JavaScript is not the only vector: CSS can also be abused, for example to harvest passwords. A single compromised third-party host can turn any resource you load into an attack against your users.
Subresource Integrity as a Mitigation
Subresource Integrity (SRI) is a browser security feature that lets you specify an expected cryptographic hash for a resource. If the resource's actual hash does not match the expected value, the browser refuses to execute or apply it. An attacker who modifies a file on the host will break the hash match, and the malicious content will never run.
HTTPS Does Not Cover This
HTTPS protects data in transit, but it does nothing about tampering that happens on the origin server itself. If a file is changed before it is sent, HTTPS will faithfully deliver the modified version. SRI is complementary: it verifies the integrity of the content you receive, regardless of how it was altered.
How Hashing Works Here
A hash function maps input data of arbitrary size to a fixed-size output. Good hash functions distribute outputs uniformly, so any given input is about as likely to map to one output as another. The same input always produces the same output. When two different inputs produce the same output, that is a collision — inevitable in small output spaces, but a serious flaw in cryptographic hashes when it can be deliberately triggered.
| Name (input) | Number rolled (output) |
|---|---|
| Alice | 4 |
| John | 6 |
| Bob | 2 |
| Mary | 2 |
| William | 5 |
| Susan | 1 |
| Joseph | 1 |
Practical cryptographic hashes are deterministic and designed to be one-way: reversing the hash to recover the input should be computationally infeasible, and finding two inputs that hash to the same value should be effectively impossible. A tiny change in the input should produce an entirely different output.
SRI uses the SHA family of cryptographic hash functions, supporting the 256, 384, and 512-bit variants. SHA-1 is not acceptable — a practical collision was demonstrated in 2017. To see the avalanche effect, compare the SHA-256 output for hello:
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
versus hell0 (zero instead of lowercase o):
bdeddd433637173928fe7202b663157c9e1881c3e4da1d45e8fff8fb944a4868
The two outputs share nothing in common despite the one-character difference. Collision resistance is what makes SRI trustworthy: an attacker cannot craft a malicious file that hashes to the same value as the legitimate one.
SRI hashes are expressed in base64 rather than hexadecimal. Base64 packs 6 bits per character versus 4 for hex, making the same data about 33% shorter. That matters when the hash is embedded directly in your HTML. A 384-bit SHA-384 hash, for instance, would be 96 hex characters but only 64 base64 characters.
Applying SRI to a Script Tag
To compute an SRI hash from the command line, you can pipe the file through a few standard utilities. One approach:
sha384sum [filename here] | head -c 96 | xxd -r -p | base64
sha384sumcomputes the SHA-384 digest of the file.head -c 96keeps only the first 96 characters — the length of a SHA-384 hash in hex.xxd -r -pconverts the hex string to raw binary.base64encodes that binary output as base64.
Adjust the character count for the algorithm you choose:
| Hash algorithm | Bits | Bytes | Hex Characters |
|---|---|---|---|
| SHA-256 | 256 | 32 | 64 |
| SHA-384 | 384 | 48 | 96 |
| SHA-512 | 512 | 64 | 128 |
Use the hex length matching your SHA variant for the head -c argument. MDN suggests a similar pipeline using awk to isolate the hash field before further processing:
shasum -b -a 384 FILENAME.js | awk '{ print $1 }' | xxd -r -p | base64
Windows users can hash a file with a small batch script instead:
@echo off
set bits=384
openssl dgst -sha%bits% -binary %1% | openssl base64 -A > tmp
set /p a= < tmp
del tmp
echo sha%bits%-%a%
pause
A Working Example
Create two JavaScript files:
// file1.js
alert('Hello, world!');
and:
// file2.js
alert('Hi, world!');
Compute the SHA-384 SRI hashes for both:
| Filename | SHA-384 hash (base64) |
|---|---|
file1.js | 3frxDlOvLa6GGEUwMh9AowcepHRx/rwFT9VW9yL1wv/OcerR39FEfAUHZRrqaOy2 |
file2.js | htr1LmWx3PQJIPw5bM9kZKq/FL0jMBuJDxhwdsMHULKybAG5dGURvJIXR9bh5xJ9 |
Now build an index.html that references both files with integrity attributes:
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="./file1.js" integrity="sha384-3frxDlOvLa6GGEUwMh9AowcepHRx/rwFT9VW9yL1wv/OcerR39FEfAUHZRrqaOy2" crossorigin="anonymous"></script>
<script type="text/javascript" src="./file2.js" integrity="sha384-htr1LmWx3PQJIPw5bM9kZKq/FL0jMBuJDxhwdsMHULKybAG5dGURvJIXR9bh5xJ9" crossorigin="anonymous"></script>
</head>
</html>
Serve the folder locally, for example with npx http-server, and open the page. Both alerts fire: "Hello, world!" and "Hi, world!" If you edit either JavaScript file without updating its hash, that script silently fails to run — the browser refuses to execute it because the hash no longer matches.
You may also supply multiple hashes for the same resource; the browser selects the strongest one it supports:
<!DOCTYPE html>
<html>
<head>
<script
type="text/javascript"
src="./file1.js"
integrity="sha384-3frxDlOvLa6GGEUwMh9AowcepHRx/rwFT9VW9yL1wv/OcerR39FEfAUHZRrqaOy2 sha512-cJpKabWnJLEvkNDvnvX+QcR4ucmGlZjCdkAG4b9n+M16Hd/3MWIhFhJ70RNo7cbzSBcLm1MIMItw
9qks2AU+Tg=="
crossorigin="anonymous"></script>
<script
type="text/javascript"
src="./file2.js"
integrity="sha384-htr1LmWx3PQJIPw5bM9kZKq/FL0jMBuJDxhwdsMHULKybAG5dGURvJIXR9bh5xJ9 sha512-+4U2wdug3VfnGpLL9xju90A+kVEaK2bxCxnyZnd2PYskyl/BTpHnao1FrMONThoWxLmguExF7vNV
WR3BRSzb4g=="
crossorigin="anonymous"></script>
</head>
</html>
The Role of crossorigin
SRI requires the crossorigin attribute on the tag. The attribute controls whether credentials are sent with the request and sets the request mode:
Value (crossorigin=) | Description |
|---|---|
anonymous | The request will have its credentials mode set to same-origin and its mode set to cors. |
use-credentials | The request will have its credentials mode set to include and its mode set to cors. |
| Credentials mode | Description |
|---|---|
same-origin | Credentials will be sent with requests sent to same-origin domains and credentials that are sent from same-origin domains will be used. |
include | Credentials will be sent to cross-origin domains as well and credentials sent from cross-origin domains will be used. |
| Request mode | Description |
|---|---|
cors | The request will be a CORS request, which will require the server to have a defined CORS policy. If not, the request will throw an error. |
The crossorigin requirement exists to prevent a side-channel attack. Without it, an attacker could load arbitrary cross-origin resources in a victim's browser and use SRI hash mismatches to probe which ones load successfully, leaking information about the user's state. With crossorigin set, the external server must explicitly permit the requesting origin via CORS headers for the request to succeed at all.
Automating SRI With Webpack
Recalculating hashes by hand every time a file changes does not scale. The webpack plugin webpack-subresource-integrity injects integrity and crossorigin attributes into your generated HTML automatically.
Setup
Start a Node project and install webpack and webpack-cli:
npm i --save-dev webpack webpack-cli
Add a build script to package.json:
{
//... rest of package.json ...,
"scripts": {
"dev": "webpack --mode=development"
}
//... rest of package.json ...,
}
Then install the HTML plugin and the SRI plugin:
npm i --save-dev html-webpack-plugin webpack-subresource-integrity style-loader css-loader
| Package name | Description |
|---|---|
| html-webpack-plugin | Creates an HTML file that resources can be injected into |
| webpack-subresource-integrity | Computes and inserts subresource integrity information into resources such as <script> and <link rel=…> |
| style-loader | Applies the CSS styles that we import |
| css-loader | Enables us to import css files into our JavaScript |
Configure webpack to use both plugins. The configuration tells webpack where your source files live and how to inject the hashed bundle into the HTML template:
const path = require('path'),
HTMLWebpackPlugin = require('html-webpack-plugin'),
SriPlugin = require('webpack-subresource-integrity');
module.exports = {
output: {
// The output file's name
filename: 'bundle.js',
// Where the output file will be placed. Resolves to
// the "dist" folder in the directory of the project
path: path.resolve(__dirname, 'dist'),
// Configures the "crossorigin" attribute for resources
// with subresource integrity injected
crossOriginLoading: 'anonymous'
},
// Used for configuring how various modules (files that
// are imported) will be treated
modules: {
// Configures how specific module types are handled
rules: [
{
// Regular expression to test for the file extension.
// These loaders will only be activated if they match
// this expression.
test: /\.css$/,
// An array of loaders that will be applied to the file
use: ['style-loader', 'css-loader'],
// Prevents the accidental loading of files within the
// "node_modules" folder
exclude: /node_modules/
}
]
},
// webpack plugins alter the function of webpack itself
plugins: [
// Plugin that will inject integrity hashes into index.html
new SriPlugin({
// The hash functions used (e.g.
// <script integrity="sha256- ... sha384- ..." ...
hashFuncNames: ['sha384']
}),
// Creates an HTML file along with the bundle. We will
// inject the subresource integrity information into
// the resources using webpack-subresource-integrity
new HTMLWebpackPlugin({
// The file that will be injected into. We can use
// EJS templating within this file, too
template: path.resolve(__dirname, 'src', 'index.ejs'),
// Whether or not to insert scripts and other resources
// into the file dynamically. For our example, we will
// enable this.
inject: true
})
]
};
Create an index.ejs template that webpack will use as the base for the generated page:
<!DOCTYPE html>
<html>
<body></body>
</html>
Add an entry point, index.js, with the code you want bundled:
// Imports the CSS stylesheet
import './styles.css'
alert('Hello, world!');
Run npm run dev, and webpack generates a dist folder containing an index.html where the script tag already includes the integrity hash and crossorigin attribute:
<!DOCTYPE HTML>
<html><head><script defer src="bundle.js" integrity="sha384-lb0VJ1IzJzMv+OKd0vumouFgE6NzonQeVbRaTYjum4ql38TdmOYfyJ0czw/X1a9b" crossorigin="anonymous">
</script></head>
<body>
</body>
</html>
Note that this automation only applies to files you bundle yourself. SRI is not suited for third-party files that change frequently, since updating the hash would require coordinating with the external host. For your own assets, though, webpack removes the maintenance burden while keeping the protection intact.
Subresource integrity is a lightweight, effective defense: it confirms that every resource your site executes is exactly what you intended to ship. It is one layer in a broader security strategy — not a replacement for HTTPS, CSP, or careful dependency management — but a meaningful one that is easy to adopt.



