Overview
Rapid7 Labs discovered a critical argument injection (CWE-88) vulnerability in Gogs, a popular open-source self-hosted Git service. Rapid7 Labs scores this vulnerability as CVSSv4 9.4 (Critical). The vulnerability allows any authenticated user to achieve remote code execution (RCE) on the server by creating a pull request with a malicious branch name that injects the --exec flag into git rebase during the "Rebase before merging" merge operation. At the time of publication, the vendor has not released a patch.
The exploit requires no admin privileges and no interaction with other users; an attacker operates entirely within their own account. Since Gogs ships with open registration enabled by default (DISABLE_REGISTRATION = false) and no limit on repository creation (MAX_CREATION_LIMIT = -1), an unauthenticated attacker can simply create an account and repository on any default-configured instance. Any registered user who creates a repo is automatically its owner. From there, enabling rebase merging is a single toggle in settings, and the entire exploit chain can be operated without interaction from any other user.
Alternatively, any user with write access to a repository where rebase is already enabled can exploit it directly. On instances where repository creation is restricted, an attacker still only needs write access to any repository that has (or can have) rebase merging enabled.
The result is arbitrary command execution as the Gogs server process user, giving the attacker the ability to compromise the server, read every repository on the instance (including other users' private repos), dump credentials (password hashes, API tokens, SSH keys, 2FA secrets), pivot to other network-accessible systems, and modify any hosted repository's code.
The latest release versions at the time of research, Gogs 0.14.2 and 0.15.0+dev (commit b53d3162), were confirmed to be affected. All prior versions supporting the "Rebase before merging" style are likely vulnerable as well.
Product description
Gogs is a lightweight, self-hosted Git service written in Go. With ~50,000 GitHub stars and over 5,000 forks, it's one of the more popular self-hosted alternatives to GitHub, commonly deployed by companies, universities, and open-source projects.
A Shodan search for http.title:"Gogs" http.title:"Sign In" returns 1,141 internet-facing instances at the time of publication. The real install base is much larger since most deployments sit behind VPNs or internal networks.
Credit
This vulnerability was discovered by Jonah Burgess (CryptoCat), Senior Security Researcher at Rapid7, and is being disclosed in accordance with Rapid7's vulnerability disclosure policy.
Impact
Any Gogs instance with more than one user account is effectively "multi-tenant", meaning each user has their own repositories, credentials, and data on a shared server. This is the default for organizations, universities, and teams that use Gogs as a shared Git hosting platform. On any such instance, this vulnerability gives a single authenticated user full control of the underlying server. The attacker operates entirely within their own repository; no access to other users' repos is needed.
The vulnerability affects all supported platforms (Linux, macOS, Windows) and installation methods (pre-built binary, Docker, source). On Docker installations, the Gogs process runs as the git user (UID 1000 by default). On binary installations, the process user depends on how the administrator deployed the service (commonly git or a dedicated service account).
The practical impact:
Server compromise: Arbitrary command execution as the Gogs process user (typically git)
Cross-tenant data breach: Read every repository on the instance, including other users' private repos
Credential theft: Dump the database containing password hashes, API tokens, SSH keys, and 2FA secrets for all users
Lateral movement: Pivot to other systems reachable from the server's network
Supply chain attacks: Modify any hosted repository's code. The Gogs process user (typically git) has direct filesystem-level read/write access to every repository on the instance under a single REPOSITORY_ROOT directory, with no OS-level isolation between repositories. Direct filesystem manipulation bypasses Gogs' audit logging, and without commit signing (uncommon on self-hosted instances), forged commits are difficult to detect.
The exploit is fully automatable (a Metasploit module is provided) and runs in seconds. When the attacker creates and deletes their own repository, the only trace is an HTTP 500 in the server logs. When exploiting an existing repository, additional artifacts remain (see heading Indicators of compromise).
Technical analysis
The testing target was a Gogs 0.14.2 installation running via Docker on Linux (Ubuntu 24.04). The vulnerability was also confirmed on Gogs 0.15.0+dev (commit b53d3162). As noted above, the vulnerability affects all supported platforms (Linux, macOS, Windows) and installation methods.
Background: Merge vs. rebase in Gogs
A standard merge creates a merge commit joining two branch histories. A rebase before merge replays the head branch's commits on top of the base branch to produce a linear history. Under the hood, Gogs runs git rebase <base_branch> <head_branch> in a temp directory before pushing the result.
Critically, git rebase accepts an --exec flag that tells Git to run a shell command (via sh -c) after replaying each commit. Argument injection into --exec has been a recurring source of RCE vulnerabilities in Git-based applications. This is the exploitation primitive.
Gogs exposes 'Rebase before merging' as a per-repo setting (PullsAllowRebase). It is not enabled by default, but any repo owner or admin can enable it under Settings > Advanced. By default, any user who creates a repo is automatically its owner, so the barrier to exploitation is low. Administrators can restrict repo creation globally (MAX_CREATION_LIMIT = 0 in app.ini) or per-user (via Max Repo Creation in the admin panel), but this does not prevent exploitation by users with write access to existing repositories.
Root cause
The Merge() function in internal/database/pull.go passes the PR's base branch name directly to git rebase without a -- separator (a POSIX convention that signals the end of options, preventing subsequent arguments from being interpreted as flags):
if _, stderr, err = process.ExecDir(-1, tmpBasePath,
fmt.Sprintf("PullRequest.Merge (git rebase): %s", tmpBasePath),
"git", "rebase", "--quiet", pr.BaseBranch, remoteHeadBranch); err != nil {⠀
pr.BaseBranch comes from the URL parameter in internal/route/repo/pull.go:
baseRef := infos[0] // from strings.Split(c.Params("*"), "...")⠀
Both baseRef and headRef are validated via RevParse before the PR is created. RevParse is defined in the external git-module library and works by calling git rev-parse --verify <ref>, which only checks whether the ref resolves to a valid Git object. It does not sanitize against argument injection, and it does not need to since git rev-parse --verify treats --exec=... as a ref name and fails if it doesn't resolve. However, the attacker pushes the malicious branch name (e.g. --exec=<payload>) to the repo first, so RevParse succeeds because the ref genuinely exists. The value is stored in the database and later passed as-is to the rebase command.
Crafting the payload
Git branch names can legally contain $, {, }, =, and -. An attacker creates a branch named:
--exec=touch${IFS}/tmp/rce_proof⠀
When this is used as pr.BaseBranch, the rebase command becomes:
git rebase --quiet '--exec=touch${IFS}/tmp/rce_proof' 'head_repo/feature'⠀
Git's argument parser treats --exec=touch${IFS}/tmp/rce_proof as the --exec flag, not a branch name. --exec runs the value via sh -c after each replayed commit, and ${IFS} expands to a space in the shell, bypassing Git's prohibition on spaces in branch names.
For commands containing characters forbidden in Git refs (:, ~, ^, ?, *, [, \, //), such as URLs, the payload is base64-encoded:
--exec=echo${IFS}<base64_payload>|base64${IFS}-d|sh⠀
The vulnerability affects Windows installations as well, but the payload delivery method differs. On Linux, the payload can be base64-encoded inline in the branch name (e.g. --exec=echo${IFS}<b64>|base64${IFS}-d|sh). On Windows, this fails because NTFS forbids the | (pipe) character in filenames, and Git stores branch refs as files at refs/heads/<branch_name>.
The solution is file-based payload delivery where the exploit commits a script file (e.g. .abcdef) to the repository and uses a short, filesystem-safe branch name: --exec=sh${IFS}.abcdef. An additional complication is that MSYS2's sh (bundled with Git for Windows) mangles shell metacharacters like $, &, and backticks in the payload before PowerShell can process them. To avoid this, the script file invokes cmd.exe //c .abcdef.bat (where //c is the MSYS2 escaping for /c), which natively executes the .bat file containing the PowerShell payload without shell interpretation issues. The Metasploit module implements this cross-platform approach automatically.
Execution flow during Merge()
The MergeStyleRebase code path in Merge() runs these Git commands sequentially:
Step | Command | Result with malicious branch |
|---|---|---|
git clone -b '<malicious>' <repo> <tmp> | Succeeds - -b consumes --exec=... as the branch value | |
git remote add head_repo <repo> + git fetch head_repo | Succeeds normally | |
git rebase --quiet '<malicious>' 'head_repo/feature' | RCE fires here. --exec=<cmd> parsed as flag, command runs via sh -c | |
git checkout -b <tmpBranch> | Succeeds (tmpBranch is a server-generated timestamp) | |
git checkout '<malicious>' | Fails - Git interprets --exec=... as an invalid option for checkout |
⠀
Step 5 fails and Merge() returns HTTP 500, but the RCE already fired at Step 3. The 500 gets logged but doesn't undo anything.
Because the merge aborts partway through, the repository's git state is left corrupted (stuck in a partial rebase). This means the exploit can only be fired once per repository. In cases where the attacker created the repo themselves, this doesn't matter since the repo is deleted afterward, but when targeting an existing repository, the repo is effectively burned after a single use.
Why the PR becomes mergeable
For the exploit to work, the PR needs to reach "Mergeable" status so the merge button is available. This depends on an interesting race condition in how Gogs validates PRs:
During PR creation, testPatch() calls UpdateLocalCopyBranch(pr.BaseBranch). For a fresh repo with no local copy, it takes the Clone path, which includes --end-of-options. The malicious branch name is treated as data, clone succeeds, testPatch completes normally.
Since testPatch didn't flag a conflict, the status gets promoted to PullRequestStatusMergeable.
The background TestPullRequests goroutine periodically re-checks PRs. On the next call, the local copy does exist, so UpdateLocalCopyBranch takes the Checkout path instead. This one is missing --end-of-options, so the checkout fails.
That error causes TestPullRequests to skip checkAndUpdateStatus(), meaning the PR stays Mergeable forever.
The default exploit path creates a fresh repository, so the first testPatch always hits the Clone path and succeeds. The same applies when targeting an existing repository that has never had a PR created against it. If the target repo has had prior PRs, the local copy already exists, Checkout fails, and the PR cannot be created.
Relationship to prior argument injection fixes
Gogs has addressed argument injection vulnerabilities across multiple prior advisories. This vulnerability is in the same class but affects a different code path (Merge()) that was never patched:
CVE | Description | Fix Applied | Advisory |
|---|---|---|---|
Argument injection when tagging new releases | Added -- separator to git tag | ||
Argument injection during changes preview | Added --end-of-options to git diff | ||
Release tag option injection in deletion | Migrated to safe git-module API | ||
Argument injection in built-in SSH server | Added -- separator to git upload-pack / git receive-pack |
The git-module library (v1.8.7) was hardened with --end-of-options across Clone(), Push(), Fetch(), and 28 other call sites. However, the Merge() function in internal/database/pull.go bypasses all of these protections because it uses raw process.ExecDir (wrapping exec.Command directly) instead of the safe git-module API. The git rebase call was never migrated.
Exploitation
The Metasploit module automates the full exploit chain against both Linux and Windows targets and supports two modes of operation:
own_repo (default): The module creates a temporary repository under the attacker's account, runs the exploit, and deletes the repo on cleanup. This works on any default-configured instance and supports all payload types.
existing_repo: The module targets a repository the attacker already has write and merge access to. This is useful on instances where repo creation is restricted. Only command payloads are supported in this mode (staged payloads would require multiple merge cycles, which is not possible due to the repo corruption described above). Cleanup deletes the malicious branches and closes the PR, but the repository's git state remains corrupted.

⠀
On Windows, the module uses the file-based delivery method described above to work around NTFS filename restrictions.
⠀
Figure 2: Metasploit module obtaining a Meterpreter session on a Gogs 0.14.2 instance running on Windows 11.
Indicators of compromise (IoCs)
Defenders should watch the Gogs server logs for error entries matching this pattern:
[E] ...merge: git checkout '--exec=<...>': exit status 128 - error: unknown option `exec=<...>'⠀
This is logged via c.Error(err, "merge"), which writes the full error (including the malicious branch name) to the server log at ERROR level. Note that a more cleverly written exploit may not be this obvious in log files.
If the attack targeted an existing repository (rather than one the attacker created and deleted), additional artifacts will be present: the malicious branch name (e.g. --exec=...) in the repository's branch listing, a failed pull request in the PR history, and the repository itself will be in a corrupted git state (returning HTTP 500 on certain operations). On Windows, the committed payload files (e.g. .abcdef, .abcdef.bat) will also remain in the git history. Administrators should audit repositories for branch names beginning with --.
The Metasploit module also creates a Gogs API token (named msf_<hex>) during exploitation. Gogs does not expose a token deletion API endpoint, so this token persists after the attack and remains valid until manually revoked via the web UI or database. Defenders should check user token lists at /-/user/settings/applications for unexpected entries.
The payload file used during exploitation is written to the repository's bare git directory on the server filesystem and will persist after the attack.
Remediation
No patch is available at the time of publication. Rapid7 reported this vulnerability to the Gogs maintainers on March 17, 2026, and followed up multiple times through May 2026. The maintainer acknowledged receipt on March 28, 2026, but has not provided a fix or further response. Users of Gogs should evaluate the following mitigations:
Restricting user registration (DISABLE_REGISTRATION = true in app.ini) to prevent untrusted users from creating accounts. This is the most impactful mitigation since the exploit is self-contained within a single user's repository.
Restricting repository creation (MAX_CREATION_LIMIT = 0 in app.ini) to prevent users from creating their own repos. This can also be set per-user via Max Repo Creation in the admin panel. This blocks the easiest attack path (creating a new repo with rebase enabled), but does not prevent exploitation by users with write access to existing repositories.
Auditing rebase merge settings: While "Rebase before merging" can be disabled per-repo under Settings > Advanced, note that this is not an effective defense against a malicious user who owns or has admin access to a repo, since they can re-enable rebase at will. There is no global or organization-level setting to restrict this. Disabling rebase is only useful for reducing the attack surface on shared repositories where the attacker has write access but not admin privileges.
Disclosure timeline
March 16, 2026: Vulnerability discovered and validated against Gogs 0.14.2 and 0.15.0+dev (commit b53d3162).
March 17, 2026: Reported to Gogs maintainers via GitHub Security Advisory (GHSA-qf6p-p7ww-cwr9).
March 28, 2026: Maintainer acknowledges receipt.
April 21, 2026: Contacted maintainer for a status update (no response).
May 6, 2026: Reminded maintainer of previously planned disclosure date, and offered extension if required (no response).
May 20, 2026: Advised maintainer the blog release date is finalized for May 28, 2026 (no response).
May 28, 2026: This disclosure.
Related blog posts

Threat Research
Q1 2026 Threat Landscape Report: Zero-clicks, geopolitical tensions, and some wins for law enforcement
Rapid7 Labs

Vulnerabilities and Exploits
CVE-2026-20182: Critical authentication bypass in Cisco Catalyst SD-WAN Controller (FIXED)
Jonah Burgess, Stephen Fewer

Vulnerabilities and Exploits
The Dark Side of Efficiency: When Network Controllers Become "God Mode" for Attackers
Douglas McKee, Director, Vulnerability Intelligence

Threat Research
When IT Support Calls: Dissecting a ModeloRAT Campaign from Teams to Domain Compromise
Anna Širokova

