From superpowers-sage
Enforces WordPress security in Sage/Acorn: input sanitization (sanitize_text_field, wp_kses), output escaping (esc_html, esc_attr), nonce verification, $wpdb SQL prep, capability checks, secrets, headers.
npx claudepluginhub codigodoleo/superpowers-sage --plugin superpowers-sageThis skill uses the workspace's default tool permissions.
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.
Evaluates WordPress codebase against OWASP Top 10 and WP-specific vulnerability patterns. Use for code security reviews, plugin/theme audits, and incident investigations.
Develops WordPress plugins with structure patterns, hooks, security (nonces, sanitization, prepared $wpdb queries), REST API, custom post types, and Settings API.
Provides OWASP security patterns for Drupal 10/11 including SQL injection prevention, XSS filtering, route access control, and custom checkers. Use for code security reviews and hardening.
Share bugs, ideas, or general feedback.
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.
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'));
}
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>
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:
// 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.
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);
}
// Proceed with action
}
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:
// 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:
// DANGEROUS
DB::raw("SELECT * FROM posts WHERE title = '$userInput'");
// SAFE
DB::select('SELECT * FROM posts WHERE title = ?', [$userInput]);
If the project uses Acorn's HTTP layer, register the VerifyCsrfToken middleware in the kernel:
// app/Http/Kernel.php
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>
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', // 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:
$filetype = wp_check_filetype($filename, null);
if (! in_array($filetype['type'], ['image/jpeg', 'image/png', 'application/pdf'], true)) {
wp_die('Invalid file type.');
}
All sensitive values go in .env and are accessed via env() or config():
STRIPE_SECRET_KEY=sk_live_...
API_TOKEN=abc123
// config/services.php
'stripe' => [
'secret' => env('STRIPE_SECRET_KEY'),
],
// Usage
$key = config('services.stripe.secret');
Never hardcode secrets in:
Ensure .env is listed in .gitignore.
{!! !!} usage without prior sanitization (wp_kses_post or equivalent)current_user_can()$wpdb queries use $wpdb->prepare()DB::raw() or whereRaw() without bindings.env, never in source code.env is in .gitignore| 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 |