Skip to content

Commit

Permalink
Add custom nonces to inline scripts and styles returned by pygal
Browse files Browse the repository at this point in the history
This sets a hard-coded `script_src` and `style_src` CSP that is only
returned in the usersuite index, which is the only location where we
use `pygal`.

This allows us to forbid un-tagged inline scripts and styles via CSP.

`flask.g` is used because we (unfortunately) render the traffic graph
indirectly via global jinja callable instead of passing it directly to
the template as an argument.
  • Loading branch information
lukasjuhrich committed Oct 6, 2023
1 parent be45ba0 commit f10ce9f
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 2 deletions.
35 changes: 33 additions & 2 deletions sipa/blueprints/usersuite.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,18 @@
from datetime import datetime

from babel.numbers import format_currency
from flask import Blueprint, render_template, url_for, redirect, flash, abort, request, current_app
from flask import (
Blueprint,
render_template,
url_for,
redirect,
flash,
abort,
request,
current_app,
make_response,
g,
)
from flask_babel import format_date, gettext
from flask_login import current_user, login_required
from flask_wtf import FlaskForm
Expand All @@ -28,6 +39,7 @@
SubnetFull,
)
from sipa.model.misc import PaymentDetails
from sipa.utils.graph_utils import NonceInfo

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -103,7 +115,26 @@ def index():
logs=info.history,
)

return render_template("usersuite/index.html", payment_form=payment_form, **context)
resp = make_response(
render_template("usersuite/index.html", payment_form=payment_form, **context)
)
nonce_info = g.nonce_info
if nonce_info is None:
logger.error(
"nonce_info not set after rendering usersuite index", exc_info=True
)
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}"
return resp


@bp_usersuite.route("/contact", methods=['GET', 'POST'])
Expand Down
39 changes: 39 additions & 0 deletions sipa/utils/graph_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import secrets
from dataclasses import field, dataclass

import pygal
from flask_babel import gettext
import pygal.svg
from pygal import Graph
from pygal.colors import hsl_to_rgb
from pygal.style import Style
Expand Down Expand Up @@ -43,6 +47,26 @@ 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
Expand Down Expand Up @@ -85,6 +109,21 @@ def generate_traffic_chart(traffic_data: list[dict], inline: bool = True) -> Gra
[day['throughput'] for day in traffic_data],
stroke_style={'width': '2'})

from flask import g

if not hasattr(g, "nonce_info"):
g.nonce_info = NonceInfo()

def add_nonces(el):
for sub_el in el.findall("./defs/style"):
sub_el.set("nonce", g.nonce_info.add_style_nonce())
for script in el.findall("./defs/script"):
script.set("nonce", g.nonce_info.add_script_nonce())

return el

traffic_chart.add_xml_filter(add_nonces)

return traffic_chart


Expand Down

0 comments on commit f10ce9f

Please sign in to comment.