Understanding SQL Injection


SQL injection is a code injection technique where an attacker inserts malicious SQL statements into application queries. It has been the top vulnerability in the OWASP Top 10 for years and remains one of the most damaging attack vectors despite being well-understood.


How It Works


Consider a vulnerable login query built by string concatenation:



String query = "SELECT * FROM users WHERE email = '" + email + "' AND password = '" + password + "'";


An attacker providing `email = admin@example.com' --` comments out the password check, logging in as admin without knowing the password. Worse, providing `email = '; DROP TABLE users; --` could destroy the database entirely.


Defense Layer 1: Parameterized Queries


Parameterized queries (also called prepared statements) separate SQL logic from data. User input is always treated as data, never as executable code.


Node.js (mysql2)



const mysql = require('mysql2/promise');

const connection = await mysql.createConnection({ /* config */ });



const [rows] = await connection.execute(

    'SELECT * FROM users WHERE email = ? AND password_hash = ?',

    [email, passwordHash]

);


Python (psycopg2)



import psycopg2



conn = psycopg2.connect("dbname=test user=postgres")

cur = conn.cursor()

cur.execute(

    "SELECT * FROM users WHERE email = %s AND status = %s",

    (user_email, 'active')

)


Java (JDBC)



String sql = "SELECT * FROM products WHERE category = ? AND price < ?";

PreparedStatement stmt = connection.prepareStatement(sql);

stmt.setString(1, category);

stmt.setBigDecimal(2, maxPrice);

ResultSet rs = stmt.executeQuery();


Go (database/sql)



rows, err := db.Query(

    "SELECT * FROM users WHERE email = $1 AND active = $2",

    email, true,

)


Defense Layer 2: ORM Protections


Modern ORMs provide built-in protection against SQL injection when used correctly.


Django ORM



# Safe: ORM parameterizes automatically

users = User.objects.filter(email=user_input, is_active=True)



# Safe: Raw queries with parameters

users = User.objects.raw(

    'SELECT * FROM auth_user WHERE email = %s', [user_input]

)



# UNSAFE: String interpolation bypasses protections

users = User.objects.raw(f'SELECT * FROM auth_user WHERE email = "{user_input}"')


SQLAlchemy



# Safe

result = session.execute(

    text("SELECT * FROM users WHERE email = :email"),

    {"email": user_input}

)



# UNSAFE

result = session.execute(

    text(f"SELECT * FROM users WHERE email = '{user_input}'")

)


Defense Layer 3: Input Validation


Parameterized queries prevent SQL injection in most cases, but input validation provides defense in depth.



import re



def validate_username(username):

    """Allow only alphanumeric and underscore."""

    if not re.match(r'^[a-zA-Z0-9_]{3,30}$', username):

        raise ValueError("Invalid username format")

    return username


Defense Layer 4: Stored Procedures


Stored procedures can add an abstraction layer between queries and user input.



CREATE PROCEDURE GetUserByEmail(IN user_email VARCHAR(255))

BEGIN

    SELECT id, username, email

    FROM users

    WHERE email = user_email AND deleted_at IS NULL;

END



cursor.callproc('GetUserByEmail', [email])


Note that stored procedures must still use parameterized calls internally. A stored procedure that concatenates strings is still vulnerable.


Defense Layer 5: Least Privilege


Limit database account permissions so that a successful injection causes minimal damage:


| Account | Permissions | Purpose |

|---------|-------------|---------|

| app_read | SELECT only | Read-only queries |

| app_write | SELECT, INSERT, UPDATE | Write operations |

| app_admin | SELECT, INSERT, UPDATE, DELETE | Admin functions |

| migration | DDL (CREATE, ALTER, DROP) | Schema migrations only |


Never use the database root or admin account for application connections.


Advanced Threats


Second-Order SQL Injection


Data stored in the database can later be used unsafely in a different query. Parameterized queries on all queries mitigate this.


Blind SQL Injection


Attackers infer information through boolean or time-based responses. Use uniform error messages and prevent timing-based inference by adding random delays.


Detection Tools


| Tool | Type | Coverage |

|------|------|----------|

| SQLMap | Automated exploitation testing | Dynamic analysis |

| OWASP ZAP | DAST scanner | Runtime detection |

| SonarQube | SAST | Static code analysis |

| Semgrep | SAST | Custom rule patterns |



# Scan Go code for string concatenation in SQL queries

semgrep --config "p/sql-injection" .


Summary


SQL injection is entirely preventable. Parameterized queries are the single most effective defense and should be the default pattern for every database interaction. Layer in ORM protections, input validation, least-privilege database accounts, and static analysis tools to create defense-in-depth. Never concatenate user input directly into SQL strings, and train every developer on these principles from day one.