SQL Injection Prevention: A Developer's Complete Guide

SQL Injection Prevention: A Developer's Complete Guide

SQL injection remains one of the most dangerous web vulnerabilities. Learn how it works, see real attack examples, and master prevention techniques including parameterized queries.

Passwordly Team
11 min read

What Is SQL Injection

SQL Injection (SQLi) is a code injection technique that exploits vulnerabilities in the data layer of an application. It occurs when user-supplied input is included in a SQL query without proper sanitization, allowing an attacker to modify the query's logic and execute arbitrary SQL commands.

Despite being known for over 25 years, SQL injection remains one of the most prevalent and damaging web application vulnerabilities. In OWASP's Top 10, injection attacks rank #3 (2021) and have appeared in every version of the list since its inception.

Why is SQLi still so common?

  • Legacy applications built without secure coding practices
  • Developers who concatenate strings to build SQL queries (the fundamental mistake)
  • Dynamic query construction that isn't consistently parameterized
  • Complex applications where some code paths bypass security controls
  • Insufficient code review and testing for injection vulnerabilities

What can an attacker do with SQL injection?

  • Read unauthorized data โ€” dump entire database tables including passwords, personal info, financial records
  • Modify or delete data โ€” alter account balances, delete records, change permissions
  • Bypass authentication โ€” log in as any user, including administrators
  • Execute admin operations โ€” create new database users, shut down the database
  • Read/write files on the database server (on some database systems)
  • Execute operating system commands (in severe cases, achieving full server compromise)

How SQL Injection Works

The fundamental cause of SQL injection is treating user input as code instead of data.

Consider a login form that checks credentials with this query:

query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'"

If a user enters alice and correctpassword, the query becomes:

SELECT * FROM users WHERE username = 'alice' AND password = 'correctpassword'

This works as intended. But what if an attacker enters this as the username?

' OR '1'='1' --

Now the query becomes:

SELECT * FROM users WHERE username = '' OR '1'='1' --' AND password = ''

Breaking this down:

  • '' โ€” empty username (from the closing quote the attacker provided)
  • OR '1'='1' โ€” this condition is always true, so the WHERE clause matches all rows
  • -- โ€” SQL comment, ignores the rest of the query (the password check)

The database returns all user rows because '1'='1' is always true. The application sees a successful result and logs the attacker in โ€” usually as the first user in the table, which is often the administrator.

Another example โ€” data exfiltration via UNION attack:

A product search page runs:

SELECT name, price FROM products WHERE category = '{input}'

An attacker enters:

' UNION SELECT username, password FROM users --

The resulting query:

SELECT name, price FROM products WHERE category = '' UNION SELECT username, password FROM users --'

The UNION combines the product results with all usernames and passwords from the users table. The attacker sees the entire user database displayed on the product page.

Types of SQL Injection

SQL injection attacks are categorized by how the attacker extracts information:

1. In-Band SQLi (Classic) The attacker uses the same communication channel to launch the attack and retrieve results. This is the most common and easiest to exploit.

  • Error-based: The attacker triggers database errors that reveal information in error messages. For example, causing a type conversion error that includes a database value in the error message.
  • UNION-based: The attacker uses UNION SELECT to combine malicious queries with the original query, returning results directly in the application's response.

2. Blind SQLi The application doesn't display database errors or query results directly. The attacker infers information by observing the application's behavior.

  • Boolean-based blind: The attacker asks true/false questions via SQL and observes whether the page content changes. For example: ' AND SUBSTRING(username,1,1)='a' -- โ€” if the page loads normally, the first character of the username is 'a'.
  • Time-based blind: The attacker uses SQL time delays to infer information. For example: ' AND IF(SUBSTRING(password,1,1)='a', SLEEP(5), 0) -- โ€” if the response takes 5 seconds, the first character is 'a'.

3. Out-of-Band SQLi The attacker uses a different channel to receive data โ€” typically triggering the database to make an HTTP or DNS request to an attacker-controlled server, carrying the extracted data. This works when in-band techniques are blocked but the database can make outbound connections.

4. Second-Order SQLi The malicious input is stored by the application (e.g., in a user profile) and injected later when a different part of the application uses that stored value in a SQL query. This is harder to detect because the injection point and the vulnerable query are in different parts of the application.

Real-World Impact

