Varol Cagdas Tok

Personal notes and articles.

Second-Order SQL Injection

Second-order SQL injection is a variant where the injection point and the execution point are in different code paths. User input is stored in the database using safe practices, passing through parameterized queries or proper escaping. Later, in a separate operation, the stored value is retrieved and embedded into a new SQL query without encoding. The payload is dormant in the database until that second operation executes it.


A Concrete Scenario

A user registration form accepts a username. The registration handler uses a parameterized query to store it:

INSERT INTO users (username, password) VALUES (?, ?)

The username admin'-- is stored verbatim, safely, with the single quote as a literal character in the database. No injection occurs at registration.

Later, a password change feature retrieves the logged-in user's username from the database and builds a query to update their password:

query = "UPDATE users SET password='" + newPassword + "' WHERE username='" + username + "'";

The code retrieves username from the database, trusting it as clean data because it came from the database rather than from the current request. The retrieved value is admin'--. The constructed query becomes:

UPDATE users SET password='newpass' WHERE username='admin'--'

The single quote in the username closes the string literal. The -- comments out the remainder of the WHERE clause. The UPDATE targets the row where username is admin, not the current user. The attacker has changed the admin account's password.


Why Input-Time Defenses Do Not Stop This

Defenses applied at the point where user input enters the system do not prevent second-order injection because the injection does not occur at that point. Input validation, escaping at write time, and WAF rules that inspect incoming requests all operate on the first operation. The vulnerable second operation reads from the database, a source that input-time defenses do not inspect.

The root cause is the same as direct injection: unparameterized query construction. The difference is the data source: the current request versus a previous database read. Neither source should be trusted in a concatenated query context.


Detection Difficulty

Second-order injection is harder to find with automated scanners. A scanner that submits a payload and immediately inspects the response will see no injection in the registration endpoint because none occurs there. The payload only executes when a different endpoint, triggered separately, reads the stored value and uses it in an unsafe query. Finding it requires tracing data flow across multiple operations, often across multiple requests and code paths that a scanner does not correlate.

Code review is more reliable. Any code path that reads stored data and uses it in string-concatenated SQL is a second-order injection candidate, regardless of how safely the data was stored.