Comprehensive WordPress Plugin Security Audit: From PHP Vulnerabilities to npm Dependency Upgrades

WordPress Plugin Security Audit - shield protecting WordPress from PHP and npm vulnerabilities

Today I completed a comprehensive WordPress plugin security audit on Sermon Browser, a legacy WordPress plugin I’ve been modernizing. The audit covered everything from SSRF and XSS vulnerabilities in PHP code to npm dependency vulnerabilities in the Gutenberg blocks build toolchain. Here’s the full journey, including the troubleshooting and decisions along the way.

The Starting Point: Security Audit Results

I ran an automated security audit using the aegis agent, which scanned the entire codebase for OWASP Top 10 vulnerabilities. The results showed:

  • 0 critical vulnerabilities
  • 3 high severity issues (2 PHP, 1 npm)
  • 5 medium severity issues
  • 4 low severity issues

The good news: the v0.8.0 security hardening I’d done previously was solid—path traversal, SQL injection, CSRF, and most XSS issues were already addressed. But the audit found two PHP issues and 12 npm vulnerabilities that needed attention.

WordPress Plugin Security Audit: The PHP Fixes

Fix 1: SSRF Vulnerability in Bible Text API

The first issue was in BibleText.php, which fetches Bible passages from external APIs. The code was using wp_remote_get(), which allows requests to any URL—including internal network addresses. This is a Server-Side Request Forgery (SSRF) vulnerability.

The fix was a one-line change:

// Before (vulnerable to SSRF):
$response = wp_remote_get($pageUrl, ['headers' => $headerArray]);

// After (SSRF-safe):
$response = wp_safe_remote_get($pageUrl, ['headers' => $headerArray]);

The wp_safe_remote_get() function is WordPress’s built-in protection against SSRF. It validates the URL against a list of known-safe hosts and blocks requests to private IP ranges (10.x.x.x, 192.168.x.x, localhost, etc.).

Fix 2: XSS in JavaScript Output

The second issue was in SermonEditorPage.php, where Bible passage data was being output directly into JavaScript arrays:

// Before (vulnerable to XSS):
start1.push("<?php echo $startArr[$i]['book'] ?>");

// After (XSS-safe):
start1.push("<?php echo esc_js($startArr[$i]['book']); ?>");

If someone managed to inject malicious JavaScript into the Bible passage data (perhaps via a compromised Bible API response), it would execute in the admin’s browser. Adding esc_js() escapes any JavaScript special characters, preventing code execution.

The npm Vulnerability Challenge

With the PHP issues resolved, I turned to the npm audit results. Running npm audit showed 12 vulnerabilities across packages like ws, tar-fs, cross-spawn, webpack-dev-server, and cookie. All were in development dependencies (@wordpress/scripts), not production code.

The challenge: fixing these required upgrading @wordpress/scripts from version 27.0.0 to 31.4.0—a major version jump that could break the Gutenberg blocks build.

The Git Worktree Approach

Rather than risk breaking the develop branch, I used git worktrees to safely test the breaking change:

# Create isolated worktree from develop branch
git worktree add ../sermon-browser-npm-fix develop

# Work in the worktree
cd ../sermon-browser-npm-fix

# Attempt the upgrade
npm audit fix --force

This creates a separate checkout of the repository where I can test potentially destructive changes without affecting my main working directory.

The Upgrade Process

Running npm audit fix --force upgraded @wordpress/scripts from 27.0.0 to 31.4.0 and resolved 10 of the 12 vulnerabilities. The results:

  • Before: 12 vulnerabilities (7 high, 1 moderate, 3 low)
  • After: 2 vulnerabilities (2 moderate)

The remaining 2 moderate vulnerabilities are in webpack-dev-server ≤5.2.0. These only affect development time (when running npm start with the dev server), and the attack requires a developer to visit a malicious website while the dev server is running. I decided to accept this risk rather than downgrade to an even older version of @wordpress/scripts.

Verifying the Build Still Works

The moment of truth: would the Gutenberg blocks still compile with the new dependency versions?

# Build the blocks
npm run build

# Run the full test suite
composer test

Both passed. No webpack configuration changes were needed, and all 1,422 tests passed with 3,966 assertions. The upgrade was clean.

What Worked Well

  • Git worktrees let me test a potentially breaking change in isolation before committing to it
  • WordPress’s built-in escaping functions (wp_safe_remote_get, esc_js) are purpose-built for these security scenarios
  • @wordpress/scripts maintained backward compatibility across 4 major versions—no config changes needed
  • Comprehensive test suite gave confidence that nothing broke during the upgrade

Key Learnings

1. Dev dependencies still matter for security. Even though these vulnerabilities only affect build time, a compromised build pipeline could inject malicious code into production assets.

2. Accept calculated risks. The remaining webpack-dev-server vulnerabilities are dev-time only with low real-world exploitability. Fixing them would require downgrading @wordpress/scripts, which would reintroduce the 10 vulnerabilities I just fixed.

3. WordPress has good security primitives. Functions like wp_safe_remote_get(), esc_js(), esc_attr(), and esc_html() exist for exactly these scenarios. Use them.

4. Test before merging breaking changes. Git worktrees are an underused feature that make it safe to test destructive operations.

Final State

After merging the worktree changes back to develop and pushing to origin, the Sermon Browser plugin is now:

  • Protected against SSRF attacks via Bible API requests
  • Protected against XSS via malicious Bible passage data
  • Running @wordpress/scripts 31.4.0 with 83% fewer npm vulnerabilities
  • Passing all 1,422 tests

The commits pushed:

  • 321b967 fix(security): add SSRF and XSS protections
  • d6b593e fix(deps): upgrade @wordpress/scripts to 31.4.0 for security

Related Posts

«
»

Leave a Reply