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
•Technically reviewed by Anupam Mediratta•Published September 6, 2026•Reviewed 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

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.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1175

Related Articles

critical

ADMIN_HASH Interpolated Into mysql -e: Hex-Encoding the Seed Insert

A container setup script seeds the first administrator account by splicing the shell variable `$ADMIN_HASH` into a quoted SQL literal passed to `mysql -e`. The fix hex-encodes the value with `xxd -p` and decodes it inside MySQL with `unhex()`. No quote character from the shell can now reach the SQL parser. This is a defence-in-depth hardening, not a demonstrated exploit, but it closes off a channel that could have planted extra administrator rows at install time.

critical

Go ArticleRemove() SQL Injection via fmt.Sprintf IN Clause

The Go backend's `ArticleRemove` function built a SQL `DELETE` statement by joining a caller-supplied slice of article IDs directly into the query string with `fmt.Sprintf`. Because the `aids` values came straight from the frontend with no validation or escaping, an attacker could inject arbitrary SQL into the `IN(...)` clause. The fix replaces string concatenation with a parameterized query using placeholders and a `DB.Exec` argument list.

critical

JdbcSinkFunction.invoke() SQL Injection via Unvalidated Identifiers

The `JdbcSinkFunction.invoke()` method in Lacus's RTC engine built SQL INSERT statements with `String.format()`, placing database name, table name, and column names directly into the query string without validation. Although the actual row values used parameterized `?` placeholders, the identifiers themselves were injectable, letting a crafted sink configuration break out of the intended INSERT statement. The fix adds a strict allowlist regex that rejects any identifier containing characters out

critical

Node.js Auth Query SQL Injection via Group Code Interpolation

A sign-in authorization check built its SQL query by mapping an `authorizedGroups` array into quoted string literals and joining them directly into a template literal, creating a classic SQL injection point in a critical authentication path. The fix replaces every interpolated value — including the previously "typed" parameter — with `?` placeholders bound through a value-builder helper, closing off the injection vector entirely.

critical

Toolforge Database Creation SQL Injection via Unicode Backtick Bypass

A critical SQL injection vulnerability in the database creation routine of a Node.js wiki library allowed attackers to inject arbitrary SQL by exploiting a weak validation check. The fix replaces a character-based block with a strict allowlist pattern, ensuring only valid database identifiers reach the SQL engine.

critical

heatmap.php SQL Injection: $_REQUEST Parameters in Unparameterized

A critical SQL injection vulnerability in the heatmap data retrieval endpoint allowed attackers to execute arbitrary database commands by manipulating coordinate bounds or time range parameters. The vulnerability affected all six user-controlled $_REQUEST parameters passed directly into query construction without parameterization.