Navigation
Cybersecurity

The Pitfalls of CDN and npm: How to Protect Your Website Using SRI and CSP via Cloudflare Worker

October 31, 2026 techy.sk

Loading a library via <script src="…/latest/…"> is convenient. However, this means that your page’s content depends on what is currently hosted on a third-party server. In supply chain attacks, where an attacker takes control of a package maintainer’s account, the content can be altered without you making any changes. This guide demonstrates two layers of defense for simple websites: Subresource Integrity (SRI) and Content Security Policy (CSP) added via a Cloudflare Worker.

What’s Verified and What Isn’t

The assignment described a specific attack from late October 2026 targeting libraries for animations and components. I could not find any mention of this in the available reports. However, the year 2026 saw numerous attacks on npm via compromised administrator accounts, and these are well documented. Most known cases targeted developer machines and CI systems and exploited installation scripts (preinstall, postinstall) to steal secrets. Malicious code in visitors’ browsers is a different matter, though it is also a real risk. I did not include specific claims about bypassing ad blockers via masked subdomains because I could not confirm them.

A compelling example of the risks posed by CDNs is a case from early summer 2024. After a change in ownership, the domain of a popular polyfill service began serving malicious code to thousands of websites that loaded it directly. An attack on a browser via a CDN is therefore a real possibility and does not require its own package in the registry.

Why is latest a problem

  • A link without a version specified points to content that may change without notice.
  • SRI cannot be used with such a link. The hash must exactly match the file, so the link must point to a specific, unchanging version.
  • A file in the repository and a package published in the registry do not have to be identical. A package can also be published from a local machine with changes that were never in the repository.

Layer 1: Subresource Integrity

SRI adds a hash of the expected content to the script tag. The browser downloads the file, compares it, and if there is a mismatch, it does not execute it.

<script
  src="https://cdn.example.com/[email protected]/dist/kniznica.min.js"
  integrity="sha384-VYPOCITANY_HASH"
  crossorigin="anonymous"></script>

You calculate the hash from a file that you have actually verified:

#!/usr/bin/env bash
# Použitie: ./sri.sh URL_NA_PRIPNUTU_VERZIU
set -euo pipefail
url="${1:?zadaj URL}"
case "$url" in
  *latest*|*@latest*) echo "Odmietam nepripnutú verziu." >&2; exit 1 ;;
esac
hash="$(curl -fsSL "$url" | openssl dgst -sha384 -binary | openssl base64 -A)"
echo "integrity="sha384-${hash}""

It’s best to also compare the hash with a file from the package downloaded directly from the registry or from a different CDN. A hash from the same source—which could have been compromised—doesn’t protect you from changes made to the file at the time of the initial download.

SRI limitations: it does not protect inline scripts and does not work for code loaded by a dynamic import() or by a script that adds another script itself. For important projects, it is safest to store the library in your own repository and serve it from your own domain.

Layer 2: CSP via Cloudflare Worker

CSP determines what a page is allowed to load and where it is allowed to send data. For single-file websites with an inline script, you cannot rely on 'unsafe-inline', as that would nullify all security measures. Therefore, the Worker generates a random nonce and adds it to all script and style in the HTML that originates from your origin server. Scripts without a nonce will not run. You do not need to allow the entire CDN host, so an attacker cannot load another library from the same CDN.

export default {
  async fetch(request, env, ctx) {
    const response = await fetch(request);
    const type = response.headers.get("Content-Type") || "";

    const headers = new Headers(response.headers);
    headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
    headers.set("X-Content-Type-Options", "nosniff");
    headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
    headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
    headers.set("X-Frame-Options", "DENY");

    if (!type.includes("text/html")) {
      return new Response(response.body, {
        status: response.status,
        statusText: response.statusText,
        headers,
      });
    }

    const bytes = crypto.getRandomValues(new Uint8Array(16));
    const nonce = btoa(String.fromCharCode(...bytes));

    const csp = [
      "default-src 'none'",
      `script-src 'nonce-${nonce}'`,
      `style-src 'self' 'nonce-${nonce}'`,
      "img-src 'self' data:",
      "font-src 'self'",
      "connect-src 'self'",
      "base-uri 'none'",
      "form-action 'self'",
      "frame-ancestors 'none'",
      "object-src 'none'",
      "upgrade-insecure-requests",
    ].join("; ");

    const headerName = env.CSP_REPORT_ONLY === "1"
      ? "Content-Security-Policy-Report-Only"
      : "Content-Security-Policy";
    headers.set(headerName, csp);

    const rewritten = new HTMLRewriter()
      .on("script", { element(el) { el.setAttribute("nonce", nonce); } })
      .on("style", { element(el) { el.setAttribute("nonce", nonce); } })
      .transform(response);

    return new Response(rewritten.body, {
      status: response.status,
      statusText: response.statusText,
      headers,
    });
  },
};

How it works: connect-src 'self' It blocks fetch, XHR, WebSocket, and beacons to external domains. Even if malicious code in a loaded library were to run, it would have nowhere to send the data. Subdomains are not 'self', so requests to a disguised subdomain are also blocked.

How to set it up:

  1. First, set the variable CSP_REPORT_ONLY=1. The browser does not enforce the policy, but it reports violations in the console. Test the page.
  2. Adjust the policy if it’s blocking something legitimate—for example, allow a specific host in img-src.
  3. Only then should you remove the variable so that the policy is enforced.

Once HSTS is enabled, it prevents the website from being opened via HTTP. includeSubDomains only if all subdomains support HTTPS.

What CSP Can't Solve

  • Data can also be extracted through other means, such as elements you allow (images from external domains) or through DNS lookups. Therefore, the policy is based on default-src 'none' and only allows what is strictly necessary.
  • If you allow a connect-src a third-party domain, an attacker could also exploit it.
  • A nonce protects against third-party scripts injected into the page. It does not protect against a compromised origin where a malicious script could be injected into the HTML before the Worker, since the Worker would also receive the nonce. The origin must remain trustworthy.
  • A script that has already run on your page with an SRI hash calculated from a malicious file will not protect you. Verify the hash independently.

System prompt for an AI coding agent

If you use an AI agent to write pages, this prompt will instruct it to treat external scripts with caution. Importantly, it prohibits the agent from generating hashes, since the model cannot calculate a hash without downloading the file.

Pri práci s externými skriptami a štýlmi dodržuj tieto pravidlá:

1. Nikdy nepoužívaj nepripnuté verzie. Zakázané sú „latest“, „@latest“, rozsahy verzií a URL bez presnej verzie. Vždy použi presnú verziu, napríklad „@1.2.3“.
2. Uprednostni knižnicu uložiť do repozitára a servírovať z vlastnej domény. Externý CDN použi len keď je to nevyhnutné a povedz mi prečo.
3. Ku každému externému skriptu a štýlu pridaj atribúty integrity="sha384-…" a crossorigin="anonymous".
4. Hash nikdy nevymýšľaj ani neodhaduj. Vypočítaj ho zo skutočne stiahnutého súboru príkazom:
   curl -fsSL URL | openssl dgst -sha384 -binary | openssl base64 -A
   Ak príkaz nemôžeš spustiť, nechaj v kóde viditeľný zástupný text „DOPLN_SRI_HASH“ a upozorni ma, že ho treba doplniť.
5. Nepoužívaj dynamické importy ani skripty, ktoré načítavajú ďalšie skripty z cudzích domén. Ak je to nutné, upozorni ma.
6. Nepridávaj inline obsluhu udalostí (onclick a podobne) ani eval. Skripty píš tak, aby fungovali s politikou CSP bez „unsafe-inline“ a „unsafe-eval“.
7. Po každej zmene vypíš zoznam všetkých externých zdrojov s verziou a hashom, aby som ich mohol skontrolovať.

Checklist

  • No latest in the links. Versions are pinned.
  • Every external script has SRI and crossorigin.
  • CSP with default-src 'none' and connect-src 'self' is deployed, initially in reporting mode.
  • Key libraries are stored in our own repository, not on a third-party CDN.
  • Access to accounts (npm, GitHub, Cloudflare) is protected by hardware keys or access keys, not just a password and SMS.

What to Take Away From This

A supply chain attack doesn’t require compromising your website. All it takes is for you to load something that has been altered in the meantime. SRI prevents the altered file from running, and CSP prevents data leaks if something does happen to run. You can enable both layers for free if you serve your website through Cloudflare. Neither of them replaces the need for caution when selecting dependencies.

Share Article

You Might Be Interested