All of lore.kernel.org
 help / color / mirror / Atom feed
From: "Anupam Mediratta via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Anupam Mediratta <mediratta@gmail.com>,
	Anupam Mediratta <mediratta@gmail.com>
Subject: [PATCH v2] git-p4: avoid shell interpretation of commit ids in applyCommit
Date: Thu, 24 Sep 2026 08:28:02 +0000	[thread overview]
Message-ID: <pull.2411.v2.git.git.1790238482045.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2411.git.git.1790093506966.gitgitgadget@gmail.com>

From: Anupam Mediratta <mediratta@gmail.com>

applyCommit() builds a `git diff-tree ... | git apply ...` pipeline as a
shell command string, interpolating the commit id and running it via
os.system()/system(shell=True). The id usually comes from `git rev-list`
output (safe, plain SHA-1s), but it can also come verbatim from the
user-supplied `--commit` option, which is never validated
(git-p4.py:2620-2631). A value such as `$(some-command)` passed to
`--commit` is executed by the shell during command substitution, even
though the value is wrapped in double quotes.

Replace the shell pipeline with two argument-vector subprocess calls
connected directly through a pipe, matching the pattern already used
throughout this file (read_pipe, read_pipe_lines, p4_system). This
removes the shell entirely, rather than relying on quoting the
interpolated value.

The shell-based call that applied the patch for real went through
git-p4.py's own system() helper, which raises CalledProcessError on a
non-zero exit status, so a failed apply aborted the submit. Keep that
behaviour by giving the new helper the same ignore_error contract
system() uses: it raises unless the caller asks for the status, and the
two callers that test the status for themselves ask for it.

Add a regression test exercising `git p4 submit --commit` with a shell
metacharacter payload, verifying it is never interpreted.

Signed-off-by: Anupam Mediratta <mediratta@gmail.com>
---
    git-p4: avoid shell interpretation of commit ids in applyCommit
    
    P4Submit.applyCommit() builds a git diff-tree | git apply pipeline as a
    shell command string and runs it with os.system()/system(shell=True).
    The commit id it interpolates is usually a plain SHA-1 from git
    rev-list, but it can also come straight from the unvalidated --commit
    command-line option, so a value such as $(some-command) passed to
    --commit gets executed by the shell during command substitution.
    
    This replaces the shell pipeline with two argument-vector subprocess
    calls connected directly through a pipe, the same pattern already used
    everywhere else in this file (read_pipe, read_pipe_lines, p4_system), so
    there's no shell left to escape correctly. It also adds a regression
    test in t9803 that submits a commit id crafted with shell metacharacters
    and checks they're never executed.
    
    I have read https://git-scm.com/docs/SubmittingPatches#ai and confirm
    this contribution complies with it.
    
    Changes since v1: as Junio pointed out, the call that applies the patch
    for real used to go through git-p4.py's own system() helper, which
    raises CalledProcessError on a non-zero exit status, so a failed git
    apply aborted the submit; v1 dropped that and silently carried on. The
    new helper now takes the same ignore_error argument system() does and
    raises by default, and the two callers that inspect the exit status
    themselves pass ignore_error=True.

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2411%2Fanupamme%2Ffix-repo-git-git-p4-cwe-78-shell-injection-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2411/anupamme/fix-repo-git-git-p4-cwe-78-shell-injection-v2
Pull-Request: https://github.com/git/git/pull/2411

