Back to Blog
critical SEVERITY7 min read

How SQL Injection Happens in CSV-to-SQL Converters and How to Fix It

A critical SQL injection vulnerability was discovered in the `csv2sql()` function in `src/data/converter/csv.js`, where CSV data and table names were directly interpolated into SQL INSERT statements without sanitization. The fix implements input validation through identifier sanitization and proper value escaping, eliminating the attack surface while preserving legitimate functionality.

O
By Orbis AppSec
Published August 7, 2026Reviewed August 7, 2026

Answer Summary

This is a SQL injection vulnerability (CWE-89) in a Node.js CSV-to-SQL converter function where user-controlled CSV data and table names are concatenated directly into SQL queries without sanitization or parameterization. The fix applies identifier sanitization to table and column names using allowlist regex patterns and escapes string values using SQL-standard single-quote escaping, preventing attackers from breaking out of the intended SQL structure.

Vulnerability at a Glance

cweCWE-89 (Improper Neutralization of Special Elements used in an SQL Command)
fixImplement identifier sanitization (allowlist alphanumeric + underscore) and SQL-safe value escaping (single-quote doubling)
riskRemote attackers can execute arbitrary SQL commands, potentially exfiltrating or deleting data
languageJavaScript (Node.js)
root causeCSV data and table names concatenated directly into SQL strings without validation
vulnerabilitySQL Injection via String Interpolation

Introduction

In the src/data/converter/csv.js file, a critical SQL injection vulnerability was lurking in the csv2sql() function—a utility designed to convert CSV data into SQL INSERT statements. The problem was straightforward but dangerous: both the table name and CSV content (column names and values) were directly interpolated into SQL strings using template literals and string concatenation, with no sanitization or validation whatsoever.

This file is part of a production Node.js library, meaning the vulnerability affected not just one application, but every downstream consumer using this package. An attacker could craft malicious CSV data to break out of the intended SQL structure and execute arbitrary commands.

The Vulnerability Explained

The Vulnerable Code

Let's look at the problematic function before the fix:

const csv2sql=(csv, Table)=>{
    const lines = csv.trim().trimEnd().split('\n').filter(n=>n);
    const columns = lines[0].split(',');
    let sqlQuery = `INSERT INTO ${Table} (${columns.join(', ')}) Values `
    let sqlValues = []
    for (let i = 1; i < lines.length; i++) {
      const values = lines[i].split(',');
      sqlValues.push(`(${values})`)
    }
    return sqlQuery+sqlValues.join(",\n");
}

Notice three critical problems:

  1. Line 4: ${Table} — The table name is directly interpolated without any validation
  2. Line 3: columns.join(', ') — Column names from CSV are spliced directly into the query
  3. Line 9: ${values} — CSV values are interpolated without escaping

The Attack Scenario

Imagine an attacker provides this CSV data:

id, name); DROP TABLE users;--
1, test

With Table = "users", the vulnerable function would generate:

INSERT INTO users (id,  name); DROP TABLE users;--) Values (1, test)

The attacker successfully injected a DROP TABLE command! The -- comment syntax prevents SQL syntax errors that would normally block the injection. The entire users table could be deleted.

A more sophisticated attack might use UNION SELECT to exfiltrate data, or INTO OUTFILE to write files to the filesystem.

Why This Matters

This function is used to process untrusted CSV files—possibly uploaded by users, received from external APIs, or read from user-controlled sources. Any application consuming this library and passing unsanitized CSV data to csv2sql() becomes vulnerable to SQL injection attacks.

The Fix

The fix implements two complementary security controls:

1. Identifier Sanitization

const sanitizeId = s => s.trim().replace(/[^a-zA-Z0-9_]/g, '');

This function uses an allowlist approach: it removes all characters except alphanumeric characters and underscores. Valid SQL identifiers (table names, column names) only need these characters, so this safely strips any special characters an attacker might use for injection.

Applied to:
- Line 6: const columns = lines[0].split(',').map(sanitizeId);
- Line 7: sanitizeId(Table)

2. SQL-Safe Value Escaping

