Hey folks –
Here’s a community thread to kick off security-related patterns & concerns when writing Dash apps.
The Dash framework itself is architected in a way to avoid many security issues outright (like many XSS issues encountered when rendering arbitrary HTML) and our commercial Dash Enterprise platform provides strict security controls beyond the application code (with authorization, authentication, sandboxing apps in containers away from the host server, security audits, and more).
However, it is still possible to write insecure code. This list is in no way exhaustive, and the scope is small - We’re not considering things like the operating system, authentication or authorization, securing remote connections, etc.
Please comment below if you’d like something added to the list!
Security Background
OWASP’s Top Ten Security Issues is a great place to start to learn about security. Note that not all of these issues are areas of risk in Dash (due to how Dash is architected).
1. Treat All Callback Inputs as Untrusted
Your UI might have a finite set of options (e.g. in a dcc.Dropdown
) but malicious users can construct HTTP requests that fire your callbacks with any inputs that they want.
So, treat your callback input arguments as untrusted. This means:
a. Avoid eval
or exec
'ing anything
b. Use werkzeug.utils.secure_filename(filename)
when constructing a file path that comes from a callback input. This prevents malicious users from reading or accessing files on your system that live outside of the project folder.
Instead of this:
@app.callback(Output('graph', 'figure'), Input('dropdown', 'value'))
def update_figure(file_path):
with open(file_path, 'r') as f: # DON'T DO THIS
f.read()
Do this:
@app.callback(Output('graph', 'figure'), Input('dropdown', 'value'))
def update_figure(untrusted_file_path):
safe_file_path = werkzeug.utils.secure_filename(unrusted_file_path)
with open(safe_file_path, 'r') as f: # THIS IS OK
f.read()
c. Escape SQL parameters using whatever methods for parameterizing queries are available in the library:
Instead of this:
@app.callback(Output('graph', 'figure'), Input('dropdown', 'value'))
def update_figure(untrusted_value):
query = 'SELECT x, y FROM data WHERE city = {}'.format(untrusted_value) # DON'T DO THIS
query = 'SELECT x, y FROM data WHERE city = %s' % untrusted_value # DON'T DO THIS
return cur.execute(query)
Do this:
@app.callback(Output('graph', 'figure'), [Input('dropdown', 'value')])
def update_figure(untrusted_value):
query = 'SELECT x, y FROM data WHERE city = %s'
return cur.execute(query, (untrusted_value,)) # GOOD, untrusted_value WILL BE ESCAPED BY execute()
d. Avoid passing callback inputs into dangerously_set_inner_html
and any other component properties in other community packages that render arbitrary HTML. Or better yet, avoid dangerously_set_inner_html
altogether.
Rendering arbitrary HTML from untrusted user inputs is an XSS issue.
2. dcc.Store
Data is accessible, even if it’s not visible
All of the data that is returned in a callback can be accessed by the current user of the app, even if that data isn’t necessarily visible.
So, be careful not to store secret or sensitive data in dcc.Store
or other callbacks if you don’t intend all users to be able to access the data.
For example, if you have a dataframe that has several unused but sensitive columns (e.g. data about a particular client), then remove those columns before saving it in a dcc.Store
. Even if your app doesn’t use those columns, the data is still accessible in the browser session.
3. Avoid Storing Secrets in Code
API keys, database passwords, etc - These should be saved in environment variables outside of your application code. If they are in your application code, then they are easily inadvertently stored in other systems like GitHub or between other people.
If you are using Dash Enterprise, then use the built-in Environment Variable manager:
4. Don’t Roll Your Own Authentication or User Management
It is very easy to inadvertently introduce security issues when writing authentication or user management code. Leave authentication to a separate industry-accepted platform, vendor, or library.
If you use Dash Enterprise, you don’t need to worry about this. Dash Enterprise connects to several IdPs (Active Directory FS, Azure AD, Ping Federate, Okta) through several standard protocols (LDAP, SAML). The authentication (who is logged in) and authorization (who has access to the happen) happens before the request even hits the Dash app, so no logic is required in the Dash app code.