# Secure Plugin Development

> >

- **Type:** Skill
- **Install:** `agentstack add skill-wpultimatesecurity-wordpress-security-skills-secure-plugin-development`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [wpultimatesecurity](https://agentstack.voostack.com/s/wpultimatesecurity)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [wpultimatesecurity](https://github.com/wpultimatesecurity)
- **Source:** https://github.com/wpultimatesecurity/WordPress-Security-Skills/tree/dev/skills/secure-plugin-development

## Install

```sh
agentstack add skill-wpultimatesecurity-wordpress-security-skills-secure-plugin-development
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Secure plugin & theme development (baseline)

## When to use this skill

Use this skill at the **start** of any WordPress development work and whenever you add
a feature that crosses a trust boundary:

- Creating a new plugin main file or theme `functions.php` addition.
- Registering hooks (`add_action` / `add_filter`) that handle input or render output.
- Adding admin pages, settings, shortcodes, blocks, widgets, or REST routes.
- Reviewing an existing plugin to bring it up to a secure baseline.

This is the **router** skill. Follow the
[security decision tree](references/decision-tree.md): choose the entry path
(browser/API, renderer, cron, or CLI), then add the relevant data and policy
branches. It explains when to combine focused skills and when browser nonce
checks do not apply; do not load every skill for every task.

## Core principles (and why they matter)

1. **Never trust input; always escape output.** Every value from `$_GET`, `$_POST`,
   `$_REQUEST`, `$_COOKIE`, the database, or a remote API is untrusted until sanitized,
   and untrusted again the moment it is echoed. These are two separate jobs.
2. **Block direct file access.** Plugin files are reachable by URL. Without an `ABSPATH`
   guard, an attacker can execute them outside WordPress, bypassing all your checks.
3. **Separate authentication, CSRF, and authorization.** Use a nonce for
   cookie-authenticated state changes and an appropriate capability/object check for
   privileged actions. REST API credentials, cron, and CLI have different trust
   models; follow the decision tree rather than adding browser checks everywhere.
4. **Use core APIs, not hand-rolled code.** Prefer maintained sanitize/escape/DB/HTTP
   APIs, but choose the API and its arguments for the actual trust boundary.
5. **Least privilege by default.** Default options to the safe value, scope capabilities
   tightly, and expose the minimum surface.
6. **Fail closed.** On any failed check, stop and return an error — never fall through.

## Step-by-step implementation

1. **Guard the file:** `defined( 'ABSPATH' ) || exit;` at the top of every PHP file.
2. **Namespace everything:** prefix functions, hooks, options, and globals (e.g.
   `my_plugin_*`) to avoid collisions and accidental overrides.
3. **Choose the handler trust model** using the [decision tree](references/decision-tree.md):
   1. Apply transport-appropriate authentication and CSRF protection.
   2. Check authority over the action and specific resource.
   3. Validate input shape/type, unslash WordPress-slashed request input, and sanitize.
   4. Use `$wpdb->prepare()` for dynamic values in custom queries.
   5. Escape at each output sink for its actual context.
4. **Set safe defaults** for all options; validate on save and on read.
5. **Enqueue assets properly** (`wp_enqueue_script/style`) and pass data via
   `wp_localize_script()` rather than inline-echoing PHP into JS.
6. **Keep secrets out of the repo** and out of client-readable output.

## Common AI mistakes / anti-patterns

### Mistake 1 — No ABSPATH guard

```php
// ❌ Insecure: file executes if requested directly over HTTP.
get_results( "SELECT * FROM t WHERE id = " . $_GET['id'] );
echo "x";
$body = file_get_contents( $remote_url );
```

```php
// ✅ Secure: prepared query, escaped output, HTTP API.
$id   = absint( $_GET['id'] ?? 0 );
$rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}t WHERE id = %d", $id ) );
echo 'x';
$response = wp_remote_get( $remote_url );
$body     = is_wp_error( $response ) ? '' : wp_remote_retrieve_body( $response );
```

### Mistake 4 — Unsafe defaults

```php
// ❌ Insecure: feature ships enabled, capability defaults wide open.
add_option( 'my_plugin_allow_uploads', true );
```

```php
// ✅ Secure: default to the safe value; opt-in to risk.
add_option( 'my_plugin_allow_uploads', false );
```

## Correct code examples

A minimal but complete secure plugin skeleton — ABSPATH guard, an admin page behind a
capability, and the full verify → authorize → sanitize → act → escape flow — lives in
[`references/secure-plugin-skeleton.php`](references/secure-plugin-skeleton.php).

## Checklist

- [ ] Every PHP file opens with `defined( 'ABSPATH' ) || exit;`.
- [ ] Functions, hooks, and options are uniquely prefixed.
- [ ] Each request handler verifies nonce, then capability, then sanitizes input.
- [ ] All custom DB access uses `$wpdb->prepare()`.
- [ ] All dynamic output is escaped at the point of echo.
- [ ] Options have safe defaults and are validated on save.
- [ ] Remote requests use the WP HTTP API (`wp_remote_*`), not `file_get_contents`/cURL.
- [ ] No secrets, keys, or credentials are hard-coded or sent to the browser.
- [ ] Scripts are enqueued and given data via `wp_localize_script()`.

## Official references

- [Security — Common APIs Handbook](https://developer.wordpress.org/apis/security/)
- [Plugin Security — Plugin Handbook](https://developer.wordpress.org/plugins/security/)
- [Checking User Capabilities](https://developer.wordpress.org/plugins/security/checking-user-capabilities/)
- [Data Validation](https://developer.wordpress.org/apis/security/data-validation/)
- [Escaping Data](https://developer.wordpress.org/apis/security/escaping/)
- [HTTP API](https://developer.wordpress.org/plugins/http-api/)
- [OWASP Top Ten](https://owasp.org/www-project-top-ten/)

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [wpultimatesecurity](https://github.com/wpultimatesecurity)
- **Source:** [wpultimatesecurity/WordPress-Security-Skills](https://github.com/wpultimatesecurity/WordPress-Security-Skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-wpultimatesecurity-wordpress-security-skills-secure-plugin-development
- Seller: https://agentstack.voostack.com/s/wpultimatesecurity
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
