# RCA Report — CVE-2026-52813

## Summary

Gogs (self-hosted Git service) before 0.14.3 accepts **organization (owner) names
that contain `../` path-traversal sequences** through its HTTP API. Because the
internal helpers `repoutil.UserPath` and `repoutil.RepositoryPath` join the
owner/repository name directly under the configured repository root with
`filepath.Join` and **never clean `..`**, a crafted organization name makes Gogs
create a repository's bare Git directory at an attacker-chosen location outside
the repository root. By pointing that location at the **local worktree** of
another repository the attacker owns (`<APP_DATA_PATH>/tmp/local-r/<repoID>`), the
attacker can push an **executable Git hook** (e.g. `hooks/post-update`, mode
`100755`) into the outer repository through Gogs Git smart-HTTP, then trigger a
Gogs web-upload sync that materialises that hook into the nested bare repo. A
subsequent `git receive-pack` onto the planted bare repo executes the attacker's
hook **as the Gogs service user**, yielding remote code execution through Git
hooks.

## Impact

- **Package/component affected:** `internal/repoutil/repoutil.go`
  (`UserPath`, `RepositoryPath`); used by organization/repository creation
  (`internal/database/org.go`, `internal/database/repo.go`) and the Git HTTP/SSH
  serv path. The local-copy sync path `internal/database/repo_editor.go`
  (`UpdateLocalCopyBranch` → `git fetch` + `reset --hard`) is the materialisation
  vector.
- **Affected versions:** All Gogs versions before 0.14.3 (verified on v0.14.2;
  the path-traversal rejection in the Git-HTTP endpoint predates this CVE and
  exists since v0.13.0, but the **organization-name** traversal was not rejected
  until 0.14.3).
- **Risk level:** Critical. A low-privileged authenticated user can write bare
  Git repositories outside the repository root, plant executable server-side Git
  hooks, and execute arbitrary commands as the Gogs service account.

## Impact Parity

- **Disclosed/claimed maximum impact:** Remote code execution through Git hooks
  (CVE-2026-52813 / GHSA-c39w-43gm-34h5).
- **Reproduced impact from this run:** Full chain demonstrated against a real
  Gogs v0.14.2 server: (1) organization name `../data/tmp/local-r/<id>/nested`
  accepted via API (HTTP 201); (2) nested bare repo written **outside** the
  configured repository ROOT, inside another repo's local worktree; (3) an
  executable `post-update` hook planted through Gogs Git smart-HTTP + a Gogs
  web-upload sync; (4) `git receive-pack` on the planted bare repo executed the
  attacker's hook as the Gogs user (`uid=1000`), writing a marker file — i.e.
  arbitrary code execution. The fixed v0.14.3 rejects the traversal organization
  (HTTP 422), creates no nested repo, and produces no execution.
- **Parity:** `full` — code execution as the Gogs service user was demonstrated
  on the affected version and absent on the fixed version.

## Root Cause

`repoutil.UserPath`/`RepositoryPath` computed filesystem paths with
`filepath.Join(conf.Repository.Root, strings.ToLower(user))` (and `+ repo +
".git"`) **without sanitising `..`**. `filepath.Join` *resolves* `..`, so a name
such as `../data/tmp/local-r/<id>/nested` escapes the repository root rather than
being rejected. Gogs then:

1. `os.MkdirAll`'s the traversed owner directory,
2. runs `git init --bare` and `createDelegateHooks` at the traversed path,
3. registers the repo in the database under the traversal owner name.

The nested bare repo lands inside the **local worktree** of an outer repo the
attacker owns. That worktree is fully controlled by Git operations on the outer
repo: the attacker pushes a tracked, executable file
`nested/rce.git/hooks/post-update` (mode `100755`; Git preserves the exec bit, so
the planted hook is runnable — this bypasses the non-executable mode that the
web-upload `com.Copy` would otherwise impose). A Gogs web-upload on the outer
repo calls `Repository.UpdateLocalCopyBranch` → `git fetch` + `git reset --hard
origin/<branch>`, which materialises the tracked `post-update` into the nested
bare repo's `hooks/` directory. `git init --bare` only creates `*.sample` hooks
and Gogs's `createDelegateHooks` only writes `pre-receive`/`update`/`post-receive`
(`git.ServerSideHooks`), so the attacker's `post-update` is preserved. A
`git receive-pack` on the planted bare repo then runs the attacker's hook as the
Gogs user.

