A request that should have been blocked came back 200.
No error. Not a line in the guard’s log. The most dangerous response is the one that looks fine.
This is the long version of my November 2 talk at AIMUG in Austin. I had a 22-minute slot, so a lot got cut. This version has all of it. How a token bill led me to a security advisory. How the bug works, and how to check for it. What coordinated disclosure really took. And what I think it would take to do it faster. Watch the talk, or click through the slides.
Watch the talk on YouTube (41 minutes with Q&A).
The short version. One routing header let a request skip the policy guard in vLLM Semantic Router’s Istio integration, because two filters shared a name. The fix is a rename, and I sent it with the report; the advisory was public six days later. Getting the lesson to the other projects I found running the same pattern took two weeks. Run Istio 1.27 or later with an InferencePool route and a third-party ext_proc filter? Check yours.
Black October
At November’s AIMUG meetup, I asked who had patched PHP since October 21. Nearly the whole room raised a hand.
A widely shared post on October 22 used the name Black October. It listed a long run of incidents and called them the most serious 48 hours in cybersecurity history. That claim doesn’t survive a close look. Several of those attacks had been running for months and only became public that week. Several came down to old-fashioned failures: a leaked key that led to a hijacked release pipeline, exposed credentials, a certificate authority with weak keys. It was a disclosure wave, not one campaign.
The centerpiece was real, though. On October 21, PHP shipped fixes for two critical bugs, CVE-2026-109618 and CVE-2026-109619, that allow unauthenticated remote code execution. People call it PHP2Shell. Three teams found it separately, Calif, OpenAI and Wiz, all with AI in the loop.
Amazon saw an exploitation attempt on October 17, four days before the fix shipped. CISA added both bugs to its Known Exploited Vulnerabilities catalog on October 26. And PHP’s own foundation says early signs of the out-of-memory bug behind one of the two CVEs surfaced during follow-up to a 2024 audit; the fix stalled on hard tradeoffs, uncertain exploitability and limited resources.
It also wasn’t the end of the internet. Within a week, Wiz saw about two thirds fewer exposed, vulnerable machines in its telemetry, and no complete exploit land there. Honeypot researchers did catch working shells on deliberately easy targets. Wiz reads that as people patching, upgrading, retiring machines and locking down access. That took a lot of people dropping what they were doing.
If you run PHP and haven’t patched, start there. Install the fixed release and restart whatever serves it. Look for suspicious activity at least back to October 17. Then read PHP’s October 23 follow-up advisory: it covers one more, rarer vector, CVE-2026-115560. If your application switches its session serialize handler at runtime, turn off session upload progress until the next release.
That’s the real story. Finding the bugs wasn’t the hard part. Everything after was.
Finding got cheap
Finding bugs got a lot easier this year, for everyone:
- Mozilla fixed 271 bugs in Firefox 150 that it found with Anthropic’s Mythos Preview.
- Google’s Big Sleep found 45 actionable V8 bugs in six months, against 37 from its Fuzzilli fuzzer. Google’s conclusion: run both.
- In Anthropic’s own study, agents turned 8 of 18 patches to Firefox’s JavaScript engine into working exploits, the first in under an hour. Ship a patch, and the diff is the map.

In July through September 2026, the CVE Program published 36,962 records, more than in the first nine months of 2025 combined. These are published records, not discovery dates.
CISA’s catalog of known-exploited bugs grew much more slowly: 276 additions between January 1 and October 27, against 210 over the same stretch last year. That’s up about a third, while published records more than doubled. The catalog isn’t a census of exploitation, and those counts don’t tell us how long we have. But most newly published bugs have no confirmed exploitation yet. That’s the window: there’s still time to fix them, if anyone can get to them.
Their queue is your queue. As of October 26, curl handles low and medium severity reports in public issues, because the private queue can’t keep up. Not because of AI slop, which has mostly disappeared, but because the valid reports keep coming.
Finding scales with compute. Fixing doesn’t.
A token bill, a router and eight fixes
I’m not a security person. In 27 years of doing this work I had never found a zero-day. Then I stumbled into one.
At work, I was near the top of the Claude Code bill. I wanted to get more efficient, and that sent me looking for a router: one place to decide which model answers. I put my coding agents behind vLLM Semantic Router, pointed at models on my own lab. OpenCode and Codex worked through it on day one. Claude Code didn’t. The router didn’t fully support Anthropic’s Messages API yet: it tripped on a request field it didn’t know, and some replies came back empty.

