Tech12 min read

Probes arrived 10 minutes after the cohttp path traversal fix PR went public

IkesanContents

TL;DR

What happened OSEC-2026-16 (CWE-22). Cohttp.Path.resolve_local_file resolves outside the docroot on every cohttp release before 6.3.0

What to do Upgrade to cohttp 6.3.0. Servers that gate on path segments must run Request.uri through the new Cohttp.Path.normalise before splitting it

Until you can upgrade Normalise percent-encoded path separators in the request URL, so ..%2f never survives to the decode step


Anil Madhavapeddy, a maintainer of the OCaml HTTP library cohttp, published a note on August 22, 2026. About ten minutes after he opened the fix PR, his own webserver logs filled with probes for the exact bug pattern.

The title is “Just a rumour of a bug is enough to find a security exploit these days”, and the argument is that a rumour of a bug is all the attacking side needs.

A security embargo means holding the details back for a while, building the fix, shipping it to affected users first, and only then going public. It works because an attacker without the details cannot reach the same bug on their own. In other words, secrecy is what buys the time. That is what broke here.

What happened in cohttp

The affected function is Cohttp.Path.resolve_local_file, which builds a path on disk out of the request URI. Here is the original code, as quoted in the OCaml security advisory OSEC-2026-16.

let path = Uri.(pct_decode (path (resolve "http" (of_string "/") uri))) in

resolve normalises the path, and only then does pct_decode percent-decode it. The order is backwards. Before decoding, %2f is just an ordinary string, so it slips past the pass that removes . and .. segments. It turns back into / after that pass is done.

/static/..%2f..%2f..%2fetc/passwd

This request goes through normalisation as a single encoded segment and then becomes /static/../../../etc/passwd. A file outside the docroot becomes readable. It is a CWE-22 path traversal, and it affects every cohttp release before 6.3.0.

The fix percent-decodes exactly once, before . and .. are dropped. Cohttp.Path.normalise was added alongside it. It converts a URI into a relative path that cannot climb above its root, and servers that make access control decisions on path segments should run Request.uri through it instead of splitting the URI directly. It is not applied by default, though, because existing code may depend on the current semantics.

The advisory timeline and the PR record cover the path from report to release. Dates follow the OSEC-2026-16 timeline, and anything with a timestamp is given as its UTC calendar day.

DateEvent
August 11, 2026Report reaches security@ocaml.org
August 14, 2026Fix PR #1145 opened. Probes arrive about ten minutes later, per Madhavapeddy’s observation
August 20, 2026PR merged, v6.3.0 and OSEC-2026-16 published
August 22, 2026Landing in opam-repository announced on discuss.ocaml.org

The probes were percent-encoded traversal sequences, exactly the pattern in the patch. Madhavapeddy reads this as evidence that automated watchers are keeping an eye on public repositories.

He had also pointed his own agent at the affected code before opening the PR, asking what else was lurking around path normalisation. Claude Fable refused outright on its security block, because he has no access to Project Glasswing. DeepSeek V4 Pro obliged instead, independently turned up several related issues, and built an exploit that probed a local live server in under a minute.

All the agent needed was a rough direction. Madhavapeddy lists the ways that direction leaks besides a public PR: a mailing list question, an odd commit in an orphan branch, a context leak from a conversation log. Any one of them is enough to set someone else’s agent moving.

