WordPress security hardening — nonce verification, wp_nonce_field, check_admin_referer, sanitize_text_field, sanitize_email, esc_html, esc_attr, esc_url, wp_kses, SQL injection prevention, $wpdb->prepare(), capability checks, current_user_can, authentication hardening, wp-config.php secrets, security headers, file permissions, Bedrock .env secrets, disable XML-RPC, brute force wp-login.php, CSRF protection. Invoke for: security audit, input sanitization, output escaping, nonce verification, S...
Scanned 9/11/2026
Install to Claude Code
npx -y skills add hekivo/superpowers-sage --skill wp-security --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Wp Security?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/hekivo-wp-security-superpowers-sage)More formats (shields.io, HTML) on the badges page.
---
name: superpowers-sage:wp-security
description: >
WordPress security hardening — nonce verification, wp_nonce_field,
check_admin_referer, sanitize_text_field, sanitize_email, esc_html, esc_attr,
esc_url, wp_kses, SQL injection prevention, $wpdb->prepare(), capability checks,
current_user_can, authentication hardening, wp-config.php secrets,
security headers, file permissions, Bedrock .env secrets, disable XML-RPC,
brute force wp-login.php, CSRF protection.
Invoke for: security audit, input sanitization, output escaping, nonce verification,
SQL injection prevention, CSRF protection, capability checks in REST endpoints.
user-invocable: false
---
# WordPress Security Patterns for Sage/Acorn
## When to use
Apply this skill whenever writing or reviewing code that handles user input, renders output, checks permissions, interacts with the database via `$wpdb`, processes file uploads, or stores sensitive configuration. This skill is also used as a final review pass by the sage-router when other skills produce code.
## Inputs required
- The code or feature under review/development
- The context: Blade template, Livewire component, Service class, middleware, or REST endpoint
## Procedure
### Step 1 — Sanitize on input
Every value originating from the user (`$_GET`, `$_POST`, `$_REQUEST`, form submissions, URL parameters) must be sanitized before storage or processing.
**Common sanitization functions:**
| Function | Use for |
|---|---|
| `sanitize_text_field()` | Single-line plain text |
| `sanitize_textarea_field()` | Multi-line plain text |
| `sanitize_email()` | Email addresses |
| `sanitize_url()` | URLs |
| `absint()` | Non-negative integers |
| `sanitize_file_name()` | File names |
| `wp_kses_post()` | Rich HTML (post-level allowed tags) |
| `wp_kses()` | HTML with custom allowed tags |
| `sanitize_title()` | Slugs |
In Acorn Service classes and controllers, sanitize at the point of entry:
```php
public function store(Request $request): void
{
$title = sanitize_text_field($request->input('title'));
$body = wp_kses_post($request->input('body'));
}
```
### Step 2 — Escape on output
Every dynamic value rendered in a Blade template must be escaped. Blade's `{{ }}` syntax auto-escapes via `htmlspecialchars`. Use `{!! !!}` only when you have already sanitized the content and intentionally need raw HTML.
```blade
{{-- Safe — auto-escaped --}}
<h1>{{ $post->post_title }}</h1>
<p>{{ get_the_excerpt() }}</p>
{{-- Raw output — only after sanitization --}}
{!! wp_kses_post($post->post_content) !!}
```
**Contextual escaping helpers:**
| Context | Function |
|---|---|
| HTML body | `esc_html()` or `{{ }}` |
| HTML attribute | `esc_attr()` |
| URL / href | `esc_url()` |
| JavaScript inline | `esc_js()` |
| Textarea content | `esc_textarea()` |
In Blade, when outputting into attributes:
```blade
<a href="{{ esc_url($link) }}" title="{{ esc_attr($title) }}">
{{ $label }}
</a>
```
### Step 3 — Nonce verification
Always pair form submissions and AJAX requests with nonce verification.
**Traditional forms in Blade:**
```blade
<form method="POST" action="{{ admin_url('admin-post.php') }}">
@csrf {{-- If using Acorn middleware --}}
{!! wp_nonce_field('my_action', '_my_nonce', true, false) !!}
<button type="submit">Submit</button>
</form>
```
Server-side verification:
```php
if (! wp_verify_nonce($_POST['_my_nonce'] ?? '', 'my_action')) {
wp_die('Security check failed.', 403);
}
```
**AJAX requests:**
```php
// In callback
check_ajax_referer('my_ajax_action', 'nonce');
```
**Livewire components:** Livewire handles CSRF automatically via its middleware. No manual nonce is needed for Livewire actions, but verify capabilities on the server side in every action method.
### Step 4 — Capability checks
Never rely on nonces alone. Always verify the user has permission to perform the action:
```php
if (! current_user_can('edit_posts')) {
wp_die('Unauthorized.', 403);
}
```
Pair capability checks with nonces — the nonce proves intent, the capability proves authorization:
```php
public function handleFormSubmission(): void
{
if (! wp_verify_nonce($_POST['_nonce'] ?? '', 'update_settings')) {
wp_die('Invalid nonce.', 403);
}
if (! current_user_can('manage_options')) {
wp_die('Insufficient permissions.', 403);
}
// Proceed with action
}
```
### Step 5 — SQL preparation
When using `$wpdb` directly (in Service classes or legacy code), always use `$wpdb->prepare()`:
```php
global $wpdb;
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE post_type = %s AND post_status = %s",
'custom_type',
'publish'
)
);
```
**Eloquent is safe by default.** Acorn's Eloquent ORM uses parameterized queries internally. Standard Eloquent usage does not require manual preparation:
```php
// Safe — parameterized by Eloquent
$posts = Post::where('post_type', 'custom_type')
->where('post_status', 'publish')
->get();
```
Never pass raw user input into `DB::raw()` or `whereRaw()` without bindings:
```php
// DANGEROUS
DB::raw("SELECT * FROM posts WHERE title = '$userInput'");
// SAFE
DB::select('SELECT * FROM posts WHERE title = ?', [$userInput]);
```
### Step 6 — CSRF protection via Acorn middleware
If the project uses Acorn's HTTP layer, register the `VerifyCsrfToken` middleware in the kernel:
```php
// app/Http/Kernel.php
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\VerifyCsrfToken::class,
],
];
```
Blade forms using Acorn routes must include `@csrf`:
```blade
<form method="POST" action="{{ route('contact.store') }}">
@csrf
{{-- fields --}}
</form>
```
### Step 7 — File upload validation
For Livewire components using `WithFileUploads`:
```php
use Livewire\WithFileUploads;
class UploadForm extends \Livewire\Component
{
use WithFileUploads;
public $file;
protected $rules = [
'file' => 'required|file|mimes:jpg,png,pdf|max:2048', // 2MB max
];
public function save(): void
{
$this->validate();
// Use WordPress upload functions for proper media library integration
$attachment_id = media_handle_upload('file', 0);
}
}
```
For traditional uploads, validate MIME types server-side — never trust the client extension:
```php
$filetype = wp_check_filetype($filename, null);
if (! in_array($filetype['type'], ['image/jpeg', 'image/png', 'application/pdf'], true)) {
wp_die('Invalid file type.');
}
```
### Step 8 — Secrets management
All sensitive values go in `.env` and are accessed via `env()` or `config()`:
```env
STRIPE_SECRET_KEY=sk_live_...
API_TOKEN=abc123
```
```php
// config/services.php
'stripe' => [
'secret' => env('STRIPE_SECRET_KEY'),
],
// Usage
$key = config('services.stripe.secret');
```
Never hardcode secrets in:
- PHP source files
- Blade templates
- JavaScript files
- Version-controlled config files
- Composer scripts
Ensure `.env` is listed in `.gitignore`.
## Verification
- [ ] No `{!! !!}` usage without prior sanitization (`wp_kses_post` or equivalent)
- [ ] All form submissions verify a nonce
- [ ] All privileged actions check `current_user_can()`
- [ ] All `$wpdb` queries use `$wpdb->prepare()`
- [ ] No raw user input in `DB::raw()` or `whereRaw()` without bindings
- [ ] File uploads validate MIME types server-side
- [ ] Secrets are in `.env`, never in source code
- [ ] `.env` is in `.gitignore`
- [ ] CSRF middleware is active for Acorn web routes
## Failure modes
| Symptom | Cause | Fix |
|---|---|---|
| XSS in rendered page | Using `{!! !!}` with unsanitized data | Switch to `{{ }}` or sanitize with `wp_kses_post()` before raw output |
| CSRF attack succeeds | Missing nonce or `@csrf` | Add `wp_nonce_field()` or `@csrf` and verify server-side |
| Unauthorized access | Missing `current_user_can()` check | Add capability check before every privileged operation |
| SQL injection | Raw `$wpdb` query without `prepare()` | Wrap query in `$wpdb->prepare()` with typed placeholders |
| Secrets leaked in repo | Hardcoded API keys | Move to `.env`, rotate compromised keys immediately |
| Malicious file upload | Missing MIME validation | Validate with `wp_check_filetype()` and restrict allowed types |
## Escalation
- If a vulnerability is found in production code, flag it to the user immediately with severity level and remediation steps.
- If a third-party plugin introduces a security concern, recommend the user contact the plugin author or find an alternative.
- If the security requirement exceeds WordPress capabilities (e.g., SOC 2 compliance), recommend consulting a security specialist.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!