So I started fixing it. Instead of filing a request and waiting, I could write the code and send it upstream. vLLM first, in September, then the router itself: stop sequences, reasoning that streamed before the answer, error frames that got split in the stream. Eight fixes, all merged. One in vLLM (#55325; the story is in The Router Was Right). Six in the router (#4266, #4391, #4539, #4658, #4901 and #5150). One in NVIDIA’s Switchyard (#884). Claude Code mostly works through the router now.
Somewhere in there it became the most fun I’d had in years. I applied to the router’s Data Plane and Networking working group on September 24 and became a member on October 9. Now I get to do the work alongside people who are way smarter than me.
On October 5, at Mixture of Models here in Austin, I made a promise about routers. Most teams start with one router for one app. Then a second team copies it, a third forgets the guard, and now your privacy policy has three versions. Put the policy in one place instead: “One policy, enforced once, for every app.” Cost, privacy and security at one control point.
Five days later, building a lab to show that promise off, I found one that wasn’t a bug in my client.
One header, and the guard is skipped
It was October 10, around 3 in the morning. I was burning down bugs with AI in the loop, but I was also reading the code, because I wanted to understand how routing works across these projects. That meant learning Go. The AI called what I’d found a bug. I read the code again, and the bug turned into something else. Holy smoke: I could skip policy by setting a header.
The advisory is public, so I can walk through it. Two roles matter. The guard enforces your policy: it’s the service that says no to a prompt injection, or to someone pasting a Social Security number into an agent. The endpoint picker chooses which model server gets the request.
Here’s the chain. A request enters the Istio gateway. The router runs as an ext_proc filter and calls the guard service over gRPC. Then Istio’s own ext_proc filter calls the endpoint picker to choose a pod. Then Envoy’s router sends the request on. Same filter type, and, it turned out, the same name.

Watch this part (6:09) · Step through it in the deck
- Two filters share one name. The router’s filter and Istio’s endpoint picker filter were both registered as
envoy.filters.http.ext_proc. - Istio files its per-route settings under that name. When a request lands on an InferencePool route, Istio attaches settings for the filter with that name. Envoy looks them up by name, not by owner, so they reach both filters.
- So your filter’s call goes to the endpoint picker, and the guard is never called.
Any client can set the routing header that puts a request on that route. And if a catch-all rule already sends traffic to the pool, no header is needed at all.
Per-route config keys are addresses, not labels.
I recorded it in a lab with a neutral stub guard, on Istio 1.28.0, on October 18:
| Request | Route | Result |
|---|---|---|
| Marked (should be blocked) | Route without a pool | 403, blocked |
| Clean | InferencePool route | 200, inference served |
| Marked (should be blocked) | InferencePool route | 200, no error, no line in the guard’s log |
| Rename the filter, then: | ||
| Marked (should be blocked) | InferencePool route | 403, blocked again |
| Clean | InferencePool route | 200, inference still served |
Look at the second row. That clean request had already skipped the guard. It just didn’t matter, because the request was clean.
There’s a second effect. On other backends in a rule that includes a pool, Istio writes a “disabled” setting under the same name, and your filter is switched off. That came with a September change that resolves endpoint pickers per backend (istio/istio#61601), backported to 1.29.9, 1.30.6 and 1.31.2. The change is sound. It just widened what the collision does.
Your guard was configured, deployed and healthy. The route told it to call someone else. Anything you put behind that filter, from content policy to monitoring, gets skipped the same way.
One policy, enforced once, only counts if the request reaches it.
How it got in
Nobody wrote a bug.

In March 2025, the Gateway API Inference Extension’s body-based routing chart used the bare filter name, and in April the router did the same in plain Envoy. Harmless then: nothing else in the chain wrote settings under that name. In July, Istio started filing per-route settings by filter name, shipping in 1.27. That fall, the router’s Istio guide used the bare name, and a client header could pick the pool. That’s when it became reachable, by my reading of the public history.
In March 2026, the chart renamed its filter to envoy.filters.http.ext_proc.bbr as a routine bug fix (#2643, merged March 20, shipped in v1.5.0). Nothing carried that lesson to the other integrations using the name: dependency tooling doesn’t track a shared filter name, and that’s a gap in all of our processes. In September, the Istio backports added the switched-off effect. On October 10, I found it.
It takes three ingredients:
- Flat filter names. Every filter in the chain shares one namespace.
- A control plane that writes per-route settings by name.
- An insertion path that doesn’t check whether the name is already taken.
In weakness terms, duplicate identifiers (CWE-694) are the cause. When the filter is a security control, the consequence is a protection mechanism failure (CWE-693).
Istio uses that name for the filter it generates, so the fix belongs on the filter side: give yours a distinct name. People ask why Istio doesn’t just rename its own filter. That would remove the collision, but EnvoyFilters that match Istio’s filter by name today could change behavior, so I think it’s a separate question. I laid out that reasoning in the public issue, istio/istio#62556.
Every step made sense where it was made. Flat names, settings keyed by name, and no shared check for collisions: that’s a class of bug, not anyone’s mistake.
Name your filters like you own them
Checking takes one command, and the fix is a rename. The setup I reproduced had three things on one gateway: Istio 1.27 or later, an InferencePool route, and a third-party ext_proc filter under the bare name. If you run anything like it, check.
Dump the gateway’s config and look for the name twice:
istioctl proxy-config all <gateway-pod> -o json
- Two HTTP filters named
envoy.filters.http.ext_procin one HTTP connection manager. - A per-route entry under that name that points at the endpoint picker, or sets
disabled: true. - The fastest tell: the endpoint picker logs every request twice.
The rename is small: same filter type, its own instance name.
- name: envoy.filters.http.ext_proc
+ name: vllm-semantic-router.ext_proc

Validating it and rolling it out is the work. Move every reference with it: per-route keys, patch matches, charts and tests. Then check the config the gateway actually received, and send the marked request again. The router’s fix also added a contract test, TestIstioEnvoyFilterRouterNameIsUnique, so the bare name can’t quietly come back. If a product generates the filter for you, ask the vendor for the rename.
I proposed an istioctl analyze warning for exactly this in istio/istio#62560. If it’s accepted and released, istioctl analyze will warn about the configurations it recognizes. As proposed, it reads like this (the text can change before it merges):
EnvoyFilter adds a gRPC ext_proc filter named envoy.filters.http.ext_proc
(target "my-guard.default.svc:9002") to Gateway "inference-gw", which serves
InferencePool vllm-pool. Istio keys the pool's per-route settings by that name,
so this filter can call the endpoint picker or be disabled. Use a distinct name.
A clean analyze isn’t proof. The config dump shows what was installed. A blocked request shows the guard runs.
That felt like the end of the story. It was the middle.
My own AI tools locked me out
Rewind to that first week. Claude Code, the assistant I use every day, locked me out more than once. While I treated this as a bug, it was happy to help. The moment I treated it as a vulnerability I needed to report responsibly, Anthropic’s safety classifier stopped the reproduction, the disclosure handoff, and even a slide explainer of the bug. That stung.
Reviewing the fix was fine. Once I’d written the fix elsewhere and brought it back, it would help me explain the bug, too. It’s the reproduction and the attack path it won’t touch. From the outside, defending looks a lot like attacking, and those defaults make sense.

Watch this part (12:42) · Step through it in the deck
So I went local. Qwen 3.8 27B, an open-weight model, on a GH200, ran the reproduction, wrote the patches and helped guide the disclosure. Making the patches took about an hour.
Defenders need capable models they control.
There was a door. The one I knew of asked for an assigned CVE or a security-research employer. What I didn’t know was that Anthropic had already widened its Cyber Verification Program on October 6: maintainers and established individual researchers can apply. If you do this work, apply before you need it.
The fix came with the report
A month ago, “coordinated disclosure” was a phrase I’d read about. Then I had to learn its central tension by doing it. Say too much, too early, and you hand attackers a map to your systems, or your customers’ systems, or someone else’s. Say too little, too late, and nobody patches. Coordinated disclosure is how you walk that line, and once you’ve reported a bug, what you say in public is a choice, not a reflex.

I reported it privately, through GitHub’s private vulnerability reporting as the project’s security policy asks, and attached the fix the same day.
Xunzhuo, one of the router’s maintainers, accepted the report four days later and approved my patch as written. Two days after that, the advisory went public as GHSA-xr66-pq9r-2c8j, a CVE was requested through GitHub’s CNA, and my commit merged to main. Six days from report to advisory: the maintainers moved fast, on top of everything else in their queue, and I’m proud to work with them.
That’s what bringing the fix buys you.
If you run the router’s Istio integration, you don’t have to wait for the next release: rename the filter in your gateway, or apply the fix commit. Merged isn’t deployed. That part is on all of us.
Then I had to find people
The rename fixed the router. But the same pattern was running in other projects too.
So I searched GitHub for other projects using the same filter name in their own Istio integrations, and sent private notices to the ones it touched. Nobody asked me to do this part. But I’d found it, and I wanted it handled the way I’d want someone to handle it for me.
Someone asked a good question in the Q&A: wouldn’t those projects just pick up the fix in an update? No. They don’t depend on the router. They share a name in the gateway’s config, and nothing like Dependabot tracks a shared name. When a security function sits in a shared namespace and the normal notification paths don’t apply, what’s left? Email, and hunting people down.

Watch this part (19:37) · Step through it in the deck
Finding the right person was the work: security contacts that bounce, projects with no private way to report, maintainers you can reach only by guesswork, and volunteers with no time left to answer. None of this is anyone being bad at their job. A lot of these projects run on volunteers with day jobs, and the process was built for a slower world. To the maintainers who wrote back, often on their own time: thank you.
Writing the fix took less time than writing the messages.
I kept a ledger of who knows what, who owns which fact, and what I promised to whom. Three AI models reviewed every message, and every send and every commit was still mine. I kept wishing there was one place to send all this, once, and have it reach everyone who needed it. Until something handles cases like this end to end, this post is my version of it, for the public half: the bug, the check and the fix, in one place.
What worked: a hallway
I was at PyTorch Conference and AGNTCon while this was going on, and I spent a good part of that week hunting people down in person. GrokBot helped me pick sessions and organize my schedule, and cross-referenced my list against the exhibitors and speakers. I found engineering leaders on site, several of whom led the teams that shipped fixes on their side. I made some friends, too, which is the fun part of open source.

I was lucky to be at a conference that week. It was strange that I needed to be. A security handoff shouldn’t depend on being in the same hallway.
Then upstream
Once the private notices were out, I proposed fixes for the whole class upstream. I opened an Istio issue (istio/istio#62556) and a pull request for the istioctl analyze warning (#62560). I also opened a docs pull request in the Gateway API Inference Extension with naming guidance (#3078), so the next project doesn’t repeat it. Both pull requests are open for review. Istio’s design is the Istio team’s call. A check that warns you is something everyone can use.
Where the time went

In my own case, finding it took a day, with AI in the loop. Private report to public advisory took six days. The handoffs to the other projects running the same pattern took two weeks, finished October 30, mostly on nights and weekends. Doing it the right way took most of a month.
And this wasn’t a bug that takes down the internet. It’s narrow and specific. But if I were an agent looking to get around policy, that’s exactly how I’d do it. It was good practice for the world we’re heading into.
Finding got faster this year. The handoffs didn’t.
Handoffs in seconds, not days
So how do we solve this? Two months ago I knew reviewers were underwater. What I didn’t understand was how tangled this generation of software is. One filter name crossed three projects, and fixing it meant coordinating across all of them. I have a lot more empathy now for the people doing remediation every day.

Watch this part (24:47) · Step through it in the deck
AI-driven development is a flood of code to review, and the people reviewing it are maintainers, most of them volunteers. Vulnerability reports are part of that flood. GitHub now rate-limits private vulnerability reports. Attackers move at agentic speed. We still pass email and triage like it’s 2008.
What it would take
Three things:
- Communication. Automated handoffs, so the right person gets a report and its status in seconds, not days.
- Remediation. Software factories that run vulnerability work through the same pipeline we already run features through: find, validate, fix, deploy, verify and upstream, with automated fixes and backports and a person approving each one.
- People. Paying the ones who maintain it.

The clocks are already tightening:
- Google Project Zero gives vendors 90 days, and 7 when a bug is already exploited.
- Since September 11, 2026, the EU Cyber Resilience Act has required manufacturers to send an early warning on an actively exploited vulnerability within 24 hours of becoming aware of it.
- Anthropic’s disclosure policy paces its reports to what maintainers can absorb.
The direction is clear. I want coordination to move from days to seconds. Not the fixes: those still need review, testing and a rollout. The handoffs. That means tiers of maturity, so the teams that can take reports at machine speed get them that way, without flooding the projects that still run on volunteers and email.
I’d draw the tiers like this:
- Level 0, unreachable. No security contact.
- Level 1, reachable. A published security contact and private reporting turned on.
- Level 2, responsive. Triage in days, CVE coordination through a CNA, and fixes in a tagged release.
- Level 3, automated. Your own pipeline can take machine-speed reports without drowning.

Where’s your project? Level 1 takes minutes to turn on.
And when someone finds a fix that works, there should be a fast lane: from finding, to validated fix, to reference implementation. Signal it to everyone who needs it through a chain of trust, and leave review and rollout in each maintainer’s hands.
Who’s working on it
One place this could live is SAFE, the Shared AI Findings Exchange at the Open Secure AI Alliance. Its members share AI incidents and near misses and turn them into shared controls. Its framework already sets clocks for them: notify the directly affected organization immediately, and customers with credible exposure within 72 hours. What I want to bring there is the handoff, and the tiers.
A lot of people are working on the other pieces:
- Communication: OpenSSF’s disclosure guidance, machine-readable security contacts with security.txt, advisories machines can read, through OSV, CSAF and CVE Services, and exploitability statements through OpenVEX.
- Remediation: OSS-Fuzz and CodeMender propose fixes that maintainers review, OpenSSF’s OSS-CRS runs AI find-and-fix systems together, and the Linux Foundation’s Akrites is building shared responders for confidential fixes.
- People: Alpha-Omega, the Sovereign Tech Agency and the GitHub Secure Open Source Fund pay the people who maintain it.
Nobody has the whole thing yet. Confidential reports don’t move between platforms automatically. Human-approved backports and deployment tracking are fragmented. Funding doesn’t follow the workload. The gaps between the pieces are still email.
If you run security for a project or a foundation and want to help build this, let’s talk.
A bill the rest of us haven’t paid
I started this with a bill I wanted to cut. Here’s the one we haven’t paid.
Remember the chart from the beginning? Black October was the first month of a quarter nobody has counted yet. It’s only the start. Three things to do this week, one for each piece of that line: communication, remediation and people.
Watch this part (28:35) · Step through it in the deck
- Be reachable. Turn on private vulnerability reporting and keep a security contact that answers. That’s Level 1. Turning it on takes minutes; answering is the part that needs support. The router had it on, and that’s where this story went right.
- Bring the fix. Anyone with a model can file a report now; validating one still takes real expertise. So offer a fix, or help test one. Sending the patch with mine let the maintainers move fast. And if you run this stack, look for the filter name twice.
- Show up for the maintainers. Review and backtest in the projects you use, on hardware they may not have. Pick up the unglamorous work. Not a flood of AI pull requests: that’s more load, not less. (Mine were AI-assisted too: small, disclosed under each project’s rules, and I stayed through review.)
If you lead a team, give a slice of your engineers 20% time upstream, and fund the foundations. All of this costs money. So does the next PHP2Shell.
A 200 that should have been a 403 looks fine. A report that never reached the right person looks fine too.
Finding already scales with compute. Now fixing has to. Let’s get good at this, fast.
What are you seeing? Tell me. And if you found something in vLLM or Semantic Router and aren’t sure how to report it, I’ll help.
