Skip to content

Commit 94c09ce

Browse files
committed
Merge remote-tracking branch 'upstream/dev' into danielklim/basedashrunner-host-option
2 parents b9ee54c + 2bda835 commit 94c09ce

File tree

8 files changed

+98
-15
lines changed

8 files changed

+98
-15
lines changed

CHANGELOG.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ This project adheres to [Semantic Versioning](https://semver.org/).
55
## UNRELEASED
66

77
### Changed
8+
- [#1531](https://github.com/plotly/dash/pull/1531) Update the format of the docstrings to make them easier to read in the reference pages of Dash Docs and in the console. This also addresses [#1205](https://github.com/plotly/dash/issues/1205)
89

9-
- [#1531](https://github.com/plotly/dash/pull/1531). Updates the format of the docstrings to make them easier to read in
10-
the reference pages of Dash Docs and in the console. This also addresses [#1205](https://github.com/plotly/dash/issues/1205)
11-
10+
### Fixed
11+
- [#1546](https://github.com/plotly/dash/pull/1546) Validate callback request `outputs` vs `output` to avoid a perceived security issue.
1212

1313
## [1.19.0] - 2021-01-19
1414

dash/_validate.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,26 @@ def validate_id_string(arg):
111111
)
112112

113113

114+
def validate_output_spec(output, output_spec, Output):
115+
"""
116+
This validation is for security and internal debugging, not for users,
117+
so the messages are not intended to be clear.
118+
`output` comes from the callback definition, `output_spec` from the request.
119+
"""
120+
if not isinstance(output, (list, tuple)):
121+
output, output_spec = [output], [output_spec]
122+
elif len(output) != len(output_spec):
123+
raise exceptions.CallbackException("Wrong length output_spec")
124+
125+
for outi, speci in zip(output, output_spec):
126+
speci_list = speci if isinstance(speci, (list, tuple)) else [speci]
127+
for specij in speci_list:
128+
if not Output(specij["id"], specij["property"]) == outi:
129+
raise exceptions.CallbackException(
130+
"Output does not match callback definition"
131+
)
132+
133+
114134
def validate_multi_return(outputs_list, output_value, callback_id):
115135
if not isinstance(output_value, (list, tuple)):
116136
raise exceptions.InvalidCallbackReturnValue(

dash/dash.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
from .fingerprint import build_fingerprint, check_fingerprint
2929
from .resources import Scripts, Css
30-
from .dependencies import handle_callback_args
30+
from .dependencies import handle_callback_args, Output
3131
from .development.base_component import ComponentRegistry
3232
from .exceptions import PreventUpdate, InvalidResourceError, ProxyError
3333
from .version import __version__
@@ -1004,6 +1004,7 @@ def wrap_func(func):
10041004
@wraps(func)
10051005
def add_context(*args, **kwargs):
10061006
output_spec = kwargs.pop("outputs_list")
1007+
_validate.validate_output_spec(output, output_spec, Output)
10071008

10081009
# don't touch the comment on the next line - used by debugger
10091010
output_value = func(*args, **kwargs) # %% callback invoked %%

dash/favicon.ico

100644100755
5.61 KB
Binary file not shown.

dash/testing/browser.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,10 @@ def __init__(
7777
)
7878
self.percy_runner.initialize_build()
7979

80-
logger.info("initialize browser with arguments")
81-
logger.info(" headless => %s", self._headless)
82-
logger.info(" download_path => %s", self._download_path)
83-
logger.info(" percy asset root => %s", os.path.abspath(percy_assets_root))
80+
logger.debug("initialize browser with arguments")
81+
logger.debug(" headless => %s", self._headless)
82+
logger.debug(" download_path => %s", self._download_path)
83+
logger.debug(" percy asset root => %s", os.path.abspath(percy_assets_root))
8484

8585
def __enter__(self):
8686
return self

requires-testing.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pytest-mock>=2.0.0,<3;python_version=="2.7"
66
lxml>=4.6.2
77
selenium>=3.141.0
88
percy>=2.0.2
9+
cryptography<3.4;python_version<"3.7"
910
requests[security]>=2.21.0
1011
beautifulsoup4>=4.8.2,<=4.9.3;python_version=="2.7"
1112
beautifulsoup4>=4.8.2;python_version>="3.0"
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import requests
2+
3+
4+
import dash_core_components as dcc
5+
import dash_html_components as html
6+
import dash
7+
from dash.dependencies import Input, Output
8+
9+
10+
def test_cbmf001_bad_output_outputs(dash_thread_server):
11+
app = dash.Dash(__name__)
12+
app.layout = html.Div(
13+
[
14+
dcc.Input(id="i", value="initial value"),
15+
html.Div(html.Div([1.5, None, "string", html.Div(id="o1")])),
16+
]
17+
)
18+
19+
@app.callback(Output("o1", "children"), [Input("i", "value")])
20+
def update_output(value):
21+
return value
22+
23+
dash_thread_server(app)
24+
25+
# first a good request
26+
response = requests.post(
27+
dash_thread_server.url + "/_dash-update-component",
28+
json=dict(
29+
output="o1.children",
30+
outputs={"id": "o1", "property": "children"},
31+
inputs=[{"id": "i", "property": "value", "value": 9}],
32+
changedPropIds=["i.value"],
33+
),
34+
)
35+
assert response.status_code == 200
36+
assert '"o1": {"children": 9}' in response.text
37+
38+
# now some bad ones
39+
outspecs = [
40+
{"output": "o1.nope", "outputs": {"id": "o1", "property": "nope"}},
41+
{"output": "o1.children", "outputs": {"id": "o1", "property": "nope"}},
42+
{"output": "o1.nope", "outputs": {"id": "o1", "property": "children"}},
43+
{"output": "o1.children", "outputs": {"id": "nope", "property": "children"}},
44+
{"output": "nope.children", "outputs": {"id": "nope", "property": "children"}},
45+
]
46+
for outspeci in outspecs:
47+
response = requests.post(
48+
dash_thread_server.url + "/_dash-update-component",
49+
json=dict(
50+
inputs=[{"id": "i", "property": "value", "value": 9}],
51+
changedPropIds=["i.value"],
52+
**outspeci
53+
),
54+
)
55+
assert response.status_code == 500
56+
assert "o1" not in response.text
57+
assert "children" not in response.text
58+
assert "nope" not in response.text
59+
assert "500 Internal Server Error" in response.text

tests/integration/renderer/test_iframe.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from multiprocessing import Value
2-
31
import dash
42
from dash.dependencies import Input, Output
53
from dash.exceptions import PreventUpdate
@@ -9,7 +7,6 @@
97

108
def test_rdif001_sandbox_allow_scripts(dash_duo):
119
app = dash.Dash(__name__)
12-
call_count = Value("i")
1310

1411
N_OUTPUTS = 50
1512

@@ -26,7 +23,6 @@ def update_output(n_clicks):
2623
if n_clicks is None:
2724
raise PreventUpdate
2825

29-
call_count.value += 1
3026
return ["{}={}".format(i, i + n_clicks) for i in range(N_OUTPUTS)]
3127

3228
@app.server.after_request
@@ -39,6 +35,11 @@ def apply_cors(response):
3935

4036
dash_duo.start_server(app)
4137

38+
dash_duo.find_element("#btn").click()
39+
dash_duo.wait_for_element("#output-0").text == "0=1"
40+
41+
assert dash_duo.get_logs() == []
42+
4243
iframe = """
4344
<!DOCTYPE html>
4445
<html>
@@ -53,8 +54,9 @@ def apply_cors(response):
5354

5455
dash_duo.driver.switch_to.frame(0)
5556

56-
dash_duo.wait_for_element("#output-0")
57-
dash_duo.wait_for_element_by_id("btn").click()
58-
dash_duo.wait_for_element("#output-0").text == "0=1"
57+
assert dash_duo.get_logs() == []
58+
59+
dash_duo.find_element("#btn").click()
60+
dash_duo.wait_for_text_to_equal("#output-0", "0=1")
5961

6062
assert dash_duo.get_logs() == []

0 commit comments

Comments
 (0)