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
2 changes: 2 additions & 0 deletions src/moin/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from moin.apps.frontend.views import bad_request
from moin.i18n import i18n_init
from moin.search import SearchForm
from moin.security.csp import set_csp_nonce
from moin.storage.middleware import protecting, indexing, routing
from moin.themes import setup_jinja_env, themed_error, ThemeSupport
from moin.utils import get_xstatic_module_path_map
Expand Down Expand Up @@ -434,6 +435,7 @@ def before_wiki():
cli_no_request_ctx = False
try:
flaskg.user = setup_user()
set_csp_nonce(request)
except RuntimeError: # CLI call has no valid request context, create dummy
flaskg.user = user.User(name=ANON, auth_method="invalid")
cli_no_request_ctx = True
Expand Down
38 changes: 34 additions & 4 deletions src/moin/security/csp.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,37 @@

from __future__ import annotations

from flask import url_for, Response
from typing import TYPE_CHECKING

import secrets

from flask import request, url_for, Response
from moin import current_app

if TYPE_CHECKING:
from flask.wrappers import Request
from flask import Response

NONCE_ATTR = "csp_nonce"

NONCE_LENGTH = 32


def make_csp_nonce() -> str:
"""
Returns a random nonce.
"""
return secrets.token_urlsafe(NONCE_LENGTH)


def get_csp_nonce() -> str:
return getattr(request, NONCE_ATTR, "")


def set_csp_nonce(request: Request) -> None:
if not getattr(request, NONCE_ATTR, None):
setattr(request, NONCE_ATTR, make_csp_nonce())


def add_csp_headers(response: Response) -> Response:
"""
Expand All @@ -19,8 +46,11 @@ def add_csp_headers(response: Response) -> Response:
response.headers["Content-Security-Policy"] = cfg.content_security_policy

if cfg.content_security_policy_report_only:
response.headers["Content-Security-Policy-Report-Only"] = (
f"{cfg.content_security_policy_report_only} report-uri {url_for('frontend.cspreport')}; "
)
nonce_value = get_csp_nonce()
# report only policy
policy_value = cfg.content_security_policy_report_only
policy_value = policy_value.replace("{NONCE}", nonce_value)
policy_value = f"{policy_value} report-uri { url_for('frontend.cspreport') };"
response.headers["Content-Security-Policy-Report-Only"] = policy_value

return response
2 changes: 2 additions & 0 deletions src/moin/themes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from moin.constants.misc import FLASH_REPEAT, ICON_MAP
from moin.constants.namespaces import NAMESPACE_DEFAULT, NAMESPACE_USERS, NAMESPACE_USERPROFILES, NAMESPACE_ALL
from moin.constants.rights import SUPERUSER
from moin.security.csp import get_csp_nonce
from moin.user import User
from moin.utils.interwiki import split_interwiki, getInterwikiHome, is_local_wiki, is_known_wiki, url_for_item
from moin.utils.clock import timed
Expand Down Expand Up @@ -762,6 +763,7 @@ def setup_jinja_env(jinja_env):

jinja_env.globals.update(
{
"csp_nonce": get_csp_nonce,
# please note that flask-babel/jinja2.ext installs:
# _, gettext, ngettext
"isinstance": isinstance,
Expand Down