mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-12-02 15:39:57 +00:00
Add generic hCaptcha CSP support and tests (ref: ChatGPT conversation https://chatgpt.com/share/6929f2ba-cedc-800f-9c4c-2049810cea94)
This commit is contained in:
@@ -202,31 +202,39 @@ class FilterModule(object):
|
||||
|
||||
tokens = ["'self'"]
|
||||
|
||||
# 1) Flags (with sane defaults)
|
||||
# Flags (with sane defaults)
|
||||
flags = self.get_csp_flags(applications, application_id, directive)
|
||||
tokens += flags
|
||||
|
||||
# 2) Internal CDN defaults for selected directives
|
||||
# Internal CDN defaults for selected directives
|
||||
if directive in ('script-src-elem', 'connect-src', 'style-src-elem', 'style-src'):
|
||||
tokens.append(get_url(domains, 'web-svc-cdn', web_protocol))
|
||||
|
||||
# 3) Matomo (if enabled)
|
||||
# Matomo (if enabled)
|
||||
if directive in ('script-src-elem', 'connect-src'):
|
||||
if self.is_feature_enabled(applications, matomo_feature_name, application_id):
|
||||
tokens.append(get_url(domains, 'web-app-matomo', web_protocol))
|
||||
|
||||
# 4) Simpleicons (if enabled) – typically used via connect-src (fetch)
|
||||
# Simpleicons (if enabled) – typically used via connect-src (fetch)
|
||||
if directive == 'connect-src':
|
||||
if self.is_feature_enabled(applications, 'simpleicons', application_id):
|
||||
tokens.append(get_url(domains, 'web-svc-simpleicons', web_protocol))
|
||||
|
||||
# 5) reCAPTCHA (if enabled) – scripts + frames
|
||||
# reCAPTCHA (if enabled) – scripts + frames
|
||||
if self.is_feature_enabled(applications, 'recaptcha', application_id):
|
||||
if directive in ('script-src-elem', 'frame-src'):
|
||||
tokens.append('https://www.gstatic.com')
|
||||
tokens.append('https://www.google.com')
|
||||
|
||||
# 6) Frame ancestors (desktop + logout)
|
||||
# hCaptcha (if enabled) – scripts + frames
|
||||
if self.is_feature_enabled(applications, 'hcaptcha', application_id):
|
||||
if directive in ('script-src-elem'):
|
||||
tokens.append('https://www.hcaptcha.com')
|
||||
tokens.append('https://js.hcaptcha.com')
|
||||
if directive in ('frame-src'):
|
||||
tokens.append('https://newassets.hcaptcha.com/')
|
||||
|
||||
# Frame ancestors (desktop + logout)
|
||||
if directive == 'frame-ancestors':
|
||||
if self.is_feature_enabled(applications, 'desktop', application_id):
|
||||
# Allow being embedded by the desktop app domain's site
|
||||
@@ -237,16 +245,16 @@ class FilterModule(object):
|
||||
tokens.append(get_url(domains, 'web-svc-logout', web_protocol))
|
||||
tokens.append(get_url(domains, 'web-app-keycloak', web_protocol))
|
||||
|
||||
# 6b) Logout support requires inline handlers (script-src-attr)
|
||||
# Logout support requires inline handlers (script-src-attr)
|
||||
if directive in ('script-src-attr','script-src-elem'):
|
||||
if self.is_feature_enabled(applications, 'logout', application_id):
|
||||
tokens.append("'unsafe-inline'")
|
||||
|
||||
|
||||
# 7) Custom whitelist
|
||||
# Custom whitelist
|
||||
tokens += self.get_csp_whitelist(applications, application_id, directive)
|
||||
|
||||
# 8) Inline hashes (only if this directive does NOT include 'unsafe-inline')
|
||||
# Inline hashes (only if this directive does NOT include 'unsafe-inline')
|
||||
if "'unsafe-inline'" not in tokens:
|
||||
for snippet in self.get_csp_inline_content(applications, application_id, directive):
|
||||
tokens.append(self.get_csp_hash(snippet))
|
||||
|
||||
@@ -22,7 +22,7 @@ server {
|
||||
|
||||
{% include 'roles/sys-svc-proxy/templates/location/html.conf.j2' %}
|
||||
|
||||
{% if location_ws is defined %}
|
||||
{% if location_ws | default(false) %}
|
||||
{% include 'roles/sys-svc-proxy/templates/location/ws.conf.j2' %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ features:
|
||||
oidc: true
|
||||
central_database: true
|
||||
logout: true
|
||||
recaptcha: true # Required for leads formulars
|
||||
server:
|
||||
csp:
|
||||
flags:
|
||||
@@ -16,7 +17,7 @@ server:
|
||||
unsafe-eval: true
|
||||
whitelist:
|
||||
connect-src:
|
||||
- "{{ WEBSOCKET_PROTOCOL }}://espocrm.{{ PRIMARY_DOMAIN }}"
|
||||
- "{{ WEBSOCKET_PROTOCOL }}://espo.crm.{{ PRIMARY_DOMAIN }}"
|
||||
- "data:"
|
||||
frame-src:
|
||||
- https://s.espocrm.com/
|
||||
|
||||
@@ -6,6 +6,7 @@ features:
|
||||
central_database: true
|
||||
oidc: true
|
||||
logout: true
|
||||
hcaptcha: true
|
||||
server:
|
||||
domains:
|
||||
canonical:
|
||||
@@ -17,12 +18,6 @@ server:
|
||||
flags:
|
||||
script-src-elem:
|
||||
unsafe-inline: true
|
||||
whitelist:
|
||||
script-src-elem:
|
||||
- "https://www.hcaptcha.com"
|
||||
- "https://js.hcaptcha.com"
|
||||
frame-src:
|
||||
- "https://newassets.hcaptcha.com/"
|
||||
docker:
|
||||
services:
|
||||
database:
|
||||
|
||||
@@ -11,8 +11,6 @@ server:
|
||||
unsafe-inline: true # Required for ONLYOFFICE
|
||||
whitelist:
|
||||
script-src-elem:
|
||||
- "https://www.hcaptcha.com"
|
||||
- "https://js.hcaptcha.com"
|
||||
- "{{ WEB_PROTOCOL }}://onlyoffice.{{ PRIMARY_DOMAIN }}"
|
||||
font-src:
|
||||
- "data:"
|
||||
@@ -27,7 +25,6 @@ server:
|
||||
- "{{ WEBSOCKET_PROTOCOL }}://collabora.{{ PRIMARY_DOMAIN }}"
|
||||
- "{{ WEB_PROTOCOL }}://onlyoffice.{{ PRIMARY_DOMAIN }}"
|
||||
- "{{ WEB_PROTOCOL }}://collabora.{{ PRIMARY_DOMAIN }}"
|
||||
- "https://newassets.hcaptcha.com/"
|
||||
- "*" # Required to load all external websites in Whiteboard
|
||||
worker-src:
|
||||
- "blob:"
|
||||
@@ -123,13 +120,14 @@ docker:
|
||||
# @see https://apps.nextcloud.com/apps/sociallogin
|
||||
flavor: "oidc_login" # Keeping on sociallogin because the other option is not implemented yet
|
||||
features:
|
||||
matomo: true
|
||||
css: false
|
||||
desktop: true
|
||||
ldap: true
|
||||
oidc: true
|
||||
central_database: true
|
||||
logout: true
|
||||
matomo: true
|
||||
css: false
|
||||
desktop: true
|
||||
ldap: true
|
||||
oidc: true
|
||||
central_database: true
|
||||
logout: true
|
||||
hcaptcha: true
|
||||
default_quota: '1000000000' # Quota to assign if no quota is specified in the OIDC response (bytes)
|
||||
legacy_login_mask:
|
||||
enabled: False # If true, then legacy login mask is shown. Otherwise just SSO
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
vars:
|
||||
# Necessary to overwrite parent values
|
||||
client_max_body_size: "10M"
|
||||
location_ws: ""
|
||||
|
||||
- name: Create symbolic link from .env file to repository
|
||||
file:
|
||||
|
||||
@@ -565,5 +565,44 @@ class TestCspFilters(unittest.TestCase):
|
||||
self.assertIn("'unsafe-inline'", attr_tokens)
|
||||
self.assertIn("'unsafe-inline'", elem_tokens)
|
||||
|
||||
def test_build_csp_header_hcaptcha_toggle(self):
|
||||
"""
|
||||
When the 'hcaptcha' feature is enabled, the CSP must include
|
||||
the hCaptcha script and frame hosts. When disabled, they must
|
||||
not appear in any directive.
|
||||
"""
|
||||
# enabled case
|
||||
self.apps['app1'].setdefault('features', {})['hcaptcha'] = True
|
||||
header_enabled = self.filter.build_csp_header(
|
||||
self.apps, 'app1', self.domains, web_protocol='https'
|
||||
)
|
||||
|
||||
# script-src-elem must contain hCaptcha hosts
|
||||
script_elem_tokens = self._get_directive_tokens(header_enabled, 'script-src-elem')
|
||||
self.assertIn("https://www.hcaptcha.com", script_elem_tokens)
|
||||
self.assertIn("https://js.hcaptcha.com", script_elem_tokens)
|
||||
|
||||
# base script-src must also include them (family union)
|
||||
script_base_tokens = self._get_directive_tokens(header_enabled, 'script-src')
|
||||
self.assertIn("https://www.hcaptcha.com", script_base_tokens)
|
||||
self.assertIn("https://js.hcaptcha.com", script_base_tokens)
|
||||
|
||||
# frame-src must contain the hCaptcha assets host
|
||||
frame_tokens = self._get_directive_tokens(header_enabled, 'frame-src')
|
||||
self.assertIn("https://newassets.hcaptcha.com/", frame_tokens)
|
||||
|
||||
# disabled case
|
||||
self.apps['app1']['features']['hcaptcha'] = False
|
||||
header_disabled = self.filter.build_csp_header(
|
||||
self.apps, 'app1', self.domains, web_protocol='https'
|
||||
)
|
||||
|
||||
for directive in ('script-src', 'script-src-elem', 'frame-src'):
|
||||
tokens = self._get_directive_tokens(header_disabled, directive)
|
||||
self.assertNotIn("https://www.hcaptcha.com", tokens)
|
||||
self.assertNotIn("https://js.hcaptcha.com", tokens)
|
||||
self.assertNotIn("https://newassets.hcaptcha.com/", tokens)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user