Back to Blog
high SEVERITY6 min read

How SQL Injection via Template Literals happens in Node.js and how to fix it

A high-severity SQL injection vulnerability was discovered in server-agents/common/src/search/schema.ts where the `insertRowsBatch` function constructed SQL queries using JavaScript template literals with dynamic input. The fix replaced the vulnerable `db.exec()` call with parameterized queries using `db.query().run()`, eliminating the injection risk in the full-text search merge operation.

O
By Orbis AppSec
Published September 6, 2026Reviewed September 6, 2026

Answer Summary

SQL injection via template literals (CWE-89) occurs when JavaScript template strings concatenate unsanitized variables directly into SQL queries, allowing attackers to inject malicious SQL commands. In the server-agents Node.js library, the `insertRowsBatch` function in schema.ts used `db.exec(\`INSERT INTO search_chunks_fts(search_chunks_fts, rank) VALUES ('merge', ${SEARCH_FTS_MERGE_PAGES_PER_TXN})\`)` which embedded a variable directly into the query string. The fix replaces this with parameterized queries: `db.query('INSERT INTO search_chunks_fts(search_chunks_fts, rank) VALUES (?, ?)').run('merge', SEARCH_FTS_MERGE_PAGES_PER_TXN)`, ensuring the database driver properly escapes all input values.

Vulnerability at a Glance

cweCWE-89 (Improper Neutralization of Special Elements used in an SQL Command)
fixReplace db.exec() with parameterized db.query().run() to separate SQL structure from data
riskAttackers could manipulate SQL queries to access, modify, or delete database records
languageTypeScript/Node.js
root causeDynamic value embedded directly in template literal SQL query using `${variable}` syntax
vulnerabilitySQL Injection via Template Literals

Introduction

In the server-agents Node.js library, we discovered a high-severity SQL injection vulnerability in server-agents/common/src/search/schema.ts at line 471. The insertRowsBatch function, which handles full-text search indexing for chat messages, was constructing SQL queries using JavaScript template literals with the variable SEARCH_FTS_MERGE_PAGES_PER_TXN directly embedded in the query string. This seemingly innocuous pattern—common in many Node.js codebases—created a critical security flaw that could allow attackers to manipulate database operations.

The vulnerability was particularly concerning because this is a library package consumed by downstream applications. Any security flaw here propagates to every application using this dependency, multiplying the potential attack surface across an entire ecosystem of services.

The Vulnerability Explained

The vulnerable code appeared in the insertRowsBatch function, specifically in this line:

db.exec(`INSERT INTO search_chunks_fts(search_chunks_fts, rank)
  VALUES ('merge', ${SEARCH_FTS_MERGE_PAGES_PER_TXN})`);

This code uses JavaScript template literals (backtick strings) to construct an SQL query, directly interpolating the SEARCH_FTS_MERGE_PAGES_PER_TXN constant into the query string. The db.exec() method then executes this constructed string as raw SQL.

Why is this dangerous?

While SEARCH_FTS_MERGE_PAGES_PER_TXN appears to be a constant in this specific case, the pattern itself is inherently unsafe. Template literal interpolation happens at the JavaScript level, before the database driver sees the query. This means:

  1. The database has no way to distinguish between SQL structure and data
  2. If the variable's value ever changes or comes from an external source, malicious SQL can be injected
  3. The pattern creates an "exploit primitive"—a code structure that automated attack tools can potentially chain with other vulnerabilities

Concrete Attack Scenario:

Imagine if SEARCH_FTS_MERGE_PAGES_PER_TXN were ever modified to accept user input or configuration values (a common refactoring in growing codebases). An attacker could inject a value like:

1); DROP TABLE search_chunks_fts; --

The resulting executed query would become:

INSERT INTO search_chunks_fts(search_chunks_fts, rank)
  VALUES ('merge', 1); DROP TABLE search_chunks_fts; --')

This would execute the INSERT, then drop the entire full-text search table, destroying indexed chat data across the application. More sophisticated attacks could exfiltrate sensitive data, modify search rankings, or inject malicious content into search results.

Even though SEARCH_FTS_MERGE_PAGES_PER_TXN is currently a constant, this vulnerability represents what security researchers call an "exploit primitive"—a code pattern that, while not independently exploitable today, could be chained with other weaknesses by increasingly sophisticated automated exploit-development tools.

The Fix

The fix replaces the vulnerable template literal approach with proper parameterized queries:

Before (Vulnerable):

db.exec(`INSERT INTO search_chunks_fts(search_chunks_fts, rank)
  VALUES ('merge', ${SEARCH_FTS_MERGE_PAGES_PER_TXN})`);

