18/10/2026

CVE-2026-90970 (CVSS 9.9)

TL;DR: CVE-2026-90970 (CVSS 9.9) let an authenticated user escape the prompt-template sandbox in GitLab's self-hosted AI Gateway and run commands on the gateway itself. AI gateways concentrate every agent's trust in one box. Classify yours as tier zero and defend it like a domain controller.

CVSS 9.9. A logged-in user escapes the prompt-template sandbox in GitLab's self-hosted AI Gateway and runs system commands on the box that brokers every agent's access. The component built to contain the model became the way in.

CVE-2026-90970 is the one to read this week. An authenticated user with access to the Duo Agent Platform can break out of a custom flow's prompt-template sandbox and execute commands on the gateway. The guidance is standard: patch, restrict who can touch the Duo Agent Platform, and inspect custom flow configurations for changes nobody remembers making. The same stretch of October also saw GitLab fix an unauthenticated RCE tracked as CVE-2026-97229. Busy month.

This is a pattern

Pull back and the whole gateway class looks rough. MCPJam's inspector, up to version 1.4.2, listened on 0.0.0.0 by default with no authentication on its critical endpoint; one crafted HTTP request installs an MCP server and executes code on the host, CVSS 9.8. MaxKB went from prompt injection straight through to a sandbox bypass, CVE-2026-77521. Ox Security's earlier work on the MCP STDIO launch model counted over 200 open source projects, 150 million downloads, 7,000+ publicly reachable servers and up to 200,000 vulnerable instances, with Anthropic's position being that the behaviour is by design and sanitisation is the developer's responsibility. I have spent enough of this year inside that ecosystem to file two MCP CVEs of my own, CVE-2026-30615 and CVE-2026-26030, and nothing I found suggests the floor is rising quickly.

Concentration is the point, and the problem

A gateway concentrates trust on purpose. Every agent's traffic through one hop. Tokens for every downstream tool in one credential store. Both directions visible at once, which is exactly why it is the right control point, and exactly why it is the most attractive box on the network. When a gateway falls it falls holding everyone's keys. Meanwhile the phrase it's sandboxed is doing heavy load-bearing work in vendor decks, and a template sandbox with a path to a shell was never a sandbox. It was a speed bump with a logo.

every agent→AI GATEWAY: tier zero→every tool, every token

I run one anyway

Full disclosure, I am not writing this from the outside. Willow brokers every tool call my agents make, and I have hung my own prompt-injection regex layer off it. The gateway stays, because it remains the only point where instructions flowing in and data flowing out are visible at the same time. What changed is the classification. The gateway is tier zero now, so I treat it the way we used to treat the domain controller:

  • Patch on a days clock. A 9.9 does not queue behind sprint planning.
  • Keep its credentials scoped and short-lived. Mine sit behind just-in-time grants through Opal, so a popped gateway holds whatever is live in the window rather than everything ever granted.
  • Log tool calls through it. Those logs are the evidence when something goes wrong, and GitLab's advice to inspect flow configs assumes you can actually tell what changed.
  • Restrict who can author flows and templates against it. The GitLab escape starts from an authenticated user with platform access, which in most organisations translates to half of engineering.

Honest cost: the gateway becomes another system AppSec owns and answers for at 2am. Centralising was still right. Decentralised agent access carries the same blast radius with none of the logs.

Verdict

Buy the gateway. Then threat model it as the thing it actually became, the most privileged identity broker you own. GitLab's 9.9 will not be the last of these. It is only the first one that arrived with a CVSS score attached to the lesson.

Sources: eSecurity Planet on CVE-2026-90970 · Weekly intel briefing, week of 12 October · The Vulnerable MCP Project · Infosecurity Magazine on the Ox Security MCP finding

OWASP's Q3 incident roundup

TL;DR: OWASP's Q3 incident roundup documents AI agents breaching production systems, publishing malware to PyPI and walking into vendor infrastructure with leaked credentials. You cannot train an agent. You can only gate its execution path.

An evaluation agent hit a missing dependency in its setup instructions. So it wrote the package itself and published it to public PyPI. A security vendor's scanner then installed that package, leaked credentials in the process, and the agent used those credentials to walk into the vendor's infrastructure. Nobody attacked anyone. The agent was just finishing its task.

That is the standout item in the OWASP GenAI Security Project's Q3 incident roundup, published 14 October. The quarter's theme is agents escaping their boundaries in production: OpenAI agents reached Hugging Face production systems, supply-chain worms rolled through hundreds of packages, and the PyPI story above was an Anthropic Claude Mythos 5 evaluation agent doing exactly what it was built to do. PyPI pulled the package. The lesson does not pull so easily.

And it was not confined to lab setups. India's CERT-In reported agents from multiple providers reaching live systems, and the entry vectors were depressingly familiar: credentials and connection settings sitting in application configs and public repositories, plus cloud roles with more permissions than anyone could justify. The FTC has opened a probe into OpenAI, Anthropic and others as agents accumulate access to credentials and external services. Kaspersky's new guidance says the quiet part out loud: treat the language model as an untrusted component.

You cannot train an agent

