gitea/forgejo: restrict access to internal API - #816
Open
MarshallAsch wants to merge 1 commit into
Open
Conversation
The internal API is only ever called by the app itself over loopback (git hooks and `gitea serv`), so it has no legitimate caller through the reverse proxy. Leaving it reachable allowed CVE-2026-59774 to escalate an unauthenticated org-mode file read into RCE: read app.ini, extract INTERNAL_TOKEN, then inject a git hook via /api/internal/manager/add-logger and trigger it on an anonymous clone. ^~ is required so the deny takes precedence over the existing regex location that matches /api. Forgejo is included as it shares the same internal API. GHSA-6v53-hr58-556r
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds a
deny alllocation block for Gitea's/Forgejo's internal API to the gitea and forgejo confs (subdomain and subfolder variants):/api/internalis not a user-facing API. It is only ever called by the application talking to itself over loopback —gitea servhandling an SSH session, and git hooks calling back into the web process. Those calls originate inside the container and never traverse the reverse proxy, so denying them at the proxy has no legitimate impact.^~is required: without it the existinglocation ~ (/gitea)?/(api|info/lfs)regex takes precedence over a plain prefix match and the deny is silently bypassed.Version dates bumped on all four files.
Benefits of this PR and context
/api/internalis authenticated only byINTERNAL_TOKEN, a shared secret stored inapp.ini. Any vulnerability that discloses that file turns the internal API into a remote code execution primitive.That is exactly the chain in CVE-2026-59774 (CVSS 9.8, published 2026-08-02, exploited in the wild):
POST /{owner}/{repo}/markupwith a.orgfilename triggers Org-mode renderinggo-org's default#+INCLUDEhandler reads arbitrary files →app.ini→INTERNAL_TOKENPOST /api/internal/manager/add-logger, which is used to write an executable git hookStep 3 is the only step this conf can influence, and blocking it breaks the chain at the escalation point: the file read remains (that needs the app patch), but it no longer converts into RCE.
Patching is obviously the real fix, but proxy confs are exactly where "this endpoint should never have been reachable from the internet" belongs, and the block keeps holding for the next issue of this shape. Forgejo is included because it is a Gitea fork sharing the same internal API.
There is precedent for restricting a sensitive endpoint in a conf:
jellyfin.subdomain.conf.sampleships an uncommented allow/deny block on/metrics, and #693 added the same pattern for the vaultwarden admin page. Unlike those two, no allow-list is included here because there is no legitimate proxied caller at all, not even from LAN.One note on the contributing guideline "If the application has known API endpoints, we prefer these to be exempt from auth through a location block (provided the application has security on the endpoint)" — that exemption is preserved untouched for
/api/v1and/info/lfs./api/internalis deliberately treated differently: it is an internal RPC channel rather than a documented API, and its only protection is the very secret this CVE class leaks.How Has This Been Tested?
Tested against
nginx:alpinewith a config reproducing the location structure of both variants, confirming the deny wins over the regex and nothing else regresses.Subdomain:
//api/internal/api/internal/manager/add-logger/api/v1/repos/search/info/lfs/objects/gitea/api/v1/x/login/oauth/grantSubfolder:
/gitea//gitea/api/internal/gitea/api/internal/manager/add-logger/gitea/api/v1/repos/search/gitea/info/lfs/xnginx -tpasses on both. I also confirmed that without^~the regex location wins and/api/internalis proxied through — hence the explicit prefix modifier and the inline comment.Source / References
jellyfin.subdomain.conf.sample(/metrics), restrict vaultwarden admin page to LAN #693 (vaultwarden admin)