Skip to content

Commit d0d449e

Browse files
committed
Add SRI (Subresource Integrity) hash to CDN script tags
When include_plotlyjs='cdn', the generated HTML now includes an integrity attribute with a SHA256 hash of the bundled plotly.js content. This provides enhanced security by ensuring the browser verifies the integrity of the CDN-served file. - Added _generate_sri_hash() function to create SHA256 hashes - Modified CDN script tag generation to include integrity and crossorigin attributes - Added comprehensive tests to verify SRI functionality - Updated existing tests to account for new script tag format
1 parent a2697d5 commit d0d449e

File tree

3 files changed

+55
-3
lines changed

3 files changed

+55
-3
lines changed

plotly/io/_html.py

Lines changed: 18 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 = """\
@@ -252,11 +262,17 @@ def to_html(
252262
load_plotlyjs = ""
253263

254264
if include_plotlyjs == "cdn":
265+
# Generate SRI hash from the bundled plotly.js content
266+
plotlyjs_content = get_plotlyjs()
267+
sri_hash = _generate_sri_hash(plotlyjs_content)
268+
255269
load_plotlyjs = """\
256270
{win_config}
257-
<script charset="utf-8" src="{cdn_url}"></script>\
271+
<script charset="utf-8" src="{cdn_url}" integrity="{integrity}" crossorigin="anonymous"></script>\
258272
""".format(
259-
win_config=_window_plotly_config, cdn_url=plotly_cdn_url()
273+
win_config=_window_plotly_config,
274+
cdn_url=plotly_cdn_url(),
275+
integrity=sri_hash
260276
)
261277

262278
elif include_plotlyjs == "directory":

tests/test_core/test_offline/test_offline.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
<script type="text/javascript">\
3838
window.PlotlyConfig = {MathJaxConfig: 'local'};</script>"""
3939

40-
cdn_script = '<script charset="utf-8" src="{cdn_url}"></script>'.format(
40+
cdn_script = '<script charset="utf-8" src="{cdn_url}"'.format(
4141
cdn_url=plotly_cdn_url()
4242
)
4343

tests/test_io/test_html.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import plotly.graph_objs as go
88
import plotly.io as pio
99
from plotly.io._utils import plotly_cdn_url
10+
from plotly.offline.offline import get_plotlyjs
11+
from plotly.io._html import _generate_sri_hash
1012

1113

1214
if sys.version_info >= (3, 3):
@@ -46,3 +48,37 @@ def test_html_deterministic(fig1):
4648
assert pio.to_html(fig1, include_plotlyjs="cdn", div_id=div_id) == pio.to_html(
4749
fig1, include_plotlyjs="cdn", div_id=div_id
4850
)
51+
52+
53+
def test_cdn_includes_integrity_attribute(fig1):
54+
"""Test that the CDN script tag includes an integrity attribute with SHA256 hash"""
55+
html_output = pio.to_html(fig1, include_plotlyjs="cdn")
56+
57+
# Check that the script tag includes integrity attribute
58+
assert 'integrity="sha256-' in html_output
59+
assert 'crossorigin="anonymous"' in html_output
60+
61+
# Verify it's in the correct script tag
62+
import re
63+
cdn_pattern = re.compile(r'<script[^>]*src="' + re.escape(plotly_cdn_url()) + r'"[^>]*integrity="sha256-[A-Za-z0-9+/=]+"[^>]*>')
64+
match = cdn_pattern.search(html_output)
65+
assert match is not None, "CDN script tag with integrity attribute not found"
66+
67+
68+
def test_cdn_integrity_hash_matches_bundled_content(fig1):
69+
"""Test that the SRI hash in CDN script tag matches the bundled plotly.js content"""
70+
html_output = pio.to_html(fig1, include_plotlyjs="cdn")
71+
72+
# Extract the integrity hash from the HTML output
73+
import re
74+
integrity_pattern = re.compile(r'integrity="(sha256-[A-Za-z0-9+/=]+)"')
75+
match = integrity_pattern.search(html_output)
76+
assert match is not None, "Integrity attribute not found"
77+
extracted_hash = match.group(1)
78+
79+
# Generate expected hash from bundled content
80+
plotlyjs_content = get_plotlyjs()
81+
expected_hash = _generate_sri_hash(plotlyjs_content)
82+
83+
# Verify they match
84+
assert extracted_hash == expected_hash, f"Hash mismatch: expected {expected_hash}, got {extracted_hash}"

0 commit comments

Comments
 (0)