WordPress wpdb::prepare Example: Prevent SQL Injection in Custom Queries
Learn how to use wpdb::prepare in WordPress custom queries with safe placeholders, LIKE searches, IN clauses, validation, and SQL injection prevention checks.
Published
April 16, 2026
Reading Time
5 min read
Updated
May 4, 2026

Hardening Notes
Baselines, access reduction, and default settings that stand up in production.
Best For
Teams preparing, launching, or maintaining WordPress as a backend service in a production stack.
Primary Topics
Editorial Focus
Control Ledger: Baselines, access reduction, and default settings that stand up in production. Updated on May 4, 2026.
Full Report
Last reviewed: April 16, 2026
WordPress SQL injection prevention is a high-intent search because developers eventually reach for $wpdb when built-in APIs do not fit. The risky moment is not using $wpdb itself. The risk is building SQL strings with request data, shortcode attributes, AJAX parameters, or admin form values without prepared placeholders.
This guide shows how to use $wpdb->prepare() for safe custom queries, when to avoid custom SQL entirely, and which validation steps matter before code reaches production. It is written for practical plugin work, not abstract database theory.
wpdb prepare example: what must be protected
A useful wpdb prepare example should protect every value that enters custom SQL, not just obvious search fields. Email addresses, IDs, shortcode attributes, AJAX parameters, report filters, dates, and status values all need a placeholder or a stricter WordPress API. Prepared SQL also does not decide whether the current user should run the query, so pair it with capability checks and nonce checks on privileged admin or AJAX paths.
Who this is for
This article is for WordPress plugin developers, agencies maintaining custom code, and operators auditing plugins that store or query custom tables. If a feature receives input and then builds SQL, it needs a careful review.
Use WordPress APIs before custom SQL
Before writing SQL, check whether a higher-level WordPress API already solves the problem. WP_Query, metadata APIs, taxonomy APIs, user APIs, and options APIs often provide enough functionality with less risk. Custom SQL is useful when you truly need custom table access, aggregate reporting, or a query shape WordPress APIs do not support cleanly.
If custom SQL is necessary, use prepared statements, validate input, keep result sets bounded, and avoid exposing raw database structure to frontend code.
Unsafe example: string interpolation
The pattern below is dangerous because user-controlled input is inserted directly into a SQL string.
$email = $_GET['email'] ?? '';
$row = $wpdb->get_row(
"SELECT * FROM {$wpdb->prefix}vulnwp_leads WHERE email = '{$email}'"
);
Even if the field usually contains a normal email address, the database query now depends on untrusted input. Escaping manually is also easy to get wrong. Use placeholders instead.
Safe example with $wpdb->prepare()
global $wpdb;
$email = isset( $_GET['email'] )
? sanitize_email( wp_unslash( $_GET['email'] ) )
: '';
if ( ! is_email( $email ) ) {
return null;
}
$table = $wpdb->prefix . 'vulnwp_leads';
$row = $wpdb->get_row(
$wpdb->prepare(
"SELECT id, email, created_at
FROM {$table}
WHERE email = %s
LIMIT 1",
$email
)
);
The placeholder %s tells WordPress to prepare the email as a string value. The input is also sanitized and validated before the query because prepared statements do not replace business validation.
Common placeholders
%sfor strings.%dfor integers.%ffor floats.%ifor identifiers where supported by the running WordPress version.
Use the placeholder that matches the value. Do not quote placeholders manually inside the SQL string. WordPress handles that as part of preparation.
Safe search query example
Search queries need special care because wildcards are part of the intended behavior. Escape the LIKE value, then prepare the final SQL.
global $wpdb;
$search = isset( $_GET['q'] )
? sanitize_text_field( wp_unslash( $_GET['q'] ) )
: '';
if ( strlen( $search ) < 3 ) {
return array();
}
$like = '%' . $wpdb->esc_like( $search ) . '%';
$table = $wpdb->prefix . 'vulnwp_leads';
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT id, email, created_at
FROM {$table}
WHERE email LIKE %s
ORDER BY created_at DESC
LIMIT 20",
$like
)
);
This example also limits results. Security and performance are connected: unbounded search endpoints can become a production load problem even when they are not directly injectable.
Safe IN clause example
For a variable list of IDs, sanitize the IDs first, build the correct number of placeholders, then pass the values into prepare().
global $wpdb;
$ids = isset( $_GET['ids'] ) ? (array) $_GET['ids'] : array();
$ids = array_values( array_filter( array_map( 'absint', $ids ) ) );
if ( empty( $ids ) ) {
return array();
}
$placeholders = implode( ', ', array_fill( 0, count( $ids ), '%d' ) );
$table = $wpdb->prefix . 'vulnwp_leads';
$sql = $wpdb->prepare(
"SELECT id, email, created_at
FROM {$table}
WHERE id IN ({$placeholders})
LIMIT 50",
$ids
);
$rows = $wpdb->get_results( $sql );
Do not pass raw comma-separated strings into SQL. Convert them into typed values first.
Production checklist
- Use WordPress APIs instead of custom SQL when they fit the use case.
- Use
$wpdb->prepare()for values that enter custom SQL. - Sanitize and validate input before building the query.
- Use
$wpdb->esc_like()for LIKE patterns. - Bound result sets with explicit
LIMITvalues where appropriate. - Use capability checks and nonce checks before privileged queries.
- Log or handle database errors without exposing raw SQL to visitors.
- Test invalid input, empty input, long input, and low-privilege requests.
Common mistakes
- Concatenating request data into SQL. This is the classic injection path.
- Sanitizing but not preparing. Sanitization helps, but prepared placeholders are still the correct SQL boundary.
- Preparing the wrong part of the query. Placeholders protect values, not arbitrary SQL fragments.
- Trusting admin-only screens too much. Admin tools can still be abused if CSRF or capability checks are missing.
- Returning raw database errors publicly. Error handling should not leak implementation details.
Where this fits in a plugin review
SQL review should happen whenever a plugin adds custom tables, reporting screens, import tools, or AJAX search endpoints. Pair it with our WordPress dbDelta custom table example, WordPress nonce example, and plugin vulnerability response checklist. Prepared SQL is only one part of a safe request path.
How to audit an existing plugin quickly
Start by searching for direct database access and superglobals in the same files. The goal is not to prove a vulnerability from grep alone. The goal is to find code paths where request data may enter SQL.
# Search custom code for direct database usage.
grep -R "$wpdb->" wp-content/plugins/your-plugin
# Look for prepared statements.
grep -R "$wpdb->prepare" wp-content/plugins/your-plugin
# Find request input near database code.
grep -R "$_GET\|$_POST\|$_REQUEST" wp-content/plugins/your-plugin
For every match, trace the input path. Identify where the value comes from, how it is sanitized, whether it is validated against a business rule, whether the SQL uses placeholders, and whether the action has a capability or nonce boundary. A query can be prepared correctly and still be dangerous if any logged-in user can trigger an expensive report or retrieve data they should not see.
Operational signals after deployment
After shipping custom SQL, monitor slow queries, PHP errors, unexpected empty result sets, and spikes in admin-ajax or REST traffic. SQL safety is not only about injection. It is also about predictable behavior under production data volume. A query that works on 50 rows in staging may hurt a real site with years of content and metadata.


