Poisoned Pipeline / Incidents / AS341

AS341 · 2026-10

DebugBear's RUM script URL served a wallet drainer to Polymarket for about an hour

CDN / web assets

On 29 October 2026 the real-user-monitoring script Polymarket loads from
cdn.debugbear[.]com was replaced with a wallet drainer, and for roughly an
hour it ran inside logged-in trading sessions. Polymarket's status page
reports unauthorized wallet activity at 17:38 UTC, moves the exchange to
cancel-only mode with relayer actions paused at 18:05 and resolves the
incident at 18:06. Its postmortem, published at 20:07 UTC on 30 October,
says a third-party vendor whose performance-monitoring script loaded on
polymarket.com was compromised, that the attacker replaced the script
with one built to target Polymarket users, and that for some people who
had the site open during a short window it used their active session to
sell positions and move funds. Polymarket removed the script, cleared
every cache, reset all active login sessions, paused trading and the
relayer, and referred the matter to law enforcement. It refunded $3.9
million in total: $2.7 million to 726 drained wallets, of which 689 were
made whole by 30 October, and $1.19 million in goodwill payments to about
5,500 wallets that lost value during the pause or had positions taken.

DebugBear's own account is the vendor confirmation. At 18:00:58 UTC on
29 October it posted that it was impacted by an ongoing security incident
and told RUM customers to remove the embed code from their sites. On
30 October it said its DNS server records had been compromised through
its domain registrar, that it could not receive email sent to its domain,
that its initial investigation pointed at the website of one specific
customer rather than at its internal systems, and that the investigation
was continuing. cdn.debugbear[.]com had no DNS record when reporters
checked on 1 November.

Ohey's analysis, built on two captures Hudson Rock took at 18:12:06 and
18:13:04 UTC and on Internet Archive copies of the same URL, dates the
substitution tightly. Every archived response for
cdn.debugbear[.]com/C7xcKKrjUnXK.js from June 2025 to 12:06:55 UTC on
29 October is DebugBear's stock RUM code: a per-project file whose
configuration carries "snippetKey":"C7xcKKrjUnXK" and
"sendTo":"https://data.debugbear.com", and whose last clean copy is
60,493 bytes served from Google Cloud Storage with an MD5 ETag and
x-goog-* headers. At 16:54:57 UTC Cloudflare could not reach that origin
and returned error 521. What answered afterwards was 15,187 bytes of
plain, unminified JavaScript in a function named startdebugger, carrying
an nginx-format ETag (6ae09e23-3b53, which encodes the file's timestamp
and size) and none of the Cloud Storage headers — a different server
answering for the DebugBear hostnames, data.debugbear[.]com included,
which returned an nginx/1.24.0 404 page at 18:13:44.

The drainer steals no keys. It reads three localStorage entries that
Polymarket's own app writes — polymarket.auth.params (login type and
wallet address), polymarket.auth.proxyWallet (the deposit wallet holding
the account's funds) and poly_clob_api_key_map (trading credentials for
each wallet) — and stops if the visitor is not signed in or holds no
trading credentials for that wallet. It then borrows the trading client
the page has already built. React keeps component props and hook state on
internal fiber objects reachable from DOM elements under keys beginning
__reactFiber$; the script walks up to 400 buttons, links and containers
for five seconds or 60,000 nodes and keeps any object exposing postOrder
with createOrder or createMarketOrder, which is Polymarket's order client
with the user's signer still attached. Accounts are sorted into eoa
(browser wallet such as MetaMask) and magic (email login). Cash goes
first: the script reads the deposit wallet's balances over Polygon RPC
and up to 200 open positions from data-api.polymarket.com, asks
api.fun.xyz for a quote that swaps the balance and pays the recipient in
POL, falls back to native USDC and then to a direct USDC.e transfer, and
wraps the calls in an EIP-712 DepositWallet batch signed with the page's
signer and posted to relayer-v2.polymarket.com/submit with a one-hour
deadline. Positions worth more than $10 follow: browser-wallet accounts
move them with one safeBatchTransferFrom call on the conditional-token
contract 0x4D97DCd97eC945f40cF65F87097ACe5EA0476045, while email-login
accounts sell each position with fill-and-kill SELL orders at 97 per cent
of the current price, then 50 per cent, then the market's minimum tick,
and sweep the proceeds through the cash routine. Reports on each step go
to three endpoints on data.debugbear[.]com and carry the deposit and
login wallets, login type, operating system, a tracking ID and the IP
address and country the geoblock API returned. Every other host the
drainer called was already in Polymarket's Content Security Policy:
*.polymarket.com for the APIs and relayer, *.alchemy.com for RPC and
*.fun.xyz for swap quotes.

