This wiki is automatically published from ohmyzsh/wiki. To edit this page, go to ohmyzsh/wiki, make your changes and submit a Pull Request.

Secure Code

Oh My Zsh runs directly in people's environments, and there's currently no alternative. This is why it is paramount that our code is vetted and reviewed according to security best practices, specifically targeting Zsh.

General Guidelines

Follow these practices when contributing code that runs in users' shells:

Insecure patterns to avoid

[!NOTE] Here, we use read something as a command to symbolize untrusted input. In real world scenarios, untrusted input usually comes from some other place.

eval with untrusted input

Past vulnerabilities:

Use case 1: Dynamic command execution

Insecure pattern:

read filename  # User input: "file; echo pwned"
eval "stat $filename"  # Executes: stat file; echo pwned

This pattern is often seen in more complex code where the command is determined dynamically:

somefunction() {
  local arg="$1" filename="$2"
  local cmd="" 

  case "$arg" in
  -s) cmd="stat" ;;
  -l) cmd="ls" ;;
  esac

  eval "$cmd $filename"  # VULNERABLE
}

# Attack example
somefunction -s "file; rm -rf $HOME"

Better pattern:

somefunction() {
  local arg="$1" filename="$2"
  local cmd="" 

  case "$arg" in
  -s) cmd="stat" ;;
  -l) cmd="ls" ;;
  esac

  # SECURE: Zsh handles argument parsing safely
  $cmd "$filename"
}

The shell correctly parses each parameter as part of the same command. Even if $filename contains ;, it will not be parsed as the beginning of a new command.

What's preventing injection are not the quotes around $filename, but the fact that we are passing each argument separately in the command. This way, zsh knows exactly what each argument is supposed to be in the parsing phase, before any variable expansion happens.

Use case 2: Variable assignment

Insecure pattern:

This almost never makes sense:

read var  # User input: "file; echo pwned"
eval "foo=$var"  # Executes: foo=file; echo pwned

But this one is more common:

read var  # User input: "echo pwned; myvariable"
eval "$var=helloworld"  # Executes: echo pwned; myvariable=helloworld

Better pattern:

Use typeset (or declare, or any of the other built-in variable assignment commands):

read var
typeset "$var=helloworld"  # SECURE: throws error instead of executing

This approach throws an error on malicious input rather than executing arbitrary commands:

➜  var="file; echo pwned"
➜  typeset "$var=helloworld"
➜  typeset -p myvariable
typeset myvariable=helloworld

➜  var="echo pwned; myvariable"
➜  typeset "$var=helloworld"
typeset: not valid in this context: echo pwned; myvariable

print -P with untrusted input

Past vulnerabilities:

Use case: displaying formatted output with prompt sequences

Insecure pattern:

setopt promptsubst
dailyquote="$(get_daily_quote_from_the_internet)"
print -P "%F{yellow}%B${dailyquote}%b%f"  # VULNERABLE

If dailyquote contains $() or backticks (`), zsh will execute those commands.

Better pattern:

# Disable prompt substitution for untrusted content
setopt localoptions nopromptsubst
dailyquote="$(get_daily_quote_from_the_internet)"
print -P "%F{yellow}%B${dailyquote}%b%f"  

# Use `print` without `-P` to avoid prompt substitution
setopt promptsubst
dailyquote="$(get_daily_quote_from_the_internet)"
print "${fg[yellow]}${dailyquote}${reset_color}"

Unescaped % characters in prompt functions

Past vulnerabilities:

Use case: Functions that output untrusted content in prompts

Insecure pattern:

gitbranch() {
  command git symbolic-ref --short HEAD
}
PROMPT='myuser:$(gitbranch) %% '  # VULNERABLE

Git branch names may contain % characters. In zsh versions older than 5.8.1 with setopt promptsubst enabled, this is vulnerable to CVE-2021-45444. An attacker could craft a malicious branch name to execute arbitrary commands.

Better pattern:

gitbranch() {
  local branch="$(command git symbolic-ref --short HEAD)"
  echo -n "${branch//\%/%%}"  # Escape all % characters
}
PROMPT='myuser:$(gitbranch) %% '  # SECURE

Functions that output untrusted content for use in $PROMPT or $RPROMPT must:

  1. Escape all % characters by replacing them with %%
  2. Be documented and named to indicate they are prompt-safe
  3. Be used only in prompt contexts where this escaping is appropriate