Repository: plotly/dash

Issue:
DashRenderer initialization and CSP
Dash is a [CSP friendly](https://en.wikipedia.org/wiki/Content_Security_Policy) framework, enabling writing XSS safe applications, however this had a setback in #367.

With `pip install 'dash==0.38.0' dash_html_components flask-talisman` you could test quite strict CSP settings in Dash locally with success:
```python
import dash
import dash_html_components as html
from flask_talisman import Talisman

app = dash.Dash(__name__)
app.css.config.serve_locally = True
app.scripts.config.serve_locally = True

csp = {
       'default-src': '\'self\'',
       'script-src': '\'self\'',
       'style-src': '\'self\''
      }

Talisman(app.server, content_security_policy=csp, force_https=False)
app.layout = html.Div(children=['Hello Dash!'])

if __name__ == '__main__':
    app.run_server()
```

With `dash==0.39.0` however this fails due to the new inline script https://github.com/plotly/dash/blob/05545461214e43707e7d4e36485935dc166be987/dash/dash.py#L174

A work-around could be to add hash of the current Dash generated inline script (`sha256-jZlsGVOhUAIcH+4PVs7QuGZkthRMgvT2n0ilH6/zTM0=`) to the server CSP script src headers, however that would be in need of update when the dash renderer string/configuration changes.

Not sure if it is feasible/fits the overall framework/plans, but could one suggestion be to follow the same concept as when the user e.g. wants to override the default `favicon.ico` (i.e. a file `favicon.ico` is placed in the `assets` folder - here in this case it could be e.g. a file `dash-renderer-config.js`). In addition to following the same concept in terms of overriding default assets and continue not having inline scripts in Dash core, it perhaps also better facilitates separation of Python- and JavaScript code.


Discussion:
Hello, can you tell me please how can i create a hash for dash generated script? I use this lib as python package with django and CSP. Thks
Not entirely sure what you mean with "dash generated script", but maybe this helps:

In our Dash based project, we use something along these lines to calculate CSP hashes for all inline scripts (the `app.renderer` inline script, as well as inline scripts due to clientside callbacks):

```python
import hashlib
import base64
from typing import List

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
from flask_talisman import Talisman


def calculate_inline_hashes(app: dash.Dash) -> List[str]:
    """Given a Dash app instance, this function calculates
    CSP hashes (sha256 + base64) of all inline scripts, such that
    one of the biggest benefits of CSP (banning general inline scripts)
    can be utilized.

    Warning: Note that this function uses a "private" Dash app attribute,
    app._inline_scripts, which might change from one Dash version to the next.
    """
    return [
        f"'sha256-{base64.b64encode(hashlib.sha256(script.encode('utf-8')).digest()).decode('utf-8')}'"
        for script in [app.renderer] + app._inline_scripts
    ]


app = dash.Dash()

app.layout = html.Div(
    [dcc.Input(id="my-id", value="initial value", type="text"), html.Div(id="my-div")]
)


app.clientside_callback(
    """
    function(input) {
        return "You have entered: " + input;
    }
    """,
    Output(component_id="my-div", component_property="children"),
    [Input(component_id="my-id", component_property="value")],
)

csp = {
    "default-src": "'self'",
    "script-src": ["'self'"] + calculate_inline_hashes(app),
    "style-src": ["'self'", "'unsafe-inline'"],
}

Talisman(app.server, content_security_policy=csp, force_https=False)

if __name__ == "__main__":
    app.run_server(debug=True)
```
Thks this help me! 
Any way to keep     
"style-src": ["'self'"] without using "'unsafe-inline'" please?

> Any way to keep `"style-src": ["'self'"]` without using `"'unsafe-inline'"` please?

That very much depends on which components you use in your Dash application (e.g. `dash` alone does not require `"'unsafe-inline'"` wrt. `style`). If you on the other hand use `dash-core-components` this issue is related: https://github.com/plotly/dash-core-components/issues/752

If you make your own Dash components, you can ensure you don't need `"'unsafe-inline'"` by providing static `.css` in a separate CSS-file (Dash has functionality for loading component provided `.css` files), and set dynamic CSS with JavaScript using a CSP compatible syntax (see the `dcc` issue above).
@anders-kiaer how are you dealing with dcc.graph() under this approach? Calculating CSP hashes appears to be working for everything except dcc.graph()
> @anders-kiaer how are you dealing with dcc.graph() under this approach? Calculating CSP hashes appears to be working for everything except dcc.graph()

Not sure if you are talking about CSP hashes of inline `CSS` og `JavaScript`. I have not seen/had any problems with `dcc.Graph` and inline JavaScript. Inline CSS is discussed in the issue linked above, i.e. https://github.com/plotly/dash-core-components/issues/752
> > @anders-kiaer how are you dealing with dcc.graph() under this approach? Calculating CSP hashes appears to be working for everything except dcc.graph()
> 
> Not sure if you are talking about CSP hashes of inline `CSS` og `JavaScript`. I have not seen/had any problems with `dcc.Graph` and inline JavaScript. Inline CSS is discussed in the issue linked above, i.e. [plotly/dash-core-components#752](https://github.com/plotly/dash-core-components/issues/752)

inline js, I'll re-explore the issue, what version of dash and dash-core-components are you on?

@Poseyy I am also experiencing this issue. Are you still investigating this? I am using the following versions.

dash                      1.12.0
dash-core-components      1.10.0
@rpmchale Couldn't solve this one, still interested in finding a solution, just not actively investigating. 

Resolving patch:
diff --git a/dash/dash.py b/dash/dash.py
--- a/dash/dash.py
+++ b/dash/dash.py
@@ -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
@@ -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,


Test patch:
diff --git a/tests/integration/test_csp.py b/tests/integration/test_csp.py
new file mode 100644
--- /dev/null
+++ b/tests/integration/test_csp.py
@@ -0,0 +1,61 @@
+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_incs001_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"