flag.php 2.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. <?php
  2. /**
  3. * Sends the flag for the logged-in student's open flags question.
  4. *
  5. * The question page shows <img src="flag.php?r=...">. Linking the file
  6. * directly (resources/flags/AUS.svg) would put the answer's ISO code in the
  7. * page for anyone who looks at the image address. This looks the flag up
  8. * from the question stored for the student (TkAnswerStore / tkPendingAnswer)
  9. * and sends it without saying which file it was.
  10. *
  11. * Only a logged-in user gets anything, and only the flag of their own open
  12. * question: the file name comes from the stored question, never from the URL,
  13. * and has to match a strict pattern and exist in resources/flags.
  14. *
  15. * SVGs are cleaned of comments and <title>/<desc>/<script> before they are
  16. * sent (a title can name the country), and are sent with a content security
  17. * policy that stops scripts if someone opens the address directly.
  18. */
  19. if (!defined('e107_INIT')) {
  20. require_once('../../class2.php');
  21. }
  22. if (!e107::isInstalled('timekeeper') || !USER) {
  23. http_response_code(403);
  24. exit;
  25. }
  26. $db = e107::getDb();
  27. $row = $db->retrieve('tkPendingAnswer', 'payload', "username = '" . $db->escape(e107::getUser()->getLoginName()) . "'");
  28. if (is_array($row)) {
  29. $row = isset($row['payload']) ? $row['payload'] : reset($row);
  30. }
  31. $payload = is_string($row) ? json_decode($row, true) : null;
  32. $file = (is_array($payload) && isset($payload['flag'])) ? (string) $payload['flag'] : '';
  33. if (!preg_match('/^[A-Za-z]{3}\.(svg|png)$/', $file)) {
  34. http_response_code(404);
  35. exit;
  36. }
  37. $path = __DIR__ . '/resources/flags/' . $file;
  38. if (!is_file($path)) {
  39. http_response_code(404);
  40. exit;
  41. }
  42. while (ob_get_level() > 0) {
  43. ob_end_clean();
  44. }
  45. header('Cache-Control: private, no-store');
  46. header('X-Content-Type-Options: nosniff');
  47. if (substr($file, -4) === '.svg') {
  48. $svg = (string) file_get_contents($path);
  49. foreach (['#<!--.*?-->#s', '#<title\b.*?</title>#is', '#<desc\b.*?</desc>#is', '#<script\b.*?</script>#is'] as $pattern) {
  50. $clean = preg_replace($pattern, '', $svg);
  51. if ($clean !== null) {
  52. $svg = $clean;
  53. }
  54. }
  55. header('Content-Type: image/svg+xml');
  56. header("Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox");
  57. header('Content-Length: ' . strlen($svg));
  58. echo $svg;
  59. } else {
  60. header('Content-Type: image/png');
  61. header('Content-Length: ' . filesize($path));
  62. readfile($path);
  63. }
  64. exit;