Back to Blog
high SEVERITY8 min read

How SQL Injection via Template Literals happens in TypeScript and how to fix it

A high-severity SQL injection vulnerability was discovered in the admin panel's database tools where schema names were directly interpolated into SQL queries using JavaScript template literals. The fix replaced unsafe string concatenation with a proper `quoteSchemaLiteral()` function to sanitize inputs before query construction, eliminating the injection vector in two critical database inspection functions.

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

Answer Summary

SQL injection via template literals (CWE-89) occurs in TypeScript when user-controlled data is directly embedded into SQL queries using template string interpolation. In `database-tools.ts`, schema names were unsafely inserted into PostgreSQL queries like `ARRAY[${schemas.map((s: string) => \`'${s}'\`).join(",")}]`. The fix replaces direct string interpolation with `quoteSchemaLiteral(s)`, which properly escapes special characters and prevents attackers from breaking out of the string context to inject malicious SQL commands.

Vulnerability at a Glance

cweCWE-89 (Improper Neutralization of Special Elements used in an SQL Command)
fixReplaced manual string quoting with `quoteSchemaLiteral()` function for proper escaping
riskAttackers could execute arbitrary SQL commands to read, modify, or delete database data
languageTypeScript/JavaScript with PostgreSQL
root causeSchema names concatenated directly into SQL using template literals without sanitization
vulnerabilitySQL Injection via Template Literal String Interpolation

Introduction

