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.


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.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #313

Related Articles

critical

SQL's Insert() and Update() Methods Used F-String Interpolation in u2share_batch_give_sugar

The SQL helper class in u2share_batch_give_sugar used Python f-strings to construct INSERT and UPDATE queries, creating SQL injection vulnerabilities even though values appeared to come from internal constants. The fix replaces all f-string query construction with sqlite3 parameterized queries using `?` placeholders, eliminating string interpolation entirely from the database path.

high

TrackOptionsManager DDL: Template-Literal SQL Injection Closed

The `TrackOptionsManager` service built its `CREATE TABLE` and `ALTER TABLE ... ADD COLUMN alias` statements by interpolating a `DEFAULT_ALIAS` constant directly inside single quotes in a JavaScript template literal, and its private `_query()` helper had no parameter channel at all. The fix routes the default value through `mysql.escape()` and gives `_query(q, params = [])` a real bound-parameter argument that is forwarded to `db.query()`. This removes an injection primitive on a schema-bootstra

high

How SQL injection via template literals happens in Node.js SQLite and how to fix it

A SQL injection vulnerability in `src/lib/codex-state.mjs` allowed dynamic column names to reach SQL queries through JavaScript template literals. The fix implements defense-in-depth with strict identifier validation using `SAFE_IDENTIFIER` regex before query construction.

high

How Python SQLAlchemy Raw Query SQL Injection happens and how to fix it

A high-severity SQL injection vulnerability was fixed in the `skills/last30days/scripts/store.py` file where untrusted input was being concatenated directly into raw SQL queries. The fix replaces string concatenation with SQLAlchemy's TextualSQL prepared statements using named parameters, preventing attackers from manipulating database queries through malicious input.

critical

How SQL injection happens in Python DuckDB view creation and how to fix it

A critical SQL injection flaw in `python/src/idx/api.py:265` built five DuckDB `CREATE VIEW` statements with Python f-strings, interpolating a filesystem path directly into SQL text. The fix replaces the interpolated path with a bound parameter (`read_parquet(?)`) and moves the view names into a hardcoded, non-interpolated statement map — eliminating any path where filenames or directory values can alter SQL structure.

critical

How SQL Injection happens in PHP bulk email systems and how to fix it

A critical SQL injection vulnerability in `admin/utilities/bulkEmailSystem.php` allowed attackers to inject arbitrary SQL through unvalidated database names passed from user input. The fix implements strict input validation using regex pattern matching to ensure only safe database identifiers are processed, preventing exploitation of the bulk email functionality.