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.

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_]/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.

References

Frequently Asked Questions

What is SQL injection?

SQL injection occurs when untrusted input is concatenated into SQL queries, allowing attackers to manipulate the query structure and execute unintended commands.

How do you prevent SQL injection in JavaScript?

Use parameterized queries with prepared statements, or if that's unavailable, rigorously validate identifiers against allowlists and escape string values using language-specific escaping rules.

What CWE is SQL injection?

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

Is HTML escaping enough to prevent SQL injection?

No. SQL injection requires SQL-specific escaping rules. HTML escaping does not prevent SQL injection attacks.

Can static analysis detect SQL injection?

Yes. Static analysis tools like Semgrep, Snyk, and SonarQube can detect string concatenation patterns in SQL queries and flag them as potential SQL injection risks.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #6

Related Articles

critical

How SQL Injection happens in PHP PDO queries and how to fix it

A critical SQL injection vulnerability was discovered in the `getOfficialContests()` method of ContestRepository.php, where the `$site_id` parameter was directly interpolated into a SQL query string instead of using prepared statements. This vulnerability allowed attackers to inject arbitrary SQL commands and potentially access or manipulate the entire contest database. The fix replaced `pdo->query()` with `pdo->prepare()` and proper parameter binding.

critical

How SQL injection happens in Node.js string interpolation and how to fix it

A critical SQL injection vulnerability was discovered in the `getScript()` method of `src/core/statistics.js`, where the `metadata_id` variable was directly interpolated into DELETE and UPDATE SQL statements without any validation. An attacker controlling this parameter could inject malicious SQL payloads to delete entire tables or exfiltrate sensitive data. The fix implements strict input validation using `parseInt()` and regex patterns to ensure only safe values reach the database queries.

critical

How SQL Injection happens in Node.js MySQL queries and how to fix it

A critical SQL injection vulnerability was discovered in `divisible_asset.js` where `message_index` and `output_index` values from external payment data were directly interpolated into SQL queries without proper escaping. This fix applies `conn.escape()` to these parameters, preventing attackers from manipulating database queries through crafted payment elements.

critical

How SQL injection happens in PHP MySQLi and how to fix it

A critical SQL injection vulnerability was discovered in `sign_up.php` where user registration inputs—including Username and Email—were directly concatenated into SQL queries. Despite using `mysqli_real_escape_string()`, the code remained exploitable. The fix replaces all string-concatenated queries with MySQLi prepared statements and bound parameters, completely eliminating the injection vector.

critical

How SQL injection via unsafe template literals happens in TypeScript database scripts and how to fix it

A critical SQL injection vulnerability in `scripts/verify-db.ts` allowed attackers to execute arbitrary SQL commands by manipulating table names passed to the `countTable()` function. The script used `client.unsafe()` with string interpolation, directly embedding unsanitized input into SQL queries. The fix replaced the unsafe pattern with parameterized queries using the postgres client's built-in escaping.

critical

How Sensitive Data Exposure in Error Logging happens in TypeScript/Deno and how to fix it

A critical vulnerability in Supabase Edge Functions allowed sensitive authentication errors and API credentials to leak through verbose error logging. The `cancel-subscription/index.ts` function logged full error objects to the console, potentially exposing Paddle API keys and auth tokens in deployment logs. The fix sanitizes all error messages to log only safe error text while preserving debugging capability.