After (Secure):

db.query(`INSERT INTO search_chunks_fts(search_chunks_fts, rank)
  VALUES ('merge', ?)`).run(SEARCH_FTS_MERGE_PAGES_PER_TXN);

What changed and why it matters:

  1. Switched from db.exec() to db.query().run(): The exec() method executes raw SQL strings, while query() creates a prepared statement that supports parameter binding.

  2. Replaced ${SEARCH_FTS_MERGE_PAGES_PER_TXN} with ? placeholder: The question mark is a parameter placeholder. The actual value is passed separately as an argument to .run().

  3. Separated SQL structure from data: The SQL query structure is now fixed at compile time. The value of SEARCH_FTS_MERGE_PAGES_PER_TXN is passed as a parameter, which the database driver automatically escapes and treats as pure data, never as executable SQL code.

How this prevents SQL injection:

When you use parameterized queries, the database driver handles the value in two distinct phases:

  1. Parse phase: The SQL structure (INSERT INTO search_chunks_fts...) is parsed and compiled
  2. Bind phase: The parameter value is bound to the placeholder and properly escaped

This separation ensures that no matter what value is passed—even if it contains SQL syntax—it will always be treated as a literal string value for the rank column, never as executable SQL commands.

Prevention & Best Practices

1. Always Use Parameterized Queries

Make parameterized queries your default approach for all database operations:

// ❌ NEVER do this
db.exec(`SELECT * FROM users WHERE id = ${userId}`);

// ✅ ALWAYS do this
db.query(`SELECT * FROM users WHERE id = ?`).get(userId);

2. Avoid Template Literals for SQL Construction

JavaScript template literals are excellent for string formatting, but they're dangerous for SQL. Treat any SQL query with ${} interpolation as a code smell that requires immediate review.

3. Use TypeScript and Type-Safe Query Builders

Consider using type-safe query builders like Prisma, TypeORM, or Kysely that provide compile-time guarantees against SQL injection:

// Type-safe query with Kysely
const result = await db
  .insertInto('search_chunks_fts')
  .values({ search_chunks_fts: 'merge', rank: SEARCH_FTS_MERGE_PAGES_PER_TXN })
  .execute();

4. Implement Static Analysis in CI/CD

Integrate tools like Semgrep into your continuous integration pipeline to catch SQL injection patterns before code reaches production:

# .github/workflows/security.yml
- name: Run Semgrep
  uses: returntocorp/semgrep-action@v1
  with:
    config: p/security-audit

5. Apply Defense in Depth

While parameterized queries are the primary defense, implement multiple layers:

  • Principle of least privilege: Database users should have minimal necessary permissions
  • Input validation: Validate data types and ranges before database operations
  • Web Application Firewall (WAF): Deploy WAF rules to detect SQL injection attempts
  • Regular security audits: Review database access patterns periodically

Security Standards Reference

This vulnerability maps to several security frameworks:

  • CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
  • OWASP Top 10 2021: A03:2021 – Injection
  • OWASP ASVS: V5.3.4 – Database Security Requirements

Key Takeaways

  • The insertRowsBatch function in schema.ts line 471 used template literal interpolation (${SEARCH_FTS_MERGE_PAGES_PER_TXN}) which created an SQL injection vulnerability in the full-text search merge operation
  • Template literals with db.exec() bypass all database-level protections because the SQL string is constructed in JavaScript before the database driver sees it
  • The fix switched to db.query().run() with ? placeholders, ensuring the SQLite driver treats SEARCH_FTS_MERGE_PAGES_PER_TXN as data, not executable SQL
  • Exploit primitives matter in library code: Even if a constant seems safe today, patterns that could enable SQL injection become security liabilities as code evolves and as automated exploit tools become more sophisticated
  • Static analysis tools like Semgrep can detect these patterns before they reach production, making them essential for Node.js projects handling database operations

How Orbis AppSec Detected This

  • Source: The SEARCH_FTS_MERGE_PAGES_PER_TXN variable embedded in a template literal
  • Sink: db.exec() call in server-agents/common/src/search/schema.ts:471 executing the constructed SQL string
  • Missing control: No parameterized query mechanism; direct template literal interpolation allowed variable content to be interpreted as SQL syntax
  • CWE: CWE-89 (Improper Neutralization of Special Elements used in an SQL Command)
  • Fix: Replaced db.exec() with parameterized db.query().run(), using ? placeholder to safely bind the SEARCH_FTS_MERGE_PAGES_PER_TXN value

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 critical web application vulnerabilities, and template literal interpolation is a common vector in Node.js applications. The fix in server-agents/common/src/search/schema.ts demonstrates that secure coding doesn't require complex changes—switching from db.exec() with template literals to db.query().run() with parameterized queries is a simple pattern that eliminates an entire class of vulnerabilities.