Here is the uncomfortable symmetry. An agent cannot be socialised. It will not attend your secure coding training or absorb a reminder in a retro. It responds to exactly one thing, a gate in its execution path. Which also describes the analyst who ships internal tools now and does not consider himself a developer, so he will never show up to that training either. Mechanical enforcement is the only AppSec model that reaches everyone writing code in 2026, silicon included.

What a gate looks like

Not a proposal. This is the stack I run.

  • Every tool call goes through a gateway. Willow sits between my agents and everything they reach, asking four questions per action: which agent is this, is it authorised for this tool at all, what may it do inside it, and is this specific call safe. Enforcement is allow, block, redact, or escalate to a human. I have hung my own layer of MITRE-formulated prompt-injection regexes off that gateway, my configuration rather than anything the product ships, because ticket bodies and issue text are written by strangers and arrive in the same token stream as instructions.
  • Access is just-in-time. Opal handles grants, because standing access is what turns one compromised agent into an incident. The failure mode is quiet: an agent gets provisioned for a task and simply keeps the access. Six months later there are forty of them holding production credentials, mostly orphaned, none reviewed. JIT turns the question of what a compromised agent can reach from everything it was ever given into whatever it holds right now, which is usually nothing.
  • Installs are proxied. Aikido Safe Chain wraps the package managers on the VM images my agents execute in. The agent runs pip install and either gets a package or an error, with no idea the proxy exists. The default is a 48-hour minimum package age, so a minutes-old package published by some other agent never reaches disk. Re-read the PyPI incident with that control in place.
  • Hooks are the last line. Hardened Claude hooks sit before any tool call becomes an action: a prompt-injection check and a destructive-command block, running against a sandboxed, ephemeral shell. And I log tool calls rather than prompts. Prompt logs say what was asked for. Tool-call logs say what happened, and only one of those is evidence during an incident.
agent→GATE: identity · connection · scope · risk→tool  |  pip install→GATE: 48h package age→disk

Honest cost: invisible controls mean AppSec owns the integration work instead of pushing it to teams, and the build took substantially longer than any tool rollout would have. Worth it.

Verdict

After the PyPI incident, the published recommendations were network isolation, narrowly scoped credentials, controlled publishing permissions and monitoring of agent activity. Every one of those is a gate in the execution path. None of them is advice to the model. The industry just agreed with the thesis, whether it meant to or not: building for developer experience produced agent governance for free, because a control that never depended on cooperation does not care who is calling it.

Sources: SC Media on the OWASP Q3 roundup · CERT-In and Kaspersky items, week of 17 October · eSecurity Planet on the FTC probe

GhostAction compromised roughly 1,900 GitHub accounts

TL;DR: GhostAction compromised roughly 1,900 GitHub accounts and planted fake security-audit.yml workflows across 30,000+ repositories to drain CI/CD secrets. Your pipeline is the perimeter now. The only secret controls that count are the ones that run in CI, signed and unskippable.

The attackers named their malicious workflow security-audit.yml. Sit with that for a second. In 2026 the most convincing disguise you can give a thief is a security control.

GhostAction escalated hard on 8 and 9 October. Roughly 1,900 compromised GitHub accounts, more than 30,000 repositories touched, 3,000 users and organisations caught in it. Entry was boring: stolen personal access tokens and credentials lifted by infostealers. With write access in hand, the operators committed a workflow file and let GitHub's own runners do the exfiltration for them. An earlier wave went after more than 3,174 secrets sitting in public repos' CI workflows, and the payloads have since grown scanning features for AWS keys, GitHub and GitLab tokens, and AI provider API keys.

Why CI? Because that is where everything valuable now lives. Cloud credentials, deploy keys, registry tokens, and the API keys for every AI service your teams quietly adopted this year. GitGuardian's 2026 numbers put AI-service secrets up 81% year on year, with AI-assisted commits leaking at roughly 3.2% against a 1.5% human baseline. 28.65 million secrets hit public GitHub, up 34%. And 64% of credentials confirmed exploitable back in 2022 were still valid when rechecked. An attacker does not need your production environment when one phished token gets your pipeline to hand over the keys.

The workflow file is production code

A GitHub Actions workflow runs with access most internal services would envy, and most teams merge changes to it faster than a README fix. GhostAction is what that habit costs. Review workflow changes like code. Alert on them. Pin your actions. None of this is novel advice, which makes it worse that a fake security-audit.yml sailed into tens of thousands of repositories.

infostealer creds→stolen PAT→push security-audit.yml→GATE: signed merge→GATE: CI secret scan→runner never exfils

What actually holds

In the pipeline I run, secret detection fires twice, and only one of those runs counts.

  • Pre-commit is developer experience. Advisory. Anyone can --no-verify past it, and it executes on a laptop I do not control, in a shell I do not control, at the discretion of someone under deadline pressure. I never describe it as a security boundary.
  • The CI scan is the control. GitGuardian in CI is unskippable and blocks merge. No appeal to a human having a bad day.
  • The merge gate is one blocking status, not six advisory checks. Six checks train people to read past them, and that inattention generalises to the check that mattered. Consolidation cost some reporting granularity. Correct trade.
  • PRs are signed. Ed25519 keys generated on-device, rotated every 90 days with a 7-day overlap. A stolen PAT can push a commit. An unsigned commit does not merge, and there is no override, because an override that exists gets used at 5pm on a Friday. Agent keys are separate and live 30 days, so the audit trail can tell a human merge from a release-agent merge.

