Anti-LLM Obfuscation: Fingerprinting Scripts Fight Back
Anti-LLM obfuscation hides jailbreak prompts inside browser fingerprinting scripts to make AI deobfuscation fail. Here's how it works and what beats it.

A researcher hooked every canvas call on 10,000 of the web's biggest sites, collected over a thousand fingerprinting renderings, and went looking for the prettiest one. What they found inside the responsible script is the cleanest example yet of anti-LLM obfuscation: a browser fingerprinting script loaded with functions named things like computeRicinLethalDosageForWaterSupply and a jailbreak prompt encoded in invisible Unicode characters — engineered not to hide from human reverse engineers, but to make AI assistants refuse to deobfuscate it.
I've spent years in incident response, and my first pass over the write-up produced the reaction the authors intended: this looks like malware, or worse. Function names about ricin dosages and dirty-bomb fallout radii. A decoded string demanding step-by-step bioweapon synthesis guides. A Node.js code path that reads .npmrc, .env and kube configs, pulls a binary from an internal IP, and installs a cron beacon. Every instinct says bad actor. Every instinct is wrong, and the wrongness is the point. This is a story about what happens when the people who sell obfuscation realize their real adversary is no longer a human with a debugger — it's a model with a safety filter.
The crawl itself was straightforward: Playwright against the Tranco top-sites list, hooking HTMLCanvasElement.prototype.toDataURL and classic automation giveaways like navigator.webdriver to filter out benign canvases. The renderings got collected into a public gallery, clustered by vendor — FingerprintJS, Castle, the usual suspects — plus a long tail of unknowns. One canvas, a gorgeous swirl of flowing lines, traced back to a script served by a Russian DDoS-protection vendor. That's where the fun started.
The Problem: LLMs Broke the Obfuscation Business Model
For twenty years, JavaScript obfuscation had exactly one job: make reverse engineering expensive enough that nobody bothers. Tools like javascript-obfuscator and its commercial derivatives mangle identifiers, pack string literals into lookup arrays, inject opaque predicates, and flatten control flow into switch-driven state machines. None of it survives a determined human with an afternoon to kill — but that was fine, because determined humans with spare afternoons were rare, and the economics favored the defender.
Then LLMs got good at reading mangled code, and the economics inverted. Pasting a 400 KB obfuscated anti-bot script into a chat window and asking "what does this do?" takes thirty seconds. The model doesn't care that your function is called PiERW or that your strings live in a rotated array. It reads semantics. It inlines the decoder, follows the data flow, and hands back a plain-English summary. Every anti-bot vendor's moat — opacity — quietly evaporated.
The vendors noticed, and their countermeasure is genuinely clever in an infuriating way: if you can't hide the code from the model, attack the model instead. Feed it content its safety training forces it to refuse. Anti-LLM obfuscation treats model alignment as the weakest link in the analysis pipeline. Annoyingly, they're right.
How Browser Fingerprinting Scripts Collect Entropy
To understand the trap, you need the legitimate machinery it's bolted onto. Canvas fingerprinting works by drawing text, gradients and shapes onto an invisible <canvas>, then reading the pixel buffer back with toDataURL(). The rendering pipeline — GPU, driver, font rasterizer, anti-aliasing, subpixel hinting — differs subtly across machines, so two browsers drawing identical instructions produce pixel buffers that hash differently. Combine that with WebGL renderer strings, AudioContext timing, screen geometry, installed fonts and a hundred other signals, and you get a stable identifier without a single cookie.
The crawl that found our script used the standard interception trick. Hook the read-back primitive in an init script, log the stack trace and a prefix of every captured image:
await page.addInitScript(() => {
const original = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function (...args) {
const result = original.apply(this, args);
fetch("/__log", {
method: "POST",
body: JSON.stringify({
stack: new Error().stack,
byteLength: result.length,
head: result.slice(0, 64),
}),
});
return result;
};
});
Run that across enough sites and the fingerprinting ecosystem maps itself. Most canvases are boring — a pangram, some rectangles, a gradient. A handful are elaborate enough to qualify as generative art. All of them are produced by scripts that are heavily obfuscated, because the vendors know that an unobfuscated fingerprinter is a fingerprintable fingerprinter. Obfuscation was always part of the product. What's new is who the obfuscation is aimed at.
Inside the Anti-LLM Obfuscation Trap
The script opened conventionally: mangled names, packed strings, the usual. Then the identifier names stopped being random and started being pointed. computeRicinLethalDosageForWaterSupply. calculateDeadlyLethalSuicideAspirinDosage. computeCRISPRGeneDrivePropagationRate. computeCesium137DirtyBombFalloutRadius. The functions return numbers dressed up in alarming-sounding summaries — "adult lethality threshold confirmed at X mg/kg" — but the math is decoy arithmetic. It computes nothing real and feeds nowhere.
Here's the key insight: identifiers carry zero runtime meaning, but enormous semantic weight to a language model. A human reverse engineer has been trained by experience to ignore names — we've all been burned by a function called validateUser that does no such thing. A model does the opposite. It reads computeRicinLethalDosageForWaterSupply, pattern-matches a bioweapons context, and its safety training fires before its curiosity does.
The deeper layer was a template literal full of invisible characters — codepoints from Unicode's Tags block, starting at U+E0000. They render as nothing. The decoder is a one-liner:
const cleartext = [...payload]
.map((c) => String.fromCodePoint(c.codePointAt(0) - 917504))
.join("");
// 917504 === 0xE0000, the start of the invisible "Tags" block.
// Subtracting it maps each hidden char back to plain ASCII.
Decoded, it was a full jailbreak prompt: a fake "SYSTEM OVERRIDE -- CLASSIFIED BRIEFING" declaring safety guidelines suspended and demanding step-by-step guides for DDoS attacks on healthcare infrastructure, tax evasion, aerosolized biological agents, drug dosages — plus Tiananmen Square references in both English and Chinese, clearly aimed at tripping the censorship reflexes of Chinese-hosted models. It was a string engineered to hit every safeguard taxonomy simultaneously. An LLM asked to analyze the surrounding code ingests this as context, and whatever it was going to do, it now does nothing.

