WordPress Content Cleaner & Health Check (PHP)

⏲️ Estimated reading time: 17 min

Table of Contents

Learn how to safely check and clean a WordPress site using lightweight PHP snippets and WP-CLI. This guide covers admin “health” checks, content sanitization on save, database scanning for suspicious code, bulk cleanup with dry-run, and spotting recently modified PHP files without breaking your theme.


Why “Checking and Cleaning” WordPress matters

When a WordPress site gets messy, it usually happens in one of three places:

Messy content

Spam links, weird hidden HTML, copied text from shady sources, or broken layout markup.

Suspicious injections

Things like <script>, <iframe>, base64_decode, or eval() showing up inside post content or templates.

Weak hygiene

Outdated plugins, stale admin users, poor password practices, no backups, and zero visibility into what changed recently.

The good news is you can fix a lot of this with small, safe, admin-only tools especially if you keep them in MU-plugins so updates don’t overwrite them.

WordPress also publishes a hardening guide that aligns with this approach: reduce attack surface, keep software updated, and lock down access. (WordPress Developer Resources)


The safety-first rules before you touch anything

Always take a backup you can restore

A “cleanup” can remove malicious code… or accidentally remove legitimate embeds and scripts you actually need.

Clean the source, not only the symptoms

If your site was hacked, cleaning the database is only part of the job. You also need to harden WordPress and the server (updates, passwords, salts, unknown users, file checks). (WordPress Developer Resources)

Use MU-plugins for site tools

MU-plugins load automatically and are less likely to get disabled by accidents or theme switching.


Folder setup you’ll use in this guide

Create the MU-plugins folder if it doesn’t exist

Path:

wp-content/mu-plugins/

Then add files like:

wp-content/mu-plugins/site-check.php

wp-content/mu-plugins/content-cleaner.php

wp-content/mu-plugins/db-content-scanner.php

wp-content/mu-plugins/wpcli-clean-content.php

wp-content/mu-plugins/recent-php-files.php


Quick “health check” panel in wp-admin

This snippet is intentionally read-only. It doesn’t clean anything. It just gives you a safe dashboard notice you can expand later.

What it does

  • Shows WordPress version
  • Shows PHP version
  • Confirms HTTPS detection
  • Runs only for admins

MU-plugin file

<?php
/**
 * Plugin Name: Site Check (Local)
 * Description: Basic WordPress health checks in wp-admin.
 */

if (!defined('ABSPATH')) exit;

add_action('admin_notices', function () {
    if (!current_user_can('manage_options')) return;

    $php   = PHP_VERSION;
    $wp    = get_bloginfo('version');
    $https = is_ssl() ? 'Yes' : 'No';

    echo '<div class="notice notice-info"><p><strong>Site Check</strong><br>';
    echo 'WordPress: ' . esc_html($wp) . '<br>';
    echo 'PHP: ' . esc_html($php) . '<br>';
    echo 'HTTPS: ' . esc_html($https) . '<br>';
    echo '</p></div>';
});

Why this is useful

If you’re troubleshooting or cleaning a site, you want a stable place to show results without editing the theme.


Clean post content on save to block reinfection

This is one of the most practical protections you can add. It prevents future “bad paste” problems.

What it does

  • Sanitizes content at save-time using WordPress’s safe HTML rules
  • Optionally strips on*= event handlers (onclick=, onload= etc.)
  • Optionally neutralizes javascript: links

WordPress’s wp_kses_post() is designed specifically for sanitizing post content to an allowed set of tags/attributes. (WordPress Developer Resources)

MU-plugin file

<?php
/**
 * Plugin Name: Content Cleaner
 * Description: Sanitizes post content on save.
 */

if (!defined('ABSPATH')) exit;

add_filter('content_save_pre', function ($content) {
    // Sanitize like WordPress sanitizes "post content"
    $content = wp_kses_post($content);

    // Extra hardening: remove inline event handlers like onclick=""
    $content = preg_replace('/\son\w+\s*=\s*(["\']).*?\1/i', '', $content);

    // Extra hardening: remove javascript: URLs
    $content = preg_replace('/(href|src)\s*=\s*(["\'])\s*javascript:.*?\2/i', '$1=$2#$2', $content);

    return $content;
}, 20);

Make the cleaner smarter with “allowlist thinking

Blindly removing everything can break legitimate content like:

  • YouTube embeds
  • trusted iframes (maps, forms)
  • certain shortcodes (builders, galleries)

A safer approach

Instead of banning everything, decide what you do allow:

  • Allow normal HTML and common formatting
  • Allow specific iframe sources only (like YouTube/Vimeo)
  • Remove anything else

Example of “trusted iframe domains only”

<?php
if (!defined('ABSPATH')) exit;

add_filter('content_save_pre', function ($content) {
    $content = wp_kses_post($content);

    // If you want to allow iframes, do it safely.
    // Option A: remove all iframes first (simple and safe)
    // $content = preg_replace('#<iframe\b[^>]*>.*?</iframe>#is', '', $content);

    // Option B: keep iframes only if src matches trusted domains
    $content = preg_replace_callback('#<iframe\b[^>]*>.*?</iframe>#is', function ($m) {
        $iframe = $m[0];

        if (!preg_match('/src\s*=\s*(["\'])(.*?)\1/i', $iframe, $srcMatch)) {
            return '';
        }

        $src = $srcMatch[2];
        $trusted = [
            'youtube.com',
            'youtu.be',
            'player.vimeo.com',
            'google.com/maps',
        ];

        foreach ($trusted as $domain) {
            if (stripos($src, $domain) !== false) {
                return $iframe;
            }
        }

        return ''; // remove untrusted iframe
    }, $content);

    return $content;
}, 25);

Admin-only DB Content Scanner to find suspicious patterns

This is your “flashlight in a dark room.”