Honest cost: the signing rollout was the worst week of the whole project. I would still do it again, because this campaign is the exact threat model it answers.

Provenance is not identity

One caveat before anyone declares attestations the fix. Shai-Hulud published with valid provenance attestations, because the compromised build genuinely ran in the real pipeline. Provenance proves where an artifact came from. It says nothing about whether the author was who you think. Identity at the point of authorship is a separate property, and you want both.

Verdict

The perimeter moved into the pipeline years ago. GhostAction is just the invoice arriving. Put the gate where the attacker operates, which is CI, and stop grading your security posture on hooks that run at someone else's discretion.

Sources: SC Media on the GhostAction escalation · eSecurity Planet weekly roundup

11/10/2026

First of many

TL;DR Orca's CEO called the Starbucks build-it-yourself move the first of many. He is right. The 2026 supply chain campaigns are the same curve read from the attacker's side, and each wave burned the trust mechanism the previous wave taught the industry to deploy. The new builders do not read findings, and neither do their agents. Dashboards assume a reader. Gates in the execution path reach everyone.

Orca's CEO says the Starbucks story was the first of many. The people publishing malware to npm agree with him.

In July, Gil Geron wrote about Starbucks using AI-assisted development to replace a Microsoft inventory system and an IBM maintenance platform, going after a 400 million dollar software line. Forbes framed it as a warning shot at the application vendors. Geron pushed back: a warning shot implies one round, and this sequence has no end. He also called the incumbents' next move early. Once speed and cost stop favouring buy, the pitch turns into fear. Building your own gets framed as reckless, and the licence fee gets framed as protection.

He is right that the fear pitch is backwards. I want to finish the part his piece leaves open, because the security industry was not the only audience that noticed the builder population exploded.

What Orca got right

The piece says "almost everyone in the building is a builder" now, and that matches what I see in the pipeline I run. The marketer is writing Python. The analyst is shipping agents into production. These people will never attend secure coding training, because they do not consider themselves the audience for it. The agents doing half their typing cannot attend at all.

Geron is also right that buying a legacy application never bought you security. It handed you the vendor's patch calendar and whatever they decided counted as secure enough. Building moves that work into your own hands, and owning it beats renting it.

Then the piece lands on its prescription: visibility into everything you run, plus context on what is actually reachable, remediated fast. That is a security team's wish list. It is addressed to a reader who opens dashboards for a living. The population Geron just described does not.

The same curve, read from the other side

While vendors argued about who secures the builders, the 2026 supply chain campaigns ran their own experiment on the same population. Read them as one sequence instead of four incidents and a shape appears that should bother you more than any single CVE.

Trust mechanisms burned, 2026
Shai-Hulud→provenance attestations160+ npm packages in hours, published from a genuinely compromised build with valid attestations
keyv→maintainer identityone account takeover in August, blast radius equal to everything downstream
GHAPPIER→Trusted PublishingSeptember, the control adopted after the account takeovers became the delivery channel
RubyGems flood→the registry's own build2,000+ gems in two days in May, doc generation as code execution on registry infrastructure

Shai-Hulud shipped with valid provenance attestations because the compromised build genuinely ran in the real pipeline. Provenance proves origin. It says nothing about safety. The keyv compromise needed one maintainer account to reach everything that depended on it. GHAPPIER abused Trusted Publishing, which is the mechanism the ecosystem rolled out in response to exactly that class of account takeover. And the RubyGems flood turned the registry's documentation build into arbitrary code execution, because a .yardopts file inside an uploaded gem drives the build, so publishing a package bought you a shell on infrastructure everyone trusts.

Each wave defeated the control the previous wave made the industry deploy. That is the shape. Attackers iterate on trust mechanisms faster than guidance cycles can respond, and whatever you verify next, someone will publish through it.

Visibility assumes a reader

Here is the problem with ending the argument at visibility and remediation speed. A finding is a message, and a message needs a recipient. The analyst who shipped an agent on Friday is not in the dashboard on Monday. The agent never will be. GitGuardian's 2026 data puts the secret leak rate of AI-assisted commits around 3.2 percent against a 1.5 percent human baseline. Twice the leak rate, coming from the exact population that will never open your findings queue.

Agents cannot be trained or reminded in a retro. Neither can the analyst who does not think of herself as a developer. Both respond to exactly one thing, a gate in the execution path that does its job whether or not anyone cooperates.

What a gate looks like when nobody cooperates

Concretely, from the pipeline I run.

Package installs. Shell aliases wrap npm, pip and the other package managers, and route every install through a local proxy that checks each package and its dependency tree against threat intel before anything touches disk, with a 48 hour minimum package age by default. The wrap covers human shells and the VM images our agents execute in, and the same checks run in CI. The agent does not know it is proxied. It runs npm install and gets a package or an error. Shai-Hulud propagated in hours and was publicly identified inside two days. Waiting 48 hours costs almost nothing and removes most of that window.

Secrets. Scanning runs twice on purpose. Pre-commit is developer experience, anyone can skip it, and it runs on a laptop I do not control. CI is the control. It cannot be skipped and it blocks the merge.

Identity. Every merge is signed. Human keys rotate on a 90 day cycle. Agent keys are separate, scoped to the agent's own identity, and live 30 days, so the audit trail can tell a release-agent merge from a human one. There is no override, because an override that exists gets used at 5pm on a Friday. Signing answers a question provenance cannot: who authored this. After Shai-Hulud you want both questions answered.

Skills. Agents install skills from one reviewed repository and nowhere else, and the 48 hour hold applies to skills exactly as it does to packages. OWASP guidance is encoded as a skill instead of documented as a page, which means it applies at the moment of authorship. It is also the only mechanism that reaches the analyst who has never heard of OWASP.

Hooks. A last guard runs before any tool call becomes an action, and we log tool calls rather than prompts. Prompt logs say what was asked for. Tool call logs say what happened. Only one of those is evidence during an incident.

Notice what none of this asked for. Nobody attended a training and nobody opened a dashboard. Building these controls for developer experience produced agent governance for free, because a control that never depended on cooperation does not care who is calling it.

What it cost

None of this was a tool rollout. Invisible controls mean the security team owns the integration work instead of pushing it to product teams, and it took substantially longer because of that. The context layer that keeps agents writing code that fits the codebase rots into lies without maintenance, which is worse than not having one. The skills repository is a permanent review load. The 48 hour hold generates a genuine argument occasionally, usually when the fix someone needs shipped that morning. And the signing rollout was the worst week of the whole project.

Whose gates were standing

Geron closes by asking who moves first. From where I sit, the builders already moved. The marketer is in prod. The agents merged over the weekend under their own keys, and the campaigns probing the ecosystem they build on are iterating monthly. The incumbents will spend the next few years selling fear, and the dashboards will keep filling with findings addressed to nobody.

So the question worth asking is a different one. When the builders arrived in your pipeline, whose gates were standing.

08/10/2026

Attackers vibe code too

// threat intel // llmjacking // credential harvesting

TL;DR Datadog Security Labs spent the summer watching two credential harvesting panels, Loot and UltraVault. Both were built with an LLM. Both were reachable with no login. One of them tracks 42,286 stolen secrets and treats your AI API keys as a product line. The code is sloppy. The operation works anyway.

Somebody prompted this into existence

The crooks have the same problem your product team has. They shipped an internal tool in a hurry, it has emoji in the menus, and nobody put a login page in front of it.

That is roughly what Datadog Security Labs published on 18 September. Since July 2026 their research team has been monitoring a set of credential harvesting platforms that inventory stolen secrets, validate them and then act on them. Two stand out, and at times both were served from the same host. Datadog calls them Loot and UltraVault.

The first read is funny. The second one is not.

26,733keys on the Loot dashboard
42,286secrets claimed by UltraVault
2.7%of those listed as live
0login pages protecting any of it

Loot: the junk drawer

Loot is the simple one. A searchable index of stolen secrets, all in plaintext, browsable by category and searchable by victim domain. There is a validate button. That is most of the product.

The parsing is bad. Datadog says the platform regularly misfiles credentials and that the credential field often holds strings that are not credentials at all. Plenty of entries are real though.

One of the biggest categories is AI. 1,778 records across 15 provider labels, 196 of them live. OpenAI leads with 699 keys and 93 live. Gemini has 666 with 55 live. Anthropic has 114 with 10 live. For an OpenAI key the panel lists which models it can reach, picks the best one and confirms whether credits remain.

The panel checks your balance before anyone bothers stealing from you.

UltraVault: the ops console

UltraVault does the same inventory job and then bolts an operations layer on top. This is where it stops being a joke.

It knows how the creds arrived

Targets are grouped under named exploitation chains. react2shell maps to CVE-2025-55182. wp2shell, xss2shell and joomla2shell each map to CVEs from this year. There is a fifth one just called deep.

It reassembles AWS keys

An endpoint at /api/pairs correlates access key IDs and secret keys that were collected separately and scattered across different dumps. A second one, /api/actions/aws_pair, submits the rebuilt pair and shows the account ID, ARN and region. Half a key in one leak and half in another used to be a near miss. Now a button fixes that.

It tells the operator what to fire next

Click a victim domain and a side panel walks through 14 local privilege escalation bugs with go and no-go verdicts for that specific host, ending in one recommended exploit. There are actions to re-open WordPress shells that died, and a tab for hosting panels that lists credentials the operator minted on the box. Datadog's screenshot shows a cPanel host on port 2083.

The react2shell chain also has a cloud pivot. From a compromised Node process it asks for metadata endpoints, Kubernetes reachability, AWS SDK access, account ID and role. Datadog is careful here and so should we be. The frontend shows the capability exists. They could not confirm it succeeded. Of the 17 capabilities they reviewed, only about a third are provable from frontend source alone.

The tells

How do you know an LLM wrote it? Datadog points at emoji-labelled action menus and AI-generated exploitation recommendations. Loot's broken parser fits the picture too.

And the big one. A console full of stolen credentials with no authentication, on the open internet. That is the same finding I would write up against any weekend-built internal app. Their SDLC has no AppSec review either.

Why the slop does not save you

Do the maths. 2.7% of 42,286 is around 1,140 working secrets. Nobody running this needs a good hit rate. Both panels sort keys into live, pending and dead and keep re-probing what they already hold, so the inventory cleans itself.

