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

1"""WSGI middleware classes for web-layer security. 

2 

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""" 

7 

8from loguru import logger 

9 

10from .network_utils import is_private_ip 

11 

12 

13class SecureCookieMiddleware: 

14 """WSGI middleware that adds the Secure flag to Set-Cookie iff the 

15 request is HTTPS. 

16 

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. 

23 

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 """ 

28 

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 

33 

34 def __call__(self, environ, start_response): 

35 self._maybe_warn_insecure_public(environ) 

36 should_add_secure = self._should_add_secure_flag(environ) 

37 

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) 

48 

49 return self.wsgi_app(environ, custom_start_response) 

50 

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" 

55 

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 ) 

72 

73 

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 """ 

79 

80 def __init__(self, wsgi_app): 

81 self.wsgi_app = wsgi_app 

82 

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) 

91 

92 return self.wsgi_app(environ, custom_start_response)