From 00826728ba91091c58e55baa4a9ff4926cc793cd Mon Sep 17 00:00:00 2001 From: Tomasz Urbaszek Date: Wed, 22 Oct 2025 15:25:10 +0200 Subject: [PATCH 1/3] SNOW-1902886: Validate account input --- src/snowflake/connector/connection.py | 23 ++++++++++++++++++++--- src/snowflake/connector/util_text.py | 16 ++++++++++++++++ test/unit/test_parse_account.py | 21 ++++++++++++++++++++- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/snowflake/connector/connection.py b/src/snowflake/connector/connection.py index 6b46249451..a89a8e2d5c 100644 --- a/src/snowflake/connector/connection.py +++ b/src/snowflake/connector/connection.py @@ -134,7 +134,12 @@ from .telemetry import TelemetryClient, TelemetryData, TelemetryField from .time_util import HeartBeatTimer, get_time_millis from .url_util import extract_top_level_domain_from_hostname -from .util_text import construct_hostname, parse_account, split_statements +from .util_text import ( + construct_hostname, + is_valid_account_identifier, + parse_account, + split_statements, +) from .wif_util import AttestationProvider if sys.version_info >= (3, 13) or typing.TYPE_CHECKING: @@ -1743,8 +1748,20 @@ def __config(self, **kwargs): ProgrammingError, {"msg": "Account must be specified", "errno": ER_NO_ACCOUNT_NAME}, ) - if self._account and "." in self._account: - self._account = parse_account(self._account) + if self._account: + # Allow legacy formats like "acc.region" to continue parsing into simple account id + if "." in self._account: + self._account = parse_account(self._account) + if not is_valid_account_identifier(self._account): + Error.errorhandler_wrapper( + self, + None, + ProgrammingError, + { + "msg": "Invalid account identifier: only letters, digits, '_' and '-' allowed; no dots or slashes", + "errno": ER_INVALID_VALUE, + }, + ) if not isinstance(self._backoff_policy, Callable) or not isinstance( self._backoff_policy(), Iterator diff --git a/src/snowflake/connector/util_text.py b/src/snowflake/connector/util_text.py index 39762c2111..156f930bef 100644 --- a/src/snowflake/connector/util_text.py +++ b/src/snowflake/connector/util_text.py @@ -254,6 +254,22 @@ def _is_china_region(r: str) -> bool: return host +ACCOUNT_ID_VALIDATOR_RE = re.compile(r"^[A-Za-z0-9_-]+$") + + +def is_valid_account_identifier(account: str) -> bool: + """Validate the Snowflake account identifier format. + + The account identifier must be a single label (no dots or slashes) composed + only of ASCII letters, digits, underscores, or hyphens. + """ + if not isinstance(account, str) or not account: + return False + if "." in account or "/" in account or "\\" in account: + return False + return bool(ACCOUNT_ID_VALIDATOR_RE.fullmatch(account)) + + def parse_account(account): url_parts = account.split(".") # if this condition is true, then we have some extra diff --git a/test/unit/test_parse_account.py b/test/unit/test_parse_account.py index c07dd46c05..e3ef04f4f1 100644 --- a/test/unit/test_parse_account.py +++ b/test/unit/test_parse_account.py @@ -1,7 +1,9 @@ #!/usr/bin/env python from __future__ import annotations -from snowflake.connector.util_text import parse_account +import pytest + +from snowflake.connector.util_text import is_valid_account_identifier, parse_account def test_parse_account_basic(): @@ -12,3 +14,20 @@ def test_parse_account_basic(): assert ( parse_account("account1-jkabfvdjisoa778wqfgeruishafeuw89q.global") == "account1" ) + + +@pytest.mark.parametrize( + "value,expected", + [ + ("abc", True), + ("ABC", True), + ("a_b-c1", True), + ("a.b", False), + ("a/b", False), + ("a\\b", False), + ("", False), + ("snowflakecomputing.com", False), + ], +) +def test_is_valid_account_identifier(value, expected): + assert is_valid_account_identifier(value) is expected From 691eac12e3f4dffa82bd1ebe6898ac1f780f90b4 Mon Sep 17 00:00:00 2001 From: Tomasz Urbaszek Date: Fri, 21 Nov 2025 14:45:53 +0100 Subject: [PATCH 2/3] fixup! SNOW-1902886: Validate account input --- src/snowflake/connector/connection.py | 24 ++++++------- src/snowflake/connector/util_text.py | 6 ++-- test/unit/test_parse_account.py | 49 +++++++++++++++++++++------ 3 files changed, 52 insertions(+), 27 deletions(-) diff --git a/src/snowflake/connector/connection.py b/src/snowflake/connector/connection.py index a89a8e2d5c..c472b5820a 100644 --- a/src/snowflake/connector/connection.py +++ b/src/snowflake/connector/connection.py @@ -669,6 +669,13 @@ def __init__( self._file_operation_parser = FileOperationParser(self) self._stream_downloader = StreamDownloader(self) + @staticmethod + def _validate_account(account_str): + if not is_valid_account_identifier(account_str): + raise ValueError( + "Invalid account identifier: only letters, digits, '_' and '-' allowed; no dots or slashes" + ) + # Deprecated @property def insecure_mode(self) -> bool: @@ -1628,6 +1635,7 @@ def __config(self, **kwargs): raise TypeError("auth_class must subclass AuthByPlugin") if "account" in kwargs: + self._validate_account(kwargs["account"]) if "host" not in kwargs: self._host = construct_hostname(kwargs.get("region"), self._account) @@ -1748,20 +1756,8 @@ def __config(self, **kwargs): ProgrammingError, {"msg": "Account must be specified", "errno": ER_NO_ACCOUNT_NAME}, ) - if self._account: - # Allow legacy formats like "acc.region" to continue parsing into simple account id - if "." in self._account: - self._account = parse_account(self._account) - if not is_valid_account_identifier(self._account): - Error.errorhandler_wrapper( - self, - None, - ProgrammingError, - { - "msg": "Invalid account identifier: only letters, digits, '_' and '-' allowed; no dots or slashes", - "errno": ER_INVALID_VALUE, - }, - ) + if self._account and "." in self._account: + self._account = parse_account(self._account) if not isinstance(self._backoff_policy, Callable) or not isinstance( self._backoff_policy(), Iterator diff --git a/src/snowflake/connector/util_text.py b/src/snowflake/connector/util_text.py index 156f930bef..5e9177d412 100644 --- a/src/snowflake/connector/util_text.py +++ b/src/snowflake/connector/util_text.py @@ -265,9 +265,11 @@ def is_valid_account_identifier(account: str) -> bool: """ if not isinstance(account, str) or not account: return False - if "." in account or "/" in account or "\\" in account: + + if "/" in account or "\\" in account: return False - return bool(ACCOUNT_ID_VALIDATOR_RE.fullmatch(account)) + + return all(bool(ACCOUNT_ID_VALIDATOR_RE.fullmatch(p)) for p in account.split(".")) def parse_account(account): diff --git a/test/unit/test_parse_account.py b/test/unit/test_parse_account.py index e3ef04f4f1..cce4cfaf45 100644 --- a/test/unit/test_parse_account.py +++ b/test/unit/test_parse_account.py @@ -3,6 +3,7 @@ import pytest +from snowflake.connector import connect from snowflake.connector.util_text import is_valid_account_identifier, parse_account @@ -17,17 +18,43 @@ def test_parse_account_basic(): @pytest.mark.parametrize( - "value,expected", + "value", [ - ("abc", True), - ("ABC", True), - ("a_b-c1", True), - ("a.b", False), - ("a/b", False), - ("a\\b", False), - ("", False), - ("snowflakecomputing.com", False), + "abc", + "aaa.bbb.ccc", + "aaa.bbb.ccc.ddd" "ABC", + "a_b-c1", + "account1", + "my_account", + "my-account", + "account_123", + "ACCOUNT_NAME", ], ) -def test_is_valid_account_identifier(value, expected): - assert is_valid_account_identifier(value) is expected +def test_is_valid_account_identifier(value): + assert is_valid_account_identifier(value) is True + + +@pytest.mark.parametrize( + "value", + [ + "a/b", + "a\\b", + "", + "aa.bb.ccc/dddd", + "account@domain", + "account name", + "account\ttab", + "account\nnewline", + "account:port", + "account;semicolon", + "account'quote", + 'account"doublequote', + ], +) +def test_is_invalid_account_identifier(value): + assert is_valid_account_identifier(value) is False + with pytest.raises(ValueError) as err: + connect(account=value, user="jdoe", password="***") + + assert "Invalid account identifier" in str(err) From 3361f2115f298811c5f10f4018b0513a81ed9106 Mon Sep 17 00:00:00 2001 From: Tomasz Urbaszek Date: Mon, 24 Nov 2025 15:32:36 +0100 Subject: [PATCH 3/3] fixup! fixup! SNOW-1902886: Validate account input --- src/snowflake/connector/connection.py | 21 ++++++++++++++------- test/unit/test_parse_account.py | 5 ++--- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/snowflake/connector/connection.py b/src/snowflake/connector/connection.py index c472b5820a..42673ea32e 100644 --- a/src/snowflake/connector/connection.py +++ b/src/snowflake/connector/connection.py @@ -669,11 +669,16 @@ def __init__( self._file_operation_parser = FileOperationParser(self) self._stream_downloader = StreamDownloader(self) - @staticmethod - def _validate_account(account_str): + def _validate_account(self, account_str): if not is_valid_account_identifier(account_str): - raise ValueError( - "Invalid account identifier: only letters, digits, '_' and '-' allowed; no dots or slashes" + Error.errorhandler_wrapper( + self, + None, + ProgrammingError, + { + "msg": "Invalid account identifier: only letters, digits, '_' and '-' allowed; no dots or slashes", + "errno": ER_NO_ACCOUNT_NAME, + }, ) # Deprecated @@ -1635,7 +1640,6 @@ def __config(self, **kwargs): raise TypeError("auth_class must subclass AuthByPlugin") if "account" in kwargs: - self._validate_account(kwargs["account"]) if "host" not in kwargs: self._host = construct_hostname(kwargs.get("region"), self._account) @@ -1756,8 +1760,11 @@ def __config(self, **kwargs): ProgrammingError, {"msg": "Account must be specified", "errno": ER_NO_ACCOUNT_NAME}, ) - if self._account and "." in self._account: - self._account = parse_account(self._account) + + if self._account: + self._validate_account(self._account) + if "." in self._account: + self._account = parse_account(self._account) if not isinstance(self._backoff_policy, Callable) or not isinstance( self._backoff_policy(), Iterator diff --git a/test/unit/test_parse_account.py b/test/unit/test_parse_account.py index cce4cfaf45..e646362ec5 100644 --- a/test/unit/test_parse_account.py +++ b/test/unit/test_parse_account.py @@ -3,7 +3,7 @@ import pytest -from snowflake.connector import connect +from snowflake.connector import ProgrammingError, connect from snowflake.connector.util_text import is_valid_account_identifier, parse_account @@ -40,7 +40,6 @@ def test_is_valid_account_identifier(value): [ "a/b", "a\\b", - "", "aa.bb.ccc/dddd", "account@domain", "account name", @@ -54,7 +53,7 @@ def test_is_valid_account_identifier(value): ) def test_is_invalid_account_identifier(value): assert is_valid_account_identifier(value) is False - with pytest.raises(ValueError) as err: + with pytest.raises(ProgrammingError) as err: connect(account=value, user="jdoe", password="***") assert "Invalid account identifier" in str(err)