SQL Injection: The Complete Practical Guide

Second-order SQL Injection

Why first-order defenses miss stored-then-executed injection. The delayed sink, how to find it, how to exploit it, and why parameterization at the sink still matters.

Published

Second-order SQL injection is injection that fires later, from a different query than the one that received your input. You submit a payload somewhere harmless, it gets stored, and the bug triggers when some other feature reads that stored value back into SQL without escaping it. It is easy to miss and famous for defeating input filters.

Why first-order defenses miss it

Most defenses sit at the front door, where input first arrives. A registration form might escape or parameterize the INSERT, store your value perfectly safely, and declare victory. The problem is the back door: a later query that pulls your stored value out and concatenates it into new SQL. Because the value already lives in the database, the developer trusts it. That trust is the vulnerability.

The stored-then-executed flow

The pattern has two steps separated in time and code:

  1. Store. You set your display name, address, or some profile field to a SQL payload. The write is safe, so nothing looks wrong, and your payload sits in a column verbatim.
  2. Execute. Later, a different feature reads that field and builds a query by concatenation. For example an admin report, a "users like you" panel, or a username change that re-queries by the old name. Now your payload is in the query text.

The injection point and the sink are in different requests, which is exactly why a point-in-time test on the input often shows nothing.

Finding the delayed sink

Hunt for places where stored data flows back into SQL:

  • Set a profile field to a canary like '||(SELECT ...)||' or ' -- - and then exercise every feature that might read it: reports, search, admin dashboards, exports, a name change.
  • Watch for a feature that behaves oddly, errors, or changes only after you set the stored value.
  • Think about writes too: an UPDATE users SET ... WHERE username = 'STORED' that re-uses a stored username is a classic sink.

A common chain is a profile update whose new value later lands, unescaped, in an administrator's reporting query.

Exploiting it

Once you have located the sink, craft the stored value so it breaks out in the second query. Because the sink is typically blind (you do not see its output), you lean on blind or out-of-band techniques to read results. The payload lives in storage, and the extraction happens when the sink runs.

Why parameterization at the sink still matters

The lesson: escaping input at entry is not enough. Data read back from your own database is still untrusted the moment you concatenate it into a new query. Parameterize every query, including the ones that read stored values, and the second-order path closes. Treat "it came from our database" as no safer than "it came from the user."

See injection in unexpected contexts for more sinks people forget, and try a stored-then-executed chain on a free live target.

← Back to SQL Injection: The Complete Practical Guide