
Forgejo 16.0.5 - High - Remote - File read - restore-repo follows dump-side file:// into attachments
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · forgejo-restore-file-open
Class: File read Reach: Local
Forgejo 16.0.5 (8d24b7d) - Forgejo
forgejo restore-repo of an untrusted dump rewrites release asset URLs with raw filepath.Join(baseDir, download_url) and prefixes file://. uri.Open still os.Opens that path. Empty --units restores releases. Gitea 1.27.0 added localFileURL and skipped escaping entries. That helper is not here.
Operator restore of an untrusted dump copies a host file the forgejo uid can open into a release attachment.
| ID | no CVE yet |
| Class | File read |
| Reach | Local (operator forgejo restore-repo of an untrusted dump) |
| CWE | CWE-22 / CWE-73 |
| CVSS | High: 7.3 CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:N/A:N |
| Product | Forgejo |
| Affected | through v16.0.5 (8d24b7d); local operator restore |
| Auth | local operator restore of an untrusted dump |
| License | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 only |
Hand the operator a dump directory (or already have INTERNAL_TOKEN and a path the server can read). Units include releases / release_assets.
| Privilege | Method / sink | Recon / integrity / availability | Lab-proven or inferred |
|---|---|---|---|
Read a host file the forgejo uid can open, via .. out of the dump | release.yml download_url: ../FORGEJO-FILE-WITNESS.txt; restore joins, uri.Open os.Opens, bytes stored as witness.txt | Recon (host-file confidentiality) | Lab-proven. Attachment body is FORGEJO-RESTORE-FILE-OPEN-WITNESS. In-dump decoy INSIDE-DUMP-NOT-HOST was not served |
| Same result with a symlink planted inside the dump | Joined path stays under baseDir; os.Open follows the link | Recon | Inferred from uri.Open file -> os.Open. Lab used .., not a symlink |
| Same sink on PR patches | pull_request.yml PatchURL is rewritten the same way, then uri.Open into repo/pulls/<n>.patch | Recon | Inferred from restore.go GetPullRequests. Lab restored releases only |
| Readers of the restored repo download the host file | GET /<owner>/<repo>/releases/download/<tag>/<name> | Recon | Lab-proven as labadmin Basic on the restored public repo |
Typical host files: app.ini, INTERNAL_TOKEN, SECRET_KEY, DB password | forgejo uid can read the config tree | Recon | Inferred. Lab witness was a planted file next to the dump, not live secrets |
Closed here: live POST /repos/migrate file:// LFI (DownloadFunc set; PR PatchURL prefix-checked against the clone HTTP base). Absolute DownloadURL dump-root join escape (Go filepath.Join("/dump", "/etc/passwd") stays /dump/etc/passwd). Unauth HTTP restore (CLI / internal token only). Backup dump packing app.ini (secrets by design, archive mode 0600). Not OS LPE. Not remote LFI.
I had two restore ideas. I labbed the Gitea absolute-join first in my head. Go filepath.Join does not drop an absolute second argument the way Python os.path.join does. The working input is relative ... Gitea's localFileURL is not in this tree.
Wrong turns already recorded: dump-root join escape via an absolute DownloadURL; live migrate file:// LFI; X-Forgejo-Internal-Auth (restore is CLI as the forgejo uid, not /api/internal); treating restore HTTP as remote unauth; a reverse shell. Theatre.
python3 forgejo-restore-file-open-Abraxas-Labs.py
or:
cd lab
./run.sh
Target only http://127.0.0.1:19340. Image codeberg.org/forgejo/forgejo:16.0.5. Witness FORGEJO-RESTORE-FILE-OPEN-WITNESS.
IOC asset name='witness.txt' ...
IOC download status=200 bytes=34 snippet=b'FORGEJO-RESTORE-FILE-OPEN-WITNESS\n'
SUCCESS forgejo-restore-file-open how=live ..-join file:// os.Open FORGEJO-RESTORE-FILE-OPEN-WITNESS
Contain joined paths under baseDir after EvalSymlinks (Gitea PR 38215 localFileURL). Refuse ... Do not follow dump-side symlinks out of the dump. Drop file:// from uri.Open except a dump-rooted helper.
restore.go · uri.goGNU Affero GPL v3.0. See LICENSE. Loopback lab only. No warranty.