middleware: Implement middleware for common headers such as CSP

- This allows overriding these headers in individual pages easily instead of
relaxing global policy.

- Drop the obsolete CSP directive "block-all-mixed-content" and avoid a console
warning in Firefox.

Tests:

- Load a page and notice in the browser developer tools that the three headers
referrer-policy, content-security-policy, and x-content-type-options are set as
before.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Sunil Mohan Adapa 2025-11-09 22:10:48 -08:00 committed by James Valleroy
parent 3eef1d9324
commit 2467d6a033
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
3 changed files with 91 additions and 42 deletions

View File

@ -47,48 +47,6 @@
RedirectMatch "^/$" "/plinth"
</IfFile>
##
## Disable sending Referer (sic) header from FreedomBox web interface to
## external websites. This improves privacy by not disclosing FreedomBox
## domains/URLs to external domains. Apps such as blogs which want to popularize
## themselves with referrer header may still do so.
##
## A strict Content Security Policy.
## - @fonts are allowed only from FreedomBox itself.
## - <frame>/<iframe> sources are disabled.
## - <img> sources are allowed only from FreedomBox itself.
## - Manifest file is not allowed as there is none yet.
## - <audio>, <video>, <track> tags are not allowed yet.
## - <object>, <embed>, <applet> tags are not allowed yet.
## - Allow JS from FreedomBox itself (no inline and attribute scripts).
## - Allow inline CSS and CSS files from Freedombox itself.
## - Web worker sources are allowed only from FreedomBox itself (for JSXC).
## - All other fetch sources including Ajax are not allowed from FreedomBox
## itself.
## - <base> tag is not allowed.
## - No plugins types are alllowed since object-src is 'none'.
## - Form action should be to FreedomBox itself.
## - This interface may be not embedded in <frame>, <iframe>, etc. tags.
## - When serving HTTPS, don't allow HTTP assets.
##
## Enable strict sandboxing enabled with some exceptions:
## - Allow running Javascript.
## - Allow popups as sometimes we use <a target=_blank>
## - Allow popups to have different sandbox requirements as we launch apps' web
## clients.
## - Allow forms to support configuration forms.
## - Allow policies to treat same origin differently from other origins
## - Allow downloads such as backup tarballs.
##
## Disable browser guessing of MIME types. FreedoBox already sets good content
## types for all the common file types.
##
<LocationMatch "^/(plinth|freedombox)">
Header set Referrer-Policy 'same-origin'
Header set Content-Security-Policy "font-src 'self'; frame-src 'none'; img-src 'self' data:; manifest-src 'none'; media-src 'none'; object-src 'none'; script-src 'self'; style-src 'self'; worker-src 'self'; default-src 'self'; base-uri 'none'; sandbox allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-same-origin allow-downloads; form-action 'self'; frame-ancestors 'none'; block-all-mixed-content;"
Header set X-Content-Type-Options 'nosniff'
</LocationMatch>
##
## On all sites, provide FreedomBox on a default path: /plinth
##

View File

@ -188,3 +188,93 @@ class CommonErrorMiddleware(MiddlewareMixin):
breadcrumbs = views.get_breadcrumbs(request)
parent_index = 1 if len(breadcrumbs) > 1 else 0
return list(breadcrumbs.keys())[parent_index]
class CSPDict(dict):
"""A dictionary to store Content Security Policy.
And return a full value of the HTTP header.
"""
def get_header_value(self) -> str:
"""Return the string header value for the policy stored."""
return ' '.join([f'{key} {value};' for key, value in self.items()])
CONTENT_SECURITY_POLICY = CSPDict({
# @fonts are allowed only from FreedomBox itself.
'font-src': "'self'",
# <frame>/<iframe> sources are disabled.
'frame-src': "'none'",
# <img> sources are allowed only from FreedomBox itself. Allow
# data: URLs for SVGs in CSS.
'img-src': "'self' data:",
# Manifest file is not allowed as there is none yet.
'manifest-src': "'none'",
# <audio>, <video>, <track> tags are not allowed yet.
'media-src': "'none'",
# <object>, <embed>, <applet> tags are not allowed yet. No plugins
# types are alllowed since object-src is 'none'.
'object-src': "'none'",
# Allow JS from FreedomBox itself (no inline and attribute
# scripts).
'script-src': "'self'",
# Allow inline CSS and CSS files from Freedombox itself.
'style-src': "'self'",
# Web worker sources are allowed only from FreedomBox itself (for
# JSXC).
'worker-src': "'self'",
# All other fetch sources including Ajax are not allowed from
# FreedomBox itself.
'default-src': "'self'",
# <base> tag is not allowed.
'base-uri': "'none'",
# Enable strict sandboxing enabled with some exceptions:
# - Allow running Javascript.
# - Allow popups as sometimes we use <a target=_blank>
# - Allow popups to have different sandbox requirements as we
# launch apps' web clients.
# - Allow forms to support configuration forms.
# - Allow policies to treat same origin differently from other
# - origins
# - Allow downloads such as backup tarballs.
'sandbox': 'allow-scripts allow-popups '
'allow-popups-to-escape-sandbox allow-forms '
'allow-same-origin allow-downloads',
# Form action should be to FreedomBox itself.
'form-action': "'self'",
# This interface may be not embedded in <frame>, <iframe>, etc.
# tags.
'frame-ancestors': "'none'",
})
class CommonHeadersMiddleware:
def __init__(self, get_response):
"""Initialize the middleware object."""
self.get_response = get_response
def __call__(self, request):
"""Add common security middleware."""
# Disable sending Referer (sic) header from FreedomBox web interface to
# external websites. This improves privacy by not disclosing FreedomBox
# domains/URLs to external domains. Apps such as blogs which want to
# popularize themselves with referrer header may still do so.
response = self.get_response(request)
if not response.get('Referrer-Policy'):
response['Referrer-Policy'] = 'same-origin'
# Disable browser guessing of MIME types. FreedoBox already sets good
# content types for all the common file types.
if not response.get('X-Content-Type-Options'):
response['X-Content-Type-Options'] = 'nosniff'
csp = ' '.join([
f'{key} {value};'
for key, value in CONTENT_SECURITY_POLICY.items()
])
if not response.get('Content-Security-Policy'):
response['Content-Security-Policy'] = csp
return response

View File

@ -149,6 +149,7 @@ LOGIN_REDIRECT_URL = 'index'
MESSAGE_TAGS: dict = {}
MIDDLEWARE = (
'plinth.middleware.CommonHeadersMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',