Coverage for src/local_deep_research/security/web_middleware.py: 100%
45 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-03 23:15 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-03 23:15 +0000
1"""WSGI middleware classes for web-layer security.
3These wrap the Flask WSGI app to enforce cookie security and strip
4information-disclosure headers. Wired into the middleware stack by
5``local_deep_research.web.app_factory.create_app``.
6"""
8from loguru import logger
10from .network_utils import is_private_ip
13class SecureCookieMiddleware:
14 """WSGI middleware that adds the Secure flag to Set-Cookie iff the
15 request is HTTPS.
17 The Secure flag tells the browser to send the cookie only over HTTPS.
18 Setting it on a response served over HTTP causes the browser to drop
19 the cookie entirely, so it must only be added when the user's
20 connection is actually HTTPS. ``ProxyFix`` (with ``x_proto=1``)
21 translates ``X-Forwarded-Proto`` into ``wsgi.url_scheme`` before this
22 middleware runs.
24 Side effect: logs a one-shot warning when the app serves HTTP to a
25 non-private end-user IP, signalling a likely missing HTTPS proxy
26 configuration. Skipped in ``LDR_TESTING_MODE``.
27 """
29 def __init__(self, wsgi_app, flask_app):
30 self.wsgi_app = wsgi_app
31 self.flask_app = flask_app
32 self._warned_insecure_public = False
34 def __call__(self, environ, start_response):
35 self._maybe_warn_insecure_public(environ)
36 should_add_secure = self._should_add_secure_flag(environ)
38 def custom_start_response(status, headers, exc_info=None):
39 if should_add_secure:
40 new_headers = []
41 for name, value in headers:
42 if name.lower() == "set-cookie":
43 if "; Secure" not in value and "; secure" not in value:
44 value = value + "; Secure"
45 new_headers.append((name, value))
46 headers = new_headers
47 return start_response(status, headers, exc_info)
49 return self.wsgi_app(environ, custom_start_response)
51 def _should_add_secure_flag(self, environ):
52 if self.flask_app.config.get("LDR_TESTING_MODE"):
53 return False
54 return environ.get("wsgi.url_scheme") == "https"
56 def _maybe_warn_insecure_public(self, environ):
57 if self._warned_insecure_public:
58 return
59 if self.flask_app.config.get("LDR_TESTING_MODE"):
60 return
61 if environ.get("wsgi.url_scheme") == "https":
62 return
63 remote_addr = environ.get("REMOTE_ADDR", "")
64 if is_private_ip(remote_addr):
65 return
66 self._warned_insecure_public = True
67 logger.warning(
68 f"Serving HTTP to non-private client {remote_addr}. "
69 f"Session cookies will be sent in plaintext. Configure HTTPS "
70 f"at the reverse proxy and ensure X-Forwarded-Proto is set."
71 )
74class ServerHeaderMiddleware:
75 """WSGI middleware that strips the Server header from responses to
76 prevent information disclosure about the underlying web server.
77 Applied as the outermost wrapper.
78 """
80 def __init__(self, wsgi_app):
81 self.wsgi_app = wsgi_app
83 def __call__(self, environ, start_response):
84 def custom_start_response(status, headers, exc_info=None):
85 filtered_headers = [
86 (name, value)
87 for name, value in headers
88 if name.lower() != "server"
89 ]
90 return start_response(status, filtered_headers, exc_info)
92 return self.wsgi_app(environ, custom_start_response)