Sanitize.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. <?php
  2. /**
  3. * This class includes various sanitization methods that can be called statically
  4. */
  5. declare(strict_types=1);
  6. namespace PhpMyAdmin;
  7. use PhpMyAdmin\Html\MySQLDocumentation;
  8. use function array_keys;
  9. use function array_merge;
  10. use function count;
  11. use function htmlspecialchars;
  12. use function in_array;
  13. use function is_array;
  14. use function is_bool;
  15. use function is_int;
  16. use function is_string;
  17. use function preg_match;
  18. use function preg_replace;
  19. use function preg_replace_callback;
  20. use function str_replace;
  21. use function strlen;
  22. use function strncmp;
  23. use function strtolower;
  24. use function strtr;
  25. use function substr;
  26. /**
  27. * This class includes various sanitization methods that can be called statically
  28. */
  29. class Sanitize
  30. {
  31. /**
  32. * Checks whether given link is valid
  33. *
  34. * @param string $url URL to check
  35. * @param bool $http Whether to allow http links
  36. * @param bool $other Whether to allow ftp and mailto links
  37. *
  38. * @return bool True if string can be used as link
  39. */
  40. public static function checkLink($url, $http = false, $other = false)
  41. {
  42. $url = strtolower($url);
  43. $valid_starts = [
  44. 'https://',
  45. './url.php?url=https%3a%2f%2f',
  46. './doc/html/',
  47. './index.php?',
  48. ];
  49. $is_setup = self::isSetup();
  50. // Adjust path to setup script location
  51. if ($is_setup) {
  52. foreach ($valid_starts as $key => $value) {
  53. if (substr($value, 0, 2) !== './') {
  54. continue;
  55. }
  56. $valid_starts[$key] = '.' . $value;
  57. }
  58. }
  59. if ($other) {
  60. $valid_starts[] = 'mailto:';
  61. $valid_starts[] = 'ftp://';
  62. }
  63. if ($http) {
  64. $valid_starts[] = 'http://';
  65. }
  66. if ($is_setup) {
  67. $valid_starts[] = '?page=form&';
  68. $valid_starts[] = '?page=servers&';
  69. }
  70. foreach ($valid_starts as $val) {
  71. if (substr($url, 0, strlen($val)) == $val) {
  72. return true;
  73. }
  74. }
  75. return false;
  76. }
  77. /**
  78. * Check if we are currently on a setup folder page
  79. */
  80. public static function isSetup(): bool
  81. {
  82. return $GLOBALS['PMA_Config'] !== null && $GLOBALS['PMA_Config']->get('is_setup');
  83. }
  84. /**
  85. * Callback function for replacing [a@link@target] links in bb code.
  86. *
  87. * @param array $found Array of preg matches
  88. *
  89. * @return string Replaced string
  90. */
  91. public static function replaceBBLink(array $found)
  92. {
  93. /* Check for valid link */
  94. if (! self::checkLink($found[1])) {
  95. return $found[0];
  96. }
  97. /* a-z and _ allowed in target */
  98. if (! empty($found[3]) && preg_match('/[^a-z_]+/i', $found[3])) {
  99. return $found[0];
  100. }
  101. /* Construct target */
  102. $target = '';
  103. if (! empty($found[3])) {
  104. $target = ' target="' . $found[3] . '"';
  105. if ($found[3] === '_blank') {
  106. $target .= ' rel="noopener noreferrer"';
  107. }
  108. }
  109. /* Construct url */
  110. if (substr($found[1], 0, 4) === 'http') {
  111. $url = Core::linkURL($found[1]);
  112. } else {
  113. $url = $found[1];
  114. }
  115. return '<a href="' . $url . '"' . $target . '>';
  116. }
  117. /**
  118. * Callback function for replacing [doc@anchor] links in bb code.
  119. *
  120. * @param array $found Array of preg matches
  121. *
  122. * @return string Replaced string
  123. */
  124. public static function replaceDocLink(array $found)
  125. {
  126. if (count($found) >= 4) {
  127. /* doc@page@anchor pattern */
  128. $page = $found[1];
  129. $anchor = $found[3];
  130. } else {
  131. /* doc@anchor pattern */
  132. $anchor = $found[1];
  133. if (strncmp('faq', $anchor, 3) == 0) {
  134. $page = 'faq';
  135. } elseif (strncmp('cfg', $anchor, 3) == 0) {
  136. $page = 'config';
  137. } else {
  138. /* Guess */
  139. $page = 'setup';
  140. }
  141. }
  142. $link = MySQLDocumentation::getDocumentationLink($page, $anchor, self::isSetup() ? '../' : './');
  143. return '<a href="' . $link . '" target="documentation">';
  144. }
  145. /**
  146. * Sanitizes $message, taking into account our special codes
  147. * for formatting.
  148. *
  149. * If you want to include result in element attribute, you should escape it.
  150. *
  151. * Examples:
  152. *
  153. * <p><?php echo Sanitize::sanitizeMessage($foo); ?></p>
  154. *
  155. * <a title="<?php echo Sanitize::sanitizeMessage($foo, true); ?>">bar</a>
  156. *
  157. * @param string $message the message
  158. * @param bool $escape whether to escape html in result
  159. * @param bool $safe whether string is safe (can keep < and > chars)
  160. */
  161. public static function sanitizeMessage(string $message, $escape = false, $safe = false): string
  162. {
  163. if (! $safe) {
  164. $message = strtr($message, ['<' => '&lt;', '>' => '&gt;']);
  165. }
  166. /* Interpret bb code */
  167. $replace_pairs = [
  168. '[em]' => '<em>',
  169. '[/em]' => '</em>',
  170. '[strong]' => '<strong>',
  171. '[/strong]' => '</strong>',
  172. '[code]' => '<code>',
  173. '[/code]' => '</code>',
  174. '[kbd]' => '<kbd>',
  175. '[/kbd]' => '</kbd>',
  176. '[br]' => '<br>',
  177. '[/a]' => '</a>',
  178. '[/doc]' => '</a>',
  179. '[sup]' => '<sup>',
  180. '[/sup]' => '</sup>',
  181. // used in common.inc.php:
  182. '[conferr]' => '<iframe src="show_config_errors.php"><a href='
  183. . '"show_config_errors.php">show_config_errors.php</a></iframe>',
  184. // used in libraries/Util.php
  185. '[dochelpicon]' => Html\Generator::getImage('b_help', __('Documentation')),
  186. ];
  187. $message = strtr($message, $replace_pairs);
  188. /* Match links in bb code ([a@url@target], where @target is options) */
  189. $pattern = '/\[a@([^]"@]*)(@([^]"]*))?\]/';
  190. /* Find and replace all links */
  191. $message = (string) preg_replace_callback($pattern, static function (array $match) {
  192. return self::replaceBBLink($match);
  193. }, $message);
  194. /* Replace documentation links */
  195. $message = (string) preg_replace_callback(
  196. '/\[doc@([a-zA-Z0-9_-]+)(@([a-zA-Z0-9_-]*))?\]/',
  197. static function (array $match) {
  198. return self::replaceDocLink($match);
  199. },
  200. $message
  201. );
  202. /* Possibly escape result */
  203. if ($escape) {
  204. $message = htmlspecialchars($message);
  205. }
  206. return $message;
  207. }
  208. /**
  209. * Sanitize a filename by removing anything besides legit characters
  210. *
  211. * Intended usecase:
  212. * When using a filename in a Content-Disposition header
  213. * the value should not contain ; or "
  214. *
  215. * When exporting, avoiding generation of an unexpected double-extension file
  216. *
  217. * @param string $filename The filename
  218. * @param bool $replaceDots Whether to also replace dots
  219. *
  220. * @return string the sanitized filename
  221. */
  222. public static function sanitizeFilename($filename, $replaceDots = false)
  223. {
  224. $pattern = '/[^A-Za-z0-9_';
  225. // if we don't have to replace dots
  226. if (! $replaceDots) {
  227. // then add the dot to the list of legit characters
  228. $pattern .= '.';
  229. }
  230. $pattern .= '-]/';
  231. $filename = preg_replace($pattern, '_', $filename);
  232. return $filename;
  233. }
  234. /**
  235. * Format a string so it can be a string inside JavaScript code inside an
  236. * eventhandler (onclick, onchange, on..., ).
  237. * This function is used to displays a javascript confirmation box for
  238. * "DROP/DELETE/ALTER" queries.
  239. *
  240. * @param string $a_string the string to format
  241. * @param bool $add_backquotes whether to add backquotes to the string or not
  242. *
  243. * @return string the formatted string
  244. *
  245. * @access public
  246. */
  247. public static function jsFormat($a_string = '', $add_backquotes = true)
  248. {
  249. $a_string = htmlspecialchars((string) $a_string);
  250. $a_string = self::escapeJsString($a_string);
  251. // Needed for inline javascript to prevent some browsers
  252. // treating it as a anchor
  253. $a_string = str_replace('#', '\\#', $a_string);
  254. return $add_backquotes
  255. ? Util::backquote($a_string)
  256. : $a_string;
  257. }
  258. /**
  259. * escapes a string to be inserted as string a JavaScript block
  260. * enclosed by <![CDATA[ ... ]]>
  261. * this requires only to escape ' with \' and end of script block
  262. *
  263. * We also remove NUL byte as some browsers (namely MSIE) ignore it and
  264. * inserting it anywhere inside </script would allow to bypass this check.
  265. *
  266. * @param string $string the string to be escaped
  267. *
  268. * @return string the escaped string
  269. */
  270. public static function escapeJsString($string)
  271. {
  272. return preg_replace(
  273. '@</script@i',
  274. '</\' + \'script',
  275. strtr(
  276. (string) $string,
  277. [
  278. "\000" => '',
  279. '\\' => '\\\\',
  280. '\'' => '\\\'',
  281. '"' => '\"',
  282. "\n" => '\n',
  283. "\r" => '\r',
  284. ]
  285. )
  286. );
  287. }
  288. /**
  289. * Formats a value for javascript code.
  290. *
  291. * @param string $value String to be formatted.
  292. *
  293. * @return int|string formatted value.
  294. */
  295. public static function formatJsVal($value)
  296. {
  297. if (is_bool($value)) {
  298. if ($value) {
  299. return 'true';
  300. }
  301. return 'false';
  302. }
  303. if (is_int($value)) {
  304. return (int) $value;
  305. }
  306. return '"' . self::escapeJsString($value) . '"';
  307. }
  308. /**
  309. * Formats an javascript assignment with proper escaping of a value
  310. * and support for assigning array of strings.
  311. *
  312. * @param string $key Name of value to set
  313. * @param mixed $value Value to set, can be either string or array of strings
  314. * @param bool $escape Whether to escape value or keep it as it is
  315. * (for inclusion of js code)
  316. *
  317. * @return string Javascript code.
  318. */
  319. public static function getJsValue($key, $value, $escape = true)
  320. {
  321. $result = $key . ' = ';
  322. if (! $escape) {
  323. $result .= $value;
  324. } elseif (is_array($value)) {
  325. $result .= '[';
  326. foreach ($value as $val) {
  327. $result .= self::formatJsVal($val) . ',';
  328. }
  329. $result .= "];\n";
  330. } else {
  331. $result .= self::formatJsVal($value) . ";\n";
  332. }
  333. return $result;
  334. }
  335. /**
  336. * Removes all variables from request except allowed ones.
  337. *
  338. * @param string[] $allowList list of variables to allow
  339. *
  340. * @access public
  341. */
  342. public static function removeRequestVars(&$allowList): void
  343. {
  344. // do not check only $_REQUEST because it could have been overwritten
  345. // and use type casting because the variables could have become
  346. // strings
  347. $keys = array_keys(
  348. array_merge((array) $_REQUEST, (array) $_GET, (array) $_POST, (array) $_COOKIE)
  349. );
  350. foreach ($keys as $key) {
  351. if (! in_array($key, $allowList)) {
  352. unset($_REQUEST[$key], $_GET[$key], $_POST[$key]);
  353. continue;
  354. }
  355. // allowed stuff could be compromised so escape it
  356. // we require it to be a string
  357. if (isset($_REQUEST[$key]) && ! is_string($_REQUEST[$key])) {
  358. unset($_REQUEST[$key]);
  359. }
  360. if (isset($_POST[$key]) && ! is_string($_POST[$key])) {
  361. unset($_POST[$key]);
  362. }
  363. if (isset($_COOKIE[$key]) && ! is_string($_COOKIE[$key])) {
  364. unset($_COOKIE[$key]);
  365. }
  366. if (! isset($_GET[$key]) || is_string($_GET[$key])) {
  367. continue;
  368. }
  369. unset($_GET[$key]);
  370. }
  371. }
  372. }