security.lib.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  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) {
  244. if ($user_status == COURSEMANAGERLOWSECURITY) {
  245. return $var; // No filtering.
  246. }
  247. static $purifier = array();
  248. if (!isset($purifier[$user_status])) {
  249. if (!class_exists('HTMLPurifier')) {
  250. // Lazy loading.
  251. require api_get_path(LIBRARY_PATH).'htmlpurifier/library/HTMLPurifier.auto.php';
  252. }
  253. $cache_dir = api_get_path(SYS_ARCHIVE_PATH).'Serializer';
  254. if (!file_exists($cache_dir)) {
  255. mkdir($cache_dir, 0777);
  256. }
  257. $config = HTMLPurifier_Config::createDefault();
  258. //$config->set('Cache.DefinitionImpl', null); // Enable this line for testing purposes, for turning off caching. Don't forget to disable this line later!
  259. $config->set('Cache.SerializerPath', $cache_dir);
  260. $config->set('Core.Encoding', api_get_system_encoding());
  261. $config->set('HTML.Doctype', 'XHTML 1.0 Transitional');
  262. $config->set('HTML.TidyLevel', 'light');
  263. $config->set('Core.ConvertDocumentToFragment', false);
  264. $config->set('Core.RemoveProcessingInstructions', true);
  265. if ($user_status == STUDENT) {
  266. global $allowed_html_student;
  267. $config->set('HTML.SafeEmbed', true);
  268. $config->set('HTML.SafeObject', true);
  269. $config->set('Filter.YouTube', true);
  270. $config->set('HTML.FlashAllowFullScreen', true);
  271. $config->set('HTML.Allowed', $allowed_html_student);
  272. } elseif ($user_status == COURSEMANAGER) {
  273. global $allowed_html_teacher;
  274. $config->set('HTML.SafeEmbed', true);
  275. $config->set('HTML.SafeObject', true);
  276. $config->set('Filter.YouTube', true);
  277. $config->set('HTML.FlashAllowFullScreen', true);
  278. $config->set('HTML.Allowed', $allowed_html_teacher);
  279. } else {
  280. global $allowed_html_anonymous;
  281. $config->set('HTML.Allowed', $allowed_html_anonymous);
  282. }
  283. $config->set('Attr.EnableID', true); // We need it for example for the flv player (ids of surrounding div-tags have to be preserved).
  284. $config->set('CSS.AllowImportant', true);
  285. $config->set('CSS.AllowTricky', true); // We need for the flv player the css definition display: none;
  286. $config->set('CSS.Proprietary', true);
  287. $purifier[$user_status] = new HTMLPurifier($config);
  288. }
  289. if (is_array($var)) {
  290. return $purifier[$user_status]->purifyArray($var);
  291. } else {
  292. return $purifier[$user_status]->purify($var);
  293. }
  294. }
  295. /**
  296. * This method provides specific protection (against XSS and other kinds of attacks) for static images (icons) used by the system.
  297. * Image paths are supposed to be given by programmers - people who know what they do, anyway, this method encourages
  298. * a safe practice for generating icon paths, without using heavy solutions based on HTMLPurifier for example.
  299. * @param string $img_path The input path of the image, it could be relative or absolute URL.
  300. * @return string Returns sanitized image path or an empty string when the image path is not secure.
  301. * @author Ivan Tcholakov, March 2011
  302. */
  303. public static function filter_img_path($image_path) {
  304. static $allowed_extensions = array('png', 'gif', 'jpg', 'jpeg');
  305. $image_path = htmlspecialchars(trim($image_path)); // No html code is allowed.
  306. // We allow static images only, query strings are forbidden.
  307. if (strpos($image_path, '?') !== false) {
  308. return '';
  309. }
  310. if (($pos = strpos($image_path, ':')) !== false) {
  311. // Protocol has been specified, let's check it.
  312. if (stripos($image_path, 'javascript:') !== false) {
  313. // Javascript everywhere in the path is not allowed.
  314. return '';
  315. }
  316. // We allow only http: and https: protocols for now.
  317. //if (!preg_match('/^https?:\/\//i', $image_path)) {
  318. // return '';
  319. //}
  320. if (stripos($image_path, 'http://') !== 0 && stripos($image_path, 'https://') !== 0) {
  321. return '';
  322. }
  323. }
  324. // We allow file extensions for images only.
  325. //if (!preg_match('/.+\.(png|gif|jpg|jpeg)$/i', $image_path)) {
  326. // return '';
  327. //}
  328. if (($pos = strrpos($image_path, '.')) !== false) {
  329. if (!in_array(strtolower(substr($image_path, $pos + 1)), $allowed_extensions)) {
  330. return '';
  331. }
  332. } else {
  333. return '';
  334. }
  335. return $image_path;
  336. }
  337. }