Skip to content

Commit

Permalink
Add support for calculating CSP hashes of inline scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
anders-kiaer committed Aug 18, 2020
1 parent 58eb07b commit ff2f454
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).

### Added
- [#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)`.
- [#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_inline_scripts()` (both Dash internal inline scripts, and those added with `app.clientside_callback`) .

### Changed
- [#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.
Expand Down
29 changes: 29 additions & 0 deletions dash/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import re
import logging
import mimetypes
import hashlib
import base64

from functools import wraps
from future.moves.urllib.parse import urlparse
Expand Down Expand Up @@ -1128,6 +1130,33 @@ def _serve_default_favicon():
pkgutil.get_data("dash", "favicon.ico"), content_type="image/x-icon"
)

def csp_hashes_inline_scripts(self, hash_algorithm="sha256"):
"""Calculates CSP hashes (sha + base64) of all inline scripts, such that
one of the biggest benefits of CSP (disallowing general inline scripts)
can be utilized together with Dash clientside callbacks (inline scripts).
:param hash_algorithm: One of the recognized CSP hash algorithms ('sha256', 'sha384', 'sha512').
:return: List of CSP hash strings of all inline scripts.
"""

HASH_ALGORITHMS = ["sha256", "sha384", "sha512"]
if hash_algorithm not in HASH_ALGORITHMS:
raise ValueError(
"Possible CSP hash algorithms: " + ", ".join(HASH_ALGORITHMS)
)

method = getattr(hashlib, hash_algorithm)

return [
"'{hash_algorithm}-{base64_hash}'".format(
hash_algorithm=hash_algorithm,
base64_hash=base64.b64encode(
method(script.encode("utf-8")).digest()
).decode("utf-8"),
)
for script in self._inline_scripts + [self.renderer]
]

def get_asset_url(self, path):
asset = get_asset_path(
self.config.requests_pathname_prefix,
Expand Down
1 change: 1 addition & 0 deletions requires-testing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ percy==2.0.2
requests[security]==2.21.0
beautifulsoup4==4.8.2
waitress==1.4.3
flask-talisman==0.7.0
59 changes: 59 additions & 0 deletions tests/integration/test_csp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import contextlib

import pytest
import flask_talisman
from selenium.common.exceptions import NoSuchElementException

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output


@contextlib.contextmanager
def does_not_raise():
yield


@pytest.mark.parametrize(
"add_hashes, hash_algorithm, expectation",
[
(False, None, pytest.raises(NoSuchElementException)),
(True, "sha256", does_not_raise()),
(True, "sha384", does_not_raise()),
(True, "sha512", does_not_raise()),
(True, "sha999", pytest.raises(ValueError)),
],
)
def test_csp_hashes_inline_scripts(dash_duo, add_hashes, hash_algorithm, expectation):
app = dash.Dash(__name__)

app.layout = html.Div(
[dcc.Input(id="input_element", type="text"), html.Div(id="output_element")]
)

app.clientside_callback(
"""
function(input) {
return input;
}
""",
Output(component_id="output_element", component_property="children"),
[Input(component_id="input_element", component_property="value")],
)

with expectation:
csp = {
"default-src": "'self'",
"script-src": ["'self'"]
+ (app.csp_hashes_inline_scripts(hash_algorithm) if add_hashes else []),
}

flask_talisman.Talisman(
app.server, content_security_policy=csp, force_https=False
)

dash_duo.start_server(app)

dash_duo.find_element("#input_element").send_keys("xyz")
assert dash_duo.wait_for_element("#output_element").text == "xyz"

0 comments on commit ff2f454

Please sign in to comment.