SQL injection occurs when untrusted input changes the structure of a database query. In WordPress, this risk most often appears in custom plugins, themes, AJAX handlers, REST API endpoints, search tools, and WooCommerce extensions that construct SQL incorrectly.
The core rule is straightforward: never concatenate request data directly into a SQL query. Use prepared statements, validate values against their expected types, restrict SQL identifiers with allowlists, and check permissions before reading or changing sensitive data.
What SQL injection looks like
This query is unsafe because the value from $_GET['email'] is inserted directly into the SQL string:
$email = $_GET['email'];
$query = "SELECT * FROM {$wpdb->users} WHERE user_email = '$email'";
$results = $wpdb->get_results($query);
A visitor controls the value of email. Quotes and SQL operators in that value may alter the query’s meaning. Cleaning the input after the query has been assembled is not a dependable substitute for parameterized queries.
Depending on the vulnerable code and the database user’s permissions, SQL injection can expose private records, change content, delete data, or contribute to unauthorized access. Every custom query that handles external input deserves a security review.
Use WordPress prepared statements with $wpdb
WordPress provides $wpdb->prepare() to keep SQL code separate from its values. Put a placeholder in the query and provide the value as a separate argument:
global $wpdb;
$email = isset($_GET['email'])
? sanitize_email(wp_unslash($_GET['email']))
: '';
$sql = $wpdb->prepare(
"SELECT ID, user_email FROM {$wpdb->users} WHERE user_email = %s",
$email
);
$user = $wpdb->get_row($sql);
Use %s for strings, %d for integers, and %f for floating-point values. The placeholder does not make arbitrary SQL safe; it safely represents a value within an otherwise controlled query.
For more detail on placeholders, table prefixes, CRUD operations, and debugging, see how to safely use $wpdb in WordPress.
Validate integers and restrict sorting options
Convert numeric input to the expected type and use the matching placeholder. Do not place an integer inside a quoted string just because it came from a request:
$post_id = isset($_GET['post_id']) ? absint($_GET['post_id']) : 0;
$sql = $wpdb->prepare(
"SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = %s",
$post_id,
'_custom_status'
);
$value = $wpdb->get_var($sql);
Placeholders are intended for values, not table names, column names, or sort directions. If a request can choose an ORDER BY column or direction, select it from a fixed allowlist:
$allowed_orderby = array('post_date', 'post_title', 'ID');
$orderby = isset($_GET['orderby']) ? sanitize_key($_GET['orderby']) : 'post_date';
$orderby = in_array($orderby, $allowed_orderby, true) ? $orderby : 'post_date';
$allowed_order = array('ASC', 'DESC');
$order = isset($_GET['order']) ? strtoupper(sanitize_key($_GET['order'])) : 'DESC';
$order = in_array($order, $allowed_order, true) ? $order : 'DESC';
$query = "SELECT ID, post_title FROM {$wpdb->posts} ORDER BY {$orderby} {$order}";
The query is safe here because both identifiers come from server-side lists rather than being accepted as arbitrary SQL.
Know the difference between validation, sanitization, and escaping
These techniques address different problems:
- Validation checks whether a value matches the application’s requirements, such as being a positive integer or an approved status.
- Sanitization removes or transforms unwanted characters before processing. Examples include
sanitize_text_field()andsanitize_email(). - Escaping protects data when it is output in a particular context, such as HTML, an attribute, a URL, or JavaScript.
- Prepared statements separate SQL instructions from SQL values and are the primary defense against SQL injection.
For values with a defined set of options, validate against that set instead of accepting any string:
$allowed_statuses = array('pending', 'approved', 'rejected');
$status = isset($_POST['status'])
? sanitize_key(wp_unslash($_POST['status']))
: '';
if (!in_array($status, $allowed_statuses, true)) {
wp_die('Invalid status.');
}
Protect write operations with nonces and capabilities
A prepared statement protects the query, but it does not establish whether the current user is allowed to perform the operation. Admin forms, AJAX actions, and REST endpoints also need request and authorization checks:
if (!current_user_can('manage_options')) {
wp_die('You are not allowed to perform this action.');
}
check_admin_referer('update_custom_record');
A nonce helps reduce cross-site request forgery, but it is not an authorization mechanism. Keep the capability check. For REST API routes, define an appropriate permission_callback and validate request data before running a query.
Prefer WordPress APIs when they fit
Raw SQL is not always necessary. APIs such as get_posts(), WP_Query, get_users(), update_post_meta(), and the Settings API can reduce the database code your project has to maintain.
For WooCommerce, use its product, order, and CRUD APIs where practical. Direct table edits may bypass application rules and create compatibility problems, even when the SQL itself is parameterized.
Review custom code and test on staging
Search the codebase for $wpdb->query(), get_results(), get_row(), and get_var(). Review every query that handles data from $_GET, $_POST, cookies, headers, REST requests, or AJAX payloads.
- Are all variable values passed through
$wpdb->prepare()? - Are numeric fields converted with functions such as
absint()? - Are column names, sort directions, and table choices restricted by allowlists?
- Are permissions checked before sensitive reads and writes?
- Are database errors logged privately rather than displayed to visitors?
Test changes on a staging copy, not on a live store. A staging workflow such as creating a WordPress staging copy in cPanel gives you a safer place to review query behavior and error handling.
For a complex plugin, custom integration, or WooCommerce feature, a full stack WordPress developer can also review query construction, authorization, and data handling together rather than treating SQL injection as an isolated issue.
Common mistakes to avoid
Relying on addslashes()
Manual escaping is easy to apply inconsistently and does not address every SQL context. Use prepared statements instead.
Assuming sanitize_text_field() is enough
Sanitization may make input cleaner, but it does not make a dynamically assembled SQL query safe. Parameterize values at the database boundary.
Building an IN clause from raw input
When querying multiple IDs, convert each value to an integer and create one placeholder per ID:
$ids = isset($_GET['ids']) ? (array) $_GET['ids'] : array();
$ids = array_filter(array_map('absint', $ids));
if ($ids) {
$placeholders = implode(', ', array_fill(0, count($ids), '%d'));
$sql = $wpdb->prepare(
"SELECT ID FROM {$wpdb->posts} WHERE ID IN ($placeholders)",
$ids
);
$rows = $wpdb->get_results($sql);
}
Do not join unvalidated request values directly into the IN clause.
Frequently asked questions
Does WordPress automatically prevent SQL injection?
WordPress core follows secure database practices, but custom themes, plugins, snippets, and third-party extensions can still introduce vulnerable queries. Custom database code must be reviewed separately.
Is output escaping enough to stop SQL injection?
No. Output escaping protects a particular display context. SQL values need to be passed through prepared statements before the query executes.
Can placeholders be used for table names?
No. Placeholders are for values. Table and column names should come from trusted configuration or strict allowlists. Use the correct WordPress table prefix rather than accepting a prefix from a request.
Should database errors be disabled in production?
Detailed database errors should not be shown to visitors. Log useful diagnostics privately and use staging when debugging query failures.
Conclusion
Preventing SQL injection in WordPress requires consistent query discipline: prepare values with $wpdb->prepare(), validate expected types, allowlist identifiers, check capabilities, and use WordPress or WooCommerce APIs where they provide the required operation. For broader guidance on building and reviewing WordPress applications, see this full stack WordPress development guide.
