fix: escape % characters in git prompts

This patch adds missing % character escaping for custom git prompts
used in a few themes. It also includes escaping for git-prompt.sh.

In combination with CVE-2021-45444, this could allow code execution
when displaying branch information in cloned malicious git repositories.
However, zsh 5.8.1 and newer are largely the default zsh versions, and
on those supported distributions with older zsh versions, the CVE has been
found to be also patched.

For this reason, this doesn't qualify as a security patch, but a
bug fix for proper printing of git branches.
This commit is contained in:
Marc Cornellà 2026-05-28 18:57:52 +02:00
commit c90141ed77
11 changed files with 18 additions and 9 deletions

View file

@ -235,7 +235,7 @@ __git_ps1_show_upstream ()
if [ $pcmode = yes ] && [ $ps1_expanded = yes ]; then if [ $pcmode = yes ] && [ $ps1_expanded = yes ]; then
upstream="$upstream \${__git_ps1_upstream_name}" upstream="$upstream \${__git_ps1_upstream_name}"
else else
upstream="$upstream ${__git_ps1_upstream_name}" upstream="$upstream ${__git_ps1_upstream_name//\%/%%}"
# not needed anymore; keep user's # not needed anymore; keep user's
# environment clean # environment clean
unset __git_ps1_upstream_name unset __git_ps1_upstream_name
@ -570,6 +570,9 @@ __git_ps1 ()
if [ $pcmode = yes ] && [ $ps1_expanded = yes ]; then if [ $pcmode = yes ] && [ $ps1_expanded = yes ]; then
__git_ps1_branch_name=$b __git_ps1_branch_name=$b
b="\${__git_ps1_branch_name}" b="\${__git_ps1_branch_name}"
else
# escape % in branch name to avoid prompt expansion issues
b="${b//\%/%%}"
fi fi
if [ -n "${GIT_PS1_SHOWCOLORHINTS-}" ]; then if [ -n "${GIT_PS1_SHOWCOLORHINTS-}" ]; then

View file

@ -16,7 +16,8 @@ ZSH_THEME_GIT_PROMPT_CLEAN=""
git_custom_status() { git_custom_status() {
local cb=$(git_current_branch) local cb=$(git_current_branch)
if [ -n "$cb" ]; then if [ -n "$cb" ]; then
echo "$(parse_git_dirty)$ZSH_THEME_GIT_PROMPT_PREFIX$(git_current_branch)$ZSH_THEME_GIT_PROMPT_SUFFIX" cb="${cb//\%/%%}"
echo "$(parse_git_dirty)$ZSH_THEME_GIT_PROMPT_PREFIX${cb}$ZSH_THEME_GIT_PROMPT_SUFFIX"
fi fi
} }

View file

@ -10,6 +10,7 @@ ZSH_THEME_GIT_PROMPT_CLEAN=""
git_custom_status() { git_custom_status() {
local branch=$(git_current_branch) local branch=$(git_current_branch)
[[ -n "$branch" ]] || return 0 [[ -n "$branch" ]] || return 0
branch="${branch//\%/%%}"
print "%{${fg_bold[yellow]}%}$(work_in_progress)%{$reset_color%}\ print "%{${fg_bold[yellow]}%}$(work_in_progress)%{$reset_color%}\
${ZSH_THEME_GIT_PROMPT_PREFIX}$(parse_git_dirty)${branch}\ ${ZSH_THEME_GIT_PROMPT_PREFIX}$(parse_git_dirty)${branch}\
${ZSH_THEME_GIT_PROMPT_SUFFIX}" ${ZSH_THEME_GIT_PROMPT_SUFFIX}"

View file

@ -31,7 +31,7 @@ function josh_prompt {
prompt=" $prompt" prompt=" $prompt"
done done
prompt="%{%F{green}%}$PWD$prompt%{%F{red}%}$(ruby_prompt_info)%{$reset_color%} $(git_current_branch)" prompt="%{%F{green}%}$PWD$prompt%{%F{red}%}$(ruby_prompt_info)%{$reset_color%} ${branch//\%/%%}"
echo $prompt echo $prompt
} }

View file

@ -41,4 +41,4 @@ USER_COLOR=$GREEN_BOLD
PROMPT=' PROMPT='
%{$USER_COLOR%}%n@%m%{$WHITE%}:%{$YELLOW%}%~%u$(parse_git_dirty)$(git_prompt_ahead)%{$RESET_COLOR%} %{$USER_COLOR%}%n@%m%{$WHITE%}:%{$YELLOW%}%~%u$(parse_git_dirty)$(git_prompt_ahead)%{$RESET_COLOR%}
%{$BLUE%}>%{$RESET_COLOR%} ' %{$BLUE%}>%{$RESET_COLOR%} '
RPROMPT='%{$GREEN_BOLD%}$(git_current_branch)$(git_prompt_short_sha)$(git_prompt_status)%{$RESET_COLOR%}' RPROMPT='%{$GREEN_BOLD%}${$(git_current_branch)//\%/%%}$(git_prompt_short_sha)$(git_prompt_status)%{$RESET_COLOR%}'

View file

@ -7,7 +7,7 @@ function my_git_prompt_info() {
ref=$(git symbolic-ref HEAD 2> /dev/null) || return ref=$(git symbolic-ref HEAD 2> /dev/null) || return
GIT_STATUS=$(git_prompt_status) GIT_STATUS=$(git_prompt_status)
[[ -n $GIT_STATUS ]] && GIT_STATUS=" $GIT_STATUS" [[ -n $GIT_STATUS ]] && GIT_STATUS=" $GIT_STATUS"
echo "$ZSH_THEME_GIT_PROMPT_PREFIX${ref#refs/heads/}$GIT_STATUS$ZSH_THEME_GIT_PROMPT_SUFFIX" echo "$ZSH_THEME_GIT_PROMPT_PREFIX${${ref#refs/heads/}//\%/%%}$GIT_STATUS$ZSH_THEME_GIT_PROMPT_SUFFIX"
} }
PROMPT='%{$fg_bold[green]%}%n@%m%{$reset_color%} %{$fg_bold[blue]%}%2~%{$reset_color%} $(my_git_prompt_info)%{$reset_color%}%B»%b ' PROMPT='%{$fg_bold[green]%}%n@%m%{$reset_color%} %{$fg_bold[blue]%}%2~%{$reset_color%} $(my_git_prompt_info)%{$reset_color%}%B»%b '