SQL injection has been behind some of the largest data breaches in history:

  • Heartland Payment Systems (2008): SQL injection leading to the theft of 130 million credit card numbers. Cost: over $140 million.
  • Sony Pictures (2011): SQL injection exposed 77 million PlayStation Network accounts, including passwords stored in plaintext.
  • TalkTalk (2015): SQL injection by a teenager compromised 157,000 customer records. The company was fined ยฃ400,000.
  • Equifax (2017): While the initial breach was via a different vulnerability, the subsequent data exfiltration of 147 million records relied on SQL injection techniques.

Financial impact: According to IBM's Cost of a Data Breach Report, the average cost of a data breach involving SQL injection is significantly higher than average because attackers typically gain broad database access, affecting more records.

Legal consequences: Regulations like GDPR, CCPA, and PCI DSS hold organizations responsible for preventing known vulnerability classes like SQL injection. A breach caused by SQL injection can result in regulatory fines, lawsuits, and mandatory breach notifications.

Parameterized Queries: The Primary Defense

Parameterized queries (prepared statements) are the primary and most effective defense against SQL injection. They work by separating the SQL code from the data, making it impossible for user input to alter the query's structure.

How parameterized queries work:

  1. The SQL query is defined with placeholders (parameters) instead of concatenated values
  2. The query structure is sent to the database and compiled
  3. The parameter values are sent separately
  4. The database treats the parameters strictly as data โ€” never as SQL code

Example in various languages:

Node.js (with pg/postgres):

// VULNERABLE โ€” string concatenation
const result = await db.query(
  "SELECT * FROM users WHERE username = '" + username + "'"
);

// SECURE โ€” parameterized query
const result = await db.query(
  'SELECT * FROM users WHERE username = $1',
  [username]
);

Python (with psycopg2):

# VULNERABLE
cursor.execute("SELECT * FROM users WHERE username = '%s'" % username)

# SECURE
cursor.execute("SELECT * FROM users WHERE username = %s", (username,))

Java (with PreparedStatement):

// VULNERABLE
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(
  "SELECT * FROM users WHERE username = '" + username + "'"
);

// SECURE
PreparedStatement pstmt = conn.prepareStatement(
  "SELECT * FROM users WHERE username = ?"
);
pstmt.setString(1, username);
ResultSet rs = pstmt.executeQuery();

C# (with SqlCommand):

// VULNERABLE
var cmd = new SqlCommand("SELECT * FROM users WHERE username = '" + username + "'");

// SECURE
var cmd = new SqlCommand("SELECT * FROM users WHERE username = @username");
cmd.Parameters.AddWithValue("@username", username);

Why parameterized queries are foolproof: Even if the attacker enters ' OR '1'='1' -- as the username, the database treats the entire string as a literal value to search for. The query looks for a user whose username is literally the string ' OR '1'='1' -- โ€” which won't match anything.

Rule: Never, ever concatenate user input into SQL strings. Use parameterized queries for 100% of database operations that include any external input.

ORM and Query Builder Protection

Object-Relational Mappers (ORMs) and query builders provide an additional layer of protection by abstracting SQL query construction:

Prisma (Node.js/TypeScript):

// Automatically parameterized
const user = await prisma.user.findFirst({
  where: { username: userInput }
});

SQLAlchemy (Python):

# Automatically parameterized
user = session.query(User).filter(User.username == user_input).first()

Hibernate/JPA (Java):

// Using JPQL with parameters
TypedQuery<User> query = em.createQuery(
  "SELECT u FROM User u WHERE u.username = :username", User.class
);
query.setParameter("username", userInput);

Drizzle ORM (TypeScript):

// Automatically parameterized
const user = await db.select().from(users).where(eq(users.username, userInput));

Caution with ORMs: ORMs are not immune to SQL injection if you bypass their safe APIs:

  • Raw queries in ORMs can still be vulnerable. prisma.$queryRawUnsafe(...) or session.execute(text(...)) with string concatenation reintroduces the vulnerability.
  • Always use the parameterized version of raw queries: prisma.$queryRaw with tagged templates, session.execute(text(...), params), etc.
  • Watch for query builder escape hatches โ€” any method that accepts raw SQL strings needs careful parameterization.

Input Validation as Defense in Depth

Input validation is not a substitute for parameterized queries but provides valuable defense in depth:

