Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 12cd7a6

Browse files
committed
merging recent changes from main
2 parents 4a1a814 + 1ec864b commit 12cd7a6

File tree

5 files changed

+77
-5
lines changed

5 files changed

+77
-5
lines changed

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
2020
- Add support for Kaleido>=v1.0.0 for image generation [[#5062](https://github.com/plotly/plotly.py/pull/5062), [#5177](https://github.com/plotly/plotly.py/pull/5177)]
2121
- Reduce package bundle size by 18-24% via changes to code generation [[#4978](https://github.com/plotly/plotly.py/pull/4978)]
2222

23+
### Added
24+
- Add SRI (Subresource Integrity) hash support for CDN script tags when using `include_plotlyjs='cdn'`. This enhances security by ensuring browser verification of CDN-served plotly.js files [[#PENDING](https://github.com/plotly/plotly.py/pull/PENDING)]
25+
2326
### Fixed
2427
- Fix third-party widget display issues in v6 [[#5102](https://github.com/plotly/plotly.py/pull/5102)]
2528
- Add handling for case where `jupyterlab` or `notebook` is not installed [[#5104](https://github.com/plotly/plotly.py/pull/5104/files)]

‎plotly/io/_html.py‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import uuid
22
from pathlib import Path
33
import webbrowser
4+
import hashlib
5+
import base64
46

57
from _plotly_utils.optional_imports import get_module
68
from plotly.io._utils import validate_coerce_fig_to_dict, plotly_cdn_url
@@ -9,6 +11,14 @@
911
_json = get_module("json")
1012

1113

14+
def _generate_sri_hash(content):
15+
"""Generate SHA256 hash for SRI (Subresource Integrity)"""
16+
if isinstance(content, str):
17+
content = content.encode("utf-8")
18+
sha256_hash = hashlib.sha256(content).digest()
19+
return "sha256-" + base64.b64encode(sha256_hash).decode("utf-8")
20+
21+
1222
# Build script to set global PlotlyConfig object. This must execute before
1323
# plotly.js is loaded.
1424
_window_plotly_config = """\
@@ -244,10 +254,18 @@ def to_html(
244254
load_plotlyjs = ""
245255

246256
if include_plotlyjs == "cdn":
257+
# Generate SRI hash from the bundled plotly.js content
258+
plotlyjs_content = get_plotlyjs()
259+
sri_hash = _generate_sri_hash(plotlyjs_content)
260+
247261
load_plotlyjs = """\
248262
{win_config}
249-
<script charset="utf-8" src="{cdn_url}"></script>\
250-
""".format(win_config=_window_plotly_config, cdn_url=plotly_cdn_url())
263+
<script charset="utf-8" src="{cdn_url}" integrity="{integrity}" crossorigin="anonymous"></script>\
264+
""".format(
265+
win_config=_window_plotly_config,
266+
cdn_url=plotly_cdn_url(),
267+
integrity=sri_hash,
268+
)
251269

252270
elif include_plotlyjs == "directory":
253271
load_plotlyjs = """\

‎tests/test_core/test_offline/test_offline.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
from unittest import TestCase
99

1010
import plotly
11+
from plotly.offline import get_plotlyjs
1112
import plotly.io as pio
1213
from plotly.io._utils import plotly_cdn_url
14+
from plotly.io._html import _generate_sri_hash
1315

1416
packages_root = os.path.dirname(
1517
os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(plotly.__file__))))
@@ -36,8 +38,8 @@
3638
<script type="text/javascript">\
3739
window.PlotlyConfig = {MathJaxConfig: 'local'};</script>"""
3840

39-
cdn_script = '<script charset="utf-8" src="{cdn_url}"></script>'.format(
40-
cdn_url=plotly_cdn_url()
41+
cdn_script = '<script charset="utf-8" src="{cdn_url}" integrity="{js_hash}" crossorigin="anonymous"></script>'.format(
42+
cdn_url=plotly_cdn_url(), js_hash=_generate_sri_hash(get_plotlyjs())
4143
)
4244

4345
directory_script = '<script charset="utf-8" src="plotly.min.js"></script>'

‎tests/test_io/test_html.py‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import pytest
22
import numpy as np
3+
import re
34

45

56
import plotly.graph_objs as go
67
import plotly.io as pio
78
from plotly.io._utils import plotly_cdn_url
9+
from plotly.offline.offline import get_plotlyjs
10+
from plotly.io._html import _generate_sri_hash
811

912

1013
@pytest.fixture
@@ -30,3 +33,41 @@ def test_html_deterministic(fig1):
3033
assert pio.to_html(fig1, include_plotlyjs="cdn", div_id=div_id) == pio.to_html(
3134
fig1, include_plotlyjs="cdn", div_id=div_id
3235
)
36+
37+
38+
def test_cdn_includes_integrity_attribute(fig1):
39+
"""Test that the CDN script tag includes an integrity attribute with SHA256 hash"""
40+
html_output = pio.to_html(fig1, include_plotlyjs="cdn")
41+
42+
# Check that the script tag includes integrity attribute
43+
assert 'integrity="sha256-' in html_output
44+
assert 'crossorigin="anonymous"' in html_output
45+
46+
# Verify it's in the correct script tag
47+
cdn_pattern = re.compile(
48+
r'<script[^>]*src="'
49+
+ re.escape(plotly_cdn_url())
50+
+ r'"[^>]*integrity="sha256-[A-Za-z0-9+/=]+"[^>]*>'
51+
)
52+
match = cdn_pattern.search(html_output)
53+
assert match is not None, "CDN script tag with integrity attribute not found"
54+
55+
56+
def test_cdn_integrity_hash_matches_bundled_content(fig1):
57+
"""Test that the SRI hash in CDN script tag matches the bundled plotly.js content"""
58+
html_output = pio.to_html(fig1, include_plotlyjs="cdn")
59+
60+
# Extract the integrity hash from the HTML output
61+
integrity_pattern = re.compile(r'integrity="(sha256-[A-Za-z0-9+/=]+)"')
62+
match = integrity_pattern.search(html_output)
63+
assert match is not None, "Integrity attribute not found"
64+
extracted_hash = match.group(1)
65+
66+
# Generate expected hash from bundled content
67+
plotlyjs_content = get_plotlyjs()
68+
expected_hash = _generate_sri_hash(plotlyjs_content)
69+
70+
# Verify they match
71+
assert (
72+
extracted_hash == expected_hash
73+
), f"Hash mismatch: expected {expected_hash}, got {extracted_hash}"

‎tests/test_io/test_renderers.py‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import plotly.io as pio
1212
from plotly.offline import get_plotlyjs
1313
from plotly.io._utils import plotly_cdn_url
14+
from plotly.io._html import _generate_sri_hash
1415

1516
import unittest.mock as mock
1617
from unittest.mock import MagicMock
@@ -292,12 +293,19 @@ def test_repr_html(renderer):
292293
# id number of figure
293294
id_html = str_html.split('document.getElementById("')[1].split('")')[0]
294295
id_pattern = "cd462b94-79ce-42a2-887f-2650a761a144"
296+
297+
# Calculate the SRI hash dynamically
298+
plotlyjs_content = get_plotlyjs()
299+
sri_hash = _generate_sri_hash(plotlyjs_content)
300+
295301
template = (
296302
'<div> <script type="text/javascript">'
297303
"window.PlotlyConfig = {MathJaxConfig: 'local'};</script>\n "
298304
'<script charset="utf-8" src="'
299305
+ plotly_cdn_url()
300-
+ '"></script> '
306+
+ '" integrity="'
307+
+ sri_hash
308+
+ '" crossorigin="anonymous"></script> '
301309
'<div id="cd462b94-79ce-42a2-887f-2650a761a144" class="plotly-graph-div" '
302310
'style="height:100%; width:100%;"></div> <script type="text/javascript">'
303311
" window.PLOTLYENV=window.PLOTLYENV || {};"

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /