by SQLFix (LX AI)

Last updated: 2026-09-16

Parameterized queries vs generated SQL

A model that writes a correct-looking query has not solved injection. The security property that matters lives in how the query meets the data, and that is the job of parameterized queries with prepared statements. Generated text is a starting point for review; it is not a substitute for the boundary that prepared statements enforce between code and data.

What a prepared statement actually does

When the driver receives a prepare request it sends the SQL text to the server. The server parses the text, builds an execution plan, and stores that plan. Later, when the application calls execute, the driver sends only the parameter values. The server applies the stored plan to those values without re‑parsing the SQL text. Because the values never become part of the statement string, a payload such as ' OR 1=1-- is treated as a literal value, not as part of the command.

I have seen this work in MySQL 5.0, PostgreSQL 8.0, SQL Server 2005, and Oracle 9i. Every mainstream language provides a binding API: JDBC’s PreparedStatement, Python’s cursor.execute with placeholders, Go’s database/sql.Query, and Node’s mysql2 pool.execute. The model can output the shape SELECT * FROM orders WHERE customer_id = ? AND status = ?; the surrounding code supplies the two values at runtime.

Why the separation matters

How to verify it in your stack

  1. Enable general query log or equivalent debug output.
  2. Run a prepared statement with a value that contains a quote.
  3. Check the log: the statement text will show the placeholder, not the quote.
  4. The value will appear in a separate “parameters” line.

If you see the quote inside the statement text, you are not using a true prepared statement; you are likely doing client‑side string interpolation, which does not give the injection safety.

Why reviewed SQL still needs parameters

Human review can confirm that the query shape matches the intended business rule. It can spot missing joins, wrong column names, or inefficient filters. However, the values that flow into those placeholders still come from external sources: web forms, API payloads, message queues, or legacy files. Those sources are not under the developer’s direct control at the moment of execution.

OWASP’s SQL Injection Prevention Cheat Sheet states clearly: “Defense Option 1: Prepared Statements (Parameterized Queries)”. The cheat sheet does not list “human approval” as a substitute. Approval addresses logical correctness; parameterization addresses the trust boundary between code and data. Dropping either leaves the other unguarded.

Concrete failure scenario

Even though the human reviewer saw a sensible INSERT statement, the injection happened at the value‑binding step, which the review did not cover.

How to keep the review step useful

  1. Store the approved query shape as a template string with placeholders.
  2. Never concatenate raw values into that template.
  3. At runtime, pass the template to the driver’s prepare function.
  4. Supply the values as separate arguments to the execute call.
  5. Log both the template and the bound values for audit.

This way the reviewer’s work remains valuable, and the execution step retains its safety guarantee.

Stored procedures are not automatic safety

Many teams assume that moving logic into a stored procedure eliminates injection risk. That assumption fails when the procedure itself builds dynamic SQL.

Consider a procedure written in T‑SQL:

