LEARNING OBJECTIVES ⌵
- Understand why cross-origin resources with
integrityrequire thecrossoriginattribute. - Explain the concept of "opaque responses" under the Same-Origin Policy.
- Demystify the cryptographic side-channel probing attack that made the CORS requirement mandatory.
- Correctly configure CDN response headers (
Access-Control-Allow-Origin,Vary: Origin) to avoid caching bugs.
📖 The Mental Model & Story (Intuitive Foundation)
Imagine a customs inspection checkpoint at an international border.
If a diplomatic courier carries a sealed diplomatic pouch, international law strictly forbids customs officials from opening the pouch to inspect the contents. If the customs officer is handed a document that says "Verify that the exact text inside the diplomatic pouch matches this secret fingerprint," the officer has to refuse. To verify the fingerprint, the officer would have to read every byte inside the pouch—which is illegal without explicit diplomatic authorization (a waiver).
+-----------------------------------------------------------------------------------------+
| THE DIPLOMATIC POUCH ANALOGY |
+-----------------------------------------------------------------------------------------+
| |
| Without CORS Permission (No-CORS / Opaque): |
| +-------------------+ +-----------------------+ +-----------------------+ |
| | Origin: App.com | ----> | Cross-Origin Server | ----> | Opaque Response | |
| | <script integrity>| | (Private User Data) | | Browser cannot read | |
| +-------------------+ +-----------------------+ | the raw bytes to hash | |
| +-----------------------+ |
| | |
| v |
| 🛑 BLOCKED BY BROWSER |
| |
| With CORS Permission (crossorigin="anonymous" + Access-Control-Allow-Origin): |
| +-------------------+ +-----------------------+ +-----------------------+ |
| | Origin: App.com | ----> | CDN Server with | ----> | CORS-Exposed Response | |
| | <script | | Access-Control-Allow- | | Browser reads bytes, | |
| | crossorigin="..." | | Origin: * | | computes SHA-384 hash | |
| | integrity="..."> | +-----------------------+ +-----------------------+ |
| +-------------------+ | |
| v |
| ✅ EXECUTES IF HASH OK |
+-----------------------------------------------------------------------------------------+
In browser architecture, cross-origin resources fetched without CORS are treated as opaque. The browser is allowed to execute the JavaScript, but it is strictly forbidden from inspecting the raw bytes. Because calculating a SHA hash requires inspecting every single byte, the browser must have CORS permission before performing Subresource Integrity validation.
Technical Deep Dive & Specifications
The Vulnerability: The Cryptographic Oracle Attack
Why did browser vendors and the W3C mandate that SRI fails without CORS?
Imagine an attacker hosting https://evil-attacker.com. Suppose a user visits the attacker's site while logged into their online bank (https://mybank.com).
The bank has a sensitive JSONP or JavaScript endpoint:
https://mybank.com/api/account-details.js which renders:
var account = { balance: "$14,500.00", user: "Alice" };
Under normal Same-Origin Policy rules, evil-attacker.com can load this script via <script src="https://mybank.com/api/account-details.js">, but the attacker cannot directly read the source code text.
Now imagine if the browser allowed SRI without CORS:
evil-attacker.comguesses possible account balance strings ("$10,000.00","$10,001.00", etc.).- The attacker calculates the SHA-384 hash for each guess.
- The attacker dynamically creates
<script src="https://mybank.com/api/account-details.js" integrity="sha384-[GUESS_HASH]">. - If the guess is wrong, the browser throws an
onerrorevent. - If the guess is correct, the browser fires
onload!
+------------------------------------------------------------------------------------+
| HOW SRI WITHOUT CORS WOULD BECOME A DATA-THEFT ORACLE |
+------------------------------------------------------------------------------------+
| |
| Attacker guesses: "balance: $14,500" -> Hash: sha384-abc |
| Injects: <script src="https://bank.com/api" integrity="sha384-abc"> |
| Result: 🛑 onerror (Guess wrong) |
| |
| Attacker guesses: "balance: $14,501" -> Hash: sha384-xyz |
| Injects: <script src="https://bank.com/api" integrity="sha384-xyz"> |
| Result: ✅ onload (MATCH FOUND! Attacker now knows the user's private balance!) |
| |
+------------------------------------------------------------------------------------+
By turning SRI into a cryptographic oracle, an attacker could extract private user data, CSRF tokens, or personal identifiers across origins.
To eliminate this vulnerability, the W3C SRI specification mandates: If a resource is fetched from a different origin and includes an integrity attribute, the fetch mode must be CORS.
The crossorigin Attribute Values
To satisfy the CORS requirement on <script> and <link> elements, you must supply the crossorigin attribute:
| Value | Fetch Mode | Credentials (Cookies/Auth) Sent? | Server Response Header Required |
|---|---|---|---|
| (omitted) | no-cors (Opaque) |
Yes | None (Incompatible with SRI across origins) |
"" or "anonymous" |
cors |
No (No cookies or client certs) | Access-Control-Allow-Origin: * or Access-Control-Allow-Origin: <origin> |
"use-credentials" |
cors |
Yes (Sends session cookies & auth headers) | Access-Control-Allow-Origin: <origin> AND Access-Control-Allow-Credentials: true |
📌 Best Practice: For static public CDNs (cdnjs, jsDelivr, unpkg, Google Fonts), always use
crossorigin="anonymous".
CDN Server Header Requirements
For SRI to succeed, the CDN server hosting the script or stylesheet must respond with the appropriate CORS headers:
HTTP/2 200 OK
Content-Type: application/javascript; charset=utf-8
Access-Control-Allow-Origin: *
Vary: Origin, Accept-Encoding
Cache-Control: public, max-age=31536000, immutable
Why Vary: Origin is Critical on the CDN
If a CDN edge server caches a response for an asset requested without CORS (e.g. from a client that didn't pass Origin), and then serves that cached response (which lacks Access-Control-Allow-Origin) to a client performing an SRI CORS check, the SRI check will fail.
The Vary: Origin header instructs intermediate proxies and CDNs to cache separate copies based on the presence and value of the incoming Origin request header.
💻 Interactive Code Playground
Starter Code
Line-by-Line Code Breakdown
- Lines 8–14 (
<link rel="stylesheet"...): Loads Font Awesome icons from Cloudflare CDN. It includes bothintegrityandcrossorigin="anonymous". Cloudflare responds withAccess-Control-Allow-Origin: *, allowing the browser to read the stylesheet bytes and compute the SHA-512 digest. - Lines 32–36 (
<script src="..." integrity="..." crossorigin="anonymous">): Loads Axios. Becausecrossorigin="anonymous"is declared, the browser sends the HTTP request with modecorsand excludes credentials. The downloaded bytes are hashed and compared with thesha512-string. - Lines 38–51 (
<script>...): Confirms runtime execution. Ifcrossoriginhad been omitted, modern browsers would reject the fetch immediately with a NetworkError.
Expected Browser Render Output
+-----------------------------------------------------------------------+
| 🛡️ Subresource Integrity + CORS Check |
| This page demonstrates valid cross-origin asset loading with CORS. |
| |
| ✅ Axios v1.6.7 successfully loaded with CORS & SRI verification! |
+-----------------------------------------------------------------------+🏋️ Hands-On Exercise
🎯 The Challenge: Diagnose and Fix a Broken CDN Integration
Instructions:
- A junior engineer deployed the following snippet to production, but the script fails to execute, and the browser console displays:
Access to script at 'https://cdn.partner.org/analytics.js' from origin 'https://mybank.com' has been blocked by CORS policy. - Identify the two defects in the
<script>tag configuration. - Fix the tag so that it:
- Requests the asset using CORS mode without transmitting session cookies.
- Verifies the provided SHA-384 hash:
sha384-9yX8w7V6u5T4s3R2q1P0o9N8m7L6k5J4i3H2g1F0e9D8c7B6a5Z4Y3X2W1V0U - Handles integrity verification errors gracefully by logging to the telemetry system.
🏁 Starter Code Sandbox
⚠️ Common Pitfalls
- Omitting
crossorigin="anonymous"on Cross-Origin Scripts: When you addintegrityto an external script withoutcrossorigin, browsers refuse to compute the digest because the response is opaque, causing an immediate network failure. - Using
crossorigin="use-credentials"with Wildcard CORS: If you setcrossorigin="use-credentials"on a CDN script, the CDN server cannot useAccess-Control-Allow-Origin: *. Browsers strictly reject credentialed requests with wildcard origins. Public CDNs almost never support credentialed requests. - Missing
Vary: Originon Custom CDNs: If your origin server hosts static assets via an AWS S3/CloudFront CDN, you must ensure CloudFront forwards theOriginheader and caches onVary: Origin. Otherwise, non-CORS requests may populate edge caches without CORS headers, breaking subsequent SRI requests.
💡 Pro Tips
- Same-Origin SRI Does Not Need
crossorigin: If the script or stylesheet is hosted on the same origin (e.g.,/static/bundle.js), the response is not opaque. You can declareintegrity="sha384-..."without needingcrossorigin="anonymous". - Nginx CDN Configuration for SRI: When configuring Nginx or an edge proxy for static assets, always add:
location ~* \.(js|css|woff2)$ { add_header Access-Control-Allow-Origin "*" always; add_header Vary "Origin, Accept-Encoding" always; expires 1y; add_header Cache-Control "public, immutable"; }
📌 Key Takeaways
- SRI requires the browser to inspect every byte of a resource to compute its cryptographic hash.
- Cross-origin requests without CORS produce "opaque" responses, which browsers cannot read.
- Allowing SRI on opaque responses would create a cryptographic oracle vulnerability, leaking sensitive cross-origin data.
- Therefore, cross-origin resources with
integritymust declarecrossorigin="anonymous"(orcrossorigin="use-credentials"). - CDN servers must respond with
Access-Control-Allow-Origin: *andVary: Origin. - --