Sanitize.php 14 KB

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