Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Added option in Flask Talisman to add Adobe Typekit CSP rules with `allow_typekit_content_security_policy=True`
- Allow `Access-Control-Allow-Origin` header to be set in `Talisman` with `allow_cors_origin`

### Changed

Expand Down
38 changes: 29 additions & 9 deletions tests/test_flask_talisman.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ def test_naked_app(self):
self.assertNotIn("Cross-Origin-Opener-Policy", rv.headers)
self.assertNotIn("Cross-Origin-Resource-Policy", rv.headers)

self.assertNotIn("Referrer-Policy", rv.headers)
self.assertNotIn("Access-Control-Allow-Origin", rv.headers)

self.assertIn("Set-Cookie", rv.headers)
self.assertIn("session=", rv.headers["Set-Cookie"])
self.assertNotIn("Secure", rv.headers["Set-Cookie"])
Expand Down Expand Up @@ -59,6 +62,11 @@ def test_default_talisman_app(self):
self.assertIn("Cross-Origin-Resource-Policy", rv.headers)
self.assertEqual("same-origin", rv.headers["Cross-Origin-Resource-Policy"])

self.assertIn("Referrer-Policy", rv.headers)
self.assertEqual(
"strict-origin-when-cross-origin", rv.headers["Referrer-Policy"]
)

self.assertIn("Set-Cookie", rv.headers)
self.assertIn("session=", rv.headers["Set-Cookie"])
self.assertNotIn(" Secure", rv.headers["Set-Cookie"]) # force_https is False
Expand Down Expand Up @@ -89,27 +97,27 @@ def test_talisman_google_csp(self):
self.assertIn("Content-Security-Policy", rv.headers)
self.assertIn("default-src 'self';", rv.headers["Content-Security-Policy"])
self.assertIn(
"connect-src 'self' *.google-analytics.com www.googletagmanager.com;",
"connect-src 'self' *.google-analytics.com *.googletagmanager.com *.analytics.google.com;",
rv.headers["Content-Security-Policy"],
)
self.assertIn(
"frame-src 'self' www.google.com www.youtube.com www.youtube-nocookie.com;",
rv.headers["Content-Security-Policy"],
)
self.assertIn(
"font-src 'self' *.gstatic.com;",
"font-src 'self' *.gstatic.com data:;",
rv.headers["Content-Security-Policy"],
)
self.assertIn(
"img-src 'self' img.youtube.com i.ytimg.com www.googletagmanager.com;",
"img-src 'self' img.youtube.com i.ytimg.com *.googletagmanager.com ssl.gstatic.com www.gstatic.com *.google-analytics.com;",
rv.headers["Content-Security-Policy"],
)
self.assertIn(
"script-src 'self' ajax.googleapis.com *.googleanalytics.com *.google-analytics.com www.youtube.com *.gstatic.com www.googletagmanager.com;",
"script-src 'self' ajax.googleapis.com *.googleanalytics.com *.google-analytics.com www.youtube.com *.gstatic.com *.googletagmanager.com tagmanager.google.com;",
rv.headers["Content-Security-Policy"],
)
self.assertIn(
"style-src 'self' ajax.googleapis.com fonts.googleapis.com *.gstatic.com;",
"style-src 'self' ajax.googleapis.com fonts.googleapis.com *.gstatic.com googletagmanager.com tagmanager.google.com;",
rv.headers["Content-Security-Policy"],
)

Expand Down Expand Up @@ -144,15 +152,15 @@ def test_talisman_google_and_typekit_csp(self):

self.assertIn("Content-Security-Policy", rv.headers)
self.assertIn(
"font-src 'self' *.gstatic.com use.typekit.net;",
"font-src 'self' *.gstatic.com data: use.typekit.net;",
rv.headers["Content-Security-Policy"],
)
self.assertIn(
"style-src 'self' ajax.googleapis.com fonts.googleapis.com *.gstatic.com *.typekit.net;",
"style-src 'self' ajax.googleapis.com fonts.googleapis.com *.gstatic.com googletagmanager.com tagmanager.google.com *.typekit.net;",
rv.headers["Content-Security-Policy"],
)
self.assertIn(
"img-src 'self' img.youtube.com i.ytimg.com www.googletagmanager.com;",
"img-src 'self' img.youtube.com i.ytimg.com *.googletagmanager.com ssl.gstatic.com www.gstatic.com *.google-analytics.com;",
rv.headers["Content-Security-Policy"],
)

