security.lib.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. <?php
  2. /* For licensing terms, see /license.txt */
  3. /**
  4. * This is the security library for Chamilo.
  5. *
  6. * This library is based on recommendations found in the PHP5 Certification
  7. * Guide published at PHP|Architect, and other recommendations found on
  8. * http://www.phpsec.org/
  9. * The principles here are that all data is tainted (most scripts of Chamilo are
  10. * open to the public or at least to a certain public that could be malicious
  11. * under specific circumstances). We use the white list approach, where as we
  12. * consider that data can only be used in the database or in a file if it has
  13. * been filtered.
  14. *
  15. * For session fixation, use ...
  16. * For session hijacking, use get_ua() and check_ua()
  17. * For Cross-Site Request Forgeries, use get_token() and check_tocken()
  18. * For basic filtering, use filter()
  19. * For files inclusions (using dynamic paths) use check_rel_path() and check_abs_path()
  20. *
  21. * @package chamilo.library
  22. * @author Yannick Warnier <ywarnier@beeznest.org>
  23. */
  24. /**
  25. * Security class
  26. *
  27. * Include/require it in your code and call Security::function()
  28. * to use its functionalities.
  29. *
  30. * This class can also be used as a container for filtered data, by creating
  31. * a new Security object and using $secure->filter($new_var,[more options])
  32. * and then using $secure->clean['var'] as a filtered equivalent, although
  33. * this is *not* mandatory at all.
  34. */
  35. class Security
  36. {
  37. public static $clean = array();
  38. /**
  39. * Checks if the absolute path (directory) given is really under the
  40. * checker path (directory)
  41. * @param string Absolute path to be checked (with trailing slash)
  42. * @param string Checker path under which the path should be (absolute path, with trailing slash, get it from api_get_path(SYS_COURSE_PATH))
  43. * @return bool True if the path is under the checker, false otherwise
  44. */
  45. public static function check_abs_path($abs_path, $checker_path)
  46. {
  47. global $_configuration;
  48. if (empty($checker_path)) {
  49. return false;
  50. } // The checker path must be set.
  51. $true_path = str_replace("\\", '/', realpath($abs_path));
  52. $found = strpos($true_path.'/', $checker_path);
  53. if ($found === 0) {
  54. return true;
  55. } else {
  56. // Code specific to Windows and case-insensitive behaviour
  57. if (api_is_windows_os()) {
  58. $found = stripos($true_path.'/', $checker_path);
  59. if ($found === 0) {
  60. return true;
  61. }
  62. }
  63. // Code specific to courses directory stored on other disk.
  64. /*
  65. $checker_path = str_replace(api_get_path(SYS_COURSE_PATH), $_configuration['symbolic_course_folder_abs'], $checker_path);
  66. $found = strpos($true_path.'/', $checker_path);
  67. if ($found === 0) {
  68. return true;
  69. }*/
  70. }
  71. return false;
  72. }
  73. /**
  74. * Checks if the relative path (directory) given is really under the
  75. * checker path (directory)
  76. * @param string $rel_path Relative path to be checked (relative to the current directory) (with trailing slash)
  77. * @param string $checker_path Checker path under which the path should be (absolute path, with trailing slash, get it from api_get_path(SYS_COURSE_PATH))
  78. *
  79. * @return bool True if the path is under the checker, false otherwise
  80. */
  81. public static function check_rel_path($rel_path, $checker_path)
  82. {
  83. if (empty($checker_path)) {
  84. return false;
  85. } // The checker path must be set.
  86. $current_path = getcwd(); // No trailing slash.
  87. if (substr($rel_path, -1, 1) != '/') {
  88. $rel_path = '/'.$rel_path;
  89. }
  90. $abs_path = $current_path.$rel_path;
  91. $true_path = str_replace("\\", '/', realpath($abs_path));
  92. $found = strpos($true_path.'/', $checker_path);
  93. if ($found === 0) {
  94. return true;
  95. }
  96. return false;
  97. }
  98. /**
  99. * Filters dangerous filenames (*.php[.]?* and .htaccess) and returns it in
  100. * a non-executable form (for PHP and htaccess, this is still vulnerable to
  101. * other languages' files extensions)
  102. * @param string Unfiltered filename
  103. * @param string Filtered filename
  104. */
  105. public static function filter_filename($filename)
  106. {
  107. return FileManager::disable_dangerous_file($filename);
  108. }
  109. /**
  110. * This function checks that the token generated in get_token() has been kept (prevents
  111. * Cross-Site Request Forgeries attacks)
  112. * @param string $request_type The array in which to get the token ('get' or 'post')
  113. *
  114. * @return bool True if it's the right token, false otherwise
  115. *
  116. */
  117. public static function check_token($request_type = 'post')
  118. {
  119. $currentSessionToken = Security::getCurrentToken();
  120. switch ($request_type) {
  121. case 'request':
  122. if (isset($currentSessionToken) && isset($_REQUEST['sec_token']) && $currentSessionToken === $_REQUEST['sec_token']) {
  123. return true;
  124. }
  125. return false;
  126. case 'get':
  127. if (isset($currentSessionToken) && isset($_GET['sec_token']) && $currentSessionToken === $_GET['sec_token']) {
  128. return true;
  129. }
  130. return false;
  131. case 'post':
  132. if (isset($currentSessionToken) && isset($_POST['sec_token']) && $currentSessionToken === $_POST['sec_token']) {
  133. return true;
  134. }
  135. return false;
  136. default:
  137. if (isset($currentSessionToken) && isset($request_type) && $currentSessionToken === $request_type) {
  138. return true;
  139. }
  140. return false;
  141. }
  142. return false; // Just in case, don't let anything slip.
  143. }
  144. /**
  145. * Checks the user agent of the client as recorder by get_ua() to prevent
  146. * most session hijacking attacks.
  147. * @return bool True if the user agent is the same, false otherwise
  148. */
  149. public static function check_ua()
  150. {
  151. if (isset($_SESSION['sec_ua']) and $_SESSION['sec_ua'] === $_SERVER['HTTP_USER_AGENT'].$_SESSION['sec_ua_seed']) {
  152. return true;
  153. }
  154. return false;
  155. }
  156. /**
  157. * Clear the security token from the session
  158. * @return void
  159. */
  160. public static function clear_token()
  161. {
  162. $_SESSION['sec_token'] = null;
  163. unset($_SESSION['sec_token']);
  164. }
  165. /**
  166. * This function sets a random token to be included in a form as a hidden field
  167. * and saves it into the user's session. Returns an HTML form element
  168. * This later prevents Cross-Site Request Forgeries by checking that the user is really
  169. * the one that sent this form in knowingly (this form hasn't been generated from
  170. * another website visited by the user at the same time).
  171. * Check the token with check_token()
  172. * @return string Hidden-type input ready to insert into a form
  173. */
  174. public static function get_HTML_token()
  175. {
  176. $token = md5(uniqid(rand(), true));
  177. $string = '<input type="hidden" name="sec_token" value="'.$token.'" />';
  178. $_SESSION['sec_token'] = $token;
  179. return $string;
  180. }
  181. /**
  182. * This function sets a random token to be included in a form as a hidden field
  183. * and saves it into the user's session.
  184. * This later prevents Cross-Site Request Forgeries by checking that the user is really
  185. * the one that sent this form in knowingly (this form hasn't been generated from
  186. * another website visited by the user at the same time).
  187. * Check the token with check_token()
  188. * @return string Token
  189. */
  190. public static function get_token()
  191. {
  192. $token = md5(uniqid(rand(), true));
  193. $_SESSION['sec_token'] = $token;
  194. return $token;
  195. }
  196. public static function getCurrentToken()
  197. {
  198. return isset($_SESSION['sec_token']) ? $_SESSION['sec_token'] : null;
  199. }
  200. /**
  201. * Gets the user agent in the session to later check it with check_ua() to prevent
  202. * most cases of session hijacking.
  203. * @return void
  204. */
  205. public static function get_ua()
  206. {
  207. $_SESSION['sec_ua_seed'] = uniqid(rand(), true);
  208. $_SESSION['sec_ua'] = $_SERVER['HTTP_USER_AGENT'].$_SESSION['sec_ua_seed'];
  209. }
  210. /**
  211. * This function returns a variable from the clean array. If the variable doesn't exist,
  212. * it returns null
  213. * @param string Variable name
  214. * @return mixed Variable or NULL on error
  215. */
  216. public static function get($varname)
  217. {
  218. if (isset(self::$clean[$varname])) {
  219. return self::$clean[$varname];
  220. }
  221. return null;
  222. }
  223. /**
  224. * This function tackles the XSS injections.
  225. * Filtering for XSS is very easily done by using the htmlentities() function.
  226. * This kind of filtering prevents JavaScript snippets to be understood as such.
  227. * @param mixed The variable to filter for XSS, this params can be a string or an array (example : array(x,y))
  228. * @param integer The user status,constant allowed (STUDENT, COURSEMANAGER, ANONYMOUS, COURSEMANAGERLOWSECURITY)
  229. * @return mixed Filtered string or array
  230. */
  231. public static function remove_XSS($var, $user_status = ANONYMOUS, $filter_terms = false)
  232. {
  233. // @todo improvement - HTMLpurifier eats server memory ~ 3M
  234. // return $var;
  235. if ($filter_terms) {
  236. $var = self::filter_terms($var);
  237. }
  238. if ($user_status == COURSEMANAGERLOWSECURITY) {
  239. return $var; // No filtering.
  240. }
  241. static $purifier = array();
  242. if (!isset($purifier[$user_status])) {
  243. global $app;
  244. $cache_dir = $app['htmlpurifier.serializer'];
  245. $config = HTMLPurifier_Config::createDefault();
  246. //$config->set('Cache.DefinitionImpl', null); // Enable this line for testing purposes, for turning off caching. Don't forget to disable this line later!
  247. $config->set('Cache.SerializerPath', $cache_dir);
  248. $config->set('Core.Encoding', api_get_system_encoding());
  249. $config->set('HTML.Doctype', 'XHTML 1.0 Transitional');
  250. $config->set('HTML.MaxImgLength', '2560');
  251. $config->set('HTML.TidyLevel', 'light');
  252. $config->set('Core.ConvertDocumentToFragment', false);
  253. $config->set('Core.RemoveProcessingInstructions', true);
  254. if (api_get_setting('enable_iframe_inclusion') == 'true') {
  255. $config->set('Filter.Custom', array(new HTMLPurifier_Filter_AllowIframes()));
  256. }
  257. //Shows _target attribute in anchors
  258. $config->set('Attr.AllowedFrameTargets', array('_blank', '_top', '_self', '_parent'));
  259. if ($user_status == STUDENT) {
  260. global $allowed_html_student;
  261. $config->set('HTML.SafeEmbed', true);
  262. $config->set('HTML.SafeObject', true);
  263. $config->set('Filter.YouTube', true);
  264. $config->set('HTML.FlashAllowFullScreen', true);
  265. $config->set('HTML.Allowed', $allowed_html_student);
  266. } elseif ($user_status == COURSEMANAGER) {
  267. global $allowed_html_teacher;
  268. $config->set('HTML.SafeEmbed', true);
  269. $config->set('HTML.SafeObject', true);
  270. $config->set('Filter.YouTube', true);
  271. $config->set('HTML.FlashAllowFullScreen', true);
  272. $config->set('HTML.Allowed', $allowed_html_teacher);
  273. } else {
  274. global $allowed_html_anonymous;
  275. $config->set('HTML.Allowed', $allowed_html_anonymous);
  276. }
  277. $config->set(
  278. 'Attr.EnableID',
  279. true
  280. ); // We need it for example for the flv player (ids of surrounding div-tags have to be preserved).
  281. $config->set('CSS.AllowImportant', true);
  282. $config->set('CSS.AllowTricky', true); // We need for the flv player the css definition display: none;
  283. $config->set('CSS.Proprietary', true);
  284. $purifier[$user_status] = new HTMLPurifier($config);
  285. }
  286. if (is_array($var)) {
  287. return $purifier[$user_status]->purifyArray($var);
  288. } else {
  289. return $purifier[$user_status]->purify($var);
  290. }
  291. }
  292. /**
  293. *
  294. * Filter content
  295. * @param string content to be filter
  296. * @return string
  297. */
  298. static function filter_terms($text)
  299. {
  300. static $bad_terms = array();
  301. if (empty($bad_terms)) {
  302. $list = api_get_setting('filter_terms');
  303. $list = explode("\n", $list);
  304. $list = array_filter($list);
  305. if (!empty($list)) {
  306. foreach ($list as $term) {
  307. $term = str_replace(array("\r\n", "\r", "\n", "\t"), '', $term);
  308. $html_entities_value = api_htmlentities($term, ENT_QUOTES, api_get_system_encoding());
  309. $bad_terms[] = $term;
  310. if ($term != $html_entities_value) {
  311. $bad_terms[] = $html_entities_value;
  312. }
  313. }
  314. $bad_terms = array_filter($bad_terms);
  315. }
  316. }
  317. $replace = '***';
  318. if (!empty($bad_terms)) {
  319. //Fast way
  320. $new_text = str_ireplace($bad_terms, $replace, $text, $count);
  321. //We need statistics
  322. /*
  323. if (strlen($new_text) != strlen($text)) {
  324. $table = Database::get_main_table(TABLE_STATISTIC_TRACK_FILTERED_TERMS);
  325. $attributes = array();
  326. $attributes['user_id'] =
  327. $attributes['course_id'] =
  328. $attributes['session_id'] =
  329. $attributes['tool_id'] =
  330. $attributes['term'] =
  331. $attributes['created_at'] = api_get_utc_datetime();
  332. $sql = Database::insert($table, $attributes);
  333. }
  334. */
  335. $text = $new_text;
  336. }
  337. return $text;
  338. }
  339. /**
  340. * This method provides specific protection (against XSS and other kinds of attacks) for static images (icons) used by the system.
  341. * Image paths are supposed to be given by programmers - people who know what they do, anyway, this method encourages
  342. * a safe practice for generating icon paths, without using heavy solutions based on HTMLPurifier for example.
  343. * @param string $img_path The input path of the image, it could be relative or absolute URL.
  344. * @return string Returns sanitized image path or an empty string when the image path is not secure.
  345. * @author Ivan Tcholakov, March 2011
  346. */
  347. public static function filter_img_path($image_path)
  348. {
  349. static $allowed_extensions = array('png', 'gif', 'jpg', 'jpeg');
  350. $image_path = htmlspecialchars(trim($image_path)); // No html code is allowed.
  351. // We allow static images only, query strings are forbidden.
  352. if (strpos($image_path, '?') !== false) {
  353. return '';
  354. }
  355. if (($pos = strpos($image_path, ':')) !== false) {
  356. // Protocol has been specified, let's check it.
  357. if (stripos($image_path, 'javascript:') !== false) {
  358. // Javascript everywhere in the path is not allowed.
  359. return '';
  360. }
  361. // We allow only http: and https: protocols for now.
  362. //if (!preg_match('/^https?:\/\//i', $image_path)) {
  363. // return '';
  364. //}
  365. if (stripos($image_path, 'http://') !== 0 && stripos($image_path, 'https://') !== 0) {
  366. return '';
  367. }
  368. }
  369. // We allow file extensions for images only.
  370. //if (!preg_match('/.+\.(png|gif|jpg|jpeg)$/i', $image_path)) {
  371. // return '';
  372. //}
  373. if (($pos = strrpos($image_path, '.')) !== false) {
  374. if (!in_array(strtolower(substr($image_path, $pos + 1)), $allowed_extensions)) {
  375. return '';
  376. }
  377. } else {
  378. return '';
  379. }
  380. return $image_path;
  381. }
  382. }