Range-diff vs v1:

 1:  391e429a2a ! 1:  6fcacc71b8 git-p4: avoid shell interpretation of commit ids in applyCommit
     @@ Commit message
          removes the shell entirely, rather than relying on quoting the
          interpolated value.
      
     +    The shell-based call that applied the patch for real went through
     +    git-p4.py's own system() helper, which raises CalledProcessError on a
     +    non-zero exit status, so a failed apply aborted the submit. Keep that
     +    behaviour by giving the new helper the same ignore_error contract
     +    system() uses: it raises unless the caller asks for the status, and the
     +    two callers that test the status for themselves ask for it.
     +
          Add a regression test exercising `git p4 submit --commit` with a shell
          metacharacter payload, verifying it is never interpreted.
      
     @@ git-p4.py: def p4_system(cmd, *k, **kw):
               raise subprocess.CalledProcessError(retcode, real_cmd)
       
       
     -+def diffTreeApply(id, applyArgs):
     ++def diffTreeApply(id, applyArgs, ignore_error=False):
      +    """Pipe `git diff-tree --full-index -p <id>` into `git apply <applyArgs>`
      +    without a shell, so id can never be interpreted as shell syntax. Returns
     -+    the exit status of git apply."""
     ++    the exit status of git apply, raising CalledProcessError on a non-zero
     ++    status unless ignore_error is set."""
      +    diffArgv = ["git", "diff-tree", "--full-index", "-p", id]
      +    applyArgv = ["git", "apply"] + applyArgs
      +    if verbose:
     @@ git-p4.py: def p4_system(cmd, *k, **kw):
      +    diffProc.stdout.close()
      +    applyProc.wait()
      +    diffProc.wait()
     -+    return applyProc.returncode
     ++    retcode = applyProc.returncode
     ++    if retcode and not ignore_error:
     ++        raise subprocess.CalledProcessError(retcode, applyArgv)
     ++    return retcode
      +
      +
       def die_bad_access(s):
     @@ git-p4.py: class P4Submit(Command, P4UserMap):
      -            print("TryPatch: %s" % tryPatchCmd)
      -
      -        if os.system(tryPatchCmd) != 0:
     -+        if diffTreeApply(id, tryPatchArgs) != 0:
     ++        if diffTreeApply(id, tryPatchArgs, ignore_error=True) != 0:
                   fixed_rcs_keywords = False
                   patch_succeeded = False
                   print("Unfortunately applying the change failed!")
     @@ git-p4.py: class P4Submit(Command, P4UserMap):
                   if fixed_rcs_keywords:
                       print("Retrying the patch with RCS keywords cleaned up")
      -                if os.system(tryPatchCmd) == 0:
     -+                if diffTreeApply(id, tryPatchArgs) == 0:
     ++                if diffTreeApply(id, tryPatchArgs, ignore_error=True) == 0:
                           patch_succeeded = True
                           print("Patch succeesed this time with RCS keywords cleaned")
       


 git-p4.py                         | 35 ++++++++++++++++++++++---------
 t/t9803-git-p4-shell-metachars.sh | 16 ++++++++++++++
 2 files changed, 41 insertions(+), 10 deletions(-)

diff --git a/git-p4.py b/git-p4.py
index c0ca7becaf..e716831554 100755
--- a/git-p4.py
+++ b/git-p4.py
@@ -465,6 +465,26 @@ def p4_system(cmd, *k, **kw):
         raise subprocess.CalledProcessError(retcode, real_cmd)
 
 
+def diffTreeApply(id, applyArgs, ignore_error=False):
+    """Pipe `git diff-tree --full-index -p <id>` into `git apply <applyArgs>`
+    without a shell, so id can never be interpreted as shell syntax. Returns
+    the exit status of git apply, raising CalledProcessError on a non-zero
+    status unless ignore_error is set."""
+    diffArgv = ["git", "diff-tree", "--full-index", "-p", id]
+    applyArgv = ["git", "apply"] + applyArgs
+    if verbose:
+        print("TryPatch: %s | %s" % (" ".join(diffArgv), " ".join(applyArgv)))
+    diffProc = subprocess.Popen(diffArgv, stdout=subprocess.PIPE)
+    applyProc = subprocess.Popen(applyArgv, stdin=diffProc.stdout)
+    diffProc.stdout.close()
+    applyProc.wait()
+    diffProc.wait()
+    retcode = applyProc.returncode
+    if retcode and not ignore_error:
+        raise subprocess.CalledProcessError(retcode, applyArgv)
+    return retcode
+
+
 def die_bad_access(s):
     die("failure accessing depot: {0}".format(s.rstrip()))
 
@@ -2234,16 +2254,11 @@ class P4Submit(Command, P4UserMap):
             else:
                 die("unknown modifier %s for %s" % (modifier, path))
 
-        diffcmd = "git diff-tree --full-index -p \"%s\"" % (id)
-        patchcmd = diffcmd + " | git apply "
-        tryPatchCmd = patchcmd + "--check -"
-        applyPatchCmd = patchcmd + "--check --apply -"
+        tryPatchArgs = ["--check", "-"]
+        applyPatchArgs = ["--check", "--apply", "-"]
         patch_succeeded = True
 
-        if verbose:
-            print("TryPatch: %s" % tryPatchCmd)
-
-        if os.system(tryPatchCmd) != 0:
+        if diffTreeApply(id, tryPatchArgs, ignore_error=True) != 0:
             fixed_rcs_keywords = False
             patch_succeeded = False
             print("Unfortunately applying the change failed!")
@@ -2279,7 +2294,7 @@ class P4Submit(Command, P4UserMap):
 
             if fixed_rcs_keywords:
                 print("Retrying the patch with RCS keywords cleaned up")
-                if os.system(tryPatchCmd) == 0:
+                if diffTreeApply(id, tryPatchArgs, ignore_error=True) == 0:
                     patch_succeeded = True
                     print("Patch succeesed this time with RCS keywords cleaned")
 
@@ -2291,7 +2306,7 @@ class P4Submit(Command, P4UserMap):
         #
         # Apply the patch for real, and do add/delete/+x handling.
         #
-        system(applyPatchCmd, shell=True)
+        diffTreeApply(id, applyPatchArgs)
 
         for f in filesToChangeType:
             p4_edit(f, "-t", "auto")
diff --git a/t/t9803-git-p4-shell-metachars.sh b/t/t9803-git-p4-shell-metachars.sh
index 2913277013..ef8fd6e094 100755
--- a/t/t9803-git-p4-shell-metachars.sh
+++ b/t/t9803-git-p4-shell-metachars.sh
@@ -105,4 +105,20 @@ test_expect_success 'branch with shell char' '
 	)
 '
 
+test_expect_success 'git p4 submit --commit does not execute shell metachars in commit id' '
+	git p4 clone --dest="$git" //depot &&
+	test_when_finished cleanup_git &&
+	(
+		cd "$git" &&
+		git config git-p4.skipSubmitEditCheck true &&
+		echo f3 >file3 &&
+		git add file3 &&
+		git commit -m "add file3" &&
+		name='"'"'$(touch${IFS}injection-marker)'"'"' &&
+		git branch "$name" HEAD &&
+		P4EDITOR="test-tool chmtime +5" git p4 submit --commit "$name"
+	) &&
+	test_path_is_missing "$cli/injection-marker"
+'
+
 test_done

base-commit: d38352cd43ab9745686d697872408bc3249a153f
-- 
gitgitgadget

      parent reply	other threads:[~2026-09-24  8:28 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-22 16:11 [PATCH] git-p4: avoid shell interpretation of commit ids in applyCommit Anupam Mediratta via GitGitGadget
2026-09-23 19:11 ` Junio C Hamano
2026-09-24  8:28 ` Anupam Mediratta via GitGitGadget [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=pull.2411.v2.git.git.1790238482045.gitgitgadget@gmail.com \
    --to=gitgitgadget@gmail.com \
    --cc=git@vger.kernel.org \
    --cc=mediratta@gmail.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.