Surviving a CDN Supply-Chain Attack: A Next.js Checklist (2026)
How a compromised CDN turned a game launcher into malware — and the measured Next.js checklist (SRI, CSP, self-hosting) that survives it.
In late September 2026, someone took over a domain, edited four JavaScript files on a major game publisher's official CDN, and quietly turned a game launcher into a credential stealer. It took more than a month — and a client patch on November 4 — before the tampered files were fully pulled back, and even then, one CDN edge server kept serving the poisoned file after the fix.
If the phrase "our site loads scripts from a CDN" describes your production setup in any way, this incident is your incident too. This post breaks down what happened, then walks through a verified checklist for a Next.js site: Subresource Integrity, a cache-compatible Content Security Policy, and self-hosting defaults — every config in this article was actually applied and tested on a live Next.js 16 site, not sketched from memory.
TL;DR
- On Sept 27, 2026, four official JS files on Level Infinite's CDN were tampered with; the NIKKE PC launcher loaded them on start, and the injected code pulled a LuaJIT-based remote-control agent that collected passwords, browser sessions, SSH keys, and crypto wallets.
- A stale edge cache kept serving a tampered file even after the origin was cleaned — CDN fixes are only done when every edge agrees.
- For a Next.js site you can harden without giving up caching:
experimental.sri(build-time integrity hashes), a nonce-less static CSP vianext.config headers(), and self-hosting everything you don't really need from someone else's origin. - Nonce-based strict CSP forces dynamic rendering and is incompatible with Partial Prerendering — for a cache-first blog, don't reach for it first.
What happened
The short version, cross-checked across the published research and reporting (Level Infinite's statement, researcher MotherSphere's write-up, and BleepingComputer's coverage):
| When | What |
|---|---|
| Sept 27, 2026 | The domain insightjs.com was registered the same day; four JS files on Level Infinite's official CDN (aegis-fix.min.js, images.7fec4c32.js, FeedList.feb647c9.js, vue-bucket.min.js) were modified to load a loader from that domain |
| Late Sept – Oct | Every launcher start fetched the tampered files; from late October, launches could pull a real payload |
| Nov 3 | Three of four files were replaced with clean copies — but at least one CDN edge kept serving the tampered version |
| Nov 4 | Client update shipped (02:30 UTC+9); issue acknowledged |
The modified scripts injected a <script> element pointing to
insightjs.com/l.js, executed it, and removed the tag — an observation
window of milliseconds. The loader used the launcher's own download bridge to
fetch an obfuscated LuaJIT remote-control agent, which then sat on the
machine harvesting interactive shells, browser password stores, developer
tools credentials, SSH keys, and wallet data. Users on Linux and Steam Deck
via Proton were in the loop as well, because the launcher pulled the same web
assets.
Three details matter for web people, and none of them require caring about games:
- The entry point was not the app's code — it was the origin the app trusts. The attacker got write access somewhere in the CDN/deploy pipeline, and the publisher reportedly never confirmed how. If your trust boundary ends at "it's on my CDN", your boundary is fake.
- The loader domain was registered the same day as the attack. This was not opportunistic runtime adtech: a dedicated domain, clean op-sec, and a failure fallback to a second TLD. Domain allowlists must also care about what got registered yesterday.
- "Replace the file" did not mean "the fix is live." An edge server kept serving the tampered copy for hours. Anyone whose mitigation plan ends at fixing the origin is living with an unknown number of stale copies.
A browser context makes all three land harder: if your site loads analytics,
ads, tag managers, or hotlinked libraries from origins you don't ship,
the exact same chain works — swap the launcher for your layout.tsx.
The checklist (measured on a real Next.js 16 site)
Everything below was applied to a small production blog (Next 16, App
Router, Cache Components/PPR on) and verified: build clean, browser console
silent, headers observable with curl. Copy-adapt, but re-test — CSP is
unforgiving about details.
1. Decide your trust model before writing any header
Ask one question per script: if this file changed on Friday night without
notice, what's the blast radius? For this blog the answer was: framework
chunks (self-originated), fonts (self-hosted by next/font), article images
(one dedicated asset domain, immutable files), inline JSON-LD (generated at
render time), Google AdSense (planned for later). That inventory is the
checklist; the headers just enforce it.
2. Self-host everything that doesn't need to be "somewhere else"
Fonts are the classic case — next/font already ships them through your own
origin, with zero layout shift [verified — this site ran this way from day
one]. Vendored libraries are the second case: if you hotlink jQuery-like
artifacts from a public CDN "because it's cached", you have accepted a
zero-margin trust dependency on that CDN's write access. The NIKKE files
were * insiders'* official files — the difference between "our pipeline" and
"their pipeline" is smaller than it looks.
Self-hosting also survives the second-class failure in the incident: your
asset origin joins your normal on-demand invalidation flow, so a cleanup is
a revalidateTag, not a phone call to another company's NOC.
3. Integrity for your own chunks: experimental.sri
Next supports generating Subresource Integrity hashes at build time for the framework's own scripts — an App Router-only experiment, but exactly the right shape for a static, cache-first site:
// next.config.ts (tested)
const nextConfig: NextConfig = {
experimental: {
sri: {
algorithm: "sha256",
},
},
// ...
};
Measured result on this site after a clean build: every page emitted 7
integrity="sha256-…" attributes across its script tags (home and posts
alike). Any tampered replacement of those bytes becomes browser-rejected,
even if it comes from your own compromised origin — integrity is cheap
insurance precisely because it doesn't care where the file came from.
If the space pose of "we attest to each script compressed by 'https://mycdn'" versus "we attest to the bytes" appeals to you, the byte-level one is the only rule that survives a rebuilt origin.
4. A CSP you can actually keep while caching
You have two families in Next 16, and they are mutually exclusive in practice:
- Nonce-based strict CSP (
'nonce-…'+'strict-dynamic') requires every page to be dynamically rendered. The docs state plainly: Partial Prerendering is incompatible with nonce CSP, since a pre-rendered static shell cannot know a per-request nonce. If your site is a cache-first blog — like this one, where every route is ISR/PPR-cached for weeks — a nonce policy effectively deletes your caching layer and hands you the bill per request. [Verified against the official docs, not implemented here.] - Nonce-less static CSP with
'self'-anchored sources lives innext.config.headers()and coexists perfectly with Cache Components and CDN caching. This is what a content site should use:
// next.config.ts (tested)
const cspHeader = `
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https://assets.beyondimy.com;
font-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`.replace(/\s{2,}/g, " ").trim();
const nextConfig: NextConfig = {
async headers() {
return [
{
source: "/(.*)",
headers: [
{ key: "Content-Security-Policy", value: cspHeader },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "X-Content-Type-Options", value: "nosniff" },
],
},
];
},
};
Read what the policy denies: no remote origins in script-src, no
object-src at all, no one can frame you, and injected inline <script>
blocks (like the mapping value of your JSON-LD) still work thanks to
'unsafe-inline'. That 'unsafe-inline' costs real protection — but for a
blog whose only inline scripts are generated at render time, it trades a
loopable nonce for something essential: you keep your cache. Your risk
budget should be revisited when strict-CSP inline styles matter more than
sub-second TTFB from the edge.
'self' in script-src still allows your own origin's scripts to do
anything the page can — the point of the NIKKE incident is that the origin
itself was breached. That's why sri resolves the byte-identity problem, and
the CSP confines blast damage to a domain you have revalidation logic for.
5. The scripts you genuinely need from third parties: pin them
When AdSense arrives on this blog, the inline Script tag goes under its
own CSP audit, and the audit rule is: enumerate the smallest host it
needs, allow exactly that in script-src, and add integrity with
crossorigin on the tag itself [verified syntax — HTML standard attribute;
runtime allowlist depends on what the vendor itself decides to load, which
you must revisit at integration time]. The realistic policy: a script whose
"integrity" can't be pinned is a script whose vendor you've accepted as a
co-author of the site. Document that in your deploy notes, not in your
threat model.
6. Your own cache is part of the blast radius — validate fixes the way caches do
The stale-edge lesson generalizes to your own vitally cached stack. When this blog ships updates, it runs a full invalidation battery — data tags, route-entry tags, path revalidation — and then makes warm-up requests to confirm that fresh bytes render before announcing the change. Verification belongs in two places, both scripted, neither trusted to "it worked on my laptop":
# 1. Headers as deployed (curl the *edge*, not your app)
curl -sI https://www.example.com | grep -i content-security-policy
# 2. Integrity actually present in the HTML
curl -s https://www.example.com | grep -o 'integrity="sha256-[^"]*"' | wc -l
If a mitigation tells you "fixed", but any edge node anywhere still discusses Monday's bytes, the incident is not fixed. Cache TTL hygiene for HTML, and immutability for hashed assets, is a prerequisite — this blog versioned its R2 filenames precisely so that a "refresh" can never mean "same path, new bytes".
7. Lockfile hygiene is the same lesson one level down
Tampered writes to a pipeline are one threat model; a malicious bump inside
your dependency tree is the other half. Pin your lockfile (package-lock.json
committed, reviewed in PRs), gate on unexpected post-install scripts, and
treat a new transitive dependency in a routine update as a code review topic,
not a ketchup on fries. The NIKKE attacker chose a CDN; most SaaS stacks are
attacked through npm and pip. The defense is boring on purpose.
What the NIKKE report tells us about recovery
Worth restating for anyone reading only the checklist: the visible cleanup (removing files, patching the client) does not unwind what the agent did while it was live. Credential stores, SSH keys, cloud tokens, and wallet seeds gathered in October stay stolen in November. If a user's responsibility starts at "assume execution happened during the exposure window", a site owner's version is "assume data flowed as long as the scripts did". That is what makes every step above cheaper than one incident at scale.
Wrap-up
- The NIKKE launchers incident is not a game-industry story; it is a trust-what-you-load story with four line items and a month-long half life. Any web app that loads a script it did not write into a page it serves to humans owns the same risk.
- For a cache-first Next.js site, the working combo in Next 16 is
experimental.sri+ static nonce-less CSP viaheaders()+ self-hosting defaults, not the stricter nonce path — which explicitly trades your PPR/CDN caching away. - A cleaner draft of the incident plan: your "recovery complete" moment is the last edge reporting the new bytes — run the curl checks and make it a script.
Sources: incident details from Level Infinite's public statement, as corroborated by the MotherSphere GitHub report and BleepingComputer's coverage (Nov 4, 2026). Also referenced: Next.js's official Content Security Policy guide (documented in-pack in v16 for partial-prerenders-vs-noncompat CSP behavior). Config snippets in this post were applied and measured on www.beyondimy.com (Next 16) on Nov 6, 2026.