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

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.

critical

Actual Budget addTransaction.sh SQL Injection via Shell Variable

A critical SQL injection vulnerability in Actual Budget's transaction automation script allowed attackers to manipulate database records through shell variables interpolated directly into SQL strings. The fix introduces proper escaping functions and numeric validation to prevent injection through unquoted fields.