Automatic WordPress security: local firewall, malware and file-integrity scanning, vulnerability protection, quarantine, scheduled backups, an optional page cache, and automatic global threat intelligence. See README.md for installation, update, and uninstall instructions.
103 lines
4.0 KiB
PHP
103 lines
4.0 KiB
PHP
<?php
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
class Argus_Upload_Guard {
|
|
|
|
const EXECUTABLE_PATTERN = '/\.ph(p[3457]?|t|tml)/i';
|
|
|
|
const SCAN_CHUNK_BYTES = 65536;
|
|
const MAX_FILE_SIZE_TO_SCAN = 20971520;
|
|
|
|
public static function init() {
|
|
if ( ! Argus_Settings::get( 'upload_scan_enabled', true ) ) {
|
|
return;
|
|
}
|
|
add_filter( 'wp_handle_upload_prefilter', array( __CLASS__, 'inspect' ) );
|
|
}
|
|
|
|
public static function inspect( $file ) {
|
|
if ( ! empty( $file['error'] ) ) {
|
|
return $file;
|
|
}
|
|
|
|
$name = (string) ( $file['name'] ?? '' );
|
|
|
|
if ( preg_match( self::EXECUTABLE_PATTERN, $name ) ) {
|
|
$file['error'] = __( 'For your website\'s security, ARGUS Defence blocked this upload: executable file types are not allowed.', 'argus-wordpress-defence' );
|
|
self::record_block( $name, 'executable-extension', 'critical' );
|
|
return $file;
|
|
}
|
|
|
|
$tmp = (string) ( $file['tmp_name'] ?? '' );
|
|
if ( '' === $tmp || ! is_readable( $tmp ) ) {
|
|
return $file;
|
|
}
|
|
|
|
$size = filesize( $tmp );
|
|
if ( false === $size || $size <= 0 || $size > self::MAX_FILE_SIZE_TO_SCAN ) {
|
|
return $file;
|
|
}
|
|
|
|
$content = self::read_head_and_tail( $tmp, $size );
|
|
list( $score, $matched ) = Argus_Malware_Scanner::score_content( $content );
|
|
|
|
if ( $score >= Argus_Malware_Scanner::THRESHOLD_HIGH ) {
|
|
$file['error'] = __( 'For your website\'s security, ARGUS Defence blocked this upload: its content matched known malicious patterns.', 'argus-wordpress-defence' );
|
|
self::record_block( $name, 'content-heuristic', 'critical', $score, $matched );
|
|
}
|
|
|
|
return $file;
|
|
}
|
|
|
|
protected static function read_head_and_tail( $path, $size ) {
|
|
$fh = fopen( $path, 'rb' ); // phpcs:ignore WordPress.WP.AlternativeFunctions
|
|
if ( ! $fh ) {
|
|
return '';
|
|
}
|
|
|
|
$head = fread( $fh, min( self::SCAN_CHUNK_BYTES, $size ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions
|
|
$tail = '';
|
|
if ( $size > self::SCAN_CHUNK_BYTES * 2 ) {
|
|
fseek( $fh, -self::SCAN_CHUNK_BYTES, SEEK_END );
|
|
$tail = fread( $fh, self::SCAN_CHUNK_BYTES ); // phpcs:ignore WordPress.WP.AlternativeFunctions
|
|
} elseif ( $size > self::SCAN_CHUNK_BYTES ) {
|
|
$tail = fread( $fh, $size - self::SCAN_CHUNK_BYTES ); // phpcs:ignore WordPress.WP.AlternativeFunctions
|
|
}
|
|
|
|
fclose( $fh ); // phpcs:ignore WordPress.WP.AlternativeFunctions
|
|
return $head . $tail;
|
|
}
|
|
|
|
protected static function record_block( $filename, $rule, $severity, $score = null, array $matched = array() ) {
|
|
$user = wp_get_current_user();
|
|
$who = $user && $user->exists() ? $user->user_login : 'unknown';
|
|
|
|
Argus_Findings::record(
|
|
'malware',
|
|
$severity,
|
|
array(
|
|
'what_happened' => sprintf( 'Blocked a malicious file upload before it reached your website: %s', $filename ),
|
|
'why_it_matters' => 'A file matching this pattern could execute code on your server if it had been allowed through.',
|
|
'what_argus_found' => 'executable-extension' === $rule
|
|
? sprintf( 'The filename "%s" has a PHP-executable extension, which is never allowed as a media upload.', $filename )
|
|
: sprintf( 'The file content scored %d on ARGUS\'s heuristic scanner (matched: %s), consistent with a webshell or obfuscated payload.', (int) $score, implode( ', ', wp_list_pluck( $matched, 'id' ) ) ),
|
|
'when_it_happened' => current_time( 'mysql' ),
|
|
'why_suspicious' => 'Legitimate media uploads (images, documents, video) never contain executable code or PHP-executable extensions.',
|
|
'what_could_be_affected' => 'If this upload had succeeded, it could have given an attacker code execution on your server.',
|
|
'what_should_you_do' => sprintf( 'No action needed -- ARGUS already blocked this upload before it reached your website. Uploaded by: %s.', $who ),
|
|
),
|
|
array( 'filename' => $filename, 'rule' => $rule, 'score' => $score, 'matched_rules' => $matched, 'uploaded_by' => $who )
|
|
);
|
|
|
|
Argus_Events::record(
|
|
'upload_blocked',
|
|
$severity,
|
|
sprintf( 'Blocked upload "%s" (%s)', $filename, $rule ),
|
|
array( 'filename' => $filename, 'rule' => $rule, 'uploaded_by' => $who )
|
|
);
|
|
}
|
|
}
|