Most advice on SQL injection prevention is stuck in an older application model. It says: use an ORM, parameterise your queries, stop concatenating strings, and you're done.
That advice is still necessary. It just isn't sufficient for modern stacks.
If you build on Supabase, Firebase, edge functions, mobile clients, or AI-assisted app builders, your attack surface isn't only the SQL string. It's also RLS policies, RPC endpoints, leaked API keys, permissive service roles, and dynamic query paths hidden behind “safe” abstractions. You can block classic payloads and still leak private data because your access logic is wrong.
That's the gap many teams miss. They secure the query and forget to prove the policy.
Why Your Framework Is Not Enough
A framework can make unsafe code less likely. It can't make bad security assumptions disappear.
Supabase gives you PostgreSQL, auth, policies, functions, and a polished developer experience. Firebase gives you client-friendly access patterns, rules, functions, and rapid iteration. Prisma, Drizzle, and similar tools reduce the amount of hand-written SQL. All of that helps. None of it guarantees real SQL injection prevention.
The common failure is simple. Developers hear “parameterised queries” and translate that into “database problem solved”. In practice, modern cloud apps fail in places that traditional SQLi tutorials barely mention.
According to a 2024 NCSC report on cloud application breaches and RLS misconfiguration trends, 68% of cloud application breaches in the UK involved misconfigured access controls or RLS rules rather than classic SQL injection, while 92% of UK developer security training materials still prioritise legacy SQLi prevention.
That mismatch matters because cloud stacks blur responsibilities:
- The client can call more than you think if a key or endpoint leaks.
- RPC functions can execute privileged logic even when your normal table access looks locked down.
- RLS can be syntactically valid and still logically wrong.
- Dynamic SQL can sneak back in through sorting, filtering, reporting, and admin features.
Traditional SQL injection prevention stops hostile input from changing the query structure. It does nothing to prove that an authorised-looking request should see the data it gets.
This is why “use an ORM” is incomplete advice. ORMs help with value binding. They don't validate your authorisation model. They don't tell you whether a supposedly private RPC is callable from the client. They don't prove that one tenant can't read another tenant's rows.
For modern apps, SQL injection prevention has to include both query safety and data access correctness. If you only test one, you'll miss the breach that causes harm.
The Unskippable Foundation Secure Coding Patterns
The basics still carry most of the load. You can't skip them just because your stack is newer.

According to OWASP UK benchmark data on prepared statements and mobile breach patterns, applications deploying prepared statements with bound, typed parameters achieve a 99.8% success rate in blocking injection payloads. The same benchmark notes that the dynamic query construction trap accounts for 28% of successful breaches in UK-based mobile apps.
That tells you two things. First, prepared statements work. Second, developers still break them by rebuilding SQL strings around the edges.
What safe code looks like
Unsafe Node.js with pg:
const query = `SELECT * FROM users WHERE email = '${email}'`;
const result = await pool.query(query);
Safe version:
const query = `SELECT * FROM users WHERE email = $1`;
const result = await pool.query(query, [email]);
The difference isn't cosmetic. In the safe version, the driver sends the SQL structure and the user-supplied value separately. The database treats email as data, not executable SQL.
Typed parameters matter too:
const query = `SELECT * FROM orders WHERE user_id = $1 AND created_at >= $2`;
const result = await pool.query(query, [Number(userId), startDate]);
If the input should be numeric, pass a number. If it should be a date, validate and bind a date-shaped value. Don't let everything stay as a loose string until it hits the database.
Where ORMs help and where they don't
Prisma, Drizzle, SQLAlchemy, Hibernate, and similar tools usually parameterise generated queries by default. That's why they reduce risk.
But every ORM has escape hatches:
- Raw query APIs for reporting, search, or migrations
- Dynamic filters built from request objects
- Interpolated sorting or column names
- Custom SQL inside RPCs or stored functions
A lot of teams are safe in normal CRUD flows and vulnerable in one “temporary” analytics endpoint.
Here's the classic trap:
const query = `SELECT * FROM products ORDER BY ${sort}`;
You can't parameterise identifiers like column names in the same way you parameterise values. So developers often go back to string interpolation.
The fix is a hardcoded allow-list:
const allowedSorts = {
created_at: 'created_at',
price: 'price',
name: 'name'
};
const sortColumn = allowedSorts[sort];
if (!sortColumn) {
throw new Error('Invalid sort field');
}
const query = `SELECT * FROM products ORDER BY ${sortColumn} LIMIT $1`;
const result = await pool.query(query, [limit]);
Non-negotiable coding rules
Use these as team standards:
-
Bind values every time
For filters, inserts, updates, login checks, background jobs, and admin tools, use bound parameters through the driver or ORM. -
Treat raw SQL as hazardous
If you need raw SQL, require review. The same goes for query builders that accept arbitrary fragments. -
Never concatenate identifiers from user input
ForORDER BY, table names, or column names, map user input to a fixed internal allow-list. -
Type inputs before query execution
Convert and validate early. Don't pass unbounded request strings through multiple layers.
For a good baseline to encode these rules into team habits, I'd point developers to practical secure coding standards for modern apps.
Practical rule: If a reviewer sees string interpolation anywhere near SQL, database filters, or RPC query assembly, that code should stop moving until someone proves it's safe.
Strengthening Your Database Defences
Application code shouldn't be your only line of defence. If the app makes a mistake, the database should still make the blast radius smaller.

