wordpress-hardening
Skill16 starsChanged 6 months agoMITView on GitHub
Detect and contain WordPress compromises, then harden the install against re-entry. Covers webshell detection across the Sid Gifari, WSO, FilesMan, b374k and c99 families, backdoored mu-plugins, malicious admin accounts, and shared-hosting lateral-movement defense. Invoke when a WordPress site shows unexpected files, suspicious admin accounts, defaced pages, or when hardening a fresh install on shared hosting.
In this repository37 other files it gives its agents.
- agent-client-securitySkill
- ai-agent-guardrailsSkill
- android-securitySkill
- api-securitySkill
- auth-hardeningSkill
- backend-architectureSkill
- backup-disaster-recoverySkill
- cloudflare-hardeningSkill
- codebase-auditSkill
- dach-complianceSkill
- dependency-supply-chainSkill
- distributed-system-auditSkill
- dns-domain-securitySkill
- docker-container-securitySkill
- email-deliverability-securitySkill
- file-upload-securitySkill
- gdpr-technical-controlsSkill
- github-actions-securitySkill
- honeypot-tarpitsSkill
- incident-responseSkill
- ios-securitySkill
- kubernetes-securitySkill
- llm-app-securitySkill
- llm-coding-failure-modesSkill
- log-strategySkill
- mcp-securitySkill
- message-bus-securitySkill
- nextjs-securitySkill
- object-storage-securitySkill
- payload-cms-securitySkill
- postgres-hardeningSkill
- prompt-injection-defenseSkill
- rag-securitySkill
- secret-hygieneSkill
- site-server-auditSkill
- stripe-webhook-securitySkill
- vps-hardeningSkill
The file
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 2 are distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 24
Run these from the doc-root.
- Distinctive: line 81
Take the site offline: if possible — maintenance mode at the webserver level, not via a WP plugin (the plugin may be compromised).
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
WordPress Hardening
A defensive-security skill for diagnosing and hardening WordPress installations, with a focus on shared-hosting environments where one compromised sub can pivot across the whole account.
When to invoke
Trigger this skill when any of these signals appear:
wp-content/uploads/**/*.phpexists (uploads should never contain PHP)- Admin users you do not recognize, or
wp_usersentries with creation dates that don't match the site history wp-config.phpmodified recently with no deploymu-plugins/contains files you did not place there- Posts/options contain base64-encoded blobs,
eval(,gzinflate(,str_rot13(, or hex-escape strings - The site serves a different language/title to bots than to humans (cloaking)
- A shared-hosting account contains 1 dirty sub and N other subs — assume lateral movement until proven otherwise
Detection — file-system indicators
Run these from the doc-root. Adjust paths for the hosting layout.
# 1. Any PHP in uploads is suspicious — production WP never writes PHP there
find wp-content/uploads -name '*.php' -o -name '*.phtml' -o -name '*.phar' 2>/dev/null
# 2. Recent PHP changes outside core/plugins/themes you control
find . -name '*.php' -mtime -30 -not -path './wp-content/cache/*' 2>/dev/null
# 3. Common webshell signatures (Sid Gifari, WSO, FilesMan, b374k, c99)
grep -rEl 'Sid[ _]Gifari|WSO [0-9]|FilesMan|c99shell|b374k|eval\(base64_decode|eval\(gzinflate|@eval\(\$_(POST|GET|REQUEST|COOKIE)' \
--include='*.php' --include='*.phtml' . 2>/dev/null
# 4. Suspicious file names dropped by mass-compromise tools
find . \( -name 'wp-conflg.php' -o -name 'wp-info.php' -o -name 'radio.php' \
-o -name 'about.php' -o -name 'license.php' -o -name 'lock360.php' \) 2>/dev/null
# 5. Files with execute-but-no-owner mismatch (often dropped by web user via writable dir)
find . -name '*.php' -user www-data 2>/dev/null # adjust user for the hostThe single highest-value query is (1): a PHP file in uploads/ is a near-certain indicator. Investigate every hit.
Detection — database indicators
-- 1. Unexpected admin users
SELECT u.ID, u.user_login, u.user_email, u.user_registered, m.meta_value
FROM wp_users u
JOIN wp_usermeta m ON m.user_id = u.ID
WHERE m.meta_key = 'wp_capabilities' AND m.meta_value LIKE '%administrator%';
-- 2. Recently-modified options that should be static
SELECT option_name, LENGTH(option_value) AS len, option_value
FROM wp_options
WHERE option_name IN ('siteurl','home','active_plugins','template','stylesheet','admin_email','blogname')
OR option_name LIKE 'cron%'
OR option_name LIKE '%_transient_%spam%';
-- 3. Hidden auto-loaded options (a classic backdoor mechanism)
SELECT option_id, option_name, LENGTH(option_value) AS len
FROM wp_options
WHERE autoload = 'yes' AND LENGTH(option_value) > 50000
ORDER BY len DESC LIMIT 20;
-- 4. Posts injected with hidden links / SEO spam
SELECT ID, post_title, post_status, post_modified
FROM wp_posts
WHERE post_content LIKE '%<div style=%display:none%'
OR post_content LIKE '%base64_decode%'
OR post_content LIKE '%eval(%'
LIMIT 50;Containment (do this in order)
- Snapshot first — full file + DB backup before touching anything. You need forensic state to learn from.
- Take the site offline if possible — maintenance mode at the webserver level, not via a WP plugin (the plugin may be compromised).
- Rotate all credentials treated as burned:
- WP admin passwords (all admin accounts, then delete unknown ones)
- DB user password (and update
wp-config.php) - SFTP/SSH passwords + revoke shared keys
- Hosting-panel password
- API keys stored in plugins (Stripe, Mailgun, etc.)
- Block at the edge — if a single attacker IP/ASN is hammering, block at Cloudflare/WAF, not just
.htaccess.
Cleanup
- Restore WP core from
wordpress.orgZIP (do not trustwp-admin/andwp-includes/on disk). - Restore each plugin from its official source. Compare hashes against fresh downloads — never just diff against another site (it may also be compromised).
- Restore each theme the same way. Custom themes: diff against last clean git commit.
- Wipe
wp-content/uploads/**/*.phpunconditionally. - Drop and recreate
mu-plugins/from a known-good source. - Re-issue salts in
wp-config.php(api.wordpress.org/secret-key/1.1/salt/) — this invalidates all existing auth cookies.
Hardening — defense pack
Drop these into mu-plugins/ (must-use plugins load before regular ones and cannot be disabled from the admin):
mu-plugins/00-disable-file-edit.php
<?php
// Block plugin/theme editing from the WP admin (a common post-compromise pivot).
define('DISALLOW_FILE_EDIT', true);
define('DISALLOW_FILE_MODS', true);mu-plugins/01-block-php-in-uploads.php
<?php
// Belt-and-braces: also enforce at the webserver layer (.htaccess / nginx).
add_filter('upload_mimes', function ($mimes) {
unset($mimes['php'], $mimes['phtml'], $mimes['phar']);
return $mimes;
});.htaccess (Apache) — inside wp-content/uploads/
<FilesMatch "\.(php|phtml|phar|pl|py|cgi|asp)$">
Require all denied
</FilesMatch>nginx (server block)
location ~* /wp-content/uploads/.*\.(php|phtml|phar|pl|py|cgi|asp)$ {
deny all;
return 403;
}mu-plugins/02-file-integrity.php
<?php
// Email alert when a PHP file under wp-content changes.
// Daily cron — adjust to match your noise tolerance.
add_action('init', function () {
if (!wp_next_scheduled('mu_file_integrity_check')) {
wp_schedule_event(time(), 'daily', 'mu_file_integrity_check');
}
});
add_action('mu_file_integrity_check', function () {
$state_file = WP_CONTENT_DIR . '/.integrity-state.json';
$current = [];
$iter = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(WP_CONTENT_DIR));
foreach ($iter as $f) {
if (!$f->isFile() || $f->getExtension() !== 'php') continue;
if (strpos($f->getPathname(), '/cache/') !== false) continue;
$current[$f->getPathname()] = hash_file('sha256', $f->getPathname());
}
if (file_exists($state_file)) {
$prior = json_decode(file_get_contents($state_file), true);
$diff = array_diff_assoc($current, $prior) + array_diff_key($current, $prior) + array_diff_key($prior, $current);
if (!empty($diff)) {
wp_mail(get_option('admin_email'),
'[INTEGRITY] PHP file changes on ' . home_url(),
"Changed/new/removed PHP files:\n\n" . print_r(array_keys($diff), true));
}
}
file_put_contents($state_file, json_encode($current));
});Shared-hosting pivot — investigate every sub
If one sub on a shared account is compromised, assume the others are too until proven clean. Walk every doc-root on the account and re-run the detection queries. Often the same attacker drops the same shell with slight name variations across all sites under the panel user.
What this skill will not do
- It does not exploit, brute-force, or remove protections from sites you do not own.
- It does not recommend disabling security headers, WAFs, or auth for any reason.
- It assumes you have explicit authorization for the target site.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

