claude-plugin-wordpress-manager 1.5.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +97 -0
- package/README.md +27 -13
- package/agents/wp-accessibility-auditor.md +206 -0
- package/agents/wp-content-strategist.md +18 -0
- package/agents/wp-deployment-engineer.md +34 -2
- package/agents/wp-performance-optimizer.md +12 -0
- package/agents/wp-security-auditor.md +20 -0
- package/agents/wp-security-hardener.md +266 -0
- package/agents/wp-site-manager.md +14 -0
- package/agents/wp-test-engineer.md +207 -0
- package/docs/guides/INDEX.md +46 -0
- package/docs/guides/wp-blog.md +590 -0
- package/docs/guides/wp-design-system.md +976 -0
- package/docs/guides/wp-ecommerce.md +786 -0
- package/docs/guides/wp-landing-page.md +762 -0
- package/docs/guides/wp-portfolio.md +713 -0
- package/docs/plans/2026-02-27-design-system-guide-design.md +30 -0
- package/docs/plans/2026-02-27-site-type-guides-design.md +44 -0
- package/package.json +2 -2
- package/skills/wordpress-router/references/decision-tree.md +12 -2
- package/skills/wp-accessibility/SKILL.md +170 -0
- package/skills/wp-accessibility/references/a11y-audit-tools.md +248 -0
- package/skills/wp-accessibility/references/a11y-testing.md +222 -0
- package/skills/wp-accessibility/references/block-a11y.md +247 -0
- package/skills/wp-accessibility/references/interactive-a11y.md +272 -0
- package/skills/wp-accessibility/references/media-a11y.md +254 -0
- package/skills/wp-accessibility/references/theme-a11y.md +309 -0
- package/skills/wp-audit/SKILL.md +4 -0
- package/skills/wp-block-development/SKILL.md +5 -0
- package/skills/wp-block-themes/SKILL.md +4 -0
- package/skills/wp-e2e-testing/SKILL.md +186 -0
- package/skills/wp-e2e-testing/references/ci-integration.md +174 -0
- package/skills/wp-e2e-testing/references/jest-wordpress.md +114 -0
- package/skills/wp-e2e-testing/references/phpunit-wordpress.md +141 -0
- package/skills/wp-e2e-testing/references/playwright-wordpress.md +108 -0
- package/skills/wp-e2e-testing/references/test-data-generation.md +127 -0
- package/skills/wp-e2e-testing/references/visual-regression.md +107 -0
- package/skills/wp-e2e-testing/references/wp-env-setup.md +97 -0
- package/skills/wp-e2e-testing/scripts/test_inspect.mjs +375 -0
- package/skills/wp-headless/SKILL.md +168 -0
- package/skills/wp-headless/references/api-layer-choice.md +160 -0
- package/skills/wp-headless/references/cors-config.md +245 -0
- package/skills/wp-headless/references/frontend-integration.md +331 -0
- package/skills/wp-headless/references/headless-auth.md +286 -0
- package/skills/wp-headless/references/webhooks.md +277 -0
- package/skills/wp-headless/references/wpgraphql.md +331 -0
- package/skills/wp-headless/scripts/headless_inspect.mjs +321 -0
- package/skills/wp-i18n/SKILL.md +170 -0
- package/skills/wp-i18n/references/js-i18n.md +201 -0
- package/skills/wp-i18n/references/multilingual-setup.md +219 -0
- package/skills/wp-i18n/references/php-i18n.md +196 -0
- package/skills/wp-i18n/references/rtl-support.md +206 -0
- package/skills/wp-i18n/references/translation-workflow.md +178 -0
- package/skills/wp-i18n/references/wpcli-i18n.md +177 -0
- package/skills/wp-i18n/scripts/i18n_inspect.mjs +330 -0
- package/skills/wp-interactivity-api/SKILL.md +4 -0
- package/skills/wp-plugin-development/SKILL.md +6 -0
- package/skills/wp-rest-api/SKILL.md +4 -0
- package/skills/wp-security/SKILL.md +179 -0
- package/skills/wp-security/references/api-restriction.md +147 -0
- package/skills/wp-security/references/authentication-hardening.md +105 -0
- package/skills/wp-security/references/filesystem-hardening.md +105 -0
- package/skills/wp-security/references/http-headers.md +105 -0
- package/skills/wp-security/references/incident-response.md +144 -0
- package/skills/wp-security/references/user-capabilities.md +115 -0
- package/skills/wp-security/references/wp-config-security.md +129 -0
- package/skills/wp-security/scripts/security_inspect.mjs +393 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wp-security
|
|
3
|
+
description: "Use when hardening WordPress security: filesystem permissions, HTTP security headers, authentication hardening, REST/XML-RPC API restriction, user capabilities audit, wp-config security constants, and incident response procedures."
|
|
4
|
+
compatibility: "Targets WordPress 6.9+ (PHP 7.2.24+). Filesystem-based agent with bash + node. Some workflows require WP-CLI and SSH access."
|
|
5
|
+
version: 1.0.0
|
|
6
|
+
source: "vinmor/wordpress-manager"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# WP Security
|
|
10
|
+
|
|
11
|
+
## When to use
|
|
12
|
+
|
|
13
|
+
Use this skill for security hardening and incident response:
|
|
14
|
+
|
|
15
|
+
- Hardening a WordPress installation against common attack vectors
|
|
16
|
+
- Fixing security vulnerabilities found in an audit or scan
|
|
17
|
+
- Responding to a suspected or confirmed site compromise
|
|
18
|
+
- Configuring HTTP security headers (CSP, HSTS, X-Frame-Options)
|
|
19
|
+
- Restricting REST API and XML-RPC exposure
|
|
20
|
+
- Auditing user accounts, roles, and capabilities
|
|
21
|
+
- Securing `wp-config.php` constants and secrets
|
|
22
|
+
- Implementing login protection (brute-force, 2FA)
|
|
23
|
+
|
|
24
|
+
## Inputs required
|
|
25
|
+
|
|
26
|
+
- **Site access**: SSH/SFTP credentials, WP-CLI availability, or hosting panel
|
|
27
|
+
- **Hosting type**: shared, VPS, managed WordPress, or cloud
|
|
28
|
+
- **Current security posture**: output from `wp-audit` skill or `security_inspect.mjs`
|
|
29
|
+
- **Specific threat or vulnerability**: CVE, audit finding, or incident description
|
|
30
|
+
|
|
31
|
+
## Procedure
|
|
32
|
+
|
|
33
|
+
### 0) Security inspection
|
|
34
|
+
|
|
35
|
+
Run the detection script to assess the current security posture:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
node skills/wp-security/scripts/security_inspect.mjs --cwd=/path/to/wordpress
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The script outputs JSON with:
|
|
42
|
+
- `wpConfig` — security-related constants and their values
|
|
43
|
+
- `filesystem` — file permissions and exposed sensitive files
|
|
44
|
+
- `apiExposure` — XML-RPC and REST API exposure
|
|
45
|
+
- `headers` — security headers found in `.htaccess`
|
|
46
|
+
- `riskLevel` — overall risk assessment (low/medium/high/critical)
|
|
47
|
+
- `findings[]` — specific issues with severity and recommended fixes
|
|
48
|
+
|
|
49
|
+
Address findings in priority order: critical → high → medium → low.
|
|
50
|
+
|
|
51
|
+
For a comprehensive initial assessment, run the `wp-audit` skill first. This skill focuses on **remediation** — the procedures to fix issues found during an audit.
|
|
52
|
+
|
|
53
|
+
### 1) Filesystem hardening
|
|
54
|
+
|
|
55
|
+
Set correct file permissions, prevent unauthorized file modification, and block PHP execution in upload directories.
|
|
56
|
+
|
|
57
|
+
Key actions:
|
|
58
|
+
- Files: `644`, Directories: `755`, wp-config.php: `440` or `400`
|
|
59
|
+
- Add `DISALLOW_FILE_EDIT` to wp-config.php to disable the theme/plugin editor
|
|
60
|
+
- Add `DISALLOW_FILE_MODS` to prevent all file modifications including updates
|
|
61
|
+
- Block PHP execution in `wp-content/uploads/` via `.htaccess` or nginx rules
|
|
62
|
+
- Protect `wp-config.php` and `.htaccess` from direct access
|
|
63
|
+
|
|
64
|
+
Read: `references/filesystem-hardening.md`
|
|
65
|
+
|
|
66
|
+
### 2) HTTP security headers
|
|
67
|
+
|
|
68
|
+
Configure security headers to prevent clickjacking, XSS, MIME sniffing, and other browser-level attacks.
|
|
69
|
+
|
|
70
|
+
Key headers:
|
|
71
|
+
- `Content-Security-Policy` — control which resources the browser can load
|
|
72
|
+
- `X-Frame-Options: SAMEORIGIN` — prevent clickjacking (WordPress sends this by default)
|
|
73
|
+
- `X-Content-Type-Options: nosniff` — prevent MIME type sniffing
|
|
74
|
+
- `Strict-Transport-Security` — enforce HTTPS connections
|
|
75
|
+
- `Permissions-Policy` — restrict browser feature access
|
|
76
|
+
- `Referrer-Policy: strict-origin-when-cross-origin` — control referrer information
|
|
77
|
+
|
|
78
|
+
Implementation methods: `.htaccess` (Apache), `nginx.conf`, or PHP via `send_headers` action.
|
|
79
|
+
|
|
80
|
+
Read: `references/http-headers.md`
|
|
81
|
+
|
|
82
|
+
### 3) Authentication hardening
|
|
83
|
+
|
|
84
|
+
Protect the login process against brute-force attacks, credential stuffing, and session hijacking.
|
|
85
|
+
|
|
86
|
+
Key actions:
|
|
87
|
+
- Limit failed login attempts (plugins: Limit Login Attempts Reloaded, or custom `wp_login_failed` hook)
|
|
88
|
+
- Enable two-factor authentication (plugins: Two Factor, WP 2FA)
|
|
89
|
+
- Enforce strong password policies via `user_profile_update_errors` filter
|
|
90
|
+
- Configure session idle timeout and concurrent session limits
|
|
91
|
+
- Use application passwords for REST API access instead of sharing main credentials
|
|
92
|
+
- Block username enumeration via `?author=N` redirects
|
|
93
|
+
|
|
94
|
+
Read: `references/authentication-hardening.md`
|
|
95
|
+
|
|
96
|
+
### 4) API restriction
|
|
97
|
+
|
|
98
|
+
Reduce the attack surface by restricting or disabling unnecessary API endpoints.
|
|
99
|
+
|
|
100
|
+
Key actions:
|
|
101
|
+
- Disable XML-RPC: `add_filter('xmlrpc_enabled', '__return_false')` plus `.htaccess` block
|
|
102
|
+
- Restrict REST API to authenticated users via `rest_authentication_errors` filter
|
|
103
|
+
- Block user enumeration via `/wp-json/wp/v2/users` endpoint
|
|
104
|
+
- Remove REST API discovery link from HTML `<head>`
|
|
105
|
+
- Rate-limit API endpoints at the server level
|
|
106
|
+
|
|
107
|
+
Read: `references/api-restriction.md`
|
|
108
|
+
|
|
109
|
+
### 5) User capabilities audit
|
|
110
|
+
|
|
111
|
+
Review and tighten user accounts and permissions following the principle of least privilege.
|
|
112
|
+
|
|
113
|
+
Key actions:
|
|
114
|
+
- Audit all administrator accounts (should be 1-2 maximum)
|
|
115
|
+
- Remove or downgrade dormant accounts (inactive > 90 days)
|
|
116
|
+
- Verify no accounts use weak usernames ("admin", "test", "user")
|
|
117
|
+
- Review custom capabilities and remove unnecessary elevated permissions
|
|
118
|
+
- Use WP-CLI for bulk auditing: `wp user list --role=administrator`
|
|
119
|
+
|
|
120
|
+
Read: `references/user-capabilities.md`
|
|
121
|
+
|
|
122
|
+
### 6) wp-config.php security constants
|
|
123
|
+
|
|
124
|
+
Secure WordPress configuration with appropriate constants and settings.
|
|
125
|
+
|
|
126
|
+
Key actions:
|
|
127
|
+
- Regenerate security keys and salts (use https://api.wordpress.org/secret-key/1.1/salt/)
|
|
128
|
+
- Change the default table prefix from `wp_` (for new installations)
|
|
129
|
+
- Set `WP_DEBUG` to `false` in production; disable `WP_DEBUG_LOG` and `WP_DEBUG_DISPLAY`
|
|
130
|
+
- Enable `FORCE_SSL_ADMIN` for encrypted admin connections
|
|
131
|
+
- Configure `WP_AUTO_UPDATE_CORE` for automatic security updates
|
|
132
|
+
- Move `wp-config.php` one directory above the web root if hosting allows
|
|
133
|
+
|
|
134
|
+
Read: `references/wp-config-security.md`
|
|
135
|
+
|
|
136
|
+
### 7) Incident response
|
|
137
|
+
|
|
138
|
+
If the site is suspected or confirmed to be compromised, follow a structured response process.
|
|
139
|
+
|
|
140
|
+
Phases:
|
|
141
|
+
1. **Containment** — change all passwords, revoke sessions, enable maintenance mode
|
|
142
|
+
2. **Investigation** — verify core checksums (`wp core verify-checksums`), scan for malware, review access logs, check recently modified files
|
|
143
|
+
3. **Remediation** — remove malicious code, update all components, reinstall WordPress core, review database for injected content
|
|
144
|
+
4. **Recovery** — restore from clean backup if available, re-harden the site
|
|
145
|
+
5. **Post-incident** — document the timeline, update security procedures, notify affected users if data was exposed
|
|
146
|
+
|
|
147
|
+
Read: `references/incident-response.md`
|
|
148
|
+
|
|
149
|
+
## Verification
|
|
150
|
+
|
|
151
|
+
After hardening, verify the security posture:
|
|
152
|
+
|
|
153
|
+
- Re-run `security_inspect.mjs` and confirm all critical/high findings are resolved
|
|
154
|
+
- Test security headers with https://securityheaders.com
|
|
155
|
+
- Verify file permissions: `find . -type f ! -perm 644` and `find . -type d ! -perm 755`
|
|
156
|
+
- Confirm XML-RPC is disabled: `curl -X POST https://site.com/xmlrpc.php` returns 403 or empty
|
|
157
|
+
- Confirm REST API user enumeration is blocked
|
|
158
|
+
- Test login rate limiting by attempting multiple failed logins
|
|
159
|
+
- Verify `wp-config.php` is not accessible via browser
|
|
160
|
+
|
|
161
|
+
## Failure modes / debugging
|
|
162
|
+
|
|
163
|
+
- **Headers not applying**: check `.htaccess` is being read (Apache `AllowOverride All`), or nginx config is included and reloaded
|
|
164
|
+
- **CSP breaking admin UI**: WordPress admin requires `unsafe-inline` and `unsafe-eval` for scripts; use a relaxed policy for `/wp-admin/` and a strict one for the frontend
|
|
165
|
+
- **Login lockout**: if rate limiting locks out the admin, access via WP-CLI (`wp user update admin --user_pass=newpassword`) or directly in the database
|
|
166
|
+
- **REST API restriction breaks plugins**: some plugins (Jetpack, WooCommerce) require public REST API access; whitelist specific namespaces
|
|
167
|
+
- **File permission errors after hardening**: ensure the web server user owns the files; use `chown -R www-data:www-data /path/to/wp`
|
|
168
|
+
|
|
169
|
+
## Escalation
|
|
170
|
+
|
|
171
|
+
- For sites actively under attack: involve the hosting provider's security team
|
|
172
|
+
- For data breaches: follow legal notification requirements (GDPR, state laws)
|
|
173
|
+
- For persistent malware: engage a professional WordPress security service (Sucuri, Wordfence)
|
|
174
|
+
- For complex hosting configurations: consult the server administrator
|
|
175
|
+
- For initial security assessment, use the `wp-audit` skill which provides checklists and scoring
|
|
176
|
+
|
|
177
|
+
## Recommended Agent
|
|
178
|
+
|
|
179
|
+
For implementing hardening fixes and incident response, use the **`wp-security-hardener`** agent. For read-only security audits, use the **`wp-security-auditor`** agent.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# API Restriction
|
|
2
|
+
|
|
3
|
+
Use this file when restricting WordPress API exposure (XML-RPC, REST API).
|
|
4
|
+
|
|
5
|
+
## Disable XML-RPC
|
|
6
|
+
|
|
7
|
+
XML-RPC is rarely needed and is a common brute-force target.
|
|
8
|
+
|
|
9
|
+
### Via filter (recommended)
|
|
10
|
+
|
|
11
|
+
```php
|
|
12
|
+
// Completely disable XML-RPC
|
|
13
|
+
add_filter('xmlrpc_enabled', '__return_false');
|
|
14
|
+
|
|
15
|
+
// Also remove the HTTP header advertising it
|
|
16
|
+
add_filter('wp_headers', function($headers) {
|
|
17
|
+
unset($headers['X-Pingback']);
|
|
18
|
+
return $headers;
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Via .htaccess (blocks at server level)
|
|
23
|
+
|
|
24
|
+
```apache
|
|
25
|
+
<Files xmlrpc.php>
|
|
26
|
+
order deny,allow
|
|
27
|
+
deny from all
|
|
28
|
+
</Files>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Via nginx
|
|
32
|
+
|
|
33
|
+
```nginx
|
|
34
|
+
location = /xmlrpc.php {
|
|
35
|
+
deny all;
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Restrict REST API to authenticated users
|
|
40
|
+
|
|
41
|
+
```php
|
|
42
|
+
add_filter('rest_authentication_errors', function($result) {
|
|
43
|
+
if (true === $result || is_wp_error($result)) {
|
|
44
|
+
return $result;
|
|
45
|
+
}
|
|
46
|
+
if (!is_user_logged_in()) {
|
|
47
|
+
return new WP_Error(
|
|
48
|
+
'rest_not_logged_in',
|
|
49
|
+
__('You are not currently logged in.'),
|
|
50
|
+
['status' => 401]
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return $result;
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Warning**: this breaks any public-facing REST API usage. Whitelist specific namespaces if needed:
|
|
58
|
+
|
|
59
|
+
```php
|
|
60
|
+
add_filter('rest_authentication_errors', function($result) {
|
|
61
|
+
if (true === $result || is_wp_error($result)) {
|
|
62
|
+
return $result;
|
|
63
|
+
}
|
|
64
|
+
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
|
|
65
|
+
// Allow public access to specific namespaces
|
|
66
|
+
$public = ['/wp-json/wp/v2/posts', '/wp-json/wp/v2/pages', '/wp-json/oembed/'];
|
|
67
|
+
foreach ($public as $path) {
|
|
68
|
+
if (str_contains($request_uri, $path)) {
|
|
69
|
+
return $result;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!is_user_logged_in()) {
|
|
73
|
+
return new WP_Error('rest_not_logged_in', 'Authentication required.', ['status' => 401]);
|
|
74
|
+
}
|
|
75
|
+
return $result;
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Block user enumeration via REST
|
|
80
|
+
|
|
81
|
+
```php
|
|
82
|
+
add_filter('rest_endpoints', function($endpoints) {
|
|
83
|
+
if (isset($endpoints['/wp/v2/users'])) {
|
|
84
|
+
unset($endpoints['/wp/v2/users']);
|
|
85
|
+
}
|
|
86
|
+
if (isset($endpoints['/wp/v2/users/(?P<id>[\d]+)'])) {
|
|
87
|
+
unset($endpoints['/wp/v2/users/(?P<id>[\d]+)']);
|
|
88
|
+
}
|
|
89
|
+
return $endpoints;
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Remove REST API discovery links
|
|
94
|
+
|
|
95
|
+
```php
|
|
96
|
+
// Remove from HTML <head>
|
|
97
|
+
remove_action('wp_head', 'rest_output_link_wp_head');
|
|
98
|
+
// Remove from HTTP headers
|
|
99
|
+
remove_action('template_redirect', 'rest_output_link_header', 11);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Disable file editing and modifications
|
|
103
|
+
|
|
104
|
+
```php
|
|
105
|
+
// Disable theme/plugin editor in admin
|
|
106
|
+
define('DISALLOW_FILE_EDIT', true);
|
|
107
|
+
|
|
108
|
+
// Disable all file modifications (includes updates)
|
|
109
|
+
define('DISALLOW_FILE_MODS', true);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Rate limiting
|
|
113
|
+
|
|
114
|
+
Server-level rate limiting is more effective than PHP-based:
|
|
115
|
+
|
|
116
|
+
### Nginx
|
|
117
|
+
```nginx
|
|
118
|
+
limit_req_zone $binary_remote_addr zone=wp_api:10m rate=10r/s;
|
|
119
|
+
location /wp-json/ {
|
|
120
|
+
limit_req zone=wp_api burst=20 nodelay;
|
|
121
|
+
# ... rest of config
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Apache (mod_ratelimit)
|
|
126
|
+
```apache
|
|
127
|
+
<Location /wp-json/>
|
|
128
|
+
SetOutputFilter RATE_LIMIT
|
|
129
|
+
SetEnv rate-limit 10
|
|
130
|
+
</Location>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Verification
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# Test XML-RPC is disabled
|
|
137
|
+
curl -X POST https://site.com/xmlrpc.php
|
|
138
|
+
# Should return 403 or connection refused
|
|
139
|
+
|
|
140
|
+
# Test REST API user enumeration
|
|
141
|
+
curl https://site.com/wp-json/wp/v2/users
|
|
142
|
+
# Should return 401 or 404
|
|
143
|
+
|
|
144
|
+
# Test author enumeration
|
|
145
|
+
curl -s -o /dev/null -w "%{redirect_url}" "https://site.com/?author=1"
|
|
146
|
+
# Should redirect to homepage, not author archive
|
|
147
|
+
```
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Authentication Hardening
|
|
2
|
+
|
|
3
|
+
Use this file when securing the WordPress login process and user sessions.
|
|
4
|
+
|
|
5
|
+
## Limit login attempts
|
|
6
|
+
|
|
7
|
+
### Via plugin (recommended)
|
|
8
|
+
Install "Limit Login Attempts Reloaded" or "WP Limit Login Attempts" for a ready-made solution.
|
|
9
|
+
|
|
10
|
+
### Custom implementation
|
|
11
|
+
|
|
12
|
+
```php
|
|
13
|
+
add_action('wp_login_failed', function($username) {
|
|
14
|
+
$ip = $_SERVER['REMOTE_ADDR'];
|
|
15
|
+
$key = 'login_attempts_' . md5($ip);
|
|
16
|
+
$attempts = (int) get_transient($key);
|
|
17
|
+
set_transient($key, $attempts + 1, 15 * MINUTE_IN_SECONDS);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
add_filter('authenticate', function($user, $username, $password) {
|
|
21
|
+
$ip = $_SERVER['REMOTE_ADDR'];
|
|
22
|
+
$key = 'login_attempts_' . md5($ip);
|
|
23
|
+
$attempts = (int) get_transient($key);
|
|
24
|
+
if ($attempts >= 5) {
|
|
25
|
+
return new WP_Error('too_many_attempts',
|
|
26
|
+
__('Too many login attempts. Please try again in 15 minutes.'));
|
|
27
|
+
}
|
|
28
|
+
return $user;
|
|
29
|
+
}, 30, 3);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Two-factor authentication
|
|
33
|
+
|
|
34
|
+
Recommended plugins:
|
|
35
|
+
- **Two Factor** (WordPress.org) — supports TOTP, email, FIDO U2F
|
|
36
|
+
- **WP 2FA** — user-friendly with grace periods
|
|
37
|
+
|
|
38
|
+
Enable for all administrator accounts at minimum.
|
|
39
|
+
|
|
40
|
+
## Password policies
|
|
41
|
+
|
|
42
|
+
```php
|
|
43
|
+
add_action('user_profile_update_errors', function($errors, $update, $user) {
|
|
44
|
+
if (strlen($user->user_pass) < 12) {
|
|
45
|
+
$errors->add('weak_password', __('Password must be at least 12 characters.'));
|
|
46
|
+
}
|
|
47
|
+
}, 10, 3);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
WordPress 4.3+ includes a password strength meter. Enforce "strong" passwords for privileged roles.
|
|
51
|
+
|
|
52
|
+
## Session management
|
|
53
|
+
|
|
54
|
+
List active sessions:
|
|
55
|
+
```bash
|
|
56
|
+
wp user session list <user_id>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Destroy all sessions for a user:
|
|
60
|
+
```bash
|
|
61
|
+
wp user session destroy <user_id> --all
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Limit concurrent sessions in PHP:
|
|
65
|
+
```php
|
|
66
|
+
add_filter('attach_session_information', function($info) {
|
|
67
|
+
$manager = WP_Session_Tokens::get_instance(get_current_user_id());
|
|
68
|
+
$sessions = $manager->get_all();
|
|
69
|
+
if (count($sessions) > 2) {
|
|
70
|
+
$oldest = array_key_first($sessions);
|
|
71
|
+
$manager->destroy($oldest);
|
|
72
|
+
}
|
|
73
|
+
return $info;
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Application passwords
|
|
78
|
+
|
|
79
|
+
WordPress 5.6+ includes application passwords for REST API authentication:
|
|
80
|
+
- Created in Users → Profile → Application Passwords
|
|
81
|
+
- Use Basic Auth: `Authorization: Basic base64(username:app_password)`
|
|
82
|
+
- Each app password has its own revocation control
|
|
83
|
+
- Better than sharing main credentials
|
|
84
|
+
|
|
85
|
+
## Block username enumeration
|
|
86
|
+
|
|
87
|
+
Prevent `?author=1` from revealing usernames:
|
|
88
|
+
|
|
89
|
+
```php
|
|
90
|
+
add_filter('redirect_canonical', function($redirect, $request) {
|
|
91
|
+
if (preg_match('/\?author=\d+/', $request)) {
|
|
92
|
+
return home_url('/');
|
|
93
|
+
}
|
|
94
|
+
return $redirect;
|
|
95
|
+
}, 10, 2);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Also block via REST API (see `references/api-restriction.md`).
|
|
99
|
+
|
|
100
|
+
## Login URL considerations
|
|
101
|
+
|
|
102
|
+
Plugins like "WPS Hide Login" can change the login URL. Trade-offs:
|
|
103
|
+
- **Pro**: reduces automated bot traffic to `wp-login.php`
|
|
104
|
+
- **Con**: security through obscurity; doesn't stop targeted attacks
|
|
105
|
+
- **Verdict**: useful as a supplementary measure, not a primary defense
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Filesystem Hardening
|
|
2
|
+
|
|
3
|
+
Use this file when securing WordPress file and directory permissions.
|
|
4
|
+
|
|
5
|
+
## Standard permissions
|
|
6
|
+
|
|
7
|
+
| Target | Permission | Meaning |
|
|
8
|
+
|--------|-----------|---------|
|
|
9
|
+
| Files (general) | `644` | Owner read/write, group+others read |
|
|
10
|
+
| Directories | `755` | Owner full, group+others read/execute |
|
|
11
|
+
| `wp-config.php` | `440` or `400` | Owner read only (most secure) |
|
|
12
|
+
| `.htaccess` | `644` | Owner read/write, others read |
|
|
13
|
+
|
|
14
|
+
Set recursively:
|
|
15
|
+
```bash
|
|
16
|
+
find /var/www/html -type f -exec chmod 644 {} \;
|
|
17
|
+
find /var/www/html -type d -exec chmod 755 {} \;
|
|
18
|
+
chmod 440 /var/www/html/wp-config.php
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Ownership
|
|
22
|
+
|
|
23
|
+
The web server user (usually `www-data` on Debian/Ubuntu, `nginx` on RHEL) should own files:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
chown -R www-data:www-data /var/www/html
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
On shared hosting, use your user account and ensure the web server can read files.
|
|
30
|
+
|
|
31
|
+
## Disable file editing
|
|
32
|
+
|
|
33
|
+
Add to `wp-config.php` to disable the theme/plugin editor:
|
|
34
|
+
|
|
35
|
+
```php
|
|
36
|
+
define('DISALLOW_FILE_EDIT', true);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
To also prevent plugin/theme installation and updates via admin:
|
|
40
|
+
|
|
41
|
+
```php
|
|
42
|
+
define('DISALLOW_FILE_MODS', true);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Block PHP execution in uploads
|
|
46
|
+
|
|
47
|
+
Uploads directory should never execute PHP. Create `wp-content/uploads/.htaccess`:
|
|
48
|
+
|
|
49
|
+
```apache
|
|
50
|
+
# Block PHP execution
|
|
51
|
+
<Files *.php>
|
|
52
|
+
deny from all
|
|
53
|
+
</Files>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For nginx:
|
|
57
|
+
```nginx
|
|
58
|
+
location ~* /wp-content/uploads/.*\.php$ {
|
|
59
|
+
deny all;
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Protect sensitive files
|
|
64
|
+
|
|
65
|
+
In the root `.htaccess`:
|
|
66
|
+
|
|
67
|
+
```apache
|
|
68
|
+
# Protect wp-config.php
|
|
69
|
+
<Files wp-config.php>
|
|
70
|
+
order allow,deny
|
|
71
|
+
deny from all
|
|
72
|
+
</Files>
|
|
73
|
+
|
|
74
|
+
# Protect .htaccess itself
|
|
75
|
+
<Files .htaccess>
|
|
76
|
+
order allow,deny
|
|
77
|
+
deny from all
|
|
78
|
+
</Files>
|
|
79
|
+
|
|
80
|
+
# Block access to wp-includes
|
|
81
|
+
<IfModule mod_rewrite.c>
|
|
82
|
+
RewriteEngine On
|
|
83
|
+
RewriteBase /
|
|
84
|
+
RewriteRule ^wp-admin/includes/ - [F,L]
|
|
85
|
+
RewriteRule !^wp-includes/ - [S=3]
|
|
86
|
+
RewriteRule ^wp-includes/[^/]+\.php$ - [F,L]
|
|
87
|
+
RewriteRule ^wp-includes/js/tinymce/langs/.+\.php - [F,L]
|
|
88
|
+
RewriteRule ^wp-includes/theme-compat/ - [F,L]
|
|
89
|
+
</IfModule>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Verification
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Find files with wrong permissions
|
|
96
|
+
find /var/www/html -type f ! -perm 644 -ls
|
|
97
|
+
find /var/www/html -type d ! -perm 755 -ls
|
|
98
|
+
|
|
99
|
+
# Check wp-config permissions
|
|
100
|
+
stat -c '%a %n' /var/www/html/wp-config.php
|
|
101
|
+
|
|
102
|
+
# Verify uploads PHP block
|
|
103
|
+
curl -s -o /dev/null -w "%{http_code}" https://site.com/wp-content/uploads/test.php
|
|
104
|
+
# Should return 403
|
|
105
|
+
```
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# HTTP Security Headers
|
|
2
|
+
|
|
3
|
+
Use this file when configuring HTTP security headers for a WordPress site.
|
|
4
|
+
|
|
5
|
+
## Recommended headers
|
|
6
|
+
|
|
7
|
+
### Content-Security-Policy
|
|
8
|
+
|
|
9
|
+
WordPress admin requires `unsafe-inline` and `unsafe-eval`. Use a relaxed policy for admin, strict for frontend:
|
|
10
|
+
|
|
11
|
+
```apache
|
|
12
|
+
# Frontend (strict)
|
|
13
|
+
Header set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; frame-ancestors 'self'"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
For sites using inline scripts/styles (common with plugins):
|
|
17
|
+
```apache
|
|
18
|
+
Header set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data: https:; frame-ancestors 'self'"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### X-Frame-Options
|
|
22
|
+
|
|
23
|
+
Prevents clickjacking. WordPress sends this by default via `send_frame_options_header()`:
|
|
24
|
+
|
|
25
|
+
```apache
|
|
26
|
+
Header always set X-Frame-Options "SAMEORIGIN"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### X-Content-Type-Options
|
|
30
|
+
|
|
31
|
+
Prevents MIME type sniffing:
|
|
32
|
+
|
|
33
|
+
```apache
|
|
34
|
+
Header always set X-Content-Type-Options "nosniff"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Strict-Transport-Security (HSTS)
|
|
38
|
+
|
|
39
|
+
Enforces HTTPS. Only enable if SSL is fully configured:
|
|
40
|
+
|
|
41
|
+
```apache
|
|
42
|
+
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Permissions-Policy
|
|
46
|
+
|
|
47
|
+
Restricts browser features:
|
|
48
|
+
|
|
49
|
+
```apache
|
|
50
|
+
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Referrer-Policy
|
|
54
|
+
|
|
55
|
+
Controls referrer information:
|
|
56
|
+
|
|
57
|
+
```apache
|
|
58
|
+
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Implementation methods
|
|
62
|
+
|
|
63
|
+
### Apache (.htaccess)
|
|
64
|
+
|
|
65
|
+
```apache
|
|
66
|
+
<IfModule mod_headers.c>
|
|
67
|
+
Header always set X-Frame-Options "SAMEORIGIN"
|
|
68
|
+
Header always set X-Content-Type-Options "nosniff"
|
|
69
|
+
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
|
70
|
+
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
|
|
71
|
+
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
|
72
|
+
</IfModule>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Nginx
|
|
76
|
+
|
|
77
|
+
```nginx
|
|
78
|
+
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
79
|
+
add_header X-Content-Type-Options "nosniff" always;
|
|
80
|
+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
81
|
+
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
|
82
|
+
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### PHP (via WordPress hook)
|
|
86
|
+
|
|
87
|
+
```php
|
|
88
|
+
add_action('send_headers', function() {
|
|
89
|
+
header('X-Content-Type-Options: nosniff');
|
|
90
|
+
header('Referrer-Policy: strict-origin-when-cross-origin');
|
|
91
|
+
header('Permissions-Policy: camera=(), microphone=(), geolocation=()');
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Testing
|
|
96
|
+
|
|
97
|
+
- Use https://securityheaders.com to scan headers
|
|
98
|
+
- Use browser DevTools → Network tab → click request → Headers
|
|
99
|
+
- Use `curl -I https://site.com` to view response headers
|
|
100
|
+
|
|
101
|
+
## Common issues
|
|
102
|
+
|
|
103
|
+
- **CSP breaks admin**: exclude `/wp-admin/` from strict CSP rules or add `unsafe-inline`/`unsafe-eval`
|
|
104
|
+
- **HSTS lockout**: if SSL breaks, users can't access the site; start with a short `max-age` (3600) and increase gradually
|
|
105
|
+
- **Headers not applying**: verify Apache `mod_headers` is enabled; verify nginx config is included and reloaded
|