GHSA-6CQF-375W-639G

Vulnerability from github – Published: 2026-07-21 20:40 – Updated: 2026-07-21 20:40
VLAI
Summary
Gitea: RSS/Atom feed handlers bypass API-token scope & public-only confinement (incomplete fix of #37698)
Details

Summary

Gitea's RSS/Atom feed handlers accept API-token Basic auth but perform no token-scope or public-only enforcement. A personal access token that is correctly blocked (HTTP 403) from a private repository on /raw, /media, /archive, and /releases/download/... — because it is marked public-only or lacks the repository scope category — still returns that repository's private content through the feed routes. This is a token-confinement bypass and appears to be an incomplete fix of #37698, which added that scope enforcement to the download handlers but not to the sibling feed handlers.

This is not a cross-user access bug: the requesting account must still legitimately have repo read access (RepoAssignment + reqUnitCodeReader are enforced). What is bypassed is the guarantee that a confined token cannot reach private content — which is exactly the property #37698 was shipped to provide for downloads, and which matters when such a token is handed to a third-party service/CI, leaked, or used in a lower-trust integration.

Details

37698 added context.CheckTokenScopes / CheckRepoScopedToken

(services/context/permission.go) to the raw / media / archive / attachment download handlers, so a public-only or wrong-scope-category token cannot read private-repo content even when the owning user otherwise has access.

The feed handlers are registered with webAuth.AllowBasic (so they accept token Basic auth) but call no scope / public-only check. A grep for CheckTokenScopes / CheckRepoScopedToken / IsApiToken across routers/web/feed/ and the release feed handlers returns nothing.

Affected routes (all token-reachable via webAuth.AllowBasic, none call the scope check):

Route Handler Private data exposed
GET /{owner}/{repo}.rss / .atom repo.Home → handleRepoHomeFeed (view_home.go) last-10 commits: SHA, full message, author name + email
GET /{owner}/{repo}/rss/branch/*, /atom/branch/* feed.RenderBranchFeed* → ShowBranchFeed (routers/web/feed/branch.go) same commit data, any branch
GET /{owner}/{repo}/releases.rss / .atom ReleasesFeedRSS/Atom (routers/web/repo/release.go) → ShowReleaseFeed private release names, notes, descriptions
GET /{owner}/{repo}/tags.rss / .atom TagsListFeedRSS/Atom → ShowReleaseFeed private tag names + messages
GET /{user}.rss / .atom showUserFeed (routers/web/feed/profile.go), includePrivate = self \|\| admin the token owner's private cross-repo activity stream

Inconsistency that pins this down: the branch-feed routes sit in the same route group as /raw, /media, /archive (all of which call checkDownloadTokenScope), and releases.rss sits next to /releases/attachments/{uuid} and /releases/download/... (both go through ServeAttachment → scope check). Only the feeds were missed. The API equivalents (ListReleases / ListTags) are scope-gated via tokenRequiresScopes.

Two distinct confinement bypasses:

  1. Public-only bypass. A token created with the public-only option is blocked (403) from a private repo on /raw, /archive, /releases/download/..., but returns private commit / release data via .../releases.rss, .../rss/branch/*, /{owner}/{repo}.rss, and the owner's private activity via /{user}.rss.
  2. Scope-category bypass. A token scoped to only e.g. read:issue (no read:repository) is rejected by the download handlers but reads repository commit/release content via the feeds.

PoC

Verified live against the official gitea/gitea:1.26.2 Docker image (sqlite, feeds enabled).

Setup: non-admin user alice; private repo alice/secret with a commit "SECRET-COMMIT-MARKER ..." (file secret.txt) and a release "Private Release" / body "SECRET-RELEASE-MARKER ...". Two confined personal access tokens, both sent via HTTP Basic so the auth method is identical across download and feed — only the route differs: - Token A: scopes ["public-only", "read:repository"] - Token B: scopes ["read:issue"]

# Token A — download is correctly blocked, feeds leak private content:
curl -u alice:$TOKEN_A https://<host>/alice/secret/raw/branch/main/secret.txt   # => 403  (fix works)
curl -u alice:$TOKEN_A https://<host>/alice/secret/rss/branch/main             # => 200, <title>SECRET-COMMIT-MARKER ...</title>
curl -u alice:$TOKEN_A https://<host>/alice/secret/releases.rss               # => 200, SECRET-RELEASE-MARKER ...
curl -u alice:$TOKEN_A "https://<host>/alice.rss"                             # => 200, private activity

# Token B — wrong scope category, same split:
curl -u alice:$TOKEN_B https://<host>/alice/secret/raw/branch/main/secret.txt   # => 403
curl -u alice:$TOKEN_B https://<host>/alice/secret/rss/branch/main             # => 200, private commit leaked

Anonymous baseline returns 404 on the repo feeds (data is genuinely private) and a marker-free 200 on /alice.rss (public activity only) — confirming the leak is gated only by the missing token check.

Impact

Information disclosure of private commit metadata (SHA, message, author name+email), release/tag notes, and the owner's private activity stream, to the holder of a confined token that was specifically configured not to reach private content. Not raw file blobs (feeds don't serve file contents). Requires a token belonging to an account that already has repo read access, so the realistic threat is a leaked / shared / lower-trust token rather than an anonymous attacker — which is precisely the threat model #37698 addressed for downloads.

Suggested remediation

Add a token-scope check at the top of each feed handler, mirroring checkDownloadTokenScope:

if context.CheckRepoScopedToken(ctx, ctx.Repo.Repository, auth_model.Read); ctx.Written() {
    return
}

for ShowBranchFeed, ShowRepoFeed, ShowFileFeed, ShowReleaseFeed (repo feeds). For the user feed, gate includePrivate behind a non-public-only token (or require the user / repository scope) so a confined token can't pull private activity.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "code.gitea.io/gitea"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.27.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-50105"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-200",
      "CWE-862"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-21T20:40:24Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "### Summary\n\nGitea\u0027s RSS/Atom feed handlers accept API-token Basic auth but perform **no token-scope or\npublic-only enforcement**. A personal access token that is correctly blocked (HTTP 403) from a\nprivate repository on `/raw`, `/media`, `/archive`, and `/releases/download/...` \u2014 because it is\nmarked *public-only* or lacks the `repository` scope category \u2014 still returns that repository\u0027s\nprivate content through the feed routes. This is a token-confinement bypass and appears to be an\nincomplete fix of #37698, which added that scope enforcement to the download handlers but not to the\nsibling feed handlers.\n\nThis is **not** a cross-user access bug: the requesting account must still legitimately have repo\nread access (`RepoAssignment` + `reqUnitCodeReader` are enforced). What is bypassed is the guarantee\nthat a *confined* token cannot reach private content \u2014 which is exactly the property #37698 was\nshipped to provide for downloads, and which matters when such a token is handed to a third-party\nservice/CI, leaked, or used in a lower-trust integration.\n\n### Details\n\n#37698 added `context.CheckTokenScopes` / `CheckRepoScopedToken`\n(`services/context/permission.go`) to the raw / media / archive / attachment download handlers, so a\npublic-only or wrong-scope-category token cannot read private-repo content even when the owning user\notherwise has access.\n\nThe feed handlers are registered with `webAuth.AllowBasic` (so they accept token Basic auth) but call\nno scope / public-only check. A grep for `CheckTokenScopes` / `CheckRepoScopedToken` / `IsApiToken`\nacross `routers/web/feed/` and the release feed handlers returns nothing.\n\nAffected routes (all token-reachable via `webAuth.AllowBasic`, none call the scope check):\n\n| Route | Handler | Private data exposed |\n|---|---|---|\n| `GET /{owner}/{repo}.rss` / `.atom` | `repo.Home` \u2192 `handleRepoHomeFeed` (`view_home.go`) | last-10 commits: SHA, full message, author name + email |\n| `GET /{owner}/{repo}/rss/branch/*`, `/atom/branch/*` | `feed.RenderBranchFeed*` \u2192 `ShowBranchFeed` (`routers/web/feed/branch.go`) | same commit data, any branch |\n| `GET /{owner}/{repo}/releases.rss` / `.atom` | `ReleasesFeedRSS/Atom` (`routers/web/repo/release.go`) \u2192 `ShowReleaseFeed` | private release names, notes, descriptions |\n| `GET /{owner}/{repo}/tags.rss` / `.atom` | `TagsListFeedRSS/Atom` \u2192 `ShowReleaseFeed` | private tag names + messages |\n| `GET /{user}.rss` / `.atom` | `showUserFeed` (`routers/web/feed/profile.go`), `includePrivate = self \\|\\| admin` | the token owner\u0027s private cross-repo activity stream |\n\nInconsistency that pins this down: the branch-feed routes sit in the **same route group** as `/raw`,\n`/media`, `/archive` (all of which call `checkDownloadTokenScope`), and `releases.rss` sits next to\n`/releases/attachments/{uuid}` and `/releases/download/...` (both go through `ServeAttachment` \u2192 scope\ncheck). Only the feeds were missed. The API equivalents (`ListReleases` / `ListTags`) are scope-gated\nvia `tokenRequiresScopes`.\n\nTwo distinct confinement bypasses:\n\n1. **Public-only bypass.** A token created with the *public-only* option is blocked (403) from a\n   private repo on `/raw`, `/archive`, `/releases/download/...`, but returns private commit / release\n   data via `.../releases.rss`, `.../rss/branch/*`, `/{owner}/{repo}.rss`, and the owner\u0027s private\n   activity via `/{user}.rss`.\n2. **Scope-category bypass.** A token scoped to only e.g. `read:issue` (no `read:repository`) is\n   rejected by the download handlers but reads repository commit/release content via the feeds.\n\n### PoC\n\nVerified live against the official `gitea/gitea:1.26.2` Docker image (sqlite, feeds enabled).\n\nSetup: non-admin user `alice`; private repo `alice/secret` with a commit\n`\"SECRET-COMMIT-MARKER ...\"` (file `secret.txt`) and a release `\"Private Release\"` / body\n`\"SECRET-RELEASE-MARKER ...\"`. Two confined personal access tokens, both sent via HTTP Basic so the\nauth method is identical across download and feed \u2014 only the route differs:\n- Token A: scopes `[\"public-only\", \"read:repository\"]`\n- Token B: scopes `[\"read:issue\"]`\n\n```bash\n# Token A \u2014 download is correctly blocked, feeds leak private content:\ncurl -u alice:$TOKEN_A https://\u003chost\u003e/alice/secret/raw/branch/main/secret.txt   # =\u003e 403  (fix works)\ncurl -u alice:$TOKEN_A https://\u003chost\u003e/alice/secret/rss/branch/main             # =\u003e 200, \u003ctitle\u003eSECRET-COMMIT-MARKER ...\u003c/title\u003e\ncurl -u alice:$TOKEN_A https://\u003chost\u003e/alice/secret/releases.rss               # =\u003e 200, SECRET-RELEASE-MARKER ...\ncurl -u alice:$TOKEN_A \"https://\u003chost\u003e/alice.rss\"                             # =\u003e 200, private activity\n\n# Token B \u2014 wrong scope category, same split:\ncurl -u alice:$TOKEN_B https://\u003chost\u003e/alice/secret/raw/branch/main/secret.txt   # =\u003e 403\ncurl -u alice:$TOKEN_B https://\u003chost\u003e/alice/secret/rss/branch/main             # =\u003e 200, private commit leaked\n```\n\nAnonymous baseline returns 404 on the repo feeds (data is genuinely private) and a marker-free 200 on\n`/alice.rss` (public activity only) \u2014 confirming the leak is gated only by the missing token check.\n\n### Impact\n\nInformation disclosure of private **commit metadata** (SHA, message, author name+email),\n**release/tag notes**, and the owner\u0027s **private activity stream**, to the holder of a confined token\nthat was specifically configured *not* to reach private content. Not raw file blobs (feeds don\u0027t\nserve file contents). Requires a token belonging to an account that already has repo read access, so\nthe realistic threat is a leaked / shared / lower-trust token rather than an anonymous attacker \u2014\nwhich is precisely the threat model #37698 addressed for downloads.\n\n### Suggested remediation\n\nAdd a token-scope check at the top of each feed handler, mirroring `checkDownloadTokenScope`:\n\n```go\nif context.CheckRepoScopedToken(ctx, ctx.Repo.Repository, auth_model.Read); ctx.Written() {\n    return\n}\n```\n\nfor `ShowBranchFeed`, `ShowRepoFeed`, `ShowFileFeed`, `ShowReleaseFeed` (repo feeds). For the user\nfeed, gate `includePrivate` behind a non-public-only token (or require the `user` / `repository`\nscope) so a confined token can\u0027t pull private activity.",
  "id": "GHSA-6cqf-375w-639g",
  "modified": "2026-07-21T20:40:24Z",
  "published": "2026-07-21T20:40:24Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/go-gitea/gitea/security/advisories/GHSA-6cqf-375w-639g"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/go-gitea/gitea"
    },
    {
      "type": "WEB",
      "url": "https://github.com/go-gitea/gitea/releases/tag/v1.27.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Gitea: RSS/Atom feed handlers bypass API-token scope \u0026 public-only confinement (incomplete fix of #37698)"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…