mirror of
https://gitlab.com/ArkHost/WP-Security-Pack.git
synced 2026-09-19 17:37:30 +02:00
v1.0
This commit is contained in:
@@ -108,27 +108,48 @@ class WPSP_Malware_Scanner {
|
||||
/**
|
||||
* Load known malware file hashes.
|
||||
*
|
||||
* These are MD5 hashes of known malicious files. When a file matches,
|
||||
* it's 100% confirmed malware - no false positives possible.
|
||||
* Hashes can come from:
|
||||
* 1. Local database (user-added)
|
||||
* 2. Custom filter (wpsp_malware_hashes)
|
||||
*/
|
||||
private function load_malware_hashes() {
|
||||
// Try to load from database (updated hashes).
|
||||
$stored_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();
|
||||
// Load from database (user-added or previously downloaded).
|
||||
$this->malware_hashes = get_option( self::HASH_DB_OPTION, array() );
|
||||
|
||||
// Allow adding custom hashes via filter.
|
||||
$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.
|
||||
*
|
||||
@@ -516,6 +537,8 @@ class WPSP_Malware_Scanner {
|
||||
* @return array
|
||||
*/
|
||||
public function scan_files( $paths = array() ) {
|
||||
$start_time = microtime( true );
|
||||
|
||||
if ( empty( $paths ) ) {
|
||||
// Only scan wp-content directories, NOT WordPress core.
|
||||
$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(
|
||||
'wpsp_malware_scan_stats',
|
||||
array(
|
||||
'files_scanned' => $file_count,
|
||||
'duration' => round( $duration, 2 ),
|
||||
'issues_found' => count( $results ),
|
||||
)
|
||||
);
|
||||
|
||||
return $results;
|
||||
}
|
||||
@@ -763,4 +797,219 @@ class WPSP_Malware_Scanner {
|
||||
|
||||
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() );
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user