Coverage for src/local_deep_research/settings/env_definitions/security.py: 100%

4 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-22 21:20 +0000

1""" 

2Security environment settings. 

3 

4These settings control security-related behavior like SSRF validation 

5and CORS origin restrictions. 

6""" 

7 

8import os 

9from ..env_settings import BooleanSetting, StringSetting 

10 

11 

12# External environment variables (set by pytest, CI systems) 

13# These are read directly since we don't control them 

14PYTEST_CURRENT_TEST = os.environ.get("PYTEST_CURRENT_TEST") 

15 

16 

17# LDR Security settings (our application's security configuration) 

18SECURITY_SETTINGS = [ 

19 StringSetting( 

20 key="security.cors.allowed_origins", 

21 description=( 

22 "Allowed CORS origins for API routes (comma-separated). " 

23 "Use '*' for all origins, empty for same-origin only. " 

24 "Example: 'https://example.com,https://app.example.com'" 

25 ), 

26 default=None, 

27 ), 

28 StringSetting( 

29 key="security.websocket.allowed_origins", 

30 description=( 

31 "Allowed origins for WebSocket/Socket.IO connections (comma-separated). " 

32 "Unset or empty means same-origin only (default); use '*' to allow all origins. " 

33 "Example: 'https://example.com,https://app.example.com'" 

34 ), 

35 default=None, 

36 ), 

37 BooleanSetting( 

38 key="notifications.allow_private_ips", 

39 description=( 

40 "Allow notification webhooks to target private/local IP addresses. " 

41 "Environment-only to prevent SSRF bypass via the user-writable settings API. " 

42 "Only enable this if your notification endpoints are on a trusted local network." 

43 ), 

44 default=False, 

45 ), 

46 BooleanSetting( 

47 key="security.allow_nat64", 

48 description=( 

49 "Allow outbound traffic to NAT64 prefixes (64:ff9b::/96 RFC 6052 " 

50 "well-known and 64:ff9b:1::/48 RFC 8215 local-use). Disabled by " 

51 "default to close the IPv6-wrapped SSRF bypass class — on hosts " 

52 "configured with NAT64 routes, attacker-supplied URLs can wrap " 

53 "cloud-metadata or RFC1918 destinations through these prefixes. " 

54 "Enable only on IPv6-only deployments (DNS64+NAT64) where " 

55 "outbound IPv4 traffic is synthesized through this prefix and " 

56 "the operator has accepted the residual SSRF risk. 6to4 " 

57 "(2002::/16), Teredo (2001::/32), the discard prefix " 

58 "(100::/64), the deprecated IPv4-Compatible IPv6 form " 

59 "(::/96, except ::1 loopback which follows the loopback/" 

60 "private flags), and the IPv4-Translated/SIIT form " 

61 "(::ffff:0:0:0/96) remain unconditionally blocked because " 

62 "they have no live legitimate use in 2026. The cloud-metadata block " 

63 "(ALWAYS_BLOCKED_METADATA_IPS) still applies via embedded-IPv4 " 

64 "extraction — see SECURITY.md." 

65 ), 

66 default=False, 

67 ), 

68 BooleanSetting( 

69 key="notifications.allow_outbound", 

70 description=( 

71 "Master switch for outbound notification webhooks (Apprise). " 

72 "Disabled by default so outbound delivery requires an explicit " 

73 "operator decision. The send path uses a synchronous DNS guard " 

74 "against rebinding and private/metadata access; see SECURITY.md " 

75 "'Notification Webhook SSRF' for details. Distinct from the per-user " 

76 "notifications.enabled toggle in the settings UI: this is the " 

77 "server-level operator gate, env-only so it cannot be flipped via " 

78 "the user-writable settings API." 

79 ), 

80 default=False, 

81 ), 

82 BooleanSetting( 

83 key="policy.allow_unprotected_egress", 

84 description=( 

85 "Allow users to select the UNPROTECTED egress scope. Disabled by " 

86 "default; this is an environment-only operator gate and cannot be " 

87 "changed through the user-writable settings API. Hard SSRF and " 

88 "cloud-metadata protections remain active when enabled." 

89 ), 

90 default=False, 

91 ), 

92 BooleanSetting( 

93 key="search.allow_private_engine_urls", 

94 description=( 

95 "Allow user-editable PUBLIC search-engine URLs (e.g. the SearXNG " 

96 "instance_url) to point at private / loopback / link-local " 

97 "addresses. Environment-only operator gate so it cannot be flipped " 

98 "through the user-writable settings API. Disabled by default: a " 

99 "public-nature engine (its data source is the public internet) " 

100 "pointed at an internal host lets any authenticated user turn a " 

101 "research run into an internal port scan / service probe, breaking " 

102 "the PUBLIC_ONLY egress promise. Enable only when you self-host " 

103 "SearXNG on localhost/LAN and accept that engine fetches may reach " 

104 "your private network. To approve only specific origins instead " 

105 "of all private addresses, use the finer-grained " 

106 "LDR_SEARCH_PRIVATE_ENGINE_URL_ALLOWLIST. " 

107 "Docker deployments instead pin the URL " 

108 "directly via LDR_SEARCH_ENGINE_WEB_SEARXNG_DEFAULT_PARAMS_" 

109 "INSTANCE_URL, which is trusted as operator-provisioned. " 

110 "Cloud-metadata endpoints (ALWAYS_BLOCKED_METADATA_IPS) stay " 

111 "blocked regardless of this flag." 

112 ), 

113 default=False, 

114 ), 

115 StringSetting( 

116 key="search.private_engine_url_allowlist", 

117 description=( 

118 "Comma-separated list of exact URL origins " 

119 "(scheme://host[:port]) that user-editable PUBLIC search-engine " 

120 "URLs (e.g. the SearXNG instance_url) may use even though they " 

121 "are private / loopback / link-local — e.g. " 

122 "'http://localhost:8080,http://192.168.1.5:8888'. Finer-grained " 

123 "alternative to LDR_SEARCH_ALLOW_PRIVATE_ENGINE_URLS=true: " 

124 "private egress is enabled for an engine only when its " 

125 "configured URL matches a listed origin (the engine's request " 

126 "chain, including redirects, then runs with private access — " 

127 "like the blanket flag, but conditional on the match), rather " 

128 "than for any private URL. Matching is exact scheme + host + port " 

129 "(default ports 80/443 implied, host case-insensitive, " 

130 "path/query ignored); no wildcards or CIDR ranges. Entries that " 

131 "fail to parse are ignored. Environment-only operator setting " 

132 "so it cannot be edited through the user-writable settings API. " 

133 "Cloud-metadata endpoints (ALWAYS_BLOCKED_METADATA_IPS) and " 

134 "non-http(s) schemes stay blocked even if listed." 

135 ), 

136 default=None, 

137 ), 

138 BooleanSetting( 

139 key="search.allow_private_result_fetch", 

140 description=( 

141 "Allow full-content fetching (search.snippets_only off) to " 

142 "follow the RESULT URLs a PUBLIC search engine returns into " 

143 "private / " 

144 "loopback addresses, e.g. an internal wiki that a self-hosted " 

145 "SearXNG instance indexes. Distinct from, and required in " 

146 "addition to, the instance-URL approval " 

147 "(LDR_SEARCH_ALLOW_PRIVATE_ENGINE_URLS, " 

148 "LDR_SEARCH_PRIVATE_ENGINE_URL_ALLOWLIST or an env-locked " 

149 "instance URL): that approval only covers reaching the engine " 

150 "itself, and because a public engine proxies the public web, " 

151 "extending it to the URLs the engine returns would let a " 

152 "poisoned index turn result fetching into a scan of the LDR " 

153 "host's private network. Environment-only operator gate so it " 

154 "cannot be flipped through the user-writable settings API. " 

155 "Disabled by default and not set by the bundled " 

156 "docker-compose.yml. Link-local addresses (169.254.0.0/16, " 

157 "fe80::/10) and the cloud-metadata endpoints in " 

158 "ALWAYS_BLOCKED_METADATA_IPS stay blocked regardless of this " 

159 "flag. Takes effect only under the unprotected egress scope: " 

160 "under every other scope, the default adaptive scope included, " 

161 "private result URLs stay blocked even with this flag set." 

162 ), 

163 default=False, 

164 ), 

165 BooleanSetting( 

166 key="research_library.allow_filesystem_pdf_storage", 

167 description=( 

168 "Allow users to select the UNENCRYPTED 'filesystem' PDF storage " 

169 "mode for library-downloaded PDFs. Disabled by default; this is " 

170 "an environment-only operator gate and cannot be changed through " 

171 "the user-writable settings API. Filesystem mode writes fetched " 

172 "third-party PDFs as PLAINTEXT to a shared library directory " 

173 "(cleartext storage of sensitive information, CWE-312). When " 

174 "off, the 'filesystem' option is withheld from the settings UI " 

175 "and any stored or " 

176 "environment value of 'filesystem' is coerced to the encrypted " 

177 "'database' mode at write time. Previously-written plaintext " 

178 "files remain readable. Enable only when the library directory " 

179 "lives on an operator-controlled encrypted volume." 

180 ), 

181 default=False, 

182 ), 

183 BooleanSetting( 

184 key="research_library.allow_shared_library", 

185 description=( 

186 "Allow shared-library mode, which drops the per-user library " 

187 "directory boundary so all users' downloaded PDFs live in one " 

188 "shared directory. Disabled by default; this is an " 

189 "environment-only operator gate and cannot be enabled through the " 

190 "user-writable settings API. The shared_library flag is otherwise " 

191 "user-editable; leaving shared mode user-toggleable would let an " 

192 "account drop its per-user directory boundary. The storage_path " 

193 "is now operator-only, but legacy saved paths remain in place. " 

194 "When off, the per-user subdirectory is always " 

195 "enforced regardless of the user's shared_library setting. Enable " 

196 "only on single-tenant or mutually-trusted deployments." 

197 ), 

198 default=False, 

199 ), 

200]