wordpress-hardening — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited wordpress-hardening (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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.
Trigger this skill when any of these signals appear:
wp-content/uploads/**/*.php exists (uploads should never contain PHP)wp_users entries with creation dates that don't match the site historywp-config.php modified recently with no deploymu-plugins/ contains files you did not place thereeval(, gzinflate(, str_rot13(, or hex-escape stringsRun 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.
-- 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;wp-config.php).htaccess.wordpress.org ZIP (do not trust wp-admin/ and wp-includes/ on disk).wp-content/uploads/**/*.php unconditionally.mu-plugins/ from a known-good source.wp-config.php (api.wordpress.org/secret-key/1.1/salt/) — this invalidates all existing auth cookies.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>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));
});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.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.