CVE-2023-34233 (published 2023-06-08T21:15Z)

The Snowflake Connector for Python provides an interface for developing Python applications that can connect to Snowflake and perform all standard operations. Versions prior to 3.0.2 are vulnerable to command injection via single sign-on(SSO) browser URL authentication. In order to exploit the potential for command injection, an attacker would need to be successful in (1) establishing a malicious resource and (2) redirecting users to utilize the resource. The attacker could set up a malicious, publicly accessible server which responds to the SSO URL with an attack payload. If the attacker then tricked a user into visiting the maliciously crafted connection URL, the user’s local machine would render the malicious payload, leading to a remote code execution. This attack scenario can be mitigated through URL whitelisting as well as common anti-phishing resources. Version 3.0.2 contains a patch for this issue.

CVSS v3 base score: 8.8, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-77
cwe_name: Improper Neutralization of Special Elements used in a Command ('Command Injection')
description: The product constructs all or part of a command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended command when it is sent to a downstream component.
extended_description: 
            Many protocols and products have their own custom command language. While OS or shell command strings are frequently discovered and targeted, developers may not realize that these other command languages might also be vulnerable to attacks.
         
         

Fix information:
repo_url: https://github.com/snowflakedb/snowflake-connector-python
commit_details: repo_url: https://github.com/snowflakedb/snowflake-connector-python
author: Harsh Chaturvedi
author_date: 2023-03-22 17:16:55-07:00
msg: SNOW-761004 Added URL Validator and URL escaping of strings (#1480)
repository: repo_url: https://github.com/snowflakedb/snowflake-connector-python
repo_name: snowflakedb/snowflake-connector-python
description: Snowflake Connector for Python
repo_language: Python
owner: snowflakedb
file_changes: filename: .pre-commit-config.yaml
change_type: ModificationType.MODIFY
diff: @@ -93,6 +93,7 @@ repos:
                 | ssd_internal_keys
                 | test_util
                 | util_text
+                | url_util
                 | version
             ).py$
         additional_dependencies:

programming_language: Markdown
filename: DESCRIPTION.md
change_type: ModificationType.MODIFY
diff: @@ -12,6 +12,7 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne
 
   - Fixed a memory leak in the logging module of the Cython extension.
   - Fixed a bug where the `put` command on AWS raised `AttributeError` when the file size was larger than 200M.
+  - Validate SSO URL before opening it in the browser for External browser authenticator.
 
 - v3.0.1(February 28, 2023)
 

programming_language: Markdown
filename: setup.py
change_type: ModificationType.MODIFY
diff: @@ -49,11 +49,13 @@ try:
 
     _ABLE_TO_COMPILE_EXTENSIONS = True
 except ImportError:
-    warnings.warn("Cannot compile native C code, because of a missing build dependency")
+    warnings.warn(
+        "Cannot compile native C code, because of a missing build dependency",
+        stacklevel=1,
+    )
     _ABLE_TO_COMPILE_EXTENSIONS = False
 
 if _ABLE_TO_COMPILE_EXTENSIONS:
