|
| 1 | +# WordPress Security Audit Guide |
| 2 | + |
| 3 | +This file defines how Claude Code should conduct automated security audits on this WordPress plugin/theme. It is used in conjunction with the `/security-audit` slash command. |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## Product Environment |
| 8 | + |
| 9 | +Before running the audit, read the following files if they exist to understand environment requirements: |
| 10 | +- `readme.txt` or `README.md` — for WordPress and PHP version requirements |
| 11 | +- `composer.json` — for PHP version constraints |
| 12 | +- `package.json` — for Node/build tooling |
| 13 | +- Any existing `.wp-env.json` |
| 14 | + |
| 15 | +Use this information to generate or update `.wp-env.json` accordingly. |
| 16 | + |
| 17 | +--- |
| 18 | + |
| 19 | +## Step 1 — Generate `.wp-env.json` |
| 20 | + |
| 21 | +If `.wp-env.json` does not exist, create it. If it does exist, validate it has the correct structure. |
| 22 | + |
| 23 | +The file must: |
| 24 | +- Mount the current directory as a plugin or theme (detect by checking for `*plugin*` in the main PHP file header or a `style.css` with `Theme Name:`) |
| 25 | +- Set the correct WordPress version (from readme.txt or default to `latest`) |
| 26 | +- Set the correct PHP version (from composer.json or default to `8.1`) |
| 27 | +- Include a test admin user |
| 28 | + |
| 29 | +Example structure for a plugin: |
| 30 | +```json |
| 31 | +{ |
| 32 | + "core": "WordPress/WordPress#6.5.0", |
| 33 | + "phpVersion": "8.1", |
| 34 | + "plugins": ["."], |
| 35 | + "themes": [], |
| 36 | + "config": { |
| 37 | + "WP_DEBUG": true, |
| 38 | + "WP_DEBUG_LOG": true, |
| 39 | + "SCRIPT_DEBUG": true |
| 40 | + } |
| 41 | +} |
| 42 | +``` |
| 43 | + |
| 44 | +Example structure for a theme: |
| 45 | +```json |
| 46 | +{ |
| 47 | + "core": "WordPress/WordPress#6.5.0", |
| 48 | + "phpVersion": "8.1", |
| 49 | + "plugins": [], |
| 50 | + "themes": ["."], |
| 51 | + "config": { |
| 52 | + "WP_DEBUG": true, |
| 53 | + "WP_DEBUG_LOG": true, |
| 54 | + "SCRIPT_DEBUG": true |
| 55 | + } |
| 56 | +} |
| 57 | +``` |
| 58 | + |
| 59 | +--- |
| 60 | + |
| 61 | +## Step 2 — Run Semgrep |
| 62 | + |
| 63 | +Run the helper script to execute Semgrep: |
| 64 | + |
| 65 | +```bash |
| 66 | +bash bin/security-audit.sh semgrep |
| 67 | +``` |
| 68 | + |
| 69 | +This will output `semgrep-results.json`. Read this file fully before proceeding. |
| 70 | + |
| 71 | +--- |
| 72 | + |
| 73 | +## Step 3 — Triage Semgrep Findings |
| 74 | + |
| 75 | +For each finding in `semgrep-results.json`: |
| 76 | + |
| 77 | +### Confirm or dismiss: |
| 78 | +- Trace the vulnerable variable back to its source. Is it user-controlled input (`$_GET`, `$_POST`, `$_REQUEST`, `$_COOKIE`, REST API params, `get_option()` if user-controlled)? |
| 79 | +- Check if proper sanitization/escaping is applied before the sink |
| 80 | +- Check if nonce verification exists where needed |
| 81 | +- Check if capability checks exist where needed |
| 82 | + |
| 83 | +### Assign severity: |
| 84 | +- **Critical** — Unauthenticated exploit, direct data exposure or RCE possible |
| 85 | +- **High** — Authenticated exploit with low privilege (subscriber), significant impact |
| 86 | +- **Medium** — Authenticated exploit requiring higher privilege (editor+), moderate impact |
| 87 | +- **Low** — Requires admin privilege or has limited impact |
| 88 | + |
| 89 | +### Dismiss if: |
| 90 | +- The variable is sanitized/escaped correctly before use |
| 91 | +- The function is only accessible to admins and the risk is negligible |
| 92 | +- It is a false positive due to Semgrep pattern limitations — document why |
| 93 | + |
| 94 | +--- |
| 95 | + |
| 96 | +## Step 4 — Deep Code Analysis |
| 97 | + |
| 98 | +Beyond Semgrep findings, manually analyze the following high-risk areas: |
| 99 | + |
| 100 | +### REST API Endpoints |
| 101 | +- Find all `register_rest_route()` calls |
| 102 | +- Check `permission_callback` — is it `__return_true` or missing? |
| 103 | +- Check if parameters are sanitized with `sanitize_*` functions |
| 104 | +- **For endpoints that accept settings objects or arrays** (e.g. `visualizer-settings`, `meta`, `config`): trace each individual field through to where it is stored (post meta, options) and where it is later output (admin pages, frontend). Verify that each field is either sanitized on save (`sanitize_text_field()`, `wp_kses()`, `absint()`, etc.) or escaped on every output (`esc_attr()`, `esc_html()`, `wp_kses_post()`). A valid `permission_callback` does not make the stored data safe — a contributor-level user can still inject a stored XSS payload that executes when an admin views the data. |
| 105 | + |
| 106 | +### AJAX Handlers |
| 107 | +- Find all `wp_ajax_` and `wp_ajax_nopriv_` hooks |
| 108 | +- Check nonce verification with `check_ajax_referer()` or `wp_verify_nonce()` |
| 109 | +- Check capability checks with `current_user_can()` |
| 110 | + |
| 111 | +### Database Queries |
| 112 | +- Find all `$wpdb->query()`, `$wpdb->get_results()`, `$wpdb->get_var()`, `$wpdb->get_row()` |
| 113 | +- Confirm all use `$wpdb->prepare()` when user input is involved |
| 114 | + |
| 115 | +### File Operations |
| 116 | +- Find `file_get_contents()`, `file_put_contents()`, `include()`, `require()`, `include_once()`, `require_once()` |
| 117 | +- Check if paths are user-controlled |
| 118 | + |
| 119 | +### Output |
| 120 | +- Find `echo`, `print`, `_e()`, `esc_*` usage |
| 121 | +- Check unescaped output of user-controlled data |
| 122 | + |
| 123 | +### Options & User Meta |
| 124 | +- Find `get_option()`, `get_user_meta()`, `update_option()`, `update_user_meta()` |
| 125 | +- Check if values stored or retrieved are sanitized |
| 126 | + |
| 127 | +### Shortcodes |
| 128 | +- Find `add_shortcode()` — are attributes sanitized before output? |
| 129 | + |
| 130 | +--- |
| 131 | + |
| 132 | +## Step 5 — Generate PoCs |
| 133 | + |
| 134 | +For each confirmed real vulnerability, generate a Proof of Concept. |
| 135 | + |
| 136 | +Store all PoCs in `security-pocs/` directory. Create one file per vulnerability named `poc-{severity}-{short-name}.sh` or `.php` as appropriate. |
| 137 | + |
| 138 | +### PoC requirements: |
| 139 | +- Must be self-contained and runnable |
| 140 | +- Must include comments explaining what it does and what to expect |
| 141 | +- Must specify the required user role (unauthenticated / subscriber / editor / admin) |
| 142 | +- Use `curl` for HTTP-based exploits |
| 143 | +- Use WP-CLI for database/option-based exploits |
| 144 | +- Use PHP scripts for complex payloads |
| 145 | + |
| 146 | +### PoC templates by vulnerability type: |
| 147 | + |
| 148 | +**SQL Injection (curl):** |
| 149 | +```bash |
| 150 | +#!/bin/bash |
| 151 | +# Vulnerability: SQL Injection in [function name] at [file:line] |
| 152 | +# Severity: [severity] |
| 153 | +# Required role: Unauthenticated |
| 154 | +# Expected result: Database error or data leakage in response |
| 155 | + |
| 156 | +TARGET="http://localhost:8888" |
| 157 | +PAYLOAD="1 UNION SELECT 1,user_login,user_pass,4,5,6,7,8,9,10 FROM wp_users--" |
| 158 | + |
| 159 | +curl -s -G "$TARGET/wp-admin/admin-ajax.php" \ |
| 160 | + --data-urlencode "action=your_action" \ |
| 161 | + --data-urlencode "id=$PAYLOAD" |
| 162 | +``` |
| 163 | + |
| 164 | +**XSS (curl):** |
| 165 | +```bash |
| 166 | +#!/bin/bash |
| 167 | +# Vulnerability: Reflected XSS in [function name] at [file:line] |
| 168 | +# Severity: [severity] |
| 169 | +# Required role: Unauthenticated |
| 170 | +# Expected result: <script>alert(1)</script> appears unescaped in response |
| 171 | + |
| 172 | +TARGET="http://localhost:8888" |
| 173 | +PAYLOAD='<script>alert(1)</script>' |
| 174 | + |
| 175 | +curl -s -G "$TARGET/?your_param=$PAYLOAD" | grep -o '<script>alert(1)</script>' |
| 176 | +``` |
| 177 | + |
| 178 | +**CSRF (HTML form):** |
| 179 | +```html |
| 180 | +<!-- |
| 181 | + Vulnerability: CSRF in [action] at [file:line] |
| 182 | + Severity: [severity] |
| 183 | + Required role: Victim must be logged in as [role] |
| 184 | + Expected result: Action executes without user consent |
| 185 | + Instructions: Host this file and trick a logged-in user to open it |
| 186 | +--> |
| 187 | +<form method="POST" action="http://localhost:8888/wp-admin/admin-ajax.php"> |
| 188 | + <input type="hidden" name="action" value="your_action"> |
| 189 | + <input type="hidden" name="data" value="malicious_value"> |
| 190 | + <input type="submit" value="Click me"> |
| 191 | +</form> |
| 192 | +<script>document.forms[0].submit();</script> |
| 193 | +``` |
| 194 | + |
| 195 | +**Privilege Escalation (curl with auth):** |
| 196 | +```bash |
| 197 | +#!/bin/bash |
| 198 | +# Vulnerability: Privilege escalation in [endpoint] at [file:line] |
| 199 | +# Severity: [severity] |
| 200 | +# Required role: Subscriber |
| 201 | +# Expected result: Subscriber can perform admin-only action |
| 202 | + |
| 203 | +TARGET="http://localhost:8888" |
| 204 | + |
| 205 | +# Get auth cookie as subscriber |
| 206 | +COOKIE=$(curl -s -c - -X POST "$TARGET/wp-login.php" \ |
| 207 | + -d "log=subscriber&pwd=password&wp-submit=Log+In&redirect_to=%2F&testcookie=1" \ |
| 208 | + -b "wordpress_test_cookie=WP+Cookie+check" | grep wordpress_logged_in | awk '{print $7"="$8}') |
| 209 | + |
| 210 | +# Fire privileged action as subscriber |
| 211 | +curl -s -X POST "$TARGET/wp-admin/admin-ajax.php" \ |
| 212 | + -b "$COOKIE" \ |
| 213 | + -d "action=privileged_action&data=malicious" |
| 214 | +``` |
| 215 | + |
| 216 | +--- |
| 217 | + |
| 218 | +## Step 6 — Run PoCs Against wp-env |
| 219 | + |
| 220 | +Run the helper script to start wp-env and execute all PoCs: |
| 221 | + |
| 222 | +```bash |
| 223 | +bash security-audit.sh run-pocs |
| 224 | +``` |
| 225 | + |
| 226 | +The script will: |
| 227 | +1. Start wp-env |
| 228 | +2. Create test users (admin, editor, subscriber) with known passwords |
| 229 | +3. Execute each PoC in `security-pocs/` |
| 230 | +4. Log results to `security-poc-results.json` |
| 231 | +5. Stop wp-env |
| 232 | + |
| 233 | +Read `security-poc-results.json` and determine which vulnerabilities are confirmed exploitable. |
| 234 | + |
| 235 | +--- |
| 236 | + |
| 237 | +## Step 7 — Write SECURITY_REPORT.md |
| 238 | + |
| 239 | +Write a comprehensive security report. Only include **confirmed, exploitable** vulnerabilities. |
| 240 | + |
| 241 | +### Report structure: |
| 242 | + |
| 243 | +```markdown |
| 244 | +# Security Audit Report — [Plugin/Theme Name] |
| 245 | +**Date:** [date] |
| 246 | +**Audited by:** Claude Code Automated Security Pipeline |
| 247 | +**Environment:** WordPress [version], PHP [version] |
| 248 | + |
| 249 | +## Summary |
| 250 | +- Total confirmed vulnerabilities: X |
| 251 | +- Critical: X | High: X | Medium: X | Low: X |
| 252 | + |
| 253 | +## Findings |
| 254 | + |
| 255 | +### [SEVERITY] — [Vulnerability Type] in [File] |
| 256 | + |
| 257 | +**Location:** `path/to/file.php` line X |
| 258 | +**Severity:** Critical / High / Medium / Low |
| 259 | +**Required role:** Unauthenticated / Subscriber / Editor / Admin |
| 260 | + |
| 261 | +**Description:** |
| 262 | +[Clear explanation of the vulnerability and why it is dangerous] |
| 263 | + |
| 264 | +**Reproduction:** |
| 265 | +[Step by step instructions] |
| 266 | + |
| 267 | +**Payload / PoC:** |
| 268 | +\`\`\`bash |
| 269 | +[PoC command or script] |
| 270 | +\`\`\` |
| 271 | + |
| 272 | +**Expected Result:** |
| 273 | +[What happens when exploited] |
| 274 | + |
| 275 | +**Recommended Fix:** |
| 276 | +[Specific code-level fix with example] |
| 277 | + |
| 278 | +--- |
| 279 | +``` |
| 280 | + |
| 281 | +--- |
| 282 | + |
| 283 | +## Cleanup |
| 284 | + |
| 285 | +After the report is written, run: |
| 286 | + |
| 287 | +```bash |
| 288 | +bash security-audit.sh cleanup |
| 289 | +``` |
| 290 | + |
| 291 | +This removes temporary files but preserves `SECURITY_REPORT.md` and `security-pocs/`. |
0 commit comments