This commit is contained in:
Yuri Karamian
2026-01-25 22:10:05 +01:00
parent 9f06a4c2a2
commit 82ce7ccaf3
6 changed files with 782 additions and 169 deletions
+103
View File
@@ -0,0 +1,103 @@
# WP Security Pack
WordPress security without the bullshit. No upsells, no premium tier, no fake threat counters.
## Requirements
- WordPress 5.0+
- PHP 7.4+
## What It Does
### Login Protection
- Blocks IPs after failed login attempts
- Custom login URL (hides wp-login.php)
- Hides wp-admin from logged-out users
- Honeypot field for bots
- Hides login errors (stops username enumeration)
- Email alerts for admin logins from new IPs
- Country/IP restrictions on login page
### IP Control
- Whitelist and blacklist
- Auto-blacklist after repeated lockouts
- IPv4, IPv6, CIDR supported
### Geo Blocking
- Block countries
- Uses free IP2Location LITE database
- One-click download
### Hardening
- Disable XML-RPC
- Disable dashboard file editing
- Disable application passwords
- Restrict REST API to logged-in users
- Remove WordPress version
- Block user enumeration (?author=1 and REST API)
- Disable pingbacks/trackbacks
### Security Headers
X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, Referrer-Policy, Permissions-Policy, Content-Security-Policy, HSTS
### Two-Factor Authentication
- TOTP (Google Authenticator, Authy, etc.)
- Backup codes
- Enforce for admins
### File Integrity Monitoring
- Checks WordPress core files against official checksums
- Daily scans
- Email alerts on changes
### Malware Scanner
- Scans plugins, themes, uploads
- Pattern-based detection
- Weekly scans
- Does not scan core files (that's what File Integrity is for)
### Activity Log
- Login attempts, lockouts, blocks
- IP, country, username, timestamp
- Configurable retention
- CSV export
### Tools
- Export/import settings
- Force logout all users
- Test email
## Installation
1. Upload to `/wp-content/plugins/wp-security-pack/`
2. Activate
3. Configure under Security menu
## FAQ
**Premium version?**
No. This is it.
**Will it slow my site?**
No. Checks run on login and admin access, not frontend loads.
**Locked myself out?**
Rename the plugin folder via FTP/SSH. Log in. Fix settings.
**Geo-blocking without database?**
Won't work. Download IP2Location LITE from settings. It's free.
**Use with other security plugins?**
Possible but likely conflicts. Pick one.
## Privacy
No tracking. No analytics. No telemetry.
External connections:
- WordPress.org API (core file checksums)
- IP2Location (database download, only when you click it)
## License
GPLv2 or later
-145
View File
@@ -1,145 +0,0 @@
=== WP Security Pack ===
Contributors: arkhost
Tags: security, login, firewall, 2fa, malware
Requires at least: 5.0
Tested up to: 6.4
Requires PHP: 7.4
Stable tag: 1.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
A security plugin. No upsells, no premium version, no "threat counters". Just security settings that work.
== Description ==
WP Security Pack adds security features to WordPress. It's free, open source, and doesn't nag you to upgrade.
**What it does:**
= Login Protection =
* Limit login attempts (block IPs after failed logins)
* Custom login URL (hide wp-login.php)
* Hide wp-admin from non-logged-in users
* Honeypot field to catch bots
* Hide login error messages to prevent username enumeration
* Email alerts when admins log in from new IPs
* Restrict login page access by country or IP
= IP Control =
* Whitelist IPs (always allowed)
* Blacklist IPs (always blocked)
* Auto-blacklist IPs after repeated lockouts
* Supports IPv4, IPv6, and CIDR notation
= Geo Blocking =
* Block access from specific countries
* Uses free IP2Location LITE database
* One-click database download
= Hardening =
* Disable XML-RPC
* Disable file editing in dashboard
* Disable application passwords
* Restrict REST API to logged-in users
* Remove WordPress version from source
* Disable user enumeration (?author=1 and REST API)
* Disable pingbacks and trackbacks
= Security Headers =
* X-Content-Type-Options
* X-Frame-Options
* X-XSS-Protection
* Referrer-Policy
* Permissions-Policy
* Content-Security-Policy
* Strict-Transport-Security (HSTS)
= Two-Factor Authentication =
* TOTP-based (works with Google Authenticator, Authy, etc.)
* Backup codes
* Option to enforce 2FA for administrators
= File Integrity Monitoring =
* Monitors WordPress core files
* Compares against official WordPress.org checksums
* Daily automatic scans
* Email alerts when files change
= Malware Scanner =
* Scans plugins, themes, and uploads
* Pattern-based detection for common threats
* Does not scan WordPress core (use File Integrity for that)
* Weekly automatic scans
= Activity Log =
* Records login attempts (success and failed)
* Records lockouts and blocks
* Shows IP, country, username, timestamp
* Configurable retention period
* Export to CSV
= Tools =
* Export/import settings
* Force logout all users
* Test email delivery
**What it doesn't do:**
* Firewall at the server level (use your host's firewall)
* Real-time threat intelligence (we don't phone home)
* Backup your site (use a backup plugin)
* CDN or performance optimization
**Privacy:**
The plugin doesn't send data anywhere except:
* WordPress.org API to verify core file checksums
* IP2Location to download the geo database (if you click the download button)
No tracking, no analytics, no telemetry.
== Installation ==
1. Upload the plugin files to `/wp-content/plugins/wp-security-pack/`
2. Activate the plugin through the 'Plugins' screen
3. Go to Security in the admin menu to configure
== Frequently Asked Questions ==
= Is there a premium version? =
No. This is the complete plugin.
= Will this slow down my site? =
The plugin runs security checks on specific actions (login, admin access). It doesn't add overhead to frontend page loads for regular visitors.
= I locked myself out =
Connect to your server via FTP or SSH and rename the plugin folder. This disables the plugin and you can log in normally.
= Does geo-blocking work without the database? =
No. You need to download the IP2Location LITE database from the plugin settings. It's free.
= Can I use this with other security plugins? =
Probably, but there may be conflicts. We recommend using one security plugin at a time.
== Screenshots ==
1. Status overview
2. Login protection settings
3. Activity log
4. Two-factor authentication setup
== Changelog ==
= 1.0 =
* Initial release
== Upgrade Notice ==
= 1.0 =
Initial release.
+407 -9
View File
@@ -25,6 +25,7 @@ class WPSP_Admin {
add_action( 'wp_dashboard_setup', array( $this, 'add_dashboard_widget' ) ); add_action( 'wp_dashboard_setup', array( $this, 'add_dashboard_widget' ) );
add_filter( 'plugin_row_meta', array( $this, 'plugin_row_meta' ), 10, 2 ); add_filter( 'plugin_row_meta', array( $this, 'plugin_row_meta' ), 10, 2 );
add_filter( 'plugin_action_links_' . WPSP_PLUGIN_BASENAME, array( $this, 'plugin_action_links' ) ); add_filter( 'plugin_action_links_' . WPSP_PLUGIN_BASENAME, array( $this, 'plugin_action_links' ) );
add_filter( 'submenu_file', array( $this, 'highlight_submenu' ) );
// AJAX handlers. // AJAX handlers.
add_action( 'wp_ajax_wpsp_clear_logs', array( $this, 'ajax_clear_logs' ) ); add_action( 'wp_ajax_wpsp_clear_logs', array( $this, 'ajax_clear_logs' ) );
@@ -41,6 +42,10 @@ class WPSP_Admin {
add_action( 'wp_ajax_wpsp_reset_settings', array( $this, 'ajax_reset_settings' ) ); add_action( 'wp_ajax_wpsp_reset_settings', array( $this, 'ajax_reset_settings' ) );
add_action( 'wp_ajax_wpsp_test_email', array( $this, 'ajax_test_email' ) ); add_action( 'wp_ajax_wpsp_test_email', array( $this, 'ajax_test_email' ) );
add_action( 'wp_ajax_wpsp_force_logout_all', array( $this, 'ajax_force_logout_all' ) ); add_action( 'wp_ajax_wpsp_force_logout_all', array( $this, 'ajax_force_logout_all' ) );
add_action( 'wp_ajax_wpsp_quarantine_file', array( $this, 'ajax_quarantine_file' ) );
add_action( 'wp_ajax_wpsp_restore_file', array( $this, 'ajax_restore_file' ) );
add_action( 'wp_ajax_wpsp_delete_quarantined', array( $this, 'ajax_delete_quarantined' ) );
add_action( 'wp_ajax_wpsp_delete_wp_file', array( $this, 'ajax_delete_wp_file' ) );
} }
/** /**
@@ -83,6 +88,36 @@ class WPSP_Admin {
} }
} }
/**
* Highlight the correct submenu based on current tab.
*
* @param string $submenu_file The current submenu file.
* @return string
*/
public function highlight_submenu( $submenu_file ) {
global $pagenow;
if ( 'admin.php' !== $pagenow ) {
return $submenu_file;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
if ( 'wp-security-pack' !== $page ) {
return $submenu_file;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : 'status';
if ( 'status' === $tab ) {
return 'wp-security-pack';
}
return 'wp-security-pack&tab=' . $tab;
}
/** /**
* Add plugin action links (left side - Settings, Deactivate, etc.). * Add plugin action links (left side - Settings, Deactivate, etc.).
* *
@@ -565,8 +600,129 @@ class WPSP_Admin {
<?php endif; ?> <?php endif; ?>
</td> </td>
</tr> </tr>
<tr>
<th><?php esc_html_e( 'Debug Mode', 'wp-security-pack' ); ?></th>
<td>
<?php if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) : ?>
<span class="wpsp-status wpsp-status-warning"><?php esc_html_e( 'Enabled', 'wp-security-pack' ); ?></span>
<span class="description"><?php esc_html_e( 'Disable WP_DEBUG in production', 'wp-security-pack' ); ?></span>
<?php else : ?>
<span class="wpsp-status wpsp-status-ok"><?php esc_html_e( 'Disabled', 'wp-security-pack' ); ?></span>
<?php endif; ?>
</td>
</tr>
<tr>
<th><?php esc_html_e( 'Database Prefix', 'wp-security-pack' ); ?></th>
<td>
<?php global $wpdb; ?>
<?php if ( 'wp_' === $wpdb->prefix ) : ?>
<span class="wpsp-status wpsp-status-warning"><code>wp_</code></span>
<span class="description"><?php esc_html_e( 'Default prefix - consider changing for new installs', 'wp-security-pack' ); ?></span>
<?php else : ?>
<span class="wpsp-status wpsp-status-ok"><code><?php echo esc_html( $wpdb->prefix ); ?></code></span>
<?php endif; ?>
</td>
</tr>
<tr>
<th><?php esc_html_e( 'Admin Username', 'wp-security-pack' ); ?></th>
<td>
<?php if ( username_exists( 'admin' ) ) : ?>
<span class="wpsp-status wpsp-status-warning"><?php esc_html_e( 'Exists', 'wp-security-pack' ); ?></span>
<span class="description"><?php esc_html_e( '"admin" username is commonly targeted', 'wp-security-pack' ); ?></span>
<?php else : ?>
<span class="wpsp-status wpsp-status-ok"><?php esc_html_e( 'Not found', 'wp-security-pack' ); ?></span>
<?php endif; ?>
</td>
</tr>
<?php
$custom_login = WP_Security_Pack::get_setting( 'custom_login_url', '' );
if ( ! empty( $custom_login ) ) :
?>
<tr>
<th><?php esc_html_e( 'Custom Login URL', 'wp-security-pack' ); ?></th>
<td>
<code><?php echo esc_url( home_url( '/' . $custom_login . '/' ) ); ?></code>
<span class="description"><?php esc_html_e( 'Bookmark this!', 'wp-security-pack' ); ?></span>
</td>
</tr>
<?php endif; ?>
</table> </table>
</div> </div>
<?php
// Check for files that expose WordPress version.
$readme_exists = file_exists( ABSPATH . 'readme.html' );
$license_exists = file_exists( ABSPATH . 'license.txt' );
if ( $readme_exists || $license_exists ) :
?>
<div class="wpsp-section">
<h2><?php esc_html_e( 'Recommended Actions', 'wp-security-pack' ); ?></h2>
<div class="notice notice-warning inline" style="margin: 0 0 15px;">
<p><?php esc_html_e( 'These files expose your WordPress version and can be safely removed.', 'wp-security-pack' ); ?></p>
</div>
<table class="widefat striped">
<thead>
<tr>
<th><?php esc_html_e( 'File', 'wp-security-pack' ); ?></th>
<th><?php esc_html_e( 'Status', 'wp-security-pack' ); ?></th>
<th style="width: 100px;"><?php esc_html_e( 'Action', 'wp-security-pack' ); ?></th>
</tr>
</thead>
<tbody>
<?php if ( $readme_exists ) : ?>
<tr>
<td><code>readme.html</code></td>
<td><span class="wpsp-status wpsp-status-warning"><?php esc_html_e( 'Exists', 'wp-security-pack' ); ?></span></td>
<td>
<button type="button" class="button button-small wpsp-delete-file" data-file="readme.html">
<?php esc_html_e( 'Delete', 'wp-security-pack' ); ?>
</button>
</td>
</tr>
<?php endif; ?>
<?php if ( $license_exists ) : ?>
<tr>
<td><code>license.txt</code></td>
<td><span class="wpsp-status wpsp-status-warning"><?php esc_html_e( 'Exists', 'wp-security-pack' ); ?></span></td>
<td>
<button type="button" class="button button-small wpsp-delete-file" data-file="license.txt">
<?php esc_html_e( 'Delete', 'wp-security-pack' ); ?>
</button>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<script>
jQuery(function($) {
$('.wpsp-delete-file').on('click', function() {
var $btn = $(this);
var file = $btn.data('file');
if (!confirm('<?php echo esc_js( __( 'Delete this file?', 'wp-security-pack' ) ); ?>')) {
return;
}
$btn.prop('disabled', true).text('<?php echo esc_js( __( 'Deleting...', 'wp-security-pack' ) ); ?>');
$.post(ajaxurl, {
action: 'wpsp_delete_wp_file',
file: file,
_ajax_nonce: '<?php echo esc_js( wp_create_nonce( 'wpsp_admin' ) ); ?>'
}, function(response) {
if (response.success) {
$btn.closest('tr').fadeOut();
} else {
alert(response.data.message || '<?php echo esc_js( __( 'Failed to delete file.', 'wp-security-pack' ) ); ?>');
$btn.prop('disabled', false).text('<?php echo esc_js( __( 'Delete', 'wp-security-pack' ) ); ?>');
}
});
});
});
</script>
<?php endif; ?>
<?php <?php
} }
@@ -1792,8 +1948,23 @@ class WPSP_Admin {
<tr> <tr>
<th scope="row"><?php esc_html_e( 'Last Scan', 'wp-security-pack' ); ?></th> <th scope="row"><?php esc_html_e( 'Last Scan', 'wp-security-pack' ); ?></th>
<td> <td>
<?php if ( $malware_results['time'] > 0 ) : ?> <?php
$scan_stats = get_option( 'wpsp_malware_scan_stats', array() );
if ( $malware_results['time'] > 0 ) :
?>
<?php echo esc_html( human_time_diff( $malware_results['time'] ) . ' ' . __( 'ago', 'wp-security-pack' ) ); ?> <?php echo esc_html( human_time_diff( $malware_results['time'] ) . ' ' . __( 'ago', 'wp-security-pack' ) ); ?>
<?php if ( ! empty( $scan_stats['files_scanned'] ) ) : ?>
<span class="description" style="margin-left: 10px;">
<?php
printf(
/* translators: 1: Number of files, 2: Duration in seconds */
esc_html__( '(%1$s files in %2$ss)', 'wp-security-pack' ),
esc_html( number_format_i18n( $scan_stats['files_scanned'] ) ),
esc_html( $scan_stats['duration'] )
);
?>
</span>
<?php endif; ?>
<?php else : ?> <?php else : ?>
<?php esc_html_e( 'Never', 'wp-security-pack' ); ?> <?php esc_html_e( 'Never', 'wp-security-pack' ); ?>
<?php endif; ?> <?php endif; ?>
@@ -1817,11 +1988,12 @@ class WPSP_Admin {
<tr> <tr>
<th><?php esc_html_e( 'File', 'wp-security-pack' ); ?></th> <th><?php esc_html_e( 'File', 'wp-security-pack' ); ?></th>
<th><?php esc_html_e( 'Finding', 'wp-security-pack' ); ?></th> <th><?php esc_html_e( 'Finding', 'wp-security-pack' ); ?></th>
<th style="width: 120px;"><?php esc_html_e( 'Actions', 'wp-security-pack' ); ?></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ( $malware_results['results'] as $file => $findings ) : ?> <?php foreach ( $malware_results['results'] as $file => $findings ) : ?>
<tr> <tr data-file="<?php echo esc_attr( $file ); ?>">
<td><code><?php echo esc_html( str_replace( ABSPATH, '', $file ) ); ?></code></td> <td><code><?php echo esc_html( str_replace( ABSPATH, '', $file ) ); ?></code></td>
<td> <td>
<?php foreach ( $findings as $finding ) : ?> <?php foreach ( $findings as $finding ) : ?>
@@ -1834,6 +2006,11 @@ class WPSP_Admin {
</span> </span>
<?php endforeach; ?> <?php endforeach; ?>
</td> </td>
<td>
<button type="button" class="button button-small wpsp-quarantine-file" data-file="<?php echo esc_attr( $file ); ?>">
<?php esc_html_e( 'Quarantine', 'wp-security-pack' ); ?>
</button>
</td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
@@ -1852,6 +2029,41 @@ class WPSP_Admin {
<p><?php esc_html_e( 'No malware or suspicious files detected.', 'wp-security-pack' ); ?></p> <p><?php esc_html_e( 'No malware or suspicious files detected.', 'wp-security-pack' ); ?></p>
</div> </div>
<?php endif; ?> <?php endif; ?>
<?php
$quarantined_files = $malware_scanner->get_quarantined_files();
if ( ! empty( $quarantined_files ) ) :
?>
<h3 style="margin-top: 20px;"><?php esc_html_e( 'Quarantined Files', 'wp-security-pack' ); ?></h3>
<p class="description"><?php esc_html_e( 'Files that have been isolated. They cannot execute from quarantine.', 'wp-security-pack' ); ?></p>
<table class="widefat striped" style="margin-top: 10px;">
<thead>
<tr>
<th><?php esc_html_e( 'Original Location', 'wp-security-pack' ); ?></th>
<th><?php esc_html_e( 'Quarantined', 'wp-security-pack' ); ?></th>
<th><?php esc_html_e( 'Size', 'wp-security-pack' ); ?></th>
<th style="width: 180px;"><?php esc_html_e( 'Actions', 'wp-security-pack' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $quarantined_files as $name => $meta ) : ?>
<tr data-quarantine-name="<?php echo esc_attr( $name ); ?>">
<td><code><?php echo esc_html( $meta['relative_path'] ?? $meta['original_name'] ); ?></code></td>
<td><?php echo esc_html( human_time_diff( $meta['quarantined_at'] ) . ' ' . __( 'ago', 'wp-security-pack' ) ); ?></td>
<td><?php echo esc_html( size_format( $meta['file_size'] ?? 0 ) ); ?></td>
<td>
<button type="button" class="button button-small wpsp-restore-file" data-name="<?php echo esc_attr( $name ); ?>">
<?php esc_html_e( 'Restore', 'wp-security-pack' ); ?>
</button>
<button type="button" class="button button-small wpsp-delete-quarantined" data-name="<?php echo esc_attr( $name ); ?>" style="color: #a00;">
<?php esc_html_e( 'Delete', 'wp-security-pack' ); ?>
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div> </div>
<div class="wpsp-section"> <div class="wpsp-section">
@@ -1866,8 +2078,8 @@ class WPSP_Admin {
<h3><?php esc_html_e( 'Detection Methods', 'wp-security-pack' ); ?></h3> <h3><?php esc_html_e( 'Detection Methods', 'wp-security-pack' ); ?></h3>
<table class="widefat striped" style="max-width: 600px;"> <table class="widefat striped" style="max-width: 600px;">
<tr> <tr>
<td><strong><?php esc_html_e( 'Hash-based', 'wp-security-pack' ); ?></strong></td> <td><strong><?php esc_html_e( 'Checksum verification', 'wp-security-pack' ); ?></strong></td>
<td><?php esc_html_e( 'Compares file MD5 against database of known malware', 'wp-security-pack' ); ?></td> <td><?php esc_html_e( 'Compares core files against WordPress.org checksums', 'wp-security-pack' ); ?></td>
<td><span style="color: green;">✓ <?php esc_html_e( '100% accurate', 'wp-security-pack' ); ?></span></td> <td><span style="color: green;">✓ <?php esc_html_e( '100% accurate', 'wp-security-pack' ); ?></span></td>
</tr> </tr>
<tr> <tr>
@@ -1875,11 +2087,6 @@ class WPSP_Admin {
<td><?php esc_html_e( 'Scans code for suspicious patterns and functions', 'wp-security-pack' ); ?></td> <td><?php esc_html_e( 'Scans code for suspicious patterns and functions', 'wp-security-pack' ); ?></td>
<td><span style="color: orange;">⚠ <?php esc_html_e( 'May have false positives', 'wp-security-pack' ); ?></span></td> <td><span style="color: orange;">⚠ <?php esc_html_e( 'May have false positives', 'wp-security-pack' ); ?></span></td>
</tr> </tr>
<tr>
<td><strong><?php esc_html_e( 'Checksum verification', 'wp-security-pack' ); ?></strong></td>
<td><?php esc_html_e( 'Compares core files against WordPress.org checksums', 'wp-security-pack' ); ?></td>
<td><span style="color: green;">✓ <?php esc_html_e( '100% accurate', 'wp-security-pack' ); ?></span></td>
</tr>
</table> </table>
</div> </div>
@@ -1933,6 +2140,86 @@ class WPSP_Admin {
location.reload(); location.reload();
}); });
}); });
// Quarantine file.
$('.wpsp-quarantine-file').on('click', function() {
var $btn = $(this);
var filePath = $btn.data('file');
if (!confirm('<?php echo esc_js( __( 'Quarantine this file? It will be moved to a safe location and cannot execute.', 'wp-security-pack' ) ); ?>')) {
return;
}
$btn.prop('disabled', true).text('<?php echo esc_js( __( 'Moving...', 'wp-security-pack' ) ); ?>');
$.post(ajaxurl, {
action: 'wpsp_quarantine_file',
file_path: filePath,
_ajax_nonce: '<?php echo esc_js( wp_create_nonce( 'wpsp_admin' ) ); ?>'
}, function(response) {
if (response.success) {
$btn.closest('tr').fadeOut(function() {
$(this).remove();
location.reload();
});
} else {
alert(response.data.message || '<?php echo esc_js( __( 'Failed to quarantine file.', 'wp-security-pack' ) ); ?>');
$btn.prop('disabled', false).text('<?php echo esc_js( __( 'Quarantine', 'wp-security-pack' ) ); ?>');
}
});
});
// Restore file from quarantine.
$('.wpsp-restore-file').on('click', function() {
var $btn = $(this);
var name = $btn.data('name');
if (!confirm('<?php echo esc_js( __( 'Restore this file to its original location? Make sure it is safe before restoring.', 'wp-security-pack' ) ); ?>')) {
return;
}
$btn.prop('disabled', true).text('<?php echo esc_js( __( 'Restoring...', 'wp-security-pack' ) ); ?>');
$.post(ajaxurl, {
action: 'wpsp_restore_file',
quarantine_name: name,
_ajax_nonce: '<?php echo esc_js( wp_create_nonce( 'wpsp_admin' ) ); ?>'
}, function(response) {
if (response.success) {
location.reload();
} else {
alert(response.data.message || '<?php echo esc_js( __( 'Failed to restore file.', 'wp-security-pack' ) ); ?>');
$btn.prop('disabled', false).text('<?php echo esc_js( __( 'Restore', 'wp-security-pack' ) ); ?>');
}
});
});
// Delete quarantined file permanently.
$('.wpsp-delete-quarantined').on('click', function() {
var $btn = $(this);
var name = $btn.data('name');
if (!confirm('<?php echo esc_js( __( 'Permanently delete this file? This cannot be undone.', 'wp-security-pack' ) ); ?>')) {
return;
}
$btn.prop('disabled', true).text('<?php echo esc_js( __( 'Deleting...', 'wp-security-pack' ) ); ?>');
$.post(ajaxurl, {
action: 'wpsp_delete_quarantined',
quarantine_name: name,
_ajax_nonce: '<?php echo esc_js( wp_create_nonce( 'wpsp_admin' ) ); ?>'
}, function(response) {
if (response.success) {
$btn.closest('tr').fadeOut(function() {
$(this).remove();
});
} else {
alert(response.data.message || '<?php echo esc_js( __( 'Failed to delete file.', 'wp-security-pack' ) ); ?>');
$btn.prop('disabled', false).text('<?php echo esc_js( __( 'Delete', 'wp-security-pack' ) ); ?>');
}
});
});
}); });
</script> </script>
<?php <?php
@@ -2826,4 +3113,115 @@ class WPSP_Admin {
wp_send_json_success(); wp_send_json_success();
} }
/**
* AJAX: Quarantine a suspicious file.
*/
public function ajax_quarantine_file() {
check_ajax_referer( 'wpsp_admin' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'Permission denied.', 'wp-security-pack' ) ) );
}
$file_path = isset( $_POST['file_path'] ) ? sanitize_text_field( wp_unslash( $_POST['file_path'] ) ) : '';
if ( empty( $file_path ) ) {
wp_send_json_error( array( 'message' => __( 'No file specified.', 'wp-security-pack' ) ) );
}
$scanner = new WPSP_Malware_Scanner();
$result = $scanner->quarantine_file( $file_path );
if ( is_wp_error( $result ) ) {
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
}
wp_send_json_success( $result );
}
/**
* AJAX: Restore a file from quarantine.
*/
public function ajax_restore_file() {
check_ajax_referer( 'wpsp_admin' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'Permission denied.', 'wp-security-pack' ) ) );
}
$quarantine_name = isset( $_POST['quarantine_name'] ) ? sanitize_file_name( wp_unslash( $_POST['quarantine_name'] ) ) : '';
if ( empty( $quarantine_name ) ) {
wp_send_json_error( array( 'message' => __( 'No file specified.', 'wp-security-pack' ) ) );
}
$scanner = new WPSP_Malware_Scanner();
$result = $scanner->restore_file( $quarantine_name );
if ( is_wp_error( $result ) ) {
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
}
wp_send_json_success( $result );
}
/**
* AJAX: Delete a quarantined file permanently.
*/
public function ajax_delete_quarantined() {
check_ajax_referer( 'wpsp_admin' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'Permission denied.', 'wp-security-pack' ) ) );
}
$quarantine_name = isset( $_POST['quarantine_name'] ) ? sanitize_file_name( wp_unslash( $_POST['quarantine_name'] ) ) : '';
if ( empty( $quarantine_name ) ) {
wp_send_json_error( array( 'message' => __( 'No file specified.', 'wp-security-pack' ) ) );
}
$scanner = new WPSP_Malware_Scanner();
$result = $scanner->delete_quarantined_file( $quarantine_name );
if ( is_wp_error( $result ) ) {
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
}
wp_send_json_success( $result );
}
/**
* AJAX: Delete WordPress info files (readme.html, license.txt).
*/
public function ajax_delete_wp_file() {
check_ajax_referer( 'wpsp_admin' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'Permission denied.', 'wp-security-pack' ) ) );
}
$file = isset( $_POST['file'] ) ? sanitize_file_name( wp_unslash( $_POST['file'] ) ) : '';
// Only allow specific safe files to be deleted.
$allowed_files = array( 'readme.html', 'license.txt' );
if ( ! in_array( $file, $allowed_files, true ) ) {
wp_send_json_error( array( 'message' => __( 'Invalid file.', 'wp-security-pack' ) ) );
}
$file_path = ABSPATH . $file;
if ( ! file_exists( $file_path ) ) {
wp_send_json_error( array( 'message' => __( 'File not found.', 'wp-security-pack' ) ) );
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
if ( ! unlink( $file_path ) ) {
wp_send_json_error( array( 'message' => __( 'Failed to delete file. Check file permissions.', 'wp-security-pack' ) ) );
}
wp_send_json_success();
}
} }
@@ -155,6 +155,15 @@ class WPSP_DB {
// Delete options. // Delete options.
delete_option( 'wpsp_settings' ); delete_option( 'wpsp_settings' );
delete_option( 'wpsp_db_version' ); delete_option( 'wpsp_db_version' );
delete_option( 'wpsp_malware_scan_results' );
delete_option( 'wpsp_malware_last_scan' );
delete_option( 'wpsp_malware_hashes' );
delete_option( 'wpsp_malware_hashes_updated' );
delete_option( 'wpsp_malware_scan_stats' );
delete_option( 'wpsp_quarantined_files' );
delete_option( 'wpsp_file_scan_results' );
delete_option( 'wpsp_file_last_scan' );
delete_option( 'wpsp_file_baseline' );
// Delete transients. // Delete transients.
delete_transient( 'wpsp_geo_cache' ); delete_transient( 'wpsp_geo_cache' );
@@ -108,27 +108,48 @@ class WPSP_Malware_Scanner {
/** /**
* Load known malware file hashes. * Load known malware file hashes.
* *
* These are MD5 hashes of known malicious files. When a file matches, * Hashes can come from:
* it's 100% confirmed malware - no false positives possible. * 1. Local database (user-added)
* 2. Custom filter (wpsp_malware_hashes)
*/ */
private function load_malware_hashes() { private function load_malware_hashes() {
// Try to load from database (updated hashes). // Load from database (user-added or previously downloaded).
$stored_hashes = get_option( self::HASH_DB_OPTION, array() ); $this->malware_hashes = get_option( self::HASH_DB_OPTION, array() );
if ( ! empty( $stored_hashes ) ) {
$this->malware_hashes = $stored_hashes;
return;
}
// Hash database starts empty - relies on signature detection.
// Users can add hashes via the 'wpsp_malware_hashes' filter or
// by updating from a trusted source using update_hash_database().
$this->malware_hashes = array();
// Allow adding custom hashes via filter. // Allow adding custom hashes via filter.
$this->malware_hashes = apply_filters( 'wpsp_malware_hashes', $this->malware_hashes ); $this->malware_hashes = apply_filters( 'wpsp_malware_hashes', $this->malware_hashes );
} }
/**
* Add a hash to the local database.
*
* @param string $hash MD5 hash.
* @param string $name Malware name/description.
*/
public function add_hash_to_database( $hash, $name ) {
$hashes = get_option( self::HASH_DB_OPTION, array() );
$hashes[ strtolower( $hash ) ] = $name;
update_option( self::HASH_DB_OPTION, $hashes );
$this->malware_hashes[ strtolower( $hash ) ] = $name;
}
/**
* Remove a hash from the local database.
*
* @param string $hash MD5 hash.
*/
public function remove_hash_from_database( $hash ) {
$hashes = get_option( self::HASH_DB_OPTION, array() );
$hash = strtolower( $hash );
if ( isset( $hashes[ $hash ] ) ) {
unset( $hashes[ $hash ] );
update_option( self::HASH_DB_OPTION, $hashes );
}
if ( isset( $this->malware_hashes[ $hash ] ) ) {
unset( $this->malware_hashes[ $hash ] );
}
}
/** /**
* Update malware hash database from remote source. * Update malware hash database from remote source.
* *
@@ -516,6 +537,8 @@ class WPSP_Malware_Scanner {
* @return array * @return array
*/ */
public function scan_files( $paths = array() ) { public function scan_files( $paths = array() ) {
$start_time = microtime( true );
if ( empty( $paths ) ) { if ( empty( $paths ) ) {
// Only scan wp-content directories, NOT WordPress core. // Only scan wp-content directories, NOT WordPress core.
$paths = array( $paths = array(
@@ -586,7 +609,18 @@ class WPSP_Malware_Scanner {
} }
} }
$duration = microtime( true ) - $start_time;
// Save scan stats.
update_option( self::LAST_SCAN_OPTION, time() ); update_option( self::LAST_SCAN_OPTION, time() );
update_option(
'wpsp_malware_scan_stats',
array(
'files_scanned' => $file_count,
'duration' => round( $duration, 2 ),
'issues_found' => count( $results ),
)
);
return $results; return $results;
} }
@@ -763,4 +797,219 @@ class WPSP_Malware_Scanner {
return isset( $colors[ $severity ] ) ? $colors[ $severity ] : '#6c757d'; return isset( $colors[ $severity ] ) ? $colors[ $severity ] : '#6c757d';
} }
/**
* Get quarantine directory path.
*
* @return string
*/
public function get_quarantine_dir() {
$upload_dir = wp_upload_dir();
$quarantine = $upload_dir['basedir'] . '/wpsp-quarantine';
if ( ! file_exists( $quarantine ) ) {
wp_mkdir_p( $quarantine );
// Protect quarantine directory.
$htaccess = $quarantine . '/.htaccess';
if ( ! file_exists( $htaccess ) ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
file_put_contents( $htaccess, "Deny from all\n" );
}
$index = $quarantine . '/index.php';
if ( ! file_exists( $index ) ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
file_put_contents( $index, "<?php\n// Silence is golden.\n" );
}
}
return $quarantine;
}
/**
* Quarantine a suspicious file.
*
* @param string $file_path Full path to the file.
* @return array|WP_Error Result with quarantine info or error.
*/
public function quarantine_file( $file_path ) {
if ( ! file_exists( $file_path ) ) {
return new WP_Error( 'file_not_found', __( 'File not found.', 'wp-security-pack' ) );
}
// Security: Only allow quarantining files within wp-content.
if ( strpos( realpath( $file_path ), realpath( WP_CONTENT_DIR ) ) !== 0 ) {
return new WP_Error( 'invalid_path', __( 'Can only quarantine files within wp-content.', 'wp-security-pack' ) );
}
$quarantine_dir = $this->get_quarantine_dir();
$file_hash = md5_file( $file_path );
$timestamp = time();
$original_name = basename( $file_path );
$relative_path = str_replace( ABSPATH, '', $file_path );
// Create unique quarantine filename.
$quarantine_name = sprintf(
'%s_%s_%s.quarantined',
$timestamp,
$file_hash,
sanitize_file_name( $original_name )
);
$quarantine_path = $quarantine_dir . '/' . $quarantine_name;
// Store metadata.
$metadata = array(
'original_path' => $file_path,
'relative_path' => $relative_path,
'original_name' => $original_name,
'file_hash' => $file_hash,
'quarantined_at' => $timestamp,
'quarantined_by' => get_current_user_id(),
'file_size' => filesize( $file_path ),
);
// Move file to quarantine.
// phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename
if ( ! rename( $file_path, $quarantine_path ) ) {
return new WP_Error( 'move_failed', __( 'Failed to move file to quarantine.', 'wp-security-pack' ) );
}
// Save metadata.
$meta_file = $quarantine_path . '.meta';
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
file_put_contents( $meta_file, wp_json_encode( $metadata, JSON_PRETTY_PRINT ) );
// Update quarantine list in options.
$quarantined = get_option( 'wpsp_quarantined_files', array() );
$quarantined[ $quarantine_name ] = $metadata;
update_option( 'wpsp_quarantined_files', $quarantined );
// Remove from scan results if present.
$results = get_option( self::RESULTS_OPTION, array() );
if ( isset( $results[ $file_path ] ) ) {
unset( $results[ $file_path ] );
update_option( self::RESULTS_OPTION, $results );
}
return array(
'success' => true,
'quarantine_name' => $quarantine_name,
'metadata' => $metadata,
);
}
/**
* Restore a file from quarantine.
*
* @param string $quarantine_name The quarantine filename.
* @return array|WP_Error Result or error.
*/
public function restore_file( $quarantine_name ) {
$quarantine_dir = $this->get_quarantine_dir();
$quarantine_path = $quarantine_dir . '/' . $quarantine_name;
$meta_file = $quarantine_path . '.meta';
if ( ! file_exists( $quarantine_path ) ) {
return new WP_Error( 'file_not_found', __( 'Quarantined file not found.', 'wp-security-pack' ) );
}
// Get metadata.
$metadata = array();
if ( file_exists( $meta_file ) ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
$metadata = json_decode( file_get_contents( $meta_file ), true );
}
if ( empty( $metadata['original_path'] ) ) {
return new WP_Error( 'no_metadata', __( 'Cannot restore: original path unknown.', 'wp-security-pack' ) );
}
$original_path = $metadata['original_path'];
// Ensure parent directory exists.
$parent_dir = dirname( $original_path );
if ( ! file_exists( $parent_dir ) ) {
wp_mkdir_p( $parent_dir );
}
// Restore file.
// phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename
if ( ! rename( $quarantine_path, $original_path ) ) {
return new WP_Error( 'restore_failed', __( 'Failed to restore file.', 'wp-security-pack' ) );
}
// Clean up metadata file.
if ( file_exists( $meta_file ) ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
unlink( $meta_file );
}
// Update quarantine list.
$quarantined = get_option( 'wpsp_quarantined_files', array() );
if ( isset( $quarantined[ $quarantine_name ] ) ) {
unset( $quarantined[ $quarantine_name ] );
update_option( 'wpsp_quarantined_files', $quarantined );
}
return array(
'success' => true,
'restored_path' => $original_path,
);
}
/**
* Delete a quarantined file permanently.
*
* @param string $quarantine_name The quarantine filename.
* @return array|WP_Error Result or error.
*/
public function delete_quarantined_file( $quarantine_name ) {
$quarantine_dir = $this->get_quarantine_dir();
$quarantine_path = $quarantine_dir . '/' . $quarantine_name;
$meta_file = $quarantine_path . '.meta';
if ( ! file_exists( $quarantine_path ) ) {
return new WP_Error( 'file_not_found', __( 'Quarantined file not found.', 'wp-security-pack' ) );
}
// Delete file.
// phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
if ( ! unlink( $quarantine_path ) ) {
return new WP_Error( 'delete_failed', __( 'Failed to delete file.', 'wp-security-pack' ) );
}
// Clean up metadata file.
if ( file_exists( $meta_file ) ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
unlink( $meta_file );
}
// Update quarantine list.
$quarantined = get_option( 'wpsp_quarantined_files', array() );
if ( isset( $quarantined[ $quarantine_name ] ) ) {
unset( $quarantined[ $quarantine_name ] );
update_option( 'wpsp_quarantined_files', $quarantined );
}
return array( 'success' => true );
}
/**
* Get list of quarantined files.
*
* @return array
*/
public function get_quarantined_files() {
return get_option( 'wpsp_quarantined_files', array() );
}
/**
* Get count of quarantined files.
*
* @return int
*/
public function get_quarantine_count() {
return count( $this->get_quarantined_files() );
}
} }
-1
View File
@@ -1,7 +1,6 @@
<?php <?php
/** /**
* Plugin Name: WP Security Pack * Plugin Name: WP Security Pack
* Plugin URI: https://arkhost.com/wp-security-pack
* Description: A free, lightweight security plugin with zero upsells. Login protection, IP blocking, hardening, and activity logging. * Description: A free, lightweight security plugin with zero upsells. Login protection, IP blocking, hardening, and activity logging.
* Version: 1.0 * Version: 1.0
* Requires at least: 5.0 * Requires at least: 5.0