http_sec_fetch.py 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. # SPDX-License-Identifier: AGPL-3.0-or-later
  2. """
  3. Method ``http_sec_fetch``
  4. -------------------------
  5. The ``http_sec_fetch`` method protect resources from web attacks with `Fetch
  6. Metadata`_. A request is filtered out in case of:
  7. - http header Sec-Fetch-Mode_ is invalid
  8. - http header Sec-Fetch-Dest_ is invalid
  9. .. _Fetch Metadata:
  10. https://developer.mozilla.org/en-US/docs/Glossary/Fetch_metadata_request_header
  11. .. Sec-Fetch-Dest:
  12. https://developer.mozilla.org/en-US/docs/Web/API/Request/destination
  13. .. Sec-Fetch-Mode:
  14. https://developer.mozilla.org/en-US/docs/Web/API/Request/mode
  15. """
  16. # pylint: disable=unused-argument
  17. from __future__ import annotations
  18. from ipaddress import (
  19. IPv4Network,
  20. IPv6Network,
  21. )
  22. import re
  23. import flask
  24. import werkzeug
  25. from searx.extended_types import SXNG_Request
  26. from . import config
  27. from ._helpers import logger
  28. def is_browser_supported(user_agent: str) -> bool:
  29. """Check if the browser supports Sec-Fetch headers.
  30. https://caniuse.com/mdn-http_headers_sec-fetch-dest
  31. https://caniuse.com/mdn-http_headers_sec-fetch-mode
  32. https://caniuse.com/mdn-http_headers_sec-fetch-site
  33. Supported browsers:
  34. - Chrome >= 80
  35. - Firefox >= 90
  36. - Safari >= 16.4
  37. - Edge (mirrors Chrome)
  38. - Opera (mirrors Chrome)
  39. """
  40. user_agent = user_agent.lower()
  41. # Chrome/Chromium/Edge/Opera
  42. chrome_match = re.search(r'chrome/(\d+)', user_agent)
  43. if chrome_match:
  44. version = int(chrome_match.group(1))
  45. return version >= 80
  46. # Firefox
  47. firefox_match = re.search(r'firefox/(\d+)', user_agent)
  48. if firefox_match:
  49. version = int(firefox_match.group(1))
  50. return version >= 90
  51. # Safari
  52. safari_match = re.search(r'version/(\d+)\.(\d+)', user_agent)
  53. if safari_match:
  54. major = int(safari_match.group(1))
  55. minor = int(safari_match.group(2))
  56. return major > 16 or (major == 16 and minor >= 4)
  57. return False
  58. def filter_request(
  59. network: IPv4Network | IPv6Network,
  60. request: SXNG_Request,
  61. cfg: config.Config,
  62. ) -> werkzeug.Response | None:
  63. # Only check Sec-Fetch headers for supported browsers
  64. user_agent = request.headers.get('User-Agent', '')
  65. if is_browser_supported(user_agent):
  66. val = request.headers.get("Sec-Fetch-Mode", "")
  67. if val != "navigate":
  68. logger.debug("invalid Sec-Fetch-Mode '%s'", val)
  69. return flask.redirect(flask.url_for('index'), code=302)
  70. val = request.headers.get("Sec-Fetch-Site", "")
  71. if val not in ('same-origin', 'same-site', 'none'):
  72. logger.debug("invalid Sec-Fetch-Site '%s'", val)
  73. flask.redirect(flask.url_for('index'), code=302)
  74. val = request.headers.get("Sec-Fetch-Dest", "")
  75. if val != "document":
  76. logger.debug("invalid Sec-Fetch-Dest '%s'", val)
  77. flask.redirect(flask.url_for('index'), code=302)
  78. return None