secure-plugin-development
Skill45 starsChanged 54 days agoMITView on GitHub
Use when starting a new WordPress plugin or theme, scaffolding a plugin file, wiring hooks, or adding any feature that handles requests, options, or output. Establishes the secure-by-default baseline — ABSPATH guard, the capability + nonce + sanitize + escape flow, prepared queries, and safe defaults — and routes to the focused security skills for each concern. Apply proactively at the start of any WordPress build.
We split this skill into the rules it gives an agent
7 rules. About 0 are common, 4 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 20
Use this skill at the start of any WordPress development work and whenever you add a feature that crosses a trust boundary:
- Very specific: line 36
Never trust input; always escape output: Every value from $_GET, $_POST, $_REQUEST, $_COOKIE, the database, or a remote API is untrusted …
- Distinctive: line 41
Separate authentication, CSRF, and authorization: Use a nonce for cookie-authenticated state changes and an appropriate capability/object …
- Very specific: line 45
Use core APIs, not hand-rolled code: Prefer maintained sanitize/escape/DB/HTTP APIs, but choose the API and its arguments for the actual …
- Distinctive: line 47
Least privilege by default: Default options to the safe value, scope capabilities tightly, and expose the minimum surface.
- Distinctive: line 58
Check authority over the action and specific resource.
- Very specific: line 60
Use $wpdb->prepare() for dynamic values in custom queries.
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
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.phpaddition. - 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: 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)
- 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. - Block direct file access. Plugin files are reachable by URL. Without an
ABSPATHguard, an attacker can execute them outside WordPress, bypassing all your checks. - 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.
- 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.
- Least privilege by default. Default options to the safe value, scope capabilities tightly, and expose the minimum surface.
- Fail closed. On any failed check, stop and return an error — never fall through.
Step-by-step implementation
- Guard the file:
defined( 'ABSPATH' ) || exit;at the top of every PHP file. - Namespace everything: prefix functions, hooks, options, and globals (e.g.
my_plugin_*) to avoid collisions and accidental overrides. - Choose the handler trust model using the decision tree:
- Apply transport-appropriate authentication and CSRF protection.
- Check authority over the action and specific resource.
- Validate input shape/type, unslash WordPress-slashed request input, and sanitize.
- Use
$wpdb->prepare()for dynamic values in custom queries. - Escape at each output sink for its actual context.
- Set safe defaults for all options; validate on save and on read.
- Enqueue assets properly (
wp_enqueue_script/style) and pass data viawp_localize_script()rather than inline-echoing PHP into JS. - Keep secrets out of the repo and out of client-readable output.
Supporting references
| Reference | Load when |
| Secure plugin baseline checklist | Before final verification of the secure plugin baseline controls. |
| Choose the security review path | Selecting focused skills for the entry point and all relevant data and risk branches. |
| Secure plugin skeleton | Implementing a capability-gated plugin admin page and the full settings-save flow. |
Common AI mistakes / anti-patterns
Mistake 1 — No ABSPATH guard
// ❌ Insecure: file executes if requested directly over HTTP.
<?php
function my_plugin_init() { /* ... */ }// ✅ Secure: bail unless loaded within WordPress.
<?php
defined( 'ABSPATH' ) || exit;
function my_plugin_init() { /* ... */ }Mistake 2 — Doing the work before the security checks
// ❌ Insecure: option saved before anything is verified.
function my_plugin_save() {
update_option( 'my_opt', $_POST['val'] );
check_admin_referer( 'my_plugin_save' );
current_user_can( 'manage_options' );
}// ✅ Secure: verify, authorize, sanitize, THEN act.
function my_plugin_save() {
check_admin_referer( 'my_plugin_save', 'my_plugin_nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Forbidden', 'my-plugin' ), 403 );
}
$val = isset( $_POST['val'] ) ? sanitize_text_field( wp_unslash( $_POST['val'] ) ) : '';
update_option( 'my_opt', $val );
}Mistake 3 — Rolling your own instead of using core APIs
// ❌ Insecure: manual SQL, manual escaping, raw remote fetch.
$rows = $wpdb->get_results( "SELECT * FROM t WHERE id = " . $_GET['id'] );
echo "<a href=" . $_GET['url'] . ">x</a>";
$body = file_get_contents( $remote_url );// ✅ 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 '<a href="' . esc_url( wp_unslash( $_GET['url'] ?? '' ) ) . '">x</a>';
$response = wp_remote_get( $remote_url );
$body = is_wp_error( $response ) ? '' : wp_remote_retrieve_body( $response );Mistake 4 — Unsafe defaults
// ❌ Insecure: feature ships enabled, capability defaults wide open.
add_option( 'my_plugin_allow_uploads', true );// ✅ 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.
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_*), notfile_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
In this repository26 other files it gives its agents.
- ai-llm-integration-securitySkill
- ajax-securitySkill
- authentication-session-securitySkill
- capability-permission-checksSkill
- cron-background-job-securitySkill
- dependency-supply-chain-securitySkill
- filesystem-securitySkill
- file-upload-securitySkill
- gutenberg-block-editor-securitySkill
- http-api-ssrf-preventionSkill
- input-sanitization-validationSkill
- multisite-securitySkill
- nonces-csrf-protectionSkill
- object-injection-deserializationSkill
- output-escapingSkill
- rest-api-securitySkill
- secrets-credentials-managementSkill
- security-auditing-code-reviewSkill
- security-headers-cspSkill
- settings-options-securitySkill
- shortcode-block-securitySkill
- sql-injection-preventionSkill
- user-data-protection-privacySkill
- woocommerce-securitySkill
- wp-cli-securitySkill
- wp-hardening-best-practicesSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

