diff --git a/.claude/commands/security-audit.md b/.claude/commands/security-audit.md new file mode 100644 index 000000000..83149c9d7 --- /dev/null +++ b/.claude/commands/security-audit.md @@ -0,0 +1,31 @@ +# Security Audit + +You are about to perform a fully automated WordPress security audit on this repository. Follow every step below precisely and in order. Do not skip steps. Do not ask for confirmation between steps — execute autonomously from start to finish. + +## Scope + +**Always audit the entire repository.** Do not limit the audit to recent commits, the current branch diff, open PRs, or changed files. Every PHP and JS file in the codebase must be analyzed regardless of when it was last modified. + +## Instructions + +1. **Read `CLAUDE.md` in full** before doing anything else. This is your knowledge base for the entire audit. + +2. **Detect product type** — determine if this repo is a plugin or a theme by checking the main PHP file header for `Plugin Name:` or `style.css` for `Theme Name:`. + +3. **Read environment requirements** — check `readme.txt`, `README.md`, `composer.json`, and `package.json` for WordPress version, PHP version, and any other requirements. + +4. **Generate `.wp-env.json`** — following the instructions in CLAUDE.md Step 1. If one already exists, validate and update it. + +5. **Run Semgrep** — execute `bash security-audit.sh semgrep` and wait for it to complete. Read `semgrep-results.json` in full. + +6. **Triage findings** — following CLAUDE.md Step 3, go through every Semgrep finding. Confirm or dismiss each one. Then perform the deep code analysis described in CLAUDE.md Step 4 to find issues Semgrep may have missed. + +7. **Generate PoCs** — for every confirmed vulnerability, create a PoC file in `security-pocs/` following the templates in CLAUDE.md Step 5. + +8. **Run PoCs** — execute `bash security-audit.sh run-pocs` and wait for it to complete. Read `security-poc-results.json` to confirm which vulnerabilities are real and exploitable. + +9. **Write the report** — write `SECURITY_REPORT.md` following the structure in CLAUDE.md Step 7. Only include confirmed, exploitable vulnerabilities. + +10. **Cleanup** — run `bash security-audit.sh cleanup`. + +11. **Summarize** — once complete, give a brief summary in the terminal of how many confirmed vulnerabilities were found and their severity breakdown. \ No newline at end of file diff --git a/.wp-env.json b/.wp-env.json new file mode 100644 index 000000000..d4d36aa0b --- /dev/null +++ b/.wp-env.json @@ -0,0 +1,11 @@ +{ + "core": "WordPress/WordPress#6.5.0", + "phpVersion": "7.4", + "plugins": ["."], + "themes": [], + "config": { + "WP_DEBUG": true, + "WP_DEBUG_LOG": true, + "SCRIPT_DEBUG": true + } +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..83dd48941 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,291 @@ +# WordPress Security Audit Guide + +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. + +--- + +## Product Environment + +Before running the audit, read the following files if they exist to understand environment requirements: +- `readme.txt` or `README.md` — for WordPress and PHP version requirements +- `composer.json` — for PHP version constraints +- `package.json` — for Node/build tooling +- Any existing `.wp-env.json` + +Use this information to generate or update `.wp-env.json` accordingly. + +--- + +## Step 1 — Generate `.wp-env.json` + +If `.wp-env.json` does not exist, create it. If it does exist, validate it has the correct structure. + +The file must: +- 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:`) +- Set the correct WordPress version (from readme.txt or default to `latest`) +- Set the correct PHP version (from composer.json or default to `8.1`) +- Include a test admin user + +Example structure for a plugin: +```json +{ + "core": "WordPress/WordPress#6.5.0", + "phpVersion": "8.1", + "plugins": ["."], + "themes": [], + "config": { + "WP_DEBUG": true, + "WP_DEBUG_LOG": true, + "SCRIPT_DEBUG": true + } +} +``` + +Example structure for a theme: +```json +{ + "core": "WordPress/WordPress#6.5.0", + "phpVersion": "8.1", + "plugins": [], + "themes": ["."], + "config": { + "WP_DEBUG": true, + "WP_DEBUG_LOG": true, + "SCRIPT_DEBUG": true + } +} +``` + +--- + +## Step 2 — Run Semgrep + +Run the helper script to execute Semgrep: + +```bash +bash bin/security-audit.sh semgrep +``` + +This will output `semgrep-results.json`. Read this file fully before proceeding. + +--- + +## Step 3 — Triage Semgrep Findings + +For each finding in `semgrep-results.json`: + +### Confirm or dismiss: +- 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)? +- Check if proper sanitization/escaping is applied before the sink +- Check if nonce verification exists where needed +- Check if capability checks exist where needed + +### Assign severity: +- **Critical** — Unauthenticated exploit, direct data exposure or RCE possible +- **High** — Authenticated exploit with low privilege (subscriber), significant impact +- **Medium** — Authenticated exploit requiring higher privilege (editor+), moderate impact +- **Low** — Requires admin privilege or has limited impact + +### Dismiss if: +- The variable is sanitized/escaped correctly before use +- The function is only accessible to admins and the risk is negligible +- It is a false positive due to Semgrep pattern limitations — document why + +--- + +## Step 4 — Deep Code Analysis + +Beyond Semgrep findings, manually analyze the following high-risk areas: + +### REST API Endpoints +- Find all `register_rest_route()` calls +- Check `permission_callback` — is it `__return_true` or missing? +- Check if parameters are sanitized with `sanitize_*` functions +- **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. + +### AJAX Handlers +- Find all `wp_ajax_` and `wp_ajax_nopriv_` hooks +- Check nonce verification with `check_ajax_referer()` or `wp_verify_nonce()` +- Check capability checks with `current_user_can()` + +### Database Queries +- Find all `$wpdb->query()`, `$wpdb->get_results()`, `$wpdb->get_var()`, `$wpdb->get_row()` +- Confirm all use `$wpdb->prepare()` when user input is involved + +### File Operations +- Find `file_get_contents()`, `file_put_contents()`, `include()`, `require()`, `include_once()`, `require_once()` +- Check if paths are user-controlled + +### Output +- Find `echo`, `print`, `_e()`, `esc_*` usage +- Check unescaped output of user-controlled data + +### Options & User Meta +- Find `get_option()`, `get_user_meta()`, `update_option()`, `update_user_meta()` +- Check if values stored or retrieved are sanitized + +### Shortcodes +- Find `add_shortcode()` — are attributes sanitized before output? + +--- + +## Step 5 — Generate PoCs + +For each confirmed real vulnerability, generate a Proof of Concept. + +Store all PoCs in `security-pocs/` directory. Create one file per vulnerability named `poc-{severity}-{short-name}.sh` or `.php` as appropriate. + +### PoC requirements: +- Must be self-contained and runnable +- Must include comments explaining what it does and what to expect +- Must specify the required user role (unauthenticated / subscriber / editor / admin) +- Use `curl` for HTTP-based exploits +- Use WP-CLI for database/option-based exploits +- Use PHP scripts for complex payloads + +### PoC templates by vulnerability type: + +**SQL Injection (curl):** +```bash +#!/bin/bash +# Vulnerability: SQL Injection in [function name] at [file:line] +# Severity: [severity] +# Required role: Unauthenticated +# Expected result: Database error or data leakage in response + +TARGET="http://localhost:8888" +PAYLOAD="1 UNION SELECT 1,user_login,user_pass,4,5,6,7,8,9,10 FROM wp_users--" + +curl -s -G "$TARGET/wp-admin/admin-ajax.php" \ + --data-urlencode "action=your_action" \ + --data-urlencode "id=$PAYLOAD" +``` + +**XSS (curl):** +```bash +#!/bin/bash +# Vulnerability: Reflected XSS in [function name] at [file:line] +# Severity: [severity] +# Required role: Unauthenticated +# Expected result: appears unescaped in response + +TARGET="http://localhost:8888" +PAYLOAD='' + +curl -s -G "$TARGET/?your_param=$PAYLOAD" | grep -o '' +``` + +**CSRF (HTML form):** +```html + +
+ +``` + +**Privilege Escalation (curl with auth):** +```bash +#!/bin/bash +# Vulnerability: Privilege escalation in [endpoint] at [file:line] +# Severity: [severity] +# Required role: Subscriber +# Expected result: Subscriber can perform admin-only action + +TARGET="http://localhost:8888" + +# Get auth cookie as subscriber +COOKIE=$(curl -s -c - -X POST "$TARGET/wp-login.php" \ + -d "log=subscriber&pwd=password&wp-submit=Log+In&redirect_to=%2F&testcookie=1" \ + -b "wordpress_test_cookie=WP+Cookie+check" | grep wordpress_logged_in | awk '{print $7"="$8}') + +# Fire privileged action as subscriber +curl -s -X POST "$TARGET/wp-admin/admin-ajax.php" \ + -b "$COOKIE" \ + -d "action=privileged_action&data=malicious" +``` + +--- + +## Step 6 — Run PoCs Against wp-env + +Run the helper script to start wp-env and execute all PoCs: + +```bash +bash security-audit.sh run-pocs +``` + +The script will: +1. Start wp-env +2. Create test users (admin, editor, subscriber) with known passwords +3. Execute each PoC in `security-pocs/` +4. Log results to `security-poc-results.json` +5. Stop wp-env + +Read `security-poc-results.json` and determine which vulnerabilities are confirmed exploitable. + +--- + +## Step 7 — Write SECURITY_REPORT.md + +Write a comprehensive security report. Only include **confirmed, exploitable** vulnerabilities. + +### Report structure: + +```markdown +# Security Audit Report — [Plugin/Theme Name] +**Date:** [date] +**Audited by:** Claude Code Automated Security Pipeline +**Environment:** WordPress [version], PHP [version] + +## Summary +- Total confirmed vulnerabilities: X +- Critical: X | High: X | Medium: X | Low: X + +## Findings + +### [SEVERITY] — [Vulnerability Type] in [File] + +**Location:** `path/to/file.php` line X +**Severity:** Critical / High / Medium / Low +**Required role:** Unauthenticated / Subscriber / Editor / Admin + +**Description:** +[Clear explanation of the vulnerability and why it is dangerous] + +**Reproduction:** +[Step by step instructions] + +**Payload / PoC:** +\`\`\`bash +[PoC command or script] +\`\`\` + +**Expected Result:** +[What happens when exploited] + +**Recommended Fix:** +[Specific code-level fix with example] + +--- +``` + +--- + +## Cleanup + +After the report is written, run: + +```bash +bash security-audit.sh cleanup +``` + +This removes temporary files but preserves `SECURITY_REPORT.md` and `security-pocs/`. \ No newline at end of file diff --git a/bin/security-audit.sh b/bin/security-audit.sh new file mode 100644 index 000000000..315793405 --- /dev/null +++ b/bin/security-audit.sh @@ -0,0 +1,246 @@ +#!/bin/bash + +# ============================================================================= +# WordPress Security Audit Helper Script +# Used by Claude Code's /security-audit slash command +# ============================================================================= + +set -e + +PLUGIN_DIR="$(pwd)" +POCS_DIR="$PLUGIN_DIR/security-pocs" +WP_ENV_URL="http://localhost:8888" +WP_CLI="npx @wordpress/env run tests-cli wp" +RESULTS_FILE="$PLUGIN_DIR/security-poc-results.json" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log() { echo -e "${BLUE}[INFO]${NC} $1"; } +success(){ echo -e "${GREEN}[OK]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# ============================================================================= +# COMMAND: semgrep +# Runs Semgrep with WordPress security rulesets +# ============================================================================= +run_semgrep() { + log "Checking Semgrep installation..." + + if ! command -v semgrep &> /dev/null; then + log "Semgrep not found. Installing via pip..." + pip install semgrep --quiet || { + error "Failed to install Semgrep. Please install it manually: pip install semgrep" + exit 1 + } + fi + + success "Semgrep is available." + log "Running Semgrep with WordPress security rulesets..." + + # Run with WordPress-specific and generic PHP security rules + semgrep scan \ + --config "p/wordpress" \ + --config "p/php" \ + --config "p/owasp-top-ten" \ + --json \ + --output semgrep-results.json \ + --quiet \ + "$PLUGIN_DIR" || true # Don't exit on findings + + # Count findings + FINDING_COUNT=$(python3 -c " +import json, sys +try: + data = json.load(open('semgrep-results.json')) + print(len(data.get('results', []))) +except: + print(0) +" 2>/dev/null || echo "0") + + success "Semgrep complete. $FINDING_COUNT findings written to semgrep-results.json" +} + +# ============================================================================= +# COMMAND: run-pocs +# Starts wp-env, creates test users, runs all PoCs, captures results +# ============================================================================= +run_pocs() { + log "Checking wp-env and Docker..." + + # Check Docker + if ! docker info &> /dev/null; then + error "Docker is not running. Please start Docker and try again." + exit 1 + fi + + # Check @wordpress/env + if ! npx --yes @wordpress/env --version &> /dev/null; then + error "Could not run @wordpress/env. Please ensure Node.js and npm are installed." + exit 1 + fi + + # Check .wp-env.json exists + if [ ! -f ".wp-env.json" ]; then + error ".wp-env.json not found. Claude Code should have generated this in Step 4." + exit 1 + fi + + # Check PoCs directory + if [ ! -d "$POCS_DIR" ] || [ -z "$(ls -A $POCS_DIR 2>/dev/null)" ]; then + warn "No PoC files found in security-pocs/. Nothing to run." + echo '{"results": [], "message": "No PoCs were generated."}' > "$RESULTS_FILE" + exit 0 + fi + + # Start wp-env + log "Starting wp-env (this may take a few minutes on first run)..." + npx @wordpress/env start 2>&1 | tail -5 + success "wp-env started." + + # Wait for WordPress to be ready + log "Waiting for WordPress to be ready..." + for i in {1..30}; do + if curl -s "$WP_ENV_URL" | grep -q "WordPress" 2>/dev/null; then + break + fi + sleep 2 + done + + # Create test users + log "Creating test users..." + + $WP_CLI user create subscriber subscriber@test.local \ + --role=subscriber --user_pass=Subscriber123! 2>/dev/null || \ + $WP_CLI user update subscriber --user_pass=Subscriber123! 2>/dev/null || true + + $WP_CLI user create editor editor@test.local \ + --role=editor --user_pass=Editor123! 2>/dev/null || \ + $WP_CLI user update editor --user_pass=Editor123! 2>/dev/null || true + + $WP_CLI user update admin --user_pass=Admin123! 2>/dev/null || true + + success "Test users ready: admin/Admin123!, editor/Editor123!, subscriber/Subscriber123!" + + # Initialize results + echo '{"results": []}' > "$RESULTS_FILE" + + # Run each PoC + POC_COUNT=0 + SUCCESS_COUNT=0 + FAIL_COUNT=0 + + for poc_file in "$POCS_DIR"/*.sh "$POCS_DIR"/*.php "$POCS_DIR"/*.html; do + [ -f "$poc_file" ] || continue + + POC_NAME=$(basename "$poc_file") + POC_COUNT=$((POC_COUNT + 1)) + + log "Running PoC: $POC_NAME" + + # Execute PoC with timeout and capture output + POC_OUTPUT="" + POC_EXIT=0 + + if [[ "$poc_file" == *.sh ]]; then + chmod +x "$poc_file" + POC_OUTPUT=$(timeout 30 bash "$poc_file" 2>&1) || POC_EXIT=$? + elif [[ "$poc_file" == *.php ]]; then + POC_OUTPUT=$(timeout 30 php "$poc_file" 2>&1) || POC_EXIT=$? + elif [[ "$poc_file" == *.html ]]; then + POC_OUTPUT="HTML-based PoC generated. Manual testing required — open the file in a browser while authenticated as the required role." + POC_EXIT=0 + fi + + # Determine result + CONFIRMED=false + if echo "$POC_OUTPUT" | grep -qiE "alert\(1\)|UNION SELECT|ERROR|leaked|password|wp_users|exploited|success|vulnerable|bypassed"; then + CONFIRMED=true + SUCCESS_COUNT=$((SUCCESS_COUNT + 1)) + success " ✓ CONFIRMED: $POC_NAME" + else + FAIL_COUNT=$((FAIL_COUNT + 1)) + warn " ✗ Not confirmed (may be false positive or env issue): $POC_NAME" + fi + + # Append to results JSON + POC_OUTPUT_ESCAPED=$(echo "$POC_OUTPUT" | python3 -c "import sys, json; print(json.dumps(sys.stdin.read()))" 2>/dev/null || echo "\"output unavailable\"") + + python3 - <