GitHub Advisoryadvisory · Go
GHSA-wrr5-99h5-gq57 Gitea: Public-only tokens bypass private-resource restrictions on `/api/v1/user` self routes
HIGH
Official page ↗
- Aliases
- CVE-2026-24791
- Ecosystem
- Go
- Packages
- code.gitea.io/gitea
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
- Weaknesses
- CWE-863
- Reviewed
- 2026-06-17
Affected packages
• Go code.gitea.io/gitea: >= 1.22.3, < 1.26.2 → fixed in 1.26.2
Description
Summary
Many authenticated self routes under `/api/v1/user/...` do not enforce the `public-only` token restriction. As a result, a token or OAuth grant marked `public-only`, but otherwise carrying the route-required read/write scope category, can access or modify private account resources through self routes.
The canonical private-user endpoint correctly rejects the same tokens, for example `GET /api/v1/users/{privateUser}` returns `403`. The bypass exists because the generic `/api/v1/user` route group requires user scope and `reqToken()`, but does not enforce the token's public-only restriction for most self routes.
This is a systemic token/OAuth scope-boundary bypass, not a single endpoint bug.
This appears related to the previously fixed public-only token issue tracked as CVE-2025-68941 / GHSA-xfq3-qj7j-4565, which affected Gitea `< 1.22.3`. The behavior described here reproduces on tested main checkout `6a2706626904`. A representative SSH-key self-route PoC also reproduces on tested releases through v1.26.1. In other words, this should be treated as an incomplete fix / residual gap in a different route family, not as a duplicate of the older advisory.
Affected Code
The generic `/api/v1/user` group is mounted with user scope and `reqToken()`:
• `routers/api/v1/api.go:1008-1128`
`tokenRequiresScopes()` sets `ctx.PublicOnly` when the token contains `public-only`, but the public-only restriction is enforced only by routes that also call `checkTokenPublicOnly()`:
• `routers/api/v1/api.go:241-294` implements `checkTokenPublicOnly()`.
• `routers/api/v1/api.go:299-341` sets `ctx.PublicOnly` from the token scope.
Representative affected routes in that group:
• `/api/v1/user`: private self profile and settings.
• `/api/v1/user/emails`: read, add, and delete account email addresses.
• `/api/v1/user/keys`: list and add SSH public keys.
• `/api/v1/user/applications/oauth2`: list and create OAuth2 applications, including returned client secrets.
• `/api/v1/user/actions/secrets/{secretname}`: create or delete user-level Actions secrets.
• `/api/v1/user/actions/variables`: list, read, create, update, and delete user-level Actions variables.
• `/api/v1/user/actions/runners/...`: list, update, delete runners, and mint registration tokens.
• `/api/v1/user/actions/runs` and `/api/v1/user/actions/jobs`: list workflow metadata for private repositories.
• `/api/v1/user/repos`: create private repositories and list private repositories.
• `/api/v1/user/subscriptions`, `/api/v1/user/times`, `/api/v1/user/stopwatches`, `/api/v1/user/teams`, `/api/v1/user/hooks`: leak or modify private-account resources.
Correct public-only enforcement for comparison:
• `routers/api/v1/api.go:970-1008` applies `context.UserAssignmentAPI()` and `checkTokenPublicOnly()` to canonical `/api/v1/users/{username}` routes.
• `routers/api/v1/user/user.go:122-125` rejects public-only access to private users on `/api/v1/users/{username}`.
• `routers/api/v1/api.go:1091-1092` shows that `/api/v1/user/repos` requires the additional repository scope category, but still does not apply `checkTokenPublicOnly()`.
Local PoCs
The following dynamic PoCs were retested on checkout `6a2706626904` and all reproduced successfully. Each PoC writes a temporary integration test, runs it, and removes it afterward.
```bash
cd pocs
GITEA_REPO=/path/to/gitea-checkout GOTOOLCHAIN=auto go run ./api_public_only_self_user_private_profile_bypass_dynamic_poc.go
GITEA_REPO=/path/to/gitea-checkout GOTOOLCHAIN=auto go run ./api_public_only_user_ssh_key_bypass_dynamic_poc.go
GITEA_REPO=/path/to/gitea-checkout GOTOOLCHAIN=auto go run ./api_public_only_user_emails_bypass_dynamic_poc.go
GITEA_REPO=/path/to/gitea-checkout GOTOOLCHAIN=auto go run ./api_public_only_user_oauth_app_bypass_dynamic_poc.go
GITEA_REPO=/path/to/gitea-checkout GOTOOLCHAIN=auto go run ./api_public_only_user_repos_private_repo_bypass_dynamic_poc.go
GITEA_REPO=/path/to/gitea-checkout GOTOOLCHAIN=auto go run ./api_public_only_user_actions_secret_variable_bypass_dynamic_poc.go
GITEA_REPO=/path/to/gitea-checkout GOTOOLCHAIN=auto go run ./api_public_only_user_runner_registration_bypass_dynamic_poc.go
GITEA_REPO=/path/to/gitea-checkout GOTOOLCHAIN=auto go run ./api_public_only_user_runner_manage_bypass_dynamic_poc.go
GITEA_REPO=/path/to/gitea-checkout GOTOOLCHAIN=auto go run ./api_public_only_user_webhook_bypass_dynamic_poc.go
GITEA_REPO=/path/to/gitea-checkout GOTOOLCHAIN=auto go run ./api_public_only_user_actions_runs_private_repo_bypass_dynamic_poc.go
GITEA_REPO=/path/to/gitea-checkout GOTOOLCHAIN=auto go run ./api_public_only_user_actions_jobs_private_repo_bypass_dynamic_poc.go
GITEA_REPO=/path/to/gitea-checkout GOTOOLCHAIN=auto go run ./api_public_only_user_subscriptions_private_repo_bypass_dynamic_poc.go
GITEA_REPO=/path/to/gitea-checkout GOTOOLCHAIN=auto go run ./api_public_only_user_times_private_repo_bypass_dynamic_poc.go
GITEA_REPO=/path/to/gitea-checkout GOTOOLCHAIN=auto go run ./api_public_only_user_stopwatches_private_repo_bypass_dynamic_poc.go
GITEA_REPO=/path/to/gitea-checkout GOTOOLCHAIN=auto go run ./api_public_only_user_teams_private_org_bypass_dynamic_poc.go
```
Reproduced Impact Examples
Using private fixture user `user31`, public-only tokens are rejected by `GET /api/v1/users/user31`, but tokens with the route-required scopes can still reach the self routes below.
Confirmed with `public-only,write:user`:
• add SSH keys through `/api/v1/user/keys`;
• add account emails through `/api/v1/user/emails`;
• create OAuth2 applications and receive `client_secret` through `/api/v1/user/applications/oauth2`;
• create/delete user-level Actions secrets;
• create/read/list/update/delete user-level Actions variables;
• mint user-level runner registration tokens;
• manage user-level runners;
• create user webhooks.
Confirmed with `public-only,read:user`:
• read private self profile/settings and account email surfaces;
• list OAuth2 applications and user webhooks;
• list private repository workflow runs/jobs exposed through self Actions routes;
• list private subscriptions, tracked times, stopwatches, and team memberships.
Confirmed with `public-only` plus the route-required repository category:
• create private repositories through `POST /api/v1/user/repos` with `public-only,write:user,write:repository`;
• list those private repositories through `GET /api/v1/user/repos` with `public-only,read:user,read:repository`, while the canonical private repository endpoint remains forbidden.
Impact
The `public-only` token flag is intended to limit a token or OAuth grant to public resources. These routes violate that boundary for private accounts.
Practical abuse scenarios include:
• a third-party app or leaked token with the route-required write scope, but restricted to public resources, adding SSH credentials or OAuth applications to a private account;
• a public-resource-restricted token with the route-required write scope modifying Actions secrets/variables or registering/managing runners;
• a token limited to public resources creating and enumerating private repositories;
• a supposedly public-only integration learning private repository, workflow, team, timing, subscription, webhook, and email metadata.
Suggested Fix
Apply public-only enforcement consistently to self routes under `/api/v1/user`.
At minimum:
• for self routes, treat `ctx.Doer` as the target user/resource owner when enforcing `public-only`; mechanically adding `checkTokenPublicOnly()` is not sufficient unless `ctx.ContextUser` is set to `ctx.Doer` or the check explicitly handles self routes;
• reject `ctx.PublicOnly` on credential, identity, OAuth application, repository creation, webhook, Actions, runner, and email-management self-route mutations;
• filter list routes so public-only tokens cannot return private repositories, private organization/team metadata, private workflow runs/jobs, private tracked time, private stop…
Related
Alias · CVE (1)
- CVE-2026-24791 Public-only tokens bypass private-resource restrictions on `/api/v1/user` self routes vulnerability
Weakness · CWE (1)
- CWE-863 Incorrect Authorization weakness