Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/securityheaders.py: 10%

102 statements  

« prev     ^ index     » next       coverage.py v7.6.3, created at 2024-10-16 22:16 +0000

1""" 

2This module provides configuration for most of the http security headers. The features currently supported are: 

3 - Content security policy (https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) 

4 - Strict transport security (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security) 

5 - X-Frame-Options (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) 

6 - X-XSS-Protection (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection) 

7 - X-Content-Type-Options (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options) 

8 - X-Permitted-Cross-Domain-Policies (https://www.adobe.com/devnet-docs/acrobatetk/tools/AppSec/xdomain.html) 

9 - Referrer-Policy (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy) 

10 - Permissions-Policy (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy) 

11 - Cross origin isolation (https://web.dev/coop-coep) 

12 

13If a feature is not yet supported, you could always set the header directly (e.g. by attaching a request 

14preprocessor). ViUR contains a default configuration for most of these headers where possible, however manual 

15review is mandatory for each project. 

16 

17The content security policy will prevent inline css and javascript by default, but is configured to allow embedding 

18images from cloud-storage and sign-in with google. 

19 

20Strict transport security is enabled by default (with a TTL of one year), but without preload or include-subdomains. 

21 

22X-Frame-Options is limited to the same origin, preventing urls from this project from being embedded in iframes that 

23don't originate from the same origin. 

24 

25X-XSS-Protection is enabled. 

26 

27X-Content-Type-Options is set to nosniff 

28 

29X-Permitted-Cross-Domain-Policies is set to "none", denying embedding resources in pdf files and the like 

30 

31Referrer-Policy is set to strict-origin, preventing leakage of URLs to 3rd-partys. 

32 

33The Permissions-Policy will only allow auto-play by default (thus access to the camera-api etc. is disabled) 

34 

35Cross origin isolation is currently disabled by default (as it's incompatible with many popular services like 

36embedding a map or sign-in with google). 

37 

38ViUR also protects it's cookies by default (setting httponly, secure and samesite=lax). This can be changed by 

39setting the corresponding class-level variables on class:`Session<viur.core.session.Session>`. 

40""" 

41 

42from viur.core.config import conf 

43from viur.core import current 

44import logging 

45import typing as t 

46 

47 

48def addCspRule(objectType: str, srcOrDirective: str, enforceMode: str = "monitor"): 

49 """ 

50 This function helps configuring and reporting of content security policy rules and violations. 

51 To enable CSP, call addCspRule() from your projects main file before calling server.setup(). 

52 

53 .. code-block:: python 

54 

55 # Example Usage 

56 

57 # Enable CSP for all types and made us the only allowed source 

58 security.addCspRule("default-src","self","enforce") 

59 

60 # Start a new set of rules for stylesheets whitelist us 

61 security.addCspRule("style-src","self","enforce") 

62 

63 # This is currently needed for TextBones! 

64 security.addCspRule("style-src","unsafe-inline","enforce") 

65 

66 If you don't want these rules to be enforced and just getting a report of violations replace "enforce" with 

67 "monitor". To add a report-url use something like:: 

68 

69 security.addCspRule("report-uri","/cspReport","enforce") 

70 

71 and register a function at /cspReport to handle the reports. 

72 

73 ..note:: 

74 

75 Our tests showed that enabling a report-url on production systems has limited use. There are literally 

76 thousands of browser-extensions out there that inject code into the pages displayed. This causes a whole 

77 flood of violations-spam to your report-url. 

78 

79 

80 :param objectType: For which type of objects should this directive be enforced? (script-src, img-src, ...) 

81 :param srcOrDirective: Either a domain which should be white-listed or a CSP-Keyword like 'self', 'unsafe-inline', etc. 

82 :param enforceMode: Should this directive be enforced or just logged? 

83 """ 

84 assert enforceMode in ["monitor", "enforce"], "enforceMode must be 'monitor' or 'enforce'!" 

85 assert objectType in {"default-src", "script-src", "object-src", "style-src", "img-src", "media-src", 

86 "frame-src", "font-src", "connect-src", "report-uri", "frame-ancestors", "child-src", 

87 "form-action", "require-trusted-types-for"} 

88 assert conf.main_app is None, "You cannot modify CSP rules after server.buildApp() has been run!" 

89 assert not any( 

90 [x in srcOrDirective for x in [";", "'", "\"", "\n", ","]]), "Invalid character in srcOrDirective!" 

91 if conf.security.content_security_policy is None: 

92 conf.security.content_security_policy = {"_headerCache": {}} 

93 if enforceMode not in conf.security.content_security_policy: 

94 conf.security.content_security_policy[enforceMode] = {} 

95 if objectType == "report-uri": 

96 conf.security.content_security_policy[enforceMode]["report-uri"] = [srcOrDirective] 

