security.lib.php 17 KB

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