Skip to content

Commit 2c41c4d

Browse files
authored
Updates
1 parent 464d4d7 commit 2c41c4d

File tree

4 files changed

+141
-73
lines changed

4 files changed

+141
-73
lines changed

README.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,6 @@ A lightweight WordPress plugin designed to optimize your website by removing unn
2020
- **DNS Prefetching:** Add DNS prefetch for common external domains to improve load times (HTTPS-only for security)
2121
- **Jetpack Optimization:** Remove Jetpack advertisements and promotions
2222

23-
## Security Features
24-
25-
This plugin implements comprehensive security measures following WordPress and OWASP best practices:
26-
27-
- **CSRF Protection:** WordPress nonce verification for all form submissions
28-
- **Input Validation:** Multi-layer validation and sanitization for all user inputs
29-
- **Output Escaping:** Context-appropriate escaping for all outputs (HTML, attributes, URLs)
30-
- **HTTPS Enforcement:** DNS prefetch domains must use HTTPS protocol for security
31-
- **SSRF Prevention:** Blocks private IP ranges and localhost addresses
32-
- **Capability Checks:** Proper user permission verification for all admin functions
33-
- **Direct Access Prevention:** Prevents direct script execution outside WordPress
34-
3523
## Installation
3624

3725
### Manual Installation
@@ -61,6 +49,18 @@ composer require enginescript/simple-wp-optimizer
6149
3. **Performance Options:** Disable emojis and jQuery Migrate
6250
4. **DNS Prefetch Configuration:** Add domains for DNS prefetching
6351

52+
## Security Features
53+
54+
This plugin implements comprehensive security measures following WordPress and OWASP best practices:
55+
56+
- **CSRF Protection:** WordPress nonce verification for all form submissions
57+
- **Input Validation:** Multi-layer validation and sanitization for all user inputs
58+
- **Output Escaping:** Context-appropriate escaping for all outputs (HTML, attributes, URLs)
59+
- **HTTPS Enforcement:** DNS prefetch domains must use HTTPS protocol for security
60+
- **SSRF Prevention:** Blocks private IP ranges and localhost addresses
61+
- **Capability Checks:** Proper user permission verification for all admin functions
62+
- **Direct Access Prevention:** Prevents direct script execution outside WordPress
63+
6464
## Frequently Asked Questions
6565

6666
### Will this plugin work with my theme?

phpmd-wordpress.xml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ruleset name="WordPress Plugin PHPMD Ruleset"
3+
xmlns="http://pmd.sf.net/ruleset/1.0.0"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd"
6+
xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd">
7+
8+
<description>Custom PHPMD rules for WordPress plugins that excludes WordPress-specific patterns</description>
9+
10+
<!-- Clean Code Rules (with WordPress exceptions) -->
11+
<rule ref="rulesets/cleancode.xml">
12+
<!-- Exclude ElseExpression - WordPress often requires else for security patterns -->
13+
<exclude name="ElseExpression"/>
14+
<!-- Exclude MissingImport - WordPress core classes are auto-loaded -->
15+
<exclude name="MissingImport"/>
16+
</rule>
17+
18+
<!-- Code Size Rules -->
19+
<rule ref="rulesets/codesize.xml"/>
20+
21+
<!-- Controversial Rules (with WordPress exceptions) -->
22+
<rule ref="rulesets/controversial.xml">
23+
<!-- Exclude Superglobals - WordPress securely uses $_GET, $_POST, etc. -->
24+
<exclude name="Superglobals"/>
25+
</rule>
26+
27+
<!-- Design Rules (with WordPress exceptions) -->
28+
<rule ref="rulesets/design.xml">
29+
<!-- Exclude ExitExpression - Required for WordPress file downloads and redirects -->
30+
<exclude name="ExitExpression"/>
31+
</rule>
32+
33+
<!-- Naming Rules -->
34+
<rule ref="rulesets/naming.xml"/>
35+
36+
<!-- Unused Code Rules -->
37+
<rule ref="rulesets/unusedcode.xml"/>
38+
39+
</ruleset>

simple-wp-optimizer.php