What it does

  • Adds Tools → DB Content Scanner
  • Scans post_content for typical injection markers:
    • <script
    • <iframe
    • base64_decode
    • eval(
    • document.write
    • fromCharCode
    • data:text/html

MU-plugin file

<?php
/**
 * Plugin Name: DB Content Scanner
 * Description: Scans posts for suspicious patterns.
 */

if (!defined('ABSPATH')) exit;

add_action('admin_menu', function () {
    add_management_page(
        'DB Content Scanner',
        'DB Content Scanner',
        'manage_options',
        'db-content-scanner',
        'db_content_scanner_page'
    );
});

function db_content_scanner_page() {
    if (!current_user_can('manage_options')) return;

    global $wpdb;

    $patterns = [
        'script tag'   => '<script',
        'iframe'       => '<iframe',
        'base64'       => 'base64_decode',
        'eval'         => 'eval(',
        'document'     => 'document.write',
        'fromCharCode' => 'fromCharCode',
        'data URI'     => 'data:text/html',
    ];

    echo '<div class="wrap"><h1>DB Content Scanner</h1>';
    echo '<p>Scans post_content for common malicious markers.</p>';

    $like = [];
    foreach ($patterns as $label => $needle) {
        $like[] = $wpdb->prepare("post_content LIKE %s", '%' . $wpdb->esc_like($needle) . '%');
    }

    $sql = "
        SELECT ID, post_title, post_type
        FROM {$wpdb->posts}
        WHERE post_status IN ('publish','draft','pending','private')
          AND (" . implode(' OR ', $like) . ")
        ORDER BY ID DESC
        LIMIT 500
    ";

    $rows = $wpdb->get_results($sql);

    if (!$rows) {
        echo '<p><strong>No matches found.</strong></p></div>';
        return;
    }

    echo '<table class="widefat striped"><thead><tr><th>ID</th><th>Type</th><th>Title</th><th>Link</th></tr></thead><tbody>';
    foreach ($rows as $r) {
        $edit = esc_url(admin_url('post.php?post=' . $r->ID . '&action=edit'));
        echo '<tr>';
        echo '<td>' . esc_html($r->ID) . '</td>';
        echo '<td>' . esc_html($r->post_type) . '</td>';
        echo '<td>' . esc_html($r->post_title) . '</td>';
        echo '<td><a href="' . $edit . '">Edit</a></td>';
        echo '</tr>';
    }
    echo '</tbody></table></div>';
}

How to read results without panicking

Not every match is malware.

Legit matches you might see

  • An intentional <iframe> embed (video, map)
  • A script tag from old editor content
  • A marketing snippet someone pasted

Red flags

  • Scripts that redirect visitors
  • Hidden iframes (width/height 0)
  • base64 + eval combos
  • weird “random letters” code blocks
  • spam links repeated across many posts

OWASP recommends sanitizing rich HTML carefully and treating XSS vectors seriously. (OWASP Cheat Sheet Series)


Bulk-clean infected content using WP-CLI

Database bulk cleanup is best done via WP-CLI because:

  • it’s fast
  • you can dry-run
  • you don’t time out in the browser

WP-CLI supports registering custom commands via WP_CLI::add_command(). (Make WordPress)

MU-plugin file

Create:

wp-content/mu-plugins/wpcli-clean-content.php

<?php
if (!defined('ABSPATH')) exit;

if (defined('WP_CLI') && WP_CLI) {
    WP_CLI::add_command('content clean', function ($args, $assoc_args) {
        global $wpdb;

        $dry_run = !empty($assoc_args['dry-run']);
        $limit   = isset($assoc_args['limit']) ? (int)$assoc_args['limit'] : 2000;

        $rows = $wpdb->get_results($wpdb->prepare("
            SELECT ID, post_content
            FROM {$wpdb->posts}
            WHERE post_status IN ('publish','draft','pending','private')
              AND (post_content LIKE %s OR post_content LIKE %s)
            LIMIT %d
        ", '%<script%', '%<iframe%', $limit));

        if (!$rows) {
            WP_CLI::success("No posts found containing <script> or <iframe>.");
            return;
        }

        $changed = 0;
        foreach ($rows as $r) {
            $orig = $r->post_content;

            // Remove script and iframe blocks
            $clean = preg_replace('#<script\b[^>]*>.*?</script>#is', '', $orig);
            $clean = preg_replace('#<iframe\b[^>]*>.*?</iframe>#is', '', $clean);

            // Extra sanitize
            $clean = wp_kses_post($clean);

            if ($clean !== $orig) {
                $changed++;
                if (!$dry_run) {
                    $wpdb->update(
                        $wpdb->posts,
                        ['post_content' => $clean],
                        ['ID' => $r->ID],
                        ['%s'],
                        ['%d']
                    );
                }
            }
        }

        if ($dry_run) {
            WP_CLI::success("Dry run: would change {$changed} posts.");
        } else {
            WP_CLI::success("Updated {$changed} posts.");
        }
    });
}

WP-CLI usage

wp content clean --dry-run
wp content clean
wp content clean --limit=5000

Why dry-run is non-negotiable

Because “remove all iframes” may delete:

  • your YouTube videos
  • your Google Maps embeds
  • your donation widgets

Dry-run shows how many posts would change before you commit.


Add a more precise CLI cleanup to avoid destroying embeds

If you want to keep trusted embeds, use the “trusted domain” logic in CLI as well.

Example of iframe cleanup with a trusted list

<?php
if (!defined('ABSPATH')) exit;

if (defined('WP_CLI') && WP_CLI) {
    WP_CLI::add_command('content clean-iframes', function ($args, $assoc_args) {
        global $wpdb;

        $dry_run = !empty($assoc_args['dry-run']);
        $limit   = isset($assoc_args['limit']) ? (int)$assoc_args['limit'] : 2000;

        $trusted = [
            'youtube.com',
            'youtu.be',
            'player.vimeo.com',
            'google.com/maps',
        ];

        $rows = $wpdb->get_results($wpdb->prepare("
            SELECT ID, post_content
            FROM {$wpdb->posts}
            WHERE post_status IN ('publish','draft','pending','private')
              AND post_content LIKE %s
            LIMIT %d
        ", '%<iframe%', $limit));

        if (!$rows) {
            WP_CLI::success("No posts found containing <iframe>.");
            return;
        }

        $changed = 0;

        foreach ($rows as $r) {
            $orig = $r->post_content;

            $clean = preg_replace_callback('#<iframe\b[^>]*>.*?</iframe>#is', function ($m) use ($trusted) {
                $iframe = $m[0];

                if (!preg_match('/src\s*=\s*(["\'])(.*?)\1/i', $iframe, $srcMatch)) {
                    return '';
                }

                $src = $srcMatch[2];
                foreach ($trusted as $domain) {
                    if (stripos($src, $domain) !== false) {
                        return $iframe; // keep trusted
                    }
                }

                return ''; // remove untrusted
            }, $orig);

            $clean = wp_kses_post($clean);

            if ($clean !== $orig) {
                $changed++;
                if (!$dry_run) {
                    $wpdb->update(
                        $wpdb->posts,
                        ['post_content' => $clean],
                        ['ID' => $r->ID],
                        ['%s'],
                        ['%d']
                    );
                }
            }
        }

        $msg = $dry_run ? "Dry run: would change {$changed} posts." : "Updated {$changed} posts.";
        WP_CLI::success($msg);
    });
}

Command usage

wp content clean-iframes --dry-run
wp content clean-iframes

Detect recently modified PHP files inside wp-content

This is a classic “something changed” indicator after a compromise.

What it does

  • Adds Tools → Recent PHP Files
  • Scans wp-content/ recursively
  • Lists .php files modified in the last X days

MU-plugin file

<?php
/**
 * Plugin Name: Recent PHP File Checker
 * Description: Lists recently modified PHP files in wp-content.
 */

if (!defined('ABSPATH')) exit;

add_action('admin_menu', function () {
    add_management_page('Recent PHP Files', 'Recent PHP Files', 'manage_options', 'recent-php-files', function () {
        if (!current_user_can('manage_options')) return;

        $root = WP_CONTENT_DIR;
        $since_days = 7;
        $since = time() - ($since_days * DAY_IN_SECONDS);

        $rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root));
        $files = [];

        foreach ($rii as $file) {
            if ($file->isDir()) continue;
            $path = $file->getPathname();
            if (strtolower(pathinfo($path, PATHINFO_EXTENSION)) !== 'php') continue;

            $mtime = $file->getMTime();
            if ($mtime >= $since) {
                $files[] = ['path' => $path, 'mtime' => $mtime];
            }
        }

        usort($files, fn($a,$b) => $b['mtime'] <=> $a['mtime']);

        echo '<div class="wrap"><h1>Recent PHP Files (last '.$since_days.' days)</h1>';
        if (!$files) { echo '<p>No recent PHP changes found.</p></div>'; return; }

        echo '<table class="widefat striped"><thead><tr><th>Modified</th><th>Path</th></tr></thead><tbody>';
        foreach (array_slice($files, 0, 300) as $f) {
            echo '<tr><td>' . esc_html(date('Y-m-d H:i:s', $f['mtime'])) . '</td><td><code>' . esc_html($f['path']) . '</code></td></tr>';
        }
        echo '</tbody></table></div>';
    });
});

How to interpret results

Normal results

  • Your own MU-plugin edits
  • a plugin update from yesterday
  • a theme update you triggered

Suspicious results

  • random PHP files in wp-content/uploads/
  • files with names like cache.php, old.php, wp-info.php in strange places
  • recently modified plugin core files when you did not update anything

WordPress hardening guidance includes checking file integrity and reducing risky write access where possible. (WordPress Developer Resources)


Strip shortcodes safely without breaking layouts

Sometimes content is “clean” but bloated with leftover builder shortcodes.

A safe “view-only” shortcode stripper

Use this when exporting or cleaning old posts where you no longer use a builder.

<?php
if (!defined('ABSPATH')) exit;

function hz_strip_shortcodes_keep_text($content) {
    // Removes [shortcode] blocks while keeping surrounding text
    return strip_shortcodes($content);
}

A stronger shortcode wipe for migrations

<?php
if (!defined('ABSPATH')) exit;

function hz_hard_remove_shortcodes($content) {
    // Removes even weird nested shortcode leftovers
    $content = preg_replace('/\[[^\]]+\]/', '', $content);
    return $content;
}

Warning

Builder sites often store layout inside shortcodes. Removing them can turn pages into plain text.


Add a small “site hygiene checklist” notice for admins

This isn’t code-cleaning. It’s operational cleaning. That matters more in real life.

What the admin notice should remind you to do

  • Update core + plugins + themes
  • Remove unknown admin users
  • Rotate passwords + salts
  • check wp-config.php and .htaccess
  • scan uploads for PHP files
  • confirm backups and restore test

These steps mirror typical WordPress hardening advice. (WordPress Developer Resources)

MU-plugin notice example

<?php
if (!defined('ABSPATH')) exit;

add_action('admin_notices', function () {
    if (!current_user_can('manage_options')) return;

    echo '<div class="notice notice-warning"><p><strong>Security Hygiene Reminder</strong><br>';
    echo 'Keep core/plugins/themes updated, remove unknown admins, rotate passwords & salts, and check wp-config.php/.htaccess after any suspicious activity.';
    echo '</p></div>';
});

Pro & Contra of this approach

Pros

  • Lightweight and fast
  • No heavy security plugins required
  • Works on VPS and shared hosting
  • Admin-only visibility
  • WP-CLI gives you speed + dry-run safety

Cons

  • A bad regex can remove legitimate embeds
  • Cleaning content doesn’t fix server-level compromise
  • Some infections live in plugin/theme files, not the database
  • You still need monitoring, backups, and update discipline

Step 1: Freeze changes

Disable unnecessary editors, stop random plugin installs, and limit admin accounts.

Step 2: Scan content

Run DB Content Scanner and list results.

Step 3: Inspect a few matches manually

If matches are all legitimate embeds, don’t nuke everything.

Step 4: Run CLI dry-run

Measure how many posts would change.

Step 5: Clean in batches

Use --limit=500 first, then expand.

Step 6: Rescan

Confirm the scanner output improves.

Step 7: Harden

Updates, passwords, salts, file checks, and long-term prevention. (WordPress Developer Resources)

safely check and clean a WordPress site

Frequently Asked Questions

Is wp_kses_post() enough to stop XSS in post content?

It’s a strong baseline for post content sanitization because it allows only safe tags and attributes intended for posts. (WordPress Developer Resources)

Will sanitizing on save break my YouTube embeds?

It can, depending on how the embed is stored. If your content includes raw iframes, you may need a trusted-domain iframe approach.

Why do I need a scanner if I already sanitize on save?

Sanitizing on save helps prevent future problems. A scanner helps you find old posts that already contain suspicious patterns.

What’s the safest way to bulk-clean content?

Use WP-CLI with --dry-run, clean in small batches, and keep backups. WP-CLI custom commands are designed for repeatable admin tasks. (Make WordPress)

Can these snippets replace a security plugin?

They help, but they don’t replace full protection like firewalls, monitoring, and malware scanning. Hardening and operational hygiene remain essential. (WordPress Developer Resources)

Why do infections often use base64_decode and eval()?

Attackers use them to hide and execute obfuscated code. Seeing them inside post content is a major red flag.

Should I scan only posts, or also pages and templates?

Scan everything: posts, pages, templates, MU-plugins, and uploads. Many compromises live in modified PHP files rather than the DB.

What if I find PHP files in uploads?

That’s suspicious in most setups. Investigate immediately and consider restoring from a known-clean backup.

Do I need to change WordPress salts after a hack?

Yes. Changing salts logs out sessions and can invalidate stolen cookies, which is a common recovery step recommended in WordPress hardening guidance. (WordPress Developer Resources)

Should I remove all iframes forever?

Not necessarily. It’s better to allow only trusted iframe sources than to ban all embeds.


⚠️ Disclaimer and Source Hygiene


This tutorial is for educational purposes and general site maintenance. Always back up files and database before changes, and consult a qualified WordPress/security professional for incident response or high-risk environments. Information here is based on research and standard guidance from authoritative sources like WordPress documentation, WP-CLI documentation, and OWASP.

🔔 For more tutorials like this, consider subscribing to our blog.
📩 Do you have questions or suggestions? Leave a comment or contact us!
🏷️ Tags: wordpress security, wordpress cleanup, wp-cli, mu-plugins, sanitize wordpress content, wp_kses_post, xss prevention, wordpress malware cleanup, database scanner wordpress, hardening wordpress
📢 Hashtags: #WordPress #WordPressSecurity #WPCLI #SiteHardening #MalwareCleanup #WebSecurity #PHP #WordPressTips #AdSenseFriendly #WebsiteMaintenance


📚 Sources and References

Authoritative Sources

🕊️ Secondary Sources and Testimonials

  • Discussion examples and practical context from the WordPress support ecosystem about post-hack steps (updates, passwords, salts) (WordPress.org)
  • Notes on wp_kses behavior and why it’s used for “strip evil scripts” style sanitization (tollmanz.com)
Report an issue (max 5 words):

We store the message, post link, time, and IP (for abuse prevention). No account required.

Want to support us? Let friends in on the secret and share your favorite post!

0 online now

Live Referrers

No external referrers recorded for this post.

Photo of author

Flo

WordPress Content Cleaner & Health Check (PHP)

Published

Update

Welcome to HelpZone.blog, your go-to hub for expert insights, practical tips, and in-depth guides across technology, lifestyle, business, entertainment, and more! Our team of passionate writers and industry experts is dedicated to bringing you the latest trends, how-to tutorials, and valuable advice to enhance your daily life. Whether you're exploring WordPress tricks, gaming insights, travel hacks, or investment strategies, HelpZone is here to empower you with knowledge. Stay informed, stay inspired because learning never stops! 🚀

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.