diff --git a/sipa/blueprints/usersuite.py b/sipa/blueprints/usersuite.py index 352abbbc..56d37f92 100644 --- a/sipa/blueprints/usersuite.py +++ b/sipa/blueprints/usersuite.py @@ -39,7 +39,7 @@ SubnetFull, ) from sipa.model.misc import PaymentDetails -from sipa.utils.graph_utils import NonceInfo +from sipa.utils.csp import NonceInfo logger = logging.getLogger(__name__) @@ -126,14 +126,7 @@ def index(): return resp assert isinstance(nonce_info, NonceInfo) - script_nonces_str = " ".join(f"'nonce-{n}'" for n in nonce_info.script_nonces) - # NOTE when we do this on other occasions as well, find a way to stop hard-coding - # the rest of our `script_src` CSP and find a more flexible approach - resp.content_security_policy.script_src = ( - f"'self' {script_nonces_str} https://status.agdsn.net" - ) - style_nonces_str = " ".join(f"'nonce-{n}'" for n in nonce_info.style_nonces) - resp.content_security_policy.style_src = f"'self' {style_nonces_str}" + nonce_info.apply_to_csp(resp.content_security_policy) return resp diff --git a/sipa/initialization.py b/sipa/initialization.py index 52481fec..43a7622d 100644 --- a/sipa/initialization.py +++ b/sipa/initialization.py @@ -1,4 +1,3 @@ -import typing as t import logging import logging.config import os @@ -24,6 +23,7 @@ from sipa.session import SeparateLocaleCookieSessionInterface from sipa.utils import url_self, support_hotline_available, meetingcal from sipa.utils.babel_utils import get_weekday +from sipa.utils.csp import ensure_items from sipa.utils.git_utils import init_repo, update_repo from sipa.utils.graph_utils import generate_traffic_chart, provide_render_function @@ -258,8 +258,3 @@ def ensure_csp(r: Response) -> Response: csp.worker_src = ensure_items(csp.worker_src, ("'none'",)) # there doesn't seem to be a good way to set `upgrade-insecure-requests` return r - - -def ensure_items(current_items: str | None, items: t.Iterable[str]) -> str: - _cur = set(current_items.split()) if current_items else set() - return " ".join(_cur | set(items)) diff --git a/sipa/utils/csp.py b/sipa/utils/csp.py new file mode 100644 index 00000000..a224f9d1 --- /dev/null +++ b/sipa/utils/csp.py @@ -0,0 +1,42 @@ +import secrets +import typing as t +from dataclasses import dataclass, field + +from werkzeug.datastructures import ContentSecurityPolicy + + +def generate_nonce() -> str: + return secrets.token_hex(32) + + +def ensure_items(current_items: str | None, items: t.Iterable[str]) -> str: + _cur = set(current_items.split()) if current_items else set() + return " ".join(_cur | set(items)) + + +@dataclass(frozen=True) +class NonceInfo: + + """struct to remember which nonces have been generated for inline scripts""" + + style_nonces: list[str] = field(default_factory=list) + + script_nonces: list[str] = field(default_factory=list) + + def add_style_nonce(self) -> str: + self.style_nonces.append(n := generate_nonce()) + return n + + def add_script_nonce(self) -> str: + self.script_nonces.append(n := generate_nonce()) + return n + + def apply_to_csp(self, csp: ContentSecurityPolicy) -> ContentSecurityPolicy: + """Add nonces to the CSP object""" + csp.script_src = ensure_items( + csp.script_src, (f"'nonce-{n}'" for n in self.script_nonces) + ) + csp.style_src = ensure_items( + csp.style_src, (f"'nonce-{n}'" for n in self.style_nonces) + ) + return csp diff --git a/sipa/utils/graph_utils.py b/sipa/utils/graph_utils.py index 5fbfa411..941abea3 100644 --- a/sipa/utils/graph_utils.py +++ b/sipa/utils/graph_utils.py @@ -1,6 +1,3 @@ -import secrets -from dataclasses import field, dataclass - import pygal from flask_babel import gettext import pygal.svg @@ -11,6 +8,7 @@ from sipa.units import (format_as_traffic, max_divisions, reduce_by_base) from sipa.utils.babel_utils import get_weekday +from sipa.utils.csp import NonceInfo def rgb_string(r, g, b): @@ -47,26 +45,6 @@ def default_chart(chart_type, title, inline=True, **kwargs): ) -def generate_nonce() -> str: - return secrets.token_hex(32) - - -@dataclass(frozen=True) -class NonceInfo: - """struct to remember which nonces have been generated for inline scripts""" - - style_nonces: list[str] = field(default_factory=list) - script_nonces: list[str] = field(default_factory=list) - - def add_style_nonce(self) -> str: - self.style_nonces.append(n := generate_nonce()) - return n - - def add_script_nonce(self) -> str: - self.script_nonces.append(n := generate_nonce()) - return n - - def generate_traffic_chart(traffic_data: list[dict], inline: bool = True) -> Graph: """Create a graph object from the input traffic data with pygal. If inline is set, the chart is being passed the option to not add an XML