Skip to content

Instantly share code, notes, and snippets.

@ozgursar
Last active April 29, 2026 07:34
Show Gist options
  • Select an option

  • Save ozgursar/f249518ac14333041070f5a14c77b352 to your computer and use it in GitHub Desktop.

Select an option

Save ozgursar/f249518ac14333041070f5a14c77b352 to your computer and use it in GitHub Desktop.
Malware .htaccess Cleaner for WordPress. Safely removes injected files by content fingerprint.

Malware .htaccess Cleaner for WordPress

A PHP script that recursively scans all subdirectories of a WordPress installation and removes malicious .htaccess files injected by malware — without touching legitimate ones created by plugins, themes, or server configuration.

Why Content-Based?

Most cleanup scripts delete every .htaccess they find, which risks breaking:

  • Backup plugin rules (e.g. UpdraftPlus, BackupBuddy)
  • WooCommerce upload folder protection
  • Cache plugin configurations
  • Custom server security rules

This script fingerprints files by their content, only deleting ones that match known malware signatures — leaving everything else untouched.

Known Malware Signatures

The script currently detects two known variants:

Variant 1 — single quotes, no phtml:

<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'>

Variant 2 — double quotes, includes phtml:

<FilesMatch ".(py|exe|phtml|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$">

If you discover new variants, add them to the $malwareSignatures array in the script.

How to Use

1. Prepare the script

Open cleanup.php and change CHANGE_THIS_SECRET to a random private string only you know.

2. Upload

Upload cleanup.php to your WordPress root — the same folder as wp-config.php.

3. Dry run first (nothing gets deleted)

https://yoursite.com/cleanup.php?key=YOUR_SECRET

Review the output carefully:

  • Would delete — confirm these are all malware files
  • ⚠️ Skipped — confirm your legitimate .htaccess files are safe

4. Live run (actually deletes)

https://yoursite.com/cleanup.php?key=YOUR_SECRET&dry=0

5. Clean up

Delete cleanup.php from your server immediately after running.

After Cleanup

Don't stop at .htaccess files — malware often leaves traces elsewhere:

  • Change all WordPress, database, FTP, and hosting passwords
  • Update WordPress core, all plugins, and all themes
  • Check wp-config.php for injected or obfuscated code
  • Restore your root .htaccess from a backup if tampered with (WordPress needs it for permalinks)
  • Scan with Wordfence or MalCare
  • Check cPanel / hosting file manager for unfamiliar files in public_html

Security Notes

  • The script is protected by a secret key — never leave it with a guessable key
  • Always take a full backup before running any automated deletion script
  • The dry run mode is there for a reason — always use it first
  • Remove the script from your server as soon as you're done

License

MIT — free to use, share, and modify.

<?php
$secret = 'CHANGE_THIS_SECRET';
if (($_GET['key'] ?? '') !== $secret) {
die('Unauthorized');
}
$rootDir = __DIR__;
// All known malware signatures - add more here if you find new variants
$malwareSignatures = [
"<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'>",
'<FilesMatch ".(py|exe|phtml|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$">',
];
$dryRun = ($_GET['dry'] ?? '1') === '1';
$deleted = [];
$errors = [];
$skipped = [];
function isMalware(string $contents, array $signatures): bool {
foreach ($signatures as $sig) {
if (strpos($contents, $sig) !== false) return true;
}
return false;
}
function scanAndDelete(string $dir, array $signatures, bool $dryRun, array &$deleted, array &$errors, array &$skipped): void {
$items = scandir($dir);
foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;
$fullPath = $dir . DIRECTORY_SEPARATOR . $item;
if (is_dir($fullPath)) {
scanAndDelete($fullPath, $signatures, $dryRun, $deleted, $errors, $skipped);
} elseif ($item === '.htaccess') {
$contents = file_get_contents($fullPath);
if ($contents === false) {
$errors[] = "$fullPath (unreadable)";
continue;
}
if (isMalware($contents, $signatures)) {
if ($dryRun) {
$deleted[] = "$fullPath (DRY RUN - not deleted)";
} else {
if (unlink($fullPath)) {
$deleted[] = $fullPath;
} else {
$errors[] = "$fullPath (permission denied)";
}
}
} else {
$skipped[] = $fullPath;
}
}
}
}
scanAndDelete($rootDir, $malwareSignatures, $dryRun, $deleted, $errors, $skipped);
$mode = $dryRun ? '🔍 DRY RUN MODE (nothing deleted)' : '🗑️ LIVE MODE';
echo "<pre>";
echo "=== Malware .htaccess Cleaner — $mode ===\n\n";
echo "" . ($dryRun ? "Would delete" : "Deleted") . " (" . count($deleted) . " malware files):\n";
foreach ($deleted as $f) echo " - $f\n";
echo "\n⚠️ Skipped (" . count($skipped) . " legitimate files):\n";
foreach ($skipped as $f) echo " - $f\n";
echo "\n❌ Errors (" . count($errors) . "):\n";
foreach ($errors as $f) echo " - $f\n";
echo "\nDone. Delete this script immediately after use!\n";
echo "</pre>";
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment