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:
- Line 4:
${Table}— The table name is directly interpolated without any validation - Line 3:
columns.join(', ')— Column names from CSV are spliced directly into the query - 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:
- Sanitizes the column name:
name); DROP TABLE users;--becomesnameDROPTABLEusers(all special characters removed) - Sanitizes the table name: Already safe in this example
- Escapes the value:
testbecomes'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.
Prevention & Best Practices
1. Use Parameterized Queries When Possible
If your database driver supports prepared statements or parameterized queries, use them. They are the gold standard for SQL injection prevention:
// Better approach (if using a library like mysql2):
const mysql = require('mysql2/promise');
const connection = await mysql.createConnection(config);
const [result] = await connection.execute(
'INSERT INTO ?? (??) VALUES (?)',
[tableName, columnNames, values]
);
However, not all database drivers support parameterizing identifiers (table/column names). In those cases, identifier sanitization is necessary.
2. Allowlist Validation for Identifiers
Never allow arbitrary table or column names from untrusted sources. Use an allowlist:
const ALLOWED_TABLES = ['users', 'products', 'orders'];
const ALLOWED_COLUMNS = {
users: ['id', 'name', 'email'],
products: ['id', 'title', 'price']
};
if (!ALLOWED_TABLES.includes(tableName)) {
throw new Error('Invalid table name');
}
3. Escape String Values Properly
For string values, use SQL-standard escaping (single-quote doubling) or your database driver's escaping function:
// SQL standard escaping:
const escaped = value.replace(/'/g, "''");
const sql = `INSERT INTO users (name) VALUES ('${escaped}')`;
// Or use a library:
const mysql = require('mysql2');
const escaped = mysql.escape(value);
4. Use Static Analysis Tools
Integrate security scanning into your CI/CD pipeline:
- Semgrep: Detects string concatenation in SQL queries
- Snyk: Identifies vulnerable patterns in Node.js code
- SonarQube: Comprehensive code quality and security scanning
5. Input Validation at Multiple Layers
- Validate CSV structure before processing
- Enforce maximum lengths on table/column names
- Reject unexpected characters early
- Log suspicious inputs for security monitoring
6. OWASP References
Refer to the OWASP SQL Injection Prevention Cheat Sheet for comprehensive guidance.
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_]/gin 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.