PageRenderTime 30ms CodeModel.GetById 1ms RepoModel.GetById 0ms app.codeStats 0ms

/modules/gallery/tests/Xss_Security_Test.php

http://github.com/gallery/gallery3
PHP | 466 lines | 143 code | 21 blank | 302 comment | 29 complexity | 7fe6ea1aaeca7d4cd5ece758bc56b1e9 MD5 | raw file
Possible License(s): GPL-2.0
  1. <?php defined("SYSPATH") or die("No direct script access.");
  2. /**
  3. * Gallery - a web based photo album viewer and editor
  4. * Copyright (C) 2000-2013 Bharat Mediratta
  5. *
  6. * This program is free software; you can redistribute it and/or modify
  7. * it under the terms of the GNU General Public License as published by
  8. * the Free Software Foundation; either version 2 of the License, or (at
  9. * your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful, but
  12. * WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with this program; if not, write to the Free Software
  18. * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. class Xss_Security_Test extends Gallery_Unit_Test_Case {
  21. public function find_unescaped_variables_in_views_test() {
  22. $found = array();
  23. foreach (glob("*/*/views/*.php") as $view) {
  24. // List of all tokens without whitespace, simplifying parsing.
  25. $tokens = array();
  26. foreach (token_get_all(file_get_contents($view)) as $token) {
  27. if (!is_array($token) || ($token[0] != T_WHITESPACE)) {
  28. $tokens[] = $token;
  29. }
  30. }
  31. $frame = null;
  32. $script_block = 0;
  33. $in_script_block = false;
  34. $inline_html = "";
  35. $in_attribute_js_context = false;
  36. $in_attribute = false;
  37. $href_attribute_start = false;
  38. $preceded_by_quote = false;
  39. for ($token_number = 0; $token_number < count($tokens); $token_number++) {
  40. $token = $tokens[$token_number];
  41. // Are we in a <script> ... </script> block?
  42. if (is_array($token) && $token[0] == T_INLINE_HTML) {
  43. $inline_html = $token[1];
  44. // T_INLINE_HTML blocks can be split. Need to handle the case
  45. // where one token has "<scr" and the next has "ipt"
  46. while (self::_token_matches(array(T_INLINE_HTML), $tokens, $token_number + 1)) {
  47. $token_number++;
  48. $token = $tokens[$token_number];
  49. $inline_html .= $token[1];
  50. }
  51. $inline_html = str_replace("\n", " ", $inline_html);
  52. if ($frame) {
  53. $frame->expr_append($inline_html);
  54. }
  55. // Note: This approach won't catch <script src="..."> blocks if the src
  56. // URL is generated via < ? = url::site() ? > or some other PHP.
  57. // Assume that all such script blocks with a src URL have an
  58. // empty element body.
  59. // But we'll catch closing tags for such blocks, so don't keep track
  60. // of opening / closing tag count since it would be meaningless.
  61. // Handle multiple start / end blocks on the same line?
  62. $opening_script_pos = $closing_script_pos = -1;
  63. if (preg_match_all('{</script>}i', $inline_html, $matches, PREG_OFFSET_CAPTURE)) {
  64. $last_match = array_pop($matches[0]);
  65. if (is_array($last_match)) {
  66. $closing_script_pos = $last_match[1];
  67. } else {
  68. $closing_script_pos = $last_match;
  69. }
  70. }
  71. if (preg_match_all('{<script\b[^>]*>}i', $inline_html, $matches, PREG_OFFSET_CAPTURE)) {
  72. $last_match = array_pop($matches[0]);
  73. if (is_array($last_match)) {
  74. $opening_script_pos = $last_match[1];
  75. } else {
  76. $opening_script_pos = $last_match;
  77. }
  78. }
  79. if ($opening_script_pos != $closing_script_pos) {
  80. $in_script_block = $opening_script_pos > $closing_script_pos;
  81. }
  82. }
  83. $preceded_by_quote = preg_match('{[\'"]\s*$}i', $inline_html);
  84. $pos = false;
  85. if (($in_attribute || $in_attribute_js_context) &&
  86. ($pos = strpos($inline_html, $delimiter)) !== false) {
  87. $in_attribute_js_context = false;
  88. $in_attribute = false;
  89. $href_attribute_start = false;
  90. }
  91. if (!$in_attribute_js_context || !$in_attribute) {
  92. $pos = ($pos === false) ? 0 : $pos;
  93. if (preg_match('{\bhref\s*=\s*(")javascript:[^"]*$}i', $inline_html, $matches, 0, $pos) ||
  94. preg_match("{\bhref\s*=\s*(')javascript:[^']*$}i", $inline_html, $matches, 0, $pos) ||
  95. preg_match("{\bon[a-z]+\s*=\s*(')[^']*$}i", $inline_html, $matches, 0, $pos) ||
  96. preg_match('{\bon[a-z]+\s*=\s*(")[^"]*$}i', $inline_html, $matches, 0, $pos)) {
  97. $in_attribute_js_context = true;
  98. $in_attribute = true;
  99. $delimiter = $matches[1];
  100. $inline_html = "";
  101. } else if (preg_match('{\b([a-z]+)\s*=\s*(")([^"]*)$}i', $inline_html, $matches, 0, $pos) ||
  102. preg_match("{\b([a-z]+)\s*=\s*(')([^']*)$}i", $inline_html, $matches, 0, $pos)) {
  103. $in_attribute = true;
  104. $delimiter = $matches[2];
  105. $inline_html = "";
  106. $href_attribute_start = strtolower($matches[1]) == "href" && empty($matches[3]);
  107. }
  108. }
  109. // Look and report each instance of < ? = ... ? >
  110. if (!is_array($token)) {
  111. // A single char token, e.g: ; ( )
  112. if ($frame) {
  113. $frame->expr_append($token);
  114. }
  115. } else if ($token[0] == T_OPEN_TAG_WITH_ECHO) {
  116. // No need for a stack here - assume < ? = cannot be nested.
  117. $frame = self::_create_frame($token, $in_script_block,
  118. $href_attribute_start, $in_attribute_js_context,
  119. $in_attribute, $preceded_by_quote);
  120. $href_attribute_start = false;
  121. } else if ($frame && $token[0] == T_CLOSE_TAG) {
  122. // Store the < ? = ... ? > block that just ended here.
  123. $found[$view][] = $frame;
  124. $frame = null;
  125. } else if ($frame && $token[0] == T_VARIABLE) {
  126. $frame->expr_append($token[1]);
  127. if ($token[1] == '$theme') {
  128. if (self::_token_matches(array(T_OBJECT_OPERATOR, "->"), $tokens, $token_number + 1) &&
  129. self::_token_matches(array(T_STRING), $tokens, $token_number + 2) &&
  130. in_array($tokens[$token_number + 2][1],
  131. array("thumb_proportion", "site_menu", "album_menu", "tag_menu", "photo_menu",
  132. "context_menu", "pager", "site_status", "messages", "album_blocks",
  133. "album_bottom", "album_top", "body_attributes", "credits",
  134. "dynamic_bottom", "dynamic_top", "footer", "head", "header_bottom",
  135. "header_top", "page_bottom", "page_top", "photo_blocks", "photo_bottom",
  136. "photo_top", "resize_bottom", "resize_top", "sidebar_blocks", "sidebar_bottom",
  137. "sidebar_top", "thumb_bottom", "thumb_info", "thumb_top",
  138. "movie_menu")) &&
  139. self::_token_matches("(", $tokens, $token_number + 3)) {
  140. $method = $tokens[$token_number + 2][1];
  141. $frame->expr_append("->$method(");
  142. $token_number += 3;
  143. $token = $tokens[$token_number];
  144. $frame->is_safe_html(true);
  145. } else if (self::_token_matches(array(T_OBJECT_OPERATOR, "->"), $tokens, $token_number + 1) &&
  146. self::_token_matches(array(T_STRING), $tokens, $token_number + 2) &&
  147. in_array($tokens[$token_number + 2][1],
  148. array("css", "script", "url")) &&
  149. self::_token_matches("(", $tokens, $token_number + 3) &&
  150. // Only allow constant strings here
  151. self::_token_matches(array(T_CONSTANT_ENCAPSED_STRING), $tokens, $token_number + 4)) {
  152. $method = $tokens[$token_number + 2][1];
  153. $frame->expr_append("->$method(");
  154. $token_number += 4;
  155. $token = $tokens[$token_number];
  156. $frame->is_safe_html(true);
  157. }
  158. }
  159. } else if ($frame && $token[0] == T_STRING) {
  160. $frame->expr_append($token[1]);
  161. // t() and t2() are special in that they're guaranteed to return a SafeString().
  162. if (in_array($token[1], array("t", "t2"))) {
  163. if (self::_token_matches("(", $tokens, $token_number + 1)) {
  164. $frame->is_safe_html(true);
  165. $frame->expr_append("(");
  166. $token_number++;
  167. $token = $tokens[$token_number];
  168. }
  169. } else if ($token[1] == "SafeString") {
  170. // Looking for SafeString::of(...
  171. if (self::_token_matches(array(T_DOUBLE_COLON, "::"), $tokens, $token_number + 1) &&
  172. self::_token_matches(array(T_STRING), $tokens, $token_number + 2) &&
  173. in_array($tokens[$token_number + 2][1], array("of", "purify")) &&
  174. self::_token_matches("(", $tokens, $token_number + 3)) {
  175. // Not checking for of_safe_html(). We want such calls to be marked dirty (thus reviewed).
  176. $frame->is_safe_html(true);
  177. $method = $tokens[$token_number + 2][1];
  178. $frame->expr_append("::$method(");
  179. $token_number += 3;
  180. $token = $tokens[$token_number];
  181. }
  182. } else if ($token[1] == "json_encode") {
  183. if (self::_token_matches("(", $tokens, $token_number + 1)) {
  184. $frame->is_safe_js(true);
  185. $frame->expr_append("(");
  186. $token_number++;
  187. $token = $tokens[$token_number];
  188. }
  189. } else if ($token[1] == "url") {
  190. // url methods return safe HTML
  191. if (self::_token_matches(array(T_DOUBLE_COLON, "::"), $tokens, $token_number + 1) &&
  192. self::_token_matches(array(T_STRING), $tokens, $token_number + 2) &&
  193. in_array($tokens[$token_number + 2][1],
  194. array("site", "current", "base", "file", "abs_site", "abs_current",
  195. "abs_file", "merge")) &&
  196. self::_token_matches("(", $tokens, $token_number + 3)) {
  197. $frame->is_safe_html(true);
  198. $frame->is_safe_href_attr(true);
  199. $frame->is_safe_attr(true);
  200. $method = $tokens[$token_number + 2][1];
  201. $frame->expr_append("::$method(");
  202. $token_number += 3;
  203. $token = $tokens[$token_number];
  204. }
  205. } else if ($token[1] == "html") {
  206. if (self::_token_matches(array(T_DOUBLE_COLON, "::"), $tokens, $token_number + 1) &&
  207. self::_token_matches(array(T_STRING), $tokens, $token_number + 2) &&
  208. in_array($tokens[$token_number + 2][1],
  209. array("clean", "purify", "js_string", "clean_attribute")) &&
  210. self::_token_matches("(", $tokens, $token_number + 3)) {
  211. // Not checking for mark_clean(). We want such calls to be marked dirty (thus reviewed).
  212. $method = $tokens[$token_number + 2][1];
  213. $frame->expr_append("::$method(");
  214. $token_number += 3;
  215. $token = $tokens[$token_number];
  216. if ("js_string" == $method) {
  217. $frame->is_safe_js(true);
  218. } else {
  219. $frame->is_safe_html(true);
  220. }
  221. if ("clean_attribute" == $method) {
  222. $frame->is_safe_attr(true);
  223. }
  224. }
  225. }
  226. } else if ($frame && $token[0] == T_OBJECT_OPERATOR) {
  227. $frame->expr_append($token[1]);
  228. if (self::_token_matches(array(T_STRING), $tokens, $token_number + 1) &&
  229. in_array($tokens[$token_number + 1][1],
  230. array("for_js", "for_html", "purified_html", "for_html_attr")) &&
  231. self::_token_matches("(", $tokens, $token_number + 2)) {
  232. $method = $tokens[$token_number + 1][1];
  233. $frame->expr_append("$method(");
  234. $token_number += 2;
  235. $token = $tokens[$token_number];
  236. if ("for_js" == $method) {
  237. $frame->is_safe_js(true);
  238. } else {
  239. $frame->is_safe_html(true);
  240. }
  241. if ("for_html_attr" == $method) {
  242. $frame->is_safe_attr(true);
  243. }
  244. }
  245. } else if ($frame) {
  246. $frame->expr_append($token[1]);
  247. }
  248. }
  249. }
  250. /*
  251. * Generate the report
  252. *
  253. * States for uses of < ? = X ? >:
  254. * DIRTY_JS:
  255. * In <script> block
  256. * X can be anything without calling ->for_js()
  257. * At the start of a href= attribute
  258. * X = anything but a url method
  259. * In href="javascript: or onclick="...":
  260. * X = anything (manual review required)
  261. * DIRTY:
  262. * Outside <script> block:
  263. * X can be anything without a call to ->for_html() or ->purified_html()
  264. * CLEAN:
  265. * Outside <script> block:
  266. * X = is SafeString (t(), t2(), url::site())
  267. * X = * and for_html() or purified_html() is called
  268. * Inside <script> block:
  269. * X = * with ->for_js() or json_encode(...)
  270. * Start of href attribute:
  271. * X = url method
  272. */
  273. $new = TMPPATH . "xss_data.txt";
  274. $fd = fopen($new, "wb");
  275. ksort($found);
  276. foreach ($found as $view => $frames) {
  277. foreach ($frames as $frame) {
  278. $state = "DIRTY";
  279. if ($frame->in_script_block() && $frame->in_href_attribute()) {
  280. // This parser assumes this state does not occur.
  281. $state = "ILLEGAL";
  282. } else if ($frame->in_script_block()) {
  283. $state = "DIRTY_JS";
  284. if ($frame->is_safe_js() && !$frame->preceded_by_quote()) {
  285. $state = "CLEAN";
  286. }
  287. } else if ($frame->in_attribute_js_context()) {
  288. // Manual review required
  289. $state = "DIRTY_JS";
  290. } else if ($frame->in_href_attribute()) {
  291. $state = "DIRTY_JS";
  292. if ($frame->is_safe_href_attr()) {
  293. $state = "CLEAN";
  294. }
  295. } else if ($frame->in_attribute()) {
  296. $state = "DIRTY_ATTR";
  297. if ($frame->is_safe_attr()) {
  298. $state = "CLEAN";
  299. }
  300. } else {
  301. if ($frame->is_safe_html()) {
  302. $state = "CLEAN";
  303. }
  304. }
  305. if ("CLEAN" == $state) {
  306. // Don't print CLEAN instances - No need to update the golden
  307. // file when adding / moving clean instances.
  308. continue;
  309. }
  310. fprintf($fd, "%-60s %-3s %-8s %s\n",
  311. $view, $frame->line(), $state, $frame->expr());
  312. }
  313. }
  314. fclose($fd);
  315. // Compare with the expected report from our golden file.
  316. $canonical = MODPATH . "gallery/tests/xss_data.txt";
  317. exec("diff $canonical $new", $output, $return_value);
  318. $this->assert_false(
  319. $return_value, "XSS golden file mismatch. Output:\n" . implode("\n", $output) );
  320. }
  321. private static function _create_frame($token, $in_script_block,
  322. $href_attribute_start, $in_attribute_js_context,
  323. $in_attribute, $preceded_by_quote) {
  324. return new Xss_Security_Test_Frame($token[2], $in_script_block,
  325. $href_attribute_start, $in_attribute_js_context,
  326. $in_attribute, $preceded_by_quote);
  327. }
  328. private static function _token_matches($expected_token, &$tokens, $token_number) {
  329. if (!isset($tokens[$token_number])) {
  330. return false;
  331. }
  332. $token = $tokens[$token_number];
  333. if (is_array($expected_token)) {
  334. for ($i = 0; $i < count($expected_token); $i++) {
  335. if ($expected_token[$i] != $token[$i]) {
  336. return false;
  337. }
  338. }
  339. return true;
  340. } else {
  341. return $expected_token == $token;
  342. }
  343. }
  344. }
  345. class Xss_Security_Test_Frame {
  346. private $_expr = "";
  347. private $_in_script_block = false;
  348. private $_is_safe_html = false;
  349. private $_is_safe_js = false;
  350. private $_in_href_attribute = false;
  351. private $_is_safe_href_attr = false;
  352. private $_in_attribute_js_context = false;
  353. private $_in_attribute = false;
  354. private $_preceded_by_quote = false;
  355. private $_is_safe_attr = false;
  356. private $_line;
  357. function __construct($line_number, $in_script_block,
  358. $href_attribute_start, $in_attribute_js_context,
  359. $in_attribute, $preceded_by_quote) {
  360. $this->_line = $line_number;
  361. $this->_in_script_block = $in_script_block;
  362. $this->_in_href_attribute = $href_attribute_start;
  363. $this->_in_attribute_js_context = $in_attribute_js_context;
  364. $this->_in_attribute = $in_attribute;
  365. $this->_preceded_by_quote = $preceded_by_quote;
  366. }
  367. function expr() {
  368. return $this->_expr;
  369. }
  370. function expr_append($append_value) {
  371. return $this->_expr .= $append_value;
  372. }
  373. function in_script_block() {
  374. return $this->_in_script_block;
  375. }
  376. function in_href_attribute() {
  377. return $this->_in_href_attribute;
  378. }
  379. function in_attribute() {
  380. return $this->_in_attribute;
  381. }
  382. function in_attribute_js_context() {
  383. return $this->_in_attribute_js_context;
  384. }
  385. function is_safe_html($new_val=NULL) {
  386. if ($new_val !== NULL) {
  387. $this->_is_safe_html = (bool) $new_val;
  388. }
  389. return $this->_is_safe_html;
  390. }
  391. function is_safe_href_attr($new_val=NULL) {
  392. if ($new_val !== NULL) {
  393. $this->_is_safe_href_attr = (bool) $new_val;
  394. }
  395. return $this->_is_safe_href_attr;
  396. }
  397. function is_safe_attr($new_val=NULL) {
  398. if ($new_val !== NULL) {
  399. $this->_is_safe_attr = (bool) $new_val;
  400. }
  401. return $this->_is_safe_attr;
  402. }
  403. function is_safe_js($new_val=NULL) {
  404. if ($new_val !== NULL) {
  405. $this->_is_safe_js = (bool) $new_val;
  406. }
  407. return $this->_is_safe_js;
  408. }
  409. function preceded_by_quote() {
  410. return $this->_preceded_by_quote;
  411. }
  412. function line() {
  413. return $this->_line;
  414. }
  415. }