📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
---
|
||||
name: "wp-guard"
|
||||
description: "Review generated or changed WordPress plugins, themes, and blocks for security, internationalization, performance, and API correctness."
|
||||
risk: "offensive"
|
||||
source: "community"
|
||||
source_repo: "amElnagdy/guard-skills"
|
||||
source_type: "community"
|
||||
date_added: 2026-07-13
|
||||
author: "community"
|
||||
tags: []
|
||||
tools: []
|
||||
---
|
||||
|
||||
|
||||
# WP Guard
|
||||
|
||||
> [!WARNING]
|
||||
> **Authorized Use Only.** Review only WordPress code and environments the user owns or is explicitly authorized to assess. Keep checks non-destructive and inside the approved scope.
|
||||
|
||||
You are reviewing generated or changed WordPress code before it ships. Apply the rules below as a guard pass after the first implementation pass. Be a sharp reviewer, not a pedantic one: flag what creates vulnerabilities, breaks translations, or melts servers — ignore cosmetic preferences WPCS tooling already handles.
|
||||
|
||||
These rules exist because AI agents produce WordPress code with systematic failures: raw `echo` of request data, AJAX handlers with neither nonce nor capability check, SQL built by string interpolation, English hardcoded into user-facing strings, `posts_per_page => -1` on sites with a million posts, and hand-rolled replacements for APIs core already ships. Each one looks fine in a demo and fails in production.
|
||||
|
||||
## When to Use
|
||||
|
||||
Use this skill when reviewing generated or changed WordPress code — plugins, themes, and blocks — before it ships. Activate it reactively after an agent writes, edits, or reviews code touching WordPress APIs: hooks, custom post types, REST endpoints, database queries, and block editor integrations.
|
||||
|
||||
## How to use this skill
|
||||
|
||||
**Guard-pass mode** (recommended): after WordPress code has been generated or edited, apply the rules to the diff or target files, then run the self-check before delivery. Fix violations before showing the user.
|
||||
|
||||
**Live mode** (explicit): when the user invokes this skill before writing WordPress code, apply the same rules while writing, then run the self-check before delivery.
|
||||
|
||||
**Review mode** (the user asks you to review, audit, or rate WordPress code): walk [references/review-checklist.md](references/review-checklist.md) against the target files and produce a structured findings report. Do not edit code in review mode unless asked.
|
||||
|
||||
Pair this skill with clean-code-guard when both are installed: clean-code-guard owns generic code quality; wp-guard owns the WordPress layer.
|
||||
|
||||
## Adapt to the project first
|
||||
|
||||
1. Read the project's agent instructions (CLAUDE.md, AGENTS.md), `phpcs.xml`/WPCS config, and `composer.json`. Project conventions win on conflict.
|
||||
2. Identify the established prefix (functions, options, meta keys, handles) and the minimum supported WP/PHP versions. Match both.
|
||||
3. Detect context: WooCommerce APIs in play → apply woo-guard alongside this skill when it is installed; otherwise apply WooCommerce's HPOS, CRUD, and checkout rules from its developer documentation. Multilingual site (WPML/Polylang/multisite) → i18n rules are blocking, not advisory.
|
||||
4. Read one neighboring file before writing. Mirror its error handling, hook registration style, and escaping habits — unless they violate the security rules below, which are non-negotiable.
|
||||
|
||||
## The Rules
|
||||
|
||||
### Security — must fix, no exceptions
|
||||
|
||||
1. **Escape late, escape everything.** Every variable crossing into HTML output goes through the context-correct function: `esc_html()`, `esc_attr()`, `esc_url()`, or `wp_kses()`/`wp_kses_post()` for rich content. Data passed to inline JS goes through `wp_json_encode()` + `wp_add_inline_script()` — `esc_js()` is legacy, for single-quoted strings in inline attributes only. Escaping happens at output, not at storage. `echo $anything;` without an `esc_*` wrapper fails review.
|
||||
|
||||
2. **Sanitize early, and unslash first.** Request data (`$_POST`, `$_GET`, `$_REQUEST`, `$_SERVER`) never touches logic raw: `wp_unslash()` first, then the type-correct sanitizer (`sanitize_text_field()`, `sanitize_key()`, `absint()`, `sanitize_email()`, …). Sanitization is not escaping; doing one never excuses the other.
|
||||
|
||||
3. **Every state change proves identity and intent.** Form handlers, AJAX endpoints, and REST routes that change anything require BOTH a capability check (`current_user_can()`) AND a nonce (`check_admin_referer()`, `check_ajax_referer()`, or REST nonce handling). A nonce is not authorization. A REST `permission_callback` of `__return_true` on a writing route fails review.
|
||||
|
||||
4. **`$wpdb->prepare()` for every query containing a variable.** Placeholders (`%s`, `%d`, `%f`, and `%i` for identifiers on WP ≥ 6.2), never interpolation or concatenation. Prefer `WP_Query`, the meta and options APIs over raw SQL when they can express the query.
|
||||
|
||||
### Core API discipline
|
||||
|
||||
5. **Use the platform; don't reinvent it.** Outbound HTTP via `wp_remote_get()`/`wp_remote_post()`, never curl. Assets via `wp_enqueue_script()`/`wp_enqueue_style()`, never echoed `<script>`/`<style>` tags. Scheduling via WP-Cron or Action Scheduler. Redirects via `wp_safe_redirect()` followed by `exit`. File writes via `WP_Filesystem`. Simple persistent data via options/transients, not a custom table.
|
||||
|
||||
6. **Verify every hook and function exists.** Before `add_action()`, `add_filter()`, or calling a core/plugin function, confirm it exists in the supported versions — read the source or the project's installed code. Hallucinated hooks fail silently in WordPress: no error, no behavior. Also match the hook to the moment — front-end code does not load on `admin_init`, queries do not run before `init` expects them.
|
||||
|
||||
7. **Prefix or namespace everything public.** Functions, classes, options, transients, meta keys, script handles, AJAX actions, REST namespaces — all carry the project prefix. Generic names (`get_settings`, `data`, `api_key`) are collisions waiting for the next active plugin.
|
||||
|
||||
8. **Guard direct access.** Every PHP file that does work starts with the `ABSPATH` check (or equivalent project convention).
|
||||
|
||||
### Internationalization
|
||||
|
||||
9. **Every user-facing string is translation-ready.** The correct wrapper for the context (`__()`, `_e()`, `_x()`, `_n()`, or the escaping combos `esc_html__()`, `esc_attr__()`), a literal text domain matching the plugin slug — never a variable or constant — translator comments on every placeholder, `_n()` for plurals (never `sprintf` with a hardcoded singular/plural choice), and no sentence assembly by concatenation. Dates and numbers through `date_i18n()`/`wp_date()` and `number_format_i18n()`. Details and JS i18n: [references/i18n.md](references/i18n.md).
|
||||
|
||||
### Performance
|
||||
|
||||
10. **Query discipline.** No `posts_per_page => -1` and no `query_posts()`, ever. Use `'fields' => 'ids'` when only IDs are needed, `'no_found_rows' => true` when not paginating, and never query inside a loop what could be primed once (meta/term caches). Details: [references/performance.md](references/performance.md).
|
||||
|
||||
11. **Cache expensive work, load assets where used.** Remote calls and heavy computations go behind transients or the object cache with a deliberate TTL. Options that are large or rarely read register with `autoload => false`. Scripts and styles enqueue only on the screens that use them.
|
||||
|
||||
## Self-check before delivery
|
||||
|
||||
1. Grep your diff for `echo`, `print`, `<?=`: is every variable output escaped with the context-correct function?
|
||||
2. Grep for `$_POST`, `$_GET`, `$_REQUEST`: unslashed? sanitized? nonce-verified? capability-checked?
|
||||
3. Grep for `$wpdb->`: every variable behind a placeholder?
|
||||
4. Any user-facing string outside an i18n wrapper? Any non-literal text domain?
|
||||
5. Any hook or function you did not verify exists?
|
||||
6. Any unbounded query, uncached remote call, or unconditional enqueue?
|
||||
7. Does every new public name carry the project prefix?
|
||||
8. Would this survive WPCS (`WordPress-Extra` + `WordPress-Security`) without warnings you cannot justify?
|
||||
|
||||
If any answer is wrong, fix it before showing the user.
|
||||
|
||||
## Reporting format (review mode)
|
||||
|
||||
```
|
||||
**Rule N violation** in `path/file.php:<line or function>`
|
||||
- What: <one sentence>
|
||||
- Risk: <XSS / SQLi / CSRF / broken i18n / scaling — one phrase>
|
||||
- Fix: <one sentence>
|
||||
```
|
||||
|
||||
Group by file, lead with security findings. If a file is clean, don't mention it.
|
||||
|
||||
## Severity guide
|
||||
|
||||
- **Must fix:** Rules 1–4 — these are exploitable (XSS, SQLi, CSRF, privilege escalation)
|
||||
- **Should fix:** Rules 5–9 — conflicts, silent failures, untranslatable releases
|
||||
- **Worth noting:** Rules 10–11 — they decide whether the code survives traffic; block on them for code that runs on every request
|
||||
|
||||
## References
|
||||
|
||||
- [references/security.md](references/security.md) — escaping/sanitization function tables, nonce lifecycle, REST permissions, `$wpdb->prepare` details, file uploads
|
||||
- [references/i18n.md](references/i18n.md) — wrapper selection, text domain rules, plurals, translator comments, JS translations, RTL, multilingual-plugin gotchas
|
||||
- [references/performance.md](references/performance.md) — WP_Query flags, transients vs object cache, autoload hygiene, asset loading, cron, scaling traps
|
||||
- [references/review-checklist.md](references/review-checklist.md) — structured walk-through for review mode
|
||||
- [references/sources.md](references/sources.md) — handbook and research URLs; read only when citing a source
|
||||
|
||||
## What this skill does not do
|
||||
|
||||
- Run PHPCS, PHPStan, or Plugin Check — use the project's tooling for mechanical verification; this skill is the judgment layer above it.
|
||||
- Decide plugin architecture or business logic — it guards how WordPress code ships, not what it does.
|
||||
- Replace clean-code-guard or test-guard — generic code quality and test quality remain their jurisdiction.
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
# WP Guard — Internationalization Reference
|
||||
|
||||
## Contents
|
||||
|
||||
- Wrapper selection
|
||||
- Text domain rules
|
||||
- Placeholders and translator comments
|
||||
- Plurals
|
||||
- Sentence assembly
|
||||
- JavaScript i18n
|
||||
- Dates, numbers, RTL
|
||||
- Multilingual-plugin gotchas
|
||||
|
||||
## Wrapper selection
|
||||
|
||||
| Situation | Use |
|
||||
|---|---|
|
||||
| Return a translated string | `__( 'Text', 'my-plugin' )` |
|
||||
| Echo a translated string | `_e( 'Text', 'my-plugin' )` — or better, `esc_html_e()` |
|
||||
| Translate + escape for HTML | `esc_html__()` / `esc_html_e()` |
|
||||
| Translate + escape for attribute | `esc_attr__()` / `esc_attr_e()` |
|
||||
| Ambiguous word needing context | `_x( 'Post', 'verb', 'my-plugin' )` |
|
||||
| Plural | `_n( '%s item', '%s items', $count, 'my-plugin' )` |
|
||||
| Plural + context | `_nx()` |
|
||||
|
||||
When a string is both translated and output, the combined wrappers (`esc_html__`) are required — translations are untrusted input like any other (a compromised translation file is an XSS vector).
|
||||
|
||||
## Text domain rules
|
||||
|
||||
- Literal string, always: `__( 'Text', 'my-plugin' )`. Never `__( 'Text', PLUGIN_DOMAIN )`, never a variable — static analysis tools and translate.wordpress.org both fail on non-literal domains.
|
||||
- Must match the plugin slug exactly (the WordPress.org directory enforces this).
|
||||
- One domain per plugin/theme. AI agents copying snippets from other projects routinely import foreign text domains — grep for domains that don't match the project's.
|
||||
|
||||
## Placeholders and translator comments
|
||||
|
||||
```php
|
||||
/* translators: 1: customer name, 2: order number. */
|
||||
$message = sprintf(
|
||||
__( 'Hi %1$s, your order #%2$d is on its way.', 'my-plugin' ),
|
||||
$customer_name,
|
||||
$order_id
|
||||
);
|
||||
```
|
||||
|
||||
- Numbered placeholders (`%1$s`) whenever there is more than one — translators reorder words.
|
||||
- A `/* translators: … */` comment on every string with placeholders, immediately above the line.
|
||||
- Never put variables or HTML soup inside the translatable string when it can sit outside.
|
||||
|
||||
## Plurals
|
||||
|
||||
`_n()` exists because languages have between one and six plural forms. Never:
|
||||
|
||||
```php
|
||||
// Wrong — English-only logic.
|
||||
$label = $count === 1 ? __( 'item', 'my-plugin' ) : __( 'items', 'my-plugin' );
|
||||
```
|
||||
|
||||
Always `_n( '%s item', '%s items', $count, 'my-plugin' )`, then `sprintf()` with `number_format_i18n( $count )`.
|
||||
|
||||
## Sentence assembly
|
||||
|
||||
Never build sentences by concatenation — word order differs across languages:
|
||||
|
||||
```php
|
||||
// Wrong: translators get fragments they cannot reorder.
|
||||
echo __( 'Imported', 'my-plugin' ) . ' ' . $count . ' ' . __( 'products', 'my-plugin' );
|
||||
|
||||
// Right: one string, one placeholder, full context.
|
||||
/* translators: %s: number of imported products. */
|
||||
printf( esc_html__( 'Imported %s products.', 'my-plugin' ), number_format_i18n( $count ) );
|
||||
```
|
||||
|
||||
## JavaScript i18n
|
||||
|
||||
- `wp_set_script_translations( 'my-handle', 'my-plugin' )` after enqueueing; use `__()` from `@wordpress/i18n` in the JS.
|
||||
- Legacy pattern (`wp_localize_script` with pre-translated strings) is acceptable in legacy codebases — match the project.
|
||||
|
||||
## Dates, numbers, RTL
|
||||
|
||||
- Dates: `wp_date()` / `date_i18n()` with the site's format options — never raw `date()` for display.
|
||||
- Numbers: `number_format_i18n()`.
|
||||
- CSS: logical properties (`margin-inline-start`, not `margin-left`) for new styles; don't hand-write directional CSS — build `-rtl.css` files with RTLCSS. Auto-loading them applies only to core styles and block.json-registered styles; classic plugin/theme handles must opt in with `wp_style_add_data( $handle, 'rtl', 'replace' )`.
|
||||
|
||||
## Multilingual-plugin gotchas (WPML / Polylang)
|
||||
|
||||
- Strings stored in options/meta are NOT translated by `__()` — they need string registration (WPML String Translation / `pll_register_string`). Flag stored user-facing strings during review.
|
||||
- IDs are language-specific: a hardcoded `page_id` points at one language's page. Resolve through the multilingual plugin's API or filters when the project uses one.
|
||||
- Queries on multilingual sites are language-filtered by default — explicitly note when a query intentionally crosses languages.
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
# WP Guard — Performance Reference
|
||||
|
||||
## Contents
|
||||
|
||||
- WP_Query discipline
|
||||
- Meta and term cache priming
|
||||
- Transients vs object cache
|
||||
- Options autoload hygiene
|
||||
- Asset loading
|
||||
- Cron and background work
|
||||
- Scaling traps checklist
|
||||
|
||||
## WP_Query discipline
|
||||
|
||||
- `posts_per_page => -1` is forbidden. There is always a bound; if the caller "needs everything," page through with a loop or use a bounded cap the project agrees on.
|
||||
- `query_posts()` is forbidden — it clobbers the main query. Use `WP_Query` or `pre_get_posts`.
|
||||
- Only IDs needed → `'fields' => 'ids'` (skips row hydration and caches).
|
||||
- Not paginating → `'no_found_rows' => true` (skips `SQL_CALC_FOUND_ROWS`).
|
||||
- Not using meta/terms in the loop → `'update_post_meta_cache' => false`, `'update_post_term_cache' => false`.
|
||||
- Avoid `'meta_query'` on unindexed scans for high-traffic paths — meta queries do not scale on large `postmeta` tables; consider a lookup table or taxonomy when the access pattern is hot.
|
||||
- Never run queries inside `foreach` when one query (or cache priming) can fetch the set.
|
||||
|
||||
## Meta and term cache priming
|
||||
|
||||
The N+1 killer. When iterating IDs and reading meta per item:
|
||||
|
||||
```php
|
||||
$ids = get_posts( array( 'fields' => 'ids', /* … */ ) );
|
||||
update_meta_cache( 'post', $ids ); // one query primes the cache
|
||||
foreach ( $ids as $id ) {
|
||||
$sku = get_post_meta( $id, '_sku', true ); // served from cache
|
||||
}
|
||||
```
|
||||
|
||||
`update_meta_cache()` and `update_object_term_cache()` exist for exactly this; on WP ≥ 6.1, `_prime_post_caches()` batches posts, meta, and terms in one call (public API since 6.1 despite the underscore). AI-generated loops skip them every time.
|
||||
|
||||
## Transients vs object cache
|
||||
|
||||
- `set_transient()` / `get_transient()` — persistent cache with TTL; backed by the object cache when a drop-in exists, by the options table otherwise.
|
||||
- `wp_cache_get()` / `wp_cache_set()` — request-scope by default, persistent only with an object-cache drop-in (Redis/Memcached). Use cache groups and deliberate TTLs.
|
||||
- Expensive remote calls (Rule 11) always go behind one of these. Choose TTL consciously; never cache user-specific data in a shared key — include the user/locale in the key when output varies by them.
|
||||
- On failure paths, decide explicitly whether to cache the failure (short TTL) or retry every request — and say which in a comment.
|
||||
|
||||
## Options autoload hygiene
|
||||
|
||||
Every `autoload => true` option loads on EVERY request into `alloptions`. Rules:
|
||||
|
||||
- `add_option( 'ncs_big_report_cache', $data, '', false )` — large or rarely-read data never autoloads (pass `false` as the third arg of `update_option` too; the parameter exists since WP 4.2).
|
||||
- Keep individual autoloaded options small; an `alloptions` blob over ~1 MB is a known site-killer on high-traffic sites.
|
||||
- One serialized settings array per plugin beats twenty separate options.
|
||||
|
||||
## Asset loading
|
||||
|
||||
- Enqueue only where used: check `is_admin()`, screen IDs (`get_current_screen()`), shortcode presence, or block usage before enqueueing.
|
||||
- Front-end assets registered on `wp_enqueue_scripts`, admin on `admin_enqueue_scripts` with the `$hook_suffix` check.
|
||||
- Version assets with the plugin version (cache busting); load non-critical JS with `defer`/`async` strategies (`wp_register_script` args on WP ≥ 6.3).
|
||||
|
||||
## Cron and background work
|
||||
|
||||
- WP-Cron is traffic-driven and can stack: guard handlers against overlap (a lock transient), keep events idempotent.
|
||||
- Long or high-volume jobs → Action Scheduler when WooCommerce (or the library) is present.
|
||||
- Never `sleep()` in a request; never do batch work on `init` for every visitor.
|
||||
|
||||
## Scaling traps checklist
|
||||
|
||||
- Unbounded `get_users()` / `get_terms()` on large sites — same rules as posts.
|
||||
- `switch_to_blog()` in loops without batching (multisite).
|
||||
- Per-request remote HTTP without caching — one slow third party becomes your TTFB.
|
||||
- Writing to options on front-end requests (cache invalidation storm under traffic).
|
||||
- Counting rows with `found_posts` when an indexed count query or cached counter would do.
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
# WP Guard — Review Checklist
|
||||
|
||||
Structured walk for review mode. Lead with findings, not summary. Cite file:line. Prioritize: security → silent breakage → i18n → performance.
|
||||
|
||||
## Contents
|
||||
|
||||
- Pass 1: Security sweep
|
||||
- Pass 2: API and hook correctness
|
||||
- Pass 3: i18n
|
||||
- Pass 4: Performance
|
||||
- Pass 5: Packaging hygiene
|
||||
- Reporting
|
||||
|
||||
## Pass 1: Security sweep (must fix)
|
||||
|
||||
Grep-driven; check every hit:
|
||||
|
||||
- `echo`, `print`, `<?=`, `printf` — every variable escaped with the context-correct `esc_*`/`wp_kses`? (Rule 1)
|
||||
- `$_POST`, `$_GET`, `$_REQUEST`, `$_SERVER`, `$_COOKIE` — `wp_unslash()` then sanitized? (Rule 2)
|
||||
- `add_action( 'wp_ajax_`, `admin_post_`, `rest_api_init` — every state-changing handler has BOTH `current_user_can()` and a nonce check / real `permission_callback`? (Rule 3)
|
||||
- `$wpdb->` — every variable behind `prepare()` placeholders? `esc_like()` for LIKE? (Rule 4)
|
||||
- `$_FILES` — handled via `wp_handle_upload()` with type allowlist?
|
||||
- Secrets: API keys hardcoded? Logged? In autoloaded options when they belong in constants/env?
|
||||
|
||||
## Pass 2: API and hook correctness (should fix)
|
||||
|
||||
- Every hooked hook and called function exists in supported WP/plugin versions? (Rule 6 — hallucinated hooks fail silently)
|
||||
- Hook timing right? (No front-end work on admin hooks, no early queries, no `init`-heavy work per request)
|
||||
- Core API replacements: curl, raw `<script>` echoes, manual cron loops, direct file ops? (Rule 5)
|
||||
- Prefixes on every public name: functions, options, transients, meta keys, handles, AJAX actions? (Rule 7)
|
||||
- `ABSPATH` guard present in working files? (Rule 8)
|
||||
- `wp_safe_redirect()` + `exit` after state changes?
|
||||
|
||||
## Pass 3: i18n (should fix; blocking on multilingual projects)
|
||||
|
||||
- User-facing strings without wrappers? Wrong/non-literal text domain? (Rule 9)
|
||||
- Placeholders without translator comments? Plural logic without `_n()`?
|
||||
- Concatenated sentence fragments?
|
||||
- Stored user-facing strings (options/meta) that multilingual plugins can't see? (see [i18n.md](i18n.md))
|
||||
- Raw `date()`/`number_format()` for display?
|
||||
|
||||
## Pass 4: Performance (worth noting; blocking on every-request paths)
|
||||
|
||||
- `posts_per_page => -1`, `query_posts()`, unbounded `get_users()`/`get_terms()`? (Rule 10)
|
||||
- Queries or `get_post_meta()` inside loops without cache priming?
|
||||
- Remote calls without transient/object caching? (Rule 11)
|
||||
- Large options autoloading? Unconditional asset enqueues?
|
||||
|
||||
## Pass 5: Packaging hygiene
|
||||
|
||||
- WooCommerce code in scope → hand off to woo-guard when it is installed; otherwise apply WooCommerce's HPOS, CRUD, checkout, and money rules from its developer documentation.
|
||||
- Activation/deactivation/uninstall: scheduled events cleared, transients cleaned, uninstall removes its data?
|
||||
- Direct entry files, readme version mismatches, debug code (`error_log`, `var_dump`) left in?
|
||||
|
||||
## Reporting
|
||||
|
||||
Use the SKILL.md reporting format. Lead with the security findings and an overall verdict (merge / fix first / do not merge). End with at most three positives worth keeping — reviewers who only list faults get ignored.
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
# WP Guard — Security Reference
|
||||
|
||||
## Contents
|
||||
|
||||
- Escaping: context → function
|
||||
- Sanitization: input type → function
|
||||
- The unslash-then-sanitize order
|
||||
- Nonce + capability lifecycle
|
||||
- REST API permissions
|
||||
- $wpdb->prepare details
|
||||
- File uploads
|
||||
- Common AI-generated violations
|
||||
|
||||
## Escaping: context → function
|
||||
|
||||
Escape at the moment of output, with the function matching the destination context:
|
||||
|
||||
| Output context | Function |
|
||||
|---|---|
|
||||
| HTML body text | `esc_html()` |
|
||||
| HTML attribute | `esc_attr()` |
|
||||
| URL (href/src/action) | `esc_url()` (display) / `esc_url_raw()` (storage/redirects) |
|
||||
| Data for inline JS | `wp_json_encode()` + `wp_add_inline_script()`; `esc_js()` is legacy — single-quoted strings in inline attributes only |
|
||||
| Textarea content | `esc_textarea()` |
|
||||
| Rich/user HTML | `wp_kses_post()` or `wp_kses()` with an explicit allowlist |
|
||||
| Translation + output | `esc_html__()`, `esc_html_e()`, `esc_attr__()` — escape and translate in one call |
|
||||
|
||||
Trust nothing at output time — not even your own stored options; another plugin or a compromised import may have written them. "Escaped on save" is not a defense.
|
||||
|
||||
## Sanitization: input type → function
|
||||
|
||||
| Expected input | Function |
|
||||
|---|---|
|
||||
| Plain text line | `sanitize_text_field()` |
|
||||
| Multiline text | `sanitize_textarea_field()` |
|
||||
| Integer / ID | `absint()` or `intval()` |
|
||||
| Slug/key | `sanitize_key()` / `sanitize_title()` |
|
||||
| Email | `sanitize_email()` + `is_email()` check |
|
||||
| URL | `esc_url_raw()` |
|
||||
| HTML payload | `wp_kses_post()` / `wp_kses()` |
|
||||
| File name | `sanitize_file_name()` |
|
||||
| Anything enumerable | strict allowlist comparison (`in_array( $v, $allowed, true )`) |
|
||||
|
||||
## The unslash-then-sanitize order
|
||||
|
||||
WordPress adds slashes to superglobals. The correct pipeline is always:
|
||||
|
||||
```php
|
||||
$status = sanitize_key( wp_unslash( $_POST['ncs_status'] ?? '' ) );
|
||||
```
|
||||
|
||||
`wp_unslash()` first, sanitizer second. Sanitizing slashed data corrupts legitimate input and hides bugs.
|
||||
|
||||
## Nonce + capability lifecycle
|
||||
|
||||
Both checks, always, for any state change. They answer different questions:
|
||||
|
||||
- `current_user_can( 'manage_options' )` — is this user *allowed* to do this? (authorization)
|
||||
- `check_admin_referer( 'ncs_save_settings' )` / `check_ajax_referer( 'ncs_action', 'nonce' )` — did this request *intend* this action? (CSRF protection)
|
||||
|
||||
A nonce without a capability check lets any logged-in subscriber fire admin actions. A capability check without a nonce leaves CSRF open. AI-generated handlers routinely have one, the other, or neither — verify both exist on every `admin_post_*`, `wp_ajax_*`, and form handler.
|
||||
|
||||
Generate nonces with `wp_nonce_field()` (forms) or `wp_create_nonce()` (AJAX/REST payloads). Nonces are per-user and time-limited; do not cache pages containing them.
|
||||
|
||||
## REST API permissions
|
||||
|
||||
- Every route registers a real `permission_callback`. `__return_true` is acceptable only for genuinely public read-only data — never for writes.
|
||||
- Validate and sanitize via the route's `args` schema (`validate_callback`, `sanitize_callback`) instead of manual checks inside the handler.
|
||||
- Cookie-authenticated REST requests require the `X-WP-Nonce` header (`wp_rest` nonce); custom auth schemes must fail closed.
|
||||
|
||||
## $wpdb->prepare details
|
||||
|
||||
```php
|
||||
$row = $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$wpdb->prefix}ncs_log WHERE user_id = %d AND event = %s",
|
||||
$user_id,
|
||||
$event
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
- Placeholders: `%s`, `%d`, `%f`; `%i` for table/column identifiers (WP ≥ 6.2).
|
||||
- `IN ( … )` lists: build placeholders dynamically — `implode( ',', array_fill( 0, count( $ids ), '%d' ) )` — then prepare with the spread array.
|
||||
- LIKE queries: `$wpdb->esc_like()` the term *before* passing it as a `%s` parameter.
|
||||
- Never interpolate `$_REQUEST` data anywhere near SQL, even "validated" data.
|
||||
- Prefer `WP_Query`, `get_posts()`, meta/term/option APIs when they can express the query — they bring caching for free.
|
||||
|
||||
## File uploads
|
||||
|
||||
Use `wp_handle_upload()` / media APIs — never move `$_FILES` manually. Validate type with `wp_check_filetype_and_ext()` against an allowlist; never trust the client MIME. Store outside executable paths; WordPress handles this when you use its APIs.
|
||||
|
||||
## Common AI-generated violations
|
||||
|
||||
1. `echo '<div>' . $title . '</div>';` — unescaped output (Rule 1). Published research: XSS appears in 86% of AI generations tested on XSS-prone tasks (see [sources.md](sources.md)).
|
||||
2. `wp_ajax_` handler with neither `check_ajax_referer()` nor `current_user_can()` (Rule 3).
|
||||
3. `"SELECT * FROM {$wpdb->posts} WHERE post_title = '$title'"` — interpolated SQL (Rule 4).
|
||||
4. `permission_callback => '__return_true'` on a POST route (Rule 3).
|
||||
5. `curl_init()` inside a plugin (Rule 5) — breaks proxies, blocks, and filters that `wp_remote_*` honors.
|
||||
6. Echoed `<script>` blocks with interpolated PHP — combines Rules 1 and 5; use `wp_enqueue_script()` + `wp_add_inline_script()`/`wp_localize_script()`.
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
# WP Guard — Sources
|
||||
|
||||
Central bibliography. Operational guidance lives in the other references; read this file only when a source URL is needed.
|
||||
|
||||
## Contents
|
||||
|
||||
- WordPress handbooks and standards
|
||||
- Research on AI-generated code defects
|
||||
|
||||
## WordPress handbooks and standards
|
||||
|
||||
- Plugin Handbook — Security: https://developer.wordpress.org/plugins/security/
|
||||
- Common APIs Handbook — Sanitizing Data: https://developer.wordpress.org/apis/security/sanitizing/
|
||||
- Common APIs Handbook — Escaping Data: https://developer.wordpress.org/apis/security/escaping/
|
||||
- Nonces: https://developer.wordpress.org/apis/security/nonces/
|
||||
- WordPress Coding Standards (WPCS ruleset): https://github.com/WordPress/WordPress-Coding-Standards
|
||||
- Plugin Handbook — Internationalization: https://developer.wordpress.org/plugins/internationalization/
|
||||
- wpdb::prepare() reference: https://developer.wordpress.org/reference/classes/wpdb/prepare/
|
||||
- REST API Handbook — Adding Custom Endpoints: https://developer.wordpress.org/rest-api/extending-the-rest-api/adding-custom-endpoints/
|
||||
- Options autoloading (WP 6.6+): https://make.wordpress.org/core/2024/06/18/options-api-disabling-autoload-for-large-options/
|
||||
- Script loading strategies (WP 6.3+): https://make.wordpress.org/core/2023/07/14/registering-scripts-with-async-and-defer-attributes-in-wordpress-6-3/
|
||||
|
||||
## Research on AI-generated code defects
|
||||
|
||||
- Veracode, 2025 GenAI Code Security Report — 45% of AI-generated samples contained OWASP Top 10 vulnerabilities; XSS failed in 86% of XSS-prone tasks: https://www.veracode.com/resources/analyst-reports/2025-genai-code-security-report/
|
||||
- Perry et al., "Do Users Write More Insecure Code with AI Assistants?", ACM CCS 2023: https://arxiv.org/abs/2211.03622
|
||||
- GitGuardian, State of Secrets Sprawl 2025 — 40% higher secret-leak incidence in Copilot-active repos: https://www.gitguardian.com/state-of-secrets-sprawl-report-2025
|
||||
Reference in New Issue
Block a user