CVE-2021-43780 (published 2021-11-24T16:15Z)

Redash is a package for data visualization and sharing. In versions 10.0 and priorm the implementation of URL-loading data sources like JSON, CSV, or Excel is vulnerable to advanced methods of Server Side Request Forgery (SSRF). These vulnerabilities are only exploitable on installations where a URL-loading data source is enabled. As of time of publication, the `master` and `release/10.x.x` branches address this by applying the Advocate library for making http requests instead of the requests library directly. Users should upgrade to version 10.0.1 to receive this patch. There are a few workarounds for mitigating the vulnerability without upgrading. One can disable the vulnerable data sources entirely, by adding the following env variable to one's configuration, making them unavailable inside the webapp. One can switch any data source of certain types (viewable in the GitHub Security Advisory) to be `View Only` for all groups on the Settings > Groups > Data Sources screen. For users unable to update an admin may modify Redash's configuration through environment variables to mitigate this issue. Depending on the version of Redash, an admin may also need to run a CLI command to re-encrypt some fields in the database. The `master` and `release/10.x.x` branches as of time of publication have removed the default value for `REDASH_COOKIE_SECRET`. All future releases will also require this to be set explicitly. For existing installations, one will need to ensure that explicit values are set for the `REDASH_COOKIE_SECRET` and `REDASH_SECRET_KEY `variables.

