# WordPress Security Audit Report — SeedVault Local

**Date:** 2026-07-28  
**Target:** `/var/www/seedvault-local` (Apache2 :8899, Tailscale 100.90.189.104)  
**WordPress Version:** 7.0.2  
**PHP Version:** 8.4.23  

---

## Executive Summary

**15 findings** across 4 severity levels: 3 CRITICAL, 6 HIGH, 4 MEDIUM, 2 LOW.

The most urgent issues are **wp-config.php served over HTTP** (full DB credentials exposed), **backup files publicly downloadable** (~14 GB of .wpress archives), and **XML-RPC fully functional** despite a mu-plugin attempting to disable it.

---

## Findings

### 🔴 CRITICAL

#### C1: `wp-config.php` Accessible via HTTP (HTTP 200)

| Field | Detail |
|-------|--------|
| **Finding** | `curl http://localhost:8899/wp-config.php` returns HTTP 200 with full file contents |
| **Risk** | Database credentials, auth keys, salts, and site configuration fully exposed to anyone who can reach the server |
| **Remediation** | Block access via Apache config or .htaccess. Add to Apache VirtualHost or `/etc/apache2/conf-available/security.conf`:<br>`<Files wp-config.php>`<br>`  Require all denied`<br>`</Files>`<br><br>Or add to `.htaccess` (before WordPress rewrite rules):<br>`<Files wp-config.php>`<br>`  Require all denied`<br>`</Files>`<br><br>⚠️ **Note:** If Apache is currently serving wp-config.php as plain text, `.htaccess` itself may not be processed. The Apache `<Files>` directive in the VirtualHost is the authoritative fix. |

#### C2: Full Site Backup Files (.wpress) Downloadable Over HTTP

| Field | Detail |
|-------|--------|
| **Finding** | Three `.wpress` migration archives in `wp-content/ai1wm-backups/` are directly downloadable (HTTP 200), totaling **~14 GB**:<br>• `20260709-113104-qetn9z38p9oh.wpress` (5.7 GB)<br>• `20260709-113508-9i6r7w8eop6i.wpress` (5.7 GB)<br>• `seedvault-market-20260617-145934-93pc6pbpdp3m.wpress` (2.6 GB) |
| **Risk** | Full site dump including DB, plugins, media. Attackers get everything: DB credentials, content, plugin configs, API keys stored in options |
| **Remediation** | 1. Add `.htaccess` to `wp-content/ai1wm-backups/`: `Deny from all`<br>2. Move directory outside web root: `sudo mv /var/www/seedvault-local/wp-content/ai1wm-backups /home/vincent/seedvault-backups/`<br>3. Delete old backups no longer needed |

#### C3: `wp-admin/install.php` Accessible (HTTP 200)

| Field | Detail |
|-------|--------|
| **Finding** | `curl http://localhost:8899/wp-admin/install.php` returns HTTP 200 |
| **Risk** | Attacker with admin session could re-run installer, resetting the entire site (DB tables, admin password) |
| **Remediation** | Delete the file: `sudo rm /var/www/seedvault-local/wp-admin/install.php` — WP deletes this automatically after install, but it was left behind |

---

### 🟠 HIGH

#### H1: XML-RPC Fully Functional Despite Mu-Plugin Attempt

| Field | Detail |
|-------|--------|
| **Finding** | POST to `xmlrpc.php` returns full `system.listMethods` with 67+ methods including `system.multicall`, `demo.*`, `wp.getUsers`, `wp.setOptions`, `wp.uploadFile`, `metaWeblog.newMediaObject` |
| **Details** | The mu-plugin `seedvault-security.php` calls `add_filter('xmlrpc_enabled', '__return_false')` — but this filter does **not** actually disable XML-RPC. WordPress doesn't check this filter to decide whether to process xmlrpc.php requests. The filter was added in WP 5.6 as a DB option flag, not a method gate. |
| **Risk** | Brute-force login via `system.multicall` (100x amplification), content manipulation, file upload via `metaWeblog.newMediaObject`, user enumeration |
| **Remediation** | Option A (recommended) — Block in Apache VirtualHost:<br>`<Files xmlrpc.php>`<br>`  Require all denied`<br>`</Files>`<br>Option B — Delete the filter from mu-plugin and add to `functions.php` or mu-plugin:<br>`add_filter('xmlrpc_methods', function($m) { return array(); });`<br>Option C — Plugin: "Disable XML-RPC" or "Better Security" |

#### H2: Default Table Prefix `wp_`

| Field | Detail |
|-------|--------|
| **Finding** | `$table_prefix = 'wp_'` — the WordPress default |
| **Risk** | Makes SQL injection attacks trivially guessable. Combined with any SQL injection vulnerability in plugins, attackers can easily target known table names |
| **Remediation** | Run a prefix change script: `sudo wp db prefix change sv_ --path=/var/www/seedvault-local --allow-root`<br>⚠️ **Backup DB first.** Test thoroughly as some plugins store table names as hardcoded strings. |

#### H3: `readme.html` Exposed (HTTP 200)

| Field | Detail |
|-------|--------|
| **Finding** | `curl http://localhost:8899/readme.html` returns HTTP 200 with WordPress version info |
| **Risk** | Confirms WordPress version, aids targeted exploitation |
| **Remediation** | `sudo rm /var/www/seedvault-local/readme.html` — remove after every WP update |

#### H4: REST API Exposes Full User Data Including WooCommerce Meta

| Field | Detail |
|-------|--------|
| **Finding** | `GET /wp-json/wp/v2/users` returns complete user object for `justinhenshaw03` (super_admin) including: email, Gravatar URL, extensive WooCommerce payment preferences, Elementor intro flags, and Jetpack author URL (`http://instantartist44.wordpress.com`) |
| **Risk** | Username + email + role confirmed. Jetpack WordPress.com URL leaks linked identity. WooCommerce meta reveals payment plugin configuration details |
| **Remediation** | In mu-plugin, add:<br>`add_filter('rest_authentication_errors', function($result) {`<br>`  if (!is_user_logged_in() && strpos($_SERVER['REQUEST_URI'], '/wp-json/wp/v2/users') !== false) {`<br>`    return new WP_Error('rest_forbidden', 'Forbidden', array('status' => 401));`<br>`  }`<br>`  return $result;`<br>`});`<br><br>Or install "WP REST API Control" plugin to restrict unauthenticated access |

#### H5: wp-login.php Unprotected

| Field | Detail |
|-------|--------|
| **Finding** | `wp-login.php` returns HTTP 200 — fully accessible with no rate limiting, CAPTCHA, or IP restriction |
| **Risk** | Direct brute-force target. Login error messages are generic (mu-plugin hides them), but no actual brute-force protection |
| **Remediation** | Option A — Install "Wordfence" or "Limit Login Attempts Reloaded" plugin<br>Option B — Restrict by IP in Apache:<br>`<Location /wp-login.php>`<br>`  Require ip 100.64.0.0/10`<br>`</Location>`<br>Option C — Use a login-rename plugin (e.g., "WPS Hide Login") |

#### H6: Security Headers Missing HSTS and CSP

| Field | Detail |
|-------|--------|
| **Finding** | Present headers are good (`X-Frame-Options`, `X-Content-Type-Options`, `XSS-Protection`, `Referrer-Policy`, `Permissions-Policy`, `COOP`, `COEP`). Missing: `Strict-Transport-Security` and `Content-Security-Policy` |
| **Risk** | Without HSTS, MITM attacks possible on initial connections. Without CSP, XSS payloads have no mitigation |
| **Remediation** | Add to mu-plugin `seedvault-security.php`:<br>`header("Strict-Transport-Security: max-age=31536000; includeSubDomains");`<br>⚠️ **Only after HTTPS is configured** — adding HSTS before HTTPS breaks the site permanently.<br><br>For CSP, audit inline scripts first (Elementor injects heavily). A strict CSP is challenging with Elementor; start with `script-src 'self' 'unsafe-inline' https:` |

---

### 🟡 MEDIUM

#### M1: File Permissions — `wp-config.php` is `644` (World-Readable)

