From d0d449e45110c030269269c62895836e92982c94 Mon Sep 17 00:00:00 2001 From: David Dworken Date: Fri, 2 May 2025 11:27:03 -0700 Subject: [PATCH 1/7] 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 --- plotly/io/_html.py | 20 +++++++++-- tests/test_core/test_offline/test_offline.py | 2 +- tests/test_io/test_html.py | 36 ++++++++++++++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/plotly/io/_html.py b/plotly/io/_html.py index dbec1fc83b7..2bf7b12b156 100644 --- a/plotly/io/_html.py +++ b/plotly/io/_html.py @@ -1,6 +1,8 @@ import uuid from pathlib import Path import webbrowser +import hashlib +import base64 from _plotly_utils.optional_imports import get_module from plotly.io._utils import validate_coerce_fig_to_dict, plotly_cdn_url @@ -9,6 +11,14 @@ _json = get_module("json") +def _generate_sri_hash(content): + """Generate SHA256 hash for SRI (Subresource Integrity)""" + if isinstance(content, str): + content = content.encode('utf-8') + sha256_hash = hashlib.sha256(content).digest() + return 'sha256-' + base64.b64encode(sha256_hash).decode('utf-8') + + # Build script to set global PlotlyConfig object. This must execute before # plotly.js is loaded. _window_plotly_config = """\ @@ -252,11 +262,17 @@ def to_html( load_plotlyjs = "" if include_plotlyjs == "cdn": + # Generate SRI hash from the bundled plotly.js content + plotlyjs_content = get_plotlyjs() + sri_hash = _generate_sri_hash(plotlyjs_content) + load_plotlyjs = """\ {win_config} - \ + \ """.format( - win_config=_window_plotly_config, cdn_url=plotly_cdn_url() + win_config=_window_plotly_config, + cdn_url=plotly_cdn_url(), + integrity=sri_hash ) elif include_plotlyjs == "directory": diff --git a/tests/test_core/test_offline/test_offline.py b/tests/test_core/test_offline/test_offline.py index d0a9c80e1cb..afcf1f2e492 100644 --- a/tests/test_core/test_offline/test_offline.py +++ b/tests/test_core/test_offline/test_offline.py @@ -37,7 +37,7 @@ """ -cdn_script = ''.format( +cdn_script = '\n " ' ' + + '" integrity="sha256-oy6Be7Eh6eiQFs5M7oXuPxxm9qbJXEtTpfSI93dW16Q=" crossorigin="anonymous"> ' '
\n " ' ' + + '" integrity="sha256-oy6Be7Eh6eiQFs5M7oXuPxxm9qbJXEtTpfSI93dW16Q=" crossorigin="anonymous"> ' '
\ """.format( - win_config=_window_plotly_config, + win_config=_window_plotly_config, cdn_url=plotly_cdn_url(), - integrity=sri_hash + integrity=sri_hash, ) elif include_plotlyjs == "directory": diff --git a/tests/test_core/test_offline/test_offline.py b/tests/test_core/test_offline/test_offline.py index afcf1f2e492..53912ef45f2 100644 --- a/tests/test_core/test_offline/test_offline.py +++ b/tests/test_core/test_offline/test_offline.py @@ -37,9 +37,7 @@ """ -cdn_script = '\n " ' ' + + '" integrity="' + + sri_hash + + '" crossorigin="anonymous"> ' '