97 else: 

98 if objectType not in conf.security.content_security_policy[enforceMode]: 

99 conf.security.content_security_policy[enforceMode][objectType] = [] 

100 if srcOrDirective not in conf.security.content_security_policy[enforceMode][objectType]: 

101 conf.security.content_security_policy[enforceMode][objectType].append(srcOrDirective) 

102 

103 

104def _rebuildCspHeaderCache(): 

105 """ 

106 Rebuilds the internal conf.security.content_security_policy["_headerCache"] dictionary, ie. it constructs 

107 the Content-Security-Policy-Report-Only and Content-Security-Policy headers based on what has been passed 

108 to 'addRule' earlier on. Should not be called directly. 

109 """ 

110 conf.security.content_security_policy["_headerCache"] = {} 

111 for enforceMode in ["monitor", "enforce"]: 

112 resStr = "" 

113 if enforceMode not in conf.security.content_security_policy: 

114 continue 

115 for key, values in conf.security.content_security_policy[enforceMode].items(): 

116 resStr += key 

117 for value in values: 

118 resStr += " " 

119 if value in {"self", "unsafe-inline", "unsafe-eval", "script", "none"} or \ 

120 any([value.startswith(x) for x in ["sha256-", "sha384-", "sha512-"]]): 

121 # We don't permit nonce- in project wide config as this will be reused on multiple requests 

122 resStr += f"'{value}'" 

123 else: 

124 resStr += value 

125 resStr += "; " 

126 if enforceMode == "monitor": 

127 conf.security.content_security_policy["_headerCache"][ 

128 "Content-Security-Policy-Report-Only"] = resStr 

129 else: 

130 conf.security.content_security_policy["_headerCache"]["Content-Security-Policy"] = resStr 

131 

132 

133def extendCsp(additionalRules: dict = None, overrideRules: dict = None) -> None: 

134 """ 

135 Adds additional csp rules to the current request. ViUR will emit a default csp-header based on the 

136 project-wide config. For some requests, it's needed to extend or override these rules without having to include 

137 them in the project config. Each dictionary must be in the same format as the 

138 conf.security.content_security_policy. Values in additionalRules will extend the project-specific 

139 configuration, while overrideRules will replace them. 

140 

141 ..Note: This function will only work on CSP-Rules in "enforce" mode, "monitor" is not suppored 

142 

143 :param additionalRules: Dictionary with additional csp-rules to emit 

144 :param overrideRules: Values in this dictionary will override the corresponding default rule 

145 """ 

146 assert additionalRules or overrideRules, "Either additionalRules or overrideRules must be given!" 

147 tmpDict = {} # Copy the project-wide config in 

148 if conf.security.content_security_policy.get("enforce"): 

149 tmpDict.update({k: v[:] for k, v in conf.security.content_security_policy["enforce"].items()}) 

150 if overrideRules: # Merge overrideRules 

151 for k, v in overrideRules.items(): 

152 if v is None and k in tmpDict: 

153 del tmpDict[k] 

154 else: 

155 tmpDict[k] = v 

156 if additionalRules: # Merge the extension dict 

157 for k, v in additionalRules.items(): 

158 if k not in tmpDict: 

159 tmpDict[k] = [] 

160 tmpDict[k].extend(v) 

161 resStr = "" # Rebuild the CSP-Header 

162 for key, values in tmpDict.items(): 

163 resStr += key 

164 for value in values: 

165 resStr += " " 

166 if value in {"self", "unsafe-inline", "unsafe-eval", "script", "none"} or \ 

167 any([value.startswith(x) for x in ["nonce-", "sha256-", "sha384-", "sha512-"]]): 

168 resStr += f"'{value}'" 

169 else: 

170 resStr += value 

171 resStr += "; " 

172 current.request.get().response.headers["Content-Security-Policy"] = resStr 

173 

174 

175def enableStrictTransportSecurity(maxAge: int = 365 * 24 * 60 * 60, 

176 includeSubDomains: bool = False, 

177 preload: bool = False) -> None: 

178 """ 

179 Enables HTTP strict transport security. 

180 

181 :param maxAge: The time, in seconds, that the browser should remember that this site is only to be accessed using HTTPS. 

182 :param includeSubDomains: If this parameter is set, this rule applies to all of the site's subdomains as well. 

183 :param preload: If set, we'll issue a hint that preloading would be appreciated. 

184 """ 

185 conf.security.strict_transport_security = f"max-age={maxAge}" 

186 if includeSubDomains: 

187 conf.security.strict_transport_security += "; includeSubDomains" 

188 if preload: 

189 conf.security.strict_transport_security += "; preload" 

190 

191 

192def setXFrameOptions(action: str, uri: t.Optional[str] = None) -> None: 