The NCSC reported in 2024 that, in APT incidents involving SQL injection as the initial database compromise path, SQL injection was the primary mechanism for initial database compromise in 18% of the incidents investigated. That's a reminder that foundational controls still matter because attackers keep finding workloads where they were never enforced properly.
Least privilege beats cleanup later
If your web app connects as a highly privileged database user, one flaw becomes a full-database problem.
Use separate roles for separate jobs:
| Role | What it should do | What it should never do | |---|---|---| | Public app role | Read and write only needed tables through approved paths | Alter schema, manage users, access unrelated tables | | Admin back-office role | Perform restricted operational actions | Serve general traffic | | Migration role | Apply schema changes during deployment | Power live application requests |
On Supabase, this matters even more because teams often mix client-facing access with privileged server operations. If an edge function or backend service uses a highly privileged key, keep that boundary tight and visible.
Stored procedures can help or hurt
Stored procedures and database functions are useful when you need:
- Encapsulated business logic
- A stable interface for application code
- Controlled access to sensitive operations
They become dangerous when developers hide dynamic SQL inside them. A function that concatenates a table name, filter, or sort expression is still vulnerable even if the application itself looks clean.
For RPC-style patterns, keep these habits:
- Use strict parameter typing
- Avoid dynamic SQL unless there's no alternative
- Review
SECURITY DEFINERfunctions carefully - Assume every callable function is part of your external attack surface
RLS is a security control, not a checkbox
Row-Level Security is one of the strongest features in PostgreSQL and one of the easiest to misunderstand.
A policy can be present and still be wrong. That's the part many teams learn late. They add “authenticated users only” logic and assume tenancy is enforced. Then a join, policy condition, or permissive function allows cross-user reads.
RLS should answer precise questions:
- Can user A read only their own rows?
- Can user A update only records they own?
- Can anonymous users call any function that touches protected data?
- Do service roles bypass controls in places you didn't intend?
RLS that hasn't been tested with hostile inputs and cross-user scenarios is only a theory.
A sound defence model looks like this:
- Application code uses parameter binding and avoids interpolation.
- Database roles restrict what the app can do even after a coding mistake.
- Stored procedures or RPCs narrow how sensitive operations are performed.
- RLS policies enforce who can see or modify each row.
If you want a practical complement to that model, these database security best practices for app teams are a sensible reference point.
Validating and Sanitising User Input Correctly
Input validation is where many teams swing from one bad extreme to another. They either trust everything, or they lock fields down so aggressively that normal users can't enter legitimate data.
The right approach is context-aware. Validation decides what is allowed. Sanitisation adjusts content when you need to preserve text safely. Those aren't the same task.
According to CPNI guidance on allow-list validation, query parameterisation, and common implementation failures, combining allow-list validation with parameterised queries raises SQL injection prevention success to 99.95% and neutralises 98% of automated attack vectors. The same data notes a 35% failure rate in UK indie-hacker projects when developers misuse this logic.
Validate structure, don't guess intent
For structured fields, use tight rules.
Good examples:
- Dates should match the exact format your application accepts.
- Numeric IDs should be parsed as numbers, not left as arbitrary strings.
- Status fields should come from a fixed set like
draft,paid, orarchived.
Example patterns:
const isoDate = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
const numericId = /^[0-9]+$/;
Bad examples:
- Accepting “whatever comes in” and hoping the ORM saves you
- Block-listing a few suspicious characters and calling it security
- Reusing the same validator for every field type
Free text needs different handling
Comments, bios, support messages, and article content aren't the place for strict alphanumeric allow-lists. That's how teams break valid user input and still don't solve the core security problem.
For free-text fields:
- Validate length and expected encoding
- Store the content as data through parameterised queries
- Strip or normalise dangerous metacharacters only when your context requires it
- Escape on output for the relevant sink, especially HTML rendering
That last point matters. SQL injection prevention and XSS prevention overlap in practice because developers often try to “sanitise once” for every problem. That usually fails.
Here's a useful perspective:
| Input type | Primary control | Example | |---|---|---| | IDs, dates, enums | Strict allow-list validation | Parse as number, regex for date, fixed enum set | | Search text | Length limits plus parameter binding | Accept text, don't splice into SQL | | Rich comments or bios | Minimal structural validation, context-specific output encoding | Preserve legitimate content |
Common mistake: teams apply “only letters and numbers” to free text, damage the user experience, then still interpolate a sort field elsewhere.
If you want a concrete reminder of how avoidable this can be, examining the São Vicente tax leak is worth your time. It's the kind of incident that shows how basic failures in input handling and query construction can cascade into full data exposure.
Automating Your Security From CI to Production
Manual review catches obvious mistakes. It doesn't scale well, and it won't reliably catch policy flaws that only show up under realistic abuse cases.
That's why SQL injection prevention has to become part of delivery, not a separate ritual before release.

