GHSA-frpw-3h2q-4jj6 Gitea: Cross-Repo Information Disclosure via Org-Level Actions Run/Job APIs
**Author:** Prakhar Porwal
**Date:** 2026-05-24
**Target:** Gitea (self-hosted Git service)
**Branch tested:** `main` @ `b7e95cc48c` (development build, go1.26.3)
**Component:** `routers/api/v1/org/action.go` (org-level Actions API)
**OWASP:** API3:2023 Broken Object Property Level Authorization1. SummaryThe org-level Actions REST endpointsare gated only by **`reqOrgMembership()`** + `reqToken()`. They then call
`shared.ListRuns(ctx, ctx.Org.Organization.ID, 0)` /
`shared.ListJobs(ctx, ctx.Org.Organization.ID, 0, 0, nil)`, which selects
**every** `action_run` / `action_run_job` row whose repository belongs to the
org — with **no per-repository ACL check**.Result: any user who is a member of an organization can enumerate workflow
runs and jobs from **every repository in that org**, including:private repositories the caller has no team membership for,repositories where the caller has been explicitly denied the `repo.actions`
unit,repositories created by other teams the caller is not part of.Direct per-repo equivalents (`GET /api/v1/repos/{owner}/{repo}/actions/runs`,
`…/jobs/{job_id}/logs`, `…/runs/{run_id}/jobs`) correctly return `404` for the
same caller — proving the org-level surface is the only path that leaks.2. Affected Code2.1 Route registration`routers/api/v1/api.go:1647-1652`2.2 Helper that registers run/job listing`routers/api/v1/api.go:908-941``reqReaderCheck` for org-scope = `reqOrgMembership()` — bare org membership is
enough; no per-repo permission is consulted.2.3 Handler`routers/api/v1/org/action.go:595-683`2.4 Query construction (no ACL)`routers/api/v1/shared/action.go:138-215``models/actions/run_list.go:102-110`The join only constrains `repository.owner_id = orgID`. There is no
`access`/`team_repo`/`collaboration` join and no
`access_model.GetDoerRepoPermission(...)` filter — every row in the org is
returned.The same bug applies to `shared.ListJobs`, which calls
`db.FindAndCount[actions_model.ActionRunJob](ctx, FindRunJobOptions{OwnerID: …})`
using an analogous repository join.3. Steps to Reproduce3.1 SetupOrg **`1st-org`** with one **private** repo `1st-org-repo`.Team **`Owners`** contains user **`admin`** (org owner).Team **`1st-team`** has **zero repositories** assigned (units permission
`none` for actions, no included repos).User **`admin2`** is a regular user (`is_admin = false`), member of
`1st-team` only — so org member, but **no team grants any access to
`1st-org-repo`**.Verified that admin2 lacks direct access:A workflow file was committed to `1st-org-repo/.gitea/workflows/ci.yml` to
produce an `action_run`:3.2 Trigger**Output (truncated)**Same primitive for jobs:3.3 search primitivesAll query parameters supported by `ListRuns`/`ListJobs` work too — turning the
endpoint into a **search oracle** over private workflow metadata:All return matching rows from private repos in the org.4. ImpactA low-privileged authenticated org member (no team, no repo permission, no
admin) gains, for every private repository in the org:| Field leaked                  | Why it matters                                      |
|-------------------------------|------------------------------------------------------|
| `repository.full_name`, `description`, `private`, clone URLs | Existence + topology of private repos |
| `head_sha`, `head_branch`     | Confirms commits / branch names exist in private repos |
| `path` (workflow file)        | Reveals workflow YAML filenames                      |
| `event`, `display_title`      | Commit messages / event types                        |
| `actor`, `trigger_actor`      | Internal contributor identities incl. noreply emails |
| `created_at`, `started_at`    | Activity timing / CI cadence                         |
| Pagination + `?head_sha=`/`?branch=`/`?actor=` filters | Full **search oracle** over private workflow history |Real-world consequences:**Org reconnaissance** — confirms existence of private projects, names,
activity patterns; commit messages and branch names often reveal product
plans, security fix windows, or release schedules.**Insider-threat amplification** — any contractor / interviewee / OSS
contributor invited to a low-permission team can mine the rest of the
org's CI history.**Cross-team violation** — when an org isolates internal projects via
teams (e.g. `security/` vs `infra/` teams), this surface flatly bypasses
that boundary.**Pivot data** — commit SHAs disclosed here unlock subsequent endpoints
that *do* check ACLs but accept SHA inputs (e.g. some package / archive
download paths in third-party tooling that just trusts a SHA).The same primitive is exposed regardless of token scope, as long as the token
has `organization` scope, the user is an org member, and the org has any
private repos with action runs.
CVSS v3.1 score: 6.5