While using third party JavaScript or CSS libraries from CDN, you've probably came across with integrity
attribute on script
or link
tags.
<script
src="https://code.jquery.com/jquery-3.6.4.min.js"
integrity="sha256-oP6HI9z1XaZNBrJURtCoUT5SUnxFr8s3BzRl+cbzUq8="
crossorigin="anonymous">
</script>
If you haven't researched about them till now, then you're in the right place.
Man in the Middle Attacks
It's possible that the any data which travels through internet can be modified till it reaches to our machine. An attacker might use techniques such as network eavesdropping to intercept the requests and responses between you and the server and might manipulate the file before you even receive it.
We need a way to verify that the file we've received has not been tempered with.
Subresource Integrity in Browsers
Browsers implement a security feature called Subresource Integrity (SRI) to verify integrity of the resource they fetch and execute.
The garbage looking string of characters you see as the value of integrity
attribute is Base64 decoded cryptographic digest formed by applying specific hash algorithm to the contents of the file.
If any characters inside the file changes, then the calculated hash will also change, thus integrity verification will fail, thus the browser will know that the file has been tempered with.
If browsers cannot verify integrity of the file being requested, they'll show error message similar to this:
Failed to find a valid digest in the 'integrity' attribute
for resource '<resource-name>' with computed SHA-256 integrity '<calculated-hash>'.
The resource has been blocked.
Performing Integrity Check with NodeJS
To show a little demo how this check might be implemented, I'll use NodeJS's crypto
module. We'll use the same jquery
file the you saw in the first paragraph. I'll download that file into my machine to be able to make modifications later.
Let's create an index.js
file and import fs
and crypto
modules:
import crypto from 'crypto';
import fs from 'fs';
We'll need fs
module to read contents of the file that we want to verify integrity of. Let's declare resource name and expected integrity:
const resource = 'jquery-3.6.4.min.js';
const expectedIntegrity = 'oP6HI9z1XaZNBrJURtCoUT5SUnxFr8s3BzRl+cbzUq8=';
Note that sha256
in front of the value of integrity
attribute is just the name of hashing algorithm that this hash has been calculated with, so we skip it when comparing.
Let's read the file contents and calculate the digest using sha256
algorithm:
const jquerySource = fs.readFileSync(resource);
const digest = crypto
.createHash('sha256')
.update(jquerySource, 'utf8')
.digest();
const calculatedIntegrity = digest.toString('base64');
The value of digest
variable is a type called Buffer
which is just bunch of bytes, so we need to convert it to a string in base64
encoding.
And finally, we just need to compare calculatedIntegrity
with expectedIntegrity
, if they are equal then the jquery-3.6.4.min.js
has not been tempered with, if not, it means something has changed inside the file:
if (calculatedIntegrity == expectedIntegrity) {
console.log('✔️ Resource integrity verified!');
} else {
console.log(`❌ Failed to find a valid digest in the 'integrity' attribute
for resource '${resource}' with computed SHA-256 integrity '${calculatedIntegrity}'.`);
}
If you run this code using node index.js
then, you might see this output:
$ node index.js
✔️ Resource integrity verified!
Now, if you change the contents of jquery-3.6.4.min.js
file, and run the code again, then you'll see output similar to this:
$ echo 'some malicious code' >> jquery-3.6.4.min.js
$ node index.js
❌ Failed to find a valid digest in the 'integrity' attribute
for resource 'jquery-3.6.4.min.js' with computed SHA-256 integrity '+FiqKbj0h12Db1PZj62G+h2BMtOueBg26YQOJFY+6wg='.
Conclusion
I hope this article was helpful for you, I suggest you read more at article at Smashing Magazine about Subresource Integrity and share your thoughts.
You can see the full version of the code we wrote at my GitLab repository.
Top comments (0)