Despite being one of the oldest known web application attacks, SQL injections continue to feature in data breach headlines, from MOVEit Transfer to Zendesk and beyond. This article discusses ways of preventing SQL injection vulnerabilities to make sure incoming attack payloads can’t get a foothold in your systems.
It’s nearly 2025, yet SQL injection still ranks as the #3 most dangerous application security weakness. Despite tech industry hype suggesting that SQL databases are on their way out and it’s only a matter of time before they’re all replaced by NoSQL, GraphQL, or some other QL, SQL injections continue to feature in data breaches year after year. Clearly, relational databases are far from dead, so knowing how to prevent SQL injection is as important as ever.
Before uttering the inevitable words “parameterized queries,” let’s make one thing clear: there is nothing you can do to stop SQL attack attempts against your web apps and APIs. The vast majority of malicious probes are fully automated, so you will see bots poking around your apps with malicious SQL code payloads all the time. What you can and should do is systematically fix and prevent SQL injection vulnerabilities that would allow even one of those relentless attacks to succeed.
So, while you can’t prevent the attacks, it’s definitely possible to fix and avoid the vulnerabilities—and that’s best done with a layered approach.
Like other injection vulnerabilities, SQL injection is possible when you mix unsanitized user input with application code, in this case when building SQL statements. SQLi prevention methods revolve around different ways to perform all the database commands required by application logic but without sending raw user-controlled input as part of the query.Â
The main secure coding practice when working with SQL queries is to avoid directly concatenating strings into a query if you can possibly help it. Parameterized queries let you do that using what are essentially query templates, where the statement itself is specified in advance (hence the other name—prepared statements), and the input data is only represented by placeholders.
To illustrate, here’s a quick and insecure way to build a query that fetches customer data by customer ID. Deployed like this, the application would be vulnerable to SQL injection via input fields that end up in the userInput
string, allowing attackers to input SQL commands for the database:
sqlQuery = "SELECT * FROM customers WHERE customerId = '" + userInput + "';"
With parameterized queries, you avoid the whole insecure and error-prone business of manually sticking strings together. Here’s a Python example for executing the same SELECT
statement more securely (where cursor
is a database access cursor over some existing connection):
cursor.execute("SELECT * FROM customers WHERE customerId = %s;", userInput)
This becomes especially important when dealing with longer and more complex queries with multiple input parameters. Prepared statements allow you to clearly separate the query logic from the input data, improving security and making it easier to work with database queries overall.
In many cases, you can completely avoid building queries yourself by calling existing data access procedures stored on the database server. Instead of having your application assemble and send an SQL query to fetch customer data by ID, you might simply call a stored procedure like getCustomerById()
that you previously defined in your database.
While they are convenient and also good for code maintenance because your application doesn’t need to know the exact current database structure each time, stored procedures are not an automatic cure for SQLi because they, too, need to be written securely. If your stored procedure just performs string concatenation internally, it’s no more secure than putting the query together yourself. The procedures also need maintenance to keep up with any database changes.
Object-relational mapping (ORM) frameworks offer a way to completely get away from working with raw SQL, especially for object-heavy programming languages such as Java.
Â
With ORM, internal application objects are automatically stored in a database, in theory eliminating the need to explicitly execute queries and thus expose potential injection points. That said, keep in mind that ORM comes with its own limitations, plus using it is a software design decision, not a security measure in and of itself.
As part of general anti-injection hygiene, it’s always a good idea to put user-controllable inputs through some level of validation and sanitization, with value whitelisting where possible. For example, if you have a Boolean parameter for your SQL query and you’re receiving the value as a literal true
or false
string, you should do a conditional check on that value and insert your own true or false based on that (instead of directly plugging the input into your query and hoping it’s not an SQL payload).
When you know you’re only expecting some limited set of characters, it never hurts to validate against that as well—but keep in mind that this type of filtering is never a standalone defense when it comes to injection. In other words, while accepting only alphanumeric characters seems a reasonable additional security measure, be aware that merely rejecting special characters doesn’t magically protect you from SQL injection.
See the Invicti SQL injection cheat sheet for dozens of SQL injection examples and to get some idea of the countless attack payloads for getting past filters and firewalls.
Prevention is always better than cure, so not having exploitable SQLi vulnerabilities is best practice number one, two, and three. Even so, there are some additional things you can do at the level of design, configuration, and protection to further reduce the risk of successful SQL injection and minimize the impact of any attack that does succeed.
Always a recommended practice for more reasons than security, the principle of least privilege is especially important for mitigating the impact of SQL injections. This applies at multiple levels, starting with the fundamental rule of not running production software with admin permissions—from the database server and the application itself to ensuring your app connects to the database as a restricted user without access to tables it doesn’t need.
Well-designed applications, APIs, and databases are also less at risk of extensive data breaches caused by unauthorized access, again by working only with the minimum data sets required at any given time. A more precise query to a more specialized table is less of a risk than grabbing SELECT * FROM users
each time and then only filtering data in the application.
Detecting and preventing SQL injection vulnerabilities is especially important for APIs since API endpoints are often targeted first as the direct gatekeepers of sensitive data.
Â
Learn more about API vulnerability testing in the real world.
While having a WAF to stop incoming attacks before they get to your application sounds like a no-brainer and it’s always useful to have that extra layer of protection, once again this is filtering, and filtering is never fully waterproof. A firewall may weed out the most trivial single quote probes like ' OR 1=1--
that are checking for easy authentication bypasses in your login forms, but you can’t rely on it to stop more advanced SQLi attacks, especially if a more inquisitive hacker starts investigating.
To ensure that SQLi prevention is a routine and unavoidable part of your development and security processes, follow these general cybersecurity practices: