MCP Server Security
Subresource Integrity for HTTP-transport MCP servers
HTTP-transport MCP servers that serve any HTML page — an OAuth callback, an admin UI, a setup wizard — often load scripts from CDNs. Subresource Integrity (SRI) hashes cryptographically pin each external script to a known version, so a compromised CDN cannot inject malicious code into your server's pages.
Why SRI matters in the MCP context
A typical web app using a CDN-hosted JavaScript library carries some SRI risk, but the blast radius of compromise is contained to the user's browser session. For an HTTP-transport MCP server, the risk is amplified: the browser page is often a configuration surface for the MCP server itself. An attacker who compromises a CDN asset loaded by your server's setup page can exfiltrate API keys, modify tool configurations, or steal OAuth tokens — all before a single tool call is made.
SRI prevents this by causing the browser to refuse to execute any CDN script whose content doesn't match the cryptographic hash embedded in the HTML. Even if the CDN is fully compromised, the attacker's payload won't execute because the hash won't match.
The integrity= attribute
SRI works by adding an integrity attribute to <script> and <link> tags:
<script src="https://cdn.example.com/library@3.2.1/dist/lib.min.js" integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxmT6OpqTLssSXQ3FMZ5BGXNPRNAP0" crossorigin="anonymous" ></script>
Two attributes work together: integrity carries the hash, and crossorigin="anonymous" tells the browser to make a CORS request without credentials. The crossorigin attribute is required — without it, the browser fetches the script without CORS headers and cannot verify the integrity of the response.
Generating SRI hashes
Always generate the hash yourself from the CDN URL rather than copy-pasting hashes from documentation, which can go stale:
# using curl + openssl
curl -s https://cdn.example.com/lib@3.2.1/dist/lib.min.js \
| openssl dgst -sha384 -binary \
| openssl base64 -A \
| sed 's/^/sha384-/'
# node one-liner
node -e "
const { createHash } = require('crypto')
const fs = require('fs')
const buf = fs.readFileSync('/tmp/lib.min.js')
console.log('sha384-' + createHash('sha384').update(buf).digest('base64'))
"
Prefer sha384 or sha512 over sha256 for new integrations. SHA-256 is still secure, but the longer hashes provide a larger collision margin and are recommended by the SRI specification for sensitive content.
SRI in your Express/Fastify middleware
If your MCP server generates HTML dynamically, build the integrity attribute into your template rendering, not into static HTML files. This prevents the common mistake of updating a CDN version without updating the hash:
// sri-assets.js — version-pinned asset manifest
export const ASSETS = {
alpineJs: {
url: 'https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js',
integrity: 'sha384-KzfBjvC4GxHpBq8PjQaHjnvdD4ow/r3NBGPH8gFrBbLHzp4VNmYSwLQQkNVKCk4'
},
tailwind: {
url: 'https://cdn.tailwindcss.com/3.4.4',
integrity: 'sha384-dQKV51f0SqoHhZmPfCIFf5GJyj4cBhfHV42IECijpQLbUzFAcGR5NjQf1Df2BZDZ'
}
}
// in your route handler
app.get('/setup', (req, res) => {
const { alpineJs } = ASSETS
res.send(`
<!doctype html>
<html>
<head>
<script src="${alpineJs.url}"
integrity="${alpineJs.integrity}"
crossorigin="anonymous"></script>
</head>
...
</html>
`)
})
Keep the asset manifest in a separate file that's part of your dependency review process. When you update a CDN version, the manifest update becomes a visible diff in code review — preventing silent asset upgrades that bypass the hash check.
What SkillAudit flags
SkillAudit's static pass scans HTML served by HTTP-transport MCP servers for <script src="https://... and <link href="https://... tags that lack an integrity attribute. Each missing hash is a Medium finding under the Security axis. Multiple missing hashes on a single page compound into a High finding.
The most common pattern SkillAudit sees is a CDN-hosted framework loaded without SRI on an OAuth callback page — precisely the page that processes authentication tokens. This combination reliably flags during audit and is straightforward to fix in under an hour.
Limitations of SRI
SRI protects against CDN compromise, not against including the wrong script in the first place. It doesn't help if you pin a hash to a malicious script you chose yourself. It also doesn't protect resources loaded dynamically via import() or fetch() — only static <script> and <link> tags are checked by the browser's SRI algorithm. For dynamic imports, combine SRI with a strict Content Security Policy that restricts script-src to known hashes or nonces.
Finally, SRI requires that the CDN sends Access-Control-Allow-Origin headers — without them, crossorigin="anonymous" fails and the script won't load. All major CDNs (jsDelivr, unpkg, cdnjs) support CORS. If your CDN doesn't, self-host the asset instead.
Check your MCP server's SRI coverage
SkillAudit's free audit scans all HTML endpoints served by your MCP server for missing integrity hashes. Paste your GitHub URL to get a full report in under 60 seconds.
Run a free audit