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
3 changes: 3 additions & 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`
- Added `extra_headers` parameter in `Talisman` to update or add any global response headers

### Changed

Expand All @@ -19,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Removed

- Removed `security_headers` parameter in `Talisman`

### Fixed

- Fixed logic inversion for `default_headers` in `SimpleJsonApiClient`
Expand Down
64 changes: 47 additions & 17 deletions tests/test_flask_talisman.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ def test_naked_app(self):
self.assertNotIn("SameSite", rv.headers["Set-Cookie"])

def test_default_talisman_app(self):
Talisman(self.app, force_https=False)
Talisman(self.app)

rv = self.test_client.get("/")
rv = self.test_client.get("https://localhost/")

self.assertEqual(rv.status_code, 200)

Expand All @@ -69,15 +69,14 @@ def test_default_talisman_app(self):

self.assertIn("Set-Cookie", rv.headers)
self.assertIn("session=", rv.headers["Set-Cookie"])
self.assertNotIn(" Secure", rv.headers["Set-Cookie"]) # force_https is False
self.assertIn(" HttpOnly", rv.headers["Set-Cookie"])
self.assertIn(" SameSite=Lax", rv.headers["Set-Cookie"])

def test_default_talisman_app_delayed(self):
talisman = Talisman()
talisman.init_app(self.app, force_https=False)
talisman.init_app(self.app)

rv = self.test_client.get("/")
rv = self.test_client.get("https://localhost/")

self.assertEqual(rv.status_code, 200)

Expand All @@ -88,9 +87,9 @@ def test_default_talisman_app_delayed(self):
)

def test_talisman_google_csp(self):
Talisman(self.app, force_https=False, allow_google_content_security_policy=True)
Talisman(self.app, allow_google_content_security_policy=True)

rv = self.test_client.get("/")
rv = self.test_client.get("https://localhost/")

self.assertEqual(rv.status_code, 200)

Expand Down Expand Up @@ -122,11 +121,9 @@ def test_talisman_google_csp(self):
)

def test_talisman_typekit_csp(self):
Talisman(
self.app, force_https=False, allow_typekit_content_security_policy=True
)
Talisman(self.app, allow_typekit_content_security_policy=True)

rv = self.test_client.get("/")
rv = self.test_client.get("https://localhost/")

self.assertEqual(rv.status_code, 200)

Expand All @@ -141,12 +138,11 @@ def test_talisman_typekit_csp(self):
def test_talisman_google_and_typekit_csp(self):
Talisman(
self.app,
force_https=False,
allow_google_content_security_policy=True,
allow_typekit_content_security_policy=True,
)

rv = self.test_client.get("/")
rv = self.test_client.get("https://localhost/")

self.assertEqual(rv.status_code, 200)

Expand All @@ -167,14 +163,13 @@ def test_talisman_google_and_typekit_csp(self):
def test_talisman_custom_csp(self):
Talisman(
self.app,
force_https=False,
content_security_policy={
"default-src": ["'self'", "example.com"],
"img-src": ["'self'", "img.example.com"],
},
)

rv = self.test_client.get("/")
rv = self.test_client.get("https://localhost/")

self.assertEqual(rv.status_code, 200)

Expand All @@ -191,15 +186,14 @@ def test_talisman_custom_csp(self):
def test_talisman_custom_csp_with_google(self):
Talisman(
self.app,
force_https=False,
content_security_policy={
"default-src": ["'self'", "example.com"],
"img-src": ["'self'", "img.example.com"],
},
allow_google_content_security_policy=True,
)

rv = self.test_client.get("/")
rv = self.test_client.get("https://localhost/")

self.assertEqual(rv.status_code, 200)

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

def test_talisman_custom_extra_headers(self):
Talisman(
self.app,
extra_headers={
"Cross-Origin-Resource-Policy": "cross-origin",
"X-Custom-Header": "CustomValue",
},
)

rv = self.test_client.get("https://localhost/")

self.assertEqual(rv.status_code, 200)

self.assertIn("X-Permitted-Cross-Domain-Policies", rv.headers)
self.assertEqual("none", rv.headers["X-Permitted-Cross-Domain-Policies"])
self.assertIn("Cross-Origin-Embedder-Policy", rv.headers)
self.assertEqual("unsafe-none", rv.headers["Cross-Origin-Embedder-Policy"])
self.assertIn("Cross-Origin-Opener-Policy", rv.headers)
self.assertEqual("same-origin", rv.headers["Cross-Origin-Opener-Policy"])

self.assertIn("Cross-Origin-Resource-Policy", rv.headers)
self.assertEqual("cross-origin", rv.headers["Cross-Origin-Resource-Policy"])

self.assertIn("X-Custom-Header", rv.headers)
self.assertEqual("CustomValue", rv.headers["X-Custom-Header"])

def test_talisman_custom_referrer_policy(self):
Talisman(self.app, referrer_policy="no-referrer")

rv = self.test_client.get("https://localhost/")

self.assertEqual(rv.status_code, 200)

self.assertIn("Referrer-Policy", rv.headers)
self.assertEqual("no-referrer", rv.headers["Referrer-Policy"])
21 changes: 13 additions & 8 deletions tna_utilities/flask/talisman.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def init_app(
content_security_policy: dict | None = None,
allow_google_content_security_policy: bool = False,
allow_typekit_content_security_policy: bool = False,
security_headers: dict | None = None,
extra_headers: dict | None = None,
referrer_policy: str = "strict-origin-when-cross-origin",
force_https: bool = True,
force_https_permanent: bool = False,
Expand All @@ -83,14 +83,16 @@ def init_app(
:param content_security_policy: A dictionary defining the Content Security Policy directives and their values.
:param allow_google_content_security_policy: If True, includes Google's recommended Content Security Policy directives in addition to the custom directives specified in content_security_policy.
:param allow_typekit_content_security_policy: If True, includes Adobe Typekit's recommended Content Security Policy directives in addition to the custom directives specified in content_security_policy.
:param security_headers: A dictionary of additional security headers to apply to responses, where the keys are header names and the values are header values.
:param extra_headers: A dictionary of additional headers to apply to responses, where the keys are header names and the values are header values.
: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.
"""

content_security_policy = content_security_policy or {}
security_headers = security_headers or {}
if content_security_policy is None:
content_security_policy = {}
if extra_headers is None:
extra_headers = {}

self.app = app

Expand All @@ -106,7 +108,7 @@ def init_app(
self.allow_typekit_content_security_policy = (
allow_typekit_content_security_policy
)
self.security_headers = security_headers
self.extra_headers = extra_headers
self.referrer_policy = referrer_policy
self.force_https = force_https
self.force_https_permanent = force_https_permanent
Expand Down Expand Up @@ -135,7 +137,7 @@ def _force_https_redirect(self):
parsed.path,
parsed.params,
parsed.query,
"",
parsed.fragment,
)
)
code = 302
Expand All @@ -156,8 +158,9 @@ def _apply_extra_headers(self, response):
self.allow_google_content_security_policy,
self.allow_typekit_content_security_policy,
)
response.headers.update(common_security_headers(**self.security_headers))
response.headers.update(common_security_headers())
response.headers["Referrer-Policy"] = self.referrer_policy
response.headers.update(self.extra_headers)
return response

def _csp(
Expand All @@ -170,7 +173,9 @@ def _csp(
Generates a Content-Security-Policy header value based on the provided content security policy configuration and the option to include Google's recommended content security policy directives.
"""

csp = CspGenerator(default_src=content_security_policy.get("default-src", ""))
csp = CspGenerator(
default_src=content_security_policy.get("default-src", CspGenerator.SELF)
)

property_methods = [
("base-uri", csp.base_uri),
Expand Down
Loading