Second-Order SQL Injection
Also Known As
stored SQL injection
second-order injection
persistent SQLi
TL;DR
Malicious data is safely stored in the database but later retrieved and used unsafely in a subsequent SQL query.
Explanation
Second-order (or stored) SQL injection occurs when user input is properly escaped on first insertion but later retrieved from the database and interpolated into another query without re-escaping. Developers often trust data already in their database, forgetting it may be attacker-controlled. The fix is identical to first-order injection prevention: use parameterised queries and prepared statements everywhere data is used in a query, regardless of its origin.
How It's Exploited
A user registers with the username admin'--. The registration safely escapes the apostrophe. Later, an admin panel retrieves the stored username and embeds it directly in a query like UPDATE users SET password='new' WHERE username='admin'--', commenting out the WHERE clause.
Common Misconception
✗ Data that was safely inserted into the database is safe to use in future queries. Second-order SQLi stores malicious input correctly, then injects it when the stored value is later retrieved and concatenated into another query without parameterisation.
Why It Matters
Second-order SQL injection stores a malicious payload safely, then uses it in an unsafe query later — passing the first validation layer but exploiting the second interaction.
Common Mistakes
- Trusting data retrieved from the database as 'safe' because it was stored via parameterised query.
- Not parameterising queries that use database-sourced values — escaped on insert, concatenated on use.
- Username/email stored safely but used as-is in dynamic SQL in stored procedures or admin queries.
- Not auditing code that reads from the DB and then uses the value in another query.
Code Examples
✗ Vulnerable
// Safe insert — parameterised:
$stmt = $pdo->prepare('INSERT INTO users (name) VALUES (?)');
$stmt->execute(["' OR '1'='1"]); // Stored safely
// Unsafe later use — second-order injection:
$name = $pdo->query('SELECT name FROM users WHERE id = 1')->fetchColumn();
$users = $pdo->query("SELECT * FROM logs WHERE user = '$name'"); // SQLI!
✓ Fixed
// Always use prepared statements at query time — even for 'stored safe' data:
public function promoteToAdmin(int $userId): void {
// Even if $userId came from the DB and looks safe:
$stmt = $this->pdo->prepare(
'UPDATE users SET role = ? WHERE id = ?'
);
$stmt->execute(['admin', $userId]);
}
// Fetch then re-use safely:
$username = $this->pdo
->prepare('SELECT username FROM users WHERE id = ?')
->execute([$id])
->fetchColumn();
// Now use it safely in another query:
$stmt = $this->pdo->prepare('SELECT * FROM logs WHERE username = ?');
$stmt->execute([$username]); // Prepared — safe regardless of stored content
References
Tags
🤝 Adopt this term
£79/year · your link shown here
Added
15 Mar 2026
Edited
22 Mar 2026
Views
21
🤖 AI Guestbook educational data only
|
|
Last 30 days
Agents 0
No pings yet today
Amazonbot 7
Google 2
Perplexity 2
Unknown AI 2
ChatGPT 2
Ahrefs 1
Also referenced
How they use it
crawler 13
crawler_json 2
pre-tracking 1
Related categories
⚡
DEV INTEL
Tools & Severity
🔴 Critical
⚙ Fix effort: Medium
⚡ Quick Fix
Use prepared statements for every database operation including reads of previously stored data — safe storage does not make data safe for later reuse in queries
📦 Applies To
PHP 5.0+
web
cli
🔗 Prerequisites
🔍 Detection Hints
Data read from DB then concatenated into another query without parameterisation; stored username used in dynamic SQL later
Auto-detectable:
✗ No
semgrep
⚠ Related Problems
🤖 AI Agent
Confidence: Medium
False Positives: Medium
✗ Manual fix
Fix: High
Context: File
Tests: Update
CWE-89
CWE-564