One of the workshops in the Software Engineering programme at Multiverse is about data security. We cover the basics of concepts such as encryption and hashing. We normally talk about hashing in the context of password security, but when I delivered the workshop last week, I decided to demonstrate how hashing can be used for data integrity. Specifically, I showed how subresource integrity can be used to keep users safe when embedding third-party scripts.

Determinism

One of the most important features of a hash function is that it is deterministic. This means that the same input will always product the same output. The output of a hash function is called a cryptographic digest, but it is more commonly known as a hash.

Determinism is not so good for password security. This is because if two people use the same password, they will have the same hash. Although hashes are irreversible, an attacker could guess that it must be a hash of a common password. They could simply Google the hash or use a rainbow table attack. This is why we use salted passwords.

However, determinism is great for data integrity! We can put a file through a hash function to produce a unique hash. If we need to check that the file has not been tampered with, we can hash the file again and verify that the hashes are the same.

Subresource Integrity

Modern web browers have a built-in security featured called Subresource Integrity (SRI). You can provide an expected hash of a script or stylesheet, and if the hash of the requested resource is different, the browser will refuse to execute it.

For example, imagine I have a file called calculator.js that contains the following JavaScript code. The calculator variable is assigned to an object with methods for addition, subtraction, multiplication, and division.

var calculator = {
add(a, b) {
return a + b;
},
subtract(a, b) {
return a - b;
},
multiply(a, b) {
return a * b;
},
divide(a, b) {
return a / b;
},
};

Assuming the file is in my current working directory, I can generate a SHA-384 hash of the file and Base64 encode it using the following shell command:

openssl dgst -sha384 -binary calculator.js | openssl base64 -A

The output is as follows.

lMM6fbJ1mv/thgJNkIxkhSIDIH0RJYtkuu2kBilGrtQIx2tXhU3oy4Glel3aRyYa

When I embed the script using a <script> element, I can add the Base64-encoded hash to the element’s integrity attribute. I need to specify the hash function that was used (sha256, sha384, or sha512), a hyphen (-), and then the Base64-encoded hash. You can even specify multiple hashes in this format, separated by spaces.

<script
src="calculator.js"
integrity="sha384-lMM6fbJ1mv/thgJNkIxkhSIDIH0RJYtkuu2kBilGrtQIx2tXhU3oy4Glel3aRyYa">
</script>

If someone tampers with the script, the browser will refuse to execute it. For example, imagine someone changes the return value of every method to the string "get rekt".

var calculator = {
add(a, b) {
return "get rekt";
},
subtract(a, b) {
return "get rekt";
},
multiply(a, b) {
return "get rekt";
},
divide(a, b) {
return "get rekt";
},
};

Here is the network error I get in Firefox. This is saying that when Firefox computed the hash, the result was different from the one I specified in the integrity attribute. Therefore, Firefox refused to execute the script.

None of the “sha384” hashes in the integrity attribute match the content of the subresource. The computed hash is “mkmK7qPGN5kRzL0kx55x8Z4nPQLMq9U8CKWyzMxeB3VRnzsV5GT2nxyLdyid4Yh+”.

I made a demo of Subresource Integrity on StackBlitz.

Content delivery networks

In the preceding section, I used Subresource Integrity for a script that was served from the same origin. However, it’s more common to use it for scripts that are loaded from a content delivery network (CDN). You can probably trust a first-party script—though the zero trust security model would beg to differ—but you certainly shouldn’t trust a third-party script. Even if you are requesting a popular library from a reputable CDN, there is no guarantee that the file hasn’t been tampered with.

Therefore, some security-conscious projects provide the hash for you. For example, if I visit the jQuery CDN and attempt to grab the minified version of jQuery 3.x, a modal window appears with the following HTML.

<script
src="https://code.jquery.com/jquery-3.7.1.min.js"
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
crossorigin="anonymous">
</script>

Of course, you may want to hash the file yourself and verify that the hash is the same. You can paste the URL of the script into the SRI Hash Generator. However, if an attacker did breach the CDN and the jQuery website, it would be trivial for them to tamper with the file and generate a matching hash. This is highly unlikely, but it’s worth noting. We’re too trusting of third-party scripts! The only way to be sure would be to check the source code, but even then, malicious code could go unnoticed.

Note that the <script> element provided by jQuery has its crossorigin attribute set to anonymous. This tells the browser to send the request with the appropriate CORS headers, allowing the script to be loaded and executed without violating the same-origin policy.

Here is the error I get in Firefox if I omit the crossorigin attribute.

"https://code.jquery.com/jquery-3.7.1.min.js" is not eligible for integrity checks since it's neither CORS-enabled nor same-origin.

Generating your own hashes

Some projects don’t provide the hash for you. For example, the Vue.js quick start guide suggests the following <script> element. Note that the integrity and crossorigin attributes are not provided.

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

If you want to generate your own hash, you can paste the URL into the SRI Hash Generator that I linked to in the preceding section. However, you should use a specific version number, otherwise you would need to keep updating the hash as updates are released.

Here is an example. I searched for “unpkg vue,” browsed the available files, and grabbed the link to vue.global.js from the dist directory. Then I pasted the URL into the SRI Hash Generator to generate the following HTML.

<script
src="https://unpkg.com/vue@3.4.21/dist/vue.global.js"
integrity="sha384-8CdW77YPqMZ3v22pThUIR22Qp1FB5oisZG2WE3OpE0l1fTHAIsdIwjQZFf/rmQ/B"
crossorigin="anonymous">
</script>

Summary

Subresource Integrity (SRI) is a built-in security feature of modern web browsers. You can specify a hash of the expected contents of a script or stylesheet, and if the computed hash is different, your browser will refuse to execute the script or stylesheet.