All articles

BLOG / HOW TO AVOID SQL INJECTIONS

How to Avoid SQL Injections Before They Ship

Learn how to avoid SQL injections with parameterized queries, least privilege, CI checks, and a clear incident path for signup and login.

The reliable way to avoid SQL injection is to bind request values as data in server-side queries. SQL injection still appears in tested applications, so teams responsible for signup, login, and checkout need to cover every query path behind those browser actions.

String cleaning cannot provide that coverage. A helper may strip quotes while a signup query still concatenates input, or a report or background job may skip the helper entirely. A real name, password, or promo code can contain punctuation; the query should handle it as data, not depend on removing characters.

Avoiding SQL injection means reviewing the paths behind each action, including alternate queries and jobs. Rate limits and browser checks can help protect those actions, but they do not replace parameter binding.

Separate SQL Structure from Request Data

Avoid SQL injection by sending a fixed SQL statement and binding every request value as data. A placeholder is the question-mark marker in the statement where a value will go. Binding means passing that value through the driver so the database treats it as data rather than SQL.

This synthetic login lookup is an example, not a customer incident. The unsafe form concatenates the email:

SELECT id FROM users WHERE email = ' + email + '

A quote can close the string and append a Boolean condition, so the lookup no longer means what the developer wrote. The safe form keeps the structure fixed:

SELECT id FROM users WHERE email = ?

The driver binds the email. The statement shape doesn't change if the address contains ' or --.

Control Keeps SQL structure fixed Safe default Usual failure
Server-side parameterized query or prepared statement Yes, when values are bound Yes Statement is concatenated before prepare
ORM named-parameter or bind API Yes, if no raw SQL is built Yes, with review of escape hatches Strings interpolated into query fragments
Server-side allow-list for type, range, or identifier Only for permitted values, not free text Additional control only Legitimate punctuation rejected or a missed identifier still concatenated
Manual escaping or sanitizing the string No as the sole control No Encoding, quotes, and dialect differences bypass the cleaner
Stored procedure that concatenates or executes dynamic SQL No No Procedure body rebuilds the statement
WAF, rate limit, or browser verification No Complementary only Request filtering never separates SQL from data

Allow-lists still help for types, ranges, sort columns, and identifiers. Real names and passwords contain punctuation, so validation can't replace binding. Before editing code, list every query behind signup, login, checkout, recovery, and promotion redemption.

Bind Every Query on the Server

A diagram illustrating how parameterized queries prevent SQL injection attacks in web application forms and databases.Edit Alt Text

A query can look safe in the browser and still be vulnerable on the server. The server must keep SQL structure separate from request data on every query path behind signup, login, checkout, recovery, and promotion redemption. The OWASP SQL Injection Prevention Cheat Sheet recommends parameterized queries as the default defense.

A placeholder marks where a value belongs in a statement. Binding sends the value separately, so the database treats it as data rather than SQL. Think of the statement as a form with fixed fields: user input fills a field, but cannot rewrite the form.

Default workflow

  1. Write each complete SELECT, INSERT, UPDATE, and DELETE with placeholders before using request values. Signup's insert, login's lookup, and checkout's order write all need this treatment.

  2. Bind each value through the database driver's parameter API on the application server. Use the placeholder style the driver documents. Bind the email, password-hash input, and cart ID as separate values.

  3. Apply the same rule to HTTP fields, cookies, headers, queued messages, internal service calls, and values read from the database. Stored data can carry a payload into a later query, a failure known as second-order injection.

  4. In an ORM, use its named-parameter or bind-variable API. Do not interpolate strings into query fragments, including through raw-query helpers.

  5. Prepare the statement before adding values. Concatenating input into WHERE id = ' and then preparing the completed string does not separate SQL from data.

  6. Bind on the server. Client-side code cannot enforce how a request is constructed, so it cannot protect the server's query.

  7. For dynamic ORDER BY clauses, table names, or column names, map the requested choice to an allow-listed identifier in code. A value placeholder cannot stand in for an identifier.

  8. Test each bound path with quotes, comment markers, Boolean conditions, and encoded variants. Confirm that the statement shape remains fixed, and include these checks in review and CI.

Practical rule: If request input can change the SQL text, the query is not bound.

Inventory before the pull request

Action Query Bind call Test name
Login SELECT id FROM users WHERE email = ? Driver parameter for email login_quote_keeps_shape
Signup INSERT INTO users (email, hash) VALUES (?, ?) Two parameter binds signup_encoded_quote_inserts_literal
Checkout UPDATE orders SET status = ? WHERE id = ? Bind status and order ID checkout_boolean_does_not_rewrite_where