-
     pyarrow_version = tuple(int(x) for x in pyarrow.__version__.split("."))
     extensions = cythonize(
         [
@@ -66,7 +68,6 @@ if _ABLE_TO_COMPILE_EXTENSIONS:
     )
 
     class MyBuildExt(build_ext):
-
         # list of libraries that will be bundled with python connector,
         # this list should be carefully examined when pyarrow lib is
         # upgraded

programming_language: Python
filename: webbrowser.py
change_type: ModificationType.MODIFY
diff: @@ -23,6 +23,7 @@ from ..constants import (
 )
 from ..errorcode import (
     ER_IDP_CONNECTION_ERROR,
+    ER_INVALID_VALUE,
     ER_NO_HOSTNAME_FOUND,
     ER_UNABLE_TO_OPEN_BROWSER,
 )
@@ -32,6 +33,7 @@ from ..network import (
     EXTERNAL_BROWSER_AUTHENTICATOR,
     PYTHON_CONNECTOR_USER_AGENT,
 )
+from ..url_util import is_valid_url
 from . import Auth
 from .by_plugin import AuthByPlugin, AuthType
 
@@ -131,6 +133,22 @@ class AuthByWebBrowser(AuthByPlugin):
             socket_connection.listen(0)  # no backlog
             callback_port = socket_connection.getsockname()[1]
 
+            logger.debug("step 1: query GS to obtain SSO url")
+            sso_url = self._get_sso_url(
+                conn, authenticator, service_name, account, callback_port, user
+            )
+
+            logger.debug("Validate SSO URL")
+            if not is_valid_url(sso_url):
+                self._handle_failure(
+                    conn=conn,
+                    ret={
+                        "code": ER_INVALID_VALUE,
+                        "message": (f"The SSO URL provided {sso_url} is invalid"),
+                    },
+                )
+                return
+
             print(
                 "Initiating login request with your identity provider. A "
                 "browser window should have opened for you to complete the "
@@ -138,11 +156,6 @@ class AuthByWebBrowser(AuthByPlugin):
                 "or your OS settings. Press CTRL+C to abort and try again..."
             )
 
-            logger.debug("step 1: query GS to obtain SSO url")
-            sso_url = self._get_sso_url(
-                conn, authenticator, service_name, account, callback_port, user
-            )
-
             logger.debug("step 2: open a browser")
             print(f"Going to open: {sso_url} to authenticate...")
             if not self._webbrowser.open_new(sso_url):

programming_language: Python
filename: snow_logging.py
change_type: ModificationType.MODIFY
diff: @@ -67,7 +67,7 @@ class SnowLogger(logging.LoggerAdapter):
         warnings.warn(
             "The 'warn' method is deprecated, " "use 'warning' instead",
             DeprecationWarning,
-            2,
+            stacklevel=2,
         )
         self.warning(msg, path_name, func_name, *args, **kwargs)
 

programming_language: Python
filename: url_util.py
change_type: ModificationType.ADD
diff: @@ -0,0 +1,43 @@
+#
+# Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
+#
+
+from __future__ import annotations
+
+import re
+import urllib.parse
+from logging import getLogger
+
+logger = getLogger(__name__)
+
+
+URL_VALIDATOR = re.compile(
+    "^http(s?)\\:\\/\\/[0-9a-zA-Z]([-.\\w]*[0-9a-zA-Z@:])*(:(0-9)*)*(\\/?)([a-zA-Z0-9\\-\\.\\?\\,\\&\\(\\)\\/\\\\\\+&%\\$#_=@]*)?$"
+)
+
+
+def is_valid_url(url: str) -> bool:
+    """Confirms if the provided URL is a valid HTTP/ HTTPs URL
+
+    Args:
+        url: the URL that needs to be validated
+
+    Returns:
+        true/ false depending on whether the URL is valid or not
+    """
+    return bool(URL_VALIDATOR.match(url))
+
+
+def url_encode_str(target: str | None) -> str:
+    """Converts a target string into escaped URL safe string
+
+    Args:
+        target: string to be URL encoded
+
+    Returns:
+        URL encoded string
+    """
+    if target is None:
+        logger.debug("The string to be URL encoded is None")
+        return ""
+    return urllib.parse.quote_plus(target, safe="")

programming_language: Python
filename: test_auth_webbrowser.py
change_type: ModificationType.MODIFY
diff: @@ -32,6 +32,7 @@ PASSWORD = "testpassword"
 SERVICE_NAME = ""
 REF_PROOF_KEY = "MOCK_PROOF_KEY"
 REF_SSO_URL = "https://testsso.snowflake.net/sso"
+INVALID_SSO_URL = "this is an invalid URL"
 
 
 def mock_webserver(target_instance, application, port):
@@ -318,3 +319,39 @@ def test_idtoken_reauth():
                     account="account",
                     auth_class=auth_inst,
                 )
+
+
+def test_auth_webbrowser_invalid_sso(monkeypatch):
+    """Authentication by WebBrowser with failed to start web browser case."""
+    rest = _init_rest(INVALID_SSO_URL, REF_PROOF_KEY)
+
+    # mock webbrowser
+    mock_webbrowser = MagicMock()
+    mock_webbrowser.open_new.return_value = False
+
+    # mock socket
+    mock_socket_instance = MagicMock()
+    mock_socket_instance.getsockname.return_value = [None, 12345]
+
+    mock_socket_client = MagicMock()
+    mock_socket_client.recv.return_value = (
+        "\r\n".join(["GET /?token=MOCK_TOKEN HTTP/1.1", "User-Agent: snowflake-agent"])
+    ).encode("utf-8")
+    mock_socket_instance.accept.return_value = (mock_socket_client, None)
+    mock_socket = Mock(return_value=mock_socket_instance)
+
+    auth = AuthByWebBrowser(
+        application=APPLICATION,
+        webbrowser_pkg=mock_webbrowser,
+        socket_pkg=mock_socket,
+    )
+    auth.prepare(
+        conn=rest._connection,
+        authenticator=AUTHENTICATOR,
+        service_name=SERVICE_NAME,
+        account=ACCOUNT,
+        user=USER,
+        password=PASSWORD,
+    )
+    assert rest._connection.errorhandler.called  # an error
+    assert auth.assertion_content is None

programming_language: Python
filename: test_url_util.py
change_type: ModificationType.ADD
diff: @@ -0,0 +1,26 @@
+#
+# Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
+#
+
+try:
+    from snowflake.connector.url_util import is_valid_url, url_encode_str
+except ImportError:
+
+    def is_valid_url(s):
+        return False
+
+
+def test_url_validator():
+    assert is_valid_url("https://ssoTestURL.okta.com")
+    assert is_valid_url("https://ssoTestURL.okta.com:8080")
+    assert is_valid_url("https://ssoTestURL.okta.com/testpathvalue")
+
+    assert not is_valid_url("-a Calculator")
+    assert not is_valid_url("This is a random text")
+    assert not is_valid_url("file://TestForFile")
+
+
+def test_encoder():
+    assert url_encode_str("Hello @World") == "Hello+%40World"
+    assert url_encode_str("Test//String") == "Test%2F%2FString"
+    assert url_encode_str(None) == ""

programming_language: Python