const escapeVal = s => "'" + s.trim().replace(/'/g, "''") + "'";

This function:
- Wraps the value in single quotes ('...')
- Escapes any existing single quotes by doubling them (' becomes '')
- This is the SQL standard for escaping literal values

Applied to:
- Line 11: const values = lines[i].split(',').map(escapeVal);

The Fixed Code

const csv2sql=(csv, Table)=>{
    const sanitizeId = s => s.trim().replace(/[^a-zA-Z0-9_]/g, '');
    const escapeVal = s => "'" + s.trim().replace(/'/g, "''") + "'";
    const lines = csv.trim().trimEnd().split('\n').filter(n=>n);
    const columns = lines[0].split(',').map(sanitizeId);
    let sqlQuery = "INSERT INTO " + sanitizeId(Table) + " (" + columns.join(', ') + ") Values ";
    let sqlValues = []
    for (let i = 1; i < lines.length; i++) {
      const values = lines[i].split(',').map(escapeVal);
      sqlValues.push("(" + values.join(', ') + ")")
    }
    return sqlQuery+sqlValues.join(",\n");
}

How This Blocks the Attack

With the malicious CSV from our earlier example:

id, name); DROP TABLE users;--
1, test

The fixed function now:

  1. Sanitizes the column name: name); DROP TABLE users;-- becomes nameDROPTABLEusers (all special characters removed)
  2. Sanitizes the table name: Already safe in this example
  3. Escapes the value: test becomes 'test'

The resulting SQL is:

INSERT INTO users (id, nameDROPTABLEusers) Values ('1', 'test')

The injection is completely neutralized. The malicious SQL syntax is treated as literal data.

Why String Concatenation Instead of Template Literals?

Notice the fix also changes from template literals to string concatenation on line 7:

// Before:
let sqlQuery = `INSERT INTO ${Table} (${columns.join(', ')}) Values `

// After:
let sqlQuery = "INSERT INTO " + sanitizeId(Table) + " (" + columns.join(', ') + ") Values ";

This is a stylistic choice that makes it visually clearer where values are being inserted and emphasizes that sanitization functions are being called. The security benefit comes from the sanitizeId() calls, not the string concatenation itself.

Key Takeaways

  • Never trust CSV headers: The column names in CSV files are user-controlled data. Treat them with the same suspicion as any other user input.

  • Identifier sanitization requires allowlisting: The regex /[^a-zA-Z0-9_]/g in this fix works because SQL identifiers only need alphanumeric characters and underscores. Blacklisting dangerous characters is insufficient.

  • String interpolation in SQL is inherently risky: Even with sanitization, parameterized queries are preferable when available. This fix applies sanitization as a defense-in-depth measure.

  • The csv2sql() function now validates both structure and content: Column names are sanitized before inclusion in the query, and values are escaped using SQL-standard rules.

  • Test your fixes with adversarial input: The best way to verify a SQL injection fix is to attempt injection attacks. Try payloads with ', ;, --, /**/, and common SQL keywords.

How Orbis AppSec Detected This

Source: User-supplied CSV data passed to the csv2sql(csv, Table) function in src/data/converter/csv.js, specifically the Table parameter and the CSV content (headers and values)

Sink: The string interpolation operations at lines 4, 3, and 9 where ${Table}, columns, and values are directly embedded into SQL query strings without sanitization

Missing control: No validation, sanitization, or parameterization of the Table parameter; no escaping of column names or values; no allowlist validation of identifiers

CWE: CWE-89 (Improper Neutralization of Special Elements used in an SQL Command)

Fix: Implemented sanitizeId() function using allowlist regex /[^a-zA-Z0-9_]/g to strip special characters from table and column names, and escapeVal() function to escape string values using SQL-standard single-quote doubling

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

SQL injection remains one of the most dangerous and preventable vulnerabilities in modern applications. This fix to the csv2sql() function demonstrates that even utility functions handling "simple" data transformations can introduce critical security risks if they process untrusted input without proper validation.

The combination of identifier sanitization and value escaping applied here provides strong defense-in-depth protection. However, developers should remember that parameterized queries are the ideal solution when available, and this fix represents a necessary security hardening when parameterization isn't an option.

Always treat CSV headers and content as untrusted input, validate identifiers against allowlists whenever possible, and use static analysis tools to catch these patterns before they reach production.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #6

Related Articles

critical

SQL's Insert() and Update() Methods Used F-String Interpolation in u2share_batch_give_sugar

The SQL helper class in u2share_batch_give_sugar used Python f-strings to construct INSERT and UPDATE queries, creating SQL injection vulnerabilities even though values appeared to come from internal constants. The fix replaces all f-string query construction with sqlite3 parameterized queries using `?` placeholders, eliminating string interpolation entirely from the database path.

high

TrackOptionsManager DDL: Template-Literal SQL Injection Closed

The `TrackOptionsManager` service built its `CREATE TABLE` and `ALTER TABLE ... ADD COLUMN alias` statements by interpolating a `DEFAULT_ALIAS` constant directly inside single quotes in a JavaScript template literal, and its private `_query()` helper had no parameter channel at all. The fix routes the default value through `mysql.escape()` and gives `_query(q, params = [])` a real bound-parameter argument that is forwarded to `db.query()`. This removes an injection primitive on a schema-bootstra

high

How SQL injection via template literals happens in Node.js SQLite and how to fix it

A SQL injection vulnerability in `src/lib/codex-state.mjs` allowed dynamic column names to reach SQL queries through JavaScript template literals. The fix implements defense-in-depth with strict identifier validation using `SAFE_IDENTIFIER` regex before query construction.

high

How Python SQLAlchemy Raw Query SQL Injection happens and how to fix it

A high-severity SQL injection vulnerability was fixed in the `skills/last30days/scripts/store.py` file where untrusted input was being concatenated directly into raw SQL queries. The fix replaces string concatenation with SQLAlchemy's TextualSQL prepared statements using named parameters, preventing attackers from manipulating database queries through malicious input.

critical

How SQL injection happens in Python DuckDB view creation and how to fix it

A critical SQL injection flaw in `python/src/idx/api.py:265` built five DuckDB `CREATE VIEW` statements with Python f-strings, interpolating a filesystem path directly into SQL text. The fix replaces the interpolated path with a bound parameter (`read_parquet(?)`) and moves the view names into a hardcoded, non-interpolated statement map — eliminating any path where filenames or directory values can alter SQL structure.

critical

How SQL Injection happens in PHP bulk email systems and how to fix it

A critical SQL injection vulnerability in `admin/utilities/bulkEmailSystem.php` allowed attackers to inject arbitrary SQL through unvalidated database names passed from user input. The fix implements strict input validation using regex pattern matching to ensure only safe database identifiers are processed, preventing exploitation of the bulk email functionality.