| Field | Detail |
|-------|--------|
| **Finding** | `wp-config.php` permissions: `-rw-r--r--` (644) owned by `www-data:www-data` |
| **Risk** | Any compromised process on the system can read DB credentials and auth keys |
| **Remediation** | `sudo chmod 400 /var/www/seedvault-local/wp-config.php`<br>Owner should be `root:www-data` or `root:root`: `sudo chown root:www-data /var/www/seedvault-local/wp-config.php` |

#### M2: `wp-content/uploads/` Permissions `777`

| Field | Detail |
|-------|--------|
| **Finding** | All upload subdirectories are `777` (world-writable): `uploads/`, `2025/`, `2026/`, `elementor/`, `mailpoet/`, etc. |
| **Risk** | Any local user or compromised process can write arbitrary files (including PHP) to uploads |
| **Remediation** | `sudo chmod -R 755 /var/www/seedvault-local/wp-content/uploads/`<br>`sudo find /var/www/seedvault-local/wp-content/uploads/ -type f -exec chmod 644 {} +` |

#### M3: `wp-content/plugins/` and `wp-content/themes/` Permissions `777`

| Field | Detail |
|-------|--------|
| **Finding** | `drwxrwxrwx` (777) on both `plugins/` and `themes/` directories |
| **Risk** | Any local user can modify plugin/theme files, potentially injecting malicious code |
| **Remediation** | `sudo chmod 755 /var/www/seedvault-local/wp-content/plugins /var/www/seedvault-local/wp-content/themes` |

#### M4: Outdated Active Plugins

| Field | Detail |
|-------|--------|
| **Finding** | 7 active plugins with available updates:<br>• `crowdsignal-forms` 1.8.1 → **1.8.2** (auto_update: OFF)<br>• `google-listings-and-ads` 3.7.1 → **3.8.1** (auto_update: OFF)<br>• `gutenberg` 23.3.2 → **23.6.2** (auto_update: OFF)<br>• `jetpack` 16.0-a.1 → **16.0.1** (alpha → stable, auto_update: OFF)<br>• `zero-bs-crm` 6.7.2 → **6.8.2** (auto_update: OFF)<br>• `mailpoet` 5.34.2 → **5.34.3** (auto_update: OFF)<br>• `woocommerce-services` 3.6.5 → **3.6.10** (auto_update: OFF)<br>• `polldaddy` 3.1.6 → 3.1.8 (**INACTIVE** but outdated)<br><br>Also: Astra theme 4.11.18 → **4.13.8** (active, auto_update: OFF) |
| **Risk** | Known CVEs in older versions. WooCommerce Services gap (3.6.5→3.6.10) is significant. Jetpack alpha version may have unpatched bugs |
| **Remediation** | `sudo wp plugin update crowdsignal-forms google-listings-and-ads gutenberg jetpack zero-bs-crm mailpoet woocommerce-services --path=/var/www/seedvault-local --allow-root`<br>`sudo wp theme update astra --path=/var/www/seedvault-local --allow-root`<br><br>Enable auto_update for plugins currently set to OFF: `sudo wp plugin update-all --path=/var/www/seedvault-local --allow-root` |

---

### 🟢 LOW

#### L1: Apache `ServerTokens OS` and `ServerSignature On`

| Field | Detail |
|-------|--------|
| **Finding** | Apache reveals `Server: Apache/2.4.68 (Debian)` in headers. `ServerSignature On` shows version on error pages. `TraceEnable Off` is correct. |
| **Risk** | Information disclosure aids targeted attacks against Apache 2.4.68-specific vulnerabilities |
| **Remediation** | Edit `/etc/apache2/conf-available/security.conf`:<br>`ServerTokens Prod`<br>`ServerSignature Off`<br>Then `sudo systemctl reload apache2` |

#### L2: `WP_DEBUG` is `false` (Correct)

| Field | Detail |
|-------|--------|
| **Finding** | `WP_DEBUG` is set to `false`. `display_errors` in PHP is `Off`. |
| **Risk** | None — correctly configured. Included for completeness. |
| **Status** | ✅ PASS |

---

## Positive Findings (Already Hardened)

| Check | Status | Detail |
|-------|--------|--------|
| ✅ Security keys/salts | PASS | Unique, non-default keys present |
| ✅ `WP_DEBUG` | PASS | Set to `false` |
| ✅ `display_errors` | PASS | PHP `display_errors = Off` |
| ✅ Security headers | PASS | X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Referrer-Policy, Permissions-Policy, COOP, COEP |
| ✅ `DISALLOW_FILE_EDIT` | PASS | Defined in mu-plugin |
| ✅ Login error messages | PASS | Generic "Invalid credentials" |
| ✅ wp-login.php | PASS | Exists but returns 200 (expected — needs rate limiting per H5) |
| ✅ `wp-content/uploads/` | PASS | Returns 403 (directory listing blocked) |
| ✅ `wp-content/plugins/` | PASS | Returns 200 but empty (index.php prevents listing) |
| ✅ `wp-content/themes/` | PASS | Returns 200 but empty (index.php prevents listing) |
| ✅ `.htaccess.bak` | PASS | Returns 403 (Apache blocks dotfiles) |
| ✅ `backups/` dir | PASS | Returns 403 |
| ✅ Static WP_HOME | PASS | Hardcoded to `100.90.189.104:8899` — no HTTP_HOST injection |
| ✅ `DISABLE_WP_CRON` | PASS | Set to `false` (system cron should be configured) |
| ✅ Jetpack WAF | PASS | `jetpack-waf/` directory present with bootstrap |

---

## Remediation Priority Order

1. **[IMMEDIATE]** C1: Block `wp-config.php` HTTP access — Apache VirtualHost `<Files>` directive
2. **[IMMEDIATE]** C2: Remove/restrict `.wpress` backup files from web-accessible directory
3. **[IMMEDIATE]** C3: Delete `wp-admin/install.php`
4. **[TODAY]** H1: Disable XML-RPC via Apache `<Files xmlrpc.php> Require all denied`
5. **[TODAY]** H5: Add rate limiting to `wp-login.php` (plugin or Apache IP restriction)
6. **[TODAY]** M1/M2/M3: Fix file permissions (wp-config.php 400, uploads/plugins/themes 755)
7. **[THIS WEEK]** H4: Restrict REST API user enumeration
8. **[THIS WEEK]** H2: Change table prefix from `wp_` to custom (requires careful migration)
9. **[THIS WEEK]** H3: Remove `readme.html`
10. **[THIS WEEK]** M4: Update all outdated plugins and Astra theme
11. **[BEFORE LAUNCH]** H6: Add HSTS (only after HTTPS via Cloudflare tunnel)
12. **[BEFORE LAUNCH]** L1: Set `ServerTokens Prod` and `ServerSignature Off`

---

## Quick-Fix Commands

```bash
# Delete install.php
sudo rm /var/www/seedvault-local/wp-admin/install.php

# Fix wp-config.php permissions
sudo chown root:www-data /var/www/seedvault-local/wp-config.php
sudo chmod 400 /var/www/seedvault-local/wp-config.php

# Fix upload/plugin/theme permissions
sudo chmod 755 /var/www/seedvault-local/wp-content/uploads /var/www/seedvault-local/wp-content/plugins /var/www/seedvault-local/wp-content/themes
sudo find /var/www/seedvault-local/wp-content/uploads/ -type f -exec chmod 644 {} +

# Move backup files out of web root
sudo mv /var/www/seedvault-local/wp-content/ai1wm-backups /home/vincent/seedvault-backups/

# Remove readme.html
sudo rm /var/www/seedvault-local/readme.html

# Update plugins
sudo wp plugin update crowdsignal-forms google-listings-and-ads gutenberg jetpack zero-bs-crm mailpoet woocommerce-services --path=/var/www/seedvault-local --allow-root
sudo wp theme update astra --path=/var/www/seedvault-local --allow-root

# Block wp-config.php, xmlrpc.php, install.php in Apache
# Add to /etc/apache2/sites-enabled/seedvault.conf or create conf-available file:
# <FilesMatch "^.(wp-config|install)\.php$">
#   Require all denied
# </FilesMatch>
# <Files xmlrpc.php>
#   Require all denied
# </Files>
```
