Home Assistant Unofficial Reference 2024.12.1
security_filter.py
Go to the documentation of this file.
1 """Middleware to add some basic security filtering to requests."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Awaitable, Callable
6 from functools import lru_cache
7 import logging
8 import re
9 from typing import Final
10 from urllib.parse import unquote
11 
12 from aiohttp.web import Application, HTTPBadRequest, Request, StreamResponse, middleware
13 
14 from homeassistant.core import callback
15 
16 _LOGGER = logging.getLogger(__name__)
17 
18 # fmt: off
19 FILTERS: Final = re.compile(
20  r"(?:"
21 
22  # Common exploits
23  r"proc/self/environ"
24  r"|(<|%3C).*script.*(>|%3E)"
25 
26  # File Injections
27  r"|(\.\.//?)+" # ../../anywhere
28  r"|[a-zA-Z0-9_]=/([a-z0-9_.]//?)+" # .html?v=/.//test
29 
30  # SQL Injections
31  r"|union.*select.*\‍("
32  r"|union.*all.*select.*"
33  r"|concat.*\‍("
34 
35  r")",
36  flags=re.IGNORECASE,
37 )
38 # fmt: on
39 
40 # Unsafe bytes to be removed per WHATWG spec
41 UNSAFE_URL_BYTES = ["\t", "\r", "\n"]
42 
43 
44 @callback
45 def setup_security_filter(app: Application) -> None:
46  """Create security filter middleware for the app."""
47 
48  @lru_cache
49  def _recursive_unquote(value: str) -> str:
50  """Handle values that are encoded multiple times."""
51  if (unquoted := unquote(value)) != value:
52  unquoted = _recursive_unquote(unquoted)
53  return unquoted
54 
55  @middleware
56  async def security_filter_middleware(
57  request: Request, handler: Callable[[Request], Awaitable[StreamResponse]]
58  ) -> StreamResponse:
59  """Process request and block commonly known exploit attempts."""
60  path_with_query_string = f"{request.path}?{request.query_string}"
61 
62  for unsafe_byte in UNSAFE_URL_BYTES:
63  if unsafe_byte in path_with_query_string:
64  if unsafe_byte in request.query_string:
65  _LOGGER.warning(
66  "Filtered a request with unsafe byte query string: %s",
67  request.raw_path,
68  )
69  raise HTTPBadRequest
70  _LOGGER.warning(
71  "Filtered a request with an unsafe byte in path: %s",
72  request.raw_path,
73  )
74  raise HTTPBadRequest
75 
76  if FILTERS.search(_recursive_unquote(path_with_query_string)):
77  # Check the full path with query string first, if its
78  # a hit, than check just the query string to give a more
79  # specific warning.
80  if FILTERS.search(_recursive_unquote(request.query_string)):
81  _LOGGER.warning(
82  "Filtered a request with a potential harmful query string: %s",
83  request.raw_path,
84  )
85  raise HTTPBadRequest
86 
87  _LOGGER.warning(
88  "Filtered a potential harmful request to: %s", request.raw_path
89  )
90  raise HTTPBadRequest
91 
92  return await handler(request)
93 
94  app.middlewares.append(security_filter_middleware)