security.php 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. <?php
  2. /**
  3. * Part of the Fuel framework.
  4. *
  5. * @package Fuel
  6. * @version 1.5
  7. * @author Fuel Development Team
  8. * @license MIT License
  9. * @copyright 2010 - 2013 Fuel Development Team
  10. * @link http://fuelphp.com
  11. */
  12. namespace Fuel\Core;
  13. /**
  14. * Security Class
  15. *
  16. * @package Fuel
  17. * @category Core
  18. * @author Dan Horrigan
  19. * @link http://docs.fuelphp.com/classes/security.html
  20. */
  21. class Security
  22. {
  23. /**
  24. * @var string the token as submitted in the cookie from the previous request
  25. */
  26. protected static $csrf_old_token = false;
  27. /**
  28. * @var string the array key for cookie & post vars to check for the token
  29. */
  30. protected static $csrf_token_key = false;
  31. /**
  32. * @var string the token for the next request
  33. */
  34. protected static $csrf_token = false;
  35. /**
  36. * Class init
  37. *
  38. * Fetches CSRF settings and current token
  39. */
  40. public static function _init()
  41. {
  42. static::$csrf_token_key = \Config::get('security.csrf_token_key', 'fuel_csrf_token');
  43. static::$csrf_old_token = \Input::cookie(static::$csrf_token_key, false);
  44. // if csrf automatic checking is enabled, and it fails validation, bail out!
  45. if (\Config::get('security.csrf_autoload', true))
  46. {
  47. static::check_token();
  48. }
  49. // throw an exception if the output filter setting is missing from the app config
  50. if (\Config::get('security.output_filter', null) === null)
  51. {
  52. throw new \FuelException('There is no security.output_filter defined in your application config file');
  53. }
  54. // deal with duplicate filters, no need to slow the framework down
  55. foreach (array('output_filter', 'uri_filter', 'input_filter') as $setting)
  56. {
  57. $config = \Config::get('security.'.$setting, array());
  58. is_array($config) and \Config::set('security.'.$setting, array_keys(array_flip($config)));
  59. }
  60. }
  61. /**
  62. * Cleans the request URI
  63. *
  64. * @param string $uri uri to clean
  65. * @param bool $strict whether to remove relative directories
  66. */
  67. public static function clean_uri($uri, $strict = false)
  68. {
  69. $filters = \Config::get('security.uri_filter', array());
  70. $filters = is_array($filters) ? $filters : array($filters);
  71. $strict and $uri = preg_replace(array("/\.+\//", '/\/+/'), '/', $uri);
  72. return static::clean($uri, $filters);
  73. }
  74. /**
  75. * Cleans the global $_GET, $_POST and $_COOKIE arrays
  76. */
  77. public static function clean_input()
  78. {
  79. $_GET = static::clean($_GET);
  80. $_POST = static::clean($_POST);
  81. $_COOKIE = static::clean($_COOKIE);
  82. }
  83. /**
  84. * Generic variable clean method
  85. */
  86. public static function clean($var, $filters = null, $type = 'security.input_filter')
  87. {
  88. is_null($filters) and $filters = \Config::get($type, array());
  89. $filters = is_array($filters) ? $filters : array($filters);
  90. foreach ($filters as $filter)
  91. {
  92. // is this filter a callable local function?
  93. if (is_string($filter) and is_callable('static::'.$filter))
  94. {
  95. $var = static::$filter($var);
  96. }
  97. // is this filter a callable function?
  98. elseif (is_callable($filter))
  99. {
  100. if (is_array($var))
  101. {
  102. foreach($var as $key => $value)
  103. {
  104. $var[$key] = call_user_func($filter, $value);
  105. }
  106. }
  107. else
  108. {
  109. $var = call_user_func($filter, $var);
  110. }
  111. }
  112. // assume it's a regex of characters to filter
  113. else
  114. {
  115. if (is_array($var))
  116. {
  117. foreach($var as $key => $value)
  118. {
  119. $var[$key] = preg_replace('#['.$filter.']#ui', '', $value);
  120. }
  121. }
  122. else
  123. {
  124. $var = preg_replace('#['.$filter.']#ui', '', $var);
  125. }
  126. }
  127. }
  128. return $var;
  129. }
  130. public static function xss_clean($value)
  131. {
  132. if ( ! is_array($value))
  133. {
  134. if ( ! function_exists('htmLawed'))
  135. {
  136. import('htmlawed/htmlawed', 'vendor');
  137. }
  138. return htmLawed($value, array('safe' => 1, 'balanced' => 0));
  139. }
  140. foreach ($value as $k => $v)
  141. {
  142. $value[$k] = static::xss_clean($v);
  143. }
  144. return $value;
  145. }
  146. public static function strip_tags($value)
  147. {
  148. if ( ! is_array($value))
  149. {
  150. $value = filter_var($value, FILTER_SANITIZE_STRING);
  151. }
  152. else
  153. {
  154. foreach ($value as $k => $v)
  155. {
  156. $value[$k] = static::strip_tags($v);
  157. }
  158. }
  159. return $value;
  160. }
  161. public static function htmlentities($value, $flags = null, $encoding = null, $double_encode = null)
  162. {
  163. static $already_cleaned = array();
  164. is_null($flags) and $flags = \Config::get('security.htmlentities_flags', ENT_QUOTES);
  165. is_null($encoding) and $encoding = \Fuel::$encoding;
  166. is_null($double_encode) and $double_encode = \Config::get('security.htmlentities_double_encode', false);
  167. // Nothing to escape for non-string scalars, or for already processed values
  168. if (is_bool($value) or is_int($value) or is_float($value) or in_array($value, $already_cleaned, true))
  169. {
  170. return $value;
  171. }
  172. if (is_string($value))
  173. {
  174. $value = htmlentities($value, $flags, $encoding, $double_encode);
  175. }
  176. elseif (is_array($value) or ($value instanceof \Iterator and $value instanceof \ArrayAccess))
  177. {
  178. // Add to $already_cleaned variable when object
  179. is_object($value) and $already_cleaned[] = $value;
  180. foreach ($value as $k => $v)
  181. {
  182. $value[$k] = static::htmlentities($v, $flags, $encoding, $double_encode);
  183. }
  184. }
  185. elseif ($value instanceof \Iterator or get_class($value) == 'stdClass')
  186. {
  187. // Add to $already_cleaned variable
  188. $already_cleaned[] = $value;
  189. foreach ($value as $k => $v)
  190. {
  191. $value->{$k} = static::htmlentities($v, $flags, $encoding, $double_encode);
  192. }
  193. }
  194. elseif (is_object($value))
  195. {
  196. // Check if the object is whitelisted and return when that's the case
  197. foreach (\Config::get('security.whitelisted_classes', array()) as $class)
  198. {
  199. if (is_a($value, $class))
  200. {
  201. // Add to $already_cleaned variable
  202. $already_cleaned[] = $value;
  203. return $value;
  204. }
  205. }
  206. // Throw exception when it wasn't whitelisted and can't be converted to String
  207. if ( ! method_exists($value, '__toString'))
  208. {
  209. throw new \RuntimeException('Object class "'.get_class($value).'" could not be converted to string or '.
  210. 'sanitized as ArrayAccess. Whitelist it in security.whitelisted_classes in app/config/config.php '.
  211. 'to allow it to be passed unchecked.');
  212. }
  213. $value = static::htmlentities((string) $value, $flags, $encoding, $double_encode);
  214. }
  215. return $value;
  216. }
  217. /**
  218. * Check CSRF Token
  219. *
  220. * @param string CSRF token to be checked, checks post when empty
  221. * @return bool
  222. */
  223. public static function check_token($value = null)
  224. {
  225. $value = $value ?: \Input::post(static::$csrf_token_key, 'fail');
  226. // always reset token once it's been checked and still the same
  227. if (static::fetch_token() == static::$csrf_old_token and ! empty($value))
  228. {
  229. static::set_token(true);
  230. }
  231. return $value === static::$csrf_old_token;
  232. }
  233. /**
  234. * Fetch CSRF Token for the next request
  235. *
  236. * @return string
  237. */
  238. public static function fetch_token()
  239. {
  240. if (static::$csrf_token !== false)
  241. {
  242. return static::$csrf_token;
  243. }
  244. static::set_token();
  245. return static::$csrf_token;
  246. }
  247. protected static function set_token($reset = false)
  248. {
  249. // re-use old token when found (= not expired) and expiration is used (otherwise always reset)
  250. if ( ! $reset and static::$csrf_old_token and \Config::get('security.csrf_expiration', 0) > 0)
  251. {
  252. static::$csrf_token = static::$csrf_old_token;
  253. }
  254. // set new token for next session when necessary
  255. else
  256. {
  257. static::$csrf_token = md5(uniqid().time());
  258. $expiration = \Config::get('security.csrf_expiration', 0);
  259. \Cookie::set(static::$csrf_token_key, static::$csrf_token, $expiration);
  260. }
  261. }
  262. /**
  263. * JS fetch token
  264. *
  265. * Produces JavaScript fuel_csrf_token() function that will return the current
  266. * CSRF token when called. Use to fill right field on form submit for AJAX operations.
  267. *
  268. * @return string
  269. */
  270. public static function js_fetch_token()
  271. {
  272. $output = '<script type="text/javascript">
  273. function fuel_csrf_token()
  274. {
  275. if (document.cookie.length > 0)
  276. {
  277. var c_name = "'.static::$csrf_token_key.'";
  278. c_start = document.cookie.indexOf(c_name + "=");
  279. if (c_start != -1)
  280. {
  281. c_start = c_start + c_name.length + 1;
  282. c_end = document.cookie.indexOf(";" , c_start);
  283. if (c_end == -1)
  284. {
  285. c_end=document.cookie.length;
  286. }
  287. return unescape(document.cookie.substring(c_start, c_end));
  288. }
  289. }
  290. return "";
  291. }'.PHP_EOL;
  292. $output .= '</script>'.PHP_EOL;
  293. return $output;
  294. }
  295. /**
  296. * JS set token
  297. *
  298. * Produces JavaScript fuel_set_csrf_token() function that will update the current
  299. * CSRF token in the form when called, based on the value of the csrf cookie
  300. *
  301. * @return string
  302. */
  303. public static function js_set_token()
  304. {
  305. $output = '<script type="text/javascript">
  306. function fuel_set_csrf_token(form)
  307. {
  308. if (document.cookie.length > 0 && typeof form != undefined)
  309. {
  310. var c_name = "'.static::$csrf_token_key.'";
  311. c_start = document.cookie.indexOf(c_name + "=");
  312. if (c_start != -1)
  313. {
  314. c_start = c_start + c_name.length + 1;
  315. c_end = document.cookie.indexOf(";" , c_start);
  316. if (c_end == -1)
  317. {
  318. c_end=document.cookie.length;
  319. }
  320. value=unescape(document.cookie.substring(c_start, c_end));
  321. if (value != "")
  322. {
  323. for(i=0; i<form.elements.length; i++)
  324. {
  325. if (form.elements[i].name == c_name)
  326. {
  327. form.elements[i].value = value;
  328. break;
  329. }
  330. }
  331. }
  332. }
  333. }
  334. }'.PHP_EOL;
  335. $output .= '</script>'.PHP_EOL;
  336. return $output;
  337. }
  338. }