The taint mark is a single bit stored on the string itself, not on the variable holding it. Assigning, passing or otherwise sharing a tainted string keeps the mark. String concatenation and interpolation propagate it as well:
How the taint mark is propagated
The taint mark is a single bit stored on the string itself, not on the variable holding it. Assigning, passing or otherwise sharing a tainted string keeps the mark. String concatenation and interpolation propagate it as well:
= (assignment, including list()/array destructuring) |
. (concatenation) |
.= (concatenating assignment) |
"{$var}" (string interpolation, including the ROPE fast path) |
In addition, taint understands a fixed set of string functions: when any of the relevant string arguments is tainted, the returned string is marked tainted too. Both the regular call and, on PHP 8.4+, the frameless fast-path call are covered.
trim(), rtrim(), ltrim() |
substr(), strstr() |
str_replace(), str_ireplace() |
str_pad(), strtolower(), strtoupper(), strval() |
explode() (every element of the resulting array) |
implode()/join() (a tainted separator taints the result as well) |
sprintf(), vsprintf() (only the %s specifier carries the mark; sprintf("%d", $t) returns a clean string) |
dirname(), basename(), pathinfo() |
Any function taint does not explicitly understand returns a fresh, unmarked string — including escaping helpers such as htmlspecialchars(), htmlentities() or mysqli_real_escape_string(). This is deliberate: taint over-reports rather than trying to decide whether a value is safe for a particular output context. Use untaint() to clear the mark on values you have validated yourself.
Where taint raises warnings
When a tainted string reaches one of the sinks below, taint raises a warning (by default an E_USER_WARNING; the level is configurable via taint.error_level). Only top-level string arguments are inspected; dumping an array that merely contains tainted values does not warn.
| Sink | Checked |
|---|
fopen(), opendir(), unlink() | the path |
file(), readfile(), file_get_contents(), highlight_file()/show_source() | the path |
copy(), rename(), move_uploaded_file() | both the source and destination paths |
mkdir(), rmdir(), touch() | the path |
include, include_once, require, require_once | the file path |
| Sink | Checked |
|---|
mysqli_query(), mysqli_prepare(), mysqli_real_query(), mysqli_multi_query() | the query string |
mysql_query(), sqlite_query(), sqlite_single_query(), oci_parse(), pg_query(), pg_send_query() | the query string |
mysqli::query, mysqli::prepare, mysqli::real_query, mysqli::multi_query | the query string |
PDO::query, PDO::prepare, PDO::exec | the query string |
SQLite3::query, SQLite3::prepare, SQLite3::exec, SQLiteDatabase::query, SQLiteDatabase::singleQuery | the query string |
| Sink | Checked |
|---|
exec(), system(), passthru(), shell_exec() (including the backtick operator) | the command string |
proc_open(), popen() | the command string |
eval | the evaluated code |
dynamic calls such as $func(), $obj->$method(), call_user_func(), array callables | the function/method/class name being resolved |
preg_match(), preg_match_all(), preg_replace(), preg_split(), preg_grep(), preg_replace_callback() | the pattern (and the callback name for preg_replace_callback()) |
| Sink | Checked |
|---|
unserialize() | the serialized string |
mail() | to, subject, additional parameters and additional headers (the message body is content and is not checked) |
Warnings follow the format function_name() [sink]: message, where sink identifies the checked operation (for example echo, include or the function name) and the message describes what was found to be possibly tainted.