Lines changed: 90 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -370,21 +370,23 @@ function es_optimizer_render_textarea_option($options, $optionName, $title, $des
370370
* @return array Validated and sanitized options
371371
*/
372372
function es_optimizer_validate_options($input) {
373-
// Security: Verify nonce for CSRF protection
374-
// WordPress requires unslashing $_POST data before sanitization
375-
$nonce_value = isset($_POST['es_optimizer_settings_nonce']) ? wp_unslash($_POST['es_optimizer_settings_nonce']) : '';
376-
377-
if (empty($nonce_value) || !wp_verify_nonce($nonce_value, 'es_optimizer_settings_action')) {
378-
// Add admin notice for failed nonce verification
379-
add_settings_error(
380-
'es_optimizer_options',
381-
'nonce_failed',
382-
esc_html__('Security verification failed. Please try again.', 'Simple-WP-Optimizer'),
383-
'error'
384-
);
373+
// Security: Verify nonce for CSRF protection when using WordPress Settings API
374+
// The nonce is automatically handled by WordPress Settings API, but we add extra verification
375+
if (isset($_POST['es_optimizer_settings_nonce'])) {
376+
$nonceValue = sanitize_text_field(wp_unslash($_POST['es_optimizer_settings_nonce']));
385377

386-
// Return current options without changes
387-
return get_option('es_optimizer_options', es_optimizer_get_default_options());
378+
if (!wp_verify_nonce($nonceValue, 'es_optimizer_settings_action')) {
379+
// Add admin notice for failed nonce verification
380+
add_settings_error(
381+
'es_optimizer_options',
382+
'nonce_failed',
383+
esc_html__('Security verification failed. Please try again.', 'Simple-WP-Optimizer'),
384+
'error'
385+
);
386+
387+
// Return current options without changes
388+
return get_option('es_optimizer_options', es_optimizer_get_default_options());
389+
}
388390
}
389391

390392
$valid = array();
@@ -411,82 +413,109 @@ function es_optimizer_validate_options($input) {
411413
/**
412414
* Validate DNS prefetch domains with enhanced security
413415
*
414-
* @param string $domains_input Raw domain input from user
416+
* @param string $domainsInput Raw domain input from user
415417
* @return string Validated and sanitized domains
416418
*/
417-
function es_optimizer_validate_dns_domains($domains_input) {
418-
$domains = explode("\n", trim($domains_input));
419-
$sanitized_domains = array();
420-
$rejected_domains = array();
419+
function es_optimizer_validate_dns_domains($domainsInput) {
420+
$domains = explode("\n", trim($domainsInput));
421+
$sanitizedDomains = array();
422+
$rejectedDomains = array();
421423

422424
foreach ($domains as $domain) {
423425
$domain = trim($domain);
424426
if (empty($domain)) {
425427
continue;
426428
}
427429

428-
// Enhanced URL validation with security checks
429-
if (!filter_var($domain, FILTER_VALIDATE_URL)) {
430-
$rejected_domains[] = $domain . ' (invalid URL format)';
431-
continue;
432-
}
433-
434-
// Use wp_parse_url instead of parse_url for WordPress compatibility
435-
$parsed_url = wp_parse_url($domain);
430+
$validationResult = es_optimizer_validate_single_domain($domain);
436431

437-
// Security: Enforce HTTPS-only domains for DNS prefetch
438-
if (!isset($parsed_url['scheme']) || $parsed_url['scheme'] !== 'https') {
439-
$rejected_domains[] = $domain . ' (HTTPS required for security)';
440-
continue;
441-
}
442-
443-
// Additional security checks
444-
if (!isset($parsed_url['host'])) {
445-
$rejected_domains[] = $domain . ' (no host found)';
446-
continue;
432+
if ($validationResult['valid']) {
433+
$sanitizedDomains[] = $validationResult['domain'];
434+
} else {
435+
$rejectedDomains[] = $validationResult['error'];
447436
}
448-
449-
$host = $parsed_url['host'];
450-
451-
// Prevent localhost and private IP ranges for security
452-
$is_local = in_array($host, array('localhost', '127.0.0.1', '::1'));
453-
$is_private_ip = filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false;
454-
455-
if ($is_local || !$is_private_ip) {
456-
$rejected_domains[] = $domain . ' (private/local address not allowed)';
457-
continue;
458-
}
459-
460-
// Security: Use esc_url_raw to sanitize URLs before storing in database
461-
$sanitized_domains[] = esc_url_raw($domain);
462437
}
463438

464439
// Show admin notice if any domains were rejected for security reasons
465-
if (!empty($rejected_domains)) {
466-
es_optimizer_show_domain_rejection_notice($rejected_domains);
440+
if (!empty($rejectedDomains)) {
441+
es_optimizer_show_domain_rejection_notice($rejectedDomains);
442+
}
443+
444+
return implode("\n", $sanitizedDomains);
445+
}
446+
447+
/**
448+
* Validate a single DNS prefetch domain
449+
*
450+
* @param string $domain Domain to validate
451+
* @return array Validation result with 'valid' boolean and 'domain' or 'error'
452+
*/
453+
function es_optimizer_validate_single_domain($domain) {
454+
// Enhanced URL validation with security checks
455+
if (!filter_var($domain, FILTER_VALIDATE_URL)) {
456+
return array(
457+
'valid' => false,
458+
'error' => $domain . ' (invalid URL format)'
459+
);
467460
}
468461

469-
return implode("\n", $sanitized_domains);
462+
// Use wp_parse_url instead of parse_url for WordPress compatibility
463+
$parsedUrl = wp_parse_url($domain);
464+
465+
// Security: Enforce HTTPS-only domains for DNS prefetch
466+
if (!isset($parsedUrl['scheme']) || $parsedUrl['scheme'] !== 'https') {
467+
return array(
468+
'valid' => false,
469+
'error' => $domain . ' (HTTPS required for security)'
470+
);
471+
}
472+
473+
// Additional security checks
474+
if (!isset($parsedUrl['host'])) {
475+
return array(
476+
'valid' => false,
477+
'error' => $domain . ' (no host found)'
478+
);
479+
}
480+
481+
$host = $parsedUrl['host'];
482+
483+
// Prevent localhost and private IP ranges for security
484+
$isLocal = in_array($host, array('localhost', '127.0.0.1', '::1'));
485+
$isPrivateIp = filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false;
486+
487+
if ($isLocal || !$isPrivateIp) {
488+
return array(
489+
'valid' => false,
490+
'error' => $domain . ' (private/local address not allowed)'
491+
);
492+
}
493+
494+
// Security: Use esc_url_raw to sanitize URLs before storing in database
495+
return array(
496+
'valid' => true,
497+
'domain' => esc_url_raw($domain)
498+
);
470499
}
471500

472501
/**
473502
* Show admin notice for rejected domains
474503
*
475-
* @param array $rejected_domains Array of rejected domain strings
504+
* @param array $rejectedDomains Array of rejected domain strings
476505
*/
477-
function es_optimizer_show_domain_rejection_notice($rejected_domains) {
506+
function es_optimizer_show_domain_rejection_notice($rejectedDomains) {
478507
// Security: Properly escape and limit the rejected domains in error messages
479-
$escaped_domains = array_map('esc_html', array_slice($rejected_domains, 0, 3));
480-
$rejected_message = implode(', ', $escaped_domains);
508+
$escapedDomains = array_map('esc_html', array_slice($rejectedDomains, 0, 3));
509+
$rejectedMessage = implode(', ', $escapedDomains);
481510

482-
if (count($rejected_domains) > 3) {
483-
$rejected_message .= esc_html__('...', 'Simple-WP-Optimizer');
511+
if (count($rejectedDomains) > 3) {
512+
$rejectedMessage .= esc_html__('...', 'Simple-WP-Optimizer');
484513
}
485514

486515
// translators: %s is the list of rejected domain names
487516
$message = sprintf(
488517
esc_html__('Some DNS prefetch domains were rejected for security reasons: %s', 'Simple-WP-Optimizer'),
489-
$rejected_message
518+
$rejectedMessage
490519
);
491520

492521
add_settings_error(

0 commit comments

Comments
 (0)