Polymarket's CSP Allowed Every Part of the $2.6 Million Crypto Theft
For about fifty minutes on Thursday afternoon, a performance monitoring script loaded on polymarket.com was compromised and started quietly emptying the accounts of anyone it targeted. It walked off with roughly 28.6 million POL, worth about USD $2.6 million.
This is exactly the kind of attack we built our integrity tooling for, so I decided to investigate to understand what happened, and to see how we could have detected and even stopped it. We recovered the malicious JS file, matched it byte for byte to what Polymarket served, rebuilt the timeline from the blockchain and public scan data, and found other DebugBear customers.
What happened
Polymarket used DebugBear's Real User Monitoring (RUM) product and like most RUM tools, you install it by dropping a small loader into your page. Polymarket had it as an inline script in the server-rendered HTML, not in a tag manager:
<script id="debugbear-rum">... scr.src="https://cdn.debugbear.com/C7xcKKrjUnXK.js"; o.head.appendChild(scr); ...</script>
That file is normally 60,493 bytes of genuine monitoring code served out of Google Cloud Storage but on 29 October it was replaced with 15,187 bytes that don't measure anything at all. DebugBear posted that it was "impacted by an ongoing security incident" and asked RUM customers to remove the embed code, and Polymarket shipped a build at 17:59 UTC with the commit message chore(polymarket): remove debugbear rum script (#15337).
The headers on the served JS file tell an interesting story. Before the incident, every response for that file carried Google's fingerprints: via: 1.1 google, x-goog-generation, x-goog-hash and an MD5-style ETag. Once the JS file was compromised and contained the malicious code, none of those headers were present. Instead, they carry ETag: W/"6ae09e23-3b53", which is nginx's format, and it decodes neatly to a modification time of 27 Oct 2026 12:50:43 UTC and a size of 15,187 bytes. Around the same time, the Internet Archive captured other DebugBear snippet URLs returning a plain 404 Not Found with an nginx/1.24.0 (Ubuntu) footer, all proxied through Cloudflare. Whatever was answering for cdn.debugbear.com during the attack, it was not the bucket that normally serves the file.
What the script did
The malicious payload was written specifically for Polymarket and didn't target any other site. It reads three of Polymarket's localStorage keys (polymarket.auth.params, polymarket.auth.proxyWallet and poly_clob_api_key_map) and bails out immediately if they aren't present. If they are, it walks the React component tree looking for Polymarket's own order client, the object with postOrder and createOrder on it, and then uses that client to do the stealing for it.
Users who logged in with a self-custody wallet had their outcome tokens moved in a single safeBatchTransferFrom to 0x3F99139F1C5854bb628c2e576Aaf638e99dFfc18. Users who logged in with email had their positions dumped with fill-and-kill sell orders, first at 97% of the current price, then at 50%, then at the minimum tick. Any cash balance was converted through Polymarket's own Collateral Offramp contract and swapped to POL via fun.xyz and Relay, with the attacker as the recipient. Every transaction went through Polymarket's relayer and Deposit Wallet Factory, which is exactly why it all looked so normal. It even attempted to report each victim's portfolio, IP address, country and OS back to data.debugbear.com/metrics/submit, which is a domain the real product talks to anyway.
On chain it's all there. The first POL landed at 17:10:23 UTC and between 17:26:29 and 18:02:17 the attacker received 36 batch transfers of outcome tokens from 33 different deposit wallets, sent by 31 different relayer addresses. By 18:19:58 the wallet held 28,636,284 POL which has since been emptied out.
Why CSP didn't help
Polymarket had an enforced Content Security Policy, and it was a pretty long one, that we pulled from an archived copy of the homepage taken on the morning of the attack. It allowed https://cdn.debugbear.com in script-src-elem, which it had to for the product to work. It also allowed https://data.debugbear.com, https://*.polymarket.com, https://*.alchemy.com and https://*.fun.xyz in connect-src. That covers every single request the drainer made, including the fun.xyz API key it used was Polymarket's own, lifted from Polymarket's frontend.
There was also no report-uri or report-to directive in the policy that I could see, so even if something had been blocked, nobody would have heard about it. But nothing was going to be blocked anyway, the allowlist answers the question "is this host trusted?" and the answer for every host involved was 'yes' this is trusted.
This is the limit of any defence that's based on URLs: The address of the script was identical before, during and after the attack. Only the bytes of the script changed.
How Report URI could have helped
If the problem is that the content of a trusted script changed, the solution has to monitor the content of that script. This is exactly where Report URI can help.
There are two ways to monitor the content of trusted JS files in the browser today, and Report URI supports both.
Pin the file with SRI and enforce it with Integrity Policy
Subresource Integrity lets you tell the browser exactly what a file should hash to, and if the bytes don't match, the script doesn't run. Polymarket's loader built the script element in JavaScript, so adding it would have been two extra properties on the script tag:
scr.integrity = "sha384-nvAJuHS0Z6q7AfkyvZRJzDdvy9RsuDreB+3j3Vgj3rpFpCE4ArD1afVx9Sgc/N2n";
scr.crossOrigin = "anonymous";
That's the real hash of the genuine file Polymarket was serving on the morning of the 29th. The drainer hashes to sha384-vEX2FtSWv1FEUNLeZdyukLm9rZuQBShp4oXp2jqOJlAERGVhOwbvqVdDH359Jvrg, so once the file was compromised and the malicious code was injected, the browser would have refused to execute it.
The hard part with SRI has never been the mechanism itself, it's so simple to deploy and use, it's the coverage of your script tags with SRI. Someone has to remember to add the integrity attribute to every script tag, and there's no easy way to audit your progress in doing that. That's where Integrity Policy comes in. You can deploy it in report-only mode first and the browser tells you about every script that loaded without SRI on your site:
Integrity-Policy-Report-Only: blocked-destinations=(script), endpoints=(default)
You can now see exactly what exposure you have where you're loading script from third parties without any integrity checking on what you're loading. If you want, you can go one step further and drop the -Report-Only and any script without SRI is blocked outright. A script that you didn't put an integrity attribute on doesn't run, and a script that loads but doesn't match its hash doesn't run either.
A common issue with SRI is that the third-party script is dynamic, it can change at any time by design. I won't digress into the issues with allowing a third-party to run arbitrary code on your production site, and deploy code changes at any moment they see fit, so instead, we'll look at another solution.
Get an alert the moment a script changes
If you aren't ready or able to pin third-party scripts, you can still have the browser tell you exactly what code it executed. Adding 'report-sha256' to script-src asks supporting browsers to send a SHA-256 hash of every script they run:
Content-Security-Policy-Report-Only: script-src 'report-sha256'; report-to default
Script Watch, part of our JavaScript Integrity Monitoring suite, records that hash alongside each URL, so it can tell when the file served at an address it already knows has changed. Set a watcher to alert on all file changes and a swapped file at a familiar URL is sent to you as an alert. For Polymarket, that would have been https://cdn.debugbear.com/C7xcKKrjUnXK.js reporting sha256-BSXwRsg9qkKeD2dtNmUNUdnig7pcrPpWCXyAutWGEos= right up until the afternoon, and then a completely different value, sha256-B1XnZ1BNgQP2wnuedloQJlB6F8BFEyQq21A993d4eeU=, arriving from logged-in users' browsers.
The fingerprint solves a second problem too, one we often run into first-hand with our customers. Getting hold of a JS file that was served historically can be quite hard, especially when the file isn't yours. With CSP Integrity, another feature in our JavaScript Integrity Monitoring suite, when we hold a copy of the file behind a reported hash, you can download the exact bytes your visitors ran from Script Vault, rather than trying to reconstruct it or retrieve it from somebody else.
It wasn't only Polymarket
DebugBear wasn't only used by Polymarket, of course, it was used by many other sites too. A quick scan of our data listed thousands of sites loading JS from cdn.debugbear.com in the same way as Polymarket, and I'd bet there are countless others too. This means they were all at the mercy of the attackers who modified the JS file and the attackers likely had the ability to run any JS they like across those sites.
For now, it seems the sole target was Polymarket and the malicious JS appears to have been benign on those other sites. We'll update the post if we find any evidence that other sites were targeted.
What should you do?
If you use DebugBear RUM, follow DebugBear's advice and remove the embed code immediately and wait until they publish a full account of what happened.
For every other third-party script on your site, assume the same thing can happen to it, because it can. Load scripts with crossorigin wherever the vendor supports CORS, pin what you can with SRI, use Integrity Policy to find the scripts you missed, and turn on 'report-sha256' for CSP Integrity so Script Watch can tell you when a file changes underneath a URL you trust. If you are using CSP, give it a reporting directive so you have some ability to see what's happening in production.
Credit
Hudson Rock first identified the payload and the wallet, and DeFi Prime published an early technical breakdown. Our own analysis relied on data we gathered and corroborated what others have published. Polymarket are not a customer and we do not protect their website.
Get started with Report URI
Our JavaScript Integrity Monitoring brings CSP Integrity, Script Watch, Script Vault and Integrity Policy together, and none of them need you to change or deploy code to start collecting data. A couple of response headers is enough to see every script running on your site, the exact hash of what each browser executed, which ones are loading without SRI, and an alert when the file behind a URL you trust is suddenly a different file. You can start a 30-day free trial with no credit card required, and you'll have data flowing within minutes.