Skip to content
Snippets Groups Projects
Commit 8a2b6853 authored by Thomas Hackl's avatar Thomas Hackl Committed by David Siegfried
Browse files

Resolve "Virencheck beim Dateiupload"

Closes #1658

Merge request studip/studip!1083
parent 7dddea8c
No related branches found
No related tags found
No related merge requests found
<?php
class ViruscheckForUploads extends Migration
{
public function description()
{
return 'Provide config options for ClamAV usage on file uploads';
}
public function up()
{
$query = "INSERT IGNORE INTO `config` (`field`, `value`, `type`, `range`, `section`, `mkdate`, `chdate`,
`description`)
VALUES (:name, :value, :type, :range, :section, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), :description)";
$statement = DBManager::get()->prepare($query);
$statement->execute([
'name' => 'VIRUSSCAN_ON_UPLOAD',
'description' => 'Sollen Dateien beim Upload mit ClamAV auf Viren überprüft werden?',
'type' => 'boolean',
'range' => 'global',
'section' => 'files',
'value' => '0'
]);
$statement->execute([
'name' => 'VIRUSSCAN_SOCKET',
'description' => 'Pfad zum Unix Socket (wird statt TCP verwendet, falls etwas eingetragen ist)',
'type' => 'string',
'range' => 'global',
'section' => 'files',
'value' => '/var/run/clamav/clamd.ctl'
]);
$statement->execute([
'name' => 'VIRUSSCAN_HOST',
'description' => 'Host des Virenscanners (wird nur verwendet, falls kein Socket eingetragen ist)',
'type' => 'string',
'range' => 'global',
'section' => 'files',
'value' => '127.0.0.1'
]);
$statement->execute([
'name' => 'VIRUSSCAN_PORT',
'description' => 'Port des Virenscanners (wird nur verwendet, falls kein Socket eingetragen ist)',
'type' => 'integer',
'range' => 'global',
'section' => 'files',
'value' => 3310
]);
$statement->execute([
'name' => 'VIRUSSCAN_MAX_STREAMLENGTH',
'description' => 'Maximale Streamlänge in Bytes, die beim Virenscanner erlaubt ist',
'type' => 'integer',
'range' => 'global',
'section' => 'files',
'value' => 26214400
]);
}
public function down()
{
$query = "DELETE `config`, `config_values`
FROM `config`
LEFT JOIN `config_values` USING (`field`)
WHERE `field` IN (
'VIRUSSCAN_ON_UPLOAD',
'VIRUSSCAN_SOCKET',
'VIRUSSCAN_HOST',
'VIRUSSCAN_PORT',
'VIRUSSCAN_MAX_STREAMLENGTH'
)";
DBManager::get()->exec($query);
}
}
<?php
/**
* Abstraction for checking files with an external virus scanner.
* Supports connections via TCP or socket and is focused on using ClamAV at the moment.
* Derived from https://github.com/nextcloud/files_antivirus
*
* @author Thomas Hackl <hackl@data-quest.de>
* @author Sebastian Biller <s.biller@tu-braunschweig.de>
* @license GPL 2 or later
* @since 5.3
*/
class Virusscanner
{
// Contains the singleton used.
protected static $instance;
// Definitions for possible status.
public const SCANRESULT_UNCHECKED = -1;
public const SCANRESULT_CLEAN = 0;
public const SCANRESULT_INFECTED = 1;
/**
* Scans the given path for viruses.
*
* @param string $path
* @return array Contains the found virus signature, error message or is an empty array on successful scan
*/
public static function scan(string $path)
{
// Get virus scanner singleton.
if (is_null(static::$instance)) {
static::$instance = new static();
}
$scanner = static::$instance;
try {
// Connect to scanner.
$handle = $scanner->connect();
// Read file.
$file = $scanner->readFile($path);
// ClamAV has a maximum stream length, so we need to track how much data has already been sent.
$bytesWritten = $scanner->sendContent($handle, "nINSTREAM\n");
// Send file chunks via socket or TCP.
while ($chunk = @fread($file, 8192)) {
$chunkLength = pack('N', strlen($chunk));
// Send next chunk.
if ($bytesWritten + strlen($chunk) <= Config::get()->VIRUSSCAN_MAX_STREAMLENGTH) {
$bytesWritten += $scanner->sendContent($handle, $chunkLength . $chunk);
// Stream limit will be reached: abort.
} else {
return [
'error' => _('Die Datei ist zu groß, um vom Virenscanner gelesen zu werden.')
];
}
}
fclose($file);
// All chunks have been sent - signal stream end and get scanner response.
$result = $scanner->finalize($handle);
// Nothing found.
if ($result['status'] == static::SCANRESULT_CLEAN) {
return [];
// Virus found or error.
} else if ($result['status'] == static::SCANRESULT_INFECTED) {
return [
'found' => $result['details']
];
} else {
return [
'error' => $result['details']
];
}
// There has been an error: send error message back.
} catch (Exception $e) {
return [
'error' => $e->getMessage()
];
}
}
/**
* Establishes a connection to virus scanner via socket or TCP, depending on Stud.IP configuration.
*
* @return resource|null
*/
protected function connect()
{
$handle = false;
// Use socket connection.
if (Config::get()->VIRUSSCAN_SOCKET) {
$handle = @stream_socket_client('unix://' . Config::get()->VIRUSSCAN_SOCKET, $errno, $errstr, 5);
// use TCP connection.
} else if (Config::get()->VIRUSSCAN_HOST && Config::get()->VIRUSSCAN_PORT) {
$handle = @fsockopen(Config::get()->VIRUSSCAN_HOST, Config::get()->VIRUSSCAN_PORT);
}
if ($handle === false) {
throw new RuntimeException(_('Der Virenscanner ist nicht verfügbar.'));
}
return $handle;
}
/**
* Get contents of the file to scan.
*
* @param string $path
* @return resource
*/
protected function readFile(string $path)
{
$handle = fopen($path, 'r');
if ($handle === false) {
throw new RuntimeException(_('Die Datei kann nicht gelesen werden.'));
}
return $handle;
}
/**
* Send some content to the virus scanner.
*
* @param resource $handle
* @param string $content
* @return int
*/
protected function sendContent($handle, string $content): int
{
$written = @fwrite($handle, $content);
// An error has happened -> throw exception.
if ($written === false) {
throw new RuntimeException(_('Fehler bei der Kommunikation mit dem Virenscanner.'));
// Return written byte count.
} else {
return $written;
}
}
/**
* All file chunks have been sent: we now signal the end of the stream by sending a "0".
* Afterwarda, the response we got from virus scanner is parsed and (in case something was found)
* the name of the virus is returned.
*
* @param resource $handle
* @return array
*/
protected function finalize($handle): array
{
// End stream to socket or TCP endpoint.
$this->sendContent($handle, pack('N', 0));
// Fetch virus scanner response.
$response = fgets($handle);
fclose($handle);
// Parse response.
$matches = [];
// Possible response types.
$rules = [
[
'match' => '/.*: OK$/',
'status' => self::SCANRESULT_CLEAN
],
[
'match' => '/.*: (.*) FOUND$/',
'status' => self::SCANRESULT_INFECTED
],
[
'match' => '/.*: (.*) ERROR$/',
'status' => self::SCANRESULT_UNCHECKED
],
];
$status = static::SCANRESULT_UNCHECKED;
$details = _('Die Antwort des Virenscanners wurde nicht erkannt.');
foreach ($rules as $rule) {
if (preg_match($rule['match'], $response, $matches)) {
$status = (int) $rule['status'];
if ((int) $rule['status'] !== static::SCANRESULT_CLEAN) {
$details = $matches[1] ?? _('unbekannt');
} else {
$details = '';
}
break;
}
}
return [
'status' => $status,
'details' => $details
];
}
}
......@@ -328,35 +328,63 @@ class FileManager
//two-dimensional array. Each index of the first dimension
//contains an array attribute for uploaded files, one entry per file.
if (is_array($uploaded_files['name'])) {
$error = [];
foreach ($uploaded_files['name'] as $key => $filename) {
$uploaded_file = StandardFile::create([
'name' => $filename,
'type' => $uploaded_files['type'][$key] ?: get_mime_type($filename),
'size' => $uploaded_files['size'][$key],
'tmp_name' => $uploaded_files['tmp_name'][$key],
'error' => $uploaded_files['error'][$key]
]);
if ($uploaded_file instanceof FileType) {
//validate the upload by looking at the folder where the
//uploaded file shall be stored:
if ($folder_error = $folder->validateUpload($uploaded_file, $user_id)) {
$error[] = $folder_error;
$uploaded_file->delete();
continue;
$proceed = true;
if (Config::get()->VIRUSSCAN_ON_UPLOAD) {
$scanned = Virusscanner::scan($uploaded_files['tmp_name'][$key]);
if (count($scanned) > 0) {
$proceed = false;
if ($scanned['found']) {
$error[] = sprintf(
_('Die Datei "%1$s" wurde vom Virenscanner überprüft. Dabei wurde das Virus ' .
'"%2$s" gefunden. Die Datei wird nicht hochgeladen.'),
$filename, $scanned['found']
);
} else if ($scanned['error']) {
$error[] = sprintf(
_('Die Datei "%1$s" wurde vom Virenscanner überprüft. Dabei ist ein Problem ' .
'aufgetreten: %2$s'),
$filename, $scanned['error']
);
}
}
}
if ($proceed) {
$uploaded_file = StandardFile::create([
'name' => $filename,
'type' => $uploaded_files['type'][$key] ?: get_mime_type($filename),
'size' => $uploaded_files['size'][$key],
'tmp_name' => $uploaded_files['tmp_name'][$key],
'error' => $uploaded_files['error'][$key]
]);
if ($uploaded_file instanceof FileType) {
//validate the upload by looking at the folder where the
//uploaded file shall be stored:
if ($folder_error = $folder->validateUpload($uploaded_file, $user_id)) {
$error[] = $folder_error;
$uploaded_file->delete();
continue;
}
$new_reference = $folder->addFile($uploaded_file, $user_id);
if (!$new_reference){
$error[] = _('Ein Systemfehler ist beim Upload aufgetreten.');
$new_reference = $folder->addFile($uploaded_file, $user_id);
if (!$new_reference) {
$error[] = _('Ein Systemfehler ist beim Upload aufgetreten.');
} else {
$result['files'][] = $new_reference;
}
} else {
$result['files'][] = $new_reference;
$error = array_merge($error, $uploaded_file);
}
} else {
$error = array_merge($error, $uploaded_file);
}
}
}
return array_merge($result, compact('error'));
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment