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';

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.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #526

Related Articles

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.