SQL injection
SQL injection is the 7-on-the-original-OWASP-top-10, never-actually-died, still-causing-real-breaches class of bug. Aikido's State of SQL Injection report shows it still accounts for ~7–10% of vulnerabilities found across open- and closed-source code. Daloy is an HTTP framework, not a database driver, so it can't parameterize your queries for you — but it does ship the layers before the database that make SQLi materially harder to introduce, and the patterns below close the rest.
What Daloy already does for you
These are core-enforced and require no opt-in. They don't replace parameterized queries, but they shrink the attack surface that reaches your repository layer in the first place.
| Layer | What it blocks |
|---|---|
| Strict per-route schemas (Zod) | Routes declare params, query, and bodyshapes. Inputs that don't match the schema are rejected with 400 problem+json before your handler runs, so you almost never have to coerce raw strings into query parameters yourself. |
| JSON parser hardening | safeJsonParse strips __proto__, constructor, and prototype keys. Prevents prototype pollution that NoSQL/SQL adapters can turn into operator injection. |
| Body-size cap | 1 MiB default, streamed. Removes the “upload a 50 MB payload of OR 1=1” DoS-amplified-SQLi pattern. |
| Structured logging redaction | redactRecord()scrubs known credential-shaped fields before they hit logs — helpful when post-incident triage needs to share logs without re-leaking the very secrets the injection grabbed. |
| Secure-by-default HTTP boundary | CRLF header sanitization, path-traversal rejection in the router, and 405 instead of 404 on method confusion all mean attackers can't smuggle DB-bound payloads through quirky transport layers. |
None of that parameterizes your query. That is on you and your ORM. The next sections show what “safe” looks like for the ORMs Daloy documents, and what the unsafe siblings look like so you can grep for them in code review.
The shape of a safe Daloy route
Validated input + parameterized query is the whole pattern. Aikido's report calls it out as defense #1 and #2; Daloy gives you both in one block.
Notice what is not here: no template string with ${params.id}, no manual quoting, no “just this once we'll trust the input.” If you find yourself writing those in a Daloy handler, treat it as a bug.
Safe vs. unsafe per ORM
Prisma
Drizzle
Kysely
node-postgres / mysql2 (no ORM)
Operator injection (the “NoSQL injection in Prisma” trap)
Aikido's Prisma + PostgreSQL is vulnerable to NoSQL-style injection write-up describes a real, common bug: even though Prisma always emits parameterized SQL, the filter object you pass to where is interpreted by Prisma itself. If a field is annotated as string in TypeScript but the runtime value is an object like { "not": "x" } or { "contains": "" }, Prisma treats it as a filter operator. An attacker who can submit raw JSON to a login or password-reset endpoint can use that to bypass equality checks. The same idea bites Mongoose, TypeORM FindOptions, and any builder that accepts “value or operator” in the same slot.
Daloy's contract-first routes neutralize this by default: every body, query, and params slot is validated against a Zod schema before your handler runs, and Zod's primitive checks (z.string(), z.string().email(), z.number(), …) reject nested objects with a 400 problem+json. The vulnerability shows up when developers route around that — usually with z.any(), z.unknown(), a pass-through z.record(), or by reading await req.json() directly and spreading it into where.
If you genuinely need to accept a caller-controlled filter (an admin search endpoint, for example), wrap each operator explicitly so a rogue key can't reach Prisma:
Review-time rules:
- Never use
z.any(),z.unknown(), or unconstrainedz.record()for a field that is then read out of a Prisma / Mongoose / TypeORMwhereclause. Constrain each property with a primitive schema. - Never spread
...bodyor...queryintowhere,data, ororderBy. Map fields one at a time after validation. - Treat a missing
requestschema on a route that touches the DB as the same severity as a missing CSRF token — Daloy's strict-schema gate is doing real work here.
Dynamic SQL: when you can't parameterize
Bind parameters cover values, not identifiers. ORDER BY columns, table names, direction (ASC/DESC), and dynamic IN (...)arities can't be bound, so the safe pattern is to allowlistthe legal values instead of escaping. Daloy's recommended approach: encode the allowlist directly in your Zod schema so the HTTP boundary rejects anything else, then index into a typed map of identifiers in the handler.
Rules of thumb when even allowlisting isn't enough:
- If you must accept a free-form identifier, validate it against a tight regex (
/^[a-zA-Z_][a-zA-Z0-9_]*$/) andquote it with your driver's identifier-escape helper (pg-format's%I, Knex'sclient.wrapIdentifier, etc.). Never roll your own. - For variable-arity
IN, build the placeholder list from the array length and bind the values:WHERE id IN ($1, $2, $3). Most ORMs do this for you when you pass an array toinArray()/in. - For
LIKEwith user input, escape%and_in the value, then bind the escaped value. Don't splice the wildcards into the SQL string.
Things to grep for in code review
Aikido's report says vulnerable organizations average ~30 separate SQLi sites. The fastest way to keep that number at zero is a periodic grep across the repo. The Daloy maintainers use this list:
Wire one of those into CI as a soft check (or as a Semgrep / CodeQL rule) and you'll catch ~all new SQLi at PR time. It's not as clever as a SAST tool, but it's free and runs in 200 ms.
Defense in depth: runtime firewalls
Daloy intentionally does not ship a heuristic SQLi-detector middleware. Pattern-matching ' OR 1=1 -- on every request body is noisy, false- positives easily on legitimate text (think a blog post about SQL injection…), and gives a false sense of security. If you want a runtime backstop, install a proper in-app firewall that tokenizes queries against your real schema:
- Aikido Zen — Node/Bun-compatible in-app firewall that hooks the driver and blocks requests whose query structure was altered by user input.
- A reverse-proxy WAF (Cloudflare, AWS WAF, Fastly) for coarse signature matching at the edge. Cheap to deploy; not a substitute for parameterized queries.
Reporting
Found a SQLi-shaped weakness in Daloy itself (e.g. a sanitizer that leaks DB-meaningful characters, or a code example that demonstrates an unsafe pattern)? Report it privately via github.com/daloyjs/daloy/security/advisories/new. Don't open a public issue.