View file

@ -42,7 +42,9 @@ function my_git_prompt() {
} }
function my_current_branch() { function my_current_branch() {
echo $(git_current_branch || echo "(no branch)") local branch
branch=$(git_current_branch || echo "(no branch)")
echo "${branch//\%/%%}"
} }
function ssh_connection() { function ssh_connection() {

View file

@ -10,6 +10,7 @@ ZSH_THEME_GIT_PROMPT_CLEAN=""
git_custom_status() { git_custom_status() {
local branch=$(git_current_branch) local branch=$(git_current_branch)
[[ -n "$branch" ]] || return 0 [[ -n "$branch" ]] || return 0
branch="${branch//\%/%%}"
echo "$(parse_git_dirty)\ echo "$(parse_git_dirty)\
%{${fg_bold[yellow]}%}$(work_in_progress)%{$reset_color%}\ %{${fg_bold[yellow]}%}$(work_in_progress)%{$reset_color%}\
${ZSH_THEME_GIT_PROMPT_PREFIX}${branch}${ZSH_THEME_GIT_PROMPT_SUFFIX}" ${ZSH_THEME_GIT_PROMPT_PREFIX}${branch}${ZSH_THEME_GIT_PROMPT_SUFFIX}"

View file

@ -31,7 +31,7 @@ git_prompt() {
local cb=$(git_current_branch) local cb=$(git_current_branch)
if [[ -n "$cb" ]]; then if [[ -n "$cb" ]]; then
local repo_path=$(git_repo_path) local repo_path=$(git_repo_path)
echo " %{$fg_bold[grey]%}$cb %{$fg[white]%}$(git_commit_id)%{$reset_color%}$(git_mode)$(git_dirty)" echo " %{$fg_bold[grey]%}${cb//\%/%%} %{$fg[white]%}$(git_commit_id)%{$reset_color%}$(git_mode)$(git_dirty)"
fi fi
} }

View file

@ -23,7 +23,8 @@ function mygit() {
if [[ "$(git config --get oh-my-zsh.hide-status)" != "1" ]]; then if [[ "$(git config --get oh-my-zsh.hide-status)" != "1" ]]; then
ref=$(command git symbolic-ref HEAD 2> /dev/null) || \ ref=$(command git symbolic-ref HEAD 2> /dev/null) || \
ref=$(command git rev-parse --short HEAD 2> /dev/null) || return ref=$(command git rev-parse --short HEAD 2> /dev/null) || return
echo "$ZSH_THEME_GIT_PROMPT_PREFIX${ref#refs/heads/}$(git_prompt_short_sha)$(git_prompt_status)%{$fg_bold[blue]%}$ZSH_THEME_GIT_PROMPT_SUFFIX " ref=${${ref#refs/heads/}//\%/%%}
echo "${ZSH_THEME_GIT_PROMPT_PREFIX}${ref}$(git_prompt_short_sha)$(git_prompt_status)%{$fg_bold[blue]%}${ZSH_THEME_GIT_PROMPT_SUFFIX} "
fi fi
} }

View file

@ -62,7 +62,7 @@ custom_git_prompt_status() {
# get the name of the branch we are on (copied and modified from git.zsh) # get the name of the branch we are on (copied and modified from git.zsh)
function custom_git_prompt() { function custom_git_prompt() {
ref=$(git symbolic-ref HEAD 2> /dev/null) || return ref=$(git symbolic-ref HEAD 2> /dev/null) || return
echo "$ZSH_THEME_GIT_PROMPT_PREFIX${ref#refs/heads/}$(parse_git_dirty)$(git_prompt_ahead)$(custom_git_prompt_status)$ZSH_THEME_GIT_PROMPT_SUFFIX" echo "$ZSH_THEME_GIT_PROMPT_PREFIX${${ref#refs/heads/}//\%/%%}$(parse_git_dirty)$(git_prompt_ahead)$(custom_git_prompt_status)$ZSH_THEME_GIT_PROMPT_SUFFIX"
} }
# %B sets bold text # %B sets bold text