Back to Blog
critical SEVERITY6 min read

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.

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

Answer Summary

SQL injection (CWE-89) in Node.js occurs when user-controlled input is directly interpolated into SQL queries using template literals or string concatenation. In this case, the `metadata_id` and `start` variables in `statistics.js` were inserted directly into DELETE and UPDATE statements without validation. The fix applies `parseInt()` to enforce integer-only metadata IDs and regex validation for date strings, preventing malicious SQL payloads from being injected into the query structure.

Vulnerability at a Glance

cweCWE-89
fixAdded parseInt() validation for metadata_id and regex validation for date strings
riskComplete database compromise including data deletion and exfiltration
languageJavaScript (Node.js)
root causeDirect variable interpolation into SQL strings without input validation
vulnerabilitySQL Injection via Template Literal Interpolation

Introduction

The src/core/statistics.js file in this Node.js library handles database operations for statistics tracking, but a critical flaw in the getScript() method at line 56 created a severe security risk. The metadata_id variable was directly interpolated into DELETE and UPDATE SQL statements using JavaScript template literals—a pattern that opened the door to SQL injection attacks.

This vulnerability is particularly dangerous because this is a Node.js library, meaning every downstream consumer who uses this package inherits the security flaw. A single compromised dependency can cascade into hundreds or thousands of vulnerable applications.

The Vulnerability Explained

What Went Wrong

The vulnerable code in getScript() constructed SQL queries by directly embedding variables into template literal strings:

const metadata_id = this.lastStatistic.metadata_id;
const start = this.lastStatistic.start;

if (options.existingDataMode == 'update') {
    const updateSql1 = `update statistics set sum = sum + ${this.lastStatistic.sum.toFixed(3)} where metadata_id = ${metadata_id} and start_ts > unixepoch("${start}")\n\n`;
    const updateSql2 = `update statistics_short_term set sum = sum + ${this.lastStatistic.sum.toFixed(3)} where metadata_id = ${metadata_id} and start_ts > unixepoch("${start}")\n\n`;
    sql = sql + updateSql1 + updateSql2;
}

if (options.existingDataMode == 'delete') {
    const deleteSql1 = `delete from statistics where metadata_id = ${metadata_id}\n\n`;
    const deleteSql2 = `delete from statistics_short_term where metadata_id = ${metadata_id}\n\n`;
    // ...
}

The metadata_id and start variables are pulled directly from this.lastStatistic without any validation. If an attacker can control these values—whether through API input, configuration manipulation, or upstream data poisoning—they can inject arbitrary SQL.

Attack Scenario

Consider an attacker who manages to set metadata_id to the following value:

1; DROP TABLE statistics;--

When this value is interpolated into the DELETE statement, the resulting SQL becomes:

delete from statistics where metadata_id = 1; DROP TABLE statistics;--

This executes two commands: the intended DELETE (limited to ID 1), followed by a complete table drop. The -- comments out any remaining query text.

For data exfiltration, an attacker could use UNION-based injection:

1 UNION SELECT username, password, NULL FROM users--

This could expose sensitive data from other tables in the database, depending on how query results are handled.

Real-World Impact

For this statistics library, successful exploitation could:

  1. Delete all statistics data across both statistics and statistics_short_term tables
  2. Corrupt aggregated metrics by injecting malicious UPDATE values
  3. Exfiltrate sensitive data if the database contains other tables with confidential information
  4. Pivot to further attacks if database credentials have elevated privileges

The Fix

The fix implements strict input validation before any values reach the SQL query construction:

Before (Vulnerable)

const metadata_id = this.lastStatistic.metadata_id;
const start = this.lastStatistic.start;

After (Secure)

const metadata_id = parseInt(this.lastStatistic.metadata_id, 10);
if (isNaN(metadata_id)) {
    throw 'invalid metadata_id';
}
const start = String(this.lastStatistic.start);
if (!/^[\d\-: T.Z+]+$/.test(start)) {
    throw 'invalid start date';
}

How This Solves the Problem

For metadata_id:
- parseInt(value, 10) converts the input to a base-10 integer
- If the input contains SQL injection payloads like 1; DROP TABLE, parseInt() returns just 1
- The isNaN() check catches completely invalid inputs and throws an error
- Only clean integer values can proceed to the query

For start:
- The regex /^[\d\-: T.Z+]+$/ creates a whitelist of allowed characters
- Only digits, hyphens, colons, spaces, T, periods, Z, and plus signs are permitted
- This matches ISO 8601 date formats like 2024-01-15T10:30:00.000Z
- SQL metacharacters like semicolons, quotes, and dashes (when not part of dates) are rejected

The query construction was also changed from template literals to string concatenation, though the primary security improvement comes from the input validation:

const updateSql1 = 'update statistics set sum = sum + ' + this.lastStatistic.sum.toFixed(3) + ' where metadata_id = ' + metadata_id + ' and start_ts > unixepoch("' + start + '")\n\n';

Prevention & Best Practices

1. Use Parameterized Queries

The gold standard for SQL injection prevention is parameterized queries (prepared statements). Instead of string interpolation:

// Vulnerable
const sql = `SELECT * FROM users WHERE id = ${userId}`;

// Secure (using a library like better-sqlite3)
const stmt = db.prepare('SELECT * FROM users WHERE id = ?');
const result = stmt.get(userId);

2. Validate Input Types Strictly

When parameterized queries aren't available (such as when generating SQL scripts for later execution), enforce strict type validation:

