{"thread":{"id":"64076","subject":"[PATCH] gitweb: plug various command-line option injection holes","startedAt":"2025-09-02T19:11:26Z","lastAt":"2025-09-02T21:52:25Z","messageCount":2,"participants":["Taylor Blau","Eric Sunshine"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"525369","messageId":"22c8463a5eebc609b4848f6db1db231fcce7d839.1756840261.git.me@ttaylorr.com","threadId":"64076","inReplyTo":null,"subject":"[PATCH] gitweb: plug various command-line option injection holes","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2025-09-02T19:11:23Z","receivedAt":"2025-09-02T19:11:26Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"When running Gitweb and loading a blobdiff with the \"hpb\" (\"hash parent\nbase\") query parameter set to a valid diff-tree option, say,\n\"--output=/tmp/pwned\", Gitweb will faithfully execute \"diff-tree\"\ninternally (via \"sub git_blobdiff\") and blindly pass in the \"hpb\" query\nparameter.\n\nIn other words, visiting a URL like:\n\n    http://127.0.0.1:1234/?p=<PROJECT_NAME>;a=blobdiff;f=*;hpb=--output=/tmp/pwned;hb=HEAD\n\nwill result in the file \"/tmp/pwned\" being created. This happens as a\nresult of gitweb executing something like:\n\n    git diff-tree -r -M --output=/tmp/pwned HEAD --\n\n, where \"--output=/tmp/pwned\" is substituted in as the value of\n\"$hash_parent_base\".\n\nThere are various other spots in Gitweb which are too eager to pass\nuntrusted query parameter values as command-line arguments, leading to\nat least the above option-injection attack, and likely many others.\n\nSince 51b4594b40 (parse-options: allow --end-of-options as a synonym for\n\"--\", 2019-08-06), we have the \"--end-of-options\" command-line flag as\na standard mechanism to indicate that any further argument should not be\ninterpreted as command-line options.\n\nGuard agains this and other option-injection attacks by placing the\n\"--end-of-options\" flag before any untrusted user-input in any place\nthat gitweb spawns Git as a sub-process.\n\nReported-by: Moritz Sanft <moritz.sanft@outlook.de>\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\n---\n gitweb/gitweb.perl | 77 +++++++++++++++++++++++++++++++---------------\n 1 file changed, 52 insertions(+), 25 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex b5490dfecf2..a0f2c981e33 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -2722,7 +2722,7 @@ sub git_get_hash {\n \tmy $retval = undef;\n \t$git_dir = \"$projectroot/$project\";\n \tif (open my $fd, '-|', git_cmd(), 'rev-parse',\n-\t    '--verify', '-q', @options, $hash) {\n+\t    '--verify', '-q', @options, '--end-of-options', $hash) {\n \t\t$retval = <$fd>;\n \t\tchomp $retval if defined $retval;\n \t\tclose $fd;\n@@ -2737,7 +2737,9 @@ sub git_get_hash {\n sub git_get_type {\n \tmy $hash = shift;\n \n-\topen my $fd, \"-|\", git_cmd(), \"cat-file\", '-t', $hash or return;\n+\topen my $fd, \"-|\", git_cmd(), \"cat-file\", '-t', '--end-of-options',\n+\t\t$hash\n+\t\tor return;\n \tmy $type = <$fd>;\n \tclose $fd or return;\n \tchomp $type;\n@@ -2885,7 +2887,8 @@ sub git_get_hash_by_path {\n \n \t$path =~ s,/+$,,;\n \n-\topen my $fd, \"-|\", git_cmd(), \"ls-tree\", $base, \"--\", $path\n+\topen my $fd, \"-|\", git_cmd(), \"ls-tree\", \"--end-of-options\", $base,\n+\t\t\"--\", $path\n \t\tor die_error(500, \"Open git-ls-tree failed\");\n \tmy $line = <$fd>;\n \tclose $fd or return undef;\n@@ -2912,7 +2915,8 @@ sub git_get_path_by_hash {\n \n \tlocal $/ = \"\\0\";\n \n-\topen my $fd, \"-|\", git_cmd(), \"ls-tree\", '-r', '-t', '-z', $base\n+\topen my $fd, \"-|\", git_cmd(), \"ls-tree\", '-r', '-t', '-z',\n+\t\t'--end-of-options', $base\n \t\tor return undef;\n \twhile (my $line = <$fd>) {\n \t\tchomp $line;\n@@ -3334,6 +3338,7 @@ sub git_get_last_activity {\n \t     '--format=%(committer)',\n \t     '--sort=-committerdate',\n \t     '--count=1',\n+\t     '--end-of-options',\n \t     map { \"refs/$_\" } get_branch_refs ()) or return;\n \tmy $most_recent = <$fd>;\n \tclose $fd or return;\n@@ -3390,6 +3395,7 @@ sub git_get_references {\n \t# 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11\n \t# c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}\n \topen my $fd, \"-|\", git_cmd(), \"show-ref\", \"--dereference\",\n+\t\t\"--end-of-options\",\n \t\t($type ? (\"--\", \"refs/$type\") : ()) # use -- <pattern> if $type\n \t\tor return;\n \n@@ -3410,7 +3416,8 @@ sub git_get_references {\n sub git_get_rev_name_tags {\n \tmy $hash = shift || return undef;\n \n-\topen my $fd, \"-|\", git_cmd(), \"name-rev\", \"--tags\", $hash\n+\topen my $fd, \"-|\", git_cmd(), \"name-rev\", \"--tags\", \"--end-of-options\",\n+\t\t$hash\n \t\tor return;\n \tmy $name_rev = <$fd>;\n \tclose $fd;\n@@ -3472,7 +3479,9 @@ sub parse_tag {\n \tmy %tag;\n \tmy @comment;\n \n-\topen my $fd, \"-|\", git_cmd(), \"cat-file\", \"tag\", $tag_id or return;\n+\topen my $fd, \"-|\", git_cmd(), \"cat-file\", \"tag\", \"--end-of-options\",\n+\t\t$tag_id\n+\t\tor return;\n \t$tag{'id'} = $tag_id;\n \twhile (my $line = <$fd>) {\n \t\tchomp $line;\n@@ -3600,6 +3609,7 @@ sub parse_commit {\n \t\t\"--parents\",\n \t\t\"--header\",\n \t\t\"--max-count=1\",\n+\t\t\"--end-of-options\",\n \t\t$commit_id,\n \t\t\"--\",\n \t\tor die_error(500, \"Open git-rev-list failed\");\n@@ -3624,6 +3634,7 @@ sub parse_commits {\n \t\t(\"--max-count=\" . $maxcount),\n \t\t(\"--skip=\" . $skip),\n \t\t@extra_options,\n+\t\t\"--end-of-options\",\n \t\t$commit_id,\n \t\t\"--\",\n \t\t($filename ? ($filename) : ())\n@@ -3784,6 +3795,7 @@ sub git_get_heads_list {\n \t\t($limit ? '--count='.($limit+1) : ()),\n \t\t'--sort=-HEAD', '--sort=-committerdate',\n \t\t'--format=%(objectname) %(refname) %(subject)%00%(committer)',\n+\t\t'--end-of-options',\n \t\t@patterns\n \t\tor return;\n \twhile (my $line = <$fd>) {\n@@ -3998,7 +4010,8 @@ sub run_highlighter {\n \n \tclose $fd;\n \tmy $syntax_arg = (defined $syntax) ? \"--syntax $syntax\" : \"--force\";\n-\topen $fd, quote_command(git_cmd(), \"cat-file\", \"blob\", $hash).\" | \".\n+\topen $fd, quote_command(git_cmd(), \"cat-file\", \"blob\",\n+\t\t    \"--end-of-options\", $hash).\" | \".\n \t          quote_command($^X, '-CO', '-MEncode=decode,FB_DEFAULT', '-pse',\n \t            '$_ = decode($fe, $_, FB_DEFAULT) if !utf8::decode($_);',\n \t            '--', \"-fe=$fallback_encoding\").\" | \".\n@@ -4687,7 +4700,8 @@ sub git_get_link_target {\n \tmy $link_target;\n \n \t# read link\n-\topen my $fd, \"-|\", git_cmd(), \"cat-file\", \"blob\", $hash\n+\topen my $fd, \"-|\", git_cmd(), \"cat-file\", \"blob\", \"--end-of-options\",\n+\t\t$hash\n \t\tor return;\n \t{\n \t\tlocal $/ = undef;\n@@ -6377,7 +6391,7 @@ sub git_search_files {\n \n \tlocal $/ = \"\\n\";\n \topen my $fd, \"-|\", git_cmd(), 'grep', '-n', '-z',\n-\t\t$search_use_regexp ? ('-E', '-i') : '-F',\n+\t\t$search_use_regexp ? ('-E', '-i') : '-F', '--end-of-options',\n \t\t$searchtext, $co{'tree'}\n \t\t\tor die_error(500, \"Open git-grep failed\");\n \n@@ -6768,17 +6782,18 @@ sub git_blame_common {\n \tmy $fd;\n \tif ($format eq 'incremental') {\n \t\t# get file contents (as base)\n-\t\topen $fd, \"-|\", git_cmd(), 'cat-file', 'blob', $hash\n+\t\topen $fd, \"-|\", git_cmd(), 'cat-file', 'blob',\n+\t\t\t'--end-of-options', $hash\n \t\t\tor die_error(500, \"Open git-cat-file failed\");\n \t} elsif ($format eq 'data') {\n \t\t# run git-blame --incremental\n \t\topen $fd, \"-|\", git_cmd(), \"blame\", \"--incremental\",\n-\t\t\t$hash_base, \"--\", $file_name\n+\t\t\t\"--end-of-options\", $hash_base, \"--\", $file_name\n \t\t\tor die_error(500, \"Open git-blame --incremental failed\");\n \t} else {\n \t\t# run git-blame --porcelain\n \t\topen $fd, \"-|\", git_cmd(), \"blame\", '-p',\n-\t\t\t$hash_base, '--', $file_name\n+\t\t\t\"--end-of-options\", $hash_base, '--', $file_name\n \t\t\tor die_error(500, \"Open git-blame --porcelain failed\");\n \t}\n \tbinmode $fd, ':utf8';\n@@ -7058,7 +7073,8 @@ sub git_blob_plain {\n \t\t$expires = \"+1d\";\n \t}\n \n-\topen my $fd, \"-|\", git_cmd(), \"cat-file\", \"blob\", $hash\n+\topen my $fd, \"-|\", git_cmd(), \"cat-file\", \"blob\", \"--end-of-options\",\n+\t\t$hash\n \t\tor die_error(500, \"Open git-cat-file blob '$hash' failed\");\n \n \t# content-type (can include charset)\n@@ -7121,7 +7137,8 @@ sub git_blob {\n \t}\n \n \tmy $have_blame = gitweb_check_feature('blame');\n-\topen my $fd, \"-|\", git_cmd(), \"cat-file\", \"blob\", $hash\n+\topen my $fd, \"-|\", git_cmd(), \"cat-file\", \"blob\", \"--end-of-options\",\n+\t\t$hash\n \t\tor die_error(500, \"Couldn't cat $file_name, $hash\");\n \tmy $mimetype = blob_mimetype($fd, $file_name);\n \t# use 'blob_plain' (aka 'raw') view for files that cannot be displayed\n@@ -7216,7 +7233,8 @@ sub git_tree {\n \t{\n \t\tlocal $/ = \"\\0\";\n \t\topen my $fd, \"-|\", git_cmd(), \"ls-tree\", '-z',\n-\t\t\t($show_sizes ? '-l' : ()), @extra_options, $hash\n+\t\t\t($show_sizes ? '-l' : ()), @extra_options,\n+\t\t\t\"--end-of-options\", $hash\n \t\t\tor die_error(500, \"Open git-ls-tree failed\");\n \t\t@entries = map { chomp; $_ } <$fd>;\n \t\tclose $fd\n@@ -7417,7 +7435,7 @@ sub git_snapshot {\n \tmy $cmd = quote_command(\n \t\tgit_cmd(), 'archive',\n \t\t\"--format=$known_snapshot_formats{$format}{'format'}\",\n-\t\t\"--prefix=$prefix/\", $hash);\n+\t\t\"--prefix=$prefix/\", \"--end-of-options\", $hash);\n \tif (exists $known_snapshot_formats{$format}{'compressor'}) {\n \t\t$cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});\n \t}\n@@ -7569,6 +7587,7 @@ sub git_commit {\n \topen my $fd, \"-|\", git_cmd(), \"diff-tree\", '-r', \"--no-commit-id\",\n \t\t@diff_opts,\n \t\t(@$parents <= 1 ? $parent : '-c'),\n+\t\t\"--end-of-options\",\n \t\t$hash, \"--\"\n \t\tor die_error(500, \"Open git-diff-tree failed\");\n \t@difftree = map { chomp; $_ } <$fd>;\n@@ -7649,7 +7668,8 @@ sub git_object {\n \t\tmy $object_id = $hash || $hash_base;\n \n \t\topen my $fd, \"-|\", quote_command(\n-\t\t\tgit_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'\n+\t\t\tgit_cmd(), 'cat-file', '-t', '--end-of-options',\n+\t\t\t$object_id) . ' 2> /dev/null'\n \t\t\tor die_error(404, \"Object does not exist\");\n \t\t$type = <$fd>;\n \t\tdefined $type && chomp $type;\n@@ -7660,11 +7680,13 @@ sub git_object {\n \t} elsif ($hash_base && defined $file_name) {\n \t\t$file_name =~ s,/+$,,;\n \n-\t\tsystem(git_cmd(), \"cat-file\", '-e', $hash_base) == 0\n+\t\tsystem(git_cmd(), \"cat-file\", '-e', '--end-of-options',\n+\t\t\t$hash_base) == 0\n \t\t\tor die_error(404, \"Base object does not exist\");\n \n \t\t# here errors should not happen\n-\t\topen my $fd, \"-|\", git_cmd(), \"ls-tree\", $hash_base, \"--\", $file_name\n+\t\topen my $fd, \"-|\", git_cmd(), \"ls-tree\", \"--end-of-options\",\n+\t\t\t$hash_base, \"--\", $file_name\n \t\t\tor die_error(500, \"Open git-ls-tree failed\");\n \t\tmy $line = <$fd>;\n \t\tclose $fd;\n@@ -7700,7 +7722,7 @@ sub git_blobdiff {\n \t\tif (defined $file_name) {\n \t\t\t# read raw output\n \t\t\topen $fd, \"-|\", git_cmd(), \"diff-tree\", '-r', @diff_opts,\n-\t\t\t\t$hash_parent_base, $hash_base,\n+\t\t\t\t\"--end-of-options\", $hash_parent_base, $hash_base,\n \t\t\t\t\"--\", (defined $file_parent ? $file_parent : ()), $file_name\n \t\t\t\tor die_error(500, \"Open git-diff-tree failed\");\n \t\t\t@difftree = map { chomp; $_ } <$fd>;\n@@ -7715,7 +7737,8 @@ sub git_blobdiff {\n \n \t\t\t# read filtered raw output\n \t\t\topen $fd, \"-|\", git_cmd(), \"diff-tree\", '-r', @diff_opts,\n-\t\t\t\t$hash_parent_base, $hash_base, \"--\"\n+\t\t\t\t\"--end-of-options\", $hash_parent_base, $hash_base,\n+\t\t\t\t\"--\"\n \t\t\t\tor die_error(500, \"Open git-diff-tree failed\");\n \t\t\t@difftree =\n \t\t\t\t# ':100644 100644 03b21826... 3b93d5e7... M\tls-files.c'\n@@ -7751,6 +7774,7 @@ sub git_blobdiff {\n \t\t# open patch output\n \t\topen $fd, \"-|\", git_cmd(), \"diff-tree\", '-r', @diff_opts,\n \t\t\t'-p', ($format eq 'html' ? \"--full-index\" : ()),\n+\t\t\t\"--end-of-options\",\n \t\t\t$hash_parent_base, $hash_base,\n \t\t\t\"--\", (defined $file_parent ? $file_parent : ()), $file_name\n \t\t\tor die_error(500, \"Open git-diff-tree failed\");\n@@ -7945,7 +7969,7 @@ sub git_commitdiff {\n \tif ($format eq 'html') {\n \t\topen $fd, \"-|\", git_cmd(), \"diff-tree\", '-r', @diff_opts,\n \t\t\t\"--no-commit-id\", \"--patch-with-raw\", \"--full-index\",\n-\t\t\t$hash_parent_param, $hash, \"--\"\n+\t\t\t\"--end-of-options\", $hash_parent_param, $hash, \"--\",\n \t\t\tor die_error(500, \"Open git-diff-tree failed\");\n \n \t\twhile (my $line = <$fd>) {\n@@ -7957,7 +7981,8 @@ sub git_commitdiff {\n \n \t} elsif ($format eq 'plain') {\n \t\topen $fd, \"-|\", git_cmd(), \"diff-tree\", '-r', @diff_opts,\n-\t\t\t'-p', $hash_parent_param, $hash, \"--\"\n+\t\t\t'-p', '--end-of-options', $hash_parent_param, $hash,\n+\t\t\t\"--\"\n \t\t\tor die_error(500, \"Open git-diff-tree failed\");\n \t} elsif ($format eq 'patch') {\n \t\t# For commit ranges, we limit the output to the number of\n@@ -7982,7 +8007,8 @@ sub git_commitdiff {\n \t\t\tpush @commit_spec, '--root', $hash;\n \t\t}\n \t\topen $fd, \"-|\", git_cmd(), \"format-patch\", @diff_opts,\n-\t\t\t'--encoding=utf8', '--stdout', @commit_spec\n+\t\t\t'--encoding=utf8', '--stdout', '--end-of-options',\n+\t\t\t@commit_spec\n \t\t\tor die_error(500, \"Open git-format-patch failed\");\n \t} else {\n \t\tdie_error(400, \"Unknown commitdiff format\");\n@@ -8334,7 +8360,8 @@ sub git_feed {\n \t\t# get list of changed files\n \t\topen my $fd, \"-|\", git_cmd(), \"diff-tree\", '-r', @diff_opts,\n \t\t\t$co{'parent'} || \"--root\",\n-\t\t\t$co{'id'}, \"--\", (defined $file_name ? $file_name : ())\n+\t\t\t$co{'id'}, \"--end-of-options\", \"--\",\n+\t\t\t(defined $file_name ? $file_name : ())\n \t\t\tor next;\n \t\tmy @difftree = map { chomp; $_ } <$fd>;\n \t\tclose $fd\n\nbase-commit: 2462961280690837670d997bde64bd4ebf8ae66d\n-- \n2.51.0.179.g22c8463a5ee\n"},{"id":"525380","messageId":"CAPig+cTPgkkhAdmws9XM58744zs7A4QDOkgnYPcbgB0G9f1ivQ@mail.gmail.com","threadId":"64076","inReplyTo":"22c8463a5eebc609b4848f6db1db231fcce7d839.1756840261.git.me@ttaylorr.com","subject":"Re: [PATCH] gitweb: plug various command-line option injection holes","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2025-09-02T21:52:13Z","receivedAt":"2025-09-02T21:52:25Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Tue, Sep 2, 2025 at 3:11 PM Taylor Blau <me@ttaylorr.com> wrote:\n> When running Gitweb and loading a blobdiff with the \"hpb\" (\"hash parent\n> base\") query parameter set to a valid diff-tree option, say,\n> \"--output=/tmp/pwned\", Gitweb will faithfully execute \"diff-tree\"\n> internally (via \"sub git_blobdiff\") and blindly pass in the \"hpb\" query\n> parameter.\n>\n> In other words, visiting a URL like:\n>\n>     http://127.0.0.1:1234/?p=<PROJECT_NAME>;a=blobdiff;f=*;hpb=--output=/tmp/pwned;hb=HEAD\n>\n> will result in the file \"/tmp/pwned\" being created. This happens as a\n> result of gitweb executing something like:\n>\n>     git diff-tree -r -M --output=/tmp/pwned HEAD --\n>\n> , where \"--output=/tmp/pwned\" is substituted in as the value of\n> \"$hash_parent_base\".\n>\n> There are various other spots in Gitweb which are too eager to pass\n> untrusted query parameter values as command-line arguments, leading to\n> at least the above option-injection attack, and likely many others.\n>\n> Since 51b4594b40 (parse-options: allow --end-of-options as a synonym for\n> \"--\", 2019-08-06), we have the \"--end-of-options\" command-line flag as\n> a standard mechanism to indicate that any further argument should not be\n> interpreted as command-line options.\n>\n> Guard agains this and other option-injection attacks by placing the\n> \"--end-of-options\" flag before any untrusted user-input in any place\n> that gitweb spawns Git as a sub-process.\n\ns/agains/against/\n\n> Reported-by: Moritz Sanft <moritz.sanft@outlook.de>\n> Signed-off-by: Taylor Blau <me@ttaylorr.com>\n"}]}