From 9de8409f484c6a500c18fdcef95023fbeee23507 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Jan 2024 19:17:18 -1000 Subject: [PATCH] Speed up security filter middleware (#108703) * Speed up security filter middleware Check the path and query string with the filter expression once instead of checking the path and query string seperately. If we get a hit than we check the query string to ensure we give a more verbose error about where the filter hit. Additionally since we see the same urls over and over, cache the unquote * request.url is to expensive, cheaper to join * aiohttp has a path_qs fast path * construct the string outselves so it functions exactly as before --- .../components/http/security_filter.py | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/http/security_filter.py b/homeassistant/components/http/security_filter.py index e8e3aa4699c..4d71334f1cf 100644 --- a/homeassistant/components/http/security_filter.py +++ b/homeassistant/components/http/security_filter.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable +from functools import lru_cache import logging import re from typing import Final @@ -43,6 +44,7 @@ UNSAFE_URL_BYTES = ["\t", "\r", "\n"] def setup_security_filter(app: Application) -> None: """Create security filter middleware for the app.""" + @lru_cache def _recursive_unquote(value: str) -> str: """Handle values that are encoded multiple times.""" if (unquoted := unquote(value)) != value: @@ -54,34 +56,38 @@ def setup_security_filter(app: Application) -> None: request: Request, handler: Callable[[Request], Awaitable[StreamResponse]] ) -> StreamResponse: """Process request and block commonly known exploit attempts.""" + path_with_query_string = f"{request.path}?{request.query_string}" + for unsafe_byte in UNSAFE_URL_BYTES: - if unsafe_byte in request.path: + if unsafe_byte in path_with_query_string: + if unsafe_byte in request.query_string: + _LOGGER.warning( + "Filtered a request with unsafe byte query string: %s", + request.raw_path, + ) + raise HTTPBadRequest _LOGGER.warning( "Filtered a request with an unsafe byte in path: %s", request.raw_path, ) raise HTTPBadRequest - if unsafe_byte in request.query_string: + if FILTERS.search(_recursive_unquote(path_with_query_string)): + # Check the full path with query string first, if its + # a hit, than check just the query string to give a more + # specific warning. + if FILTERS.search(_recursive_unquote(request.query_string)): _LOGGER.warning( - "Filtered a request with unsafe byte query string: %s", + "Filtered a request with a potential harmful query string: %s", request.raw_path, ) raise HTTPBadRequest - if FILTERS.search(_recursive_unquote(request.path)): _LOGGER.warning( "Filtered a potential harmful request to: %s", request.raw_path ) raise HTTPBadRequest - if FILTERS.search(_recursive_unquote(request.query_string)): - _LOGGER.warning( - "Filtered a request with a potential harmful query string: %s", - request.raw_path, - ) - raise HTTPBadRequest - return await handler(request) app.middlewares.append(security_filter_middleware)