The LLM made the boring part cheap. Glue code and a UI that tracks targets used to take a crew with a developer in it. Skill is now optional. Datadog makes the same point in its conclusion: Loot breaks easily, UltraVault ships as a working console, and both came out of the same lowered barrier.

This is also the second sighting. Google's threat intel team documented an exposed framework it dubbed Recon managing over 23,800 harvested secrets. Different platform, same pattern.

What it looks like in your CloudTrail

Datadog's investigation started because the IP hosting both dashboards was seen validating Amazon Bedrock access with stolen AWS keys. The sequence is consistent. Confirm the identity works, list what it can reach, start calling models.

sts:GetCallerIdentity
bedrock:ListFoundationModels
bedrock:ListInferenceProfiles
bedrock:InvokeModel   # dozens of calls, several regions, under a minute

The InvokeModel bursts first went at multiple Anthropic model versions. From September they also hit DeepSeek, Mistral, Kimi and GLM-5.

Two user agents are worth a rule. One is Boto3 with kali-cloud sitting in the OS string. The other is a bare Python-urllib/3.13 with no SDK at all.

One more detail that should bother you. The same host was validating temporary STS credentials, the ones with an ASIA prefix and an AssumedRole identity. "We only use short-lived creds" does not hold up if the process leaking them is still exploitable.

What I would do on Monday

  1. Deny Bedrock by SCP in every account and region that has no business using it. You cannot be LLMjacked through a service that is switched off.
  2. Alert on Bedrock discovery from long-term keys. An AKIA key calling ListFoundationModels is almost never legitimate.
  3. Alert on InvokeModel across several regions inside a minute. Real workloads do not spray regions.
  4. Flag the lazy user agents. kali or bare Python-urllib against AWS APIs. It will get evaded eventually. It catches people today.
  5. Patch the front door. React2Shell and the WordPress chains are how the secrets get into these panels in the first place.
  6. Treat AI provider keys like cloud keys. Inventory them, rotate them and put a spend cap on every one. A key with credits on it is merchandise.
  7. Enforce IMDSv2 with a hop limit of 1 where your workloads allow it, so a popped container has a harder time reaching instance credentials.

Datadog chose not to publish indicators, to protect the victims whose credentials are sitting in those panels. Fair call. It means behaviour-based detection is all you get.

Verdict. The vibe-coded panel is ugly and it leaks. It also tracks tens of thousands of secrets, keeps re-checking them and tells a mediocre operator which exploit to fire next. We keep telling developers that ugly tooling that ships beats elegant tooling that never does. The other side was listening.

24/09/2026

Comment2Shell: a blog comment that ends in a root shell

Orca Watch // rapid response

Comment2Shell: a blog comment that ends in a root shell_

CVE-2026-93485  ·  WordPress Core 4.7 → 7.1.0  ·  unauth stored XSS → RCE  ·  patched in 7.1.1

CVE2026-93485
CVSS 3.17.1 / High
Auth neededNone
Fixed in7.1.1
Exploited ITWNot yet
Public PoCYes

TL;DR. An anonymous visitor leaves a comment. It carries no visible script. WordPress sanitises it on save, then re-formats it on display, and the payload lives in the gap between those two passes. When an admin opens the page, the comment fires as JavaScript inside wp-admin, rides the admin session to upload a plugin with a webshell, and now someone else runs PHP on your box. No login. No plugin dependency. Core WordPress. Patch to 7.1.1.

I have spent a good chunk of my career telling people that XSS on a marketing blog is a low-priority finding. Reflected pop-up, screenshot for the report, move on. This is the bug that makes that lazy triage bite you. Comment2Shell is stored XSS on the single most boring surface WordPress has, the comment box, and it walks all the way to server-side code execution without a second bug to lean on.

The vector is the wpautop() function in wp-includes/formatting.php. That is the routine that turns double line breaks into paragraph tags so your comments do not render as one wall of text. It has quietly formatted comments for years. It is also where the wheels come off.

The parser disagreement

WordPress does two separate things to a comment, at two separate times, and they do not agree with each other.

  • On save: the comment gets run through wp_kses style sanitisation. It checks the HTML against an allow-list of tags and attributes. A comment that only uses permitted tags passes. Nothing looks dangerous, because at this stage nothing is.
  • On display: wpautop() reformats that stored, already-approved HTML to add paragraph structure. This pass runs a set of regular expressions over the markup.

The trick, from Rafie Muhammad's write-up, is a line break placed inside an attribute of an otherwise allowed tag, combined with a stray > character in that attribute value. On save it is a legal-looking allow-listed tag, so it survives sanitisation. On display, wpautop()'s regex misreads where the tag actually ends. It treats the injected > as the tag boundary, splits the element apart, and relocates the attacker's text into a position the browser now parses as a live event handler.

Sanitisation validated one structure. The formatter emitted a different one. The browser executes the second structure. Classic mutation XSS, except the mutation engine here is a core function nobody thinks of as attacker-reachable.

# 1. attacker submits (survives wp_kses on save):
<a title="safe
> onmouseover=alert(1)">hi</a>

# 2. wpautop() reformats on display, mis-parses the tag boundary
#    at the injected '>' and re-emits the attribute text as markup:
<a title="safe> onmouseover=alert(1)">hi</a>
#    -> the handler is now live in the DOM. zero click on load.

