How to Prevent SQL Injection in WordPress

Senior WebCoder

Imagine a hacker entering a password that isn't a password, but a command that deletes your entire database. That is SQL Injection (SQLi).
Despite being one of the oldest web vulnerabilities, it remains the most dangerous. In WordPress, it usually happens when a developer unknowingly passes unsafe user data directly into a database query.
Here is how to code responsibly and prevent SQLi.
๐ The Mistake: Direct Variable Insertion
The most common error is inserting a variable directly into a SQL string.
DO NOT DO THIS:
// โ UNSAFE
$id = $_GET['id'];
$results = $wpdb->get_results( "SELECT * FROM $wpdb->posts WHERE ID = $id" );
If the user sets ?id=1 OR 1=1, the query becomes SELECT * FROM wp_posts WHERE ID = 1 OR 1=1. Since 1=1 is always true, the database returns every single post, potentially exposing private data.
โ The Fix: Prepare Your Queries
WordPress provides a built-in method called $wpdb->prepare(). It functions like printf() in C or sprintf() in PHP. It separates the SQL query from the data, ensuring the data is treated as text, not executable code.
DO THIS:
// โ
SAFE
$id = $_GET['id'];
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM $wpdb->posts WHERE ID = %d",
$id
)
);
Placeholders:
%d= Integer (numbers)%s= String (text)%f= Float (decimals)
๐ Mistake: Trusting $_POST or $_GET data
Never assume input is safe. Even if it's from a dropdown menu, a hacker can intercept the request and change the value.
Sanitize First, Validate Second
WordPress has helper functions to clean data before you even use it.
sanitize_text_field(): Removes HTML tags and checks string safety.sanitize_email(): Ensures it's a valid email format.absint(): Forces a value to be a non-negative integer.
Example:
$user_email = sanitize_email( $_POST['email'] );
$age = absint( $_POST['age'] );
4. The LIKE Statement Trap
This is a specific vulnerability that $wpdb->prepare() alone doesn't fix.
If you are searching for a name:
$name = 'admin'; // User input
$wpdb->prepare( "SELECT * FROM wp_users WHERE user_login LIKE %s", '%' . $name . '%' );
If the user enters %, the query becomes LIKE '%%%', returning ALL users.
The Fix: Use $wpdb->esc_like() before preparing.
$name = $wpdb->esc_like( $user_input ); // Escapes % and _
$query = $wpdb->prepare( "SELECT * FROM wp_users WHERE user_login LIKE %s", '%' . $name . '%' );
5. Cast Your Variables (The Ultimate Safety Net)
If you know an input must be a number, force it to be one.
$id = (int) $_GET['id'];
$query = "SELECT * FROM wp_posts WHERE ID = $id";
Because $id is mathematically guaranteed to be an integer now, SQL injection is impossible, even without prepare() (though you should still use prepare for consistency!).
6. Don't Hardcode Table Prefixes
You might be using wp_posts now, but what if you migrate the site and the prefix changes to db123_posts for security?
Bad:
SELECT * FROM wp_posts
Good:
SELECT * FROM {$wpdb->prefix}posts
This makes your code portable and compatible with all WordPress installs.
7. When prepare() isn't enough: esc_sql()
Sometimes you have a dynamic order query that prepare() can't handle because you can't put column names in placeholders.
Scenario: Ordering by a user-chosen column.
$orderby = $_GET['sort']; // Could be 'post_title' OR '1=1; DROP TABLE wp_users'
$safe_col = esc_sql( $orderby );
$query = "SELECT * FROM {$wpdb->posts} ORDER BY $safe_col";
Note: For ORDER BY, it's even safer to whitelist allowed columns using in_array().
Summary
Security is a mindset, not a feature.
- Never trust user input.
- Always use
$wpdb->prepare(). - Sanitize all incoming data.
By following these three rules, you eliminate 99% of SQL injection risks in your custom WordPress development.

Gokila Manickam
Senior WebCoder
Gokila Manickam is a Senior WebCoder at FUEiNT, contributing expert insights on technology, development, and digital strategy.
