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.