Severity: MEDIUM
CVSS v2 base score: 6.0 (AV:N/AC:M/Au:S/C:P/I:P/A:P)
CVSS v3 base score: 8.8, HIGH (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-918
cwe_name: Server-Side Request Forgery (SSRF)
description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
extended_description: By providing URLs to unexpected hosts or ports, attackers can make it appear that the server is sending the request, possibly bypassing access controls such as firewalls that prevent the attackers from accessing the URLs directly. The server can be used as a proxy to conduct port scanning of hosts in internal networks, use other URLs such as that can access documents on the system (using file://), or use other protocols such as gopher:// or tftp://, which may provide greater control over the contents of requests.
         

Fix information:
repo_url: https://github.com/getredash/redash
commit_details: repo_url: https://github.com/getredash/redash
author: Jesse
author_date: 2021-11-23 16:58:20-06:00
msg: Merge pull request from GHSA-fcpv-hgq6-87h7
repository: repo_url: https://github.com/getredash/redash
repo_name: getredash/redash
description: Make Your Company Data Driven. Connect to any data source, easily visualize, dashboard and share your data.
repo_language: Python
owner: getredash
file_changes: filename: __init__.py
change_type: ModificationType.MODIFY
diff: @@ -13,7 +13,8 @@ from redash import settings, utils
 from redash.utils import json_loads, query_is_select_no_limit, add_limit_to_query
 from rq.timeouts import JobTimeoutException
 
-from redash.utils.requests_session import requests, requests_session
+from redash.utils.requests_session import requests_or_advocate, requests_session, UnacceptableAddressException
+
 
 logger = logging.getLogger(__name__)
 
@@ -236,12 +237,6 @@ class BaseSQLQueryRunner(BaseQueryRunner):
             return query_text
 
 
-def is_private_address(url):
-    hostname = urlparse(url).hostname
-    ip_address = socket.gethostbyname(hostname)
-    return ipaddress.ip_address(text_type(ip_address)).is_private
-
-
 class BaseHTTPQueryRunner(BaseQueryRunner):
     should_annotate_query = False
     response_error = "Endpoint returned unexpected status code"
@@ -285,8 +280,6 @@ class BaseHTTPQueryRunner(BaseQueryRunner):
             return None
 
     def get_response(self, url, auth=None, http_method="get", **kwargs):
-        if is_private_address(url) and settings.ENFORCE_PRIVATE_ADDRESS_BLOCK:
-            raise Exception("Can't query private addresses.")
 
         # Get authentication values if not given
         if auth is None:
@@ -307,12 +300,15 @@ class BaseHTTPQueryRunner(BaseQueryRunner):
             if response.status_code != 200:
                 error = "{} ({}).".format(self.response_error, response.status_code)
 
-        except requests.HTTPError as exc:
+        except requests_or_advocate.HTTPError as exc:
             logger.exception(exc)
             error = "Failed to execute query. " "Return Code: {} Reason: {}".format(
                 response.status_code, response.text
             )
-        except requests.RequestException as exc:
+        except UnacceptableAddressException as exc:
+            logger.exception(exc)
+            error = "Can't query private addresses."
+        except requests_or_advocate.RequestException as exc:
             # Catch all other requests exceptions and return the error.
             logger.exception(exc)
             error = str(exc)

programming_language: Python
filename: csv.py
change_type: ModificationType.MODIFY
diff: @@ -1,9 +1,9 @@
 import logging
 import yaml
-import requests
 import io
 
-from redash import settings
+from redash.utils.requests_session import requests_or_advocate, UnacceptableAddressException
+
 from redash.query_runner import *
 from redash.utils import json_dumps
 
@@ -52,14 +52,11 @@ class CSV(BaseQueryRunner):
             args.pop('url', None)
             ua = args['user-agent']
             args.pop('user-agent', None)
-
-            if is_private_address(path) and settings.ENFORCE_PRIVATE_ADDRESS_BLOCK:
-                raise Exception("Can't query private addresses.")
         except:
             pass
 
         try:
-            response = requests.get(url=path, headers={"User-agent": ua})
+            response = requests_or_advocate.get(url=path, headers={"User-agent": ua})
             workbook = pd.read_csv(io.BytesIO(response.content),sep=",", **args)
 
             df = workbook.copy()
@@ -88,6 +85,9 @@ class CSV(BaseQueryRunner):
         except KeyboardInterrupt:
             error = "Query cancelled by user."
             json_data = None
+        except UnacceptableAddressException:
+            error = "Can't query private addresses."
+            json_data = None
         except Exception as e:
             error = "Error reading {0}. {1}".format(path, str(e))
             json_data = None

programming_language: Python
filename: excel.py
change_type: ModificationType.MODIFY
diff: @@ -1,8 +1,8 @@
 import logging
 import yaml
-import requests
 
-from redash import settings
+from redash.utils.requests_session import requests_or_advocate, UnacceptableAddressException
+
 from redash.query_runner import *
 from redash.utils import json_dumps
 
@@ -49,13 +49,11 @@ class Excel(BaseQueryRunner):
             ua = args['user-agent']
             args.pop('user-agent', None)
 
-            if is_private_address(path) and settings.ENFORCE_PRIVATE_ADDRESS_BLOCK:
-                raise Exception("Can't query private addresses.")
         except:
             pass
 
         try:
-            response = requests.get(url=path, headers={"User-agent": ua})
+            response = requests_or_advocate.get(url=path, headers={"User-agent": ua})
             workbook = pd.read_excel(response.content, **args)
 
             df = workbook.copy()
@@ -84,6 +82,9 @@ class Excel(BaseQueryRunner):
         except KeyboardInterrupt:
             error = "Query cancelled by user."
             json_data = None
+        except UnacceptableAddressException:
+            error = "Can't query private addresses."
+            json_data = None
         except Exception as e:
             error = "Error reading {0}. {1}".format(path, str(e))
             json_data = None

programming_language: Python
filename: json_ds.py
change_type: ModificationType.MODIFY
diff: @@ -2,7 +2,9 @@ import logging
 import yaml
 import datetime
 from funcy import compact, project
-from redash import settings
+
+from redash.utils.requests_session import requests_or_advocate, UnacceptableAddressException
+
 from redash.utils import json_dumps
 from redash.query_runner import (
     BaseHTTPQueryRunner,
@@ -12,7 +14,6 @@ from redash.query_runner import (
     TYPE_FLOAT,
     TYPE_INTEGER,
     TYPE_STRING,
-    is_private_address,
 )
 
 
@@ -163,8 +164,6 @@ class JSON(BaseHTTPQueryRunner):
         if "url" not in query:
             raise QueryParseError("Query must include 'url' option.")
 
-        if is_private_address(query["url"]) and settings.ENFORCE_PRIVATE_ADDRESS_BLOCK:
-            raise Exception("Can't query private addresses.")
 
         method = query.get("method", "get")
         request_options = project(query, ("params", "headers", "data", "auth", "json"))

programming_language: Python
filename: requests_session.py
change_type: ModificationType.MODIFY
diff: @@ -1,8 +1,14 @@
-import requests
 from redash import settings
 
+from advocate.exceptions import UnacceptableAddressException
+if settings.ENFORCE_PRIVATE_ADDRESS_BLOCK:
+    import advocate as requests_or_advocate
+else:
+    import requests as requests_or_advocate
 
-class ConfiguredSession(requests.Session):
+
+
+class ConfiguredSession(requests_or_advocate.Session):
     def request(self, *args, **kwargs):
         if not settings.REQUESTS_ALLOW_REDIRECTS:
             kwargs.update({"allow_redirects": False})

programming_language: Python
filename: requirements_all_ds.txt
change_type: ModificationType.MODIFY
diff: @@ -41,3 +41,4 @@ cmem-cmempy==21.2.3
 xlrd==2.0.1
 openpyxl==3.0.7
 firebolt-sqlalchemy
+advocate==1.0.0
\ No newline at end of file

programming_language: Batchfile
filename: test_http.py
change_type: ModificationType.MODIFY
diff: @@ -1,7 +1,7 @@
 import mock
 from unittest import TestCase
 
-from redash.utils.requests_session import requests, ConfiguredSession
+from redash.utils.requests_session import requests_or_advocate, ConfiguredSession
 from redash.query_runner import BaseHTTPQueryRunner
 
 
@@ -84,7 +84,7 @@ class TestBaseHTTPQueryRunner(TestCase):
         mock_response = mock.Mock()
         mock_response.status_code = 500
         mock_response.text = "Server Error"
-        http_error = requests.HTTPError()
+        http_error = requests_or_advocate.HTTPError()
         mock_response.raise_for_status.side_effect = http_error
         mock_get.return_value = mock_response
 
@@ -101,7 +101,7 @@ class TestBaseHTTPQueryRunner(TestCase):
         mock_response.status_code = 500
         mock_response.text = "Server Error"
         exception_message = "Some requests exception"
-        requests_exception = requests.RequestException(exception_message)
+        requests_exception = requests_or_advocate.RequestException(exception_message)
         mock_response.raise_for_status.side_effect = requests_exception
         mock_get.return_value = mock_response
 

programming_language: Python