Four days ago this site covered Forgejo shipping a fix for a review-trust hole across 17.0.1, 16.0.7 and 15.0.11 — the commits landed October 26: a pull_request_review event whose commit_id the platform never validated, so a review on a trusted pull request could point Actions at an arbitrary commit and run its workflow as trusted. Forgejo's fix aligned forgejo.ref with refs/pull/<n>/head, the documented GitHub behavior. Gitea has now closed its own version of the same bug in the same shape, six days after Forgejo's, in 28.3.0, published November 1. Below: the two lead advisories with the ranges this site pulled directly, the full twenty-one-entry count from the release blog, what the token-scope family actually allowed, and the checklist.
The cadence and the ledger¶
The release feed now reads: 28.0.0 September 29 23:25 UTC, 28.1.0 October 6, 28.2.0 October 18, 28.3.0 November 1. Four releases with advisory batches in thirty-four days. The correction post rebuilt the first two batches from the advisories API: twenty-nine published October 6, twelve on October 18, thirty-nine of the forty-one carrying no 1.27.x fix. 28.3.0's blog Security section carries twenty-one CVEs; the advisories went up alongside the release — page one of the advisories list shows four publications dated November 1 (three Highs, one Moderate) — and one entry links to the Go project itself. The release blog, for once, names the theme in one sentence, and it is on theme: "a large number of security fixes, mostly around token scopes."
| Group | Count | Entries | One line each |
| --- | --- | --- | --- |
| Public-only token enforcement | 9 | CVE-2026-117635 (admin API), -119441 (org repo deletion), -117685 (OCI blob mount), -118394 (OIDC userinfo), -119627 (limited-visibility profiles, shared fix), -116289 (org runner registration), -117646 (profile tabs), -118395 (deprecated org route), -114199 (heatmap) | One fix batch (#39690/#39853) enforced the token restriction across routes that never checked it |
| Profile/feed/token second batch | 4 | CVE-2026-119625 (Basic-auth token scope), -119627 (shared), -112887 (Actions task token unscoped), -116474 (GPG key on system user) | #39945/#40115 |
| Archived repository write protection | 5 | CVE-2026-116296 (releases/milestones/labels), -117649 (linked private issues in timeline), -119533 (project boards expose private PRs), -118504 (remaining issue routes — completes 28.2.0's block), -117677 (Helm chart upload zip-inflation DoS) | #39942/#40085 |
| Legacy artifact upload | 1 | CVE-2026-113429 (stored XSS: Content-Encoding stored unchecked and served back as Content-Type on the download) | #39855/#39996 |
| The two lead trust bugs + toolchain | 3 | CVE-2026-118386 (review commit_id), -120263 (OAuth2 auto-linking takeover), -111943 (Go 1.27.3 rebuild / eMudhra) | Separate fixes |
*CVE-2026-119627 appears in both fix batches and is counted once.
The theme in the advisory titles repeats the October pattern the correction post documented: the failure mode is almost always a check that exists in one code path and not its twin — the API checks the lock, the two web handlers never did; the route checks the scope, the deprecated alias of the route did not; the artifact endpoint validates, the legacy one stores an unvalidated header. On a single-binary instance where web and API share one process, that is not a taxonomy footnote; the un-checked half of each pair is the one a browser reaches.
The trust hole Forgejo closed first¶
GHSa-32h8-5xqg-h466 — CVE-2026-118386, High, CVSS 3.1 8.1 (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N), affected ≥ 1.19.0 (January 2023), ≤ 28.2.0, patched in 28.3.0. This is the same bug class as Forgejo's October 26 fix, and the advisory's own lineage note explains why it survived its first fix: the reviewer-controlled ref dates back to the commit that introduced Actions in 2023, and an earlier hardening (#39005, "enforce fork pull request trust boundaries") attached the pull request to review runs — which made fork restrictions apply to fork pull requests — but kept the reviewer-supplied ref as the thing the workflow files were read from. The remaining half is exactly the Forgejo case: on a pull request whose head branch lives in the repository itself (how maintainers usually work — open or merged, either works), a user with read access posts a COMMENT review naming their own commit as commit_id, delivering that commit into the base repository's object store via a fork pull request or, without any fork, a read-access push to refs/for/main/<topic>. The run is inserted with IsForkPullRequest = false: no approval, the repository's and the owner's secrets in the job, a GITEA_TOKEN with write access. The advisory documents a user-level secret delivered alongside the repository secret and notes organization secrets load through the same owner scope. One sentence every instance admin should read twice: disabling forks is not a workaround.
The fix moves review-triggered workflows to the pull request head — the same refs/pull/<n>/head alignment Forgejo shipped on October 26 — and the release carries a compatibility entry next to it: "(pulls) Tolerate invalid stored review commit_id" (#39849). Your database contains rows written by the old code; the upgrade path expects them.
Two attribution facts worth keeping in the notebook. The advisory's footer reads: "Reported by the Anthropic OSS scanner, finding ANT-2026-KORF2CDO (scan gitea-20261022-004)" — found in a scan dated October 22, patched in a release ten days later. And the second lead advisory comes from the same shop, which is where its clock becomes the story.
The account takeover reported in April¶
GHSA-8p3x-w464-4g6v — CVE-2026-120263, High, affected ≤ 28.2.0. With ACCOUNT_LINKING = auto — a non-default, documented configuration — the link-account sign-up form (/user/link_account_signup) was the takeover surface: a user signed in through any configured OAuth2 provider typed an existing account's username or email into the form, and the auto-link branch looked the account up by that attacker-controlled field, linked the external identity to it, and signed the attacker in — no password, no victim credential involved. The linked external_login_user row persists across password changes: a permanent backdoor re-authenticated by OAuth.
The advisory's most useful sentence is the one that kills the false comfort. The documented Gitea warning about ACCOUNT_LINKING describes a provider-trust problem — if your IdP asserts someone else's username, Gitea links it. This bug is different: "the IdP's assertions are irrelevant. Even a fully trusted corporate OIDC that verifies all claims provides no protection," because the lookup key came from the HTTP form, not the IdP. A fully patched identity provider, verified claims, signed assertions — none of it mattered. The fix (per the same advisory, implemented in #40121/#40137) derives the auto-link lookup from the IdP assertion instead of the form, and Gitea now "only auto-links the account that the external identity's email or user name points to."
Two details that make this configuration worth a config file review even on instances that think they are clean:
- WebAuthn-only accounts get no second factor in this path. The advisory documents that
oauth2LinkAccountchecks TOTP enrollment only; a victim configured with WebAuthn but no TOTP is signed in without any prompt. The correct cross-check function exists elsewhere in the same codebase. - The disclosure clock ran seven months. The advisory states the report was originally submitted privately to
security@gitea.ioand resubmitted through GitHub Security Advisories at the maintainers' request — and that for disclosure-timeline purposes, the 90-day period began on April 7, 2026. The fix shipped November 1: roughly 210 days against a 90-day policy, with "a 14-day extension may be available" as the only allowance the policy grants. This site does not know where the months went — the resubmission mechanics are not public — but the window between private report and public fix is on the record in Gitea's own advisory, it is an account takeover in a feature flag community forges turn on for smooth SSO onboarding, and it is the longest private-report window in the Gitea ledger this site has counted.
For this site's audience — community forges with open registration and volunteer-run SSO — the configuration check is one line: look up ACCOUNT_LINKING under the [oauth2] section of app.ini. The default is login; auto was the exposure, and the window between the private report and the fix runs from April to November. If you ran auto with open registrations at any point this year, the audit question is who created OAuth2-linked identities in that window — the external_login_user table maps identity to account, and the takeover's trail is in it.
The public-only token was mostly advisory¶
The token-scope theme. Gitea lets an operator issue a token restricted to public-only access — the class you hand to read-only integrations, dashboards, CI systems you do not fully trust. Ten of the twenty-one entries describe that class not being enforced on the routes that mattered — seven enforced by one fix batch, the feed-and-profile family by a second:
| Entry | What a public-only token could do |
| --- | --- |
| CVE-2026-117635 | A site administrator's public-only token: list the email addresses of all users, create private organizations, create runner registration tokens, create new administrators |
| CVE-2026-119441 | DELETE /api/v1/orgs/{org}/repos as an organization owner: delete all repositories of the organization, private included |
| CVE-2026-116289 | With write:organization: create an organization runner registration token — a runner registered with it then receives jobs including their secrets, from the organization's private repositories |
| CVE-2026-117685 | OCI cross-repository blob mount checked the source blob against the user's full permissions instead of the token's: mount a private blob into a public package and read it |
| CVE-2026-118394 | OIDC userinfo endpoint, token sent as a query parameter instead of a header: private organizations and teams in the groups claim |
| CVE-2026-119625 | /{username} with a token over HTTP Basic, scope never checked: private activity via the RSS/Atom feeds; public-only sees private repositories and organizations on profile pages |
| CVE-2026-119627 | Web profile pages and feeds of users and organizations with limited visibility, shown to public-only tokens |
| CVE-2026-117646 | Repositories/stars/watching profile tabs and the organization home page, showing private repositories |
| CVE-2026-118395 | The deprecated POST /api/v1/org/{org}/repos route: create private repositories |
| CVE-2026-114199 | Contribution heatmap counted private-repository contributions |
The pattern is one enforcement commit covering the API routes plus one covering the token-identity half, which means the surface was fixed the way it was broken: everywhere at once. For operators the actionable part is not the code, it is the inventory: every public-only token you issued operated at its holder's full effective permissions on these routes for as long as it existed. If a public-only token of an org owner or an admin was embedded in a read-only-looking integration, its holder's real reach — runner registration with secret receipt, and on admin tokens, administrator creation — is the honest blast radius. Rotate them; the log lines for the specific routes (DELETE /api/v1/orgs/{org}/repos, runner registration, /admin) are where the history lives.
The Actions token class¶
Two more entries, both about what a workflow's token is allowed to be:
- CVE-2026-112887 (Moderate): Actions task tokens carried no scope at all, and repository filters treated the negative ID of the Actions system user as "no owner filter" — so a job's
GITEA_TOKENcould list every private repository on the instance throughGET /api/v1/user/repos. The fix makes system users own nothing and restricts the/api/v1/userroutes to individual and bot accounts. On a shared community instance, the Actions tenant boundary was effectively one flat namespace until this release. - CVE-2026-116474 (Moderate): a job's
GITEA_TOKENcould add an email address and a GPG key to thegitea-actionssystem user, so commits signed with that key displayed as signed by the instance. The advisory states only the displayed signature status was affected — it is a verification-marketing bug, not a verification-breaking one — but "your forge says this commit is signed" is a claim your members read.
Alongside them in the same Notable Fixes section, one line that deserves its own sentence for anyone reading the changelog as prose: "Jobs of repositories with Actions disabled are no longer picked up by runners" (#40106) — until this release, runners could fetch queued jobs from repositories with Actions disabled. Actions-disabled-as-a-kill-switch is a common community stance for repos inherited from less careful times; the switch works now.
The eMudhra loop closes inside a Go build¶
CVE-2026-111943 is listed by a link to the Go project, not a Gitea advisory, because that is what it is: the release binaries are now built with Go 1.27.3, which no longer trusts the set of intermediate certificates this site counted, named, and cleaned off systems in October. The Gitea-specific scope is the outgoing side — certificate checks on OAuth2 providers, webhooks, mirrors, and migrations — the connections a community forge makes to the rest of a sovereign stack. Until this build, a Gitea instance verifying a webhook target's certificate chain was still trusting intermediates whose private keys were numbers small enough to type. That was never a Gitea bug; it is what happens when a CA event propagates through toolchains. The operator note is the mirror image of the October checklist: whatever you did to your system trust stores in October — the ca-certificates update, the certifi cleanup in venvs — your forge's outbound trust was its own surface, and it closes in an application update, not a host one.
What a community forge does this week¶
- Running the 28 line: 28.2.0 → 28.3.0 is a maintenance release; the blog's own recommendation is the strongest wording it has used. With Actions on a runner, 28.3.0 is the release where the review-workflow trust hole, the unscoped task token, and the disabled-repo job pickup all close at once.
- Running the 1.27 line: no 1.27.4 exists — verified in the releases feed on November 4 — and the two advisory ranges this site pulled (≥ 1.19.0 and ≤ 28.2.0) both sweep it. The options from October 19 are unchanged in shape: migrate to the 28 line and budget the configuration review the 28.0 jump requires, or hold and treat every entry on this ledger as accepted exposure. There are now fifty-nine entries on that ledger with no 1.27 fixes.
- Check
ACCOUNT_LINKINGin app.ini. Anything other thanlogin(the default) is worth a second look;autowith open registration was an account-takeover path for roughly seven months of 2026. If you ran it, auditexternal_login_userfor identities linked in that window. - Inventory and rotate public-only tokens, and treat their historical reach as per the table above — runner registration and admin creation included for high-permission holders.
- Audit runner registrations for anything issued by tokens, and check the org secrets a private-repository runner could have received.
- Archived repositories: if you keep archived repos as frozen history, releases, milestones, labels, deadlines, pins and reactions on them were API-writable by collaborators until this release. Diff anything you care about.
What goes in the notebook¶
Three things this release adds to the ledger method, beyond the CVEs:
- The twins argument is now cross-vendor. Forgejo closed the review-trust hole on October 26; Gitea closed its own on November 1, ten days after the report's scan date. When a forge bug class appears, the other forge is usually running the same class on a different clock — check both, don't wait for the other project's blog.
- The toolchain is part of the patch. A CA revocation is not over when the OS says it is over; an application built with a stale toolchain trusts the dead intermediates anyway. Gitea needed a release to carry a Go rebuild. Inventory any service making outgoing TLS where the application bundles its own build.
- The seven-month window is in the ledger now. April 7 to November 1 on an account takeover, recorded in the project's own advisory. Whatever the reason, it is the longest window in this site's Gitea accounting, and it is the data point to bring to any vendor's "we fix fast" pitch — including one made by a project you like, which is the harder and better use of the number.