Each row identifies a query path a reviewer can trace from browser action to server-side bind call. Add a row for every query that action triggers, including follow-up queries. A missing row leaves a path outside the check.

Give Each Action the Least Database Privilege

Binding removes the injection class when it's used. Least privilege limits damage when a path is missed. Those are different jobs, and both belong on signup, login, and checkout.

In a 2024 Secure by Design alert, CISA and the FBI called SQL injection a predictable and commonly exploited vulnerability, and noted it had been called an “unforgivable” vulnerability by at least 2007. The milestone they recommend isn't another one-off patch. Parameterized queries and prepared statements should be standard in the development environment, and libraries should make the safe path easier than raw SQL.

One account per action

The web account behind a login lookup should SELECT the credential fields it needs and nothing else. It shouldn't ALTER tables, read unrelated schemas, or connect as a database administrator. An insert that creates an account shouldn't also hold broad UPDATE rights across the user table.

Spell the grants in plain terms:

  • Login reader: SELECT on the credential columns used to verify the account.

  • Signup writer: INSERT on the account row, without a blanket UPDATE on every user column.

  • Checkout writer: INSERT and the specific UPDATE the order flow needs, not schema rights.

  • Migration role: schema changes, and that role isn't embedded in the application runtime.

A stored procedure can be a privilege boundary when the runtime account can only EXECUTE it. It isn't a substitute for binding if the procedure body concatenates input.

Depth that isn't another query tutorial

The same alert also calls for server-side input validation, regular patching of the database and drivers, audit-log review, penetration testing, and disabling database features the product doesn't use. A parameterized query can still be the wrong query if authorization is missing, so the account check and the SQL check stay separate. Rate limits and browser verification can slow automated abuse around the action. Neither replaces parameterization, authorization, transaction validation, or database privileges. Broader hardening for the same surfaces sits in this guide to web app security practices.

For each high-value action, which database user runs it, and which single grant could be removed?

Block Unbound Queries in Review and CI

A safe query in one file doesn't help if the next pull request ships an unbound one. The merge gate is the control.

Four gates belong on every change that touches a database from a browser action:

  • Query inventory: the changed action lists its statements.

  • Static rule: concatenation, format strings, or template calls passed into SQL or an ORM raw API fail the check.

  • Negative test: a quote, comment, Boolean, or encoded input doesn't change statement text.

  • Privilege note: the runtime database user for that action is named, and it isn't an admin.

A simple coverage target is enough to audit. Every browser-originated action that touches the database appears once, with its bind call and its test name.

What the reviewer scans

Check Failure signal Pass condition
Query inventory for the action New database call with no inventory row Signup, login, checkout, recovery, or redemption query is listed
Static ban on SQL concatenation String built into SQL or an ORM raw fragment Values reach a bind or parameter API only
Negative query-shape test Quote, comment, Boolean, or encoded input changes statement text Database or driver log shows the same statement shape
Identifier allow-list Sort, column, or table name taken from the request Request maps to a fixed identifier in code
Generated-code review Model output concatenated into SQL Same binding, authorization, and regression checks as handwritten queries
Runtime privilege note Application connects with admin or broad grants Action uses a separate least-privilege account

Generated code needs the same query inventory, parameter binding, authorization checks, and negative tests as handwritten code. Review raw-query helpers and dynamic identifiers before accepting a model suggestion.

Reviewers should look for raw-query helpers, interpolated table names, and report filters in admin tools, not only the public login form. A failing security test isn't a failing business test. A quote in an email field should be stored or matched as data, not rejected because it looks dangerous. Teams that want a sharper review habit can improve your code review process with the same kind of explicit gate.

Run the checks on the main branch and on migration code. A safe application query can sit next to an unsafe script. The build should fail on a new unbound query even when the feature tests are green.

Tips for Logins, Checkouts, and Admin Paths

A diagram illustrating safe database interaction methods including bound parameters and validation to prevent SQL injection vulnerabilities.Edit Alt Text

Once binding is the default, a few leftovers still break login, checkout, and admin paths.

LIKE filters need a bound value plus explicit wildcard handling. If the application adds % around a user string, a percent sign the user typed can widen the search. Escape wildcards in application code, then bind the finished pattern as data.

Errors shown to the browser can leak query structure. Return a generic error and a correlation ID. Keep restricted diagnostic logs with statement templates or query identifiers; redact bound values, credentials, tokens, and personal data.

A second request source can skip the helper the public form uses. Webhooks, admin exports, and batch jobs are common skips. Audit them the same way as the form.

