Back to Blog
critical SEVERITY8 min read

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

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

This is a SQL injection vulnerability (CWE-89) in `lib/ParamediciOSPermissions.js`, a Node.js library for managing iOS simulator permissions. The `${service}` and `${app}` variables were interpolated directly into SQL `INSERT` and `UPDATE` statements passed to the `sqlite3` CLI without escaping, allowing an attacker who controls those parameters to manipulate the iOS TCC permission database. The fix escapes single quotes in both variables using `.replace(/'/g, "''")` before embedding them in the SQL strings, neutralizing the injection vector.

Vulnerability at a Glance

cweCWE-89
fixEscape single quotes in both variables with `.replace(/'/g, "''")` before SQL interpolation
riskAttacker can manipulate iOS simulator TCC permission database entries
languageJavaScript (Node.js)
root cause`${service}` and `${app}` interpolated into raw SQL without escaping
vulnerabilitySQL Injection via unescaped template literals in sqlite3 CLI calls

How SQL Injection Happens in Node.js SQLite CLI Calls and How to Fix It


Vulnerability at a Glance

Field Detail
Vulnerability SQL Injection via unescaped template literals
CWE CWE-89
Language JavaScript (Node.js)
Risk Manipulation of iOS simulator TCC permission database
Root Cause ${service} and ${app} interpolated into raw SQL without escaping
Fix Escape single quotes with .replace(/'/g, "''") before interpolation

Summary

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 they are embedded in the SQL strings.


Introduction

The lib/ParamediciOSPermissions.js file is responsible for managing iOS simulator permissions by writing directly to the TCC (Transparency, Consent, and Control) SQLite database — the same database macOS uses to track which apps have been granted access to sensitive resources like the camera, microphone, and contacts.

Inside a for loop starting around line 55, the code builds SQL INSERT and UPDATE statements using JavaScript template literals, then passes them as arguments to the sqlite3 CLI via spawnAsync. The two variables at the heart of the issue are service (a permission service name like kTCCServiceCamera) and app (the application bundle identifier, sourced from this.appName).

Neither variable was sanitized before being embedded in the SQL string. For any developer building iOS testing tooling or CI/CD pipelines that consume this library, this represents a meaningful attack surface — especially because the appName value can be supplied by the caller.


The Vulnerability Explained

The Vulnerable Code

Here is the original INSERT statement from the loop body:

const insetProc = await spawnAsync(
    'sqlite3',
    [
        destinationTCCFile,
        `"INSERT INTO access (service, client, client_type, allowed, prompt_count, csreq) VALUES('${service}', '${app}', 0, 1, 1, NULL)"`
    ]
);

And the fallback UPDATE statement:

const updateProc = await spawnAsync(
    'sqlite3',
    [
        destinationTCCFile,
        `"UPDATE access SET client_type=0, allowed=1, prompt_count=1, csreq=NULL WHERE service='${service}' AND client='${app}'"`
    ]
);

Both statements embed ${service} and ${app} directly inside single-quoted SQL string literals. If either variable contains a single quote character ('), the SQL string literal is terminated prematurely — and anything that follows becomes raw SQL.

A Concrete Attack Scenario

Suppose an attacker controls the appName passed to the ParamediciOSPermissions constructor (for example, via a CI configuration file, a test manifest, or a crafted npm package that consumes this library). They set:

appName = "com.evil.app', 0, 1, 1, NULL); DROP TABLE access; --"

The resulting SQL sent to sqlite3 would be:

INSERT INTO access (service, client, client_type, allowed, prompt_count, csreq)
VALUES('kTCCServiceCamera', 'com.evil.app', 0, 1, 1, NULL); DROP TABLE access; --', 0, 1, 1, NULL)

This executes two statements: the intended INSERT and a destructive DROP TABLE access, wiping all stored permissions from the TCC database. More targeted payloads could silently grant a malicious app camera or microphone access in the simulator environment.

Why This Matters for a Node.js Library

This file is in the production library codebase, not test-only code. Every downstream project that installs this package and passes user-influenced or externally-sourced values to the ParamediciOSPermissions constructor inherits this vulnerability. In CI/CD environments — where simulator setup is often automated and parameters come from configuration files checked into version control — the blast radius is significant.


The Fix

What Changed

Two escaping assignments were added at the top of the loop, immediately before the SQL strings are constructed:

const escapedService = service.replace(/'/g, "''");
const escapedApp = app.replace(/'/g, "''");

The SQL strings were then updated to use these escaped variables instead of the raw originals:

const insertSQL = `"INSERT INTO access (service, client, client_type, allowed, prompt_count, csreq) VALUES('${escapedService}', '${escapedApp}', 0, 1, 1, NULL)"`;

const updateSQL = `"UPDATE access SET client_type=0, allowed=1, prompt_count=1, csreq=NULL WHERE service='${escapedService}' AND client='${escapedApp}'"`;

Before and After

Before (vulnerable):

`"INSERT INTO access (...) VALUES('${service}', '${app}', 0, 1, 1, NULL)"`

After (fixed):

const escapedService = service.replace(/'/g, "''");
const escapedApp = app.replace(/'/g, "''");
const insertSQL = `"INSERT INTO access (...) VALUES('${escapedService}', '${escapedApp}', 0, 1, 1, NULL)"`;

Why This Fix Works

In SQLite (and standard SQL), the correct way to include a literal single quote inside a single-quoted string is to double it: ''. The .replace(/'/g, "''") call does exactly this — it transforms every ' in the input into '', ensuring the SQL parser always sees a properly escaped string literal and never interprets the character as a string terminator.

For example, the malicious input com.evil.app', 0, 1, 1, NULL); DROP TABLE access; -- becomes:

com.evil.app'', 0, 1, 1, NULL); DROP TABLE access; --

SQLite now interprets the entire value as a single string literal containing a literal single quote, and the injected SQL is never executed.


Prevention & Best Practices

1. Prefer a Native SQLite Library Over the CLI

The root cause of this vulnerability is that the code shells out to the sqlite3 binary, which means it cannot use true parameterized queries. Switching to a library like better-sqlite3 or node-sqlite3 enables proper prepared statements:

// Using better-sqlite3 with a parameterized query
const stmt = db.prepare(
    'INSERT INTO access (service, client, client_type, allowed, prompt_count, csreq) VALUES (?, ?, 0, 1, 1, NULL)'
);
stmt.run(service, app);

With parameterized queries, the database driver handles all escaping internally — there is no string interpolation involved, and injection is structurally impossible.

2. When CLI Is Unavoidable, Escape Rigorously

If shelling out to sqlite3 is a hard requirement, apply the SQLite single-quote doubling escape (replace(/'/g, "''")) to every variable interpolated into a SQL string, as this fix does. Consider also validating inputs against an allowlist — for example, iOS permission service names follow a known pattern (kTCCService*) and bundle identifiers follow a known format (com.example.app).

3. Validate at the Constructor Boundary

Since appName is set in the constructor and used throughout the class, validating it at construction time (rather than at each use site) is a cleaner defense:

constructor(appName, serviceList) {
    if (!/^[a-zA-Z0-9.\-]+$/.test(appName)) {
        throw new Error(`Invalid appName: ${appName}`);
    }
    this.appName = appName;
    // ...
}

4. Use Static Analysis in CI

Tools like Semgrep can detect unescaped template literal interpolation in SQL strings. Adding a Semgrep scan to your CI pipeline catches this class of vulnerability before it reaches production.

5. Reference Standards


Key Takeaways

  • Template literals are not safe SQL builders. Using ${variable} inside a SQL string is functionally identical to string concatenation — if the variable contains SQL metacharacters, injection is possible.
  • The sqlite3 CLI cannot use parameterized queries. Whenever you shell out to a database CLI tool, you lose the protection that prepared statements provide, making escaping your only line of defense.
  • appName in ParamediciOSPermissions is an injection entry point. Any value sourced from the constructor that flows into a SQL string must be escaped — the fix correctly identifies both service and app (via this.appName) as needing treatment.
  • SQLite's escaping rule is simple but easy to forget. Double single quotes ('') is the correct SQLite escape for a literal ' inside a string literal — not backslash escaping, which SQLite does not support by default.
  • Library code amplifies vulnerabilities. Because ParamediciOSPermissions.js is a library, every downstream consumer inherits the vulnerability. Fixing it in one place protects the entire ecosystem of projects that depend on it.

How Orbis AppSec Detected This

  • Source: The appName parameter passed to the ParamediciOSPermissions constructor, and the service values from the serviceList array — both externally supplied by library consumers.
  • Sink: The spawnAsync('sqlite3', [...]) calls at lines ~60 and ~72 in lib/ParamediciOSPermissions.js, where the unescaped variables are embedded in raw SQL strings passed as CLI arguments.
  • Missing control: No escaping, sanitization, or allowlist validation was applied to service or app before SQL interpolation.
  • CWE: CWE-89 — Improper Neutralization of Special Elements used in an SQL Command (SQL Injection).
  • Fix: Single quotes in both service and app are now escaped with .replace(/'/g, "''") before being interpolated into the SQL strings.

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 vulnerability is a clear example of how SQL injection can hide in unexpected places — not in a web server handler or a REST API, but in a Node.js library that manages iOS simulator permissions by shelling out to the sqlite3 CLI. The pattern of using JavaScript template literals to build SQL strings is visually clean and idiomatic, which makes it easy to overlook the injection risk it introduces.

The fix is minimal and surgical: two .replace(/'/g, "''") calls that neutralize the injection vector without changing any observable behavior. But the deeper lesson is architectural — whenever you cannot use parameterized queries (because you're calling a CLI tool rather than a database library), you must be rigorous about escaping every value that touches a SQL string. Better still, migrate to a library that supports prepared statements and eliminate the escaping burden entirely.

For developers building testing tools, CI utilities, or any library that writes to SQLite databases, this case is a reminder to audit every place where external input flows into a SQL string, no matter how controlled the environment seems.


References

Frequently Asked Questions

What is SQL injection in Node.js sqlite3 CLI calls?

It occurs when user-controlled variables are embedded directly into SQL strings passed to the sqlite3 command-line tool without escaping, allowing an attacker to break out of string literals and inject arbitrary SQL commands.

How do you prevent SQL injection in Node.js when using the sqlite3 CLI?

Prefer a Node.js SQLite library (like `better-sqlite3`) that supports parameterized queries. If you must use the CLI, escape single quotes by replacing `'` with `''` in all interpolated values, and validate inputs against an allowlist where possible.

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 alone enough to prevent SQL injection?

No. Input validation (allowlisting) is a useful defense-in-depth measure, but parameterized queries or proper escaping are the primary controls. Validation alone can be bypassed if the allowlist is incomplete or incorrectly implemented.

Can static analysis detect SQL injection in sqlite3 CLI template literals?

Yes. Tools like Semgrep and multi-agent AI scanners can trace tainted data from constructor parameters through template literals into shell command arguments, flagging unescaped interpolation as a SQL injection risk.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #313

Related Articles

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 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 SQL Injection happens in Python BigQuery connectors and how to fix it

A high-severity SQL injection vulnerability was discovered in a BigQuery connector's query-building logic, where Python f-strings interpolated user-controlled identifiers—project_id, dataset_id, table_id, and timestamp_column—directly into SQL without validation. An attacker with control over connector configuration could inject arbitrary BigQuery SQL, including destructive statements. The fix introduces strict allowlist-based identifier validation using compiled regular expressions before any S

critical

How SQL Injection happens in PHP PDO queries and how to fix it

A critical SQL injection vulnerability was discovered in the `getOfficialContests()` method of ContestRepository.php, where the `$site_id` parameter was directly interpolated into a SQL query string instead of using prepared statements. This vulnerability allowed attackers to inject arbitrary SQL commands and potentially access or manipulate the entire contest database. The fix replaced `pdo->query()` with `pdo->prepare()` and proper parameter binding.

critical

How Archive Path Traversal Happens in Node.js and How to Fix It

CVE-2026-53486 is a critical path traversal vulnerability in the Decompress library, where crafted archive entries can write files and symbolic links outside the intended extraction directory. This vulnerability was transitively introduced through `@vitest/browser` and related packages pinned at version 4.1.5, and was resolved by upgrading to 4.1.6 and 5.0.0-beta.3. Left unpatched, an attacker who controls an archive file processed by any downstream consumer of this dependency chain could overwr