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
« 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)
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.
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.
20Strict transport security is enabled by default (with a TTL of one year), but without preload or include-subdomains.
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.
25X-XSS-Protection is enabled.
27X-Content-Type-Options is set to nosniff
29X-Permitted-Cross-Domain-Policies is set to "none", denying embedding resources in pdf files and the like
31Referrer-Policy is set to strict-origin, preventing leakage of URLs to 3rd-partys.
33The Permissions-Policy will only allow auto-play by default (thus access to the camera-api etc. is disabled)
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).
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"""
42from viur.core.config import conf
43from viur.core import current
44import logging
45import typing as t
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().
53 .. code-block:: python
55 # Example Usage
57 # Enable CSP for all types and made us the only allowed source
58 security.addCspRule("default-src","self","enforce")
60 # Start a new set of rules for stylesheets whitelist us
61 security.addCspRule("style-src","self","enforce")
63 # This is currently needed for TextBones!
64 security.addCspRule("style-src","unsafe-inline","enforce")
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::
69 security.addCspRule("report-uri","/cspReport","enforce")
71 and register a function at /cspReport to handle the reports.
73 ..note::
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.
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)
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
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.
152 ..Note: This function will only work on CSP-Rules in "enforce" mode, "monitor" is not suppored
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
186def enableStrictTransportSecurity(maxAge: int = 365 * 24 * 60 * 60,
187 includeSubDomains: bool = False,
188 preload: bool = False) -> None:
189 """
190 Enables HTTP strict transport security.
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"
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)
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")
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")
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
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]
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
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 ])
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
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.
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