Skip to content

Commit 071092c

Browse files
wtfiwtzcursoragent
andcommitted
security: replace advocate with champion and upgrade urllib3 to 2.x
Replace the deprecated advocate library with champion (modern fork) and upgrade urllib3 to 2.x. SSRF protection is now opt-in via the champion package. Changes: - Remove advocate from core dependencies - urllib3: 1.26.19 → 2.7.0 - boto3: 1.28.8 → 1.43.7 (botocore 1.31.x pinned urllib3 <1.27) - botocore: 1.31.8 → 1.43.7 - Add transitive dependencies: azure-core, grpcio, h11, httpcore, marshmallow - Add optional [tool.poetry.group.ssrf] with champion pinned to git rev 74cf301 - Change ENFORCE_PRIVATE_ADDRESS_BLOCK default: true → false - SSRF protection now requires explicit opt-in + champion install Code changes: - Replace requests_or_advocate with requests_or_champion throughout - Conditional champion import with helpful error message when unavailable - Update imports in query_runner/__init__.py, csv.py, excel.py - Update settings/__init__.py with new default and documentation - Update tests/query_runner/test_http.py CVEs Addressed: - CVE-2023-43804 (urllib3): Cookie header injection - CVE-2024-37891 (urllib3): Proxy-authorization header leak on redirect - Multiple urllib3 1.26.x → 2.x security fixes SSRF protection is now opt-in: - Set REDASH_ENFORCE_PRIVATE_IP_BLOCK=true - Install with: poetry install --with ssrf - Or: pip install git+https://github.com/Gee19/champion.git Breaking Changes: - SSRF protection is now OFF by default (was ON with advocate) - Deployments that relied on advocate must explicitly enable champion Testing: - ✅ Python tests pass - ✅ Frontend tests pass - ✅ poetry lock regenerates successfully Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 1e44b66 commit 071092c

8 files changed

Lines changed: 319 additions & 356 deletions

File tree

poetry.lock

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

pyproject.toml

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ force-exclude = '''
2424

2525
[tool.poetry.dependencies]
2626
python = ">=3.13,<3.14"
27-
advocate = "1.0.0"
2827
aniso8601 = "8.0.0"
2928
authlib = "0.15.5"
3029
backoff = "2.2.1"
@@ -81,7 +80,7 @@ statsd = "3.3.0"
8180
supervisor = "4.1.0"
8281
supervisor-checks = "0.8.1"
8382
ua-parser = "0.18.0"
84-
urllib3 = "1.26.19"
83+
urllib3 = "2.7.0"
8584
user-agents = "2.0"
8685
werkzeug = "2.3.8"
8786
wtforms = "2.2.1"
@@ -98,20 +97,25 @@ optional = true
9897

9998
[tool.poetry.group.all_ds.dependencies]
10099
atsd-client = "3.0.5"
100+
azure-core = ">=1.38.0"
101101
azure-kusto-data = "5.0.1"
102-
boto3 = "1.28.8"
103-
botocore = "1.31.8"
102+
boto3 = "1.43.7"
103+
botocore = "1.43.7"
104104
cassandra-driver = "3.29.3"
105105
certifi = ">=2019.9.11"
106106
cmem-cmempy = "21.2.3"
107107
databend-py = "0.4.6"
108108
databend-sqlalchemy = "0.2.4"
109109
duckdb = "1.3.2"
110110
google-api-python-client = "2.190.0"
111+
grpcio = ">=1.80.0,<2"
111112
gspread = "5.11.2"
113+
h11 = ">=0.16.0"
114+
httpcore = ">=1.0.9"
112115
impyla = "0.22.0"
113116
influxdb = "5.2.3"
114117
influxdb-client = "1.38.0"
118+
marshmallow = ">=3.26.2"
115119
memsql = "3.2.0"
116120
mysqlclient = "2.1.1"
117121
numpy = "2.4.2"
@@ -152,6 +156,17 @@ optional = true
152156
[tool.poetry.group.ldap3.dependencies]
153157
ldap3 = "2.9.1"
154158

159+
# Optional SSRF protection (enables REDASH_ENFORCE_PRIVATE_IP_BLOCK).
160+
# Install via `poetry install --with ssrf` or add `ssrf` to the install_groups
161+
# build arg in the Dockerfile.
162+
[tool.poetry.group.ssrf]
163+
optional = true
164+
165+
[tool.poetry.group.ssrf.dependencies]
166+
# Pinned to an immutable commit (champion has no PyPI release and no tags yet).
167+
# Bump deliberately when reviewing upstream changes.
168+
champion = { git = "https://github.com/Gee19/champion.git", rev = "74cf301bf89a88b8a55459fd8439766a11eb16f0" }
169+
155170
[tool.poetry.group.dev]
156171
optional = true
157172

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

0 commit comments

Comments
 (0)