Critiq team
- rules
Rule spotlight: SQL interpolation and parameterized queries
How Critiq flags interpolated SQL in TypeScript and Java, why parameterized queries fix CWE-89, and which rule IDs to tune in CI.
Rule spotlight: SQL interpolation and parameterized queries content
SQL injection is one of the oldest classes of web vulnerability, and one of the easiest to prevent when values never become part of the query text. Yet string-built SQL still lands in production: template literals in Node services, concatenation in Hibernate HQL, and raw helpers that accept a full statement string. The bug is rarely “we forgot about injection.” It is “this one lookup looked faster with interpolation.”
This rule spotlight covers how Critiq detects those patterns in the OSS catalog: the polyglot rule security.no-sql-interpolation (adapter fact kind security.sql-interpolation), plus Java-specific rules for Hibernate and JPA. We use the same vulnerable-vs-fixed examples you see on the critiq.dev home hero, explain what the engine matches, and show how to run and inspect findings locally.
Rule IDs in the OSS catalog
- security.no-sql-interpolation, interpolated or request-driven SQL text passed to query sinks (TypeScript, JavaScript, Go, Python, Java, PHP, Ruby, Rust; high severity)
- java.security.hibernate-sql-concatenation, Hibernate Session.createQuery / createNativeQuery / createSQLQuery built with string concatenation or String.format (Java; critical severity)
- java.security.jpa-concatenated-query, JPA, JDBC, and string-based @Query values stitched with + or format (Java; critical severity, experimental stability)
There is no separate catalog file named ts.security.sql-interpolation. TypeScript and JavaScript findings come from security.no-sql-interpolation, which matches the shared fact kind security.sql-interpolation. Review examples on critiq.dev sometimes label the TypeScript slice as ts.security.sql-interpolation so multi-language carousels stay readable; critiq check JSON and SARIF use the canonical id security.no-sql-interpolation. The docs registry lists the same id: https://docs.critiq.dev/rules/security.no-sql-interpolation.
What “SQL interpolation” means here
Critiq is not running your database. It does not prove exploitability. It flags code where SQL text is built from dynamic fragments, template literals, string concatenation, or formatted strings, and passed into a recognized query sink (pool.query, knex.raw, ORM helpers, and similar). When user input can reach those fragments, query structure is attacker-influenced. That is CWE-89 (SQL Injection).
The safe default is parameterized execution: fixed SQL shape with placeholders, values bound separately. Drivers and ORMs send parameters out-of-band so quotes in an email address cannot close a string literal or append OR 1=1. ORMs add another layer, named parameters in HQL/JPQL, but only when the query string stays static.
Escaping is not the same as parameterization. Doubling single quotes or hand-rolling sanitizers still couples data with syntax. One missed branch or encoding edge case reopens the hole. Parameter binding keeps the parser from treating attacker-controlled bytes as SQL tokens, which is why OWASP’s prevention cheat sheet centers on prepared statements and stored procedures with bound parameters, not ad hoc string hygiene.
security.no-sql-interpolation (TypeScript and polyglot)
Rule metadata titles it “Avoid raw or interpolated SQL.” Severity is high; confidence is 0.95 in the catalog. References include CWE-89 and the OWASP SQL Injection Prevention Cheat Sheet (https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html). Remediation summary: use prepared statements, placeholder parameters, or a typed query builder, not executed raw SQL text with embedded values.
Example: template literal in a query (bad)
This is the same shape shown in the critiq.dev hero review for src/db/users.ts, user input inside backticks passed to query:
async function findUser(email: string) {
const sql = `SELECT * FROM users WHERE email = '${email}'`;
return db.query(sql);
}Critiq emits security.no-sql-interpolation on the query call. The message calls out that the sink receives SQL built from interpolated text.
Example: placeholders (good)
async function findUser(email: string) {
const sql = 'SELECT * FROM users WHERE email = $1';
return db.query(sql, [email]);
}The statement shape is fixed at compile time. The email value is bound as data. Catalog fixtures under security.no-sql-interpolation treat this pattern as clean.
Example: concatenated raw SQL (bad)
Interpolation is not only backticks. Building a string with + from request data and passing it to knex.raw or similar still matches:
export async function loadUser(req: { query: { id?: string } }) {
const statement =
"SELECT id, email FROM users WHERE id = '" + (req.query.id ?? '') + "'";
return knex.raw(statement);
}Example: bound raw helper (good)
export async function loadUser(id: string) {
return knex.raw('SELECT id, email FROM users WHERE id = ?', [id]);
}Java: Hibernate and JPA rules
Java services often hide SQL inside Hibernate APIs. security.no-sql-interpolation still applies to generic Java query sinks, but two additional rules target framework-specific concatenation that reviewers miss when the file “looks like ORM code.”
java.security.hibernate-sql-concatenation
Matches createQuery, createNativeQuery, and createSQLQuery when the query argument is built with + or String.format. Metadata references CWE-89 and the same OWASP cheat sheet; severity is critical.
List<User> findByEmail(String email) {
return session
.createQuery("from User u where u.email = '" + email + "'")
.list();
}List<User> findByEmail(String email) {
return session
.createQuery("from User u where u.email = :email", User.class)
.setParameter("email", email)
.getResultList();
}java.security.jpa-concatenated-query
Broader JPA/JDBC coverage: createQuery, createNativeQuery, JdbcTemplate usage, and string-based @Query values must not stitch SQL from untrusted fragments. Stability is experimental, treat findings as high-signal review prompts. References again include CWE-89 and OWASP SQL injection prevention guidance.
Spring Data @Query annotations are a frequent footgun: a method signature looks declarative, but a string literal still concatenates request parameters in the annotation value. Critiq’s JPA rule targets that shape so “we use Spring Data” is not mistaken for “we use bound parameters.” Prefer method names with derived queries, Specification APIs, or explicit :param placeholders with @Param when you need custom JPQL.
Myths reviewers should stop accepting
- “It is only internal admin code.”, Internal apps still have authenticated users; injection is about role boundaries, not public internet exposure alone.
- “ORMs mean we are safe.”, Hibernate and JPA still accept raw query strings; concatenation bypasses the mapping layer’s protections.
- “We validate input with a regex.”, Validation reduces bad data; it does not stop quote characters or unicode homoglyphs from breaking out of a string literal you built by hand.
- “Numeric ids cannot inject.”, Concatenated “numeric” fields are still parsed as SQL text unless bound; stick to typed parameters.
Same rule, other languages
security.no-sql-interpolation is intentionally polyglot. Go code that passes fmt.Sprintf-built SQL into db.Query, Python string formatting into execute, or Ruby ActiveRecord joins with embedded user text can trigger the same rule id with the security.sql-interpolation fact kind. You do not maintain parallel ts.security.* and go.security.* copies for this class, one rule file, language adapters emit facts, scope.languages in metadata lists who is in.
Rust has an additional specialized rule rust.security.sqlx-diesel-raw-interpolated-query for format! into sqlx::query or diesel::sql_query sinks. Java teams should still run the Hibernate and JPA rules above when Session or EntityManager APIs hide concatenation that generic polyglot matching might phrase differently. Treat multiple findings on the same line as a signal to refactor toward bound parameters, not as duplicate noise to silence.
How detection works (without magic)
Pattern rules in @critiq/rules declare a fact kind. Language adapters in critiq-core emit facts when source matches structural heuristics, for example a template literal or concatenation flowing into a known query API. The rule file only binds those facts to findings, severity, and remediation text. You can read the contract in the YAML and reproduce the same result on your laptop.
- Fact kind security.sql-interpolation, polyglot; drives security.no-sql-interpolation
- Fact kind java.security.hibernate-sql-concatenation, Hibernate-specific concatenation
- Fact kind java.security.jpa-concatenated-query, JPA/JDBC/@Query concatenation
Sandbox scenario scenarios/typescript/sql-injection expects security.no-sql-interpolation on the Express server path. That is the same rule id you should filter for in CI JSON, not a TypeScript-only alias.
Run critiq check locally
Install the OSS CLI and default catalog, then scan the repo. Findings include ruleId, severity, file, and line, stable across terminal, JSON, and critiq-action inline comments.
npm install -D @critiq/cli @critiq/rules
npx critiq check .
npx critiq check --format json . | jq '.findings[] | select(.ruleId | test("sql-interpolation|no-sql-interpolation|hibernate-sql|jpa-concatenated"))'Commit .critiq/config.yaml with catalog.package: "@critiq/rules" and a preset (recommended or strict) so local runs match CI. For pull requests, scope to changed files with --base and --head.
Inspect rules with critiq rules explain
To read match scope, OWASP/CWE references, and remediation without opening raw YAML cold:
npx critiq rules explain node_modules/@critiq/rules/catalog/rules/shared/security.no-sql-interpolation.rule.yaml
npx critiq rules explain node_modules/@critiq/rules/catalog/rules/java/java.security.hibernate-sql-concatenation.rule.yamlGate in CI without debate drift
Teams that block merges on high-severity catalog findings should include security.no-sql-interpolation in the same policy bucket as hardcoded credentials or open redirects. The rule id is stable across releases unless the catalog ships an explicit deprecation, tune with .critiq/config.yaml presets or rule overrides, not by ignoring comments that use a ts.security.* label from a demo UI.
Diff-scoped scans (--base / --head) keep PR feedback focused on new interpolation introduced in the branch. Staged scans (--staged) catch mistakes before push. JSON output lets you assert zero matches in CI with jq or your platform’s SARIF ingestion; the ruleId field is always security.no-sql-interpolation for the polyglot check even when the file is TypeScript.
What to do in review
- When you see security.no-sql-interpolation, require placeholders or an ORM API that binds parameters, not “we escape quotes manually.”
- When Hibernate rules fire, check whether the query string is static and parameters are set via setParameter or the Criteria API.
- If a reviewer cites ts.security.sql-interpolation from a demo UI, map it to security.no-sql-interpolation in the catalog and policy docs.
- Link to critiq rules explain output so merge debates reference inspectable metadata (CWE-89, OWASP cheat sheet URLs), not memory.
Takeaway
SQL injection defenses are boring on purpose: never let user input alter query structure. Critiq names interpolation patterns with stable rule ids, security.no-sql-interpolation for most languages, plus java.security.hibernate-sql-concatenation and java.security.jpa-concatenated-query for framework-specific Java sinks. Run critiq check before merge, use critiq rules explain when you need the full rule contract, and fix with parameterized queries every time.
More from the blog

- philosophy
The trust gap in AI-assisted coding, and what inspectable feedback looks like
Developers use AI assistants daily but often distrust review feedback. Inspectable rules, evidence, and local checks close that gap.
Read article
- philosophy
What evidence over vibes means in code review
Review comments should be defensible: tied to a rule, a line, severity, and references, not just confident prose.
Read article
- philosophy
Why we open-sourced the rules engine (and what stays in the catalog)
Critiq ships the rule engine, DSL, and 435+ OSS catalog rules in the open. Here is what you get locally, what Pro adds, and how to inspect rules yourself.
Read article