graph TD
    A[Public fix PR] --> E[Attacker's agent]
    B[Mailing list question] --> E
    C[Odd commit in an orphan branch] --> E
    D[Context leak from a conversation log] --> E
    E --> F[Locate the affected code]
    F --> G[Generate an exploit]
    G --> H[Probe public hosts]

With the CVE description, and without it

Whether a direction alone is enough was measured by Fang et al. in 2024. They collected 15 one-day vulnerabilities, including ones rated critical, and pointed a GPT-4 agent at them. Given the CVE description, it exploited 87% of them. Take the description away and that drops to 7%. Every other model, plus ZAP and Metasploit, scored 0%.

At the time this read as evidence that withholding the description is protection enough, and embargoes ran on that premise. The cohttp case is a much coarser description, though. Once the PR title and diff are public, the affected library, the function, and the bug class are all legible at once.

Time to exploit

Mandiant’s time-to-exploit metric is an average that counts exploitation on either side of the patch.

PeriodMean time to exploit
2018-201963 days
2020 to early 202144 days
2021-202232 days
20235 days
M-Trends 2026 (2025 investigations)An estimated -7 days

The 5 days for 2023 is an average with 15 outliers removed; leaving them in gives 47 days.

The -7 days sits in the edge devices and zero-days section of M-Trends 2026, with the note that exploitation now routinely happens before a patch is even released. The report as a whole draws on more than 500,000 hours of incident investigations conducted in 2025. How the -7 days itself was computed is not on the public page, and nothing there separates out an LLM contribution.

Sysdig’s honeypots have two cases where the gap from advisory to first exploitation is recorded with timestamps.

CaseAdvisory publishedFirst exploitation observedGap
Langflow CVE-2026-33017March 17, 2026, 20:05 UTCMarch 18, 16:04 UTCAbout 20 hours
marimo CVE-2026-39987April 8, 2026, 21:50 UTCApril 9, 07:31 UTC9 hours 41 minutes

Neither had public PoC code at the time. The advisory text was the only handle.

marimo is CVSS v4.0 9.3. The /terminal/ws WebSocket endpoint skipped authentication validation entirely, while the other endpoints call validate_auth() first. The advisory states the endpoint path and the missing check, which is all an exploit needs. The attacker connected at 07:31 and confirmed execution in 9 seconds, moved to what looks like manual reconnaissance at 07:33, and in a third session starting 07:43 read .env at 07:44 and left at 07:45. The “under 3 minutes” in Sysdig’s writeup refers to that third session.

marimo has roughly 20,000 GitHub stars, small next to Langflow’s 145,000+ or n8n’s 75,000+, and the first exploitation still came 9 hours 41 minutes in. Sysdig suggests this speed points to threat actors watching advisory feeds broadly rather than only high-profile targets. That said, the same writeup notes that the post-access exploration looked manual rather than AI-driven.

Lining that up against the statistics

VulnCheck’s 1H-2026 report, which is the source Madhavapeddy cites for his chart, does not show that AI-assisted discoveries are any more likely to be exploited.

In the first half of 2026, 495 vulnerabilities were confirmed as exploited, and 23.43% of them had evidence of exploitation on or before the CVE publication date. That is down from 28.93% in 2025.

The median time from CVE publication to landing in the known exploited vulnerabilities list (KEV) fell from 120 days to 80. On that measure alone, things are accelerating. On the other hand, about 200 CVEs reached exploitation within 31 days of publication, roughly matching 196 in 2024 and 194 in 2025. CVE issuance grew 45% over the same window, so as a proportion it went down.

Of 1,061 vulnerabilities attributed to AI-assisted discovery, only 14, or 1.3%, were confirmed as exploited in the wild. That is about the same rate as everything else. The volume of agent-found bugs keeps climbing, with reports like 21 zero-days in FFmpeg going as far as working PoCs, and the share that reaches confirmed exploitation still has not moved.

For Project Glasswing, as of the July 28, 2026 report, Anthropic claimed 23,019 findings, 126 published CVEs, and exactly one confirmed exploited in the wild, CVE-2026-26980. VulnCheck also criticised the disclosure ledger for not growing past the 1,611 entries it launched with in May.

That dashboard has since been updated, on August 26. Discovered and triaged is now 26,153 findings, of which 5,008 are candidates for review by external security firms, and 2,300 vulnerabilities across 392 open source projects have been disclosed. 421 have been patched upstream. The identifiers issued are 177 CVE records and 285 GitHub Security Advisories, and a single finding can carry both. There is no exploitation count on the page.

The cohttp probes arriving in 10 minutes and the statistics showing no AI-driven increase are not counting the same thing. The probes hit one open source project whose public repository is being watched, while VulnCheck counts CVEs at large. Even with thousands of zero-days coming out of Glasswing, confirmed exploitation so far amounts to a single case.

Where maintainers get stuck

A paper by Pesoli et al. from May 2026 treats this as bugonomics, the operational economics of finding, proving, and fixing security defects. Candidate generation, code comprehension, harness construction, proof-of-impact drafting, and report preparation all got cheaper with LLMs. The jam is downstream of that: the capacity to absorb, validate, prioritise, patch, and ship the larger stream of reports does not scale on its own. The paper frames this as defender remediation throughput.

Writing a security patch that does not break existing behaviour is also, simply, a lot of work.

Project Glasswing added about 150 organisations in June 2026 on top of the initial 50 or so, and now spans more than 15 countries. Each organisation has to meet security requirements before getting access. Even so, it has not reached small maintainers, as Madhavapeddy points out; the Western commercial models have security guards that make them unusable for this, and Fable refusing during the cohttp investigation is one example. The attacking side is under no such constraint, which came up in the same form when Fable 5 and Mythos 5 were suspended entirely.

Three options and what snags each one

Madhavapeddy lays out developing the fix somewhere fully private, dropping embargoes and shipping continuously, and blocking at the protocol layer.

OptionConcrete mechanismWhat snags it
Develop the fix privatelyGitHub temporary private forksIntegrations including CI cannot reach it. Only one PR can merge into main
Ship continuouslyWeekly or faster security releasesLibraries reach users embedded in downstream products, so speeding up upstream alone changes nothing at the endpoint
Block at the protocol layerDistributing virtual patchesOpen source has no distribution path outside a commercial CDN

The temporary private fork limits are spelled out in GitHub’s own documentation. To keep vulnerability information secure, integrations including CI cannot access these forks, and status checks do not run on the PRs. Adding collaborators means someone with permission registering a user or a team one at a time. In open source, where reviewers turn up whenever they have a spare hour, that registration has to happen first.

The part that this does not fix is getting the description of the bug to exactly the right people without leaking it to attackers. Open source discussion is spread across end-to-end encrypted places and leaky shared infrastructure like Discord or Slack. Madhavapeddy’s own projects use Matrix.

On continuous shipping, Chrome is moving major milestones to a two-week cadence with weekly security updates, and is piloting a shift to two security releases per week. Dynamic patching, which swaps background processes for updated binaries without a restart, has been announced but is still in research and development. The Linux kernel releases fixes for publicly known bugs immediately, and defers undisclosed ones by at most 7 calendar days from the start of the release process, with an exceptional extension to 14.

Chrome ships one binary, while open source gets repackaged by downstream distributions on their own terms.

The third option, virtual patching, fits the cohttp bug in a single rule: normalise percent-encoded path separators in the request URL. That rule was writable the day the report arrived on August 11, and deployable while review, testing, and packaging ran their course. The managed rules Cloudflare pushed during Log4Shell went in the same way, ahead of the upstream fix.

That is inside a commercial CDN, though, and open source has no path of its own for distributing such rules.

Madhavapeddy has posted an MPhil project at Cambridge around this: put a MirageOS gateway in front of a home network and investigate whether a set of mitigation rules can be made trustworthy enough to deploy automatically. There is also a capture-the-flag idea attached, handing the same rumour to an attacking agent and a defending one to see which gets there first.

References