Skip to content
Back to skills

Orbit Ajax Fuzzer

ASecurity

Discover every `wp_ajax_*` and `wp_ajax_nopriv_*` action in a WordPress plugin and fuzz-test admin-ajax.php endpoints with malformed payloads, missing nonces, anonymous access attempts, and injection vectors. Use when the user says "AJAX fuzzer", "admin-ajax security", "test AJAX handlers", or after adding any new `add_action('wp_ajax_*', ...)`.

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 9, 2026
testingphpbashapisecurity

Works with

  • api

Security analysis

A100/100

Scanned September 9, 2026

npx -y skills add adityaarsharma/orbit --skill orbit-ajax-fuzzer --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Orbit Ajax Fuzzer?

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

Security grade badge for Orbit Ajax Fuzzer
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/adityaarsharma-orbit-ajax-fuzzer/badge)](https://www.skillsdirectory.com/skills/adityaarsharma-orbit-ajax-fuzzer)

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-ajax-fuzzer
description: Discover every `wp_ajax_*` and `wp_ajax_nopriv_*` action in a WordPress plugin and fuzz-test admin-ajax.php endpoints with malformed payloads, missing nonces, anonymous access attempts, and injection vectors. Use when the user says "AJAX fuzzer", "admin-ajax security", "test AJAX handlers", or after adding any new `add_action('wp_ajax_*', ...)`.
---

# 🪐 orbit-ajax-fuzzer — admin-ajax.php fuzzing

REST has a fuzzer. Legacy AJAX (`admin-ajax.php`) needs one too — it's a different attack surface, often less hardened.

---

## Quick start

```bash
PLUGIN_SLUG=my-plugin \
WP_TEST_URL=http://localhost:8881 \
  bash ~/Claude/orbit/scripts/ajax-fuzz.sh
```

Output: `reports/ajax-fuzz-<timestamp>.md`.

---

## How it works

1. Scan plugin source for `add_action('wp_ajax_*', ...)` and `add_action('wp_ajax_nopriv_*', ...)`
2. For each:
   - Detect the handler function
   - Check for nonce verification (`check_ajax_referer`, `wp_verify_nonce`)
   - Check for capability check (`current_user_can`)
   - Detect what params it accepts
3. Send fuzz requests to `/wp-admin/admin-ajax.php?action=<action>`:
   - **Logged out** (for `nopriv` actions only)
   - **Logged in but no nonce** — must reject if action has `check_ajax_referer`
   - **Logged in with wrong nonce** — must reject
   - **Logged in with correct nonce, no capability** — must reject if `current_user_can` is required
   - **Type juggling, injection, oversized body** — same suite as REST fuzzer
4. Record responses, flag misses

---

## What it catches

### Missing nonce check
```php
// ❌ Vulnerable — no nonce, accepts any logged-in user
add_action( 'wp_ajax_my_save', function() {
  update_option( 'my_setting', $_POST['value'] );
  wp_send_json_success();
} );

// ✅
add_action( 'wp_ajax_my_save', function() {
  check_ajax_referer( 'my_save_nonce', 'nonce' );
  if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error( 'Forbidden', 403 );
  $value = sanitize_text_field( wp_unslash( $_POST['value'] ?? '' ) );
  update_option( 'my_setting', $value );
  wp_send_json_success();
} );
```

### Missing capability check
```php
// ❌ Any logged-in user (incl. subscribers) can hit this
add_action( 'wp_ajax_my_admin_action', function() {
  check_ajax_referer( 'my_nonce', 'nonce' );
  delete_post( $_POST['id'] );
} );

// ✅
add_action( 'wp_ajax_my_admin_action', function() {
  check_ajax_referer( 'my_nonce', 'nonce' );
  if ( ! current_user_can( 'delete_posts' ) ) wp_send_json_error( '', 403 );
  // ...
} );
```

### `wp_ajax_nopriv_*` handlers (the dangerous ones)
```php
// ❌ DANGEROUS — public AJAX endpoint with no validation
add_action( 'wp_ajax_nopriv_my_public', function() {
  $email = $_POST['email'];
  // do stuff with email — no nonce, no rate limit, no validation
} );

// ✅ When you must have public AJAX:
add_action( 'wp_ajax_nopriv_my_public', function() {
  // 1. Sanitize aggressively
  $email = sanitize_email( $_POST['email'] ?? '' );
  if ( ! is_email( $email ) ) wp_send_json_error( 'Invalid email' );

  // 2. Rate limit (e.g. 3 per IP per minute)
  $ip = $_SERVER['REMOTE_ADDR'];
  $key = 'my_plugin_rate_' . md5( $ip );
  if ( ( get_transient( $key ) ?: 0 ) > 3 ) wp_send_json_error( 'Rate limited', 429 );
  set_transient( $key, ( get_transient( $key ) ?: 0 ) + 1, MINUTE_IN_SECONDS );

  // 3. Optional: simple proof-of-work nonce (CSRF-style)
  if ( ! isset( $_POST['challenge'] ) || ! wp_verify_nonce( $_POST['challenge'], 'public_form' ) ) {
    wp_send_json_error( 'Invalid request' );
  }

  // ...
} );
```

### Direct PHP access bypass
admin-ajax.php loads WP. If the handler reads `$_POST` directly without sanitisation, an attacker can:
```bash
curl -X POST 'https://target.example.com/wp-admin/admin-ajax.php' \
  -d 'action=my_save' \
  -d "value=' OR 1=1--"
```

The fuzzer simulates this exact request.

---

## Example output

```markdown
# AJAX Fuzz — my-plugin

## Handlers discovered: 6

### wp_ajax_my_save (logged-in only)
**Code:** includes/class-admin.php:42

✓ Has check_ajax_referer
❌ MISSING capability check — any logged-in user (incl. subscribers) can save settings
❌ Param `value` not sanitized — direct DB write
❌ Param `value` not escaped in admin notice on response

#### Severity: Critical

#### Reproduce
```bash
# As subscriber-level user
curl -X POST 'http://localhost:8881/wp-admin/admin-ajax.php' \
  -H 'Cookie: ...subscriber session...' \
  -d 'action=my_save' \
  -d 'nonce=<valid_subscriber_nonce>' \
  -d "value='; DROP TABLE wp_options--"
```

Returns 200 + writes the malicious payload to wp_options.

#### Fix
Add: `if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error( '', 403 );`
Add: `$value = sanitize_text_field( wp_unslash( $_POST['value'] ?? '' ) );`

---

### wp_ajax_nopriv_subscribe (anonymous endpoint)
✓ Sanitizes input
✓ Rate-limited via transient
❌ No CSRF token (any site can submit on behalf of a user)

#### Severity: High
**Fix:** Add a public-form nonce verified server-side.
```

---

## Coverage targets

| Pattern | Required for `wp_ajax_*` | Required for `wp_ajax_nopriv_*` |
|---|---|---|
| Nonce check | Yes | Yes (CSRF) |
| Capability check | Yes (sensitive) | N/A |
| Input sanitization | Yes | Yes (more aggressive) |
| Output escaping in response | Yes | Yes |
| Rate limiting | Optional | **Yes** |

---

## CI

```yaml
- run: PLUGIN_SLUG=my-plugin npx playwright test --project=ajax-fuzz
```

---

## Pair with `/orbit-rest-fuzzer` + `/orbit-wp-security`

Three layers of attack-surface:
- REST endpoints → `/orbit-rest-fuzzer`
- admin-ajax handlers → this skill
- Source code review → `/orbit-wp-security`

Run all three on every release. Together they cover the WP attack surface.

---

## Hard rules

- ❌ Never fuzz against a production site.
- ❌ Never fuzz any URL you don't own.
- ✅ Local wp-env only. Findings reported with full repro steps.

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…