Files
argus-wp-defence/includes/class-argus-upload-guard.php
T
root df0f2fccb8 ARGUS WordPress Defence 1.0.0 — first production release
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.
2026-08-09 13:40:16 +00:00

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 )
);
}
}