SQL injection changes database statements. It isn't credential stuffing, trial-eligibility abuse, account takeover, or session theft. Those are different problems with different controls. Parameterization, authorization, idempotency, and database privileges remain the owner controls for the injection class. No single control here is unbypassable.

Browser Checks, CAPTCHA Alternatives, and Credential Stuffing

Credential stuffing is the automated testing of stolen username-and-password pairs against a login service. It isn't SQL injection. It also isn't trial-eligibility abuse, which is deliberate misuse of promotions or trials, and it isn't account takeover or session theft. Those last two are outcomes with their own controls. A clear account of the stuffing pattern is in this explanation of a credential stuffing attack.

The OWASP Credential Stuffing Prevention Cheat Sheet lists CAPTCHA or similar puzzles as a way to flag automated attacks and slow repeated attempts. It also says to monitor solve rates, because unusually high rates can signal automated solving or a poor experience for real users. NIST SP 800-63B requires a verifier to limit consecutive failed authentication attempts on a single account to no more than 100 unless a specific authenticator requirement says otherwise. That figure is a ceiling, not a production threshold. Teams can choose a lower limit, progressive delays, and recovery protections that don't lock out victims on purpose.

Control Targets Main tradeoff
CAPTCHA or puzzle Automated login or signup attempts Solve rates must be monitored, and friction can hurt real users
Per-account failure limit Repeated guesses against one account NIST's 100-failure figure is an upper bound, not a production threshold
Rate limit on IP or device Bursts from one source Attackers rotate addresses, so use more than one dimension
Browser verification for a chosen action Scripted traffic to sign-in, signup, or checkout without a CAPTCHA Covers only that action; not a WAF, scraping blocker, identity proof, or payment-fraud engine
Server-side replay protection Reused or retried proofs Requires server-side state; retries still need idempotency so one action isn't applied twice

Rate limits slow repeats from one source and fail when attackers rotate addresses, so per-account, per-device, and per-action signals belong together. MANDATE fits only as browser verification for a chosen high-value action, such as sign-in, account creation, or checkout, without CAPTCHAs, puzzles, or interstitials. It isn't a general scraping blocker, a WAF, a DDoS service, an identity proof, or a payment-fraud engine. Confirm current availability and capabilities in public product materials before any rollout. Start in Observe mode, compare proposed decisions with completed business events, and enforce only for that action. Application authentication, authorization, and payment-provider checks stay with their owners.

Contain a Missed Query Then Ship Again

If testing shows that request input can change SQL structure, treat it as a security incident and investigate possible data access or modification. Preserve evidence and contain the affected action while determining scope; an injection finding alone does not prove which rows were exposed.

  1. Disable or feature-flag that specific action, not the whole site, and preserve database audit logs.

  2. Rotate the application database credential for that action and revoke grants broader than the action needs.

  3. Patch the class by moving every sibling query onto bind calls, including reports and jobs in the inventory.

  4. Check whether data was read or changed, then follow the organization's disclosure duties with counsel. Scope is a legal and forensic question, not a guess in a status update.

  5. Re-run the negative tests and the CI gates before the action is turned back on.

A signup, login, checkout, reservation, or promotion action is ready only when its queries are bound, its database user is limited, and its tests fail the build if concatenation returns. Automated traffic around that action can then be watched with rate limits and, where the team wants browser evidence without a CAPTCHA, MANDATE in Observe mode before any enforcement. The product site states the verification boundary, and ongoing watch of the action belongs with security monitoring. Payment-provider fraud checks and application authorization stay with their own owners.

Don't ship because a filter, a sanitizer, or a bot control looked strict. Ship because the statement shape can't change.


MANDATE protects chosen high-value browser actions, such as sign-in, signup, and checkout, from scripted abuse and replay without CAPTCHAs, while query binding stays the control that keeps SQL fixed. It verifies the browser for that action and can start in Observe mode so teams compare decisions with real completions before any enforcement. Teams ready to review that boundary can visit MANDATE.

More from the blog

SDK INTEGRATION

What Is SDK Integration? Browser and Server Checklist

What is SDK integration? It connects your app to vendor code and a server-side check. Learn the browser-server split, rollout steps and pitfalls.

API GATEWAY RATE LIMIT

API Gateway Rate Limit: Policies, Keys and Tradeoffs

How an API gateway rate limit works, how token bucket, fixed and sliding window policies differ, and how to pick keys for browser actions.

FRAUD PREVENTION API

Fraud Prevention APIs: Where Browser Verification Fits

Compare fraud prevention API layers: identity, transaction risk, browser verification, replay protection, and safe retries for web applications.