Compare commits

...

2 Commits

Author SHA1 Message Date
Evan
c3bf19f633 test(dashboard): cover reserved-char native_filters in permalink redirect
Add an integration test ensuring a stored native_filters urlParam value
containing reserved characters (&, =, #) is percent-encoded in the
redirect Location header and cannot inject extra query parameters.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 09:34:14 -07:00
Claude Code
e1d94c0a56 fix(dashboard): URL-encode native_filters in permalink redirect
dashboard_permalink concatenated the native_filters param value into the
redirect URL without URL-encoding (every other param went through
parse.urlencode). A permalink whose stored native_filters value contained
'&'/'#'/'=' could inject additional query parameters into the redirect target,
modifying the dashboard filter state shown to a victim.

Encode all param values uniformly; Flask decodes them back on read, so
legitimate native_filters values are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 18:03:39 -07:00
2 changed files with 34 additions and 7 deletions

View File

@@ -864,12 +864,11 @@ class Superset(BaseSupersetView):
)
if url_params := state.get("urlParams"):
for param_key, param_val in url_params:
if param_key == "native_filters":
# native_filters doesnt need to be encoded here
url = f"{url}&native_filters={param_val}"
else:
params = parse.urlencode([(param_key, param_val)])
url = f"{url}&{params}"
# URL-encode every param value (including native_filters) so a
# value containing '&'/'#'/'=' cannot inject extra parameters
# into the redirect target. Flask decodes the value back on read.
params = parse.urlencode([(param_key, param_val)])
url = f"{url}&{params}"
if original_params := request.query_string.decode():
url = f"{url}&{original_params}"
if hash_ := state.get("anchor", state.get("hash")):

View File

@@ -23,7 +23,7 @@ import logging
import random
import unittest
from unittest import mock
from urllib.parse import quote
from urllib.parse import parse_qs, quote, urlsplit
import pandas as pd
import pytest
@@ -909,6 +909,34 @@ class TestCore(SupersetTestCase):
assert resp.headers["Location"] == expected_url
assert resp.status_code == 302
@mock.patch("superset.views.core.request")
@mock.patch(
"superset.commands.dashboard.permalink.get.GetDashboardPermalinkCommand.run"
)
def test_dashboard_permalink_native_filters_encoded(
self, get_dashboard_permalink_mock, request_mock
):
# A native_filters value containing reserved characters must be
# percent-encoded in the redirect target so it cannot inject extra
# query parameters into the Location header.
request_mock.query_string = b""
native_filters_value = "value&injected=evil#frag"
get_dashboard_permalink_mock.return_value = {
"dashboardId": 1,
"state": {"urlParams": [["native_filters", native_filters_value]]},
}
self.login(ADMIN_USERNAME)
resp = self.client.get("superset/dashboard/p/123/")
location = resp.headers["Location"]
assert resp.status_code == 302
# The reserved characters are encoded, so no extra params/anchors leak in.
assert "native_filters=value%26injected%3Devil%23frag" in location
assert "injected=evil" not in location
# Round-trips back to the original value when decoded.
parsed = parse_qs(urlsplit(location).query)
assert parsed["native_filters"] == [native_filters_value]
class TestLocalePatch(SupersetTestCase):
MOCK_LANGUAGES = (