The October 19 post on v28.2.0 did most of its work correctly: the release notes still name no CVEs, the analysis of the seven advisories it counted is still right, and the thesis — release notes are front-page material and the accounting lives elsewhere — was correct. The counts were wrong. Not by opinion; by the page numbers in the project's own advisory feed.
What the feed actually says¶
The security advisories API for go-gitea/gitea returns each advisory with a machine-readable published_at. Counting every advisory published between October 6 and October 18 inclusive:
| Published | Count | Severity mix | Fix versions | |---|---|---|---| | Oct 6, 19:16–21:11 UTC | 29 | 5 critical, 11 high, 13 medium | 20 in 28.0.0, 9 in 28.1.0 | | Oct 18, 18:44 UTC | 12 | 2 high, 7 medium, 3 low | 9 in 28.2.0, 1 in 28.1.0, 1 in 1.27.0, 1 in 1.27.3 | | Total, 13 days | 41 | 5 critical, 13 high, 23 medium/low | — |
The October 6 "batch of three" was twenty-nine advisories, published under one maintainer's name across two transactions one hour fifty-five minutes apart. Twenty of them trace their patched version to 28.0.0, which shipped September 30 — meaning the fixes predate the advisory dates and the disclosure was held, which is consistent with what Gitea's 28.0.0 blog said would happen ("details withheld for a week"), and which means the 1.27-line patch for none of them exists. The nine remaining trace to 28.1.0.
On the eighteenth: twelve advisories, published between 18:44:34 and 18:44:45 UTC — eleven seconds apart. The seven the October 19 post counted are real. The five it did not count:
| GHSA | CVE | Severity | What an attacker gets | Affected → Patched |
|---|---|---|---|---|
| GHSA-vcp6-2qc6-gvq3 | CVE-2026-109553 | High (7.5) | SSH-cert principal cross-authentication — see below | ≤ 28.1.0 → 28.2.0 |
| GHSA-mf69-7pqq-86vx | CVE-2026-104627 | High | OpenID account-linking state bypass — see below | ≤ 28.1.0 → 28.2.0 |
| GHSA-c3cr-qq9g-666c | CVE-2026-105661 | Moderate | Unauthenticated nil-pointer panic on /{username}/-/repositories for individual users | ≤ 28.1.0 → 28.2.0 |
| GHSA-xmfc-hqq8-33wh | CVE-2026-110324 | Moderate | POST to a private/limited user's profile returns metadata the requester cannot normally see | ≥ 1.22.0, ≤ 28.0.0 → 28.1.0 |
| GHSA-xvfh-qwmh-f92w | CVE-2026-97859 | Low | Seven users/{username} API routes (followers, following, starred, watched, SSH/GPG keys) bypass the visibility check; GPG keys often embed real names and emails | ≤ 1.27.2 → 1.27.3 |
Four of these five were also missed in the other direction by the release's blog coverage elsewhere: the 28.2.0 release post lists twelve CVEs in its Security section — one to one with the twelve advisories — plus a thirteenth, CVE-2026-78659, in its Dependencies section (a golang.org/x/net bump for HTTP/2 trailer-header memory exhaustion). Third-party coverage of this release has counted 13 (some counting the dependency CVE), 12 (the Security section), and 7 (this site, reading the advisories web list). The one count that can be reproduced from a machine-readable source is the API's: twelve advisories, twelve CVE IDs, one dependency CVE. The blog's Security section turned out to be the complete per-release accounting here — the exact inverse of what this site published four days after it, and of what this site said about that pattern in September. Read both; count from the API.
The two Highs¶
CVE-2026-109553 is the one worth re-patching your runbook for. Summary from the advisory: a trusted user CA is a normal enterprise and community-organization configuration (SSH_TRUSTED_USER_CA_KEYS). Gitea's verification of certificates issued by that CA looked up each of the certificate's principals among all stored public keys by content, with no filter on key type. Any user can read the wire format of another user's public key from /{user}.keys. A user who registers the victim's key line as their own principal — or who holds the CA's private key outright — walks away with a certificate that authenticates as the victim over HTTP Signatures on the API, and the advisory states the builtin SSH certificate path shares the same content lookup, which puts git-over-SSH in the same class. Audit logs of the attacker's API actions attribute them to the victim. The fix is a type-filtered lookup (type = KeyTypePrincipal), and the reporter — CodeBuddy Security of Tencent Yunding Lab — confirmed the issue live on 1.27.3 from docker.gitea.com/gitea:latest.
Read that last sentence with the October 19 exposure advice in hand: a 1.27.x pinned instance is confirmed vulnerable to this one. The patched version is 28.2.0. No 1.27.x backport exists in the feed as of November 1, and no 1.27.4 exists either. If you run a trusted user CA, the SSH certificate path on 1.27.x is a live cross-authentication path, not a scorecard entry.
CVE-2026-104627 is the other High, and it is the one that bites every OpenID Connect instance. The OpenID association callback in the security settings did not verify that the logged-in user actually started the linking flow. An unsolicited OpenID assertion from a provider the attacker controls could attach the attacker's identity to a victim's account, after which the attacker signs in as the victim through that identity. The fix binds a short-lived state value to the user. Any community forge that lets users link SSO accounts is in scope, and — as with the SSH advisory — the affected range is ≤ 28.1.0 with no 1.27.x backport named.
The counting method was the bug¶
Here is the part worth keeping after the numbers go stale. The GitHub advisories web list for gitea is paginated, and its ordering is not the publish date. This site verified it while working the correction:
- Page 1 mixes the seven October 18 advisories the site counted with three October 6 ones.
- Pages 1–3 of the list contain 10 of the 12 October 18 advisories and 19 of the 29 October 6 ones.
- CVE-2026-109553 — the High — is item two of page 3, under three pages of October 6 items in the first two pages.
- CVE-2026-104627 — the other High — and CVE-2026-97859, the only one fixed in 1.27.3, are not in the first three pages at all. Neither is CVE-2026-95106: a Critical, and the ugliest of the batch — "Duplicate tree-entry name: Gitea renders the benign blob while checkout and CI run the malicious one."
That last one deserves its own sentence because it is the one that defeats review: the web UI and the CI checkout disagree about what a tree says, and Gitea executes the one the reviewer did not see. The October 6 Actions cluster — the fork-PR runner RCE via cancel-then-rerun, cancelled jobs revived by later approval, the approval bypass on maintainer-triggered sub-events, the unapproved fork PR killing trusted runs by concurrency — is the same class Forgejo closed on October 26: review triggers and PR workflows running trusted logic from untrusted input. Two community forges, same month, same lesson, and the site undercounted both ends of it.
The command that replaces the method, tested unauthenticated tonight:
sh
curl -s 'https://api.github.com/repos/go-gitea/gitea/security-advisories?per_page=100' \
| jq -r '.[] | [.published_at, .cve_id, .severity, .ghsa_id] | @tsv'
Each object also carries vulnerabilities[] with vulnerable_version_range and patched_versions — which is where the exposure table below comes from. Pagination is cursor-based through the Link header (rel="next", the after= parameter), not web page numbers. The endpoint answers in one call for a hundred advisories; the web list spreads forty-one across more than four pages in an order that has to do with severity clusters, not dates.
The corrected exposure math¶
Running 28.x: the October 19 advice stands — 28.1.0 → 28.2.0 — and the two Highs above are newly added reasons. The API severity labels make one more thing visible than the release-day reading did: the eighteenth's batch has two Highs, not the distribution of all-Moderate the page-1 reading suggested.
Running 1.27.x: the October 19 post said "exposed to nine of the ten advisories since October 6." The API says: of the forty-one published October 6–18, exactly two carry a 1.27.x fix — CVE-2026-109844 (patched in 1.27.0, which is itself the reason the advisory said "disclosed with this release") and CVE-2026-97859 (patched in 1.27.3). Every other advisory's range includes the 1.27 line: exposed to 39 of 41, with at least the 1.27.x build vulnerable to CVE-2026-109553 by the reporter's own verification. The options from the October 19 post are unchanged in shape and updated in urgency: migrate to 28.2.0 and budget the configuration review the 28.0 jump actually requires, or hold at 1.27.3 and cut exposure deliberately — and if you hold, add the trusted-user-CA question to the audit list, because that configuration's certificate path is confirmed-unpatched.
Everywhere: the October 19 post's closing line — "subscribe to the advisories feed, not the blog" — survives this correction, but the implementation detail underneath it died with the discovery above. The feed that survives a non-date sort is the API, not the web list. Counting from a paginated web list is a manual method, and manual methods fail quietly exactly the way this one did: the numbers were plausible, the page was real, and four advisories — including the only High of its day — were one HTTP call and one page number away the whole time.
Credit where due on the method that caught this: the reproducible count discrepancy in third-party coverage of 28.2.0 was the visible symptom — thirteen versus seven — and this site's own ledger series (the Keycloak correction among them) is the reason the discrepancy was worth chasing. The numbers above are reproducible in one command. That is the standard the correction was built to.