Second-order SQLi
The injection point and the trigger are different requests. You submit a payload through one form, the application stores it (safely-quoted), and later uses the stored value in a different query that doesn’t re-quote it. The second query executes the injection.
- Find an input that gets stored in the database (registration, profile update, comment, filename).
- Submit a payload that survives the initial insert as a literal string.
- Trigger the second query that reads the stored value (login, profile view, file lookup, search).
- Injection fires.
Common payload shapes:
admin'-- -admin' UNION SELECT password FROM users-- -admin'+(SELECT 1 FROM users WHERE pg_sleep(5))+'Why first-order tests miss it
Section titled “Why first-order tests miss it”The first-order test (inject ', look for an error) only checks the immediate response. Second-order vulnerabilities have:
- Input request:
POST /registerwithusername=admin'-- -- succeeds with HTTP 200, no error visible. - Storage: Application uses parameterised insert, so the literal string
admin'-- -lands in the database safely. - Trigger request:
POST /loginwith the same username. The login flow doesSELECT * FROM logs WHERE created_by = '<retrieved_username>'- concatenating the stored value without re-quoting. - Injection fires at step 3, not step 1.
Classic example: registration → login
Section titled “Classic example: registration → login”# Step 1: RegisterPOST /registerusername=admin'-- -&password=test123
# Server: INSERT INTO users (username, password) VALUES (?, ?) -- safe# Stored username: admin'-- -
# Step 2: LoginPOST /loginusername=admin'-- -&password=test123
# Server: SELECT * FROM users WHERE username='admin'-- -' AND password='...'# Comment removes password check → logged in as admin (or whichever account already exists)Other common second-order paths
Section titled “Other common second-order paths”| Input request | Storage | Trigger request | Affected query |
|---|---|---|---|
| Update profile name | users.name | View dashboard | Activity-log query joins on name |
| Upload file | files.original_name | Download | Lookup by original_name |
| Set password (yes, sometimes) | users.password | Login or password change | Audit log |
| Add comment | comments.body | Search | LIKE clause on body |
| Set bio | users.bio | Public profile | Sometimes used in templating queries |
Detection workflow
Section titled “Detection workflow”Manual:
- Enumerate every field that gets stored.
- For each, register/submit with a benign-but-detectable payload like
' OR pg_sleep(0)-- -- no behavioural impact. - Walk through the application using the account/data that contains the payload.
- Look for any page where response time changes, errors appear, or behaviour shifts. That page is the trigger.
- Replace the benign payload with
pg_sleep(5)or full extraction.
Automated:
sqlmap has limited support - --second-url lets you specify a separate URL to fetch after each injection request:
sqlmap -u "https://<TARGET>/register" \ --data="username=test*&password=test" \ --second-url="https://<TARGET>/login" \ --technique=T --dbms=mysqlThis is fragile - it works for simple register-then-login flows but not for multi-step workflows.
- Second-order is harder to detect but often easier to exploit once found, because the trigger query is frequently the most privileged one (auth, admin actions).
- Look for second-order in any application that does post-processing of stored user input: audit logs, search indexers, batch jobs, report generation.
- Stored procedures and triggers can also fire second-order injection from data inserted by an unrelated path.