Query Parameterization
Introduction to Query Parameterization
SQL Injection is one of the most dangerous web vulnerabilities. So much so that it was the #1 item in both the OWASP Top 10 2013 version, and 2017 version. As of 2021, it sits at #3 on the OWASP Top 10.
It represents a serious threat because SQL Injection allows evil attacker code to change the structure of a web application's SQL statement in a way that can steal data, modify data, or potentially facilitate command injection to the underlying OS.
This cheat sheet is a derivative work of the SQL Injection Prevention Cheat Sheet.
Parameterized Query Examples
SQL Injection is best prevented through the use of parameterized queries. The following chart demonstrates, with real-world code samples, how to build parameterized queries in most of the common web languages. The purpose of these code samples is to demonstrate to the web developer how to avoid SQL Injection when building database queries within a web application.
Please note, many client side frameworks and libraries offer client side query parameterization. These libraries often just build queries with string concatenation before sending raw queries to a server. Please ensure that query parameterization is done server-side!
Prepared Statement Examples
Using Java built-in feature
String custname = request.getParameter("customerName");
String query = "SELECT account_balance FROM user_data WHERE user_name = ? ";
PreparedStatement pstmt = connection.prepareStatement( query );
pstmt.setString( 1, custname);
ResultSet results = pstmt.executeQuery( );
Using Java with Hibernate
// HQL
@Entity // declare as entity;
@NamedQuery(
name="findByDescription",
query="FROM Inventory i WHERE i.productDescription = :productDescription"
)
public class Inventory implements Serializable {
@Id
private long id;
private String productDescription;
}
// Use case
// This should REALLY be validated too
String userSuppliedParameter = request.getParameter("Product-Description");
// Perform input validation to detect attacks
List<Inventory> list =
session.getNamedQuery("findByDescription")
.setParameter("productDescription", userSuppliedParameter).list();
// Criteria API
// This should REALLY be validated too
String userSuppliedParameter = request.getParameter("Product-Description");
// Perform input validation to detect attacks
Inventory inv = (Inventory) session.createCriteria(Inventory.class).add
(Restrictions.eq("productDescription", userSuppliedParameter)).uniqueResult();
Using .NET built-in feature
String query = "SELECT account_balance FROM user_data WHERE user_name = ?";
try {
OleDbCommand command = new OleDbCommand(query, connection);
command.Parameters.Add(new OleDbParameter("customerName", CustomerName Name.Text));
OleDbDataReader reader = command.ExecuteReader();
// …
} catch (OleDbException se) {
// error handling
}
Using ASP .NET built-in feature
string sql = "SELECT * FROM Customers WHERE CustomerId = @CustomerId";
SqlCommand command = new SqlCommand(sql);
command.Parameters.Add(new SqlParameter("@CustomerId", System.Data.SqlDbType.Int));
command.Parameters["@CustomerId"].Value = 1;
Using Ruby with ActiveRecord
## Create
Project.create!(:name => 'owasp')
## Read
Project.all(:conditions => "name = ?", name)
Project.all(:conditions => { :name => name })
Project.where("name = :name", :name => name)
## Update
project.update_attributes(:name => 'owasp')
## Delete
Project.delete(:name => 'name')
Using Ruby built-in feature
insert_new_user = db.prepare "INSERT INTO users (name, age, gender) VALUES (?, ? ,?)"
insert_new_user.execute 'aizatto', '20', 'male'
Using PHP with PHP Data Objects
$stmt = $dbh->prepare("INSERT INTO REGISTRY (name, value) VALUES (:name, :value)");
$stmt->bindParam(':name', $name);
$stmt->bindParam(':value', $value);
Using Cold Fusion built-in feature
<cfquery name = "getFirst" dataSource = "cfsnippets">
SELECT * FROM #strDatabasePrefix#_courses WHERE intCourseID =
<cfqueryparam value = #intCourseID# CFSQLType = "CF_SQL_INTEGER">
</cfquery>
Using PERL with Database Independent Interface
my $sql = "INSERT INTO foo (bar, baz) VALUES ( ?, ? )";
my $sth = $dbh->prepare( $sql );
$sth->execute( $bar, $baz );
Using Rust with SQLx
// Input from CLI args but could be anything
let username = std::env::args().last().unwrap();
// Using build-in macros (compile time checks)
let users = sqlx::query_as!(
User,
"SELECT * FROM users WHERE name = ?",
username
)
.fetch_all(&pool)
.await
.unwrap();
// Using built-in functions
let users: Vec<User> = sqlx::query_as::<_, User>(
"SELECT * FROM users WHERE name = ?"
)
.bind(&username)
.fetch_all(&pool)
.await
.unwrap();
Stored Procedure Examples
The SQL you write in your web application isn't the only place that SQL injection vulnerabilities can be introduced. If you are using Stored Procedures, and you are dynamically constructing SQL inside them, you can also introduce SQL injection vulnerabilities.
Dynamic SQL can be parameterized using bind variables, to ensure the dynamically constructed SQL is secure.
Here are some examples of using bind variables in stored procedures in different databases.
Oracle using PL/SQL
Normal Stored Procedure
No dynamic SQL being created. Parameters passed in to stored procedures are naturally bound to their location within the query without anything special being required:
PROCEDURE SafeGetBalanceQuery(UserID varchar, Dept varchar) AS BEGIN
SELECT balance FROM accounts_table WHERE user_ID = UserID AND department = Dept;
END;
Stored Procedure Using Bind Variables in SQL Run with EXECUTE
Bind variables are used to tell the database that the inputs to this dynamic SQL are 'data' and not possibly code:
PROCEDURE AnotherSafeGetBalanceQuery(UserID varchar, Dept varchar)
AS stmt VARCHAR(400); result NUMBER;
BEGIN
stmt := 'SELECT balance FROM accounts_table WHERE user_ID = :1
AND department = :2';
EXECUTE IMMEDIATE stmt INTO result USING UserID, Dept;
RETURN result;
END;
SQL Server using Transact-SQL
Normal Stored Procedure
No dynamic SQL being created. Parameters passed in to stored procedures are naturally bound to their location within the query without anything special being required:
PROCEDURE SafeGetBalanceQuery(@UserID varchar(20), @Dept varchar(10)) AS BEGIN
SELECT balance FROM accounts_table WHERE user_ID = @UserID AND department = @Dept
END
Stored Procedure Using Bind Variables in SQL Run with EXEC
Bind variables are used to tell the database that the inputs to this dynamic SQL are 'data' and not possibly code:
PROCEDURE SafeGetBalanceQuery(@UserID varchar(20), @Dept varchar(10)) AS BEGIN
DECLARE @sql VARCHAR(200)
SELECT @sql = 'SELECT balance FROM accounts_table WHERE '
+ 'user_ID = @UID AND department = @DPT'
EXEC sp_executesql @sql,
'@UID VARCHAR(20), @DPT VARCHAR(10)',
@UID=@UserID, @DPT=@Dept
END
Mitigation
Here's are the list of the mitigation to prevent vulnerable input validation:
1. Use Prepared Statements with Parameterized Queries
- Always use prepared statements in database queries to separate SQL logic from user inputs.
- Avoid dynamic query construction using string concatenation or interpolation.
2. Leverage ORM Frameworks
- Use Object-Relational Mapping (ORM) frameworks that support parameterized queries by default.
- Avoid bypassing ORM protections by executing raw queries.
3. Validate and Sanitize Inputs
- Perform input validation to ensure only expected data types and formats are accepted.
- Reject or sanitize malicious inputs before passing them to queries.
4. Avoid Using String Concatenation
- Do not construct SQL queries using user inputs directly in strings.
- Replace constructs like
SELECT * FROM users WHERE name = '" + userInput + "'
with parameterized alternatives.
5. Use Stored Procedures
- Leverage stored procedures for database operations where applicable.
- Ensure that stored procedures use parameterized inputs and are free from SQL injection vulnerabilities.
6. Implement Least Privilege for Database Accounts
- Use database accounts with minimal privileges required for application functionality.
- Restrict accounts from executing potentially dangerous commands like
DROP
,ALTER
, orTRUNCATE
.
7. Escape Special Characters When Necessary
- For database systems that do not support parameterized queries, escape special characters in inputs using trusted libraries.
- Avoid relying solely on escaping as a mitigation.
8. Enable Database-Specific Security Features
- Enable features like query whitelisting or sandboxing to limit SQL execution scope.
- Use tools provided by your database to analyze and mitigate risky queries.
9. Log and Monitor Database Activities
- Log query execution details, including parameter values, to detect suspicious activities.
- Use database monitoring tools to identify potential injection attempts.
10. Regularly Update Libraries and Drivers
- Keep database libraries and drivers up to date to benefit from security patches and improvements.
- Avoid using deprecated or unsupported APIs for database interactions.
11. Conduct Regular Security Testing
- Perform penetration tests and automated scans to identify SQL injection vulnerabilities.
- Use tools like OWASP ZAP or SQLMap to validate the effectiveness of parameterization.
12. Train Developers on Secure Query Practices
- Educate developers about SQL injection risks and the importance of parameterized queries.
- Incorporate secure coding practices into development guidelines.
References
- The Bobby Tables site (inspired by the XKCD webcomic) has numerous examples in different languages of parameterized Prepared Statements and Stored Procedures
- OWASP SQL Injection Prevention Cheat Sheet