agentsclimarketplace

Wp security deep

Skill Lonsdale201/wp-agent-skills/wordpress/wp-security-deep

Deep security audit for WordPress plugin/theme PHP code, covering issues beyond the basic sanitize/escape/nonce checklist — PHP object injection (unserialize), SSRF in remote requests, CSRF on state-changing GET handlers, mass assignment via $_POST loops, insecure file include / template injection, mail header injection, ZipSlip in archive extraction, type-juggling in auth comparisons, and TOCTOU race patterns in option/meta locks. Use after or alongside wp-security-audit when reviewing complex plugins, REST APIs, integrations that fetch remote URLs, file processors, or any code that handles uploads, archives, self-rolled auth tokens or login rate-limiters, remote SQL/report definitions, or private plugin update channels.From its SKILL.md

Install
npx -y skills add Lonsdale201/wp-agent-skills --skill wp-security-deep

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 21 stars21 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

13.8 KB, ~3.1k tokens by cl100k_base, as published. Nobody here has run it

WordPress security audit — deep checks

Run AFTER wp-security-audit covers the basics (sanitize, escape, nonce, capability, SQL prepare, AJAX nopriv, REST permission). This skill catches the second-tier issues that static-analysis tools miss and that a hurried review skips.

When to use this skill

Trigger when the basic audit is clean but the code does any of:

  • Calls unserialize, maybe_unserialize on stored or transmitted data.
  • Calls wp_remote_*, file_get_contents, curl_*, fopen with a URL that could be influenced by input.
  • Has admin pages or AJAX/REST handlers driven by ?action= GET params that perform writes.
  • Loops $_POST / $_REQUEST keys into update_post_meta, update_user_meta, wp_update_user, wp_insert_post, or similar.
  • Includes/requires a path that contains any input-derived component.
  • Sends mail with user-controlled From, Cc, Bcc, or custom headers.
  • Extracts uploaded archives (ZipArchive, PharData, tar).
  • Compares tokens, hashes, or secrets with == / === instead of hash_equals.
  • Implements its own lock / counter via get_option + update_option.
  • Implements login throttling or account lockout from unauthenticated failures.
  • Uses a remote response to choose SQL, PHP/template code, callback/class names, filesystem paths, redirects, or a plugin-update package URL.

Audit checks

1. PHP object injection via unserialize

// HIGH — attacker-controlled serialized payload → gadget chains
$data = unserialize( $_POST['payload'] );

Before assigning severity, identify who can write the raw serialized bytes, whether classes and usable gadgets are loaded, and which magic method creates the security effect. maybe_unserialize( get_option(...) ) is not automatically vulnerable; state the required writer instead of assuming either admin-only or attacker-controlled storage.

Prefer JSON for network/user input. allowed_classes => false blocks normal class instantiation but not huge/deep/cyclic graphs or unsafe later recursion; also bound bytes, depth/nodes, accepted result type, and traversal.

For nested/double serialization, byte-length corruption, and transformed-key collisions, apply wp-metadata-api rather than blind string replacement.

Phar deserialization: on PHP < 8.0, filesystem functions (file_exists, is_dir, filesize, fopen, etc.) on a path using the phar:// stream wrapper would auto-unserialize the archive's metadata, enabling object-injection gadget chains. PHP 8.0+ removed this auto-unserialization (RFC: phar_stop_autoloading_metadata) — the risk now requires an explicit Phar::getMetadata() call. So the finding severity depends on the deployment's minimum PHP version:

  • PHP 7.x supported: HIGH if any filesystem function is called with user-influenced paths. Strip phar:// from input.
  • PHP 8.0+ only: still flag explicit Phar::getMetadata() over user-controlled archives, plus any unserialize() of binary blobs.

Fix: JSON for transport; reject serialized user input when possible; if legacy parsing is unavoidable, constrain classes, bytes, depth, graph traversal, and accepted result types; never accept phar:// from input on PHP 7.x.

2. SSRF in outbound requests

// HIGH — internal network probe / cloud metadata exfil
$response = wp_remote_get( $_POST['webhook_url'] );

Flag missing exact HTTPS host allowlists, plain wp_remote_* on user-influenced URLs, unbounded timeout/redirect/body size, disabled TLS verification, and ignored WP_Error/HTTP statuses. Use wp_safe_remote_* so core validates the initial URL and redirects. Do not accept a hand-rolled DNS pre-check as complete protection: it has rebinding/TOCTOU and IPv6 pitfalls. Arbitrary destinations need infrastructure egress controls as well. Apply the full wp-http-api-client skill for implementation and test patterns.

3. CSRF on state-changing GET handlers

WP plugins commonly wire admin pages with ?action=delete&id=42 links. These bypass check_admin_referer if the dev only added it to POST handlers.

// HIGH — GET with side effect, no nonce
if ( isset( $_GET['action'] ) && $_GET['action'] === 'delete' ) {
    delete_post( (int) $_GET['id'] );
}

Rule: any cookie-authenticated browser handler that writes MUST verify a nonce regardless of HTTP method. Legacy action links can be built with wp_nonce_url( $url, 'delete_post_' . $id ) and verified with check_admin_referer( 'delete_post_' . $id ); prefer POST forms for destructive new UI. Signed webhooks, CLI, and cron use their own trust boundary rather than a WordPress nonce.

4. Mass assignment

// HIGH — user can set role, status, meta_input, etc.
wp_update_user( $_POST );
wp_insert_post( $_POST );

foreach ( $_POST as $key => $value ) {
    update_user_meta( $user_id, $key, $value );
}

Fix: explicit allowlist of accepted keys. Never spread $_POST / $_REQUEST into a write function whose schema includes privileged fields (role, user_pass, post_status, post_author, meta_input, tax_input).

5. File include / template injection

// HIGH — RCE
include $template_dir . '/' . $_GET['view'] . '.php';
locate_template( $_GET['t'] . '.php' );

Fix: allowlist:

$allowed = [ 'list', 'edit', 'settings' ];
$view    = isset( $_GET['view'] ) && in_array( $_GET['view'], $allowed, true )
    ? $_GET['view'] : 'list';
include $template_dir . '/' . $view . '.php';

Even with sanitize_file_name, .. and null bytes can survive. Only allowlist is safe.

6. Mail header injection

// HIGH — \r\n injection adds Bcc
wp_mail( '[email protected]', 'Hi', $body, "From: " . $_POST['email'] );

Fix: sanitize email + reject CRLF:

$from = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) );
if ( ! $from || preg_match( '/[\r\n]/', $from ) ) {
    wp_die( 'Invalid sender', 400 );
}
wp_mail( '[email protected]', 'Hi', $body, [ 'From: ' . $from ] );

Pass headers as an array, not a concatenated string.

7. ZipSlip / archive extraction

// HIGH — extracted file can escape with ../
$zip->extractTo( $target_dir );

Preferred WordPress path: initialize WP_Filesystem() and call core's unzip_file(). Core validates each entry with validate_file(), calculates required space, creates directories through the selected transport, and returns true|WP_Error.

require_once ABSPATH . 'wp-admin/includes/file.php';

if ( ! WP_Filesystem() ) {
    return new WP_Error( 'filesystem_unavailable', 'Filesystem unavailable.' );
}

$result = unzip_file( $archive_file, $target_dir );
if ( is_wp_error( $result ) ) {
    return $result;
}

This is not permission to unpack arbitrary uploads into a web-accessible directory. Before extraction, impose compressed/uncompressed byte limits, entry-count and extension/type policies; reject executable content when it is not required. Extract to a fresh, non-public staging directory, inspect the result, then move only expected regular files. unzip_file() skips invalid paths but does not implement your product's content policy. For non-ZIP formats or custom extractors, reject absolute/traversal paths, symlinks, hardlinks, device nodes, and archive bombs, and containment-check every destination before writing it.

8. Timing-safe comparison

// MEDIUM — timing leak + type juggling
if ( $_GET['token'] == $stored_token ) { /* grant */ }

// '0e123...' == '0e456...' is true (scientific notation)

Fix: if ( hash_equals( $stored_token, (string) $_GET['token'] ) ).

Use for: API keys, password reset tokens, signed URLs, any secret comparison. Not needed for IDs / user-visible values.

9. TOCTOU race on options/meta

// MEDIUM — two simultaneous requests both pass the check
if ( ! get_option( 'myplugin_processing' ) ) {
    update_option( 'myplugin_processing', 1 );
    do_expensive_thing();
    update_option( 'myplugin_processing', 0 );
}

WP options have no atomic CAS. Mitigations:

  • Use a transient with a short TTL only as a soft stampede hint. It is not an atomic correctness lock.
  • For real exclusion: $wpdb->query( "SELECT GET_LOCK('myplugin', 0)" ) and RELEASE_LOCK. MySQL-level, atomic.
  • If correctness matters (billing, idempotency), use a unique-key insert as the gate: insert fails → another worker is in flight.

Flag the pattern, propose the lock approach. Don't claim certainty about race windows without dynamic testing.

10. Rate-limit abuse and global account lockout

A login defense can become an unauthenticated denial-of-service primitive. Flag code that lets failures supplied by one anonymous client create a global username/email lock which rejects the correct credential from every other client. A public username or email address is not proof that the account owner made the failed attempts.

Review these invariants:

  • enforce blocking primarily on a bounded source/identity pair or source rate; use a global account signal for alerting, step-up checks, or delay rather than unconditional denial of a correct login from unrelated clients;
  • enforce the same protection at every supported authentication entry point, not only a form-specific hook;
  • clear the same identifier aliases that were incremented, including canonical username versus email login;
  • prevent attacker-chosen nonexistent usernames from causing unbounded durable key creation or database writes;
  • keep error responses from distinguishing an existing account from an unknown one;
  • apply the atomic-counter guidance in the preceding TOCTOU section rather than assuming a transient read/modify/write is an exact limit.

Minimum acceptance test: after client A reaches the failure threshold for a known account, the correct credential from client B still succeeds while A remains throttled. Test every supported login surface with the same invariant.

This pattern can be HIGH when an anonymous caller can reliably and repeatedly deny a known user's correct login. State the required username knowledge, configured threshold, lock duration, and affected login paths.

11. Remote control-plane and executable response trust

Treat remote responses as attacker-controlled data even with valid HTTPS and a fixed vendor. If response fields reach SQL execution, dynamic code/templates, capabilities, redirects, or a private update package, read reference.md and trace the complete control-plane chain. Keep executable policy local and versioned; accept only a small allowlisted ID plus schema-validated parameters. Apply wp-http-api-client for transport, size, redirect, and test controls.

12. Direct file access guard

Top of every PHP file that has side effects on load:

if ( ! defined( 'ABSPATH' ) ) { exit; }

Missing guard is LOW unless the file actually executes work at top-level (most class files are fine). Flag explicitly for files in includes/ or admin/ that do procedural work.

Severity guide

Same as wp-security-audit. Object injection, SSRF on internal network, RCE via include, ZipSlip → HIGH. A remote-control-plane chain that can install PHP or execute unrestricted database statements can be CRITICAL when its stated trigger is realistic. CSRF on admin GET typically HIGH (any logged-in admin clicking a link). TOCTOU, timing → MEDIUM unless directly exploitable.

Report format

Reuse the format from wp-security-audit. If both skills run, merge findings into a single report grouped by severity, not by skill.

Cross-references

  • Run wp-security-audit first for the basic checklist (sanitize, escape, nonce, capability, SQL prepare).
  • Run wp-security-secrets for hardcoded credentials, weak randomness in tokens, and password-storage issues — these are adjacent but distinct findings.
  • Run wp-batch-mutation-audit for durable cursors, lost responses, retries, partial failures, and server-side exclusion.

What this skill does NOT cover

  • Cryptographic protocol correctness (custom JWT, signing schemes).
  • Business-logic IDOR beyond capability/ownership checks.
  • Third-party dependency CVEs and manually bundled libraries; use wp-dependency-security-audit because composer audit alone cannot inventory copied JS or manifest-less/prefixed PHP.
  • Batch retry/idempotency analysis beyond surface TOCTOU patterns.
  • Server hardening (open_basedir, disable_functions, file perms).

References

What ships with it: 1 file

2.8 KB alongside SKILL.md

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.