Skip to content

Security: FMLGamer/AQL

Security

SECURITY.md

Security Policy

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.

The core guarantee

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 data

This is verified end-to-end against a real database in tests/integration.sqlite.test.ts.

What is guaranteed

  • 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.

What is not guaranteed

  • raw() is an escape hatch. Static SQL fragments you write inside a raw template are emitted verbatim and are your responsibility. Interpolated ${...} values are still bound as parameters, but the surrounding SQL is trusted. Never build a raw fragment by string-concatenating untrusted input. Use allowRaw: 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.

Threat model & enforcement

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.

Compiling untrusted ASTs safely

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 SQL

In 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.

Determinism

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 }.

Supported versions

Security fixes target the latest 0.x release line. AQL requires Node.js 18+.

Reporting a vulnerability

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.

There aren't any published security advisories