Nobody shipped a new version
Wordfence's August 9 advisory on the BdThemes compromise describes a supply-chain attack with no supply in it. No release, no install script, no typosquat, no modified file in the plugin repository. Attackers took write access to the vendor's storage bucket and changed a JSON feed that 350,000 sites fetched on every admin page load. For a developer, the question this raises is what the machine running your coding agent is allowed to write, and to whom.
Every supply-chain control you own inspects things that get published. This one published nothing, and reached 350,000 sites anyway.
On August 7, 2026, Wordfence started hearing about WordPress sites running BdThemes plugins that had grown administrator accounts nobody created. The list runs to seven plugins: Element Pack Addons for Elementor, Prime Slider, Pixel Gallery, Ultimate Post Kit, Ultimate Store Kit, Live Copy Paste, and Smart Admin Assistant. Element Pack alone has more than 100,000 active installations, and the seven together clear 350,000.
By August 8 the vendor had cleaned its API and WordPress.org had pulled all seven plugins pending investigation. Wordfence published its PSA on the compromise the following day, and its central finding is a negative: nobody had touched the plugin code.
Nothing was published
BdThemes plugins carry an internal component called Biggopti whose job is small and dull: fetch promotional banners from the vendor's remote API and draw them in the WordPress admin dashboard. The banner data is a static JSON file in a DigitalOcean Spaces bucket, served from behind Cloudflare.
Attackers got write access to that bucket. That is the whole intrusion.
Biggopti took the display_id field out of the JSON response and dropped it
into an HTML id attribute without escaping it, a cross-site scripting flaw
Wordfence rated
CVSS 5.4, medium
and traced back to March 1, 2026, when a developer wrote it once and copied it
from one plugin to the next. A display_id carrying an onanimationstart
handler is therefore JavaScript, and it runs in the browser of every logged-in
administrator, on every wp-admin page load, on every site with the plugin
installed.
The payload used that seat well. A script called w2.js asked a
command-and-control server, the machine an attacker uses to tell a payload which
sites are worth taking, for targeting instructions, then created hidden
administrator accounts through the WordPress REST API on the back of the real
administrator's own session and nonces. A variant derived predictable usernames
from the site's hostname. A fake plugin, often named something like
wp-smart-thumbnails, delivered a web shell at emer-run.php, meaning a script
an attacker can drive by loading an ordinary URL. Must-use plugins, which
WordPress loads on every request and hides from the plugins screen, carried a
magic-login backdoor and a module that concealed the new accounts by rewriting
the user totals the admin screen displays.
Timestamps in the poisoned records put the earliest possible start at June 23, 2026, seven weeks before anyone noticed.
Line that up against the controls a careful team runs. Version pinning: the versions never changed. Lockfiles and integrity hashes: they matched, because the files they covered were untouched. Install-script review: there was no install. Typosquat detection: right name, right vendor, right repository entry. Code review of the diff between releases: no diff, because no release. Certificate and CDN: both in perfect health. Cloudflare and TLS proved that the JSON reached the site unmodified from the bucket it was supposed to come from, which is precisely what made it useful.
The incident starts with a credential that could write
Read the advisory from the vendor's side and the attack chain shortens to one step. Someone obtained write access to a storage bucket. Everything after that is consequence.
Upstream compromises keep arriving in that shape. The Red Hat npm scope worm, the repository that really was Microsoft's, the extension marketplaces, the container registries: in each of them the payload is the part that gets written up, and the part that mattered was somebody ending up with a credential that can overwrite an artifact other people fetch on a schedule.
Where does a credential like that live? In a developer's environment. A
DIGITALOCEAN_ACCESS_TOKEN in a shell, a doctl config in ~/.config, a
registry login in ~/.docker/config.json, an AWS profile, a GitHub token in
~/.git-credentials, an SSH key that can push. On a laptop, in a terminal, next
to the project. In 2026, next to the project is also where you run your coding
agent.
Most posts on this blog describe a way to turn that agent against you. A bug report that ran a command. A web page that rewrote the agent's config. A comment nobody could see. A skill that ran with your identity. Three harness bypasses in one Black Hat talk. Take the catalogue as read, and assume that on some Tuesday one of them lands on you.
How bad that Tuesday gets turns on a question you can answer today, without knowing which injection wins: what is this machine allowed to write, and to whom? On a laptop the answer is everything you are allowed to write, and if you ship anything, that includes the artifact several hundred thousand strangers fetch without looking.
The token is not in the machine
Bromure Agentic Coding runs the agent in a disposable Linux VM on Apple Silicon, and every byte of its traffic crosses a proxy on the host, outside the box the agent runs in. Bromure arranges the credentials around that boundary instead of putting them inside it.
Put your DigitalOcean personal access token in the Credentials panel and
Bromure keeps the real value on the Mac. What lands in the VM is a fake: a
DIGITALOCEAN_ACCESS_TOKEN environment variable and a ~/.config/doctl/config.yaml
that make doctl work without doctl auth init, holding a placeholder. When
the VM makes a request to api.digitalocean.com, the host proxy substitutes the
real token on the wire, for that destination, and nothing else.
Anything running inside that VM can read the environment, grep the dotfiles and
walk the whole disk: the agent, a dependency, a build step, a shell command that
came out of a poisoned issue. What it collects is brm_…. The same arrangement
covers the rest of the set: GitHub, GitLab and Bitbucket tokens, container registry
logins written as a fake base64 blob in ~/.docker/config.json, a synthetic
kubeconfig with throwaway client certificates, Linear, the HTTPS database
endpoints, and anything else you add under Other API keys. AWS goes further:
the host re-signs each request with SigV4 using the real secret, so bypassing the
proxy earns an InvalidSignatureException rather than an unauthorized write.
Guardrails decide the verb, on the Mac
This incident needed one operation: overwrite an object in a bucket.
Guardrails is a policy engine inside the host proxy, with a mode per
resource: Off, Block destructive, or Read-only. Set DigitalOcean to
Read-only and every mutation to api.digitalocean.com comes back as a hard
403 the agent reports as an ordinary API failure. Bromure makes that
decision in macOS. There is no setting inside the VM to change, no
environment variable to unset, and no file to edit.
Publishing asks first
Every credential entry has Require approval to use. Turn it on and each fake→real swap raises a consent dialog on the host before the real value reaches the wire, with time-bounded grants on the SSH path: five minutes, an hour, the rest of the session. Pushing a release is something you meant to do, and it costs you one click. A write you did not start shows up as a dialog you were not expecting, at a moment you were not expecting one.
Registries and git get the same treatment
Guardrails classifies container registry traffic by method against the
registry's own hostname: GET and HEAD are a pull, PUT and POST are a
push, DELETE is destructive. GitHub, GitLab and Bitbucket are covered for
both the REST API and git over HTTPS, where git-receive-pack counts as a
write and is blocked in Read-only mode while fetches always pass. A profile
that reads your whole dependency graph and pushes nothing is two settings.
The environment is a list you wrote
A Bromure profile sees the Mac folders you shared with it, up to eight, each
mounted under /home/ubuntu. Everything else on your Mac is not mounted,
not reachable, and not enumerable. The credential set is the one you added
to that profile, so a profile you use to build a frontend holds no cluster
credentials for anything to find, hidden or otherwise.
The runtime fetch crosses the proxy too
Now take the downstream side of the story, where you are one of the 350,000. What you can catch there depends on where you put the inspection.
Bromure's Supply Chain panel screens package fetches across npm, PyPI,
Cargo, RubyGems, Maven, NuGet, Go modules and Packagist: an age gate that is on
by default and refuses anything published in the last two days, OSV lookups,
socket.dev or Delpi as a filtering provider, install-script stripping that
rewrites the tarball and fixes up the registry metadata hash so npm still
verifies, and a prompt before lockfile-pinned tarballs pass through unmodified.
The .npmrc and pip.conf inside the VM can tighten those rules and cannot
loosen them.
Those rules earn their keep because of where Bromure enforces them. The host proxy applies them to a request rather than to a package, so a JSON file pulled from a vendor bucket at runtime by a build step, a CLI, an MCP server or the application itself crosses the same boundary a tarball does. Everything the VM says to the network crosses it.
That changes what the seven weeks look like. With Session trace at Activity, Bromure records the host, the status, the latency, the swap report and any leak warnings for every request the VM makes. Set it to Everything and it keeps the bodies too, for every host, encrypted with the same keychain key as your profile secrets and readable in Trace Inspector. The day a feed your build depends on starts answering differently is a diff you can open, on the Mac, in a log the VM has no write access to.
A poisoned feed is also, in the end, text arriving at a machine. When it reaches
the agent as a tool_result, a file, a fetched page or an instruction file like
CLAUDE.md or AGENTS.md, the Prompt Injection panel scores it on-device
with a local model, and nothing leaves the Mac. Choose Ask me what to do and
the request pauses with the flagged span shown; choose Block unilaterally and
the agent gets a hard 451. When you want the machine gone, Erase home resets
/home/ubuntu and Reset to base re-clones the workspace system disk from the
base image.
BdThemes will patch the escaping, the bucket will get tighter keys, and the next version of this story will arrive with a different vendor and a different feed. What lasts is the arrangement underneath it. Your development environment holds a fistful of credentials, each of which can rewrite something thousands of people fetch on a schedule, and that same environment now runs an agent reading text from strangers all day.
Most posts here ask what your agent should be allowed to read. Answer the other question too, the one that is uncomfortable on a laptop and easy in a profile: what is it allowed to write, and to how many people?
Install Bromure Agentic Coding, and give the agent a machine that holds no key to anything you ship.