* [PATCH] gitweb: plug various command-line option injection holes
@ 2025-09-02 19:11 Taylor Blau
2025-09-02 21:52 ` Eric Sunshine
0 siblings, 1 reply; 2+ messages in thread
From: Taylor Blau @ 2025-09-02 19:11 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Moritz Sanft, Jonathan Nieder
When running Gitweb and loading a blobdiff with the "hpb" ("hash parent
base") query parameter set to a valid diff-tree option, say,
"--output=/tmp/pwned", Gitweb will faithfully execute "diff-tree"
internally (via "sub git_blobdiff") and blindly pass in the "hpb" query
parameter.
In other words, visiting a URL like:
http://127.0.0.1:1234/?p=<PROJECT_NAME>;a=blobdiff;f=*;hpb=--output=/tmp/pwned;hb=HEAD
will result in the file "/tmp/pwned" being created. This happens as a
result of gitweb executing something like:
git diff-tree -r -M --output=/tmp/pwned HEAD --
, where "--output=/tmp/pwned" is substituted in as the value of
"$hash_parent_base".
There are various other spots in Gitweb which are too eager to pass
untrusted query parameter values as command-line arguments, leading to
at least the above option-injection attack, and likely many others.
Since 51b4594b40 (parse-options: allow --end-of-options as a synonym for
"--", 2019-08-06), we have the "--end-of-options" command-line flag as
a standard mechanism to indicate that any further argument should not be
interpreted as command-line options.
Guard agains this and other option-injection attacks by placing the
"--end-of-options" flag before any untrusted user-input in any place
that gitweb spawns Git as a sub-process.
Reported-by: Moritz Sanft <moritz.sanft@outlook.de>
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
gitweb/gitweb.perl | 77 +++++++++++++++++++++++++++++++---------------
1 file changed, 52 insertions(+), 25 deletions(-)
diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl
index b5490dfecf2..a0f2c981e33 100755
--- a/gitweb/gitweb.perl
+++ b/gitweb/gitweb.perl
@@ -2722,7 +2722,7 @@ sub git_get_hash {
my $retval = undef;
$git_dir = "$projectroot/$project";
if (open my $fd, '-|', git_cmd(), 'rev-parse',
- '--verify', '-q', @options, $hash) {
+ '--verify', '-q', @options, '--end-of-options', $hash) {
$retval = <$fd>;
chomp $retval if defined $retval;
close $fd;
@@ -2737,7 +2737,9 @@ sub git_get_hash {
sub git_get_type {
my $hash = shift;
- open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
+ open my $fd, "-|", git_cmd(), "cat-file", '-t', '--end-of-options',
+ $hash
+ or return;
my $type = <$fd>;
close $fd or return;
chomp $type;
@@ -2885,7 +2887,8 @@ sub git_get_hash_by_path {
$path =~ s,/+$,,;
- open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
+ open my $fd, "-|", git_cmd(), "ls-tree", "--end-of-options", $base,
+ "--", $path
or die_error(500, "Open git-ls-tree failed");
my $line = <$fd>;
close $fd or return undef;
@@ -2912,7 +2915,8 @@ sub git_get_path_by_hash {
local $/ = "\0";
- open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
+ open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z',
+ '--end-of-options', $base
or return undef;
while (my $line = <$fd>) {
chomp $line;
@@ -3334,6 +3338,7 @@ sub git_get_last_activity {
'--format=%(committer)',
'--sort=-committerdate',
'--count=1',
+ '--end-of-options',
map { "refs/$_" } get_branch_refs ()) or return;
my $most_recent = <$fd>;
close $fd or return;
@@ -3390,6 +3395,7 @@ sub git_get_references {
# 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
# c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
+ "--end-of-options",
($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
or return;
@@ -3410,7 +3416,8 @@ sub git_get_references {
sub git_get_rev_name_tags {
my $hash = shift || return undef;
- open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
+ open my $fd, "-|", git_cmd(), "name-rev", "--tags", "--end-of-options",
+ $hash
or return;
my $name_rev = <$fd>;
close $fd;
@@ -3472,7 +3479,9 @@ sub parse_tag {
my %tag;
my @comment;
- open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
+ open my $fd, "-|", git_cmd(), "cat-file", "tag", "--end-of-options",
+ $tag_id
+ or return;
$tag{'id'} = $tag_id;
while (my $line = <$fd>) {
chomp $line;
@@ -3600,6 +3609,7 @@ sub parse_commit {
"--parents",
"--header",
"--max-count=1",
+ "--end-of-options",
$commit_id,
"--",
or die_error(500, "Open git-rev-list failed");
@@ -3624,6 +3634,7 @@ sub parse_commits {
("--max-count=" . $maxcount),
("--skip=" . $skip),
@extra_options,
+ "--end-of-options",
$commit_id,
"--",
($filename ? ($filename) : ())
@@ -3784,6 +3795,7 @@ sub git_get_heads_list {
($limit ? '--count='.($limit+1) : ()),
'--sort=-HEAD', '--sort=-committerdate',
'--format=%(objectname) %(refname) %(subject)%00%(committer)',
+ '--end-of-options',
@patterns
or return;
while (my $line = <$fd>) {
@@ -3998,7 +4010,8 @@ sub run_highlighter {
close $fd;
my $syntax_arg = (defined $syntax) ? "--syntax $syntax" : "--force";
- open $fd, quote_command(git_cmd(), "cat-file", "blob", $hash)." | ".
+ open $fd, quote_command(git_cmd(), "cat-file", "blob",
+ "--end-of-options", $hash)." | ".
quote_command($^X, '-CO', '-MEncode=decode,FB_DEFAULT', '-pse',
'$_ = decode($fe, $_, FB_DEFAULT) if !utf8::decode($_);',
'--', "-fe=$fallback_encoding")." | ".
@@ -4687,7 +4700,8 @@ sub git_get_link_target {
my $link_target;
# read link
- open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
+ open my $fd, "-|", git_cmd(), "cat-file", "blob", "--end-of-options",
+ $hash
or return;
{
local $/ = undef;
@@ -6377,7 +6391,7 @@ sub git_search_files {
local $/ = "\n";
open my $fd, "-|", git_cmd(), 'grep', '-n', '-z',
- $search_use_regexp ? ('-E', '-i') : '-F',
+ $search_use_regexp ? ('-E', '-i') : '-F', '--end-of-options',
$searchtext, $co{'tree'}
or die_error(500, "Open git-grep failed");
@@ -6768,17 +6782,18 @@ sub git_blame_common {
my $fd;
if ($format eq 'incremental') {
# get file contents (as base)
- open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash
+ open $fd, "-|", git_cmd(), 'cat-file', 'blob',
+ '--end-of-options', $hash
or die_error(500, "Open git-cat-file failed");
} elsif ($format eq 'data') {
# run git-blame --incremental
open $fd, "-|", git_cmd(), "blame", "--incremental",
- $hash_base, "--", $file_name
+ "--end-of-options", $hash_base, "--", $file_name
or die_error(500, "Open git-blame --incremental failed");
} else {
# run git-blame --porcelain
open $fd, "-|", git_cmd(), "blame", '-p',
- $hash_base, '--', $file_name
+ "--end-of-options", $hash_base, '--', $file_name
or die_error(500, "Open git-blame --porcelain failed");
}
binmode $fd, ':utf8';
@@ -7058,7 +7073,8 @@ sub git_blob_plain {
$expires = "+1d";
}
- open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
+ open my $fd, "-|", git_cmd(), "cat-file", "blob", "--end-of-options",
+ $hash
or die_error(500, "Open git-cat-file blob '$hash' failed");
# content-type (can include charset)
@@ -7121,7 +7137,8 @@ sub git_blob {
}
my $have_blame = gitweb_check_feature('blame');
- open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
+ open my $fd, "-|", git_cmd(), "cat-file", "blob", "--end-of-options",
+ $hash
or die_error(500, "Couldn't cat $file_name, $hash");
my $mimetype = blob_mimetype($fd, $file_name);
# use 'blob_plain' (aka 'raw') view for files that cannot be displayed
@@ -7216,7 +7233,8 @@ sub git_tree {
{
local $/ = "\0";
open my $fd, "-|", git_cmd(), "ls-tree", '-z',
- ($show_sizes ? '-l' : ()), @extra_options, $hash
+ ($show_sizes ? '-l' : ()), @extra_options,
+ "--end-of-options", $hash
or die_error(500, "Open git-ls-tree failed");
@entries = map { chomp; $_ } <$fd>;
close $fd
@@ -7417,7 +7435,7 @@ sub git_snapshot {
my $cmd = quote_command(
git_cmd(), 'archive',
"--format=$known_snapshot_formats{$format}{'format'}",
- "--prefix=$prefix/", $hash);
+ "--prefix=$prefix/", "--end-of-options", $hash);
if (exists $known_snapshot_formats{$format}{'compressor'}) {
$cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
}
@@ -7569,6 +7587,7 @@ sub git_commit {
open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
@diff_opts,
(@$parents <= 1 ? $parent : '-c'),
+ "--end-of-options",
$hash, "--"
or die_error(500, "Open git-diff-tree failed");
@difftree = map { chomp; $_ } <$fd>;
@@ -7649,7 +7668,8 @@ sub git_object {
my $object_id = $hash || $hash_base;
open my $fd, "-|", quote_command(
- git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
+ git_cmd(), 'cat-file', '-t', '--end-of-options',
+ $object_id) . ' 2> /dev/null'
or die_error(404, "Object does not exist");
$type = <$fd>;
defined $type && chomp $type;
@@ -7660,11 +7680,13 @@ sub git_object {
} elsif ($hash_base && defined $file_name) {
$file_name =~ s,/+$,,;
- system(git_cmd(), "cat-file", '-e', $hash_base) == 0
+ system(git_cmd(), "cat-file", '-e', '--end-of-options',
+ $hash_base) == 0
or die_error(404, "Base object does not exist");
# here errors should not happen
- open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
+ open my $fd, "-|", git_cmd(), "ls-tree", "--end-of-options",
+ $hash_base, "--", $file_name
or die_error(500, "Open git-ls-tree failed");
my $line = <$fd>;
close $fd;
@@ -7700,7 +7722,7 @@ sub git_blobdiff {
if (defined $file_name) {
# read raw output
open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
- $hash_parent_base, $hash_base,
+ "--end-of-options", $hash_parent_base, $hash_base,
"--", (defined $file_parent ? $file_parent : ()), $file_name
or die_error(500, "Open git-diff-tree failed");
@difftree = map { chomp; $_ } <$fd>;
@@ -7715,7 +7737,8 @@ sub git_blobdiff {
# read filtered raw output
open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
- $hash_parent_base, $hash_base, "--"
+ "--end-of-options", $hash_parent_base, $hash_base,
+ "--"
or die_error(500, "Open git-diff-tree failed");
@difftree =
# ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
@@ -7751,6 +7774,7 @@ sub git_blobdiff {
# open patch output
open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
'-p', ($format eq 'html' ? "--full-index" : ()),
+ "--end-of-options",
$hash_parent_base, $hash_base,
"--", (defined $file_parent ? $file_parent : ()), $file_name
or die_error(500, "Open git-diff-tree failed");
@@ -7945,7 +7969,7 @@ sub git_commitdiff {
if ($format eq 'html') {
open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
"--no-commit-id", "--patch-with-raw", "--full-index",
- $hash_parent_param, $hash, "--"
+ "--end-of-options", $hash_parent_param, $hash, "--",
or die_error(500, "Open git-diff-tree failed");
while (my $line = <$fd>) {
@@ -7957,7 +7981,8 @@ sub git_commitdiff {
} elsif ($format eq 'plain') {
open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
- '-p', $hash_parent_param, $hash, "--"
+ '-p', '--end-of-options', $hash_parent_param, $hash,
+ "--"
or die_error(500, "Open git-diff-tree failed");
} elsif ($format eq 'patch') {
# For commit ranges, we limit the output to the number of
@@ -7982,7 +8007,8 @@ sub git_commitdiff {
push @commit_spec, '--root', $hash;
}
open $fd, "-|", git_cmd(), "format-patch", @diff_opts,
- '--encoding=utf8', '--stdout', @commit_spec
+ '--encoding=utf8', '--stdout', '--end-of-options',
+ @commit_spec
or die_error(500, "Open git-format-patch failed");
} else {
die_error(400, "Unknown commitdiff format");
@@ -8334,7 +8360,8 @@ sub git_feed {
# get list of changed files
open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
$co{'parent'} || "--root",
- $co{'id'}, "--", (defined $file_name ? $file_name : ())
+ $co{'id'}, "--end-of-options", "--",
+ (defined $file_name ? $file_name : ())
or next;
my @difftree = map { chomp; $_ } <$fd>;
close $fd
base-commit: 2462961280690837670d997bde64bd4ebf8ae66d
--
2.51.0.179.g22c8463a5ee
^ permalink raw reply related [flat|nested] 2+ messages in thread
* Re: [PATCH] gitweb: plug various command-line option injection holes
2025-09-02 19:11 [PATCH] gitweb: plug various command-line option injection holes Taylor Blau
@ 2025-09-02 21:52 ` Eric Sunshine
0 siblings, 0 replies; 2+ messages in thread
From: Eric Sunshine @ 2025-09-02 21:52 UTC (permalink / raw)
To: Taylor Blau; +Cc: git, Junio C Hamano, Moritz Sanft, Jonathan Nieder
On Tue, Sep 2, 2025 at 3:11 PM Taylor Blau <me@ttaylorr.com> wrote:
> When running Gitweb and loading a blobdiff with the "hpb" ("hash parent
> base") query parameter set to a valid diff-tree option, say,
> "--output=/tmp/pwned", Gitweb will faithfully execute "diff-tree"
> internally (via "sub git_blobdiff") and blindly pass in the "hpb" query
> parameter.
>
> In other words, visiting a URL like:
>
> http://127.0.0.1:1234/?p=<PROJECT_NAME>;a=blobdiff;f=*;hpb=--output=/tmp/pwned;hb=HEAD
>
> will result in the file "/tmp/pwned" being created. This happens as a
> result of gitweb executing something like:
>
> git diff-tree -r -M --output=/tmp/pwned HEAD --
>
> , where "--output=/tmp/pwned" is substituted in as the value of
> "$hash_parent_base".
>
> There are various other spots in Gitweb which are too eager to pass
> untrusted query parameter values as command-line arguments, leading to
> at least the above option-injection attack, and likely many others.
>
> Since 51b4594b40 (parse-options: allow --end-of-options as a synonym for
> "--", 2019-08-06), we have the "--end-of-options" command-line flag as
> a standard mechanism to indicate that any further argument should not be
> interpreted as command-line options.
>
> Guard agains this and other option-injection attacks by placing the
> "--end-of-options" flag before any untrusted user-input in any place
> that gitweb spawns Git as a sub-process.
s/agains/against/
> Reported-by: Moritz Sanft <moritz.sanft@outlook.de>
> Signed-off-by: Taylor Blau <me@ttaylorr.com>
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2025-09-02 21:52 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-09-02 19:11 [PATCH] gitweb: plug various command-line option injection holes Taylor Blau
2025-09-02 21:52 ` Eric Sunshine
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).