diff --git a/lib/matplotlib/backends/backend_webagg_core.py b/lib/matplotlib/backends/backend_webagg_core.py index 8062c7eabf4d..abd958fb0bcc 100644 --- a/lib/matplotlib/backends/backend_webagg_core.py +++ b/lib/matplotlib/backends/backend_webagg_core.py @@ -349,8 +349,11 @@ def _handle_key(self, event): handle_key_press = handle_key_release = _handle_key def handle_toolbar_button(self, event): - # TODO: Be more suspicious of the input - getattr(self.toolbar, event['name'])() + name = event['name'] + for item in self.toolbar.toolitems: + if item[3] is not None and name == item[3]: + getattr(self.toolbar, name)() + break def handle_refresh(self, event): if self.manager: diff --git a/lib/matplotlib/tests/test_backend_webagg.py b/lib/matplotlib/tests/test_backend_webagg.py index 1d6769494ef9..c63534ad20e3 100644 --- a/lib/matplotlib/tests/test_backend_webagg.py +++ b/lib/matplotlib/tests/test_backend_webagg.py @@ -1,8 +1,13 @@ import os import sys +from unittest.mock import MagicMock + import pytest import matplotlib.backends.backend_webagg_core +from matplotlib.backends.backend_webagg_core import ( + FigureCanvasWebAggCore, NavigationToolbar2WebAgg, +) from matplotlib.testing import subprocess_run_for_testing @@ -30,3 +35,46 @@ def test_webagg_fallback(backend): def test_webagg_core_no_toolbar(): fm = matplotlib.backends.backend_webagg_core.FigureManagerWebAgg assert fm._toolbar2_class is None + + +def test_toolbar_button_dispatch_allowlist(): + """Only declared toolbar items should be dispatched.""" + fig = MagicMock() + canvas = FigureCanvasWebAggCore(fig) + canvas.toolbar = MagicMock(spec=NavigationToolbar2WebAgg) + canvas.toolbar.toolitems = NavigationToolbar2WebAgg.toolitems + + # Valid toolbar action should be dispatched. + canvas.handle_toolbar_button({'name': 'home'}) + canvas.toolbar.home.assert_called_once() + + # Invalid names should be silently ignored. + canvas.toolbar.reset_mock() + canvas.handle_toolbar_button({'name': '__init__'}) + canvas.handle_toolbar_button({'name': 'not_a_real_button'}) + # No methods should have been called. + assert canvas.toolbar.method_calls == [] + + +@pytest.mark.parametrize("host, origin, allowed", [ + ("localhost:8988", "http://localhost:8988", True), + ("localhost:8988", "http://evil.com", False), + ("localhost:8988", "http://127.0.0.1:8988", False), + ("localhost:8988", "http://[::1]:8988", False), + ("127.0.0.1:8988", "http://127.0.0.1:8988", True), + ("127.0.0.1:8988", "http://localhost:8988", False), + ("127.0.0.1:8988", "http://[::1]:8988", False), + ("[::1]:8988", "http://[::1]:8988", True), + ("[::1]:8988", "http://[::2]:8988", False), + ("[::1]:8988", "http://localhost:8988", False), + ("[::1]:8988", "http://evil.com", False), +]) +def test_websocket_rejects_cross_origin(host, origin, allowed): + """Verify Tornado's default check_origin rejects cross-origin requests.""" + pytest.importorskip("tornado") + from matplotlib.backends.backend_webagg import WebAggApplication + + ws = WebAggApplication.WebSocket.__new__(WebAggApplication.WebSocket) + ws.request = MagicMock() + ws.request.headers = {"Host": host} + assert ws.check_origin(origin) is allowed