There is a long tradition of "Month of" bug projects: Month of Browser Bugs, Month of Apple Bugs, Month of Kernel Bugs, Month of PHP Bugs: "A month of bugs is a strategy used by security researchers to draw attention to the lax security procedures of commercial software corporations." - Wikipedia - Month of bugs. I really wanted to see how fast one can discover 0days with the help of AI-SAST tool against Open Source Software (OSS), specifically in projects from Apache Software Foundation (ASF). I wanted to point that same lens at one target and see what fell out. So I ran a Month of Apache Bugs and called it Operation: Binary Chaos. This meant that I had to release a bug every day within one month. The discovery pass took five days. I started on 13 August 2026, and by the 18th all 36 confirmed 0-days in 36 different projects/codebases were triaged and reported. One man, one desire, one foundation, and a question I kept coming back to: how deep does the canary go across the ASF's enormous, uneven, frequently half-abandoned project universe?
Deep enough to fill a release calendar, it turns out. I went through roughly 345 Apache projects and components and opened 40 numbered advisory tracks in that first week. The long tail was disclosure, not discovery: reporting to each PMC, working with the teams that answered, waiting on the ones that never did, and publishing 36 advisories (ZSL-2026-6003 through ZSL-2026-6038) between 11 September and 30 October 2026, the day I closed the operation out. That stretch ran about fifty days, better than one shipped advisory every two. Nine CVEs came out of the pile. Every finding followed the one rule I set on day one: trace it in the real released source, then actually stand it up and trigger it before I let myself call it confirmed or "Green-GO".
Here is the severity distribution of the 36 published advisories, using the rating published with each ZSL advisory. A lot of these are deploy-gated or gadget-gated and carry a wider range in the finding itself, so read the bar as a shape, not a verdict:
The three Criticals are all remote code execution: HugeGraph (sandbox escape), Submarine (auth bypass), and SystemDS (unauthenticated federated-worker deserialization). The long Medium-and-Low tail is mostly memory-safety DoS in the native-C corner of Apache: RocketMQ-C++, ActiveMQ-CPP, Qpid Proton-C, MiNiFi C++, NuttX. That corner is under-audited, and it paid out over and over.
Severity is only half the picture, and honestly the less interesting half. Here is the spread by vulnerability class: one foundation handed up ten distinct bug families in five days.
The slice I did not expect to be writing up: that "memory corruption" wedge includes a kernel-space out-of-bounds write in the NuttX RTOS 6LoWPAN decompressor (ZSL-2026-6019). A real kernel 0-day, inside an Apache project, from a desk (sometimes a sofa), in under a week. The other two in that wedge are an out-of-bounds read in the RocketMQ C++ client and a heap overflow in Celix's EDEF parser.
If this writeup is about one thing beyond the bugs, it is AI in offensive security research: what the model is genuinely for, where it lies, and who stays accountable for the verdict.
A single human cannot read 345 Apache codebases in five days. This includes me :P. What made the pace
possible was pairing an AI assistant (Anthropic's Claude) with a verification protocol I refused to bend. The
AI did the part it is genuinely good at: fan out across unfamiliar source trees and recognize dangerous shapes.
An unfiltered ObjectInputStream. A new File(dir, entry.getName()) with no canonical
check. A recursive-descent parser with no depth cap. A servlet route that quietly never reaches the auth
filter. It flags them, ranks them, and then a human goes and chases them.
But a static flag is a hypothesis, not a verdict. That is the same lesson our
tuktam / LightFTP work turned on, and I did not forget it here.
Nothing shipped on a model's say-so. Every finding got traced to the sink in the exact released version, then
live-triggered against the real artifact: the pinned Maven jar on a harness, the compiled C
binary under a sanitizer, the container running the actual service. That protocol is exactly what caught both
false positives. The AI, and a public PR, called Avro-C a crash; the real libavro returned
ENOMEM and never faulted. The model makes you fast. It does not get to decide what is true.
And it failed in both directions, which is the part worth sitting with. It quit on real bugs: it decided OzHera's SQL sink "wasn't reachable" when it plainly was, and Doris took a second pass before it would believe its own finding. It was also certain about bugs that were not there, the two retractions above, which it would have sworn on. A model is a superhuman generator of hypotheses and a subhuman judge of them. Infinite confidence, zero calibration, and the whole job was being the calibration.
The hardened core of Apache is a fortress. Struts, Shiro, the Dubbo and PDFBox cores: everyone attacks
them, git blame is wall-to-wall CVE fixes, and I found almost nothing there. The bugs clustered in
three softer places, and once I saw the shape I went looking for it on purpose:
Two of the projects I reported against, Submarine and jclouds, were retired to the Apache Attic during the research window. Retirement does not un-deploy software: a freshly-retired project with a live unauthenticated RCE still sitting on Maven is exactly the supply-chain shape this whole thing was about.
Most of these need no framework to fire. The DevLake SSRF, for one, is the entire exploit:
No token, no session. The server resolves and fetches the cloud-metadata endpoint, then reflects the body
back in its own JSON error (ZSL-2026-6037, CWE-918). That is the mundane end of the range. The fun end was a
Groovy Finalizer thread strolling out of HugeGraph's SecurityManager sandbox to pop a live shell, and a
crafted 802.15.4 frame writing past a static buffer in the NuttX kernel. One curl to one kernel
write, same week.
The most repeated pattern of the whole operation was not a bug class. It was a sentence. Over and over, a genuinely reachable vulnerable path got waved off as "by design", not because the code was safe, but because a threat model had decided the attacker was out of scope. Nine of my findings were declined this way, and in every one of them the vulnerable code still ships in the latest release:
A threat model is a real and useful thing. But "the attacker is assumed trusted" is a scoping choice, not a
property of the code, and it does not make readObject on an attacker-controlled stream safe. I
published these reframed, honestly, as insecure-default / missing-authentication issues,
because the operator who only reads the changelog will never learn that the default leaves them exposed. The
ASF is entitled to its triage opinion. The vulnerable byte sequence is still in the jar.
Nine CVE IDs attach to the batch. Eight of them credit ZSL directly. One, Impala, is an independent-analysis duplicate of an earlier report, June 10, by zhaokaifei. o/
Not every outcome was a clean assignment. Qpid Proton-C got silently fixed in 0.41.1 before any advisory existed; the PMC only confirmed the vulnerability after I published against the already-released fix, and the CVE landed later still. PDFBox was quietly capped with no CVE at all. On the better side, BuildStream merged a fix and turned my PoC into a credited regression test, and I offered hardening PRs to a few projects whose maintainers declined the vuln but took the patch (Doris, Avatica's allow-list, a review of Paimon's fix). A couple of those contribution threads are still open as I write this.
The HugeGraph finding earns its own line. It is a live, end-to-end sandbox escape, a residual of CVE-2024-27348 that walks straight past the thread-name guard the original fix added, without ever touching reflection. The mechanism is written up in full in our HugeGraph SecurityManager sandbox-escape paper.
A month of bugs ends on a deadline. The vulnerabilities do not. As I publish this:
All 36 published advisories, in publication order. The Shield column marks whether the vendor sheltered vulnerable code behind a security-model or "by design" decision instead of fixing it.
| ZSL ID | Date | Project / Version | Vulnerability | CWE | Severity | Shield | CVE / Status |
|---|---|---|---|---|---|---|---|
| 6003 | 11.09 | Impala 4.5.1 | Insufficient authorization, STORED BY JDBC driver.url remote JAR class-load → RCE | 862/494 | High | no | CVE-2026-65181 (dup) |
| 6004 | 20.09 | PDFBox 2.0.37 / 3.0.8 | Flate PNG-predictor disproportionate heap allocation → OOM DoS | 789 | Low | no | fixed, no CVE |
| 6005 | 24.09 | BuildStream 2.8.0 | Tar symlink traversal → host arbitrary file write | 59/22 | Medium | no | CVE-2026-82331 |
| 6006 | 28.09 | Karaf 4.4.11 | JAAS LDAP login-module filter injection | 90 | Medium | no | CVE-2026-90979 |
| 6007 | 05.10 | Submarine 0.8.0 | Spoofable User-Agent auth bypass → unauthenticated RCE | 290/807 | Critical | no | no CVE (Attic) |
| 6008 | 06.10 | Commons JCS 3.2.1 | Unauthenticated Java deserialization on the TCP lateral cache → RCE | 502 | High | yes | by-design, no CVE |
| 6009 | 06.10 | Curator 5.9.0 | Unsafe Jackson @JsonTypeInfo(Id.CLASS) discovery deser → RCE | 502 | High | no | no CVE |
| 6010 | 07.10 | Ratis ≤3.3.1 | Unfiltered Java deser of RPC reply exception bytes → client RCE | 502 | High | no | no response, 0-day |
| 6011 | 07.10 | SeaTunnel 3.0.0 | Unauthenticated REST /submit-job + DynamicCompile → RCE | 306/94 | High | yes | by-design, no CVE |
| 6012 | 08.10 | OzHera 2.2.6 | Authenticated SQL injection in Doris-backed log search | 89 | High | no | CVE-2026-80277 |
| 6013 | 08.10 | Avatica ≤1.29.0 | JDBC connection-property injection → backend file read / deser | 610/502 | Medium | yes | by-design, no CVE |
| 6014 | 09.10 | HugeGraph 1.7.0 | SecurityManager sandbox escape via Groovy Finalizer thread → RCE | 693/94 | Critical | no | residual of CVE-2024-27348 |
| 6015 | 11.10 | Celix 2.4.0 | EDEF endpoint-descriptor XML parser heap buffer overflow | 122/120 | High | no | accepted, embargoed, CVE likely |
| 6016 | 12.10 | YuniKorn 1.9.0 | LDAP group-provider index panic → DoS | 248 | Low | no | CVE-2026-78243 |
| 6017 | 12.10 | ResilientDB 1.12.0 | Pre-auth unbounded malloc in TCP acceptor → OOM DoS | 789/770 | Medium | no | no CVE |
| 6018 | 13.10 | Airflow 3.3.2 | Secrets-masker deep-nesting leak → cleartext secrets in logs | 200/532 | Medium | no | CVE-2026-113504 |
| 6019 | 13.10 | NuttX 13.1.0 | 6LoWPAN HC06 decompressor kernel out-of-bounds write | 787 | Medium | no | CVE requested |
| 6020 | 14.10 | IoTDB 2.0.11 | Authenticated SSRF via CREATE FUNCTION ... USING URI | 918 | Medium | yes | by-design, no CVE |
| 6021 | 15.10 | Bigtop Manager 1.1.0 | Tarball Zip-Slip / symlink → arbitrary file write (root) → RCE | 22/59 | High | no | no CVE |
| 6022 | 16.10 | MINA Vysper 0.7 | Unbounded XML token buffer → pre-auth OOM DoS | 400/789 | High | no | CVE-2026-115721 |
| 6023 | 16.10 | Doris 4.1.5 | JDBC catalog unsafe props → arbitrary file read | 610/200 | Medium | yes | by-design, no CVE |
| 6024 | 18.10 | SystemDS 3.4.0 | Unauthenticated federated-worker Java deserialization → RCE | 502/306 | Critical | yes | prompted their 1st SECURITY.md, then declined |
| 6025 | 19.10 | Qpid Dispatch 1.19.0 | Unbounded AMQP annotation recursion → stack-overflow DoS | 674 | Medium | no | CVE-2026-88763 |
| 6026 | 20.10 | Paimon 2.1.0 | MySQL CDC source → arbitrary file read | 610/200 | Medium | yes | by-design, will harden |
| 6027 | 20.10 | jclouds 2.7.0 | XXE in provider-response SAX parsing → file read / SSRF | 611 | Medium | no | Attic, no CVE |
| 6028 | 21.10 | AsterixDB 0.9.9 | Unauthenticated CC HTTP API: SQL++ exec + /admin/shutdown | 306/284 | High | no | no response, 0-day |
| 6029 | 22.10 | MiNiFi C++ 1.0.0 | Uncontrolled allocation from wire length → DoS | 789 | Low | yes | by-design, no CVE |
| 6030 | 22.10 | CarbonData 2.3.2 | Index Server Hadoop-IPC Java deserialization → RCE | 502/306 | High | no | no CVE |
| 6031 | 23.10 | Qpid Proton-C 0.40.0 | Unbounded AMQP decoder recursion → stack-overflow DoS | 674 | Medium | no | CVE-2026-105042 (silent fix 0.41.1) |
| 6032 | 24.10 | RocketMQ Client C++ 2.2.0 | RemotingCommand::Decode unchecked headLen → OOB read | 125 | Medium | no | no CVE |
| 6033 | 24.10 | Karaf Cellar 4.4.8 | Insecure Hazelcast default → unauth cluster-join → cross-node RCE | 1188/306/94 | High | no | PMC-confirmed, no CVE |
| 6034 | 26.10 | ActiveMQ-CPP 3.9.5 | OpenWire byte-array unmarshal unbounded resize → DoS | 789 | Medium | no | no CVE (dormant) |
| 6035 | 27.10 | Dubbo Admin 0.6.0 | Hardcoded default JWT signing secret → offline-forged admin bypass | 798/321 | High | no | documented default, no CVE |
| 6036 | 28.10 | Commons Digester 3.2 | XML external entity injection by default | 611 | Medium | yes | by-design, no CVE |
| 6037 | 29.10 | DevLake (incubating) 1.0.3 | Unauthenticated server-side request forgery via connection endpoint | 918/306 | High | no | not reported, 0-day |
| 6038 | 30.10 | Ambari 3.0.0 | Management-pack Zip-Slip → arbitrary file write → RCE (admin) | 22/918 | High | no | CVE requested |
Withdrawn as false positives (not shipped):
Derby LDAP filter injection (the released source escapes correctly; I had tested a stale mirror) and Avro-C
negative-length write (unreachable; real libavro returns ENOMEM, and the "crash" was
AddressSanitizer-only).
Three things the operation made concrete for me. First, AI collapses the discovery timeline: thirty-six confirmed 0-days across hundreds of codebases in five days was not realistic two years ago. But that only holds when it is chained to real-artifact verification, which is also the thing that keeps it honest enough to print retractions. Second, "by design" is carrying an enormous amount of weight in Apache triage, and a threat model that assumes the attacker away is a scoping choice the operator running the default config inherits as risk. Third, the native-C periphery of Apache is under-watched and keeps paying out memory-safety bugs. Whatever the security model says, the code is still vulnerable.