193 """ 

194 Sets X-Frame-Options to prevent click-jacking attacks. 

195 :param action: off | deny | sameorigin | allow-from 

196 :param uri: URL to whitelist 

197 """ 

198 if action == "off": 

199 conf.security.x_frame_options = None 

200 elif action in ["deny", "sameorigin"]: 

201 conf.security.x_frame_options = (action, None) 

202 elif action == "allow-from": 

203 if uri is None or not (uri.lower().startswith("https://") or uri.lower().startswith("http://")): 

204 raise ValueError("If action is allow-from, an uri MUST be given and start with http(s)://") 

205 conf.security.x_frame_options = (action, uri) 

206 

207 

208def setXXssProtection(enable: t.Optional[bool]) -> None: 

209 """ 

210 Sets X-XSS-Protection header. If set, mode will always be block. 

211 :param enable: Enable the protection or not. Set to None to drop this header 

212 """ 

213 if enable is True or enable is False or enable is None: 

214 conf.security.x_xss_protection = enable 

215 else: 

216 raise ValueError("enable must be exactly one of None | True | False") 

217 

218 

219def setXContentTypeNoSniff(enable: bool) -> None: 

220 """ 

221 Sets X-Content-Type-Options if enable is true, otherwise no header is emited. 

222 :param enable: Enable emitting this header or not 

223 """ 

224 if enable is True or enable is False: 

225 conf.security.x_content_type_options = enable 

226 else: 

227 raise ValueError("enable must be one of True | False") 

228 

229 

230def setXPermittedCrossDomainPolicies(value: str) -> None: 

231 if value not in [None, "none", "master-only", "by-content-type", "all"]: 

232 raise ValueError("value [None, \"none\", \"master-only\", \"by-content-type\", \"all\"]") 

233 conf.security.x_permitted_cross_domain_policies = value 

234 

235 

236# Valid values for the referrer-header as per https://www.w3.org/TR/referrer-policy/#referrer-policies 

237validReferrerPolicies = [ 

238 "no-referrer", 

239 "no-referrer-when-downgrade", 

240 "origin", 

241 "origin-when-cross-origin", 

242 "same-origin", 

243 "strict-origin", 

244 "strict-origin-when-cross-origin", 

245 "unsafe-url" 

246] 

247 

248 

249def setReferrerPolicy(policy: str): # fixme: replace str with literal[validreferrerpolicies] when py3.8 gets supported - This is not how Literal works... We can use a Enum for this. 

250 """ 

251 :param policy: The referrer policy to send 

252 """ 

253 assert policy in validReferrerPolicies, f"Policy must be one of {validReferrerPolicies}" 

254 conf.security.referrer_policy = policy 

255 

256 

257def _rebuildPermissionHeaderCache() -> None: 

258 """ 

259 Rebuilds the internal conf.security.permissions_policy["_headerCache"] string, ie. it constructs 

260 the actual header string that's being emitted to the clients. 

261 """ 

262 conf.security.permissions_policy["_headerCache"] = ", ".join([ 

263 "%s=(%s)" % (k, " ".join([("\"%s\"" % x if x != "self" else x) for x in v])) 

264 for k, v in conf.security.permissions_policy.items() if k != "_headerCache" 

265 ]) 

266 

267 

268def setPermissionPolicyDirective(directive: str, allowList: t.Optional[list[str]]) -> None: 

269 """ 

270 Set the permission policy. 

271 :param directive: The directive to set. 

272 Must be one of https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy#directives 

273 :param allowList: 

274 The list of allowed origins. Use "self" to allow the current domain. 

275 Empty list means the feature will be disabled by the browser (it's not accessible by javascript) 

276 """ 

277 conf.security.permissions_policy[directive] = allowList 

278 

279 

280def setCrossOriginIsolation(coep: bool, coop: str, corp: str) -> None: 

281 """ 

282 Configures the cross origin isolation header that ViUR may emit. This is necessary to enable features like 

283 SharedArrayBuffer. See https://web.dev/coop-coep for more information. 

284 

285 :param coep: If set True, we'll emit Cross-Origin-Embedder-Policy: 

286 - require-corp 

287 :param coop: The value for the Cross-Origin-Opener-Policy header. Valid values are 

288 - same-origin 

289 - same-origin-allow-popups 

290 - unsafe-none 

291 :param corp: The value for the Cross-Origin-Resource-Policy header. Valid values are 

292 - same-site 

293 - same-origin 

294 - cross-origin 

295 """ 

296 assert coop in ["same-origin", "same-origin-allow-popups", "unsafe-none"], "Invalid value for the COOP Header" 

297 assert corp in ["same-site", "same-origin", "cross-origin"], "Invalid value for the CORP Header" 

298 conf.security.enable_coep = bool(coep) 

299 conf.security.enable_coop = coop 

300 conf.security.enable_corp = corp