Skip to content

gitea/forgejo: restrict access to internal API - #816

Open
MarshallAsch wants to merge 1 commit into
linuxserver:masterfrom
MarshallAsch:gitea-hardening
Open

gitea/forgejo: restrict access to internal API#816
MarshallAsch wants to merge 1 commit into
linuxserver:masterfrom
MarshallAsch:gitea-hardening

Conversation

@MarshallAsch

Copy link
Copy Markdown

linuxserver.io


  • I have read the contributing guideline and understand that I have made the correct modifications

Description

Adds a deny all location block for Gitea's/Forgejo's internal API to the gitea and forgejo confs (subdomain and subfolder variants):

location ^~ /api/internal {
    deny all;
}

/api/internal is not a user-facing API. It is only ever called by the application talking to itself over loopback — gitea serv handling 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 existing location ~ (/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/internal is authenticated only by INTERNAL_TOKEN, a shared secret stored in app.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):

  1. POST /{owner}/{repo}/markup with a .org filename triggers Org-mode rendering
  2. go-org's default #+INCLUDE handler reads arbitrary files → app.iniINTERNAL_TOKEN
  3. The token authenticates POST /api/internal/manager/add-logger, which is used to write an executable git hook
  4. The hook runs on the next anonymous clone, as the Gitea OS user

Step 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.sample ships 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/v1 and /info/lfs. /api/internal is 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:alpine with a config reproducing the location structure of both variants, confirming the deny wins over the regex and nothing else regresses.

Subdomain:

Request Result
/ 200 — main location
/api/internal 403
/api/internal/manager/add-logger 403
/api/v1/repos/search 200 — API location
/info/lfs/objects 200 — API location
/gitea/api/v1/x 200 — API location
/login/oauth/grant 200 — oauth location (#800) intact

Subfolder:

Request Result
/gitea/ 200
/gitea/api/internal 403
/gitea/api/internal/manager/add-logger 403
/gitea/api/v1/repos/search 200
/gitea/info/lfs/x 200

nginx -t passes on both. I also confirmed that without ^~ the regex location wins and /api/internal is proxied through — hence the explicit prefix modifier and the inline comment.

Source / References

  • Advisory: GHSA-6v53-hr58-556r
  • CVE-2026-59774 — affects Gitea 1.22.1–1.27.0, fixed in 1.27.1
  • Forgejo is affected over a comparable range; the org-mode read was fixed in 16.0.2
  • Existing precedent in this repo: jellyfin.subdomain.conf.sample (/metrics), restrict vaultwarden admin page to LAN #693 (vaultwarden admin)

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

2 participants