Don't use variables in the printf format string

A printf format string built from user input is a format-string injection. Always use a literal format and pass data as arguments.

Problem

`printf "$msg"` lets the value of `$msg` be interpreted as a format specification. A `%s` inside the value reads from the next argument (likely garbage), and `%n` can write to memory on some implementations. This is exactly the C printf vulnerability, in shell form.

Bad

msg="Hello %s"
printf "$msg" "$name"

Good

printf '%s\n' "$msg"
printf 'Hello %s\n' "$name"

Explanation

Treat the format string the same way you'd treat a SQL query: it must be a literal that you control, with all variable data passed as arguments.

When It Matters

printf "$msg" treats the variable as the format string, so any percent sign in the data is interpreted as a conversion specifier. A log line containing "50% done" prints garbage, and a value containing %n has historically been a route to memory corruption in C implementations — in the shell it at minimum consumes arguments that are not there. Since the data usually comes from user input, filenames, or command output, this is a format-string injection in a language people forget can have one.

Second Example

Note

When printf gets more arguments than the format consumes, it reuses the format — which is why one %s\n prints an entire array, one element per line.

Exceptions

A variable holding a format string you constructed yourself, for example to build a column layout at runtime, is a legitimate use. Keep the data out of it, and add a disable comment noting that the variable is a format, not user data.

Faq

Q

How do I print a literal percent sign?

A

Use %% in the format string, or pass the text as a %s argument where it is not interpreted at all.

Q

Why is printf preferred over echo for variables?

A

echo may interpret leading dashes as options and backslashes as escapes depending on the shell. printf %s prints the value byte for byte.

Q

Can I build column widths dynamically?

A

Yes: printf accepts * as a width taken from the argument list, as in printf '%-*s|\n' "$width" "$text".