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

102 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-07 12:23 +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 { 

86 # Fetch directives 

87 "default-src", "child-src", "connect-src", "fenced-frame-src", "font-src", "frame-src", "img-src", 

88 "manifest-src", "media-src", "object-src", "prefetch-src", "script-src", "script-src-elem", 

89 "script-src-attr", "style-src", "style-src-elem", "style-src-attr", "worker-src", 

90 # Document directives 

91 "base-uri", "sandbox", 

92 # Navigation directives 

93 "form-action", "frame-ancestors", 

94 # Reporting directives 

95 "report-uri", "report-to", 

96 # Other directives 

97 "require-trusted-types-for", "trusted-types", "upgrade-insecure-requests", "block-all-mixed-content", 

98 } 

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

100 assert not any( 

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

102 if conf.security.content_security_policy is None: 

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

104 if enforceMode not in conf.security.content_security_policy: 

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

106 if objectType == "report-uri": 

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

108 else: 

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

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

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

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

113 

114 

115def _rebuildCspHeaderCache(): 

116 """ 

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

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

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

120 """ 

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

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

123 resStr = "" 

124 if enforceMode not in conf.security.content_security_policy: 

125 continue 

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

127 resStr += key 

128 for value in values: 

129 resStr += " " 

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

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

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

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

134 else: 

135 resStr += value 

136 resStr += "; " 

137 if enforceMode == "monitor": 

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

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

140 else: 

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

142 

143 

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

145 """ 

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

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

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

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

150 configuration, while overrideRules will replace them. 

151 

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

153 

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

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

156 """ 

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

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

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

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

161 if overrideRules: # Merge overrideRules 

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

163 if v is None and k in tmpDict: 

164 del tmpDict[k] 

165 else: 

166 tmpDict[k] = v 

167 if additionalRules: # Merge the extension dict 

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

169 if k not in tmpDict: 

170 tmpDict[k] = [] 

171 tmpDict[k].extend(v) 

172 resStr = "" # Rebuild the CSP-Header 

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

174 resStr += key 

175 for value in values: 

176 resStr += " " 

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

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

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

180 else: 

181 resStr += value 

182 resStr += "; " 

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

184 

185 

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

187 includeSubDomains: bool = False, 

188 preload: bool = False) -> None: 

189 """ 

190 Enables HTTP strict transport security. 

191 

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

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

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

195 """ 

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

197 if includeSubDomains: 

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

199 if preload: 

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

201 

202 

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

204 """ 

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

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

207 :param uri: URL to whitelist 

208 """ 

209 if action == "off": 

210 conf.security.x_frame_options = None 

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

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

213 elif action == "allow-from": 

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

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

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

217 

218 

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

220 """ 

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

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

223 """ 

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

225 conf.security.x_xss_protection = enable 

226 else: 

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

228 

229 

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

231 """ 

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

233 :param enable: Enable emitting this header or not 

234 """ 

235 if enable is True or enable is False: 

236 conf.security.x_content_type_options = enable 

237 else: 

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

239 

240 

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

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

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

244 conf.security.x_permitted_cross_domain_policies = value 

245 

246 

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

248validReferrerPolicies = [ 

249 "no-referrer", 

250 "no-referrer-when-downgrade", 

251 "origin", 

252 "origin-when-cross-origin", 

253 "same-origin", 

254 "strict-origin", 

255 "strict-origin-when-cross-origin", 

256 "unsafe-url" 

257] 

258 

259 

260def 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. 

261 """ 

262 :param policy: The referrer policy to send 

263 """ 

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

265 conf.security.referrer_policy = policy 

266 

267 

268def _rebuildPermissionHeaderCache() -> None: 

269 """ 

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

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

272 """ 

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

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

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

276 ]) 

277 

278 

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

280 """ 

281 Set the permission policy. 

282 :param directive: The directive to set. 

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

284 :param allowList: 

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

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

287 """ 

288 conf.security.permissions_policy[directive] = allowList 

289 

290 

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

292 """ 

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

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

295 

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

297 - require-corp 

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

299 - same-origin 

300 - same-origin-allow-popups 

301 - unsafe-none 

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

303 - same-site 

304 - same-origin 

305 - cross-origin 

306 """ 

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

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

309 conf.security.enable_coep = bool(coep) 

310 conf.security.enable_coop = coop 

311 conf.security.enable_corp = corp