Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions lib/matplotlib/backends/backend_webagg_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
48 changes: 48 additions & 0 deletions lib/matplotlib/tests/test_backend_webagg.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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
Loading