Skip to content

Commit a0288c5

Browse files
authored
Merge branch 'master' into axis_spanning_layout_object_xref_yref_bug
2 parents c2860de + fc3ef00 commit a0288c5

File tree

5 files changed

+62
-5
lines changed

5 files changed

+62
-5
lines changed

Diff for: CHANGELOG.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ This project adheres to [Semantic Versioning](http://semver.org/).
66

77
### Fixed
88
- Fixed another compatibility issue with Pandas 2.0, just affecting `px.*(line_close=True)` [[#4190](https://github.com/plotly/plotly.py/pull/4190)]
9+
910
- Added some rounding to the `make_subplots` function to handle situations where the user-input specs cause the domain to exceed 1 by small amounts https://github.com/plotly/plotly.py/pull/4153
11+
- Sanitize JSON output to prevent an XSS vector when graphs are inserted directly into HTML [[#4196](https://github.com/plotly/plotly.py/pull/4196)]
1012
- Fixed issue with shapes and annotations plotting on the wrong y axis when supplied with a specific axis in the `yref` parameter [[#4177](https://github.com/plotly/plotly.py/pull/4177)]
11-
12-
13+
1314

1415
## [5.14.1] - 2023-04-05
1516

Diff for: packages/python/plotly/plotly/io/_json.py

+26-3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,25 @@ def coerce_to_strict(const):
5757
return const
5858

5959

60+
_swap_json = (
61+
("<", "\\u003c"),
62+
(">", "\\u003e"),
63+
("/", "\\u002f"),
64+
)
65+
_swap_orjson = _swap_json + (
66+
("\u2028", "\\u2028"),
67+
("\u2029", "\\u2029"),
68+
)
69+
70+
71+
def _safe(json_str, _swap):
72+
out = json_str
73+
for unsafe_char, safe_char in _swap:
74+
if unsafe_char in out:
75+
out = out.replace(unsafe_char, safe_char)
76+
return out
77+
78+
6079
def to_json_plotly(plotly_object, pretty=False, engine=None):
6180
"""
6281
Convert a plotly/Dash object to a JSON string representation
@@ -120,7 +139,9 @@ def to_json_plotly(plotly_object, pretty=False, engine=None):
120139

121140
from _plotly_utils.utils import PlotlyJSONEncoder
122141

123-
return json.dumps(plotly_object, cls=PlotlyJSONEncoder, **opts)
142+
return _safe(
143+
json.dumps(plotly_object, cls=PlotlyJSONEncoder, **opts), _swap_json
144+
)
124145
elif engine == "orjson":
125146
JsonConfig.validate_orjson()
126147
opts = orjson.OPT_NON_STR_KEYS | orjson.OPT_SERIALIZE_NUMPY
@@ -136,7 +157,9 @@ def to_json_plotly(plotly_object, pretty=False, engine=None):
136157

137158
# Try without cleaning
138159
try:
139-
return orjson.dumps(plotly_object, option=opts).decode("utf8")
160+
return _safe(
161+
orjson.dumps(plotly_object, option=opts).decode("utf8"), _swap_orjson
162+
)
140163
except TypeError:
141164
pass
142165

@@ -146,7 +169,7 @@ def to_json_plotly(plotly_object, pretty=False, engine=None):
146169
datetime_allowed=True,
147170
modules=modules,
148171
)
149-
return orjson.dumps(cleaned, option=opts).decode("utf8")
172+
return _safe(orjson.dumps(cleaned, option=opts).decode("utf8"), _swap_orjson)
150173

151174

152175
def to_json(fig, validate=True, pretty=False, remove_uids=True, engine=None):

Diff for: packages/python/plotly/plotly/tests/test_io/test_to_from_plotly_json.py

+31
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import pandas as pd
77
import json
88
import datetime
9+
import re
910
import sys
1011
from pytz import timezone
1112
from _plotly_utils.optional_imports import get_module
@@ -201,6 +202,14 @@ def to_str(v):
201202

202203
array_str = to_json_test(dt_values)
203204
expected = build_test_dict_string(array_str)
205+
if orjson:
206+
# orjson always serializes datetime64 to ns, but json will return either
207+
# full seconds or microseconds, if the rest is zeros.
208+
# we don't care about any trailing zeros
209+
trailing_zeros = re.compile(r'[.]?0+"')
210+
result = trailing_zeros.sub('"', result)
211+
expected = trailing_zeros.sub('"', expected)
212+
204213
assert result == expected
205214
check_roundtrip(result, engine=engine, pretty=pretty)
206215

@@ -221,3 +230,25 @@ def test_mixed_string_nonstring_key(engine, pretty):
221230
value = build_test_dict({0: 1, "a": 2})
222231
result = pio.to_json_plotly(value, engine=engine)
223232
check_roundtrip(result, engine=engine, pretty=pretty)
233+
234+
235+
def test_sanitize_json(engine):
236+
layout = {"title": {"text": "</script>\u2028\u2029"}}
237+
fig = go.Figure(layout=layout)
238+
fig_json = pio.to_json_plotly(fig, engine=engine)
239+
layout_2 = json.loads(fig_json)["layout"]
240+
del layout_2["template"]
241+
242+
assert layout == layout_2
243+
244+
replacements = {
245+
"<": "\\u003c",
246+
">": "\\u003e",
247+
"/": "\\u002f",
248+
"\u2028": "\\u2028",
249+
"\u2029": "\\u2029",
250+
}
251+
252+
for bad, good in replacements.items():
253+
assert bad not in fig_json
254+
assert good in fig_json

Diff for: packages/python/plotly/test_requirements/requirements_37_optional.txt

+1
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ matplotlib==2.2.3
1919
scikit-image==0.14.4
2020
psutil==5.7.0
2121
kaleido
22+
orjson==3.8.12

Diff for: packages/python/plotly/test_requirements/requirements_39_optional.txt

+1
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ matplotlib==2.2.3
1919
scikit-image==0.18.1
2020
psutil==5.7.0
2121
kaleido
22+
orjson==3.8.12

0 commit comments

Comments
 (0)