Allowlist validation (preferred):

  • If a field should be a number, validate it as a number before using it
  • If a field should be one of a known set of values (like a category name), validate against the allowlist
  • If a field has known constraints (email format, date format, phone number), validate the format

Type checking: Ensure inputs match expected types. A username should be a string of specific length and characters. An ID should be an integer. A date should be in ISO format.

Input length limits: SQL injection payloads are often longer than legitimate input. Limiting input length doesn't prevent injection but makes exploitation harder:

  • Username: max 50 characters
  • Email: max 254 characters
  • Search query: max 200 characters

Character filtering (use cautiously): You can reject inputs containing SQL metacharacters (', ;, --, /*), but this is fragile:

  • Legitimate inputs may contain these characters (O'Brien, sentences with semicolons)
  • There are many ways to encode and bypass character filters
  • Never rely on filtering as your sole defense

The layered approach:

  1. Parameterized queries โ€” primary defense (stops injection completely)
  2. Input validation โ€” secondary defense (catches unexpected data early)
  3. Least privilege โ€” tertiary defense (limits damage if injection succeeds)
  4. WAF rules โ€” catch common attack patterns at the network level

Database Least Privilege

The principle of least privilege limits the damage an attacker can cause even if SQL injection is exploited:

Separate database accounts per function:

  • The application's read-only queries should use a database account with SELECT permissions only
  • Write operations should use a different account with INSERT/UPDATE permissions
  • Schema changes and admin operations should never be accessible to the application's database user

Restrict database permissions:

  • Remove FILE privileges (prevents reading/writing filesystem)
  • Remove EXECUTE privileges on system stored procedures
  • Remove privileges to create new database users
  • Remove DROP privileges on production tables
  • Restrict access to system tables and information_schema where possible

Network-level restrictions:

  • The database should only accept connections from the application server (not from the internet)
  • Use firewall rules to restrict database port access
  • Enable TLS for database connections

Stored procedure approach: Instead of granting direct table access, create stored procedures for each operation and grant the application user EXECUTE permission only on those procedures. This limits SQL injection to the operations defined by the procedures.

Testing and Detection

Automated testing tools:

  • SQLMap โ€” Open-source automated SQL injection tester (penetration testing)
  • OWASP ZAP โ€” Free web application security scanner
  • Burp Suite โ€” Professional web security testing (has SQLi detection)
  • Semgrep โ€” Static analysis that catches vulnerable query patterns in code
  • SonarQube โ€” Code quality platform with SQL injection detection rules

Manual testing techniques: Test every input field (form fields, URL parameters, headers, cookies) with basic injection payloads:

  • ' โ€” single quote (look for SQL error messages)
  • ' OR '1'='1 โ€” boolean test
  • ' AND '1'='2 โ€” negative boolean test (page should differ from the positive test)
  • '; DROP TABLE test -- โ€” never on production! (use only in testing environments)
  • ' UNION SELECT NULL,NULL -- โ€” UNION test (adjust NULL count to match columns)

Detection in production:

  • Web Application Firewall (WAF) โ€” Cloudflare, AWS WAF, and ModSecurity detect common SQLi patterns
  • Query logging and anomaly detection โ€” monitor for unusual query patterns, excessive errors, or unexpected data access
  • Input logging โ€” log suspicious inputs that contain SQL metacharacters (for investigation, not as prevention)
  • Rate limiting โ€” automated SQLi tools make many rapid requests; rate limiting slows them down

Code review checklist for SQL injection:

  • [ ] All database queries use parameterized queries or prepared statements
  • [ ] No string concatenation or format strings in SQL queries
  • [ ] Raw SQL in ORMs uses the parameterized API
  • [ ] Input validation applied where appropriate
  • [ ] Database user has least-privilege permissions
  • [ ] Error messages don't expose database details to users
  • [ ] WAF rules are active for SQL injection patterns

Make sure your application's user accounts are also protected with strong, unique passwords โ€” generate them with our password generator.


SQL injection is the oldest trick in the book, yet it still works against countless applications because developers continue to concatenate strings into SQL queries. The fix is definitive and absolute: use parameterized queries for every database interaction, no exceptions. Combine with input validation, least-privilege database accounts, and automated scanning, and SQL injection becomes a vulnerability your application simply doesn't have.

Related Articles

Continue exploring related topics