#0305
A user profile endpoint lets users update their bio. The handler stores the bio in the database — but builds the UPDATE query by concatenating both the bio and the userId inline.
Even if a frontend layer escapes the bio before display, the storage query itself is injectable. An attacker can craft a bio that contains SQL that fires when the bio is first written: '; UPDATE users SET role='admin' WHERE '1'='1.
This is called second-order (or stored) SQL injection: the payload is injected at write time, and may execute silently in the background or when the bio is later reused in another query.
Second-order injection is harder to detect than first-order because the injected payload is stored safely-looking text. It bypasses input validation at the API layer and can lie dormant until triggered by a different code path.
buildProfileUpdateQuery so both bio and userId are passed as parameters.{ sql: "UPDATE profiles SET bio=$1 WHERE user_id=$2", params: [bio, userId] }.buildProfileUpdateQuery('u1', "'; UPDATE users SET role='admin' WHERE '1'='1")// FIX → { sql: 'UPDATE profiles SET bio=$1 WHERE user_id=$2', params: ["'; UPDATE...", 'u1'] }
buildProfileUpdateQuery('u1', 'I love coding')// → { sql: '...SET bio=$1 WHERE user_id=$2', params: ['I love coding', 'u1'] }
sql must use $1 for bio and $2 for userId.params array must be [bio, userId] in that order.