Cybersecurity

Gitea Shipped 21 CVEs in One Release and Eleven of Them Are the Same Bug

Gitea 28.3.0 landed November 1 with twenty-one CVE fixes. Read the list and a pattern jumps out: more than half are the same failure repeated across different route handlers, where a token scoped to public data gets handed private data anyway. That's not twenty-one mistakes.

I read a lot of release notes. Most security sections are a grab bag — an XSS here, a path traversal there, somebody bumped a dependency. You skim, check if anything's yours, move on.

Gitea 28.3.0 landed November 1 with twenty-one CVEs, and about four entries in I stopped skimming. Because the same sentence kept showing up wearing slightly different clothes.

A token restricted to public data. A route that hands it private data anyway.

Again. And again. Eleven times out of twenty-one.

That's not twenty-one bugs. That's one design decision producing eleven bugs, and if you self-host Gitea it's worth sitting with, because it tells you roughly where the next one's going to be.

The pattern

Gitea has a thing called a public-only token — an access token scoped so it can only touch public data. Perfectly reasonable feature. You hand it to CI, to a bot, to some integration you don't entirely trust, and it reads your public repos and nothing else.

The catch is in how that restriction gets enforced. It has to be checked by every route that returns data. Not declared once, somewhere central. Checked. Individually. In every handler. By whoever happened to write it.

Here's what that bought them:

CVE What leaked to a public-only token
CVE-2026-117635 The admin API — email listing, private org creation, runner tokens, creating admins
CVE-2026-119441 DELETE /api/v1/orgs/{org}/repos — deletion of all org repos, private included
CVE-2026-118395 Deprecated POST /api/v1/org/{org}/repos — private repo creation in orgs
CVE-2026-118394 OIDC userinfo read scope from the header only, so a query-param token exposed private orgs and teams
CVE-2026-119625 Basic-auth tokens unchecked on /{username} — private activity feeds, private repos and orgs on profiles
CVE-2026-119627 Profile pages and RSS/Atom feeds of limited-visibility users and orgs
CVE-2026-117646 Repository, star and watch tabs on profiles and org home pages
CVE-2026-114199 The contribution heatmap, counting private repo activity
CVE-2026-117685 OCI cross-repo blob mounts used the user's permissions, not the token's
CVE-2026-112887 Actions task tokens had no scope at all; /api/v1/user/repos listed every private repo
CVE-2026-116289 A write:organization token could mint an org runner registration token

Eleven routes. Eleven separate decisions about whether to check the same flag. Eleven times somebody didn't.

And look — the easy read here is "Gitea's developers were careless." I don't think that's right, and I've been on the wrong side of this exact thing enough times to say so.

Every one of those handlers looks fine on its own. You're writing the heatmap endpoint. You're thinking about contribution counts and date buckets. The question "does this token's scope permit seeing private repository activity" is not the question in front of you. It's three layers away from what you're doing, and it lives in a different file, and nothing in your editor is going to bring it up.

That's the actual failure. A permission model that relies on every developer remembering it is a permission model that will get forgotten. How many times is just a function of how many routes you've got.

The fix isn't "be more careful." It's never "be more careful." It's enforcing the scope somewhere every route has to pass through, so that forgetting stops being an option that's available to you.

If you've ever inherited a codebase where authorization lives in the handlers, you know this feeling in your stomach. It works fine until the codebase gets big enough that nobody can hold the whole permission matrix in their head. Then it quietly stops working. And nobody notices for years, because nothing errors. It just returns data.

The two I'd act on tonight

Not all twenty-one are equal, and neither of the worst ones is in that table.

CVE-2026-120263 — account takeover through OAuth linking

This is the one. Check your config before you finish reading.

With ACCOUNT_LINKING = auto and registration enabled, Gitea linked external OAuth2 identities to existing accounts based on the user name or email the user submitted — not the identity the provider actually asserted.

Sit with that distinction, because it's the whole bug. The provider tells you "this is user X." The user tells you "I'm user Y." Gitea believed the user.

So: attacker has an account on your configured OAuth provider. Sets their name or email to match one of yours. Signs in. Lands in that account. No password. Threat Radar's write-up is blunt that this includes administrators.

It's CWE-287, improper authentication, affecting everything before 28.3.0.

Go look now:

grep -rn "ACCOUNT_LINKING\|ENABLE_OPEN_ID_SIGNIN\|DISABLE_REGISTRATION" /etc/gitea/app.ini

If ACCOUNT_LINKING = auto and registration's open, you want this upgrade today — and you want to look hard at who registered recently.

CVE-2026-116474 — forging your instance's own signature

A job token could add an email address and a GPG key to the gitea-actions system user. Result: commits that appear signed by the instance itself.

That's not data disclosure. That's signature forgery, CWE-347. The entire point of commit signing is that a verified badge means something, and this lets anyone who can run a CI job produce commits carrying your instance's verification.