The chain date is clean. The recipient,
0x3F99139F1C5854bb628c2e576Aaf638e99dFfc18, had held nothing and sent no
transactions before 17:10:23 UTC. Between 17:10 and 18:02 it received
28,636,284.45 POL delivered by swap contracts, 36 position-token transfers
from 33 deposit wallets, 10,383.33 USDC.e in fifteen transfers and
6,320.69 USDC in fourteen. The balance climbed sharply after Cloudflare's
Chicago edge cached the drainer at 17:27 and stopped rising at 18:11,
twelve minutes after Polymarket built its release without the script
(17:59:20 UTC, commit message "chore(polymarket): remove debugbear rum
script (#15337)"). Everything was forwarded by 18:38 — the POL to three
wallets, the positions to a fourth. At DefiLlama's 0.0922 the POL alone
was about $2.64 million.

Hudson Rock reviewed the file as text without running it and found
nothing that sends the API keys, a password, a seed phrase or a private
key to the attacker's server, and notes that a signing request does not
by itself prove a trader approved it blind: the wallet provider may still
prompt, refuse or apply its own rules for the session. Ohey's static
analysis agrees that the script carries no keys and targets Polymarket
alone — it reads Polymarket's storage keys, calls Polymarket's APIs and
finds Polymarket's trading client inside the page.

Mapped techniques

supply-chain DebugBear's real-user-monitoring script host

T0205 · Initial Access · Compromise third-party CDN or hosted web asset

DebugBear serves one script per customer from cdn.debugbear[.]com,
and Polymarket embedded its own: C7xcKKrjUnXK.js. DebugBear says
the DNS server records were changed through its domain registrar,
so the hostname kept resolving, the embed code kept working and
the page kept loading a script — from whatever server the new
records named. T0205 names registrar hijack as one of the ways a
widely embedded asset host is taken over.

T0121 · Resource Development · Compromised legitimate artifact

The URL, the filename and the per-project snippet key did not
change; the bytes did. The last clean response was 60,493 bytes
from Google Cloud Storage, the malicious one was 15,187 bytes from
an nginx host, and no version, hash or integrity attribute
distinguished them on the consumer side. There was nothing to
detect by identity, only by content.

post-supply-chain Signed-in Polymarket traders

T0159 · Execution · Malicious artifact execution

The script ran in the page's own origin during normal page load,
in sessions that were already authenticated, with no extension, no
download and no install step. Visitors the geoblock API marked
blocked were skipped; everyone else was enumerated for a login.

T0222 · Execution · Borrow the page's own client and signer

The drainer carries no credential. It reads the app's localStorage
authentication and trading keys, walks React fiber trees to find
the order client the page already built, and reuses that client's
signer to sign EIP-712 DepositWallet batches which it submits
through Polymarket's own relayer. The signed requests are the ones
the application is designed to send; the attacker supplied only
the destination and the payload.

T0207 · Defense Evasion · Skip or forge artifact integrity verification

The script was hotlinked from the vendor's hostname with no
Subresource Integrity, so no browser could refuse the changed
bytes, and the Content Security Policy allowed the vendor domain
by name — a policy that says which hosts may serve code, not which
code they may serve. Browsers and edge caches held the malicious
copy while it was live; Polymarket cleared its caches as part of
the response.

T0201 · Impact · Customers compromised

Polymarket counts 726 drained wallets and about $2.7 million, plus
$1.19 million paid to roughly 5,500 wallets that lost value during
the trading pause. The on-chain trace is narrower and harder: 33
deposit wallets in the first 52 minutes, 28.6 million POL, and a
later count of 721 source wallets behind 2.72 million pUSD. The
supplier's script was the delivery vehicle; the traders' own
sessions were the prize.

Framework gaps noted

Sources