CWE-89: SQL Injection
Overview
SQL Injection occurs when untrusted data is incorporated into SQL queries without proper sanitization, allowing attackers to manipulate query logic, access unauthorized data, or execute administrative operations.
OWASP Classification
A05:2025 - Injection
Risk
Critical: Can lead to complete database compromise, data theft, data modification, or authentication bypass.
Remediation Steps
Core principle: Never build SQL by concatenating untrusted input; use parameterized queries (prepared statements) so user input is always treated as data, not query structure.
Trace the Data Path and Identify the Vulnerability
Analyze the data_path from source to sink to understand how untrusted data reaches the SQL query:
- Source: Where untrusted data enters (user input, external file, database, network request)
- Sink: The SQL execution function (
.execute(),.query(),.createQuery()) - String concatenation: Look for
+,concat(),format(), or template literals building SQL - Identify the SQL construction method (concatenation vs parameterized)
Use Parameterized Queries (Primary Defense)
This is the primary defense. Parameterized queries ensure untrusted data is treated as data, never as SQL code.
- Refactor all SQL queries to use parameterized queries or prepared statements
- Separate query structure from data values
- Use placeholders (
?,$1,:name) for all untrusted values - Never concatenate untrusted data into SQL strings
- Verify all untrusted inputs in the query are parameterized
Use ORM Query Builders Safely
Modern ORMs provide safe query construction APIs that prevent injection when used correctly:
- Use ORM methods like
.filter(),.where(),.find()with parameter binding - Avoid raw SQL queries or use ORM's parameterization for raw queries
- Consult language-specific guidance below for framework-specific safe patterns
Add Input Validation (Defense in Depth)
Even with parameterized queries, validate all untrusted data as an additional layer:
- Validate data types (numeric IDs should be integers)
- Validate format (dates, emails, UUIDs)
- Use allowlists for enumerated values
- Enforce length limits to prevent DoS
- Never rely solely on input validation as primary defense
Apply Least Privilege and Database Hardening
Restrict database permissions to limit SQLi impact even if injection occurs:
Minimum Required Permissions:
- Use database accounts with minimal permissions
- Read-only accounts for SELECT operations
- Restrict access to system tables and stored procedures
- Never grant DROP, CREATE, ALTER, TRUNCATE to application users
- Deny access to system stored procedures (e.g.,
xp_cmdshell)
Application Database User Permissions (PostgreSQL example):
-- Create restricted application user
CREATE USER app_user WITH PASSWORD 'strong_password';
-- Grant only necessary permissions
GRANT SELECT, INSERT, UPDATE ON app_tables TO app_user;
GRANT SELECT ON lookup_tables TO app_user;
-- NEVER grant these to application users:
-- REVOKE DROP, CREATE, ALTER, TRUNCATE, DELETE FROM app_user;
-- REVOKE EXECUTE ON system stored procedures
MySQL/MariaDB Example:
CREATE USER 'app_user'@'app_server_ip' IDENTIFIED BY 'strong_password';
GRANT SELECT, INSERT, UPDATE ON myapp.users TO 'app_user'@'app_server_ip';
REVOKE FILE, PROCESS, SUPER, SHUTDOWN, CREATE USER ON *.* FROM 'app_user'@'app_server_ip';
SQL Server Example:
CREATE LOGIN app_user WITH PASSWORD = 'Strong_P@ssw0rd';
CREATE USER app_user FOR LOGIN app_user;
ALTER ROLE db_datareader ADD MEMBER app_user;
ALTER ROLE db_datawriter ADD MEMBER app_user;
DENY ALTER ANY USER TO app_user;
DENY DROP ANY TABLE TO app_user;
DENY EXECUTE ON xp_cmdshell TO app_user;
Separate Users for Different Operations:
-- Read-only user for reports
CREATE USER report_user WITH PASSWORD 'password';
GRANT SELECT ON ALL TABLES IN SCHEMA public TO report_user;
-- Write user for application (no DELETE)
CREATE USER app_user WITH PASSWORD 'password';
GRANT SELECT, INSERT, UPDATE ON app_tables TO app_user;
What This Prevents:
Even if SQLi occurs, restricted permissions prevent:
'; DROP TABLE users; --- No DROP permission' UNION SELECT password FROM admin_users --- No access to admin tables'; EXEC xp_cmdshell 'rm -rf /' --- No EXECUTE on system procs
Key principle: Least privilege reduces blast radius but does NOT eliminate SQLi risk. Always use parameterized queries.
Test with Malicious Inputs
Verify your fixes by testing with SQL injection payloads:
' OR '1'='1(authentication bypass)'; DROP TABLE users--(data destruction)1' UNION SELECT password FROM admins--(data exfiltration)'; WAITFOR DELAY '00:00:10'--(blind SQLi detection)
Language-Specific Guidance
For detailed, framework-specific code examples and patterns, see:
- Python - sqlite3, psycopg2, MySQL, Django ORM, SQLAlchemy
- Java - JDBC, Spring Data JPA, Hibernate with parameterized queries
- JavaScript/Node.js - mysql2, pg, Sequelize, TypeORM, Knex.js
- C# - ADO.NET, Entity Framework with parameterization
- PHP - PDO, MySQLi with prepared statements
Dynamic Scan Guidance
For guidance on remediating this CWE when detected by dynamic (DAST) scanners:
- Dynamic Scan Guidance - Analyzing DAST findings and mapping to source code