Skip to content

fix(api): hide cross-repo PR head repository/SHA from public-only tokens - #39167

Open
qw3rtyou wants to merge 2 commits into
go-gitea:mainfrom
qw3rtyou:fix/pr-head-token-scope-leak
Open

fix(api): hide cross-repo PR head repository/SHA from public-only tokens#39167
qw3rtyou wants to merge 2 commits into
go-gitea:mainfrom
qw3rtyou:fix/pr-head-token-scope-leak

Conversation

@qw3rtyou

@qw3rtyou qw3rtyou commented Aug 30, 2026

Copy link
Copy Markdown

Summary

Follow-up to GHSA-m78w-jjjx-gp8r. Alongside the parseCompareInfo scope gap (already fixed), the
advisory noted a second, distinct code path: services/convert.ToAPIPullRequest(s) serializes
pr.HeadRepo (repository metadata, clone URLs, private flag) and the head commit SHA with no
check on whether the current API token is scoped to see that repository.

convert.ToAPIPullRequest(s) can't apply the check itself — it only receives a doer, not the
APIContext the token scope lives on — so the guard has to live in every handler that serializes a
PR's head. These didn't have it:

  • GetPullRequest
  • GetPullRequestByBaseHead
  • GetPullRequestByMergedCommit
  • ListPullRequests
  • ListPinnedPullRequests
  • EditPullRequest

A public-only, read:repository token can reach any of these through a public base repository and
read a private (or otherwise restricted) head repository's metadata and latest commit SHA, even
though it was never granted access to the head repository itself.

Fix

Adds hideCrossRepoPRHead(s) in routers/api/v1/repo, called after convert.ToAPIPullRequest(s)
in every affected handler. It nulls Head.Repository/Head.Sha when
ctx.TokenCanAccessRepo(pr.HeadRepo) fails — mirroring the guard already applied to
UpdatePullRequest and parseCompareInfo.

CreatePullRequest is intentionally left alone: the caller must already have push access to the
head branch to open the PR, so there's no token-scope gap there.

Test plan

Added TestAPIGetPullRequestPublicOnlyToken and TestAPIListPullRequestsPublicOnlyToken
(tests/integration/api_pull_public_only_test.go), modeled on the existing
TestAPIComparePublicOnlyToken. Ran locally end-to-end against a real sqlite-backed instance:

  • Both new tests pass — full token still sees the private head repo/SHA, public-only token sees
    neither, base repo/PR stays visible either way.
  • No regression in TestAPIViewPulls, TestAPIViewPullsByBaseHead, TestAPIEditPull,
    TestAPIViewPullFilesWithHeadRepoDeleted, TestAPIListPinnedPullrequests.
  • Didn't add a dedicated test for GetPullRequestByMergedCommit (couldn't find an existing API
    test file for that endpoint to model one on) — happy to add if useful.

GetPullRequest, GetPullRequestByBaseHead, GetPullRequestByMergedCommit,
ListPullRequests, ListPinnedPullRequests, and EditPullRequest all serialize
pr.HeadRepo via convert.ToAPIPullRequest(s) without checking whether the
current API token is scoped to see that repository. A public-only,
read:repository token can therefore read a private (or otherwise
restricted) head repository's metadata and latest commit SHA through any
of these endpoints on a public base repository, even though it was never
granted access to the head repository itself.

convert.ToAPIPullRequest(s) can't apply the check itself -- it only
receives a doer, not the APIContext the token scope lives on. This adds
hideCrossRepoPRHead(s) in routers/api/v1/repo, called after conversion in
every affected handler, which nulls Head.Repository/Head.Sha when
ctx.TokenCanAccessRepo(pr.HeadRepo) fails. Mirrors the guard already
applied to UpdatePullRequest and parseCompareInfo (the fix for the earlier,
related advisory).

Follow-up to GHSA-m78w-jjjx-gp8r, which reported this second code path
alongside the primary parseCompareInfo fix.
@GiteaBot GiteaBot added the lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. label Aug 30, 2026
…llRequests)

TestAPIGetPullRequestPublicOnlyToken and TestAPIListPullRequestsPublicOnlyToken, modeled on the
existing TestAPIComparePublicOnlyToken. Both verified: full token still sees the private head
repo/SHA, public-only token sees neither, base repo/PR itself stays visible either way.

Ran locally end-to-end (real server, sqlite, actual HTTP requests) -- both new tests pass, plus
no regression in TestAPIViewPulls, TestAPIViewPullsByBaseHead, TestAPIEditPull,
TestAPIViewPullFilesWithHeadRepoDeleted, TestAPIListPinnedPullrequests.
@qw3rtyou qw3rtyou changed the title api: hide cross-repo PR head repository/SHA from public-only tokens fix(api): hide cross-repo PR head repository/SHA from public-only tokens Aug 30, 2026
@github-actions github-actions Bot added topic/api Concerns mainly the API type/bug labels Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. topic/api Concerns mainly the API type/bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants