| 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:
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.
{{-- 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:
<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:
<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:
if (! wp_verify_nonce($_POST['_my_nonce'] ?? '', 'my_action')) {
wp_die('Security check failed.', 403);
}
AJAX requests:
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:
if (! current_user_can('edit_posts')) {
wp_die('Unauthorized.', 403);
}
Pair capability checks with nonces — the nonce proves intent, the capability proves authorization:
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);
}
}
Step 5 — SQL preparation
When using $wpdb directly (in Service classes or legacy code), always use $wpdb->prepare():
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:
$posts = Post::where('post_type', 'custom_type')
->where('post_status', 'publish')
->get();
Never pass raw user input into DB::raw() or whereRaw() without bindings:
DB::raw("SELECT * FROM posts WHERE title = '$userInput'");
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:
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\VerifyCsrfToken::class,
],
];
Blade forms using Acorn routes must include @csrf:
<form method="POST" action="{{ route('contact.store') }}">
@csrf
{{-- fields --}}
</form>
Step 7 — File upload validation
For Livewire components using WithFileUploads:
use Livewire\WithFileUploads;
class UploadForm extends \Livewire\Component
{
use WithFileUploads;
public $file;
protected $rules = [
'file' => 'required|file|mimes:jpg,png,pdf|max:2048',
];
public function save(): void
{
$this->validate();
$attachment_id = media_handle_upload('file', 0);
}
}
For traditional uploads, validate MIME types server-side — never trust the client extension:
$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():
STRIPE_SECRET_KEY=sk_live_...
API_TOKEN=abc123
'stripe' => [
'secret' => env('STRIPE_SECRET_KEY'),
],
$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
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.