For library maintainers, this fix is especially important. Security vulnerabilities in shared libraries multiply across every downstream consumer, making defensive hardening essential even when exploit paths aren't immediately obvious. By removing exploit primitives proactively, you raise the bar against increasingly capable automated attack tools and protect your entire ecosystem of users.

Make parameterized queries your default, integrate static analysis into your workflow, and treat SQL construction with template literals as a critical security smell that demands immediate remediation.

References

Frequently Asked Questions

What is SQL injection via template literals?

SQL injection via template literals occurs when JavaScript template strings (backtick strings with `${variable}` interpolation) are used to construct SQL queries with dynamic values, allowing attackers to inject malicious SQL code through those variables instead of treating them as safe data values.

How do you prevent SQL injection in Node.js?

Use parameterized queries (also called prepared statements) where you separate the SQL structure from the data by using placeholders (like `?`) and passing values as separate arguments. Modern database libraries like better-sqlite3, pg, and mysql2 all support parameterized queries that automatically escape values.

What CWE is SQL injection via template literals?

CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection'). This is one of the most critical and common web application vulnerabilities.

Is input validation enough to prevent SQL injection?

No. While input validation is important for defense in depth, parameterized queries are the primary defense against SQL injection. Validation can be bypassed, and it's difficult to anticipate all possible malicious inputs. Parameterized queries structurally prevent SQL injection by ensuring user input is always treated as data, never as SQL code.

Can static analysis detect SQL injection via template literals?

Yes. Static analysis tools like Semgrep can detect this pattern by identifying SQL queries constructed with template literals that include dynamic variables. The rule `utils.custom.sql-injection-template-literal` specifically flags this dangerous pattern in JavaScript and TypeScript code.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #674

Related Articles

high

How SQL Injection happens in Node.js migration scripts and how to fix it

A high-severity SQL injection vulnerability was discovered in `scripts/setup-d1.mjs`, where migration filenames were directly concatenated into SQL INSERT statements using an inadequate `escapeSqlString` function. An attacker with filesystem write access could craft a malicious filename to execute arbitrary SQL commands against the Cloudflare D1 database. The fix replaces string concatenation with parameterized queries, eliminating the injection surface entirely.

critical

How SQL Injection happens in Python database scripts and how to fix it

A critical SQL injection vulnerability was discovered in `MangosSuperUI/Scripts/discover_relationships.py`, where database, table, and column names were interpolated directly into SQL queries using Python f-strings. An attacker controlling these input parameters could execute arbitrary SQL against the database. The fix applies backtick escaping for identifier names and parameterized queries for the `LIMIT` clause.

critical

How SQL Injection happens in Node.js SQLite CLI calls and how to fix it

A critical SQL injection vulnerability was discovered in `lib/ParamediciOSPermissions.js`, where the `service` and `app` variables were interpolated directly into raw SQL strings passed to the `sqlite3` command-line tool without any escaping or parameterization. An attacker with control over these inputs could manipulate the iOS simulator's TCC permission database, potentially granting unauthorized app permissions. The fix applies SQLite-standard single-quote escaping to both variables before th

critical

How SQL Injection happens in Python SQLite utilities and how to fix it

A SQL injection risk was discovered in `scripts/db_utils.py` where the `_get_or_create` function used f-string interpolation to dynamically construct table and column names in SQL queries. While current callers passed hardcoded values, the function accepted arbitrary strings, making it a latent injection vector for any future code that passed user-controlled input. The fix replaces dynamic SQL construction with a strict allowlist of pre-written, parameterized query strings.

critical

How Unsafe Fall-Through in getWhereConditions Happens in Sequelize and How to Fix It

A critical vulnerability in Sequelize (CVE-2023-22579) allowed attackers to inject raw SQL through an unsafe fall-through in the `getWhereConditions` function when parentheses were used in query attributes. Upgrading from version 6.26.0 to 6.29.0 closes this attack vector by tightening how raw attributes are handled. Any Node.js application using Sequelize for database queries should treat this upgrade as an urgent security priority.

high

How Unauthenticated Endpoint Exposure Happens in Node.js and How to Fix It

A high-severity unauthenticated endpoint exposure was discovered in `dep/src/server/index.js`, where the `/--ziko--` route served internal application state (`globalThis.Ziko`) to any network-connected client without any authentication or environment guard. The fix adds a single production environment check that returns a `404` before the sensitive data is ever sent. This kind of "debug route left in production" vulnerability is surprisingly common in Node.js applications and can silently leak c