Learn how to secure your WordPress site against PHP vulnerabilities. Discover best practices and practical tips to prevent common threats like SQL Injection, XSS, and CSRF. Keep your website safe and secure.
Most WordPress sites don’t fall to some genius zero-day. They fall to a form field nobody escaped, an upload nobody checked, a query built by gluing strings together. The boring stuff.
WordPress runs a huge slice of the web, and all of it sits on PHP. That makes PHP the first thing an attacker pokes at. The good news: the ten mistakes below are old, well understood, and each has a fix that already ships with WordPress. Learn them once and you close most of the door.
Table of Contents
- Why PHP Security Matters in WordPress
- Top 10 Common PHP Vulnerabilities
- Best Practices for Securing PHP Code
- Conclusion
Why PHP Security Matters in WordPress
Insecure PHP is how attackers get in. Core, your theme, and your plugins all run PHP, so one careless function is enough to leak data, plant malware, or hand over the whole site.
The attacks aren’t exotic. They’re the same handful, reused against site after site. That’s exactly why the fixes are worth memorizing instead of googling mid-incident.
Top 10 Common PHP Vulnerabilities
Here’s the short list, with the WordPress-native fix for each.
1. SQL Injection (SQLi)
Build a query by pasting user input straight into a string and an attacker can rewrite that query to do whatever they want. This is SQL injection, and it’s still the most damaging bug on the list.
<?php
// Vulnerable code
$query = "SELECT * FROM wp_users WHERE username = '$_POST[username]' AND password = '$_POST[password]'";
$result = $wpdb->get_results($query);
Fix: never concatenate input into SQL. Run it through $wpdb->prepare() with placeholders (%s, %d) and let WordPress escape the values for you:
<?php
// Secure code using prepared statements
$stmt = $wpdb->prepare("SELECT * FROM wp_users WHERE username = %s AND password = %s", $_POST['username'], $_POST['password']);
$result = $wpdb->get_results($stmt);2. Cross-Site Scripting (XSS)
XSS is the mirror image. Instead of poisoning a query, the attacker gets their script rendered in someone else’s browser, then rides that user’s session.
Fix: sanitize on the way in, escape on the way out. Escaping is the step you can’t skip, and you match the escaper to the context: esc_html() for text, esc_attr() for attributes, esc_url() for links.
<?php
// Escape output to prevent XSS
echo esc_html($user_input);3. Cross-Site Request Forgery (CSRF)
CSRF makes a logged-in user fire an action they never intended, by loading a page that quietly submits to your site with their cookies attached.
Fix: stamp every state-changing form with a nonce and verify it before you act. wp_nonce_field() writes it, wp_verify_nonce() or check_admin_referer() checks it. Pair that with a capability check, because a nonce proves intent, not permission.
<?php
// Adding a nonce
wp_nonce_field('secure_action', 'secure_nonce');
// Verifying a nonce
if (!wp_verify_nonce($_POST['secure_nonce'], 'secure_action')) {
die('Invalid nonce.');
}4. Remote Code Execution (RCE)
Remote code execution is the worst case: the attacker runs their own code on your server, which usually means the whole box is theirs. It tends to ride in through a function that evaluates strings.
Solution: never feed user input to eval() or shell_exec(). On servers you control, turn the dangerous functions off outright in your PHP configuration:
<?php
// php.ini
disable_functions = "eval, shell_exec, system, exec"5. Directory Traversal
Feed ../../ into a filename and a naive script will happily read files far outside the folder you meant to expose.
Solution: strip the path down to a filename with basename(), resolve it with realpath(), then confirm it still sits inside the directory you allow:
<?php
// Prevent directory traversal
$path = realpath('/var/www/html/uploads/' . basename($_GET['file']));
if (strpos($path, '/var/www/html/uploads/') !== 0) {
die('Invalid file path.');
}6. File Upload Vulnerabilities
Accept uploads without checking them and sooner or later someone hands you a PHP script dressed up as a photo.
Solution: allow only the types you expect and keep uploads out of the web root. One honest caveat on the snippet below: $_FILES['type'] is sent by the browser, so it’s trivial to spoof. In production, lean on wp_check_filetype_and_ext() or finfo to inspect the actual bytes instead of trusting that header.
<?php
// Check file type
if (!in_array($_FILES['file']['type'], ['image/jpeg', 'image/png'])) {
die('Invalid file type.');
}7. Session Hijacking
Steal a session token and you become that user, no password required.
Solution: serve everything over HTTPS, flag cookies as secure, and rotate the ID after login. Worth knowing: WordPress doesn’t use native PHP sessions for its own auth, it manages its own signed cookies. The snippet below only applies if you start a PHP session yourself.
<?php
// Secure session cookie
ini_set('session.cookie_secure', true);
session_regenerate_id(true);8. Improper Error Handling
A raw PHP error on a live page is a free map for an attacker: file paths, versions, sometimes credentials.
Solution: log errors, never display them in production. In WordPress you’d normally control this with WP_DEBUG and WP_DEBUG_DISPLAY in wp-config.php, but the raw PHP settings do the same job:
<?php
// Hide errors in production
ini_set('display_errors', 0);
ini_set('log_errors', 1);9. Insecure Direct Object References (IDOR)
Change the id in a URL and see data that isn’t yours. That’s an insecure direct object reference, and it’s an authorization bug, not an input bug.
Solution: check permission on the specific object before you serve it. current_user_can() with a meta capability does exactly that:
<?php
// Check user permissions
if (!current_user_can('edit_post', $post_id)) {
die('Unauthorized access.');
}10. Insecure Deserialization
Hand attacker-controlled data to unserialize() and you can end up building objects you never asked for, which is a known path to code execution.
Solution: don’t unserialize untrusted input. Use json_decode() instead, since it returns plain data with no object side effects:
<?php
// Avoid unserializing untrusted data
$data = json_decode($_POST['input'], true);Best Practices for Securing PHP Code
Fixing individual bugs is half the work. The other half is the habits that stop them coming back:
- Keep everything updated: patch WordPress core, themes, and plugins promptly. Most break-ins target holes that already had a fix.
- Use HTTPS: encrypt traffic between your users and the site with a valid SSL/TLS certificate.
- Least privilege: give users and server processes only the access they actually need, nothing more.
- Turn on two-factor authentication (2FA): a stolen password shouldn’t be enough to log in.
- Audit your code: read through your own plugins and themes looking for unescaped output and missing checks.
- Run a security plugin: tools like Wordfence or Sucuri watch for changes and known attacks.
Conclusion
None of this is exotic. SQL injection, XSS, CSRF, the same short list has sat on every security checklist for twenty years, and WordPress ships a fix for each one. Escape your output, prepare your queries, nonce your forms, check your capabilities. That’s most of the battle right there.
Security isn’t a task you finish. Update often, watch your logs, and treat every piece of user input as hostile until you’ve proven otherwise. Do that consistently and you’re already ahead of most sites out there.