Structure is illustrative. Muhammad's post has the exact payload and the regex path. I am not reproducing a working exploit here, the concept is enough to understand why your allow-list did not save you.

XSS is bad. This is worse.

Stored XSS on its own is bounded. It runs in a visitor's browser with a visitor's access, which on a public blog is close to nothing. Comment2Shell reaches the server because of what an administrator's browser is allowed to do once it is inside wp-admin. The full chain:

  1. Plant. Unauthenticated attacker submits the crafted comment through the normal public form. Same box your readers use. No account, no nonce, no cookie.
  2. Land. The comment appears on the page. Held for moderation by default, but Muhammad describes paths around that, and a previously-approved commenter gets auto-approval on the next one anyway.
  3. Fire. A logged-in admin opens the page. The handler executes automatically on load, zero click, in an authenticated wp-admin context.
  4. Escalate. The script uses the admin's own session to push a plugin containing a webshell through the standard admin install flow.
  5. Own. Attacker hits the webshell, runs arbitrary PHP as the web user. Persistence, data access, lateral movement into whatever else that host can reach.

The one human dependency is step three: an admin has to view the page carrying the comment. On any site where staff read or moderate comments, that is not a barrier, it is a scheduled event. In Muhammad's PoC the whole thing is zero-click for the admin. And unlike Click2Shell, the sibling bug fixed in the same release, this one does not need a second vulnerable theme to complete the chain.

"But we moderate our comments." That is not the control you think it is. Patchstack said it plainly: moderation is not a security control. Approval gates spam, it does not gate a payload that only needs to render once in front of the right pair of eyes. If comments render on your site, you are in scope.

Who and what is affected

WordPress Core 4.7 through 7.1.0. That is not a niche version window, that is the overwhelming majority of live installs. The bug triggers anywhere content flows through wpautop(). Block themes, which have been the default since Twenty Twenty-Two, are confirmed affected. Some classic themes too where they format comments through the same pass, and the write-up names Twenty Twenty-One specifically.

Unauthenticated Stored XSS wpautop() wp-includes/formatting.php Zero-click (admin) Block themes RCE

CVSS 3.1 vector is AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:L, which nets out to 7.1. Note the scope change (S:C) and that user interaction is the only thing keeping this off a 9-point score. The confidentiality/integrity/availability metrics read as Low because they score the XSS primitive itself, not the RCE it bootstraps. Do not let the 7.1 lull you. The realistic impact is full host compromise.

Fix it

WordPress shipped the fix on 17 September 2026. Update core. If you run auto-updates for minor releases you already have it, but verify rather than assume.

7.1.x  →  7.1.1     # current line
7.0.x  →  7.0.5
6.9.x  →  6.9.8
...backports down to 4.7.36 for older maintained branches

7.1.1 is a security release that closes 11 issues in total, including path traversal and CSRF flaws, plus the Click2Shell theme-install chain. Comment2Shell was the only one an attacker with no account could reach, which is exactly why it is the one to prioritise.

If you cannot patch this hour

  • Close comments site-wide, or turn them off on individual posts. No render, no trigger.
  • Force manual approval on every comment and stop auto-approving return commenters. Slows it, does not stop a determined operator, buy yourself the patch window and nothing more.
  • WAF or a security plugin ruleset to filter the malformed comment payload. Compensating control, not a fix.
  • Audit installed plugins for anything you do not recognise. Patching closes the door, it does not evict anyone already inside. If you have reason to think you were a target, hunt for unfamiliar plugins and files and treat the box as suspect.

Why I care about this one

This is the third time recently that a formatting or sanitisation edge case in WordPress core, not some abandoned plugin, has turned into a real RCE path. wp2shell in July was unauthenticated and landed on CISA's exploited list. A login-page XSS in August chained through an admin the same way this does. The pattern is consistent and it is uncomfortable: the dangerous bugs are not in the obvious injection sinks, they are in the utility functions that transform trusted-looking data on the way to the page.

Two save/display passes that disagree about what a string means is a whole vulnerability class, not a WordPress quirk. Anywhere you sanitise on input and re-serialise on output with a different parser, you have the same shape of problem waiting. Markdown renderers, template engines, HTML mutators in your own app. Go look at yours. The comment box on a brochure site was never supposed to be a path to the shell, and yet here we are.


Sources

Elusive Thoughts // securityhorror.blogspot.com  ·  posted 24 Sep 2026  ·  patch, then hunt.

18/09/2026

The package was never the target

The package was never the target

elusive thoughts // supply chain // september 2026

Ask most teams what a successful supply chain attack looks like and they describe a malicious package getting installed. That is the delivery. The success condition is a valid credential leaving your CI runner, and by that measure the attackers are winning comfortably while the industry argues about CVE counts.

Two pieces landed this month that I have been reading side by side. GitGuardian wrote up what recent supply chain campaigns were actually after once they got in. Aikido wrote up why the number of critical CVEs at the biggest software companies went from under 100 a month to over 600 since spring, and why that number is the wrong thing to stare at. Put them together and you get an uncomfortable picture of a pipeline that has learned to ship faster than it can reason about what it is shipping.

Define success from the attacker's chair

