security.lib.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  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($array = 'post') {
  100. switch ($array) {
  101. case 'get':
  102. if (isset($_SESSION['sec_token']) && isset($_GET['sec_token']) && $_SESSION['sec_token'] === $_GET['sec_token']) {
  103. return true;
  104. }
  105. return false;
  106. case 'post':
  107. if (isset($_SESSION['sec_token']) && isset($_POST['sec_token']) && $_SESSION['sec_token'] === $_POST['sec_token']) {
  108. return true;
  109. }
  110. return false;
  111. default:
  112. if (isset($_SESSION['sec_token']) && isset($array) && $_SESSION['sec_token'] === $array) {
  113. return true;
  114. }
  115. return false;
  116. }
  117. return false; // Just in case, don't let anything slip.
  118. }
  119. /**
  120. * Checks the user agent of the client as recorder by get_ua() to prevent
  121. * most session hijacking attacks.
  122. * @return bool True if the user agent is the same, false otherwise
  123. */
  124. public static function check_ua() {
  125. if (isset($_SESSION['sec_ua']) and $_SESSION['sec_ua'] === $_SERVER['HTTP_USER_AGENT'].$_SESSION['sec_ua_seed']) {
  126. return true;
  127. }
  128. return false;
  129. }
  130. /**
  131. * Clear the security token from the session
  132. * @return void
  133. */
  134. public static function clear_token() {
  135. $_SESSION['sec_token'] = null;
  136. unset($_SESSION['sec_token']);
  137. }
  138. /**
  139. * This function sets a random token to be included in a form as a hidden field
  140. * and saves it into the user's session. Returns an HTML form element
  141. * This later prevents Cross-Site Request Forgeries by checking that the user is really
  142. * the one that sent this form in knowingly (this form hasn't been generated from
  143. * another website visited by the user at the same time).
  144. * Check the token with check_token()
  145. * @return string Hidden-type input ready to insert into a form
  146. */
  147. public static function get_HTML_token() {
  148. $token = md5(uniqid(rand(), TRUE));
  149. $string = '<input type="hidden" name="sec_token" value="'.$token.'" />';
  150. $_SESSION['sec_token'] = $token;
  151. return $string;
  152. }
  153. /**
  154. * This function sets a random token to be included in a form as a hidden field
  155. * and saves it into the user's session.
  156. * This later prevents Cross-Site Request Forgeries by checking that the user is really
  157. * the one that sent this form in knowingly (this form hasn't been generated from
  158. * another website visited by the user at the same time).
  159. * Check the token with check_token()
  160. * @return string Token
  161. */
  162. public static function get_token() {
  163. $token = md5(uniqid(rand(), TRUE));
  164. $_SESSION['sec_token'] = $token;
  165. return $token;
  166. }
  167. /**
  168. * Gets the user agent in the session to later check it with check_ua() to prevent
  169. * most cases of session hijacking.
  170. * @return void
  171. */
  172. public static function get_ua() {
  173. $_SESSION['sec_ua_seed'] = uniqid(rand(), TRUE);
  174. $_SESSION['sec_ua'] = $_SERVER['HTTP_USER_AGENT'].$_SESSION['sec_ua_seed'];
  175. }
  176. /**
  177. * This function filters a variable to the type given, with the options given
  178. * @param mixed The variable to be filtered
  179. * @param string The type of variable we expect (bool,int,float,string)
  180. * @param array Additional options
  181. * @return bool True if variable was filtered and added to the current object, false otherwise
  182. */
  183. public static function filter($var, $type = 'string', $options = array()) {
  184. // This function has not been finished! Do not use!
  185. $result = false;
  186. // Get variable name and value.
  187. $args = func_get_args();
  188. $names = array_keys($args);
  189. $name = $names[0];
  190. $value = $args[$name];
  191. switch ($type) {
  192. case 'bool':
  193. $result = (bool) $var;
  194. break;
  195. case 'int':
  196. $result = (int) $var;
  197. break;
  198. case 'float':
  199. $result = (float) $var;
  200. break;
  201. case 'string/html':
  202. $result = self::remove_XSS($var);
  203. break;
  204. case 'string/db':
  205. $result = Database::escape_string($var);
  206. break;
  207. case 'array':
  208. // An array variable shouldn't be given to the filter.
  209. return false;
  210. default:
  211. return false;
  212. }
  213. if (!empty($option['save'])) {
  214. $this->clean[$name] = $result;
  215. }
  216. return $result;
  217. }
  218. /**
  219. * This function returns a variable from the clean array. If the variable doesn't exist,
  220. * it returns null
  221. * @param string Variable name
  222. * @return mixed Variable or NULL on error
  223. */
  224. public static function get($varname) {
  225. if (isset(self::$clean[$varname])) {
  226. return self::$clean[$varname];
  227. }
  228. return NULL;
  229. }
  230. /**
  231. * This function tackles the XSS injections.
  232. * Filtering for XSS is very easily done by using the htmlentities() function.
  233. * This kind of filtering prevents JavaScript snippets to be understood as such.
  234. * @param mixed The variable to filter for XSS, this params can be a string or an array (example : array(x,y))
  235. * @param integer The user status,constant allowed (STUDENT, COURSEMANAGER, ANONYMOUS, COURSEMANAGERLOWSECURITY)
  236. * @return mixed Filtered string or array
  237. */
  238. public static function remove_XSS($var, $user_status = ANONYMOUS) {
  239. if ($user_status == COURSEMANAGERLOWSECURITY) {
  240. return $var; // No filtering.
  241. }
  242. static $purifier = array();
  243. if (!isset($purifier[$user_status])) {
  244. if (!class_exists('HTMLPurifier')) {
  245. // Lazy loading.
  246. require api_get_path(LIBRARY_PATH).'htmlpurifier/library/HTMLPurifier.auto.php';
  247. }
  248. $cache_dir = api_get_path(SYS_ARCHIVE_PATH).'Serializer';
  249. if (!file_exists($cache_dir)) {
  250. mkdir($cache_dir, 0777);
  251. }
  252. $config = HTMLPurifier_Config::createDefault();
  253. //$config->set('Cache.DefinitionImpl', null); // Enable this line for testing purposes, for turning off caching. Don't forget to disable this line later!
  254. $config->set('Cache.SerializerPath', $cache_dir);
  255. $config->set('Core.Encoding', api_get_system_encoding());
  256. $config->set('HTML.Doctype', 'XHTML 1.0 Transitional');
  257. $config->set('HTML.TidyLevel', 'light');
  258. $config->set('Core.ConvertDocumentToFragment', false);
  259. $config->set('Core.RemoveProcessingInstructions', true);
  260. if ($user_status == STUDENT) {
  261. global $allowed_html_student;
  262. $config->set('HTML.SafeEmbed', true);
  263. $config->set('HTML.SafeObject', true);
  264. $config->set('Filter.YouTube', true);
  265. $config->set('HTML.FlashAllowFullScreen', true);
  266. $config->set('HTML.Allowed', $allowed_html_student);
  267. } elseif ($user_status == COURSEMANAGER) {
  268. global $allowed_html_teacher;
  269. $config->set('HTML.SafeEmbed', true);
  270. $config->set('HTML.SafeObject', true);
  271. $config->set('Filter.YouTube', true);
  272. $config->set('HTML.FlashAllowFullScreen', true);
  273. $config->set('HTML.Allowed', $allowed_html_teacher);
  274. } else {
  275. global $allowed_html_anonymous;
  276. $config->set('HTML.Allowed', $allowed_html_anonymous);
  277. }
  278. $config->set('Attr.EnableID', true); // We need it for example for the flv player (ids of surrounding div-tags have to be preserved).
  279. $config->set('CSS.AllowImportant', true);
  280. $config->set('CSS.AllowTricky', true); // We need for the flv player the css definition display: none;
  281. $config->set('CSS.Proprietary', true);
  282. $purifier[$user_status] = new HTMLPurifier($config);
  283. }
  284. if (is_array($var)) {
  285. return $purifier[$user_status]->purifyArray($var);
  286. } else {
  287. return $purifier[$user_status]->purify($var);
  288. }
  289. }
  290. /**
  291. * This method provides specific protection (against XSS and other kinds of attacks) for static images (icons) used by the system.
  292. * Image paths are supposed to be given by programmers - people who know what they do, anyway, this method encourages
  293. * a safe practice for generating icon paths, without using heavy solutions based on HTMLPurifier for example.
  294. * @param string $img_path The input path of the image, it could be relative or absolute URL.
  295. * @return string Returns sanitized image path or an empty string when the image path is not secure.
  296. * @author Ivan Tcholakov, March 2011
  297. */
  298. public static function filter_img_path($image_path) {
  299. static $allowed_extensions = array('png', 'gif', 'jpg', 'jpeg');
  300. $image_path = htmlspecialchars(trim($image_path)); // No html code is allowed.
  301. // We allow static images only, query strings are forbidden.
  302. if (strpos($image_path, '?') !== false) {
  303. return '';
  304. }
  305. if (($pos = strpos($image_path, ':')) !== false) {
  306. // Protocol has been specified, let's check it.
  307. if (stripos($image_path, 'javascript:') !== false) {
  308. // Javascript everywhere in the path is not allowed.
  309. return '';
  310. }
  311. // We allow only http: and https: protocols for now.
  312. //if (!preg_match('/^https?:\/\//i', $image_path)) {
  313. // return '';
  314. //}
  315. if (stripos($image_path, 'http://') !== 0 && stripos($image_path, 'https://') !== 0) {
  316. return '';
  317. }
  318. }
  319. // We allow file extensions for images only.
  320. //if (!preg_match('/.+\.(png|gif|jpg|jpeg)$/i', $image_path)) {
  321. // return '';
  322. //}
  323. if (($pos = strrpos($image_path, '.')) !== false) {
  324. if (!in_array(strtolower(substr($image_path, $pos + 1)), $allowed_extensions)) {
  325. return '';
  326. }
  327. } else {
  328. return '';
  329. }
  330. return $image_path;
  331. }
  332. }