Skip to content

Commit 167beba

Browse files
authored
Merge pull request #1371 from anders-kiaer/dash_inline_js_csp
Add support for calculating CSP hashes of inline scripts
2 parents d87335b + eb20cfe commit 167beba

File tree

4 files changed

+103
-0
lines changed

4 files changed

+103
-0
lines changed

Diff for: CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ All notable changes to `dash` will be documented in this file.
33
This project adheres to [Semantic Versioning](https://semver.org/).
44

55
## [UNRELEASED]
6+
### Added
7+
- [#1371](https://github.com/plotly/dash/pull/1371) You can now get [CSP `script-src` hashes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src) of all added inline scripts by calling `app.csp_hashes()` (both Dash internal inline scripts, and those added with `app.clientside_callback`) .
8+
69
### Changed
710
- [#1385](https://github.com/plotly/dash/pull/1385) Closes [#1350](https://github.com/plotly/dash/issues/1350) and fixes a previously undefined callback behavior when multiple elements are stacked on top of one another and their `n_clicks` props are used as inputs of the same callback. The callback will now trigger once with all the triggered `n_clicks` props changes.
811

Diff for: dash/dash.py

+38
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import re
1111
import logging
1212
import mimetypes
13+
import hashlib
14+
import base64
1315

1416
from functools import wraps
1517
from future.moves.urllib.parse import urlparse
@@ -1128,6 +1130,42 @@ def _serve_default_favicon():
11281130
pkgutil.get_data("dash", "favicon.ico"), content_type="image/x-icon"
11291131
)
11301132

1133+
def csp_hashes(self, hash_algorithm="sha256"):
1134+
"""Calculates CSP hashes (sha + base64) of all inline scripts, such that
1135+
one of the biggest benefits of CSP (disallowing general inline scripts)
1136+
can be utilized together with Dash clientside callbacks (inline scripts).
1137+
1138+
Calculate these hashes after all inline callbacks are defined,
1139+
and add them to your CSP headers before starting the server, for example
1140+
with the flask-talisman package from PyPI:
1141+
1142+
flask_talisman.Talisman(app.server, content_security_policy={
1143+
"default-src": "'self'",
1144+
"script-src": ["'self'"] + app.csp_hashes()
1145+
})
1146+
1147+
:param hash_algorithm: One of the recognized CSP hash algorithms ('sha256', 'sha384', 'sha512').
1148+
:return: List of CSP hash strings of all inline scripts.
1149+
"""
1150+
1151+
HASH_ALGORITHMS = ["sha256", "sha384", "sha512"]
1152+
if hash_algorithm not in HASH_ALGORITHMS:
1153+
raise ValueError(
1154+
"Possible CSP hash algorithms: " + ", ".join(HASH_ALGORITHMS)
1155+
)
1156+
1157+
method = getattr(hashlib, hash_algorithm)
1158+
1159+
return [
1160+
"'{hash_algorithm}-{base64_hash}'".format(
1161+
hash_algorithm=hash_algorithm,
1162+
base64_hash=base64.b64encode(
1163+
method(script.encode("utf-8")).digest()
1164+
).decode("utf-8"),
1165+
)
1166+
for script in self._inline_scripts + [self.renderer]
1167+
]
1168+
11311169
def get_asset_url(self, path):
11321170
asset = get_asset_path(
11331171
self.config.requests_pathname_prefix,

Diff for: requires-testing.txt

+1
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ cryptography==3.0
1010
requests[security]==2.21.0
1111
beautifulsoup4==4.8.2
1212
waitress==1.4.3
13+
flask-talisman==0.7.0

Diff for: tests/integration/test_csp.py

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import contextlib
2+
3+
import pytest
4+
import flask_talisman
5+
from selenium.common.exceptions import NoSuchElementException
6+
7+
import dash
8+
import dash_core_components as dcc
9+
import dash_html_components as html
10+
from dash.dependencies import Input, Output
11+
12+
13+
@contextlib.contextmanager
14+
def does_not_raise():
15+
yield
16+
17+
18+
@pytest.mark.parametrize(
19+
"add_hashes, hash_algorithm, expectation",
20+
[
21+
(False, None, pytest.raises(NoSuchElementException)),
22+
(True, "sha256", does_not_raise()),
23+
(True, "sha384", does_not_raise()),
24+
(True, "sha512", does_not_raise()),
25+
(True, "sha999", pytest.raises(ValueError)),
26+
],
27+
)
28+
def test_incs001_csp_hashes_inline_scripts(
29+
dash_duo, add_hashes, hash_algorithm, expectation
30+
):
31+
app = dash.Dash(__name__)
32+
33+
app.layout = html.Div(
34+
[dcc.Input(id="input_element", type="text"), html.Div(id="output_element")]
35+
)
36+
37+
app.clientside_callback(
38+
"""
39+
function(input) {
40+
return input;
41+
}
42+
""",
43+
Output("output_element", "children"),
44+
[Input("input_element", "value")],
45+
)
46+
47+
with expectation:
48+
csp = {
49+
"default-src": "'self'",
50+
"script-src": ["'self'"]
51+
+ (app.csp_hashes(hash_algorithm) if add_hashes else []),
52+
}
53+
54+
flask_talisman.Talisman(
55+
app.server, content_security_policy=csp, force_https=False
56+
)
57+
58+
dash_duo.start_server(app)
59+
60+
dash_duo.find_element("#input_element").send_keys("xyz")
61+
assert dash_duo.wait_for_element("#output_element").text == "xyz"

0 commit comments

Comments
 (0)