In a database administration tool within packages/admin/src/shared/tools/database-tools.ts, we discovered a high-severity SQL injection vulnerability at lines 88 and 176. The vulnerable code constructed PostgreSQL queries using JavaScript template literals with schema names directly interpolated into the SQL string: ARRAY[${schemas.map((s: string) => \'${s}'`).join(",")}]`. This pattern created an injection point where malicious schema names could break out of the quoted context and execute arbitrary SQL commands. While this code served an internal admin endpoint with restricted access, it represented an exploit primitive that could be chained with other vulnerabilities by sophisticated attackers or automated exploit tools.

The Vulnerability Explained

The vulnerability existed in two database inspection functions within the admin tools: list_tables and generate_types. Let's examine the vulnerable code from line 88:

case "list_tables": {
    const sql = `SELECT schemaname as schema, tablename as table, tableowner as owner FROM pg_tables WHERE schemaname = ANY(ARRAY[${schemas.map((s: string) => `'${s}'`).join(",")}]) ORDER BY schemaname, tablename;`;
    const r = await execSql(sql);
    text = r.ok ? formatTableList(r.data, schemas) : `❌ Failed (${r.status})`;
    break;
}

The problem lies in how schema names are incorporated into the SQL query. The code manually wraps each schema name in single quotes using `'${s}'`, then joins them with commas to create an array literal for PostgreSQL's ANY(ARRAY[...]) construct.

The Attack Vector:

An attacker who could control schema names (even partially) could inject SQL by including a single quote to break out of the string context. For example, if a schema name contained:

public', 'public']) OR 1=1 --

The resulting SQL would become:

SELECT schemaname as schema, tablename as table, tableowner as owner 
FROM pg_tables 
WHERE schemaname = ANY(ARRAY['public', 'public']) OR 1=1 --']) 
ORDER BY schemaname, tablename;

The injected OR 1=1 would bypass the schema filtering entirely, exposing all tables across all schemas. More sophisticated payloads could:

  • Extract sensitive data from other schemas: public']) UNION SELECT table_schema, table_name, 'stolen' FROM information_schema.tables --
  • Modify database contents if the connection has write privileges
  • Execute PostgreSQL functions to read files or establish network connections
  • Escalate privileges depending on the database user's permissions

The same vulnerability existed in the generate_types function at line 176, which queries information_schema.tables and information_schema.columns to generate TypeScript type definitions.

Real-World Impact:

While the PR description notes this is an "internal/admin endpoint with restricted access," the vulnerability still poses significant risks:

  1. Defense in Depth Violation: Security should never rely on a single layer. If authentication is bypassed through a separate vulnerability, this SQL injection becomes directly exploitable.
  2. Insider Threats: Malicious insiders with admin access could exploit this to exceed their intended privileges.
  3. Exploit Chaining: Modern automated exploit development tools can chain multiple "unexploitable" primitives together to achieve code execution.

The Fix

The fix replaces manual string quoting with a dedicated quoteSchemaLiteral() function that properly escapes special characters. Here's the before and after comparison for the list_tables function:

Before (Vulnerable):

const sql = `SELECT schemaname as schema, tablename as table, tableowner as owner FROM pg_tables WHERE schemaname = ANY(ARRAY[${schemas.map((s: string) => `'${s}'`).join(",")}]) ORDER BY schemaname, tablename;`;

After (Secure):

const sql = `SELECT schemaname as schema, tablename as table, tableowner as owner FROM pg_tables WHERE schemaname = ANY(ARRAY[${schemas.map((s: string) => quoteSchemaLiteral(s)).join(",")}]) ORDER BY schemaname, tablename;`;

The identical change was applied to the generate_types function at line 176.

How This Fix Works:

The quoteSchemaLiteral() function (which must be implemented elsewhere in the codebase or imported from a PostgreSQL library) performs proper escaping by:

  1. Doubling single quotes: Any ' character in the input becomes '', preventing breakout from the string context
  2. Wrapping in quotes: The result is wrapped in single quotes to form a valid PostgreSQL string literal
  3. Handling edge cases: Proper implementations also handle backslashes, null bytes, and other special characters according to PostgreSQL's string literal rules

For example, if an attacker tries to inject public', 'public']) OR 1=1 --, the function would transform it to:

'public'', ''public'']) OR 1=1 --'

This is interpreted by PostgreSQL as a single string literal containing the literal text public', 'public']) OR 1=1 --, not as SQL code. The injection attempt becomes harmless data.

Why This Approach Works:

Unlike manual escaping attempts that developers might miss edge cases for, using a dedicated escaping function:

  • Leverages PostgreSQL-specific knowledge of all special characters and contexts
  • Is maintained and tested by security-conscious library developers
  • Provides a clear audit trail showing security-conscious coding
  • Prevents common mistakes like forgetting to escape backslashes or handling Unicode edge cases

Prevention & Best Practices

To prevent SQL injection vulnerabilities in TypeScript and JavaScript database code:

1. Use Parameterized Queries as Default

The gold standard for SQL injection prevention is parameterized queries (also called prepared statements):

// Best practice - parameterized query
const result = await db.query(
    'SELECT * FROM pg_tables WHERE schemaname = ANY($1)',
    [schemas]
);

With parameterized queries, the database engine keeps SQL code and data completely separate. User input never becomes part of the SQL syntax tree.

2. When Dynamic SQL is Necessary, Use Proper Escaping Functions

Some queries require dynamic SQL construction (like this case with variable array lengths). When parameterization isn't possible:

  • Never manually construct SQL with template literals or string concatenation
  • Always use database-specific escaping functions from trusted libraries
  • For PostgreSQL: Use pg-format library's literal() or similar functions
  • For MySQL: Use mysql.escape() or connection.escapeId()
  • For SQL Server: Use proper parameterization or the library's escaping functions

3. Apply Defense in Depth

  • Input Validation: Validate schema names against a whitelist of expected patterns (alphanumeric, underscores)
  • Least Privilege: Ensure database connections use accounts with minimal necessary permissions
  • Query Allowlisting: Where possible, restrict admin tools to predefined queries rather than dynamic construction
  • Regular Security Audits: Use static analysis tools to catch these patterns during development

4. Static Analysis Integration

Configure tools like Semgrep to detect template literal SQL construction:

rules:
  - id: sql-injection-template-literal
    pattern: |
      $SQL = `...${...}...`
    message: SQL query uses template literal interpolation
    severity: WARNING
    languages: [typescript, javascript]

5. Security Training

Educate developers that template literals are not safe for SQL construction, even though they feel modern and convenient. The backtick syntax provides no security benefits over string concatenation.

Standards and References

  • CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
  • OWASP: SQL Injection is #3 in the OWASP Top 10 (2021) under "Injection"
  • OWASP SQL Injection Prevention Cheat Sheet: Comprehensive guidance on parameterized queries and escaping

Key Takeaways

  • Template literals provide zero SQL injection protection: The modern JavaScript `${variable}` syntax is just as vulnerable as old-style "" + variable concatenation when constructing SQL queries.
  • The list_tables and generate_types functions in database-tools.ts were vulnerable because they manually quoted schema names with `'${s}'` instead of using proper escaping.
  • Replacing manual quoting with quoteSchemaLiteral() eliminates the injection vector by properly escaping all PostgreSQL special characters, including single quotes that could break out of the string context.
  • Admin endpoints need security too: Even "internal" tools should follow secure coding practices, as they represent exploit primitives that sophisticated attackers can chain together.
  • Automated detection works: Static analysis tools like Semgrep successfully identified this vulnerability pattern, enabling proactive fixes before exploitation.

How Orbis AppSec Detected This

  • Source: Schema names passed to database inspection functions (list_tables, generate_types) in the admin tool interface
  • Sink: Direct template literal interpolation into SQL queries at database-tools.ts:88 and database-tools.ts:176 where ${schemas.map((s: string) => \'${s}'`).join(",")}` constructs PostgreSQL array literals
  • Missing control: No escaping or sanitization of schema names before SQL interpolation; manual quoting with `'${s}'` doesn't prevent single-quote breakout attacks
  • CWE: CWE-89 (Improper Neutralization of Special Elements used in an SQL Command)
  • Fix: Replaced manual string quoting with quoteSchemaLiteral(s) function that properly escapes PostgreSQL special characters

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

This SQL injection vulnerability in database-tools.ts demonstrates how modern JavaScript syntax can create a false sense of security. Template literals look clean and feel safe, but they provide no protection against injection attacks when constructing SQL queries. The fix—replacing manual quoting with the quoteSchemaLiteral() function—follows the fundamental security principle of using battle-tested escaping functions rather than rolling your own.

For developers working on similar admin tools or database utilities, remember that every piece of user-controlled data that enters a SQL query must be either parameterized or properly escaped using database-specific functions. No exceptions, no shortcuts. Even internal tools deserve secure coding practices, because today's isolated admin endpoint could become tomorrow's attack vector when chained with other vulnerabilities.

By proactively removing exploit primitives like this one, we raise the bar against increasingly sophisticated automated attack tools and make our applications more resilient against both current and future threats.

References

Frequently Asked Questions

What is SQL injection via template literals?

SQL injection via template literals occurs when JavaScript/TypeScript template strings (backticks) are used to construct SQL queries with user input directly embedded using ${} interpolation, allowing attackers to inject malicious SQL code by breaking out of the string context.

How do you prevent SQL injection in TypeScript database code?

Use parameterized queries or prepared statements as your primary defense. When dynamic SQL construction is unavoidable, use database-specific escaping functions like PostgreSQL's `quote_literal()` or library functions like `quoteSchemaLiteral()` to sanitize all user inputs before interpolation.

What CWE is SQL injection via template literals?

CWE-89 (Improper Neutralization of Special Elements used in an SQL Command, 'SQL Injection'). Template literal injection is a specific manifestation of classic SQL injection adapted to modern JavaScript/TypeScript codebases.

Is input validation enough to prevent SQL injection?

No. While input validation helps reduce attack surface, it's insufficient as a sole defense because attackers constantly find new bypass techniques. Always use parameterized queries or proper escaping functions as your primary protection, with validation as a secondary defense layer.

Can static analysis detect SQL injection via template literals?

Yes. Modern static analysis tools like Semgrep can detect this pattern by identifying SQL queries constructed with template literals that contain dynamic expressions, flagging them for manual review or automated remediation.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1175

Related Articles

high

How utils.custom.sql-injection-template-literal happens in JavaScript and how to fix it

A high-severity SQL injection vulnerability was discovered in `CrewRouter-Desktop/src/server-manager.js` at line 266, where a SQL query was constructed using JavaScript template literals with dynamic input. This pattern allows remote attackers to inject arbitrary SQL commands through the web service's request handlers. The fix replaces the unsafe template literal interpolation with parameterized queries, eliminating the injection vector entirely.

high

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.

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

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