If you went to the trouble of setting up signed commits, you did it because you wanted to trust that badge in review or deployment. That trust was forgeable.

Same family: CVE-2026-118386, where a reviewer could point a pull request review at their own commit, making pull_request_review workflows run unapproved — with secrets and write access. Classic CI escalation. Get a workflow to run against code you control, with credentials you shouldn't have.

I've argued before for treating self-hosted infrastructure access as a zero-trust problem, and CI tokens are the textbook case. A job token is a credential that exists specifically to be handed to code you didn't write. These three are all variations on that credential being worth more than you thought.

The rest, quickly

CVE-2026-111943 is interesting for reasons that have nothing to do with Gitea. Release binaries now build with Go 1.27.3, which stops trusting certain intermediate certs from one issuing CA. Gitea doesn't name the CA — but Go 1.27.3 is the release that shipped the eMudhra change, after Mozilla added eight eMudhra intermediates to OneCRL in mid-October. I'd put money on it being the same thing arriving downstream. It hits Gitea's outgoing connections: OAuth2, webhooks, mirrors, migrations. Good reminder that your trust store is a dependency like any other, and it updates on somebody else's schedule.

CVE-2026-117677 is a zip bomb. Helm chart uploads got inflated without a size limit before the quota check. Order of operations: check the quota against what it decompresses to, not what showed up.

CVE-2026-113429 is stored XSS through Actions artifacts — legacy uploads stored Content-Encoding and then served it back as Content-Type.

CVE-2026-116296 and CVE-2026-118504 both allow writes to archived repositories through API routes that forgot to check. Archiving is supposed to mean read-only. Two more handlers, same shape as the eleven.

CVE-2026-117649 and CVE-2026-119533 leak private issues and pull requests through linked references — issue timelines exposing private dependencies, project boards showing private linked PRs. Private data arriving through a relationship rather than a direct fetch, which is exactly the thing a per-route permission check almost never catches.

Don't confuse this with the July emergency

Search for Gitea security right now and you'll hit CVE-2026-60004, a CVSS 9.8 RCE in the diffpatch endpoint. It's nasty — attacker plants a hooks/post-index-change file in a bare temporary clone, Git runs it during indexing, shell as the Gitea service account.

That is not this release. Fixed in 1.27.1 on July 27. CISA KEV on August 25. Federal deadline August 28. It hit 1.17 through 1.27.0.

I'm being emphatic about the dates because I nearly conflated the two myself while researching this, and I was reading the primary sources. If you're on 28.x you got that fix months ago.

If you're somehow still on 1.x and internet-facing, that's a different and much more urgent conversation. Shadowserver was tracking roughly 5,000 exposed instances of undetermined patch status, and the observed payload was a cryptomining dropper that in one documented case went from account registration to execution in about eleven seconds.

Nothing in 28.3.0 is reported as actively exploited.

What this doesn't cover

Here's a gap I went looking to fill and couldn't, so I'll tell you about it instead of papering over it.

There are no CVSS scores for these. I checked. NVD hasn't scored CVE-2026-120263 — the tracker I found lists it as "AI-assessed (no CVSS data)" and rates it High off "thin evidence." So if you see a confident severity number for any of these twenty-one, ask where it came from, because as far as I can tell nobody's actually scored them yet. Gitea publishes a one-line summary per CVE, not full advisories with vectors.

No exploit code here either, and no walkthrough for any of them. The descriptions are specific enough to tell you whether you're exposed, which is what you need to decide how fast to move.

And I haven't read Gitea's source to confirm my architecture read. The eleven-CVE pattern is plain from the descriptions, and scattered scope checks is the obvious explanation. But it's an inference, not something I verified in the code, and I'd rather say that than pretend otherwise.

What I'd actually do

Upgrade to 28.3.0. Twenty-one CVEs where more than half leak private data to tokens you handed out on purpose is not a release to sit on.

Then — and this is the step everybody skips — rotate your tokens.

Every one of those eleven means a token that was behaving correctly from your side may have been returning more than you thought. If you've given public-only tokens to third-party integrations, CI systems or bots, they had broader read access than the scope implied, and you have no way to find out what they did with it. Rotating is cheap. Finding out the hard way is not.

Check ACCOUNT_LINKING before anything else if registration is open.

And if you run Actions with secrets, assume job tokens were over-privileged and rotate those too. CVE-2026-112887 means an Actions task token could enumerate every private repo on the instance. CVE-2026-116289 means it could mint a runner registration token that receives private jobs and their secrets.

The lesson I'm taking out of this has nothing to do with Gitea. Most of us are running something with a scoped-token model now — git, CI, container registries, whatever else. The question worth asking about each of them is whether that scope gets enforced in one place or checked in a hundred.

Because the second kind doesn't fail loudly. It fails quietly, in the one route nobody thought about, and you find out when somebody publishes a list of twenty-one.

$ tail -f /forums

Discussion

Comments live on the community forums — sign in there to reply.