Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

validate that callback request "outputs" field matches callback #1546

Merged
merged 4 commits into from
Feb 8, 2021

Conversation

alexcjohnson
Copy link
Collaborator

@alexcjohnson alexcjohnson commented Feb 3, 2021

When a callback request comes in, it has two somewhat redundant fields: output and outputs. The former is the stringified callback ID, the latter a dict {id, property} or list of such dicts (potentially nested in case of multi-element pattern-matching wildcards). We look up the callback using only output, but then in the response we incorporate the values in outputs. We don't believe this is an actual security issue but it looks like it could be one because malicious content inserted in outputs will come back unmodified. This PR removes the perception of a problem by ensuring outputs contains only component IDs and props allowed by output (and output we know is valid because it's required to match a known callback).

Contributor Checklist

  • I have run the tests locally and they passed. (refer to testing section in contributing)
  • I have added tests, or extended existing tests, to cover any new features or bugs fixed in this PR
  • I have added entry in the CHANGELOG.md

@@ -125,7 +125,7 @@ def validate_output_spec(output, output_spec, Output):
for outi, speci in zip(output, output_spec):
speci_list = speci if isinstance(speci, (list, tuple)) else [speci]
for specij in speci_list:
if Output(specij["id"], specij["property"]) != outi:
if not Output(specij["id"], specij["property"]) == outi:
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really hoping this is the last Py2 bug I have to fix 🤦
We implemented __eq__ for dependencies but in Py2

There are no implied relationships among the comparison operators. The truth of x==y does not imply that x!=y is false.

whereas in Py3

For __ne__(), by default it delegates to __eq__() and inverts the result

@jdamiba
Copy link

jdamiba commented Feb 8, 2021

A cross-site-scripting (XSS) vulnerability exists when it is possible for an attacker to use a web server's public interface to send arbitrary code to another user.

This is commonly done by "injecting" the malicious code into a request to the server. In some cases, this code can then be served up to another user when they make a subsequent request to the server, possibly compromising the security of that user's session for the original attacker's benefit.

One way to test whether or not a web application is vulnerable to a XSS attack is to see if it is possible to get the server to respond to a request containing arbitrary code with that same code. If this is seen to occur, then there is potentially a XSS vulnerability, if the server can be coaxed in sending this arbitrary code in a form where it can be executed by the browser. Along with malicious actors, professional penetration testers are likely to perform this test if they have engaged to evaluate Dash's security by a potential customer of the Dash Enterprise platform.

In the context of Dash, the test for XSS vulnerabilities described above might lead to the perception that Dash is vulnerable to this kind of attack. This is because as we currently do not validate the content of a callback function's outputs field against the contents of its output field (which is itself validated when the associated callback is registered), the backend server will send back any arbitrary outputs that were sent to it in a request to execute a callback.

This is noticeable in the KeyError that is raised as a result of this process, which will contain verbatim the content of the outputs field that was sent to it. However, it is important to note that this KeyError (and any other backend server error message) is only ever returned by the server when the application is being run in debug mode, which is a design decision that was made specifically to prevent XSS attacks. By default, production applications are not run in debug mode, which means their backend throws this (or any other) error, the client simply sees a generic 500 - internal server error message, and the complete logs are only available to the server administrator.

This ensures that Dash is currently architected, a user's session will never execute any potentially malicious code that is injected into a callback function's outputs field, as it is not served as HTML or with the correct MIME type to be executed by the browser. The fact that the server returns the content of the outputs field while in debug mode only creates the perception of a security vulnerability- it is not actually a vector a malicious user to send arbitrary code to a user.

In the interest of making sure that the perception of Dash's security aligns with the reality, this PR introduces an additional validate_output_spec step into the process of a Dash server responding to a request to execute a callback. In this step, the server throws an error if the contents of the output and outputs fields do not match (it is important to note that in this case the contents of the fields can match without being identical to each other because in the case of pattern-matching callbacks, the outputs field contains the actual component IDs on the page whereas the output field contains the pattern).

