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 unvalidated URL input handling happens in SvelteKit with Tauri and how to fix it

A critical vulnerability in `src/routes/+page.svelte` allowed attackers to supply arbitrary URLs—including `http://` and local file paths—through query parameters and drag-drop events, which were then fetched without validation. The fix restricts input to HTTPS-only URLs and removes the dangerous local file fetch path entirely, eliminating both SSRF and local file disclosure attack vectors.

critical

How Command Injection happens in Python Flask and how to fix it

A critical command injection vulnerability was discovered in a Flask application's `/abc2xml` endpoint where user-supplied ABC music notation data could be weaponized to execute arbitrary shell commands. The `run_command` function used `subprocess.run()` with `shell=True` and string concatenation, allowing attackers to inject shell metacharacters. The fix switches to a list-based command invocation with `shell=False`, eliminating the injection vector entirely.

critical

How credential header disclosure happens in electron-updater and how to fix it

A critical vulnerability in electron-updater (CVE-2026-54673) allowed OAuth tokens and API credentials to leak when HTTP redirects occurred during application updates. The fix upgrades electron-updater from version 6.3.0 to 6.8.9, which properly strips sensitive authorization headers before following redirects to external domains.

critical

How missing authentication checks happen in React route handlers and how to fix it

A critical vulnerability in ManageMembers.jsx and Settings.jsx allowed any user with network access to perform privileged operations like adding, editing, and deleting members without authentication. The fix implements route-level authentication checks using React Router's Navigate component to redirect unauthenticated users to the login page.

high

How denial of service via malformed HTTP header decoding happens in Node.js OpenTelemetry and how to fix it

A high-severity denial of service vulnerability (CVE-2026-59892) was discovered in the @opentelemetry/propagator-jaeger package, where malformed HTTP headers could crash Node.js applications. The fix involved upgrading from version 2.8.0 to 2.9.0, which includes proper input validation for Jaeger trace context headers.

critical

How Missing Rate Limiting Happens in Next.js API Routes and How to Fix It

Three public API endpoints in a Next.js application — `/api/send-review`, `/api/contact`, and `/api/auth` — were deployed without any server-side rate limiting, allowing attackers to flood them with unlimited requests. The `/api/send-review` and `/api/contact` endpoints were especially dangerous because every request triggered an outbound email via Gmail SMTP, making them prime targets for email bombing and quota exhaustion. The fix introduces a lightweight in-memory rate limiter capping each IP