Overview
Database operations are a critical part of many WordPress plugins. Plugins commonly store settings, retrieve content, update records, and process user-generated data.
Because database queries often involve external input, insecure query construction can expose websites to SQL injection vulnerabilities.
WordPress provides the $wpdb->prepare() method as the recommended way to safely handle dynamic SQL queries.
The core principle is:
Never trust user input. Always separate SQL commands from data values.
Prepared queries ensure that user-provided values are treated as data instead of executable SQL instructions.
Why SQL Injection Happens
SQL injection occurs when untrusted values are directly inserted into SQL statements.
A vulnerable query may allow an attacker to manipulate the database command by providing specially crafted input.
Avoid building queries by combining SQL strings with variables.
Unsafe example:
$wpdb->get_row( "SELECT * FROM table WHERE id = $id" );
The variable is inserted directly into the SQL statement, which can allow attackers to influence the query structure.
Use Placeholders Correctly
$wpdb->prepare() uses placeholders to safely insert values into SQL queries.
The most commonly used placeholders are:
%sfor strings.%dfor integers.%ffor floating-point numbers.
Always choose the placeholder that matches the expected data type.
Examples:
- Database IDs should normally use
%d. - Names or text values should normally use
%s. - Decimal values should normally use
%f.
Recommended Query Structure
A secure prepared query follows these steps:
- Write the SQL query using placeholders.
- Pass the query to
$wpdb->prepare(). - Provide values as separate arguments.
- Execute the prepared query using a WordPress database method.
Prepared queries can be used with methods such as:
$wpdb->get_row()$wpdb->get_results()$wpdb->get_var()$wpdb->query()
Example:
global $wpdb;
$id = absint( $_POST['user_id'] );
$query = $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}users WHERE ID = %d",
$id
);
$user = $wpdb->get_row( $query );
The SQL structure remains fixed while the value is safely inserted separately.
Validate Data Before Preparing
Although $wpdb->prepare() protects the SQL query, validating input before preparing the query is still recommended.
Validation ensures that data matches the expected format before it reaches the database layer.
Examples:
- Use
absint()for positive integer values. - Confirm expected formats before storing information.
- Reject invalid values before processing database operations.
Validation and preparation have different purposes:
- Validation confirms that data is acceptable.
- Preparation protects the SQL query from injection.
Both practices should be used together.
Do Not Replace Prepared Queries with esc_sql()
A common mistake is using escaping functions such as esc_sql() as a substitute for $wpdb->prepare().
Escaping individual values does not provide the same protection as prepared statements.
$wpdb->prepare() is safer because it separates the SQL command from the supplied data values.
This prevents user input from changing the meaning of the SQL statement.
Use Preparation for All Dynamic Queries
Any SQL query containing external or dynamic values should use $wpdb->prepare().
This includes:
SELECTqueries.INSERTqueries.UPDATEqueries.DELETEqueries.
Database queries should never depend on manually constructed SQL strings containing user input.
Common Mistakes
Direct Variable Injection
Avoid inserting variables directly into SQL queries.
Incorrect:
"SELECT * FROM users WHERE id = $id"
Correct:
"SELECT * FROM users WHERE id = %d"
The value should be provided separately through $wpdb->prepare().
Incorrect Placeholder Types
Using the wrong placeholder can cause unexpected results.
Examples:
- Use
%dfor integer values. - Use
%sfor text values. - Use
%ffor decimal numbers.
Matching placeholders correctly improves reliability and prevents incorrect query behavior.
Database Security Checklist
Before releasing a WordPress plugin, confirm that:
- All dynamic SQL queries use
$wpdb->prepare(). - User input is validated before database operations.
- Placeholders match expected data types.
- SQL queries do not contain directly inserted variables.
- Escaping functions are not incorrectly used as replacements for prepared statements.
Summary
Secure WordPress database development requires treating all external values as potentially unsafe.
A secure plugin should:
- Always use
$wpdb->prepare()for dynamic SQL queries. - Use placeholders instead of direct variable insertion.
- Match placeholders to expected data types.
- Validate input before database operations.
- Avoid using escaping functions as a replacement for preparation.
- Apply prepared queries to all database operations involving external data.
Following these practices helps WordPress plugins reduce SQL injection risks and maintain secure, reliable database interactions.
Frequently Asked Questions
- Why should I use $wpdb->prepare() in WordPress plugins?
- $wpdb->prepare() helps prevent SQL injection by separating SQL query structure from user-provided values. It safely formats values before they are sent to the database.
- Can I directly insert variables into SQL queries if I sanitize them first?
- No. Sanitization alone does not replace prepared queries. Variables should never be inserted directly into SQL statements because this can still create SQL injection risks.
- What placeholders are available in $wpdb->prepare()?
- The most common placeholders are %s for strings, %d for integers, and %f for floating-point numbers. The placeholder should match the expected data type.
- Is esc_sql() a replacement for $wpdb->prepare()?
- No. esc_sql() should not be used as a replacement for prepared queries. $wpdb->prepare() provides stronger protection by keeping SQL commands and data values separate.
- Should input validation happen before using $wpdb->prepare()?
- Yes. Validation improves data quality and ensures values match expectations before database operations. For example, use absint() when expecting numeric identifiers.