The NCSC's 2024 cyber threats reporting found that automated SQL injection scanning was used in UK web application attacks far more often than defensive CI testing. Specifically, automated SQL injection scanning tools were used in 68% of UK-based web application attacks, yet only 25% of UK organisations had automated security testing in their CI/CD pipelines.
Attackers automate because it works. Defenders need to do the same.
What should run in CI
A useful pipeline for modern app teams includes different kinds of checks because each catches a different class of failure.
-
SAST for source-level mistakes
Flag raw SQL interpolation, dangerous query builder usage, unsafe function patterns, and missing validation. -
DAST for exposed behaviour
Hit running environments and observe whether endpoints, forms, APIs, and functions behave like vulnerable surfaces. -
Secrets scanning for frontend and mobile builds
Catch hardcoded keys, tokens, and backend credentials before they ship in web bundles or APK/IPA artefacts. -
Policy-aware checks for database access logic
Verify that rows, functions, and storage paths aren't exposed to the wrong users.
If you're comparing products, this roundup of best vulnerability scanners reviewed is a reasonable starting point for understanding the trade-offs between general-purpose scanners and more specialised tools.
Why RLS fuzzing belongs in the pipeline
Classic scanners are good at finding classic bugs. They're weaker at proving an authorisation leak inside a valid-looking workflow.
That's where RLS logic fuzzing matters.
Instead of asking, “Can I inject SQL here?”, fuzzing asks things like:
- If I authenticate as user B, can I read user A's rows?
- Can I write records that I shouldn't own?
- Does a supposedly internal RPC return protected data with crafted parameters?
- Does a service role or public function bypass policy checks indirectly?
Static analysis often misses this because the vulnerability isn't a bad string. It's bad logic.
Here's a pragmatic test matrix teams should automate:
| Surface | What to test | Failure signal | |---|---|---| | Tables behind RLS | Cross-user read and write attempts | Data leakage or unexpected mutation | | RPCs and edge functions | Anonymous and low-privilege invocation | Sensitive output or privileged action | | Mobile and web bundles | Embedded keys and secrets | Client can reach privileged backend paths | | Error handling | Invalid payloads and malformed requests | Schema details or query hints returned |
Production still needs guardrails
CI is where you prevent regressions. Production is where you detect drift.
Good habits include:
-
Scanning every release artefact
Web bundles, mobile builds, edge functions, and public endpoints should all be checked, not just the backend repo. -
Watching for security regressions
A safe RLS policy today can become unsafe after a schema change or a “temporary” RPC addition. -
Keeping error responses generic
Attackers learn quickly from verbose failures. Your users don't need table names, column names, or raw SQL errors.
For teams that need cloud-database-specific checks, a specialised SQL injection vulnerability scanner for modern stacks is far more useful than a scanner that only knows how to poke old-school form fields.
Security automation isn't only about finding obvious injections earlier. It's about continuously proving that the system still enforces the access rules you think it does.
Frequently Asked Questions about SQLi Prevention

Is an ORM enough
No. It reduces risk because it usually parameterises values by default. It stops being enough the moment you use raw SQL, interpolate sort or filter fragments, or call a function that builds dynamic SQL internally.
Is an RLS bypass the same as SQL injection
No. Classic SQL injection changes the structure of a query by abusing unsafe input handling. An RLS bypass returns or modifies data because the access policy is wrong, even when the query itself is properly parameterised.
Those are different bugs, but the outcome can look similar to the victim. Private data leaks either way.
What about Firebase or other NoSQL systems
The exact syntax changes, but the principle doesn't. If untrusted input can reshape a query or if access rules are too permissive, you still have an injection or authorisation problem. Don't assume “NoSQL” means “no injection class of issues”.
How should I handle dynamic table or column names
Never pass user input directly into identifiers. Map allowed external values to a fixed internal set, then reject anything else. If the input doesn't match the allow-list, fail the request and log it for review.
Safe dynamic SQL isn't “escaped user input”. It's user input selecting from options you already approved in code.
Building a Resilient Security Posture
SQL injection prevention works best when you stop treating it like a single coding trick.
Start with the foundation. Bind parameters, type inputs, and remove string-built queries from normal application paths. Then harden the database itself with least privilege, careful function design, and RLS policies that enforce row access where the data lives.
After that, validate inputs by context instead of applying one blunt rule to every field. Finally, automate the checks that humans miss, especially policy fuzzing, RPC exposure testing, and secret scanning for mobile and frontend builds.
When prevention fails, response matters too. Teams that want a plain-English refresher on breach impact and containment can review GoSafe data breach solutions as part of their broader incident-readiness planning.
The main shift is this. Don't ask only whether your query is safe. Ask whether your system can prove that the right user, through the right path, got the right data and nothing more.
If you're building on Supabase, Firebase, or mobile backends and want that proof without stitching together multiple tools, AuditYour.App is built for exactly that workflow. It scans for exposed RLS rules, unprotected RPCs, leaked API keys, hardcoded secrets, and real read or write leakage so you can catch modern SQLi-adjacent failures before release.
Scan your app for this vulnerability
AuditYourApp automatically detects security misconfigurations in Supabase and Firebase projects. Get actionable remediation in minutes.
Run Free Scan