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 28, 2020
1 parent d9a8d9c commit 644b1b1
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ All notable changes to `dash` will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/).

## [UNRELEASED]
### Added
- [#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`) .

### Fixed
- [#1384](https://github.com/plotly/dash/pull/1384) Fixed a bug introduced by [#1180](https://github.com/plotly/dash/pull/1180) breaking use of `prevent_initial_call` as a positional arg in callback definitions

Expand Down
38 changes: 38 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,42 @@ def _serve_default_favicon():
pkgutil.get_data("dash", "favicon.ico"), content_type="image/x-icon"
)

def csp_hashes(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).
Calculate these hashes after all inline callbacks are defined,
and add them to your CSP headers before starting the server, for example
with the flask-talisman package from PyPI:
flask_talisman.Talisman(app.server, content_security_policy={
"default-src": "'self'",
"script-src": ["'self'"] + app.csp_hashes()
})
: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 @@ -10,3 +10,4 @@ cryptography==3.0
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("output_element", "children"),
[Input("input_element", "value")],
)

with expectation:
csp = {
"default-src": "'self'",
"script-src": ["'self'"]
+ (app.csp_hashes(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 644b1b1

Please sign in to comment.