Security is the primary design goal of AQL. This document explains the guarantees the language makes, the threat model behind them, how they are enforced, and how to report a problem.
Untrusted data can never become executable query syntax.
A query is an immutable tree of typed nodes. Structure (tables, columns,
operators, joins, functions, clauses) is encoded by the node types. Data
(runtime values) exists only inside Literal and Param nodes. During
compilation, every literal and parameter is emitted as a placeholder and its
value is pushed onto a separate parameter array — it is never concatenated into
the SQL string.
Because there is exactly one place where values cross into a query (as bound parameters), and it never produces SQL text, injection through values is not possible in the safe subset of the language. There is no string escaping to get wrong, because no value is ever turned into SQL text.
from(users).where((c) => c.users.name.eq("'; DROP TABLE users; --")).selectAll();
// sql: SELECT * FROM "users" WHERE ("users"."name" = $1)
// params: ["'; DROP TABLE users; --"] ← inert dataThis is verified end-to-end against a real database in
tests/integration.sqlite.test.ts.
- Value injection is impossible in the safe subset. Any JavaScript value
passed to a combinator (
.eq,.in,.between, INSERT values, UPDATE assignments, …) becomes a bound parameter. - Identifiers are validated and quoted. Table, column, alias, and schema names are checked for control characters and then quoted with the dialect's rules (terminator-doubling), so they cannot break out of their quotes.
- The compiler rejects malformed structure. Even when an AST arrives from an untrusted source (deserialized JSON, a generator, the CLI) and bypasses TypeScript, the code generator validates every field it emits verbatim.
raw()is an escape hatch. Static SQL fragments you write inside arawtemplate are emitted verbatim and are your responsibility. Interpolated${...}values are still bound as parameters, but the surrounding SQL is trusted. Never build arawfragment by string-concatenating untrusted input. UseallowRaw: false(see below) when compiling ASTs you do not trust.- AQL does not manage authorization, row-level security, or connection security. It generates parameterized SQL; access control and transport security belong to your database and driver.
The only fields that reach SQL text without being a bound parameter or a compiler-chosen keyword are: identifiers, function names, and a few enum-like fields (operators, join types, set operators, sort directions). Each is guarded:
| Surface | Risk | Enforcement |
|---|---|---|
| Values / literals / params | Injection | Always emitted as placeholders; values bound separately |
| Identifiers (table/column/alias/schema) | Quote break-out, control chars | assertIdentifier + dialect quoting with terminator doubling |
Function names (customFn, Function node) |
Arbitrary SQL | assertFunctionName allowlist (plain, optionally schema-qualified identifier) |
| Binary/unary operators | Operator smuggling | Allowlist checked in the code generator |
| Join / set operators, sort direction, nulls order | Keyword smuggling | Allowlists checked in the code generator |
LIMIT / OFFSET literals |
Negative / non-integer values | Validated to be finite, non-negative integers |
| Named parameters | Prototype-chain lookup | Resolved with Object.hasOwn, never inherited properties |
CAST target types |
Unknown type injection | Mapped through a fixed per-dialect table |
These guards are exercised by tests/hardening.test.ts.
If you accept an AST from a source you do not fully trust, compile it in safe
mode, which forbids the raw() escape hatch entirely:
compile(untrustedAst, postgres(), { allowRaw: false });
// throws UnsafeRawError if the AST contains any raw SQLIn safe mode the only paths to SQL text are validated identifiers, allowlisted
function names, allowlisted operators/keywords, and bound parameters — so a
hostile AST cannot inject SQL. The aql CLI exposes this as --safe.
Compilation is a pure function of (AST, dialect, options). There is no
randomness, no ambient state, and parameter order is stable (left-to-right in the
emitted SQL). The same input always produces the same { sql, params }.
Security fixes target the latest 0.x release line. AQL requires Node.js 18+.
Please report suspected vulnerabilities privately using GitHub's "Report a vulnerability" feature on the repository's Security tab, or open a minimal private advisory. Do not open a public issue for undisclosed vulnerabilities. Include a reproducing AST or query and the expected vs. actual SQL. We aim to acknowledge reports promptly and to credit reporters unless anonymity is requested.