Skip to content
Back to top

# Source code for searx.botdetection.http_sec_fetch

    # SPDX-License-Identifier: AGPL-3.0-or-later
    """
    Method ``http_sec_fetch``
    -------------------------

    The ``http_sec_fetch`` method protect resources from web attacks with `Fetch
    Metadata`_.  A request is filtered out in case of:

    - http header Sec-Fetch-Mode_ is invalid
    - http header Sec-Fetch-Dest_ is invalid

    .. _Fetch Metadata:
       https://developer.mozilla.org/en-US/docs/Glossary/Fetch_metadata_request_header

    .. _Sec-Fetch-Dest:
       https://developer.mozilla.org/en-US/docs/Web/API/Request/destination

    .. _Sec-Fetch-Mode:
       https://developer.mozilla.org/en-US/docs/Web/API/Request/mode

    """
    # pylint: disable=unused-argument

    from ipaddress import (
        IPv4Network,
        IPv6Network,
    )

    import re
    import flask
    import werkzeug

    from . import config
    from ._helpers import logger

    def is_browser_supported(user_agent: str) -> bool:
        """Check if the browser supports Sec-Fetch headers.

        https://caniuse.com/mdn-http_headers_sec-fetch-dest
        https://caniuse.com/mdn-http_headers_sec-fetch-mode
        https://caniuse.com/mdn-http_headers_sec-fetch-site

        Supported browsers:
        - Chrome >= 80
        - Firefox >= 90
        - Safari >= 16.4
        - Edge (mirrors Chrome)
        - Opera (mirrors Chrome)
        """
        user_agent = user_agent.lower()

        # Chrome/Chromium/Edge/Opera
        chrome_match = re.search(r'chrome/(\d+)', user_agent)
        if chrome_match:
            version = int(chrome_match.group(1))
            return version >= 80

        # Firefox
        firefox_match = re.search(r'firefox/(\d+)', user_agent)
        if firefox_match:
            version = int(firefox_match.group(1))
            return version >= 90

        # Safari
        safari_match = re.search(r'version/(\d+)\.(\d+)', user_agent)
        if safari_match:
            major = int(safari_match.group(1))
            minor = int(safari_match.group(2))
            return major > 16 or (major == 16 and minor >= 4)

        return False

    def filter_request(
        network: IPv4Network | IPv6Network,
        request: flask.Request,
        cfg: config.Config,
    ) -> werkzeug.Response | None:

        if not request.is_secure:
            logger.warning(
                "Sec-Fetch cannot be verified for non-secure requests (HTTP headers are not set/sent by the client)."
            )
            return None

        # Only check Sec-Fetch headers for supported browsers
        user_agent = request.headers.get('User-Agent', '')
        if is_browser_supported(user_agent):
            val = request.headers.get("Sec-Fetch-Mode", "")
            if val not in ('navigate', 'cors'):
                logger.debug("invalid Sec-Fetch-Mode '%s'", val)
                return flask.redirect(flask.url_for('index'), code=302)

            val = request.headers.get("Sec-Fetch-Site", "")
            if val not in ('same-origin', 'same-site', 'none'):
                logger.debug("invalid Sec-Fetch-Site '%s'", val)
                flask.redirect(flask.url_for('index'), code=302)

            val = request.headers.get("Sec-Fetch-Dest", "")
            if val not in ('document', 'empty'):
                logger.debug("invalid Sec-Fetch-Dest '%s'", val)
                flask.redirect(flask.url_for('index'), code=302)

        return None