Skip to content

Commit bf23454

Browse files
Nigel Sheridan-Smithcursoragent
authored andcommitted
security: replace advocate with champion and upgrade urllib3 to 2.x
Upgrade urllib3 to 2.7.0 with matching AWS SDK bumps, and restore SSRF protection via champion in one PR so ENFORCE_PRIVATE_ADDRESS_BLOCK is never left dead (supersedes #7745). - Remove unmaintained advocate (blocks urllib3 2.x) - urllib3 1.26.19 → 2.7.0; boto3/botocore → 1.43.7 - Add optional uv dependency group `ssrf` pinned to champion git rev - Default ENFORCE_PRIVATE_ADDRESS_BLOCK to false (opt-in + champion) - Wire requests_or_champion through HTTP/CSV/Excel query runners Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 2daefd2 commit bf23454

8 files changed

Lines changed: 73 additions & 84 deletions

File tree

pyproject.toml

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ maintainers = [
1212
readme = "README.md"
1313
requires-python = ">=3.13,<3.14"
1414
dependencies = [
15-
"advocate==1.0.0",
1615
"aniso8601==8.0.0",
1716
"authlib==1.7.2",
1817
"backoff==2.2.1",
@@ -69,7 +68,7 @@ dependencies = [
6968
"supervisor==4.1.0",
7069
"supervisor-checks==0.8.1",
7170
"ua-parser==0.18.0",
72-
"urllib3==1.26.19",
71+
"urllib3==2.7.0",
7372
"user-agents==2.0",
7473
"werkzeug==2.3.8",
7574
"wtforms==2.2.1",
@@ -96,8 +95,8 @@ all_ds = [
9695
"atsd-client==3.0.5",
9796
"azure-core>=1.38.0",
9897
"azure-kusto-data==5.0.1",
99-
"boto3==1.28.8",
100-
"botocore==1.31.8",
98+
"boto3==1.43.7",
99+
"botocore==1.43.7",
101100
"cassandra-driver==3.29.3",
102101
"certifi>=2019.9.11",
103102
"cmem-cmempy==21.2.3",
@@ -150,6 +149,14 @@ all_ds = [
150149
ldap3 = [
151150
"ldap3==2.9.1",
152151
]
152+
# Optional SSRF protection (enables REDASH_ENFORCE_PRIVATE_IP_BLOCK).
153+
# Install via `uv sync --group ssrf` or add `ssrf` to the install_groups
154+
# build arg in the Dockerfile.
155+
ssrf = [
156+
# Pinned to an immutable commit (champion has no PyPI release and no tags yet).
157+
# Bump deliberately when reviewing upstream changes.
158+
"champion @ git+https://github.com/Gee19/champion.git@74cf301bf89a88b8a55459fd8439766a11eb16f0",
159+
]
153160
dev = [
154161
"pytest==7.4.0",
155162
"coverage==7.2.7",

redash/query_runner/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from redash import settings, utils
1212
from redash.utils.requests_session import (
1313
UnacceptableAddressException,
14-
requests_or_advocate,
14+
requests_or_champion,
1515
requests_session,
1616
)
1717

@@ -392,14 +392,14 @@ def get_response(self, url, auth=None, http_method="get", **kwargs):
392392
if response.status_code != 200:
393393
error = "{} ({}).".format(self.response_error, response.status_code)
394394

395-
except requests_or_advocate.HTTPError as exc:
395+
except requests_or_champion.HTTPError as exc:
396396
logger.exception(exc)
397397
error = "Failed to execute query. "
398398
f"Return Code: {response.status_code} Reason: {response.text}"
399399
except UnacceptableAddressException as exc:
400400
logger.exception(exc)
401401
error = "Can't query private addresses."
402-
except requests_or_advocate.RequestException as exc:
402+
except requests_or_champion.RequestException as exc:
403403
# Catch all other requests exceptions and return the error.
404404
logger.exception(exc)
405405
error = str(exc)

redash/query_runner/csv.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from redash.query_runner import BaseQueryRunner, NotSupported, register
77
from redash.utils.requests_session import (
88
UnacceptableAddressException,
9-
requests_or_advocate,
9+
requests_or_champion,
1010
)
1111

1212
logger = logging.getLogger(__name__)
@@ -59,7 +59,7 @@ def run_query(self, query, user):
5959
pass
6060

6161
try:
62-
response = requests_or_advocate.get(url=path, headers={"User-agent": ua})
62+
response = requests_or_champion.get(url=path, headers={"User-agent": ua})
6363
workbook = pd.read_csv(io.BytesIO(response.content), sep=",", **args)
6464

6565
df = workbook.copy()

redash/query_runner/excel.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from redash.query_runner import BaseQueryRunner, NotSupported, register
66
from redash.utils.requests_session import (
77
UnacceptableAddressException,
8-
requests_or_advocate,
8+
requests_or_champion,
99
)
1010

1111
logger = logging.getLogger(__name__)
@@ -57,7 +57,7 @@ def run_query(self, query, user):
5757
pass
5858

5959
try:
60-
response = requests_or_advocate.get(url=path, headers={"User-agent": ua})
60+
response = requests_or_champion.get(url=path, headers={"User-agent": ua})
6161
workbook = pd.read_excel(response.content, **args)
6262

6363
df = workbook.copy()

redash/settings/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,11 @@
7272
# Whether file downloads are enforced or not.
7373
ENFORCE_FILE_SAVE = parse_boolean(os.environ.get("REDASH_ENFORCE_FILE_SAVE", "true"))
7474

75-
# Whether api calls using the json query runner will block private addresses
76-
ENFORCE_PRIVATE_ADDRESS_BLOCK = parse_boolean(os.environ.get("REDASH_ENFORCE_PRIVATE_IP_BLOCK", "true"))
75+
# Whether api calls using the json query runner will block private addresses.
76+
# Default off: requires the champion package (SSRF guard, modern fork of advocate).
77+
# Set REDASH_ENFORCE_PRIVATE_IP_BLOCK=true and install champion to enable
78+
# (e.g. pip install git+https://github.com/Gee19/champion.git).
79+
ENFORCE_PRIVATE_ADDRESS_BLOCK = parse_boolean(os.environ.get("REDASH_ENFORCE_PRIVATE_IP_BLOCK", "false"))
7780

7881
# Whether to use secure cookies by default.
7982
COOKIES_SECURE = parse_boolean(os.environ.get("REDASH_COOKIES_SECURE", str(ENFORCE_HTTPS)))

redash/utils/requests_session.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
1-
import warnings
2-
31
from redash import settings
42

5-
with warnings.catch_warnings():
6-
# Supress advocate warning below
7-
# /usr/local/lib/python3.13/site-packages/advocate/api.py:102: SyntaxWarning: invalid escape sequence '\*'
8-
# server-1 | :param \*\*kwargs: Optional arguments that ``request`` takes.
9-
warnings.filterwarnings("ignore", category=SyntaxWarning, module=r".*advocate.*")
3+
if settings.ENFORCE_PRIVATE_ADDRESS_BLOCK:
4+
try:
5+
import champion as requests_or_champion
6+
from champion.exceptions import (
7+
UnacceptableAddressException, # noqa: F401, E402
8+
)
9+
except ImportError as e:
10+
raise RuntimeError(
11+
"ENFORCE_PRIVATE_ADDRESS_BLOCK requires the champion package. "
12+
"Install it in your environment (e.g. pip install "
13+
"git+https://github.com/Gee19/champion.git)."
14+
) from e
15+
else:
16+
import requests as requests_or_champion
1017

11-
from advocate.exceptions import UnacceptableAddressException # noqa: F401, E402
18+
class UnacceptableAddressException(Exception):
19+
"""Only raised when champion is used (ENFORCE_PRIVATE_ADDRESS_BLOCK)."""
1220

13-
if settings.ENFORCE_PRIVATE_ADDRESS_BLOCK:
14-
import advocate as requests_or_advocate
15-
else:
16-
import requests as requests_or_advocate
21+
pass
1722

1823

19-
class ConfiguredSession(requests_or_advocate.Session):
24+
class ConfiguredSession(requests_or_champion.Session):
2025
def request(self, *args, **kwargs):
2126
if not settings.REQUESTS_ALLOW_REDIRECTS:
2227
kwargs.update({"allow_redirects": False})

tests/query_runner/test_http.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from redash.query_runner import BaseHTTPQueryRunner
66
from redash.utils.requests_session import (
77
ConfiguredSession,
8-
requests_or_advocate,
8+
requests_or_champion,
99
)
1010

1111

@@ -84,7 +84,7 @@ def test_get_response_httperror_exception(self, mock_get):
8484
mock_response = mock.Mock()
8585
mock_response.status_code = 500
8686
mock_response.text = "Server Error"
87-
http_error = requests_or_advocate.HTTPError()
87+
http_error = requests_or_champion.HTTPError()
8888
mock_response.raise_for_status.side_effect = http_error
8989
mock_get.return_value = mock_response
9090

@@ -101,7 +101,7 @@ def test_get_response_requests_exception(self, mock_get):
101101
mock_response.status_code = 500
102102
mock_response.text = "Server Error"
103103
exception_message = "Some requests exception"
104-
requests_exception = requests_or_advocate.RequestException(exception_message)
104+
requests_exception = requests_or_champion.RequestException(exception_message)
105105
mock_response.raise_for_status.side_effect = requests_exception
106106
mock_get.return_value = mock_response
107107

uv.lock

Lines changed: 29 additions & 55 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)