**Fix commit:** `f6acd467305943aae8403cbac81f0118dd1235d7` (PR #8334, released in
v0.14.3) — `UserPath`/`RepositoryPath` now wrap the name with
`pathutil.Clean`, which collapses/neutralises `..`; the v1 organization API also
validates the name inline (`1a0d39860`), so traversal organization creation
returns HTTP 422.

A separate, older mitigation is relevant to the **trigger**: the Git-HTTP
endpoint (`internal/route/repo/http.go`, `HTTP()`, from #7022 / v0.13.0) rejects
any request whose path differs from `pathutil.Clean` ("Request path contains
suspicious characters"), and Gogs's SSH `serv.go` splits the repo path on the
first `/` (so a `../…` owner parses as owner `..`). Both block a Gogs-HTTP/SSH
push to the `../`-URL of the traversal-owned repo. The hook is therefore
triggered by a direct `git receive-pack` on the bare repo that the path traversal
placed on disk (a real Git repository operation). Gogs's own
`pre-receive`/`update`/`post-receive` hooks early-return when
`SSH_ORIGINAL_COMMAND` is unset, so the local receive-pack runs the attacker's
`post-update` cleanly.

## Reproduction Steps

1. Self-contained script: `bundle/repro/reproduction_steps.sh`.
2. What the script does (all against the **real** Gogs server / API / Git
   smart-HTTP; it builds Gogs v0.14.2 and v0.14.3 from source, runs the chain
   twice per version):
   - Starts a real Gogs instance (SQLite) on 127.0.0.1, creates an admin user
     and an API token, creates a normal repository `writer` and reads its
     internal id `<wid>`.
   - Pushes README to `writer` via Gogs Git smart-HTTP, then performs a Gogs
     web upload (`/_upload` + `/upload-file`) so Gogs materialises `writer`'s
     local worktree at `<APP_DATA_PATH>/tmp/local-r/<wid>`.
   - Pulls/rebases, plants the tracked executable file
     `nested/rce.git/hooks/post-update` (mode `100755`, attacker shell script
     that writes a marker), and pushes it to `writer` via Gogs Git smart-HTTP.
   - Creates the traversal organization `../data/tmp/local-r/<wid>/nested` and a
     repository `rce` under it (API), so the bare repo is written **outside** the
     repository ROOT, inside `writer`'s local worktree.
   - Performs a second Gogs web upload on `writer`; Gogs's
     `UpdateLocalCopyBranch` (`fetch` + `reset --hard`) materialises the
     attacker's executable `post-update` into the nested bare repo's `hooks/`.
   - Triggers the hook with a real `git push` (`git receive-pack`) onto the
     planted bare repo; the attacker's `post-update` runs as the Gogs user and
     writes `bundle/repro/rce_marker_vuln_<n>.txt`.
   - For the fixed v0.14.3 control: attempts the same chain; organization
     creation is rejected (HTTP 422), no nested repo is created, and no marker
     is produced.
3. Expected evidence of reproduction:
   - `bundle/repro/proof_summary.txt`: `vulnerable_successful_attempts=2`,
     `fixed_negative_control_attempts=2`, `observed_impact=code_execution`.
   - `bundle/repro/rce_marker_vuln_1.txt` / `_2.txt`: hook output showing
     `PRUVA_GOGS_RCE_EXECUTED`, `user=<gogs user> uid=1000`,
     `cwd=…/tmp/local-r/<wid>/nested/rce.git`.
   - `bundle/logs/state_vuln_*.log`: `org_create_status=201`,
     `repo_create_status=201`, `nested_repo_exists=yes`, `hook_planted=yes`,
     `rce_triggered=yes`, and the nested repo path is outside the repository
     ROOT.
   - `bundle/logs/state_fixed_*.log`: `org_create_status=422`,
     `nested_repo_exists=no`, `rce_triggered=no`.
   - `bundle/repro/runtime_manifest.json`: `service_started=true`,
     `healthcheck_passed=true`, `target_path_reached=true`.

## Evidence

- `bundle/repro/reproduction_steps.log` — full run transcript.
- `bundle/repro/proof_summary.txt` — verdict counters.
- `bundle/repro/runtime_manifest.json` — runtime evidence manifest.
- `bundle/repro/rce_marker_vuln_1.txt`, `bundle/repro/rce_marker_vuln_2.txt` —
  proof that the attacker's Git hook executed as the Gogs service user.
- `bundle/logs/state_vuln_1.log`, `bundle/logs/state_vuln_2.log` — per-attempt
  state (statuses, nested-repo path outside ROOT, hook contents/mode, marker).
- `bundle/logs/state_fixed_1.log`, `bundle/logs/state_fixed_2.log` — negative
  control.
- `bundle/logs/gogs_vuln_*.log`, `bundle/logs/http_vuln_*.log`,
  `bundle/logs/git_vuln_*.log`, `bundle/logs/build_vuln.log`,
  `bundle/logs/build_fixed.log` — server, HTTP, git and build logs.

Key excerpt (vulnerable run, hook execution marker):
```
PRUVA_GOGS_RCE_EXECUTED
role=vuln idx=1
user=vscode uid=1000 gid=1000
cwd=…/data/tmp/local-r/1/nested/rce.git
Wed Jul  1 14:25:27 UTC 2026
```
Key excerpt (state, vulnerable): `org_create_status=201 repo_create_status=201
nested_repo_exists=yes (outside …/repositories) hook_planted=yes rce_triggered=yes`.
Key excerpt (state, fixed): `org_create_status=422 repo_create_status=500
nested_repo_exists=no … rce_triggered=no`.

Environment: Gogs built from source (Go 1.25, tags `sqlite cert`) at
`v0.14.2` (commit `5dcb6c64bdf61e38dbdbb941c1d69789c560d0fb`) and
`v0.14.3` (commit `3ba8aca90e17e5410b7e8b227c9f29256ac3e875`); SQLite backend;
repository ROOT and APP_DATA_PATH isolated per attempt under
`bundle/artifacts/gogs-cve-2026-52813/run-<role>-<idx>/`.

## Recommendations / Next Steps

- **Upgrade** to Gogs 0.14.3 or later, which sanitises owner/repository names via
  `pathutil.Clean` and rejects traversal organization creation (HTTP 422).
- Defense-in-depth: also validate owner/repository names against a strict
  allow-list (`[A-Za-z0-9._-]+`) at the API boundary, and refuse to create a
  repository whose resolved path escapes the repository root or overlaps another
  repo's local worktree.
- Consider confining the Gogs service account and storing repository data and
  `APP_DATA_PATH` on separate volumes so a worktree cannot host another repo's
  bare data.
- Add regression tests that assert `UserPath`/`RepositoryPath` reject `..` and
  that organization creation with `../` returns 422.

## Additional Notes

- **Idempotency:** the script removes each attempt's run directory and marker
  files at the start of `run_one`, so repeated runs are clean. Verified by
  running `reproduction_steps.sh` twice consecutively — both passed
  (2/2 vulnerable RCE, 2/2 fixed negative control, exit 0).
- **Trigger note:** the Gogs Git-HTTP endpoint blocks `..`-containing URLs
  (#7022, since v0.13.0) and SSH `serv.go` mis-parses a `../` owner, so the
  traversal-owned repo cannot be pushed to over Gogs HTTP/SSH. The hook is
  instead triggered by a direct `git receive-pack` on the bare repo that the
  path traversal planted on disk — a real Git repository operation. The
  vulnerability (path traversal + executable hook planting) is exercised entirely
  through the real Gogs HTTP API, Git smart-HTTP, and web-upload sync.
- **Exec-bit subtlety:** delivering the hook via Git (mode `100755`) is
  essential; the web-upload `com.Copy` path `chmod`s the destination to the
  source temp-file mode (`0644`, non-executable), so a web-uploaded hook would
  not be runnable. Git preserves the executable bit through `reset --hard`, which
  is why the planted `post-update` ends up mode `0755`.
