Why this matters

The bug. EXECUTE runs a dynamic SQL string. Anything the function concatenates into that string becomes SQL grammar. search = "'; DELETE FROM users; --" rewrites the query.

The fix. Two correct patterns: EXECUTE … USING <value> binds parameters into the dynamic SQL the same way prepared statements do, or format('… %L', search) literally-quotes the value through the database's own escape logic. If the query doesn't *need* to be dynamic, drop EXECUTE and use plain RETURN QUERY SELECT … — plpgsql binds plain SQL safely.

Reality check. Most plpgsql injections come from this exact shape: a function that started as plain SQL, then someone needed a dynamic table or sort and reached for EXECUTE without learning USING/format.

Review heuristic

Every SQL string built from concatenation is guilty until proven innocent. If the value originated in a request — directly or indirectly through a stored field a user once supplied — it has to flow through a parameterized binding. ORDER BY and dynamic table names need an allowlist, never a passthrough.

External reference: CWE-89: Improper Neutralization of Special Elements used in an SQL Command.

OWASP A03; CWE-89.