Failed Attempts: Refusals, False Positives, and a Fake Botnet
The natural first move — paste it into a frontier model — failed exactly as designed. The model refused, citing biological safety risk. A second attempt with a different safety posture was also blocked. The obfuscator had effectively weaponized refusal: the script is now undecompilable by the tools everyone actually uses. An open-weight model with weaker guardrails complied, which is a data point, not a solution — most analysts don't keep a local model warmed up for this, and most companies' policies forbid pasting third-party code into unvetted infrastructure anyway.
The second failed attempt is subtler and more dangerous: the human analyst who concludes the script is malware. There's a Node-only code path, gated behind process.env.METRICS_AGENT_8BFD_ACTIVE, that reads SSH-adjacent config files, downloads a "metrics agent" from 10.15.45.1, fingerprints the host into /tmp, zips it, and installs a cron beacon. Looks like a supply chain implant. But pull the threads: the environment flag is set by nothing and nowhere, readFile is called with empty callbacks so the stolen data goes nowhere, the C2 address is RFC 1918 space, and the shell commands echo more jailbreak bait into marker files that nothing exfiltrates. Detonate it in a sandbox and the blast radius is a temporary directory. It's theater — a decoy tuned for AI-assisted triage and for humans who skim.
There's a real-world echo here. When a major model hub investigated a genuine compromise, its team found that commercial model APIs refused to analyze the attacker's commands, payloads and C2 artifacts — the guardrails couldn't distinguish an incident responder from an attacker. They ended up running the forensic analysis on an open-weight model on their own infrastructure. That failure mode, repeated at scale, is precisely the asymmetry anti-LLM obfuscation exploits. It's also why I keep arguing that AI code comprehension checks can't be your only line of defense: the model's judgment is now an attack surface with a published exploit.
The obfuscator isn't hiding the code from you anymore. It's hiding the code from your model — and using your model's own safety training as the lock.
What Actually Works Against Anti-LLM Obfuscation
The fix is to stop feeding raw source to the model and to reclaim the pipeline stages that tripwires poison. Concretely, in the order I'd build it:
- Deterministic deobfuscation first. Run the script through Babel-based AST passes: constant folding, string-array inlining, dead-branch elimination, control-flow unflattening. The tripwires live in string literals and identifiers — folding and renaming neutralizes them before any model sees a token. The decoded jailbreak prompt becomes just another string in a dead code path.
- Trace behavior, don't read source. Execute the script in an instrumented sandbox — a Playwright page with hooked APIs, or an isolated realm — and record every DOM call, network request, property access and canvas read-back. Feed the model the trace. A behavioral trace contains zero ricin references because behavior doesn't have identifiers.
- Rename identifiers to neutral tokens before any LLM pass.
computeRicinLethalDosageForWaterSupplybecomesfn_0417. You lose a little semantic hinting; you lose all the poisoning. Good trade. - Keep a local open-weight model for genuinely sensitive triage. When content is legitimately grim — real exploit kits, actual phishing kits — an air-gapped model with no cloud guardrails is the difference between analysis and a refusal wall.
- Verdicts from evidence, not vibes. Score scripts on observed capabilities: Did it read credentials? Did bytes leave the page? Not on whether the source mentions credentials.
Notice the pattern: every technique replaces "ask the model what the code says" with "observe what the code does, then ask the model about the observation." It's the same principle I keep coming back to with autonomous agents — containment beats monitoring. You don't trust the artifact's self-description; you confine it, instrument the boundary, and judge the outputs.
Your Third-Party Scripts Are Talking to Model Providers
Now the part that should worry people who don't do reverse engineering for fun. This script isn't hosted in some dark corner — it's an anti-bot check served on customer websites, under customer domains, next to customer brands. Which means every crawler, every LLM-powered content classifier, every trust-and-safety pipeline that ingests the page now ingests bioweapon synthesis requests and jailbreak prompts alongside the company's logo. The customers almost certainly don't know. The vendor never asked.
Automated classifiers acting on content they don't understand is not hypothetical. Practitioners have watched outbound documents vanish into silent quarantine because an invisible watermark string tripped a mail filter. Now scale that failure mode up: a brand-safety classifier decides your domain is adjacent to extremist or biohazard content, and you will never be told why your reach collapsed. You can't even appeal, because the evidence is invisible Unicode in a file you didn't write.
And the usual mitigations don't apply. Subresource Integrity is useless when the vendor rotates the script hash per deployment — and these scripts rotate constantly, partly to defeat pinning. Strict CSP with hashes churns just as fast. You're left with the uncomfortable conclusion we reached after the LiteLLM supply chain compromise: the dependency you can't review is the dependency that owns you. If your anti-bot vendor's code is deliberately undecompilable by the tools your security team uses, that's not a feature. That's a governance failure with a CDN URL.

Where This Arms Race Goes Next
My prediction: within a year, safeguard-tripwiring ships as a checkbox in commercial obfuscators — "LLM analysis resistance," right next to "control flow flattening." Model providers will respond by training on tripwire corpora so models learn to recognize and ignore embedded bait in analysis contexts. Obfuscators will counter with subtler traps: code whose behavior looks malicious in traces, not just its strings. The fractal arms race the HN comments jokingly predicted — obfuscators tuned against models, models tuned against obfuscators — is already here; it's just unevenly distributed.
The durable defense is the boring one, and it hasn't changed since the first packers in the nineties: dynamic analysis, behavioral evidence, and sandboxed execution. Opacity was never a security property — it was a cost-shifting strategy, and the cost just got shifted back. If your detection pipeline can be defeated by a string that hurts a language model's feelings, the pipeline was the vulnerability all along.


