|
| 1 | +<!-- prettier-ignore-start --> |
| 2 | +> _This wiki is automatically published from [ohmyzsh/wiki](https://github.com/ohmyzsh/wiki). To edit this page,_ |
| 3 | +> _go to [ohmyzsh/wiki](https://github.com/ohmyzsh/wiki), make your changes and submit a Pull Request._ |
| 4 | +<!-- prettier-ignore-end --> |
| 5 | +
|
| 6 | +# Secure Code |
| 7 | + |
| 8 | +Oh My Zsh runs directly in people's environments, and there's currently no alternative. |
| 9 | +This is why it is paramount that our code is vetted and reviewed according to security |
| 10 | +best practices, specifically targeting Zsh. |
| 11 | + |
| 12 | +## General Guidelines |
| 13 | + |
| 14 | +Follow these practices when contributing code that runs in users' shells: |
| 15 | + |
| 16 | +- Least privilege: design code to run with the minimal permissions required. Avoid elevate-right operations (sudo, setuid) unless absolutely necessary and documented. |
| 17 | +- Input validation and escaping: treat all external input as untrusted. Validate, constrain, and escape inputs (prefer arrays and properly quoted arguments; avoid building commands by concatenation). |
| 18 | +- Minimize use of eval/source/command substitution: avoid dynamic evaluation. If necessary, tightly validate inputs and limit the scope of what can be executed. |
| 19 | +- Safe error handling: return clear, minimal errors for operators but never leak secrets or sensitive environment/state in messages or logs. |
| 20 | +- Secrets and sensitive data: never hard-code credentials. Avoid writing secrets to logs, shell history, cache, or world-readable files; unset sensitive variables as soon as they are no longer needed (e.g., unset SECRET_VAR). |
| 21 | +- Secure file handling: use secure temporary files (mktemp), set restrictive permissions (umask, chmod), and avoid creating files in insecure locations. |
| 22 | +- Logging and telemetry: avoid logging and telemetry as much as possible. If necessary, only log what is necessary and redact or hash sensitive fields, and ensure log storage is access-controlled (e.g., not world-readable such as `/tmp`). |
| 23 | +- Dependencies and supply chain: prefer minimal, well-audited dependencies; pin and justify any third-party tools that run with elevated privileges. |
| 24 | +- Review, testing, and CI: require peer review for changes, include automated checks (shellcheck, unit/integration tests), and run security-focused CI scans before release. |
| 25 | +- Documentation: document any security-relevant design decisions, required privileges, and mitigation steps so reviewers can assess risk. |
| 26 | + |
| 27 | +## Insecure patterns to avoid |
| 28 | + |
| 29 | +> [!NOTE] |
| 30 | +> Here, we use `read something` as a command to symbolize untrusted input. |
| 31 | +> In real world scenarios, untrusted input usually comes from some other place. |
| 32 | +
|
| 33 | +### eval with untrusted input |
| 34 | + |
| 35 | +**Past vulnerabilities:** |
| 36 | + |
| 37 | +- [Issue #10414](https://github.com/ohmyzsh/ohmyzsh/issues/10414): Unsafe use of `eval` with untrusted input |
| 38 | + |
| 39 | +#### Use case 1: Dynamic command execution |
| 40 | + |
| 41 | +**Insecure pattern:** |
| 42 | + |
| 43 | +```sh |
| 44 | +read filename # User input: "file; echo pwned" |
| 45 | +eval "stat $filename" # Executes: stat file; echo pwned |
| 46 | +``` |
| 47 | + |
| 48 | +This pattern is often seen in more complex code where the command is determined dynamically: |
| 49 | + |
| 50 | +```sh |
| 51 | +somefunction() { |
| 52 | + local arg="$1" filename="$2" |
| 53 | + local cmd="" |
| 54 | + |
| 55 | + case "$arg" in |
| 56 | + -s) cmd="stat" ;; |
| 57 | + -l) cmd="ls" ;; |
| 58 | + esac |
| 59 | + |
| 60 | + eval "$cmd $filename" # VULNERABLE |
| 61 | +} |
| 62 | + |
| 63 | +# Attack example |
| 64 | +somefunction -s "file; rm -rf $HOME" |
| 65 | +``` |
| 66 | + |
| 67 | +**Better pattern:** |
| 68 | + |
| 69 | +```sh |
| 70 | +somefunction() { |
| 71 | + local arg="$1" filename="$2" |
| 72 | + local cmd="" |
| 73 | + |
| 74 | + case "$arg" in |
| 75 | + -s) cmd="stat" ;; |
| 76 | + -l) cmd="ls" ;; |
| 77 | + esac |
| 78 | + |
| 79 | + # SECURE: Zsh handles argument parsing safely |
| 80 | + $cmd "$filename" |
| 81 | +} |
| 82 | +``` |
| 83 | + |
| 84 | +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. |
| 85 | + |
| 86 | +What's preventing injection are not the quotes around `$filename`, but the fact that we are passing each argument separately |
| 87 | +in the command. This way, zsh knows exactly what each argument is supposed to be in the parsing phase, before any variable expansion happens. |
| 88 | + |
| 89 | +### Use case 2: Variable assignment |
| 90 | + |
| 91 | +**Insecure pattern:** |
| 92 | + |
| 93 | +This almost never makes sense: |
| 94 | + |
| 95 | +```sh |
| 96 | +read var # User input: "file; echo pwned" |
| 97 | +eval "foo=$var" # Executes: foo=file; echo pwned |
| 98 | +``` |
| 99 | + |
| 100 | +But this one is more common: |
| 101 | + |
| 102 | +```sh |
| 103 | +read var # User input: "echo pwned; myvariable" |
| 104 | +eval "$var=helloworld" # Executes: echo pwned; myvariable=helloworld |
| 105 | +``` |
| 106 | + |
| 107 | +**Better pattern:** |
| 108 | + |
| 109 | +Use `typeset` (or `declare`, or any of the other built-in variable assignment commands): |
| 110 | + |
| 111 | +```sh |
| 112 | +read var |
| 113 | +typeset "$var=helloworld" # SECURE: throws error instead of executing |
| 114 | +``` |
| 115 | + |
| 116 | +This approach throws an error on malicious input rather than executing arbitrary commands: |
| 117 | + |
| 118 | +```sh |
| 119 | +➜ var="file; echo pwned" |
| 120 | +➜ typeset "$var=helloworld" |
| 121 | +➜ typeset -p myvariable |
| 122 | +typeset myvariable=helloworld |
| 123 | + |
| 124 | +➜ var="echo pwned; myvariable" |
| 125 | +➜ typeset "$var=helloworld" |
| 126 | +typeset: not valid in this context: echo pwned; myvariable |
| 127 | +``` |
| 128 | + |
| 129 | +### print -P with untrusted input |
| 130 | + |
| 131 | +**Past vulnerabilities:** |
| 132 | + |
| 133 | +- [Issue #10414](https://github.com/ohmyzsh/ohmyzsh/issues/10414): Command injection via `print -P` with untrusted input |
| 134 | + |
| 135 | +#### Use case: displaying formatted output with prompt sequences |
| 136 | + |
| 137 | +**Insecure pattern:** |
| 138 | + |
| 139 | +```sh |
| 140 | +setopt promptsubst |
| 141 | +dailyquote="$(get_daily_quote_from_the_internet)" |
| 142 | +print -P "%F{yellow}%B${dailyquote}%b%f" # VULNERABLE |
| 143 | +``` |
| 144 | + |
| 145 | +If `dailyquote` contains `$()` or backticks (`` ` ``), zsh will execute those commands. |
| 146 | + |
| 147 | +**Better pattern:** |
| 148 | + |
| 149 | +```sh |
| 150 | +# Disable prompt substitution for untrusted content |
| 151 | +setopt localoptions nopromptsubst |
| 152 | +dailyquote="$(get_daily_quote_from_the_internet)" |
| 153 | +print -P "%F{yellow}%B${dailyquote}%b%f" |
| 154 | + |
| 155 | +# Use `print` without `-P` to avoid prompt substitution |
| 156 | +setopt promptsubst |
| 157 | +dailyquote="$(get_daily_quote_from_the_internet)" |
| 158 | +print "${fg[yellow]}${dailyquote}${reset_color}" |
| 159 | +``` |
| 160 | + |
| 161 | +### Unescaped % characters in prompt functions |
| 162 | + |
| 163 | +**Past vulnerabilities:** |
| 164 | + |
| 165 | +- [CVE-2021-45444](https://www.cve.org/CVERecord?id=CVE-2021-45444): Command execution via crafted branch names in prompts (partially fixed in zsh 5.8.1) |
| 166 | +- [Issue #10414](https://github.com/ohmyzsh/ohmyzsh/issues/10414): Unquoted % characters in prompt functions |
| 167 | + |
| 168 | +#### Use case: Functions that output untrusted content in prompts |
| 169 | + |
| 170 | +**Insecure pattern:** |
| 171 | + |
| 172 | +```sh |
| 173 | +gitbranch() { |
| 174 | + command git symbolic-ref --short HEAD |
| 175 | +} |
| 176 | +PROMPT='myuser:$(gitbranch) %% ' # VULNERABLE |
| 177 | +``` |
| 178 | + |
| 179 | +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](https://www.cve.org/CVERecord?id=CVE-2021-45444). An attacker could craft a malicious branch name to execute arbitrary commands. |
| 180 | + |
| 181 | +**Better pattern:** |
| 182 | + |
| 183 | +```sh |
| 184 | +gitbranch() { |
| 185 | + local branch="$(command git symbolic-ref --short HEAD)" |
| 186 | + echo -n "${branch//\%/%%}" # Escape all % characters |
| 187 | +} |
| 188 | +PROMPT='myuser:$(gitbranch) %% ' # SECURE |
| 189 | +``` |
| 190 | + |
| 191 | +Functions that output untrusted content for use in `$PROMPT` or `$RPROMPT` must: |
| 192 | +1. Escape all `%` characters by replacing them with `%%` |
| 193 | +2. Be documented and named to indicate they are prompt-safe |
| 194 | +3. Be used only in prompt contexts where this escaping is appropriate |
0 commit comments