Expand Down Expand Up @@ -201,7 +209,7 @@ def test_talisman_custom_csp_with_google(self):
rv.headers["Content-Security-Policy"],
)
self.assertIn(
"img-src 'self' img.example.com img.youtube.com i.ytimg.com www.googletagmanager.com;",
"img-src 'self' img.example.com img.youtube.com i.ytimg.com *.googletagmanager.com ssl.gstatic.com www.gstatic.com *.google-analytics.com;",
rv.headers["Content-Security-Policy"],
)

Expand Down Expand Up @@ -231,3 +239,15 @@ def test_talisman_force_https_permanent(self):
"https://localhost/foobar?test=1",
rv.headers["Location"],
)

def test_talisman_allow_cors_origin(self):
Talisman(self.app, force_https=False, allow_cors_origin="https://example.com")

rv = self.test_client.get("/")

self.assertEqual(rv.status_code, 200)

self.assertIn("Access-Control-Allow-Origin", rv.headers)
self.assertEqual(
"https://example.com", rv.headers["Access-Control-Allow-Origin"]
)
23 changes: 19 additions & 4 deletions tna_utilities/flask/talisman.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
from ..security import CspGenerator, common_security_headers

GOOGLE_CSP_DIRECTIVES = {
"font-src": ["*.gstatic.com"], # Fonts from fonts.google.com
"font-src": [
"*.gstatic.com", # Fonts from fonts.google.com
"data:", # Fonts for GTM preview mode
],
"frame-src": [
"www.google.com", # <iframe> based embeds for Google Maps
"www.youtube.com", # <iframe> based embeds for Youtube
Expand All @@ -14,24 +17,31 @@
"img-src": [
"img.youtube.com", # YouTube video thumbnails
"i.ytimg.com", # YouTube video thumbnails
"www.googletagmanager.com", # GTM
"*.googletagmanager.com", # GTM
"ssl.gstatic.com", # GTM preview mode
"www.gstatic.com", # GTM preview mode
"*.google-analytics.com", # GA4
],
"script-src": [
"ajax.googleapis.com", # Assorted Google-hosted Libraries/APIs
"*.googleanalytics.com", # GA4
"*.google-analytics.com", # GA4
"www.youtube.com", # YouTube embeds
"*.gstatic.com", # Google Translate
"www.googletagmanager.com", # GTM
"*.googletagmanager.com", # GTM preview mode
"tagmanager.google.com", # GTM preview mode
],
"style-src": [
"ajax.googleapis.com", # YouTube embedded player styles
"fonts.googleapis.com", # Google Fonts stylesheets
"*.gstatic.com", # Assorted Google stylesheets
"googletagmanager.com", # GTM preview mode
"tagmanager.google.com", # GTM preview mode
],
"connect-src": [
"*.google-analytics.com", # GA4
"www.googletagmanager.com", # GTM
"*.googletagmanager.com", # GTM preview mode
"*.analytics.google.com", # GA4
],
}

Expand Down Expand Up @@ -66,6 +76,7 @@ def init_app(
referrer_policy: str = "strict-origin-when-cross-origin",
force_https: bool = True,
force_https_permanent: bool = False,
allow_cors_origin: str | None = None,
):
"""
Initialises the Talisman extension for the Flask app.
Expand All @@ -77,6 +88,7 @@ def init_app(
:param referrer_policy: The Referrer-Policy header value to apply to responses. Defaults to "strict-origin-when-cross-origin".
:param force_https: If True, forces incoming requests to be redirected to HTTPS if they are not already secure and the application is not in debug mode. Defaults to True.
:param force_https_permanent: If True, uses a permanent redirect (HTTP 301) when forcing HTTPS, otherwise uses a temporary redirect (HTTP 302). Defaults to False.
:param allow_cors_origin: If specified, sets the Access-Control-Allow-Origin header to the given value. Defaults to None.
"""

content_security_policy = content_security_policy or {}
Expand All @@ -100,6 +112,7 @@ def init_app(
self.referrer_policy = referrer_policy
self.force_https = force_https
self.force_https_permanent = force_https_permanent
self.allow_cors_origin = allow_cors_origin

self.app.before_request(self._force_https_redirect)
self.app.after_request(self._apply_extra_headers)
Expand Down Expand Up @@ -148,6 +161,8 @@ def _apply_extra_headers(self, response):
)
response.headers.update(common_security_headers(**self.security_headers))
response.headers["Referrer-Policy"] = self.referrer_policy
if self.allow_cors_origin:
response.headers["Access-Control-Allow-Origin"] = self.allow_cors_origin
return response

def _csp(
Expand Down
Loading