← Home ← Codex ← DEBT
Browse by Category
+ added · updated 7d
← Back to glossary

Second-Order SQL Injection

Security CWE-89 OWASP A3:2021 CVSS 8.8 PHP 5.0+ Advanced
debt(d8/e5/b5/t8)
d8 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'silent in production until users hit it' (d9), nudged to d8 because semgrep can catch DB-read-to-query concatenation patterns, but most SAST tools miss the data-flow across storage boundaries.

e5 Effort Remediation debt — work required to fix once spotted

Closest to 'touches multiple files / significant refactor' (e5) — quick_fix says every DB operation including reads of stored data must use prepared statements, requiring audit and rewrite of many query sites across the codebase.

b5 Burden Structural debt — long-term weight of choosing wrong

Closest to 'persistent productivity tax' (b5) — applies_to web and cli contexts; every developer must remember that DB-sourced values are not trusted, shaping how all subsequent queries are written.

t8 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'serious trap that contradicts how a similar concept works elsewhere' (t7), nudged to t8 because the misconception is exactly the intuitive belief — data safely stored via prepared statements feels trustworthy on read, but isn't.

About DEBT scoring →

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

Added 15 Mar 2026
Edited 22 Mar 2026
Views 42
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 0 pings W 1 ping T 0 pings F 1 ping S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 2 pings F 0 pings S 2 pings S 0 pings M 2 pings T 0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 1 ping W 0 pings T 0 pings F 1 ping S 0 pings S 0 pings M 2 pings T 0 pings W
No pings yet today
PetalBot 1 SEMrush 1
Amazonbot 7 Scrapy 6 Ahrefs 4 ChatGPT 4 Google 2 Perplexity 2 Unknown AI 2 Claude 1 Meta AI 1 Sogou 1 PetalBot 1 SEMrush 1
crawler 27 crawler_json 4 pre-tracking 1
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


✓ schema.org compliant