PageRenderTime 45ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/antivirus/clamav/classes/scanner.php

http://github.com/moodle/moodle
PHP | 427 lines | 224 code | 33 blank | 170 comment | 42 complexity | e43baee8053f30f20ac9d1095f78dd89 MD5 | raw file
Possible License(s): MIT, AGPL-3.0, MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, Apache-2.0, LGPL-2.1, BSD-3-Clause
  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * ClamAV antivirus integration.
  18. *
  19. * @package antivirus_clamav
  20. * @copyright 2015 Ruslan Kabalin, Lancaster University.
  21. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22. */
  23. namespace antivirus_clamav;
  24. defined('MOODLE_INTERNAL') || die();
  25. /** Default socket timeout */
  26. define('ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT', 10);
  27. /** Default socket data stream chunk size */
  28. define('ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE', 1024);
  29. /**
  30. * Class implementing ClamAV antivirus.
  31. * @copyright 2015 Ruslan Kabalin, Lancaster University.
  32. * @copyright 2019 Didier Raboud, Liip AG.
  33. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  34. */
  35. class scanner extends \core\antivirus\scanner {
  36. /**
  37. * Are the necessary antivirus settings configured?
  38. *
  39. * @return bool True if all necessary config settings been entered
  40. */
  41. public function is_configured() {
  42. if ($this->get_config('runningmethod') === 'commandline') {
  43. return (bool)$this->get_config('pathtoclam');
  44. } else if ($this->get_config('runningmethod') === 'unixsocket') {
  45. return (bool)$this->get_config('pathtounixsocket');
  46. } else if ($this->get_config('runningmethod') === 'tcpsocket') {
  47. return (bool)$this->get_config('tcpsockethost') && (bool)$this->get_config('tcpsocketport');
  48. }
  49. return false;
  50. }
  51. /**
  52. * Scan file.
  53. *
  54. * This method is normally called from antivirus manager (\core\antivirus\manager::scan_file).
  55. *
  56. * @param string $file Full path to the file.
  57. * @param string $filename Name of the file (could be different from physical file if temp file is used).
  58. * @return int Scanning result constant.
  59. */
  60. public function scan_file($file, $filename) {
  61. if (!is_readable($file)) {
  62. // This should not happen.
  63. debugging('File is not readable.');
  64. return self::SCAN_RESULT_ERROR;
  65. }
  66. // We can do direct stream scanning if unixsocket or tcpsocket running methods are in use,
  67. // if not, use default process.
  68. $maxtries = get_config('antivirus_clamav', 'tries');
  69. $tries = 0;
  70. do {
  71. $runningmethod = $this->get_config('runningmethod');
  72. $tries++;
  73. switch ($runningmethod) {
  74. case 'unixsocket':
  75. case 'tcpsocket':
  76. $return = $this->scan_file_execute_socket($file, $runningmethod);
  77. break;
  78. case 'commandline':
  79. $return = $this->scan_file_execute_commandline($file);
  80. break;
  81. default:
  82. // This should not happen.
  83. throw new \coding_exception('Unknown running method.');
  84. }
  85. } while ($return == self::SCAN_RESULT_ERROR && $tries < $maxtries);
  86. $notice = get_string('tries_notice', 'antivirus_clamav',
  87. ['tries' => $tries, 'notice' => $this->get_scanning_notice()]);
  88. $this->set_scanning_notice($notice);
  89. if ($return === self::SCAN_RESULT_ERROR) {
  90. $this->message_admins($this->get_scanning_notice());
  91. // If plugin settings require us to act like virus on any error,
  92. // return SCAN_RESULT_FOUND result.
  93. if ($this->get_config('clamfailureonupload') === 'actlikevirus') {
  94. return self::SCAN_RESULT_FOUND;
  95. } else if ($this->get_config('clamfailureonupload') === 'tryagain') {
  96. // Do not upload the file, just give a message to the user to try again later.
  97. unlink($file);
  98. throw new \core\antivirus\scanner_exception('antivirusfailed', '', ['item' => $filename],
  99. null, 'antivirus_clamav');
  100. }
  101. }
  102. return $return;
  103. }
  104. /**
  105. * Scan data.
  106. *
  107. * @param string $data The variable containing the data to scan.
  108. * @return int Scanning result constant.
  109. */
  110. public function scan_data($data) {
  111. // We can do direct stream scanning if unixsocket or tcpsocket running methods are in use,
  112. // if not, use default process.
  113. $runningmethod = $this->get_config('runningmethod');
  114. if (in_array($runningmethod, array('unixsocket', 'tcpsocket'))) {
  115. $return = $this->scan_data_execute_socket($data, $runningmethod);
  116. if ($return === self::SCAN_RESULT_ERROR) {
  117. $this->message_admins($this->get_scanning_notice());
  118. // If plugin settings require us to act like virus on any error,
  119. // return SCAN_RESULT_FOUND result.
  120. if ($this->get_config('clamfailureonupload') === 'actlikevirus') {
  121. return self::SCAN_RESULT_FOUND;
  122. }
  123. }
  124. return $return;
  125. } else {
  126. return parent::scan_data($data);
  127. }
  128. }
  129. /**
  130. * Returns a Unix domain socket destination url
  131. *
  132. * @return string The socket url, fit for stream_socket_client()
  133. */
  134. private function get_unixsocket_destination() {
  135. return 'unix://' . $this->get_config('pathtounixsocket');
  136. }
  137. /**
  138. * Returns a Internet domain socket destination url
  139. *
  140. * @return string The socket url, fit for stream_socket_client()
  141. */
  142. private function get_tcpsocket_destination() {
  143. return 'tcp://' . $this->get_config('tcpsockethost') . ':' . $this->get_config('tcpsocketport');
  144. }
  145. /**
  146. * Returns the string equivalent of a numeric clam error code
  147. *
  148. * @param int $returncode The numeric error code in question.
  149. * @return string The definition of the error code
  150. */
  151. private function get_clam_error_code($returncode) {
  152. $returncodes = array();
  153. $returncodes[0] = 'No virus found.';
  154. $returncodes[1] = 'Virus(es) found.';
  155. $returncodes[2] = ' An error occured'; // Specific to clamdscan.
  156. // All after here are specific to clamscan.
  157. $returncodes[40] = 'Unknown option passed.';
  158. $returncodes[50] = 'Database initialization error.';
  159. $returncodes[52] = 'Not supported file type.';
  160. $returncodes[53] = 'Can\'t open directory.';
  161. $returncodes[54] = 'Can\'t open file. (ofm)';
  162. $returncodes[55] = 'Error reading file. (ofm)';
  163. $returncodes[56] = 'Can\'t stat input file / directory.';
  164. $returncodes[57] = 'Can\'t get absolute path name of current working directory.';
  165. $returncodes[58] = 'I/O error, please check your filesystem.';
  166. $returncodes[59] = 'Can\'t get information about current user from /etc/passwd.';
  167. $returncodes[60] = 'Can\'t get information about user \'clamav\' (default name) from /etc/passwd.';
  168. $returncodes[61] = 'Can\'t fork.';
  169. $returncodes[63] = 'Can\'t create temporary files/directories (check permissions).';
  170. $returncodes[64] = 'Can\'t write to temporary directory (please specify another one).';
  171. $returncodes[70] = 'Can\'t allocate and clear memory (calloc).';
  172. $returncodes[71] = 'Can\'t allocate memory (malloc).';
  173. if (isset($returncodes[$returncode])) {
  174. return $returncodes[$returncode];
  175. }
  176. return get_string('unknownerror', 'antivirus_clamav');
  177. }
  178. /**
  179. * Scan file using command line utility.
  180. *
  181. * @param string $file Full path to the file.
  182. * @return int Scanning result constant.
  183. */
  184. public function scan_file_execute_commandline($file) {
  185. $pathtoclam = trim($this->get_config('pathtoclam'));
  186. if (!file_exists($pathtoclam) or !is_executable($pathtoclam)) {
  187. // Misconfigured clam, notify admins.
  188. $notice = get_string('invalidpathtoclam', 'antivirus_clamav', $pathtoclam);
  189. $this->set_scanning_notice($notice);
  190. return self::SCAN_RESULT_ERROR;
  191. }
  192. $clamparam = ' --stdout ';
  193. // If we are dealing with clamdscan, clamd is likely run as a different user
  194. // that might not have permissions to access your file.
  195. // To make clamdscan work, we use --fdpass parameter that passes the file
  196. // descriptor permissions to clamd, which allows it to scan given file
  197. // irrespective of directory and file permissions.
  198. if (basename($pathtoclam) == 'clamdscan') {
  199. $clamparam .= '--fdpass ';
  200. }
  201. // Execute scan.
  202. $cmd = escapeshellcmd($pathtoclam).$clamparam.escapeshellarg($file);
  203. exec($cmd, $output, $return);
  204. // Return variable will contain execution return code. It will be 0 if no virus is found,
  205. // 1 if virus is found, and 2 or above for the error. Return codes 0 and 1 correspond to
  206. // SCAN_RESULT_OK and SCAN_RESULT_FOUND constants, so we return them as it is.
  207. // If there is an error, it gets stored as scanning notice and function
  208. // returns SCAN_RESULT_ERROR.
  209. if ($return > self::SCAN_RESULT_FOUND) {
  210. $notice = get_string('clamfailed', 'antivirus_clamav', $this->get_clam_error_code($return));
  211. $notice .= "\n\n". implode("\n", $output);
  212. $this->set_scanning_notice($notice);
  213. return self::SCAN_RESULT_ERROR;
  214. }
  215. return (int)$return;
  216. }
  217. /**
  218. * Scan file using sockets.
  219. *
  220. * @param string $file Full path to the file.
  221. * @param string $type Either 'tcpsocket' or 'unixsocket'
  222. * @return int Scanning result constant.
  223. */
  224. public function scan_file_execute_socket($file, $type) {
  225. switch ($type) {
  226. case "tcpsocket":
  227. $socketurl = $this->get_tcpsocket_destination();
  228. break;
  229. case "unixsocket":
  230. $socketurl = $this->get_unixsocket_destination();
  231. break;
  232. default;
  233. // This should not happen.
  234. debugging('Unknown socket type.');
  235. return self::SCAN_RESULT_ERROR;
  236. }
  237. $socket = stream_socket_client($socketurl,
  238. $errno, $errstr, ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT);
  239. if (!$socket) {
  240. // Can't open socket for some reason, notify admins.
  241. $notice = get_string('errorcantopensocket', 'antivirus_clamav', "$errstr ($errno)");
  242. $this->set_scanning_notice($notice);
  243. return self::SCAN_RESULT_ERROR;
  244. } else {
  245. if ($type == "unixsocket") {
  246. // Execute scanning. We are running SCAN command and passing file as an argument,
  247. // it is the fastest option, but clamav user need to be able to access it, so
  248. // we give group read permissions first and assume 'clamav' user is in web server
  249. // group (in Debian the default webserver group is 'www-data').
  250. // Using 'n' as command prefix is forcing clamav to only treat \n as newline delimeter,
  251. // this is to avoid unexpected newline characters on different systems.
  252. $perms = fileperms($file);
  253. chmod($file, 0640);
  254. // Actual scan.
  255. fwrite($socket, "nSCAN ".$file."\n");
  256. // Get ClamAV answer.
  257. $output = stream_get_line($socket, 4096);
  258. // After scanning we revert permissions to initial ones.
  259. chmod($file, $perms);
  260. } else if ($type == "tcpsocket") {
  261. // Execute scanning by passing the entire file through the TCP socket.
  262. // This is not fast, but is the only possibility over a network.
  263. // Using 'n' as command prefix is forcing clamav to only treat \n as newline delimeter,
  264. // this is to avoid unexpected newline characters on different systems.
  265. // Actual scan.
  266. fwrite($socket, "nINSTREAM\n");
  267. // Open the file for reading.
  268. $fhandle = fopen($file, 'rb');
  269. while (!feof($fhandle)) {
  270. // Read it by chunks; write them to the TCP socket.
  271. $chunk = fread($fhandle, ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE);
  272. $size = pack('N', strlen($chunk));
  273. fwrite($socket, $size);
  274. fwrite($socket, $chunk);
  275. }
  276. // Terminate streaming.
  277. fwrite($socket, pack('N', 0));
  278. // Get ClamAV answer.
  279. $output = stream_get_line($socket, 4096);
  280. fclose($fhandle);
  281. }
  282. // Free up the ClamAV socket.
  283. fclose($socket);
  284. // Parse the output.
  285. return $this->parse_socket_response($output);
  286. }
  287. }
  288. /**
  289. * Scan data socket.
  290. *
  291. * We are running INSTREAM command and passing data stream in chunks.
  292. * The format of the chunk is: <length><data> where <length> is the size of the following
  293. * data in bytes expressed as a 4 byte unsigned integer in network byte order and <data>
  294. * is the actual chunk. Streaming is terminated by sending a zero-length chunk.
  295. * Do not exceed StreamMaxLength as defined in clamd.conf, otherwise clamd will
  296. * reply with INSTREAM size limit exceeded and close the connection.
  297. *
  298. * @param string $data The variable containing the data to scan.
  299. * @param string $type Either 'tcpsocket' or 'unixsocket'
  300. * @return int Scanning result constant.
  301. */
  302. public function scan_data_execute_socket($data, $type) {
  303. switch ($type) {
  304. case "tcpsocket":
  305. $socketurl = $this->get_tcpsocket_destination();
  306. break;
  307. case "unixsocket":
  308. $socketurl = $this->get_unixsocket_destination();
  309. break;
  310. default;
  311. // This should not happen.
  312. debugging('Unknown socket type!');
  313. return self::SCAN_RESULT_ERROR;
  314. }
  315. $socket = stream_socket_client($socketurl, $errno, $errstr, ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT);
  316. if (!$socket) {
  317. // Can't open socket for some reason, notify admins.
  318. $notice = get_string('errorcantopensocket', 'antivirus_clamav', "$errstr ($errno)");
  319. $this->set_scanning_notice($notice);
  320. return self::SCAN_RESULT_ERROR;
  321. } else {
  322. // Initiate data stream scanning.
  323. // Using 'n' as command prefix is forcing clamav to only treat \n as newline delimeter,
  324. // this is to avoid unexpected newline characters on different systems.
  325. fwrite($socket, "nINSTREAM\n");
  326. // Send data in chunks of ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE size.
  327. while (strlen($data) > 0) {
  328. $chunk = substr($data, 0, ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE);
  329. $data = substr($data, ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE);
  330. $size = pack('N', strlen($chunk));
  331. fwrite($socket, $size);
  332. fwrite($socket, $chunk);
  333. }
  334. // Terminate streaming.
  335. fwrite($socket, pack('N', 0));
  336. $output = stream_get_line($socket, 4096);
  337. fclose($socket);
  338. // Parse the output.
  339. return $this->parse_socket_response($output);
  340. }
  341. }
  342. /**
  343. * Parse socket command response.
  344. *
  345. * @param string $output The socket response.
  346. * @return int Scanning result constant.
  347. */
  348. private function parse_socket_response($output) {
  349. $splitoutput = explode(': ', $output);
  350. $message = trim($splitoutput[1]);
  351. if ($message === 'OK') {
  352. return self::SCAN_RESULT_OK;
  353. } else {
  354. $parts = explode(' ', $message);
  355. $status = array_pop($parts);
  356. if ($status === 'FOUND') {
  357. return self::SCAN_RESULT_FOUND;
  358. } else {
  359. $notice = get_string('clamfailed', 'antivirus_clamav', $this->get_clam_error_code(2));
  360. $notice .= "\n\n" . $output;
  361. $this->set_scanning_notice($notice);
  362. return self::SCAN_RESULT_ERROR;
  363. }
  364. }
  365. }
  366. /**
  367. * Scan data using Unix domain socket.
  368. *
  369. * @deprecated since Moodle 3.9 MDL-64075 - please do not use this function any more.
  370. * @see antivirus_clamav\scanner::scan_data_execute_socket()
  371. *
  372. * @param string $data The variable containing the data to scan.
  373. * @return int Scanning result constant.
  374. */
  375. public function scan_data_execute_unixsocket($data) {
  376. debugging('antivirus_clamav\scanner::scan_data_execute_unixsocket() is deprecated. ' .
  377. 'Use antivirus_clamav\scanner::scan_data_execute_socket() instead.', DEBUG_DEVELOPER);
  378. return $this->scan_data_execute_socket($data, "unixsocket");
  379. }
  380. /**
  381. * Scan file using Unix domain socket.
  382. *
  383. * @deprecated since Moodle 3.9 MDL-64075 - please do not use this function any more.
  384. * @see antivirus_clamav\scanner::scan_file_execute_socket()
  385. *
  386. * @param string $file Full path to the file.
  387. * @return int Scanning result constant.
  388. */
  389. public function scan_file_execute_unixsocket($file) {
  390. debugging('antivirus_clamav\scanner::scan_file_execute_unixsocket() is deprecated. ' .
  391. 'Use antivirus_clamav\scanner::scan_file_execute_socket() instead.', DEBUG_DEVELOPER);
  392. return $this->scan_file_execute_socket($file, "unixsocket");
  393. }
  394. }