// For integers
const id = parseInt(input, 10);
if (isNaN(id) || id < 0) throw new Error('Invalid ID');

// For strings with known patterns
if (!/^[a-zA-Z0-9_]+$/.test(tableName)) throw new Error('Invalid table name');

3. Use ORM Libraries

Object-Relational Mapping libraries like Sequelize, Prisma, or Knex.js handle query parameterization automatically:

// Knex.js example
await knex('statistics')
    .where('metadata_id', metadataId)
    .del();

4. Apply Defense in Depth

  • Use database accounts with minimal required privileges
  • Implement application-level query logging and monitoring
  • Deploy Web Application Firewalls (WAF) with SQL injection rules
  • Conduct regular security audits and dependency scanning

Key Takeaways

  • Never interpolate variables directly into SQL strings, even in "internal" code—template literals (${variable}) are just as dangerous as string concatenation
  • The parseInt() function is a simple but effective defense for integer parameters, as it strips all non-numeric content
  • Regex whitelisting for date strings (/^[\d\-: T.Z+]+$/) prevents injection while allowing valid ISO 8601 formats
  • Library vulnerabilities cascade downstream—this fix protects every application that depends on this package
  • Input validation should happen at the point of use, not just at API boundaries

How Orbis AppSec Detected This

Orbis AppSec's multi-agent AI scanner identified this vulnerability through taint analysis:

  • Source: The this.lastStatistic.metadata_id and this.lastStatistic.start properties, which could be influenced by external data
  • Sink: The template literal SQL construction in getScript() at src/core/statistics.js:56, specifically the DELETE and UPDATE statements
  • Missing control: No input validation, type checking, or parameterization between the data source and the SQL query construction
  • CWE: CWE-89 (Improper Neutralization of Special Elements used in an SQL Command)
  • Fix: Added parseInt() validation for metadata_id and regex pattern validation for the start date parameter

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 prevalent vulnerabilities in web applications, consistently ranking in the OWASP Top 10. This case demonstrates how easily it can occur—even experienced developers can overlook the risks of JavaScript template literals when constructing SQL queries.

The fix applied here—strict type validation with parseInt() and regex pattern matching—provides immediate protection. However, the long-term solution for any application handling SQL queries is to adopt parameterized queries or ORM libraries that handle escaping automatically.

Remember: every string that touches a SQL query is a potential attack vector. Validate early, validate strictly, and when in doubt, use parameterized queries.

References

Frequently Asked Questions

What is SQL injection?

SQL injection is a code injection attack where malicious SQL statements are inserted into application queries through untrusted input, allowing attackers to manipulate database operations.

How do you prevent SQL injection in Node.js?

Use parameterized queries or prepared statements, validate and sanitize all user input, apply strict type checking (like parseInt() for integers), and use ORM libraries that handle escaping automatically.

What CWE is SQL injection?

SQL injection is classified as CWE-89: Improper Neutralization of Special Elements used in an SQL Command.

Is input validation enough to prevent SQL injection?

Input validation is a defense-in-depth measure but should be combined with parameterized queries. While strict validation (like parseInt() for integers) can prevent injection in specific cases, parameterized queries provide more robust protection.

Can static analysis detect SQL injection?

Yes, static analysis tools can detect SQL injection patterns by identifying data flows from untrusted sources to SQL query construction, especially when string concatenation or template literals are used with user input.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #526

Related Articles

critical

How Plaintext Credential Storage happens in JSON Configuration Files and how to fix it

A critical security issue was discovered in `assets/settings/global.json` where a real phone number (PII) was stored in plaintext alongside placeholder patterns for API keys and payment credentials. This design encouraged developers to substitute real credentials directly into a version-controlled file, creating a high risk of credential exposure via repository access or filesystem reads. The fix replaces the hardcoded phone number with a placeholder and reinforces safe configuration patterns.

high

How Quadratic CPU Consumption Vulnerabilities Happen in JavaScript YAML Parsers and How to Fix Them

A high-severity denial-of-service vulnerability in js-yaml versions 3.x and 4.x allowed attackers to trigger quadratic CPU consumption through specially crafted YAML documents using the !!omap tag. This fix upgrades js-yaml from 4.1.1 to 4.3.1 and from 3.14.2 to 3.15.1, eliminating the algorithmic complexity attack vector that could freeze Node.js applications processing untrusted YAML input.

high

How javascript.lang.security.detect-child-process.detect-child-process happens in Node.js and how to fix it

A high-severity command injection vulnerability was discovered in `scripts/build.js` where `execSync` was called with string-interpolated arguments (`sourceDir` and `outputPath`) inside a shell command. By replacing `execSync` with `spawnSync` using an argument array (no shell), the fix eliminates the possibility of shell metacharacter injection while preserving identical build behavior.

high

How Command Injection happens in Node.js child_process and how to fix it

A command injection vulnerability in nix.js's Release class allowed potentially malicious input through the `arch` parameter to be executed via shell commands. The fix replaced `execSync()` with `execFileSync()`, eliminating shell interpretation and preventing command injection by passing arguments as an array instead of a concatenated string.

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.

critical

How HTTP Header Injection Happens in Go and How to Fix It

A critical vulnerability in the file upload handler allowed attackers to inject CRLF sequences into HTTP response headers through crafted filenames. The fix sanitizes user-supplied filenames before using them in Content-Disposition headers, preventing header injection attacks that could lead to cache poisoning, session fixation, or XSS.