# Verification Report — CVE-2026-52813

## Fix Summary

CVE-2026-52813 is a path-traversal vulnerability in Gogs (< 0.14.3) where
organization (owner) names containing `../` sequences are accepted by the HTTP
API and, because `repoutil.UserPath`/`repoutil.RepositoryPath` joined the
owner/repository name directly under the repository root with `filepath.Join`
without cleaning `..`, a crafted organization name causes a bare repository to be
written **outside** the repository root — inside another repository's local
worktree. An attacker can then plant an executable Git hook and trigger RCE as
the Gogs service user. The fix mirrors the official v0.14.3 remediation
(PR #8334 / commit `f6acd467305943aae8403cbac81f0118dd1235d7`): it hardens the
filesystem-path layer by wrapping owner/repository names with the existing
`pathutil.Clean` helper (which anchors `path.Clean` at a synthetic root so `..`
is absorbed and cannot escape), and it adds an inline `AlphaDashDot` boundary
check in the org-creation API handler so traversal-bearing org names are
rejected with HTTP 422 before reaching the filesystem.

## Changes Made

Two files are modified (identical in behavior to the released Gogs v0.14.3 fix,
adapted to the v0.14.2 code layout which uses `internal/repoutil` +
`internal/pathutil` rather than the fix-commit's `internal/repox` +
`internal/pathx`):

1. **`internal/repoutil/repoutil.go`**
   - Added import `gogs.io/gogs/internal/pathutil`.
   - `UserPath`: `strings.ToLower(user)` → `pathutil.Clean(strings.ToLower(user))`
     with a `🚨 SECURITY` comment. This is the **root-cause sink**: every owner
     path (org/user creation, rename, repo creation) flows through here.
   - `RepositoryPath`: `strings.ToLower(repo)` →
     `pathutil.Clean(strings.ToLower(repo))` with a `🚨 SECURITY` comment.
     `pathutil.Clean` (`strings.Trim(path.Clean("/"+p), "/")`) collapses `..`
     because the leading `/` anchors the clean so `..` is absorbed rather than
     escaping above the repository root. `strings` is still used, so no import
     becomes orphaned.

2. **`internal/route/api/v1/org/org.go`**
   - Added imports `github.com/cockroachdb/errors` (already a dependency) and
     `github.com/go-macaron/binding` (already a dependency, v1.2.0, which
     exports `AlphaDashDotPattern = [^\d\w-_\.]`).
   - Added an inline validation block at the top of `CreateOrgForUser` (the
     single shared sink for both the non-admin `POST /api/v1/user/orgs`
     `CreateMyOrg` and the admin `POST /api/v1/admin/users/:user/orgs`
     `admin.CreateOrg`): reject when `UserName == ""`, `len > 35`, or
     `AlphaDashDotPattern.MatchString(UserName)` (i.e. it contains `/`, `\`,
     or any non-`[A-Za-z0-9-_.]` char), returning HTTP 422. This is required
     because the JSON option struct `api.CreateOrgOption` lives in the external
     `go-gogs-client` module and cannot carry `binding` tags on this branch.

The patch is intentionally minimal and behavior-preserving for legitimate names:
`pathutil.Clean` of a normal owner/repo name is a no-op, and `AlphaDashDot` is
the same rule already enforced by the web org-creation form (`form.CreateOrg`).

## Verification Steps

`bundle/coding/verify_fix.sh` is a self-contained, idempotent script that:

1. Fresh-checkouts vulnerable Gogs **v0.14.2** (`5dcb6c64`) from the cached
   mirror.
2. Applies `bundle/coding/proposed_fix.diff` with `patch -p1` — applies
   cleanly (both hunks).
3. Builds and vets the two affected packages:
   `go build ./internal/repoutil/ ./internal/route/api/v1/org/` and
   `go vet …` — both clean.
4. Runs the existing unit tests (`internal/repoutil`, `internal/pathutil`) —
   pass (no regression).
5. Drops in a targeted traversal test on the **patched** tree and runs it:
   - `TestUserPathTraversalNeutralized`: asserts `UserPath("../data/tmp/local-r/1/nested")`,
     `UserPath("../../etc")`, and a normal name all stay within
     `/home/git/gogs-repositories/`.
   - `TestRepositoryPathTraversalNeutralized`: asserts
     `RepositoryPath("../data/tmp/local-r/1/nested", "../rce")` stays within the
     root.
   - `TestOrgNameValidationRejectsTraversal`: asserts the inline guard rejects
     `../data/tmp/local-r/1/nested`, `../../etc`, `..\..\windows`, `""` and
     accepts `my-org`, `my_org`, `my.org`, `org123`.
   All pass on the patched tree.
6. **Negative control**: runs the same traversal test on an **unpatched** v0.14.2
   tree — it **fails** (e.g. `UserPath("../data/tmp/local-r/1/nested") =
   "/home/git/data/tmp/local-r/1/nested"` escapes the root
   `/home/git/gogs-repositories`), confirming the test actually catches the
   vulnerability.

Command run:
```
bash bundle/coding/verify_fix.sh
```

### Evidence that the vulnerability is resolved

- **Unpatched v0.14.2** (negative control): traversal escapes the root.
  ```
  UserPath("../data/tmp/local-r/1/nested") = "/home/git/data/tmp/local-r/1/nested"
    escapes repository root "/home/git/gogs-repositories"
  RepositoryPath traversal = "/home/git/data/tmp/local-r/1/rce.git"
    escapes repository root "/home/git/gogs-repositories"
  --- FAIL: TestUserPathTraversalNeutralized
  --- FAIL: TestRepositoryPathTraversalNeutralized
  ```
- **Patched v0.14.2**: the same names are collapsed back inside the root
  (`pathutil.Clean("../data/tmp/local-r/1/nested")` → `data/tmp/local-r/1/nested`,
  i.e. a child of the root, not an escape), and the API guard rejects the
  traversal org name with HTTP 422. This matches the repro agent's observed
  fixed-version behavior (`org_create_status=422`, `nested_repo_exists=no`,
  `rce_triggered=no` on v0.14.3).

## Test Results

| Check | Tree | Result |
|------|------|--------|
| `patch -p1` applies cleanly | v0.14.2 | PASS |
| `go build` affected packages | patched | PASS |
| `go vet` affected packages | patched | PASS |
| existing `repoutil`/`pathutil` tests | patched | PASS (no regression) |
| `TestUserPathTraversalNeutralized` | patched | PASS |
| `TestRepositoryPathTraversalNeutralized` | patched | PASS |
| `TestOrgNameValidationRejectsTraversal` | patched | PASS |
| `TestUserPathTraversalNeutralized` | unpatched v0.14.2 | FAIL (confirms vuln) |
| `TestRepositoryPathTraversalNeutralized` | unpatched v0.14.2 | FAIL (confirms vuln) |

Edge cases tested: `../data/tmp/local-r/1/nested` (the exact payload from the
repro), `../../etc`, backslash variant `..\..\windows`, empty name, oversize
name, and legitimate names (`my-org`, `my_org`, `my.org`, `org123`).

## Remaining Concerns

- **Defense-in-depth gap (non-reachable, not fixed by the official patch):**
  `database.RepoPath` (`internal/database/repo.go`) and `database.WikiPath`
  (`internal/database/wiki.go`) call `repoutil.UserPath(owner)` — which now
  cleans the owner — but still apply only `strings.ToLower(repoName)` to the
  repository name (no `pathutil.Clean`). This is **not exploitable** today
  because every repository-name entry point already enforces `AlphaDashDot`
  (which blocks `/`), so a traversal sequence can never reach these functions.
  The official v0.14.3 release left these as-is. For additional defense in
  depth they could be migrated to `repoutil.RepositoryPath` or wrapped with
  `pathutil.Clean`; doing so is out of scope for a minimal CVE fix and risks
  touching deprecated helpers.
- **Scope of this patch:** It fixes the owner/org-name traversal (the confirmed
  RCE chain) at both the path and API layers, matching the proven v0.14.3
  remediation. The full end-to-end RCE chain (real Gogs server, Git smart-HTTP
  hook planting, web-upload sync, `git receive-pack`) was already demonstrated
  by the repro/variant agents on v0.14.2 (RCE) and v0.14.3 (no RCE); this patch
  brings v0.14.2 to the v0.14.3 behavior at the two code locations that
  eliminate the traversal.
