Skip to content

Commit e7e2fce

Browse files
committed
Add support for calculating CSP hashes of inline scripts
1 parent 58eb07b commit e7e2fce

File tree

3 files changed

+91
-0
lines changed

3 files changed

+91
-0
lines changed

Diff for: CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
88

99
### Added
1010
- [#1355](https://github.com/plotly/dash/pull/1355) Removed redundant log message and consolidated logger initialization. You can now control the log level - for example suppress informational messages from Dash with `app.logger.setLevel(logging.WARNING)`.
11+
- [#1371](https://github.com/plotly/dash/pull/1371) You can now easily 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_inline_scripts()` (both Dash internal inline scripts, and those added with `app.clientside_callback`) .
1112

1213
### Changed
1314
- [#1180](https://github.com/plotly/dash/pull/1180) `Input`, `Output`, and `State` in callback definitions don't need to be in lists. You still need to provide `Output` items first, then `Input` items, then `State`, and the list form is still supported. In particular, if you want to return a single output item wrapped in a length-1 list, you should still wrap the `Output` in a list. This can be useful for procedurally-generated callbacks.

Diff for: dash/dash.py

+29
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,33 @@ def _serve_default_favicon():
11281130
pkgutil.get_data("dash", "favicon.ico"), content_type="image/x-icon"
11291131
)
11301132

1133+
def csp_hashes_inline_scripts(self, hash_algorithm="sha256"):
1134+
"""Calculates CSP hashes (sha + base64) of all added inline scripts, such that
1135+
one of the biggest benefits of CSP (disallowing general inline scripts)
1136+
can be utilized together with Dash inline scripts.
1137+
1138+
:param hash_algorithm: One of the recognized CSP hash algorithms ('sha256', 'sha384', 'sha512').
1139+
:return: List of CSP hash strings of all inline scripts.
1140+
"""
1141+
1142+
HASH_ALGORITHMS = ["sha256", "sha384", "sha512"]
1143+
if hash_algorithm not in HASH_ALGORITHMS:
1144+
raise ValueError(
1145+
"Possible CSP hash algorithms: " + ", ".join(HASH_ALGORITHMS)
1146+
)
1147+
1148+
method = getattr(hashlib, hash_algorithm)
1149+
1150+
return [
1151+
"'{hash_algorithm}-{base64_hash}'".format(
1152+
hash_algorithm=hash_algorithm,
1153+
base64_hash=base64.b64encode(
1154+
method(script.encode("utf-8")).digest()
1155+
).decode("utf-8"),
1156+
)
1157+
for script in self._inline_scripts + [self.renderer]
1158+
]
1159+
11311160
def get_asset_url(self, path):
11321161
asset = get_asset_path(
11331162
self.config.requests_pathname_prefix,

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_csp_hashes_inline_script(dash_duo, add_hashes, hash_algorithm, expectation):
29+
app = dash.Dash(__name__)
30+
31+
app.layout = html.Div(
32+
[dcc.Input(id="input_element", type="text"), html.Div(id="output_element")]
33+
)
34+
35+
app.clientside_callback(
36+
"""
37+
function(input) {
38+
return input;
39+
}
40+
""",
41+
Output(component_id="output_element", component_property="children"),
42+
[Input(component_id="input_element", component_property="value")],
43+
)
44+
45+
with expectation:
46+
csp = {
47+
"default-src": "'self'",
48+
"script-src": ["'self'"] + app.csp_hashes_inline_scripts(hash_algorithm)
49+
if add_hashes
50+
else [],
51+
"style-src": ["'self'", "'unsafe-inline'"],
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)