CREATE PROCEDURE GetOrders @Customer NVARCHAR(50)
AS
BEGIN
    DECLARE @sql NVARCHAR(MAX) = N'SELECT * FROM Orders WHERE Customer = ''' + @Customer + '''';
    EXEC sp_executesql @sql;
END

If @Customer contains '); DROP TABLE Orders;--, the resulting string becomes a valid batch that drops the table. The procedure wrapper did not protect the data; the concatenation inside the procedure did.

The same pattern appears in PL/SQL with EXECUTE IMMEDIATE, in PostgreSQL with EXECUTE format, and in MySQL with PREPARE/EXECUTE built from a variable.

What makes a stored procedure safe

How to audit existing procedures

  1. Extract the procedure definition from the database catalog.
  2. Search for patterns like +, ||, CONCAT, FORMAT that combine a variable with SQL keywords.
  3. Check any EXECUTE or sp_executesql call for a USING clause that supplies the variables separately.
  4. If you find a concatenation, rewrite the block to use parameter binding.

After the rewrite, run the procedure with a value that contains a quote and verify that the error is a data‑type mismatch, not a syntax error.

Keep generation and execution as separate layers

The safest architecture treats the AI model as a suggestion engine, not as a data‑access component. The model outputs a candidate query shape. A human or automated policy gate reviews that shape for correctness, compliance, and performance. Only after approval does the application code take the shape, prepare it with the driver, and bind the real‑time parameters.

Layer diagram in practice

Because the model never sees a live connection, it cannot be tricked into revealing schema details through error messages. Because the execution layer never trusts raw strings, even a compromised model cannot cause injection.

Implementation steps for SQLFix

  1. Prompt the model with the user’s intent and return a raw string.
  2. Strip whitespace and validate that the string contains only allowed SQL keywords and placeholders (? or $1 depending on driver). Reject anything with concatenation operators.
  3. Store the validated string in a version‑controlled query‑template repository.
  4. When a request arrives, fetch the template, call db.prepare(template), then stmt.execute(values).
  5. If any step fails, return a generic error to the user and log the failure for security review.

This process keeps the model’s output in the review loop and guarantees that the execution step uses only parameterized statements.

What this is NOT

I want to be clear about the limits of the approach described above. It does not guarantee that every possible bug will be caught. It does not promise 100 % protection against all classes of flaws. It does not claim to eliminate the need for other defenses such as input validation, output encoding, or least‑privilege database accounts.

What the method does is:

If you rely solely on the model’s output and skip the review or the parameter binding step, you lose the protection. If you allow the execution layer to concatenate values after preparation, you reintroduce the risk. The technique is a component of a defense‑in‑depth strategy, not a standalone silver bullet.

Real‑world workflow example

Imagine a SaaS product that lets managers export a list of overdue invoices. The product uses SQLFix to draft the query.

Step‑by‑step trace

  1. The manager types “show invoices overdue by more than 30 days for customer ACME”.
  2. The model receives the prompt and returns SELECT invoice_id, amount, due_date FROM invoices WHERE customer_name = ? AND due_date < ?.
  3. The review gate checks that the placeholder for customer_name is correctly placed, that the date comparison uses the proper column, and that the query does not select sensitive columns like payment_token.
  4. The approved template is stored as inv_overdue_template_v3.sql.
  5. At runtime, the service extracts the customer name from the authenticated user’s context and computes the cutoff date as today minus 30 days.
  6. The driver prepares the template, binds the two values ('ACME' and 2024-09-15), and executes.
  7. The database returns the rows; the service formats them as CSV and streams the file back to the manager.
  8. If the manager later changes the request to “show invoices overdue by more than 30 days for customer ACME'; DROP TABLE invoices;--”, the model still outputs the same template because the prompt does not affect the shape. The review gate again approves the template. The execution layer receives the malicious string as a value for the customer_name placeholder. The driver treats it as data, so the dropped‑table statement never runs.

This trace shows how the generation step stays independent from the execution step, and how the parameter boundary blocks injection even when the user tries to smuggle SQL into a field that the model never sees.

Common pitfalls and how we avoid them

Even with a solid design, teams sometimes slip into habits that weaken the protection. Below are frequent mistakes I have observed and the concrete steps we take to keep the system safe.

Pitfall 1: Using string interpolation for the IN clause

Some developers build SELECT * FROM tags WHERE tag IN (?, ?, ?) by first creating a string with the right number of placeholders, then concatenating the values directly into the SQL before preparing. This defeats the purpose because the values become part of the statement text.

Our fix

  1. Keep the placeholder pattern static: SELECT * FROM tags WHERE tag IN (?, ?, ?).
  2. Pass the list of tag values as an array to the driver’s execute method; the driver expands the placeholders internally.
  3. If the driver does not support array binding, create a temporary table, insert the values there, and join to it—still using parameters for the insert.

Pitfall 2: Re‑using a prepared statement across different logical queries

A statement prepared for SELECT * FROM orders WHERE status = ? is later executed with a value that actually contains a whole new SELECT statement, relying on the driver to treat the value as data. While the value is safely quoted, the application logic may be wrong because the developer assumed the statement could handle any SQL.

Our fix

  1. Treat each distinct SQL shape as its own prepared statement object.
  2. Never change the placeholder count or order without creating a new prepare call.
  3. Use a naming convention that includes the query’s purpose, e.g., ps_order_by_status.

Pitfall 3: Skipping the review step for “trusted” internal tools

Internal dashboards sometimes bypass the human review gate because the team assumes the data is trusted. Later, a service account is compromised and begins feeding malicious payloads into the dashboard.

Our fix

  1. Apply the same review gate to all query sources, internal or external.
  2. Automate the gate with a lint‑check that rejects any template containing concatenation, dynamic SQL, or comments that could hide payloads.
  3. Require a ticket in the issue tracker for any template change, even if the change is only whitespace.

Pitfall 4: Logging the full bound values in plain text

For debugging, teams sometimes log the exact parameter values. If those logs are exposed, they can leak sensitive data such as passwords or personally identifiable information.

Our fix

  1. Log only a hash or a truncated placeholder representation of each value.
  2. Store the full values in a secure, access‑controlled audit table if needed for forensics.
  3. Mask any value that matches patterns for secrets (e.g., looks like a credit card number) before writing to logs.

Pitfall 5: Assuming ORM safety without checking the underlying call

An ORM may offer a method that appears to accept parameters, but under the hood it builds a string and calls ExecuteNonQuery on a raw SQL string.

Our fix

  1. Review the ORM’s source or documentation to confirm that the method uses the driver’s prepare/execute path.
  2. If unsure, wrap the call in a tracer that logs the exact SQL sent to the database; verify that placeholders remain.
  3. Prefer explicit builder APIs that require you to pass a parameter object separately.

By watching for these pitfalls and applying the corresponding fixes, the separation between generation and execution stays strong, and the parameterized boundary continues to do the work it was designed for.

References

FAQ

Does a correct query protect against SQL injection?

No. Injection depends on how values meet the query, not on whether the query is logically correct. Parameterized queries keep values separate from command text regardless of the values' content.

Are stored procedures safe by default?

Not automatically. A procedure that concatenates input and executes it with EXECUTE IMMEDIATE can still inject. Safety comes from parameter binding inside the procedure.

Can an ORM replace parameterized queries?

An ORM is safe only when you use its parameterized interface. Building a string and passing it to the ORM reintroduces injection. The boundary between code and data still has to be enforced.

Should a model ever hold the database connection?

No. Keep generation and execution separate: the model drafts, a gate approves, and application code binds parameters at runtime. The model should never be the execution path.

Try the product: SQLFix

2026-09-16 · primary sources only · no fabricated traffic metrics