This additional steps means that any arbitrary text that is not related to a properly registered callback that is injected into the outputs field of a callback will never be sent in a response back to the user or have the potential be executed by the user's web browser, even when the application is runaning in debug mode. This step ensures that a user session never sees any arbitrary content injected into the outputs field of a callback in a response from the server, preventing one from developing the perception that Dash is vulnerable to a XSS attack.

How This PR Was Tested: In order to make sure that the validate_output_spec function behaves as advertised, I ran the test_malformed_request test that is introduced in this PR against the current version of Dash, which does not perform the validate_output_spec step when processing callbacks. As expected the test fails because the status code returned by the server in response to receiving a callback whose output and outputs fields do not match is a 200 - OK instead of a 500 - server error, and the non-matching outputs are also contained in the response text sent back by the server (note that in order to have the test continue past the first failure, I used the pytest_check library).

―――――――――――――――――――――――――――――――――――――――――――― test_cbmf001_bad_output_outputs ―――――――――――――――――――――――――――――――――――――――――――――
FAILURE: 
assert 200 == 500
  +200
  -500
tests/integration/callbacks/test_malformed_request.py:56 in test_cbmf001_bad_output_outputs() -> check.equal(response.status_code, 500)
FAILURE: 
assert 'o1' not in '{"response": {"o1": {"nope": 9}}, "multi": true}'
  'o1' is contained here:
    {"response": {"o1": {"nope": 9}}, "multi": true}
  ?                ++
tests/integration/callbacks/test_malformed_request.py:57 in test_cbmf001_bad_output_outputs() -> check.is_not_in("o1", response.text)
FAILURE: 
assert 'nope' not in '{"response": {"o1": {"nope": 9}}, "multi": true}'
  'nope' is contained here:
    {"response": {"o1": {"nope": 9}}, "multi": true}
  ?                       ++++
tests/integration/callbacks/test_malformed_request.py:59 in test_cbmf001_bad_output_outputs() -> check.is_not_in("nope", response.text)
FAILURE: 
assert '500 Internal Server Error' in '{"response": {"o1": {"nope": 9}}, "multi": true}'
tests/integration/callbacks/test_malformed_request.py:60 in test_cbmf001_bad_output_outputs() -> check.is_in("500 Internal Server Error", response.text)
FAILURE: 
assert 200 == 500
  +200
  -500
tests/integration/callbacks/test_malformed_request.py:56 in test_cbmf001_bad_output_outputs() -> check.equal(response.status_code, 500)
FAILURE: 
assert 'children' not in '{"response": {"nope": {"children": 9}}, "multi": true}'
  'children' is contained here:
    {"response": {"nope": {"children": 9}}, "multi": true}
  ?                         ++++++++
tests/integration/callbacks/test_malformed_request.py:58 in test_cbmf001_bad_output_outputs() -> check.is_not_in("children", response.text)
FAILURE: 
assert 'nope' not in '{"response": {"nope": {"children": 9}}, "multi": true}'
  'nope' is contained here:
    {"response": {"nope": {"children": 9}}, "multi": true}
  ?                ++++
tests/integration/callbacks/test_malformed_request.py:59 in test_cbmf001_bad_output_outputs() -> check.is_not_in("nope", response.text)
FAILURE: 
assert '500 Internal Server Error' in '{"response": {"nope": {"children": 9}}, "multi": true}'
tests/integration/callbacks/test_malformed_request.py:60 in test_cbmf001_bad_output_outputs() -> check.is_in("500 Internal Server Error", response.text)
------------------------------------------------------------
Failed Checks: 8

Combined with the fact that this test passes in the CI build of this branch, I have high confidence that the current PR adequately addresses the underlying issue. 💃🏿

Very nice work @alexcjohnson, I learned a ton by looking at this code.

@alexcjohnson alexcjohnson merged commit 1ea21c7 into dev Feb 8, 2021
@alexcjohnson alexcjohnson deleted the cb-request-validation branch February 8, 2021 19:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants