22/10/2026

PHP2Shell The CVE!!

// elusive thoughts // appsec // php

PHP2Shell

Two PHP core bugs, CVSS 9.2, unauthenticated RCE before your code runs a single line. This is a patch-tonight problem.

TL;DR. PHP2Shell is CVE-2026-109618 and CVE-2026-109619, two critical use-after-free bugs in PHP core, both scored 9.2. An unauthenticated attacker gets remote code execution or memory disclosure, and on a default Apache mod_php box it works against any PHP application, because the bug fires in the engine before your code starts. Patch to 8.2.35, 8.3.36, 8.4.27 or 8.5.12 now. If you cannot patch tonight, turn off session.upload_progress and restart every worker before you do anything else.

What just landed

PHP shipped emergency patches on 21 October 2026 for two core bugs that got dubbed PHP2Shell. Both carry a CVSS v4.0 score of 9.2. Wiz is credited with the find, with Calif and OpenAI. This is not a plugin bug or a framework bug. It is in the interpreter itself, which is why every PHP shop on the planet is in scope at once.

And it is not theoretical. Datadog reports AWS saw exploitation attempts before public disclosure. watchTowr's honeypots picked up exploitation on the 21st. The technical details leaked ahead of the coordinated announcement, so the usual window of calm before mass scanning never happened. Treat the clock as already running.

The bug, in one breath

PHP saves memory pointers that belong to a single request into variables that live longer than the request. When an out-of-memory fatal error fires, the engine jumps straight past the code that should have cleared those pointers. The next request comes in, trusts a pointer into memory that was already freed, and you have a classic use-after-free. From there it is RCE or a memory leak, attacker's choice.

CVE-2026-109618 is that pattern in the session and request-decode path. CVE-2026-109619 is the same defect in other persistent state: the saved fatal-error backtraces added in PHP 8.5, and OPcache shared memory that gets corrupted across workers. Same root cause, two faces. JFrog's analysis calls them two angles on one defect.

The chain that makes it ugly

Here is why this one is worse than a normal memory bug, following JFrog's reconstruction of the flow.

  • You send a multipart upload. With session.upload_progress.enabled On, which is the factory default, PHP writes a session file and lets you influence the session ID, a key suffix, and the stored filename.
  • On the next session open, PHP unserialises that session file before any of your script runs, and it never checks where the file came from.
  • Decoding it builds internal objects and a bookkeeping struct. Trigger a fatal error mid-decode and the cleanup is skipped, so engine globals are left pointing at freed memory.
  • A second upload handled by the same worker reuses that freed memory. Its decode now runs attacker-controlled bytes, and the session write-back calls a fake object's serialize handler. That is control of the instruction pointer.

Read that second bullet again. The dangerous work happens before your application code executes. You do not need a vulnerable upload form, a vulnerable route, or a vulnerable anything in your app. You need PHP, running, with defaults.

Why this one actually matters

Plenty of PHP memory bugs need a weird build or a specific app feature. This does not.

  • Unauthenticated. No login, no token, no foothold.
  • Default config. session.upload_progress.enabled ships On. Nobody turns it off because nobody knows it is there.
  • Application-independent on mod_php. Apache mod_php is confirmed for app-independent RCE. FrankenPHP is likely in the same boat. PHP-FPM is reachable too, through state set after a clean startup.
  • Cheap. The only real constraint is landing two requests on the same worker. On a busy mod_php server that is trivial.

Am I exposed

Check the version that is actually serving traffic, not whatever your laptop prints.

# check the version actually serving requests, not just the CLI
php -v
php -i | grep -i "Server API" # which SAPI is live (mod_php / FPM / CGI)
php-fpm8.3 -v 2>/dev/null # match your branch

Affected branches, per JFrog's numbers:

  • PHP 8.2.x before 8.2.35
  • PHP 8.3.x before 8.3.36
  • PHP 8.4.x before 8.4.27
  • PHP 8.5.x before 8.5.12

The reporting says the defect reaches back through older branches as well. There is no full fix for PHP 5, and only limited updates for 7.4, 8.0 and 8.1. If you are still running end-of-life PHP in production, this is the day that bill comes due.

Patch, or barricade

The fix is the version bump. Upgrade to 8.2.35, 8.3.36, 8.4.27 or 8.5.12 and restart. Everything below is a stopgap for the hours before you can, and every one of them is partial.

; STOPGAP ONLY. not a substitute for patching. each line is partial.
session.upload_progress.enabled = Off ; closes the main upload path
fatal_error_backtraces = Off ; 8.5.x only
memory_limit = -1 ; removes the OOM trigger, and the DoS guard

On PHP-FPM, you can also break the same-worker requirement:

; php-fpm pool: breaks the "two requests, same worker" requirement
pm.max_requests = 1

And at the edge, if a given app never takes uploads, just refuse multipart there:

# nginx stopgap: refuse multipart where you never accept uploads
if ($content_type ~* "multipart/form-data") { return 403; }
Read this before you relax. Turning off session.upload_progress closes the main upload path, but it does not touch the OPcache or backtrace paths behind CVE-2026-109619. memory_limit = -1 removes the OOM trigger and your denial-of-service protection in the same move. Restart every worker after any change, or the old state lingers. None of these is a patch. They buy you hours, not safety.

Hunting it

Be honest about this part: clean network and log signatures are thin right now. Nobody has published solid IoCs yet. So you hunt the version and the side effects.

  • Inventory. Point your SCA at everything and flag any PHP below the fixed versions. This is the highest-signal thing you can do tonight.
  • Crashes. A use-after-free usually kills a worker before it lands cleanly, so a spike of segfaults in your PHP-FPM or Apache error logs is a tell.
# a UAF usually crashes a worker before it lands cleanly. watch for it.
grep -iE "segfault|signal 11|exited on signal" \
  /var/log/php*-fpm.log /var/log/apache2/error.log
  • Odd multipart. Watch for multipart POSTs hammering endpoints that have no business receiving uploads, and for new session files with strange prefixes.
  • Do not run strangers' PoCs. Fake PoCs are already on GitHub. JFrog flagged one that is a prank which changes your desktop wallpaper and ships no real exploit. The next one will not be a prank. Read code before you run it.

Verdict

PHP2Shell does not care what your application does. The bug fires in the engine, before your first line, on a factory-default setting, with no login required. That is about as bad as the shape of a vulnerability gets. Patch now. If you genuinely cannot patch now, kill session.upload_progress, restart every worker, and patch in the morning. There is no clever third option here.


Sources and further reading

PHP2SHELL

// elusive thoughts // appsec // php

PHP2SHELL

Every road from a PHP bug to a command prompt, and how to barricade each one.

TL;DR. The webshell one-liner is the part defenders obsess over and the part that matters least. The shell is the destination. The real work is the road: an upload that runs, an include() you control, an unserialize() on attacker bytes, or a config so broken the interpreter hands you RCE for free. Patch CVE-2024-4577, treat your upload directory as enemy territory, and stop calling unserialize() on anything a user has touched.

The one-liner is the boring part

Everybody can write the shell. <?php system($_GET['c']); ?> is thirty characters and it sits in every WAF signature on earth. If that string is the whole of your PHP threat model, you are guarding the last six inches of a very long tunnel. Attackers do not struggle to write the shell. They struggle to make PHP run it. That is the fight.

So this is about the roads, not the destination, and every road ends in defence. I have walked most of these on real Diixtra jobs and watched blue teams miss all of them, because they were grepping for system( and nothing else.

Road one: the upload that runs

The classic. An upload form that stores files under the web root and lets Apache decide what to do with them. If .php is mapped to the PHP handler, uploading evil.php means the server runs it the moment someone sends a GET. The file was never storage.

Recognise this in review:

// the whole bug in four lines
$name = $_FILES['avatar']['name'];
move_uploaded_file(
    $_FILES['avatar']['tmp_name'],
    "/var/www/html/uploads/" . $name
);

No extension allow-list, no MIME check, no magic-byte check, stored in the web root. The tester ladder, straight out of OWASP and The Hacker Recipes:

  • Drop <?php phpinfo(); ?>, request it, confirm the interpreter actually ran it. That is your proof. The live payload is a command sink.
  • Filter only checks for .php? Try .pHp, .php5, .phtml. On a default Apache mapping a multi-extension name resolves to PHP if any extension in the chain is .php, so shell.jpg.php runs.
  • MIME sniffing the bytes? Prepend GIF89a. The file passes as an image and still carries PHP underneath the magic header.
  • Can write a .htaccess? Upload one that maps a benign extension to the PHP handler, then upload your payload as that extension.
  • Can only write a .user.ini? Point auto_prepend_file at a file you control and PHP loads it on every request in that directory. SiteGuarding found exactly this in the wild: a 1.2 KB invoice.php under /wp-content/uploads/ wired in by a dropped .user.ini.

This is not a 2009 problem. Unrestricted upload to RCE was still landing critical CVEs through 2025, from CMS plugins to SAP NetWeaver Visual Composer.

Road two: include() is a loaded gun

include, require, and their _once siblings. Feed any of them a path you control and you are one hop from code execution. Old-school LFI needed the server to echo the file back, or a writable log to poison, or a session file to inject. Then two pieces of research turned LFI into a weapon with no upload at all.

// language switcher, circa every legacy app
include($_GET['page'] . ".php");

loknop showed in a 2021 CTF that you can abuse the php://filter wrapper to chain encoding filters and generate arbitrary PHP at the start of a stream. No upload, no log poisoning. You control the path, you get <?php system("id"); ?> prepended into the include. Synacktiv wrote it up and shipped php_filter_chain_generator, so it is point-and-click now.

Then hash_kitten, at DownUnderCTF 2022, turned the same wrapper into a blind file-read oracle. Chain enough encodings to blow past memory_limit, watch whether the request throws a fatal, and you have a one-bit oracle that leaks config.php one character at a time, even when the app returns nothing in the body. Synacktiv's php_filter_chains_oracle_exploit automates it. Ambionics stretched the reach further with wrapwrap.

Academic? No. CVE-2023-6553 (WordPress Backup Migration, unauthenticated RCE) and CVE-2024-29858 (arbitrary file read on MISP) both ride filter chains.

Road three: unserialize() and the phar:// trapdoor

PHP object injection is the road blue teams understand least, and the one I reach for most on grey-box work. Call unserialize() on bytes an attacker controls and you let them build arbitrary objects with arbitrary properties. On their own those objects do nothing. Chained through the app's own magic methods (__wakeup, __destruct, __toString, __call) they become a POP chain, property-oriented programming, and the far end of that chain is a dangerous sink you already shipped.

// trusting a cookie
$prefs = unserialize($_COOKIE['prefs']);

Now the trapdoor nobody remembers. You do not always need an obvious unserialize(). The phar:// stream wrapper deserialises the Phar metadata the instant almost any filesystem function touches the path: file_exists, fopen, getimagesize, is_dir. Sam Thomas demonstrated this at Black Hat USA 2018 as file-operation-induced deserialisation. An upload that accepts a crafted "image" plus a filesystem call on a path you influence is a deserialisation sink hiding in plain sight.

The fix is dull and absolute. Do not unserialize() user input. Use json_decode(). If you genuinely must deserialise, pass allowed_classes => false.

Road four: the config hands you RCE (CVE-2024-4577)

Sometimes you skip the application entirely and the platform gives you the shell. CVE-2024-4577, disclosed by DEVCORE in June 2024, CVSS 9.8. PHP in CGI mode on Windows mishandles Unicode "Best-Fit" conversion. A soft hyphen (0xAD) slips through argument escaping and PHP reads it as a real hyphen (0x2D), so you inject command-line arguments into the interpreter itself. Set auto_prepend_file to php://input and your request body becomes code. It walks straight past the fix for the ancient CVE-2012-1823.

The kicker: every default XAMPP for Windows shipped vulnerable out of the box. watchTowr caught the mood in their write-up title, No Way, PHP Strikes Again. Patched in 8.3.8, 8.2.20 and 8.1.29.

I am not pasting the weaponised request here. The full PoC is on NVD, DEVCORE and watchTowr if you are testing your own estate. What you do about it:

  • Patch PHP. Now.
  • Do not run PHP in CGI mode. Use PHP-FPM.
  • Kill XAMPP defaults before anything Windows faces the internet.
  • Block the soft-hyphen byte (0xAD) and php:// at the WAF as a stopgap, not a cure.

Road five: the command sink

The least clever and the most common. User input concatenated into a shell call.

// "diagnostics" page
system("ping -c 4 " . $_GET['host']);

Send host=8.8.8.8; id and you are done. The family to hunt: system, exec, shell_exec, passthru, popen, proc_open, and backticks. escapeshellarg() and escapeshellcmd() help but they are not magic. Argument injection survives a naive escapeshellcmd(), and plenty of CLI tools will happily run options you smuggle in as arguments. The real answer is to not shell out. If you must, call a fixed binary with an argument array and a strict allow-list on the value.

Same road, honourable mentions: eval(), assert() (callable with a string on old versions), create_function(), and preg_replace() with the /e modifier, which PHP 7 killed for exactly this reason. Template engines count too. Twig and Smarty SSTI drops you to RCE the moment user input reaches the template source.

Hunting the shell

You will not catch this grepping for system(. Shells obfuscate. Hunt the behaviour and the primitives instead.

Code-level IOCs (static)

  • Eval and execution primitives: eval, assert, create_function, call_user_func, and variable functions like $_GET['x']().
  • Command sinks: system, exec, shell_exec, passthru, popen, proc_open, backticks.
  • Obfuscation tells: base64_decode, gzinflate, gzuncompress, str_rot13 stacked together, high-entropy blobs, ${"GLOBALS"} variable-variable games.

Behavioural IOCs (where you actually win)

  • Static files answering POST. A 200 on POST /uploads/2025/09/logo.png is a webshell until proven otherwise.
  • New files appearing in writable upload dirs. A .user.ini or .htaccess turning up where one never existed.
  • Outbound connections from the web user. www-data has no business opening sockets to the internet.

Tooling that earns its place

  • YARA at scan time. The NSA publishes PHP webshell rules, emposha's PHP-shell-detector and the Panelica rule packs are solid, and the US-CERT pas_tool rule still catches that family. Run yara -r rules.yar /var/www/html on a cron.
  • Velociraptor's webshell detection artifact for fleet-wide hunts.
  • File integrity monitoring on the web root so a dropped file screams.
  • Newer work pairs YARA with CNN and LLM classifiers for the polymorphic shells signatures miss. If you own a big PHP estate, the 2024 to 2026 research is worth your time.

A grep for triage, not detection:

grep -rnE \
  "eval|assert|system|shell_exec|passthru|base64_decode|gzinflate" \
  /var/www

Shutting the roads

Defence in the only order that matters. Interpreter first, app last.

php.ini, cheap wins you are not using

allow_url_include = Off
allow_url_fopen = Off
expose_php = Off
display_errors = Off ; prod only
disable_functions = exec,passthru,shell_exec,system,proc_open,popen
open_basedir = /var/www/html:/tmp

Uploads, treat the folder as hostile

  • Allow-list extension and MIME and magic bytes. All three.
  • Re-encode images on receipt. That destroys appended payloads.
  • Random filenames, stored outside the web root, served through a download handler.
  • Turn PHP execution off in the upload path at the web-server layer. Nginx: no PHP handler for that location. Apache: php_admin_flag engine off.
  • ClamAV on upload, plus a size cap.

Config and platform

  • Patch PHP and stay patched. CVE-2024-4577 is the current reminder.
  • PHP-FPM, never CGI. No XAMPP defaults in production.
  • Never unserialize() user input. JSON, or allowed_classes => false.
  • Run the web user with no write access to the web root. Read-only code, writable data elsewhere.
  • Egress filtering, so even a live shell cannot phone home.
  • WAF as a speed bump for php://, filter-chain encodings, the soft hyphen 0xAD, and multi-extension uploads.

Verdict

PHP does not give you a door to the shell. It gives you a corridor with a dozen doors, and half of them are propped open by config you inherited. The webshell at the end is trivia. The roads are the job. Patch the interpreter, lock the config, assume the upload folder is already compromised, and hunt the behaviour instead of the string. Do that and the one-liner has nowhere to land.


Sources and further reading

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.

PHP2Shell The CVE!!

// elusive thoughts // appsec // php PHP2Shell Two PHP core bugs, CVSS 9.2, unauthenticated RCE before your code runs a single line. This is...