1. Overview
Gogs is an open-source, self-hosted Git service written in Go. CVE-2026–52813 stems from missing validation on the organization name at creation time, and it escalates into something far more serious: an authenticated, ordinary user can write files to an arbitrary path on the server’s filesystem, and combine that with a Git hook to achieve remote code execution (RCE).
Because Gogs allows any signed-up user to create an organization by default, this means a plain user registration — with no admin privileges required — is enough to fully compromise the server. That’s exactly why it earned a perfect CVSS score of 10.0.
In this post, we’ll walk through the vulnerability at the source-code level and explain, in plain terms, why this particular code leads all the way to RCE.
2. Where It Starts: Missing Validation on the Organization Name
When an organization is created in Gogs, the org-creation logic in internal/database/org.go runs internally and creates a directory using the organization name as the folder name. The problematic code looks like this:
The key issue here is that org.Name is passed straight into os.MkdirAll. org.Name is simply the organization-name string a user submits in an API request, and there was no filtering for path-traversal sequences (../) applied to it.
UserPath and RepositoryPath are the functions that actually build the storage path, and in the vulnerable version they're extremely simple:
filepath.Join will happily "normalize" a relative path segment like ../, but it won't block it. If you set owner to ../../../../tmp/test, filepath.Join just computes it as-is and returns /tmp/test — a path that sits above conf.Repository.Root entirely.
To summarize:
- Organization name = ../../../../tmp/test
- Creating a repository under this “organization” actually creates a Git bare repo (xxx.git) at /tmp/test on the server, well outside the Gogs repository root.
That alone already gives an attacker arbitrary path write access. But the truly dangerous part of this vulnerability is one step further: it can also be used to overwrite a Git hook file.
3. From Path Traversal to RCE: Git Hooks and the local-r Directory
When a user edits a file through the web editor, Gogs clones the repository the user is viewing into a local working copy on the server, and handles commit/push operations inside it. That temporary clone path looks roughly like this:
This directory is, in effect, “a directory Git can freely read and write to.” And a Git bare repository’s hooks/update script is a shell script that the server automatically executes every time a push comes in.
Here’s the attacker’s plan:
- Create an ordinary repository (writer) that you own, and find its internal repository ID (say, 1).
- Using that ID, create a new organization whose name is a path-traversal string:
- Create a new repository (rce) under this "organization." Because of the traversal, it actually gets created at nested/rce.git inside writer's local working copy.
- Clone writer locally, create a nested/rce.git/hooks/update file containing a malicious Bash script, and push it to writer. (From writer's perspective, this looks like an entirely ordinary commit that just adds one file.)
- Touch the rce repository again (e.g., via an API call) to trigger execution of that hooks/update script.
- The server executes the attacker’s Bash commands under the git account's privileges.
In other words, t*he “organization” built via path traversal, and the “repository” created under it, actually resolve to another repository’s hook directory* — and the attacker can rewrite that hook script’s contents using nothing more than the normal file-editing feature (web editor / commit). The actual PoC run ends with a command executed under server privileges, as shown below:
Since anyone can register and create an organization by default, this attack requires no admin privileges and no social engineering whatsoever. Just two API calls (create org + create repo) plus an ordinary commit/push is all it takes to complete an RCE chain.
Figure 1. The 5-step exploitation flow for CVE-2026–52813
① The attacker creates an ordinary repository (writer) and identifies its internal repository ID → ② A path-traversal string containing that ID is used as the organization name to create a new organization → ③ Creating a repository under that organization actually places it inside writer's local working copy (local-r/{ID}) → ④ The hooks/update file at that location is swapped for a malicious script disguised as an ordinary commit/push → ⑤ Touching the repository again triggers the hook, executing commands under the git account. The final step, shown in dark navy, is where the actual RCE occurs.
4. Patch Analysis
The Gogs team addressed this in 0.14.3 at three points.
*4–1. Introducing a Path-Normalization Function *(internal/repox/repox.go)
The newly added pathx.Clean() strips (or rejects) path-traversal elements like ../ from user/organization names and repository names. Where the code previously relied on filepath.Join alone, it now adds an extra layer that validates the input itself.
*4–2. Stricter Input Validation on the Organization Name *(internal/route/api/v1/org.go)
Previously, the only constraint on the organization name was that it be “required.” After the patch, AlphaDashDot (letters, digits, dashes, and dots only) and MaxSize(35) (a 35-character cap) were added. This means an organization name containing characters like / or .. is now rejected at the API layer before it ever reaches the filesystem code.
In other words, the patch consists of two layers: “input validation (allow-list based)” and “defense built into the path-construction function itself.” If one layer fails, the other still catches it. This kind of defense-in-depth is a good pattern to keep in mind when designing your own security patches.
Figure 2. The defense-in-depth structure of the patch
When user input (organization/repository name) comes in, the first layer — API-level AlphaDashDot + MaxSize(35) validation — filters out disallowed characters and excessive length. Even if there were a way to bypass that first layer, the second layer — pathx.Clean() in repox.go — strips traversal elements again at the moment the actual path is constructed. With only one of these layers in place, a bypass would remain possible; with both layered together, only a safe path is ever produced, even if one layer is defeated.
5. Key Takeaway (Developer’s Perspective)
Boiled down to the code level, this vulnerability has a single root cause:
A string the user fully controls was used directly in constructing a filesystem path.
filepath.Join only "normalizes" a path — it does nothing to stop .. from escaping the intended parent directory. Whenever a file path is built from user input, you should apply at least one of the following:
- Apply an allow-list of permitted characters to the input (letters, digits, and a small set of special characters only)
- After building the path, check for the presence of .., or verify (via a prefix check) that the final path actually falls under the intended root directory
- Where possible, map user input to an internal identifier (a UUID, for example) instead of using it directly as a file or directory name
Gogs’s own patch ultimately follows exactly this principle, combining a character allow-list (AlphaDashDot) with a path-sanitization function (pathx.Clean).
6. Mitigation
- Upgrade to Gogs 0.14.3 or later immediately.
- If an immediate upgrade isn’t feasible, as a temporary measure, consider disabling user registration / organization creation, or restricting it to trusted users only.
- It’s also worth separately auditing whether other API paths (invitations, migrations, etc.) could bypass the organization-name filtering.










Top comments (0)