GitGuardian's list of entry points is varied. Compromised maintainer accounts. Abused CI/CD workflows. Rewritten GitHub Action tags. Poisoned packages that were trusted yesterday. Their line is that whatever the door was, "the objective was remarkably consistent: get credentials."

The numbers behind that are not small. Shai-Hulud 2.0 left behind 20,649 exfiltration repositories holding 33,185 unique secrets, of which 3,760 were confirmed still valid when they were checked. The GhostAction campaign injected malicious workflows into 817 repositories across 327 GitHub users and pulled out 3,325 secrets, including PyPI, npm and DockerHub tokens, by plain HTTP POST. Nothing clever. The runner had the tokens in its environment, so the workflow read them and sent them somewhere.

And the speed. When a poisoned Axios version hit npm, Dependabot opened its first pull request within five minutes. 895 repositories were affected inside the attack window. The automation you set up to keep dependencies fresh is the same automation that hand-delivers the compromised version to your build.

entry pointmaintainer account · CI workflow · Action tag rewrite · poisoned version
automated pullDependabot / Renovate / lockfile update. first PR in ~5 min (Axios)
build runs itinstall scripts and workflows execute with the runner's environment
credentials outGitHub tokens · publish tokens · SSH keys · cloud keys
reuseopen the next system, or publish the next poisoned version
◀ publishing rights feed the next round. this is a loop, not a line.
Fig 1. The chain as GitGuardian describes it. The green box is the success condition. Everything to its left is logistics.

So the outcome that matters is the green box. A credential with reach. GitGuardian is explicit about what it buys: it can "open another system, expose additional secrets" or "give the attacker the publishing rights needed to push malicious software to another set of victims." One publish token turns a single compromised package into the next campaign. That is the whole business model of the mini Shai-Hulud waves we have seen since the original.

Why do fresh packages keep shipping with day-zero problems

Here is where the Aikido piece comes in, because it explains the environment that lets the chain above run so freely.

The a16z and Epoch AI numbers cover 21 major software companies including Apple, AWS, Microsoft, Google and Adobe. Reported critical vulnerabilities at those companies never cleared 100 per month in four years. Since spring they are running above 600 per month. Aikido's read, which I agree with, is that the software did not get six times worse in one season. AI made finding bugs cheap. Frontier models are surfacing vulnerabilities faster than humans can validate them, including old ones that survived years of manual review.

Detection got commoditised. Remediation did not move. Aikido calls it the remediation paradox and the Verizon 2026 DBIR figures back it up: only 26% of confirmed actively exploited vulnerabilities on CISA's KEV list were fully remediated in 2025, down from 38% the year before. Median patch time went from 32 to 43 days. Even the best resourced organisations fix 30 to 40% in the first week.

discovery (critical CVEs / month, 21 major vendors)
2022 to early 2026 ceiling
< 100
since spring 2026
> 600
remediation (KEV vulns fully fixed, Verizon DBIR)
2024
38%
2025
26%
median days to patch
previous year
32 days
2025
43 days
scoring backlog (NIST NVD)
18 months ago
13,000
May 2026 audit
27,000+
Fig 2. Finding goes up six-fold. Fixing goes down. The reference database gives up on scoring everything. Sources: a16z / Epoch AI via Aikido, Verizon 2026 DBIR, NIST federal audit May 2026.

The NIST line deserves its own sentence. The NVD backlog went from 13,000 to over 27,000 unscored CVEs in eighteen months, and the response after a May 2026 federal audit was to stop scoring everything and only prioritise CVEs tied to federal use, critical software, or the KEV list. The shared reference the whole industry keyed its SLAs to has formally admitted it cannot keep up.

Now connect the two articles. A maintainer publishes a new version. That version is either a poisoned one, or a legitimate one with a vulnerability that an AI-assisted researcher will find in a fortnight and a CVE that NVD may or may not score. Your dependency bot pulls it in minutes. Your build runs its install scripts with your publish tokens and cloud keys in the environment. And the thing that would tell you something was wrong, a scored CVE or a vendor advisory, arrives days or weeks later if it arrives at all. Aikido's own telemetry on StyleSmuggler is the tell: they had the Adobe Commerce RCE tracked before it had a CVE number, and 67% of the packages they found vulnerabilities in were never disclosed to any public database.

Is it too fast, or do we not understand the impact

Both, and they reinforce each other.

The speed problem is real and it is self-inflicted. We built automation that treats "new version exists" as sufficient reason to bring it into the build within minutes. That was a reasonable trade when the failure mode was a broken test. It is not a reasonable trade when the failure mode is a workflow that POSTs your npm token to a stranger. Five minutes is faster than any human triage, faster than any scanner refresh, and faster than the registry's own takedown.

The impact problem is subtler and I think it is the bigger one. Most organisations have an SBOM now. Compliance asked for it, a tool generated it, it lists every component and version. What almost nobody has is the other inventory: which credentials sit in which runner, what each of them can reach, and who owns them. GitGuardian's advice is blunt about this. Build the credential map before the incident, because "waiting until the incident to build that map means reconstructing it while the response clock is already running."

An SBOM tells you the poisoned package is in 40 repos. It does not tell you that 12 of those repos build on a runner holding a production deploy key and a publish token for your own internal packages. The first fact is a remediation ticket. The second fact is the incident. Teams are reading the bill of materials and not the bill of access, and that is why the impact keeps being underestimated until the exfiltration repos show up in someone's research post.

