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:
Operators which propagate the taint mark
= (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.
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.