git-prompt.sh: don't put unsanitized branch names in $PS1

Both bash and zsh subject the value of PS1 to parameter expansion, command substitution, and arithmetic expansion. Rather than include the raw, unescaped branch name in PS1 when running in two- or three-argument mode, construct PS1 to reference a variable that holds the branch name. Because the shells do not recursively expand, this avoids arbitrary code execution by specially-crafted branch names such as '$(IFS=_;cmd=sudo_rm_-rf_/;$cmd)'. Signed-off-by: Richard Hansen <rhansen@bbn.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Richard Hansen committed Apr 21, 2014 at 19:53 UTC 8976500cbbb13270398d3b3e07a17b8cc7bff43f
2 files changed +54 -24
contrib/completion/git-prompt.sh
+32 -2
@@ -207,7 +207,18 @@ __git_ps1_show_upstream ()
207 p=" u+${count#* }-${count% *}" ;;
208 esac
209 if [[ -n "$count" && -n "$name" ]]; then
210 - p="$p $(git rev-parse --abbrev-ref "$upstream" 2>/dev/null)"
210 + __git_ps1_upstream_name=$(git rev-parse \
211 + --abbrev-ref "$upstream" 2>/dev/null)
212 + if [ $pcmode = yes ]; then
213 + # see the comments around the
214 + # __git_ps1_branch_name variable below
215 + p="$p \${__git_ps1_upstream_name}"
216 + else
217 + p="$p ${__git_ps1_upstream_name}"
218 + # not needed anymore; keep user's
219 + # environment clean
220 + unset __git_ps1_upstream_name
221 + fi
222 fi
223 fi
224
@@ -438,8 +449,27 @@ __git_ps1 ()
449 __git_ps1_colorize_gitstring
450 fi
451
452 + b=${b##refs/heads/}
453 + if [ $pcmode = yes ]; then
454 + # In pcmode (and only pcmode) the contents of
455 + # $gitstring are subject to expansion by the shell.
456 + # Avoid putting the raw ref name in the prompt to
457 + # protect the user from arbitrary code execution via
458 + # specially crafted ref names (e.g., a ref named
459 + # '$(IFS=_;cmd=sudo_rm_-rf_/;$cmd)' would execute
460 + # 'sudo rm -rf /' when the prompt is drawn). Instead,
461 + # put the ref name in a new global variable (in the
462 + # __git_ps1_* namespace to avoid colliding with the
463 + # user's environment) and reference that variable from
464 + # PS1.
465 + __git_ps1_branch_name=$b
466 + # note that the $ is escaped -- the variable will be
467 + # expanded later (when it's time to draw the prompt)
468 + b="\${__git_ps1_branch_name}"
469 + fi
470 +
471 local f="$w$i$s$u"
442 - local gitstring="$c${b##refs/heads/}${f:+$z$f}$r$p"
472 + local gitstring="$c$b${f:+$z$f}$r$p"
473
474 if [ $pcmode = yes ]; then
475 if [ "${__git_printf_supports_v-}" != yes ]; then
t/t9903-bash-prompt.sh
+22 -22
@@ -452,53 +452,53 @@ test_expect_success 'prompt - format string starting with dash' '
452 '
453
454 test_expect_success 'prompt - pc mode' '
455 - printf "BEFORE: (master):AFTER" >expected &&
455 + printf "BEFORE: (\${__git_ps1_branch_name}):AFTER\\nmaster" >expected &&
456 printf "" >expected_output &&
457 (
458 __git_ps1 "BEFORE:" ":AFTER" >"$actual" &&
459 test_cmp expected_output "$actual" &&
460 - printf "%s" "$PS1" >"$actual"
460 + printf "%s\\n%s" "$PS1" "${__git_ps1_branch_name}" >"$actual"
461 ) &&
462 test_cmp expected "$actual"
463 '
464
465 test_expect_success 'prompt - bash color pc mode - branch name' '
466 - printf "BEFORE: (${c_green}master${c_clear}):AFTER" >expected &&
466 + printf "BEFORE: (${c_green}\${__git_ps1_branch_name}${c_clear}):AFTER\\nmaster" >expected &&
467 (
468 GIT_PS1_SHOWCOLORHINTS=y &&
469 __git_ps1 "BEFORE:" ":AFTER" >"$actual"
470 - printf "%s" "$PS1" >"$actual"
470 + printf "%s\\n%s" "$PS1" "${__git_ps1_branch_name}" >"$actual"
471 ) &&
472 test_cmp expected "$actual"
473 '
474
475 test_expect_success 'prompt - bash color pc mode - detached head' '
476 - printf "BEFORE: (${c_red}(%s...)${c_clear}):AFTER" $(git log -1 --format="%h" b1^) >expected &&
476 + printf "BEFORE: (${c_red}\${__git_ps1_branch_name}${c_clear}):AFTER\\n(%s...)" $(git log -1 --format="%h" b1^) >expected &&
477 git checkout b1^ &&
478 test_when_finished "git checkout master" &&
479 (
480 GIT_PS1_SHOWCOLORHINTS=y &&
481 __git_ps1 "BEFORE:" ":AFTER" &&
482 - printf "%s" "$PS1" >"$actual"
482 + printf "%s\\n%s" "$PS1" "${__git_ps1_branch_name}" >"$actual"
483 ) &&
484 test_cmp expected "$actual"
485 '
486
487 test_expect_success 'prompt - bash color pc mode - dirty status indicator - dirty worktree' '
488 - printf "BEFORE: (${c_green}master${c_clear} ${c_red}*${c_clear}):AFTER" >expected &&
488 + printf "BEFORE: (${c_green}\${__git_ps1_branch_name}${c_clear} ${c_red}*${c_clear}):AFTER\\nmaster" >expected &&
489 echo "dirty" >file &&
490 test_when_finished "git reset --hard" &&
491 (
492 GIT_PS1_SHOWDIRTYSTATE=y &&
493 GIT_PS1_SHOWCOLORHINTS=y &&
494 __git_ps1 "BEFORE:" ":AFTER" &&
495 - printf "%s" "$PS1" >"$actual"
495 + printf "%s\\n%s" "$PS1" "${__git_ps1_branch_name}" >"$actual"
496 ) &&
497 test_cmp expected "$actual"
498 '
499
500 test_expect_success 'prompt - bash color pc mode - dirty status indicator - dirty index' '
501 - printf "BEFORE: (${c_green}master${c_clear} ${c_green}+${c_clear}):AFTER" >expected &&
501 + printf "BEFORE: (${c_green}\${__git_ps1_branch_name}${c_clear} ${c_green}+${c_clear}):AFTER\\nmaster" >expected &&
502 echo "dirty" >file &&
503 test_when_finished "git reset --hard" &&
504 git add -u &&
@@ -506,13 +506,13 @@ test_expect_success 'prompt - bash color pc mode - dirty status indicator - dirt
506 GIT_PS1_SHOWDIRTYSTATE=y &&
507 GIT_PS1_SHOWCOLORHINTS=y &&
508 __git_ps1 "BEFORE:" ":AFTER" &&
509 - printf "%s" "$PS1" >"$actual"
509 + printf "%s\\n%s" "$PS1" "${__git_ps1_branch_name}" >"$actual"
510 ) &&
511 test_cmp expected "$actual"
512 '
513
514 test_expect_success 'prompt - bash color pc mode - dirty status indicator - dirty index and worktree' '
515 - printf "BEFORE: (${c_green}master${c_clear} ${c_red}*${c_green}+${c_clear}):AFTER" >expected &&
515 + printf "BEFORE: (${c_green}\${__git_ps1_branch_name}${c_clear} ${c_red}*${c_green}+${c_clear}):AFTER\\nmaster" >expected &&
516 echo "dirty index" >file &&
517 test_when_finished "git reset --hard" &&
518 git add -u &&
@@ -521,25 +521,25 @@ test_expect_success 'prompt - bash color pc mode - dirty status indicator - dirt
521 GIT_PS1_SHOWCOLORHINTS=y &&
522 GIT_PS1_SHOWDIRTYSTATE=y &&
523 __git_ps1 "BEFORE:" ":AFTER" &&
524 - printf "%s" "$PS1" >"$actual"
524 + printf "%s\\n%s" "$PS1" "${__git_ps1_branch_name}" >"$actual"
525 ) &&
526 test_cmp expected "$actual"
527 '
528
529 test_expect_success 'prompt - bash color pc mode - dirty status indicator - before root commit' '
530 - printf "BEFORE: (${c_green}master${c_clear} ${c_green}#${c_clear}):AFTER" >expected &&
530 + printf "BEFORE: (${c_green}\${__git_ps1_branch_name}${c_clear} ${c_green}#${c_clear}):AFTER\\nmaster" >expected &&
531 (
532 GIT_PS1_SHOWDIRTYSTATE=y &&
533 GIT_PS1_SHOWCOLORHINTS=y &&
534 cd otherrepo &&
535 __git_ps1 "BEFORE:" ":AFTER" &&
536 - printf "%s" "$PS1" >"$actual"
536 + printf "%s\\n%s" "$PS1" "${__git_ps1_branch_name}" >"$actual"
537 ) &&
538 test_cmp expected "$actual"
539 '
540
541 test_expect_success 'prompt - bash color pc mode - inside .git directory' '
542 - printf "BEFORE: (${c_green}GIT_DIR!${c_clear}):AFTER" >expected &&
542 + printf "BEFORE: (${c_green}\${__git_ps1_branch_name}${c_clear}):AFTER\\nGIT_DIR!" >expected &&
543 echo "dirty" >file &&
544 test_when_finished "git reset --hard" &&
545 (
@@ -547,13 +547,13 @@ test_expect_success 'prompt - bash color pc mode - inside .git directory' '
547 GIT_PS1_SHOWCOLORHINTS=y &&
548 cd .git &&
549 __git_ps1 "BEFORE:" ":AFTER" &&
550 - printf "%s" "$PS1" >"$actual"
550 + printf "%s\\n%s" "$PS1" "${__git_ps1_branch_name}" >"$actual"
551 ) &&
552 test_cmp expected "$actual"
553 '
554
555 test_expect_success 'prompt - bash color pc mode - stash status indicator' '
556 - printf "BEFORE: (${c_green}master${c_clear} ${c_lblue}\$${c_clear}):AFTER" >expected &&
556 + printf "BEFORE: (${c_green}\${__git_ps1_branch_name}${c_clear} ${c_lblue}\$${c_clear}):AFTER\\nmaster" >expected &&
557 echo 2 >file &&
558 git stash &&
559 test_when_finished "git stash drop" &&
@@ -561,29 +561,29 @@ test_expect_success 'prompt - bash color pc mode - stash status indicator' '
561 GIT_PS1_SHOWSTASHSTATE=y &&
562 GIT_PS1_SHOWCOLORHINTS=y &&
563 __git_ps1 "BEFORE:" ":AFTER" &&
564 - printf "%s" "$PS1" >"$actual"
564 + printf "%s\\n%s" "$PS1" "${__git_ps1_branch_name}" >"$actual"
565 ) &&
566 test_cmp expected "$actual"
567 '
568
569 test_expect_success 'prompt - bash color pc mode - untracked files status indicator' '
570 - printf "BEFORE: (${c_green}master${c_clear} ${c_red}%%${c_clear}):AFTER" >expected &&
570 + printf "BEFORE: (${c_green}\${__git_ps1_branch_name}${c_clear} ${c_red}%%${c_clear}):AFTER\\nmaster" >expected &&
571 (
572 GIT_PS1_SHOWUNTRACKEDFILES=y &&
573 GIT_PS1_SHOWCOLORHINTS=y &&
574 __git_ps1 "BEFORE:" ":AFTER" &&
575 - printf "%s" "$PS1" >"$actual"
575 + printf "%s\\n%s" "$PS1" "${__git_ps1_branch_name}" >"$actual"
576 ) &&
577 test_cmp expected "$actual"
578 '
579
580 test_expect_success 'prompt - zsh color pc mode' '
581 - printf "BEFORE: (%%F{green}master%%f):AFTER" >expected &&
581 + printf "BEFORE: (%%F{green}\${__git_ps1_branch_name}%%f):AFTER\\nmaster" >expected &&
582 (
583 ZSH_VERSION=5.0.0 &&
584 GIT_PS1_SHOWCOLORHINTS=y &&
585 __git_ps1 "BEFORE:" ":AFTER" >"$actual"
586 - printf "%s" "$PS1" >"$actual"
586 + printf "%s\\n%s" "$PS1" "${__git_ps1_branch_name}" >"$actual"
587 ) &&
588 test_cmp expected "$actual"
589 '