Git flag injection: local file overwrite to remote code execution
User-controlled input was passed to a git command without separating options from arguments, letting a researcher inject git flags. That primitive enabled writing an arbitrary local file, which was escalated to remote code execution on GitLab infrastructure - a great lesson in argument injection.
Read the original HackerOne reportA GitLab feature passed user-controlled input directly to a git command without separating option flags from positional arguments - a subtle but critical mistake that let a researcher inject git flags, write an arbitrary file to the server, and escalate all the way to remote code execution.
Stay Legal
This breakdown is for educational purposes only. Argument injection and RCE attacks against systems you do not own or have explicit written authorization to test are illegal and harmful. Only practice these techniques in authorized environments such as CTF machines, your own GitLab instance, or approved penetration testing engagements.
The Target
GitLab's application server - specifically a feature that constructs and executes a git command using a value supplied or influenced by the user. GitLab extensively shells out to the git binary for repository operations, and user-supplied inputs (branch names, file paths, repository identifiers) frequently appear in those command constructions.
The Vulnerability
This was argument injection (also called flag injection or option injection) leading to RCE. The root cause is the absence of the -- separator in a git command invocation.
In POSIX command-line conventions, -- signals the end of option flags. Everything after -- is treated as a positional argument, never as a flag or option - even if it starts with - or --. When user input is appended to a git command without a preceding --, any value beginning with - or -- is interpreted by git as a command-line flag.
A simplified illustration of the vulnerable pattern:
# VULNERABLE - user input treated as part of the option space
import subprocess
subprocess.run(["git", "clone", user_supplied_repo_url])
# SAFE - -- separator ensures user_supplied_value is a positional argument
subprocess.run(["git", "clone", "--", user_supplied_repo_url])Git's rich set of flags includes several that write to or execute files. The researcher identified a git flag (such as --upload-pack, --exec, or a similar execution-enabling option depending on the specific git subcommand) that, when injected, caused git to write or execute attacker-controlled content on the server filesystem.
How It Was Found
The researcher audited GitLab's codebase and identified locations where user input flowed into git subcommand invocations without a -- separator. For each candidate, they tested whether a value beginning with -- was treated as a flag or rejected as invalid input.
An illustrative injected value to test for flag injection:
# Instead of a normal branch name like "main", supply:
--upload-pack=touch /tmp/pwned
# If GitLab runs something like:
# git clone <user-input> /tmp/repo
# The injected value becomes:
# git clone --upload-pack=touch /tmp/pwned /tmp/repo
# git interprets --upload-pack as an option specifying a command to run
# during the clone's pack-objects negotiation phase.The path to file write and then RCE followed this sequence:
# Step 1: Confirm flag injection - inject a harmless probe flag
# and observe whether git behaves differently (error message changes,
# git accepts the flag, etc.)
# Step 2: Use a git flag that writes to a controlled path
# (e.g., --local-env-vars, output redirection via --upload-pack, etc.)
# Step 3: Write a payload to a location that GitLab will subsequently
# execute - a hook script, a configuration file, or a file in a
# directory that gets sourced or eval'd.
# Step 4: Trigger execution of the written file through normal
# GitLab operations (push, pull, webhook, etc.) Impact
- Arbitrary local file write on the GitLab server - the researcher could create or overwrite files at paths accessible to the GitLab application process.
- Remote code execution - by writing to a git hook path (e.g.,
hooks/post-receive), the researcher's script was automatically invoked by subsequent git operations, achieving code execution in the context of the GitLab server process. - A compromised GitLab server has access to all hosted repository source code, CI/CD secrets, deploy keys, and potentially credentials for connected infrastructure.
- GitLab awarded $12,000 and classified this as critical severity.
The Fix
Argument injection is prevented by enforcing the boundary between options and data:
- Always include
--before user-supplied values in shell-out commands. This single character guarantees the value is treated as a positional argument, never as a flag. - Use library bindings instead of shelling out. Libraries like
libgit2(viapygit2,rugged, etc.) accept arguments as typed parameters, not as strings parsed for flags. - Validate inputs against an allowlist of expected formats. Branch names, tag names, and file paths have well-defined character sets. Reject anything containing characters outside the expected set.
- Apply least privilege - the process invoking git should run with minimal filesystem permissions, limiting the damage from an arbitrary file write even if injection is achieved.
- Audit all
exec/popen/subprocesscalls in application code for user-influenced arguments, especially those invokinggit,svn,hg, or similar tools with rich CLI options.
What You Can Learn
- The
--separator is a security boundary. Its absence is not just a style issue - it is a concrete vulnerability class. Every invocation of a subprocess with user input should use it. - Argument injection is subtle. Unlike command injection (which injects entire shell commands via metacharacters like
;or|), argument injection works within the intended subprocess call - it may not be caught by basic input filters that only block shell metacharacters. - File write is often a stepping stone to RCE. In many frameworks, config files, hook scripts, and template files are executed or sourced by the application. A controlled file write to the right path is equivalent to RCE.
- Git has a large attack surface.
gitis a powerful, decades-old tool with hundreds of flags, many of which can influence external commands, file paths, and network behavior. Applications that shell out to git must treat every user-influenced argument with care. - Code review for shell-out patterns pays off. Auditing all places where an application constructs and executes external commands - looking specifically for user input in the argument list and the absence of
--- is a high-signal review strategy for this class of bug.
Canonical Report
Full technical details are in the original HackerOne disclosure: HackerOne #658013 - Git flag injection leading to local file write and RCE on GitLab
Learn the skill behind it
Linux Privilege Escalation