{"schema_version":"1.7.5","id":"GHSA-pgqf-926r-548m","published":"2026-07-21T20:39:22Z","modified":"2026-07-27T17:11:14.742988013Z","aliases":["CVE-2026-58445","GO-2026-6067"],"summary":"Gitea: Cross-repository label-ID enumeration oracle via unscoped DeleteIssueLabel API","details":"## Summary\n\nThe API endpoint `DELETE /repos/{owner}/{repo}/issues/{index}/labels/{id}` loads the label by ID with a **global,\nunscoped** lookup and never verifies the label belongs to the URL's repository (or its owning organization). Because\nthe response status differs by whether the label ID exists **anywhere on the instance** (204) versus not (422), an\nauthenticated user can use the endpoint as a **cross-repository label-ID existence / enumeration oracle**, including\nfor labels in repositories and organizations they cannot access.\n\n## Severity\n\n- The leaked information is minimal (existence/count of label IDs instance-wide); **no label name, color, or owning\n  repository is disclosed, and no cross-repository write occurs.**\n\n## Affected / patched versions\n\n- **Affected:** through **1.26.3** (latest at time of report).\n- **Patched:** none yet.\n\n## Details\n\n`DeleteIssueLabel` resolves the label with a global loader and never checks its scope:\n\n```go\n// routers/api/v1/repo/issue_label.go  (DeleteIssueLabel)\nlabel, err := issues_model.GetLabelByID(ctx, ctx.PathParamInt64(\"id\"))   // global, unscoped\n```\n\n`GetLabelByID` (`models/issues/label.go`) is `e.ID(labelID).Get(l)` with **no** `repo_id` / `org_id` filter. The\nhandler never verifies `label.RepoID == ctx.Repo.Repository.ID` (nor the org-label equivalent), and the downstream\n`issue_service.RemoveLabel` (`services/issue/label.go`) only re-checks the doer's write permission on the **issue's\nown** repository — never that the label belongs to it.\n\nEvery sibling label handler is correctly scoped — `GetLabel` / `EditLabel` / `DeleteLabel` (repo and org) use\n`GetLabelInRepoByID` / `GetLabelInOrgByID` and return 404 for a foreign ID. `DeleteIssueLabel` is the only outlier.\n\n**Why it is only an oracle:** `deleteIssueLabel` (`models/issues/issue_label.go`) deletes the `issue_label` row keyed\nby `(issue.ID, label.ID)`. For a foreign label, no such row exists → the function returns early **before** any\nmutation or comment creation. So there is no cross-repo write and no leak of the label's name. But the HTTP status\ndiffers:\n\n- label ID exists anywhere on the instance (incl. private repos/orgs) → **204 No Content**\n- label ID does not exist → **422** (`ErrLabelNotExist`)\n\nLabel IDs are sequential auto-increment, so this enumerates the instance-wide label population and probes existence\nof specific IDs across tenant boundaries.\n\n## Proof of Concept\n\nVerified end-to-end on a build of the `v1.26.3` tag.\n\n- `alice` (private repo `alice/secret`) creates a label → internal id **1**.\n- Attacker `bob` (separate user; public repo `bob/pub` with issue #1; **no access** to `alice/secret`) holds a token\n  with `write:issue` on his own repo.\n\n```text\nbob DELETE /api/v1/repos/bob/pub/issues/1/labels/1          -> HTTP 204   (alice's PRIVATE label id exists)\nbob DELETE /api/v1/repos/bob/pub/issues/1/labels/99999999   -> HTTP 422   (no such label)\n```\n\nDiffering only by the label ID: `204` vs `422` distinguishes \"label ID exists\" from \"does not exist.\" `bob` has zero rights to `alice/secret` but can still learn label id 1 exists. (Alice's label is untouched — no write.)\n\nReproduction steps:\n1. Create two users `alice`, `bob`. As `alice`, create a private repo and a label on it (note the label `id` from\n   the API response).\n2. As `bob`, create any repo with an issue, and a token with `write:issue`.\n3. `curl -u bob:$T -X DELETE https://<gitea>/api/v1/repos/bob/pub/issues/1/labels/<alice_label_id>` → **204**.\n4. `curl -u bob:$T -X DELETE https://<gitea>/api/v1/repos/bob/pub/issues/1/labels/99999999` → **422**.\n5. The differing status across an ID `bob` cannot otherwise see is the oracle.\n\n## Impact\n\nCross-tenant authorization-key bypass producing a label-ID existence/enumeration oracle: an authenticated user can\ndetermine whether arbitrary label IDs (including in private repositories and organizations they cannot access) exist,\nand enumerate the instance-wide label population.\n\n## Remediation\n\nScope the loader like every sibling handler, returning 404 for a label that is not in the URL repository (or its\nowning organization), so the status no longer distinguishes existence:\n\n```go\n// routers/api/v1/repo/issue_label.go — in DeleteIssueLabel, after loading the label\nif label.RepoID != ctx.Repo.Repository.ID && label.OrgID != ctx.Repo.Repository.OwnerID {\n    ctx.APIErrorNotFound()\n    return\n}\n```\n\n(Equivalently, resolve via `GetLabelInRepoByID` and, for org repositories, also accept the repo owner's org labels —\nmirroring the scoping in `GetLabel`/`EditLabel`/`DeleteLabel`.)","affected":[{"package":{"name":"code.gitea.io/gitea","ecosystem":"Go","purl":"pkg:golang/code.gitea.io/gitea"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"1.27.0"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-pgqf-926r-548m/GHSA-pgqf-926r-548m.json"}}],"references":[{"type":"WEB","url":"https://github.com/go-gitea/gitea/security/advisories/GHSA-pgqf-926r-548m"},{"type":"PACKAGE","url":"https://github.com/go-gitea/gitea"},{"type":"WEB","url":"https://github.com/go-gitea/gitea/releases/tag/v1.27.0"}],"database_specific":{"cwe_ids":["CWE-203","CWE-639"],"github_reviewed":true,"github_reviewed_at":"2026-07-21T20:39:22Z","nvd_published_at":null,"severity":"LOW"},"severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:N"}]}