Skip to content
Draft
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
594 changes: 267 additions & 327 deletions poetry.lock

Large diffs are not rendered by default.

23 changes: 19 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ force-exclude = '''

[tool.poetry.dependencies]
python = ">=3.13,<3.14"
advocate = "1.0.0"
aniso8601 = "8.0.0"
authlib = "0.15.5"
backoff = "2.2.1"
Expand Down Expand Up @@ -81,7 +80,7 @@ statsd = "3.3.0"
supervisor = "4.1.0"
supervisor-checks = "0.8.1"
ua-parser = "0.18.0"
urllib3 = "1.26.19"
urllib3 = "2.7.0"
user-agents = "2.0"
werkzeug = "2.3.8"
wtforms = "2.2.1"
Expand All @@ -98,20 +97,25 @@ optional = true

[tool.poetry.group.all_ds.dependencies]
atsd-client = "3.0.5"
azure-core = ">=1.38.0"
azure-kusto-data = "5.0.1"
boto3 = "1.28.8"
botocore = "1.31.8"
boto3 = "1.43.7"
botocore = "1.43.7"
cassandra-driver = "3.29.3"
certifi = ">=2019.9.11"
cmem-cmempy = "21.2.3"
databend-py = "0.4.6"
databend-sqlalchemy = "0.2.4"
duckdb = "1.3.2"
google-api-python-client = "2.190.0"
grpcio = ">=1.80.0,<2"
gspread = "5.11.2"
h11 = ">=0.16.0"
httpcore = ">=1.0.9"
impyla = "0.22.0"
influxdb = "5.2.3"
influxdb-client = "1.38.0"
marshmallow = ">=3.26.2"
memsql = "3.2.0"
mysqlclient = "2.1.1"
numpy = "2.4.2"
Expand Down Expand Up @@ -152,6 +156,17 @@ optional = true
[tool.poetry.group.ldap3.dependencies]
ldap3 = "2.9.1"

# Optional SSRF protection (enables REDASH_ENFORCE_PRIVATE_IP_BLOCK).
# Install via `poetry install --with ssrf` or add `ssrf` to the install_groups
# build arg in the Dockerfile.
[tool.poetry.group.ssrf]
optional = true

[tool.poetry.group.ssrf.dependencies]
# Pinned to an immutable commit (champion has no PyPI release and no tags yet).
# Bump deliberately when reviewing upstream changes.
champion = { git = "https://github.com/Gee19/champion.git", rev = "74cf301bf89a88b8a55459fd8439766a11eb16f0" }

[tool.poetry.group.dev]
optional = true

Expand Down
6 changes: 3 additions & 3 deletions redash/query_runner/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from redash import settings, utils
from redash.utils.requests_session import (
UnacceptableAddressException,
requests_or_advocate,
requests_or_champion,
requests_session,
)

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

except requests_or_advocate.HTTPError as exc:
except requests_or_champion.HTTPError as exc:
logger.exception(exc)
error = "Failed to execute query. "
f"Return Code: {response.status_code} Reason: {response.text}"
except UnacceptableAddressException as exc:
logger.exception(exc)
error = "Can't query private addresses."
except requests_or_advocate.RequestException as exc:
except requests_or_champion.RequestException as exc:
# Catch all other requests exceptions and return the error.
logger.exception(exc)
error = str(exc)
Expand Down
4 changes: 2 additions & 2 deletions redash/query_runner/csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from redash.query_runner import BaseQueryRunner, NotSupported, register
from redash.utils.requests_session import (
UnacceptableAddressException,
requests_or_advocate,
requests_or_champion,
)

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -59,7 +59,7 @@ def run_query(self, query, user):
pass

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

df = workbook.copy()
Expand Down
4 changes: 2 additions & 2 deletions redash/query_runner/excel.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from redash.query_runner import BaseQueryRunner, NotSupported, register
from redash.utils.requests_session import (
UnacceptableAddressException,
requests_or_advocate,
requests_or_champion,
)

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -57,7 +57,7 @@ def run_query(self, query, user):
pass

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

df = workbook.copy()
Expand Down
7 changes: 5 additions & 2 deletions redash/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,11 @@
# Whether file downloads are enforced or not.
ENFORCE_FILE_SAVE = parse_boolean(os.environ.get("REDASH_ENFORCE_FILE_SAVE", "true"))

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

# Whether to use secure cookies by default.
COOKIES_SECURE = parse_boolean(os.environ.get("REDASH_COOKIES_SECURE", str(ENFORCE_HTTPS)))
Expand Down
31 changes: 18 additions & 13 deletions redash/utils/requests_session.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import warnings

from redash import settings

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

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

if settings.ENFORCE_PRIVATE_ADDRESS_BLOCK:
import advocate as requests_or_advocate
else:
import requests as requests_or_advocate
pass


class ConfiguredSession(requests_or_advocate.Session):
class ConfiguredSession(requests_or_champion.Session):
def request(self, *args, **kwargs):
if not settings.REQUESTS_ALLOW_REDIRECTS:
kwargs.update({"allow_redirects": False})
Expand Down
6 changes: 3 additions & 3 deletions tests/query_runner/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from redash.query_runner import BaseHTTPQueryRunner
from redash.utils.requests_session import (
ConfiguredSession,
requests_or_advocate,
requests_or_champion,
)


Expand Down Expand Up @@ -84,7 +84,7 @@ def test_get_response_httperror_exception(self, mock_get):
mock_response = mock.Mock()
mock_response.status_code = 500
mock_response.text = "Server Error"
http_error = requests_or_advocate.HTTPError()
http_error = requests_or_champion.HTTPError()
mock_response.raise_for_status.side_effect = http_error
mock_get.return_value = mock_response

Expand All @@ -101,7 +101,7 @@ def test_get_response_requests_exception(self, mock_get):
mock_response.status_code = 500
mock_response.text = "Server Error"
exception_message = "Some requests exception"
requests_exception = requests_or_advocate.RequestException(exception_message)
requests_exception = requests_or_champion.RequestException(exception_message)
mock_response.raise_for_status.side_effect = requests_exception
mock_get.return_value = mock_response

Expand Down