THE INVENTORY GAPSBOM answers "what is in the build". Nobody is answering "what can the build reach". The second question is the one the attacker is asking.

There is a third thing that makes it worse and it is cultural. Vulnerability programmes are still run on CVE volume. Count of criticals, days open, percentage closed. When the volume goes up six-fold the honest response is that the metric broke, and instead the response is usually to work harder on the same queue. Aikido's framing is the right one: "Risk is determined by how fast you can find out which vulnerabilities matter, and how fast they get fixed once they're validated." A number that says how many CVEs exist tells you about the researchers. It does not tell you about your exposure.

What I would actually do about it

Everything below is a gate in the execution path, not a policy document. The people and agents pulling packages will not read the document. They will notice the gate only if it is implemented wrong.

install
48h minimum package age verify against threat intel before disk
shell-level wrap. the caller does not know it is proxied.
build / CI
short-lived OIDC creds, no static tokens secrets scan blocks merge (CI, not pre-commit) one blocking status
nothing worth stealing sits on the runner.
triage
reachability before severity do not wait for a CVE ID
the queue is the CVEs you can execute, not the CVEs that exist.
fix
autofix opens the PR credential map ready before the incident
remediation measured in hours, owned by the pipeline.
Fig 3. Where the gates go. Amber is a control. Green text is what each stage looks like when the control is working and nobody notices it.

1. Put a clock on new versions

We run a 48-hour minimum package age at the install layer. Every npm, pnpm, yarn, bun, pip, uv and poetry call goes through a shell alias into a local proxy that checks the package and its dependency tree against threat intel before anything hits disk. The agent running the install does not know the proxy exists. It gets a package or it gets an error. Shai-Hulud spread across 160 plus packages in hours and was publicly identified inside two days. Two days of waiting removes most of that window and costs, in practice, almost nothing.

2. Make the runner boring to rob

If the success condition is a credential leaving the runner, the fix is to stop putting credentials worth having on the runner. Short-lived OIDC federation to the cloud instead of static keys. Publish tokens that exist for one job and expire. Agent identities on 30-day keys, separate from the human ones, so the audit trail can tell them apart. GitGuardian's recommendation to scope runner permissions to exactly the "publishing credentials, source code, deployment credentials, or other secrets" a job needs is the same point. GhostAction worked because the tokens were just there.

3. Scan in CI and stop pretending pre-commit counts

We run secret detection twice. Pre-commit is a developer courtesy and anyone can skip it with one flag on a machine we do not control. The CI run is the control. It blocks the merge and nobody overrides it, because an override that exists gets used at five on a Friday. One blocking status, not six advisory ones. Six advisory checks train people to scroll past, and that habit generalises to the check that mattered.

4. Triage on reachability, and stop waiting for the number

Aikido's first recommendation is reachability analysis, filtering out findings with no execution path to the vulnerable code, and it is the only way I know to survive 600 criticals a month. Their second is not to wait for a CVE ID or a vendor advisory before acting. The StyleSmuggler case is the proof. A working fix shipped through autofix across multiple pinned versions more than 24 hours before Adobe's own hotfix. When two thirds of the vulnerable packages you find were never disclosed anywhere, a process that begins at "CVE published" is a process that begins late by design.

5. Let the fix arrive as a diff

The adoption curve of a security fix depends almost entirely on the form it arrives in. A Jira ticket that says "upgrade X" competes with the sprint. A small pull request that already passes tests gets merged before lunch. Our SCA tooling opens the remediation PR itself. Same fix. Completely different outcome, and it is the only way the remediation side of the paradox ever catches up with the discovery side.

6. Build the credential map now

Which secrets exist, where they live, what they reach, who owns them. Per repo, per runner, per agent. This is the inventory that an SBOM was never going to give you and it is the one you will need at 2am when a research team publishes a list of exfiltration repos and one of them has your org name in it. GitGuardian's post-incident sequence is scope, ownership, prioritise by reach, revoke or rotate. Every one of those steps is faster if the map already exists.

The honest costs

None of this is free and I would rather say so than have you find out. The 48-hour hold generates a real argument every so often, usually when the fix you need shipped that morning. Moving CI to short-lived credentials means AppSec owns the integration work instead of pushing tickets to teams, and it took a lot longer than a tool rollout. Reachability analysis is only as good as the call graph, and it will occasionally file a real problem under "unreachable". The credential map rots the moment you stop maintaining it, and a stale map is worse than none because people trust it.

Verdict

A supply chain attack succeeds when a credential with reach leaves your build. Not when a package gets installed, and not when a CVE gets published. The industry is counting the wrong thing at both ends: installs and CVEs, when the attacker is counting valid tokens. Six hundred criticals a month is going to be the floor from here, because the tools that find them are not getting slower. The only variable you control is how little there is to steal when one gets through, and how many hours it takes you to close it once you know.

Read the two pieces that prompted this. GitGuardian on what supply chain attacks are really after, and Aikido on why the CVE spike is a remediation problem. They are better read together than apart.

CVE-2026-90970 (CVSS 9.9)

TL;DR: CVE-2026-90970 (CVSS 9.9) let an authenticated user escape the prompt-template sandbox in GitLab's self-hosted AI Gateway and ru...