"It is read-only" is the phrase that gets things approved without a meeting. The read-only token for the script. The read-only account for the consultant. The reader role in the tenant, handed out over chat in thirty seconds. It does not write, so it cannot break anything; it cannot break anything, so there is nothing to discuss. On Thursday 10 September GitLab shipped the patch for a flaw that can only read files and gave it a 10.0. Top marks, and they signed the number themselves.
The week ran like this. Thursday the 10th: GitLab ships the fixed versions. Friday the 11th: the security firm watchTowr reports probes against the flaw on internet-facing instances. That same Friday, CISA adds it to its exploited vulnerabilities catalog with a deadline of Monday the 14th. And the record is not published in the national vulnerability database until the early hours of Saturday the 12th, which is where a good share of patching inventories get their data. The reading is ours, but it is hard to dodge: the scanner syncing from the NVD found out after whoever was already trying the door.
We run a self-hosted GitLab ourselves, with the GitOps CI/CD that deploys this very site, so the advisory landed close to home. What follows is not a write-up of our own instance — that stays in house — but the order in which we look at these things when a round number shows up: first what broke, then what the vector says, and last the question the patch does not close.
What broke, with names and numbers
The flaw is CVE-2026-85706 and it affects GitLab Community Edition and Enterprise Edition. The official description says that under certain conditions, an unauthenticated user could have read arbitrary files from the GitLab server due to improper path confinement and missing authentication enforcement in the repository commits API. Two defects, not one: the path was not properly confined and the endpoint did not check who was asking.
The affected versions are everything from 18.7 up to but not including 19.1.8, the 19.2 branch before 19.2.6, and 19.3 before 19.3.2. Those three are the fixed ones, released on 10 September in a bundle carrying eighteen security fixes. It was found from outside, through the bug bounty programme: the CVE references list the HackerOne report and GitLab's own internal ticket. The classification wobbles depending on who fills it in — GitLab, which maintains the CVE record, files it as CWE-22, and CISA's catalog as CWE-35 — and both are variants of the same thing: path traversal.
The full proof of concept is not public, but the indicators to search your logs for have been public since Friday, and they are worth writing down because they change everything that follows. watchTowr published that what to hunt for is HTTP POST requests against paths of the form /api/v4/projects/{id}/repository/commits/ carrying file.path parameters. And it added the precondition that the official description leaves as "under certain conditions": the instance needs at least one public project.
That detail about the method is the best news of the week for whoever has to review. The commits route is queried daily by integrations, badge generators and deployment dashboards, and all of them use GET. A user-less POST against that route looks like nobody's traffic: it is an anomaly with a recognisable shape, and therefore something you can search a log for without being a forensic analyst.
The 10.0 does not come from the flaw. It comes from the vector
A CVSS score always comes from a vector, and this CVE's is signed by [email protected] — the vendor itself. Here it is:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N — 10.0
— CVSS 3.1 vector for CVE-2026-85706 per the CVE record, source [email protected].
The first half is what anyone expects from a serious flaw: attacked over the network (AV:N), nothing for the attacker to set up first (AC:L), no credentials (PR:N), and nobody has to click anything (UI:N). Four zeroes of friction. The second half is the interesting one. C:H is predictable: everything can be read. But sitting next to it is I:H, high integrity impact, on a flaw that writes nothing. And in front of it, S:C: scope changed, which in CVSS vocabulary means the damage leaves the vulnerable component and lands somewhere else.
Anyone can reproduce the arithmetic in the official calculator. Take the same vector and leave it as what a read flaw looks like at first glance: AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N. That scores 7.5. The flaw is the same, the network is the same and the ease is the same. The 2.5 points between one figure and the other describe what is stored inside the files, and the vendor puts them there in the only formal language it has for saying so: on a GitLab server, reading does not stop at reading.
The same patch bundle from the 10th carries a second critical that makes a useful contrast. CVE-2026-87719 scores 9.9: an insecure deserialization in a GraphQL subscription serializer that can yield Advanced Search instance configurations and sensitive credentials. One tenth lower, and yet much further away: its vector carries PR:L and the description requires an authenticated user with Duo Chat access, on top of being Enterprise Edition only. The 9.9 needs somebody inside. The 10.0 needs nobody.
"Read-only" is the permission that gets approved without a meeting
The same reasoning that makes this CVE worth a 10.0 applies, without changing a comma, to half a dozen decisions taken in any company without ever being called decisions. The "read-only" CI token that can clone all hundred and twenty repositories, including the one holding the production .env somebody committed back in 2023. The "read-only" account handed to the consultant for the audit, which sees the entire customer table. The directory reader role, which touches nothing and in exchange shows you the full org chart, the groups and who the administrators are: exactly the map somebody needs in order to write a believable email.
We already wrote about a textbook case in this family: PostgreSQL's REPLICATION privilege, granted because it sounds like copying rather than writing, and which in practice hands over the whole cluster. The pattern is identical, and the defect is in the vocabulary before it is in the software. What makes a permission dangerous is not the verb, it is the scope. "Read" describes the action; nobody asks about the object.
We are not saying reading and writing are the same thing; that would be silly and it is also false. We are saying something more uncomfortable and more useful: in any system that stores credentials in order to talk to other systems, the distance between reading and writing is a copy and paste. And a self-hosted code server is, by definition, a system that stores credentials in order to talk to other systems. That is its job. Which is why that S:C in the vector is literal, not rhetorical.
CISA did not just ask for a patch. It asked you to look back
CISA's exploited vulnerabilities catalog is a public JSON file, and it is worth downloading rather than reading what others say about it, because each entry carries fields the press coverage tends not to look at. The GitLab entry went in on 11 September with a deadline of the 14th — three days — and with a field set to "Yes" called forensicTriage.
We counted how many entries carry that flag, because the number says more than the label: 51 out of 1,709. Three per cent of the catalog. And the deadline is not everyone's either: that same 11 September, two JFrog Artifactory flaws went in with fourteen days and no triage flag. Same day, same agency, nearly five times the runway. That this is not about the vendor is shown by the day's third addition: ConnectWise ScreenConnect, also with three days and also flagged. The catalog is drawing a distinction, entry by entry, between "get current" and "get current and also check whether it already happened". The second one is expensive, because no apt upgrade solves it.
That directive binds US federal agencies and nobody else; certainly not a company in Sant Fruitós de Bages. No Spanish SME is in breach of anything for not having patched on Sunday. But the prioritisation criterion is public and free, and the criterion says this flaw belongs to the category you have to investigate backwards. The entry's own required action includes a sentence that does apply to everyone, and we agree with it: stakeholders are responsible for evaluating each asset's internet exposure. Evaluating. Not assuming.
"Were we read?" has an answer, and the file has been there all along
An HTTP read flaw leaves a trace. Not the trace an antivirus looks for, since there is no new process and no dropped binary, but a line in a log. On an installation using the official GitLab package, API calls are written to /var/log/gitlab/gitlab-rails/api_json.log, one JSON line per request, carrying these fields among others: time, status, method, path, params, remote_ip, route, user_id and username. Next to it, the web server keeps its own log at /var/log/gitlab/nginx/gitlab_access.log.
And there is a second piece of good news, this one by default: the rotation shipped with the package is daily, keeps 30 rotations and compresses. Translated: if nobody has touched that configuration, you have roughly a month of history on disk, which comfortably covers 10 and 11 September. Nothing needs buying to answer the question. The file needs opening.
Because the route is stored normalised with the /api/:version/... pattern, you can search on the endpoint fragment without knowing any project's ID. With watchTowr's indicators — POST method and file.path parameter — the sweep narrows down considerably. Rotated logs included:
# 1. lo que watchTowr da como indicador: POST + file.path contra la ruta de commits
zcat -f /var/log/gitlab/gitlab-rails/api_json.log* \
| jq -r 'select((.route // "") | test("repository/commits"))
| select(.method == "POST")
| select((.params // [] | tostring) | test("file\\.?_?path"; "i"))
| [.time, .remote_ip, .status, (.username // "-")] | @tsv'
# 2. quien pregunta por esa ruta sin usuario, agrupado por IP para ver el volumen
zcat -f /var/log/gitlab/gitlab-rails/api_json.log* \
| jq -r 'select((.route // "") | test("repository/commits"))
| select(.user_id == null)
| .remote_ip' \
| sort | uniq -c | sort -rn | head -20
The first command looks for the attempt in the shape watchTowr describes; the second draws the background, meaning which addresses ask for that route with no user attached, and at what volume. Three caveats before anybody draws conclusions. If your GitLab sits behind a proxy or a load balancer, remote_ip may be the proxy's and you need the forwarded header. There are legitimately anonymous requests against public projects, so the absence of a user, on its own, accuses nobody. And if running the command tells you that you only have seven days of history because somebody trimmed the rotation two years ago, that is your finding for today, ahead of the CVE.
And if that search turns something up, the conversation stops being technical and becomes one about deadlines: two days ago we wrote about the Cyber Resilience Act's twenty-four hours and the exact moment the clock starts. It starts when you find out, not when the CVE is published. Which is why the order matters: you look first, and you decide with what you find. It does not work the other way round.
When we would do nothing tonight
A 10.0 does not mean "they are getting in today". It means that trying it takes no credentials at all, which is what PR:N says; it is a measure of ease, and the likelihood comes from your exposure. Two recommendations follow that sound contradictory and are not. If your GitLab does not answer from the internet — and you have checked that, rather than assuming it — do not stand up an emergency window on a Sunday night for a 19.1.x. Upgrading GitLab in a rush over a weekend, with no backup taken and nobody on call, has its own way of ruining your Monday. An orderly window, a backup first, and ideally somebody watching.
That said, two caveats. First: the backup taken beforehand has to be a real backup, and we already explained why cloning the repositories is not a GitLab backup — it leaves out the issues, the merge requests, the logs and, very much in particular, the secrets file without which the database you restore cannot be decrypted. Second: even if you are not exposed, read the log anyway. A read from inside is still a read, and the uncomfortable part of a compromise is not the one happening now, it is the one that happened three weeks ago and nobody looked at. It is the same lesson we drew from the Cisco FMC incident: the patch closes the door, but it does not tell you what left through it beforehand.
We have been saying it here for years in other words: failure is inevitable, an outage is a design decision. That a CVSS 10.0 exists in the software you use is neither your fault nor something you can prevent. That you cannot answer "were we read?" is a decision, and it was taken the day somebody decided how much history to keep and who looks at it.
Who reads your logs on the day it matters?
Scope first: an agent on the server does not answer this one, because there is no odd process and no new binary, there is a POST against the commits API. What answers it is the log kept long enough and somebody who knows what to look for in it. That is the response half of our managed EDR/MDR: retention, correlation and a person on call. Alongside it sits Zero Trust, which is what makes a "read-only" genuinely mean very little. If on review it turns out you have it well set up, we will tell you so and there will be no invoice.
Talk to everyWANNote on sources
Primary sources we read ourselves. The CVE-2026-85706 record in NIST's national vulnerability database: source of the quoted description, the affected and fixed version ranges (18.7 < 19.1.8, 19.2 < 19.2.6, 19.3 < 19.3.2), the CWE-22 classification and the vector — both supplied by [email protected] as the record owner, since NIST has not analysed it yet: the entry is in «Received» state — the record's publication timestamp (12 September 2026, 03:16 UTC) and the vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N scoring 10.0, attributed to [email protected]; its references include the HackerOne report and GitLab's work item. GitLab's 19.3.2 release note of 10 September 2026, for the bundle of eighteen fixes and for the second critical, CVE-2026-87719 (9.9, Enterprise Edition only), whose PR:L vector we also checked against the NIST record. CISA's Known Exploited Vulnerabilities catalog, downloaded as JSON (snapshot of 11 September 2026, 19:32 UTC, 1,709 entries): source of the date added (11 September), the due date (14 September), the forensicTriage field set to "Yes", the required action text and the JFrog Artifactory entries from the same day with a fourteen-day deadline. GitLab's administration documentation for the paths and fields of api_json.log and gitlab_access.log, and its Linux package documentation for the default rotation values (daily, 30 rotations, compressed). watchTowr's 11 September write-up, from which we take both the probes observed and its hunting guidance — POST requests to /api/v4/projects/{id}/repository/commits/ paths carrying file.path parameters — plus the precondition that at least one public project exists on the instance; the same guidance is carried by SecurityWeek and The Hacker News that day. That ConnectWise ScreenConnect (CVE-2026-84869) was added on the 11th with three days and a forensic triage flag comes from the same CISA JSON.
What is our count or our reading rather than the sources'. The count of 51 entries flagged for forensic triage out of 1,709 is ours, computed over the downloaded JSON; it changes every time CISA publishes. The comparison between 7.5 and 10.0 is our own arithmetic using the standard CVSS 3.1 formula: the hypothetical vector AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N scores 7.5, and anyone can reproduce it in FIRST's official calculator. Reading S:C and I:H as a vendor statement about what the files contain is our interpretation, not a GitLab declaration. Also ours: the vocabulary thesis ("what makes a permission dangerous is not the verb, it is the scope"), reading the three-day deadline against the fourteen-day one as a distinction between patching and investigating, the observation that a scanner syncing from the NVD finds out after whoever is already probing, the point that a user-less POST against the commits route is an anomaly next to the GET traffic of integrations, the point that a 10.0 measures ease rather than likelihood, and the arithmetic of "roughly a month of history" from the thirty documented daily rotation cycles, and the zcat and jq command — a starting point, not a detection rule — along with its three caveats. The read-only permission examples are illustrative and describe no specific customer. That we run a self-hosted GitLab with our own CI/CD is ours. Everything was checked on 13 September 2026.
Cover photograph: "Documents on repository shelving at The National Archives", by The National Archives (United Kingdom), published on Wikimedia Commons under a Creative Commons CC BY 3.0 licence. We cropped it and overlaid our brand text.