From f5a2d644943d85b7476e5e3201493721a68e0f66 Mon Sep 17 00:00:00 2001 From: sharmagot Date: Mon, 8 Dec 2025 00:17:07 -0500 Subject: [PATCH 1/5] Added explicit validation to reject TOTP values containing non-numeric characters. --- vertica_python/vertica/connection.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/vertica_python/vertica/connection.py b/vertica_python/vertica/connection.py index 0d0e6a54..ee0ed6b2 100644 --- a/vertica_python/vertica/connection.py +++ b/vertica_python/vertica/connection.py @@ -313,6 +313,13 @@ def __init__(self, options: Optional[Dict[str, Any]] = None) -> None: if self.totp is not None: if not isinstance(self.totp, str): raise TypeError('The value of connection option "totp" should be a string') + # Validate TOTP format: must be 6 numeric digits, with explicit non-numeric error + if not self.totp.isdigit(): + self._logger.error('Invalid TOTP: contains non-numeric characters') + raise errors.ConnectionError('Invalid TOTP: contains non-numeric characters') + if len(self.totp) != 6: + self._logger.error('Invalid TOTP format in connection options. Must be a 6-digit number.') + raise errors.ConnectionError('Invalid TOTP format: Must be a 6-digit number.') self._logger.info('TOTP received in connection options') # OAuth authentication setup @@ -1005,8 +1012,11 @@ def send_startup(totp_value=None): self._logger.error("Invalid TOTP: Cannot be empty.") raise errors.ConnectionError("Invalid TOTP: Cannot be empty.") - # ❌ Validate TOTP format (must be 6 digits) - if not totp_input.isdigit() or len(totp_input) != 6: + # ❌ Validate TOTP format: explicit non-numeric error, then length check + if not totp_input.isdigit(): + self._logger.error("Invalid TOTP: contains non-numeric characters") + raise errors.ConnectionError("Invalid TOTP: contains non-numeric characters") + if len(totp_input) != 6: print("Invalid TOTP format. Please enter a 6-digit code.") self._logger.error("Invalid TOTP format entered.") raise errors.ConnectionError("Invalid TOTP format: Must be a 6-digit number.") From 67c50e77203c67d44b46905e09b5ece51c040e4d Mon Sep 17 00:00:00 2001 From: sharmagot Date: Tue, 9 Dec 2025 05:29:03 -0500 Subject: [PATCH 2/5] Improve TOTP validation: trim whitespace and standardize auth error logs --- vertica_python/vertica/connection.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/vertica_python/vertica/connection.py b/vertica_python/vertica/connection.py index ee0ed6b2..33a40a9d 100644 --- a/vertica_python/vertica/connection.py +++ b/vertica_python/vertica/connection.py @@ -313,13 +313,15 @@ def __init__(self, options: Optional[Dict[str, Any]] = None) -> None: if self.totp is not None: if not isinstance(self.totp, str): raise TypeError('The value of connection option "totp" should be a string') + # Normalize: trim surrounding whitespace + self.totp = self.totp.strip() # Validate TOTP format: must be 6 numeric digits, with explicit non-numeric error if not self.totp.isdigit(): - self._logger.error('Invalid TOTP: contains non-numeric characters') - raise errors.ConnectionError('Invalid TOTP: contains non-numeric characters') + self._logger.error('Authentication failed: Invalid TOTP: contains non-numeric characters') + raise errors.ConnectionError('Authentication failed: Invalid TOTP: contains non-numeric characters') if len(self.totp) != 6: - self._logger.error('Invalid TOTP format in connection options. Must be a 6-digit number.') - raise errors.ConnectionError('Invalid TOTP format: Must be a 6-digit number.') + self._logger.error('Authentication failed: Invalid TOTP: must be 6 digits') + raise errors.ConnectionError('Authentication failed: Invalid TOTP: must be 6 digits') self._logger.info('TOTP received in connection options') # OAuth authentication setup @@ -981,10 +983,10 @@ def send_startup(totp_value=None): short_msg = match.group(1).strip() if match else error_msg.strip() if "Invalid TOTP" in short_msg: - print("Authentication failed: Invalid TOTP token.") - self._logger.error("Authentication failed: Invalid TOTP token.") + print("Authentication failed: Invalid TOTP") + self._logger.error("Authentication failed: Invalid TOTP") self.close_socket() - raise errors.ConnectionError("Authentication failed: Invalid TOTP token.") + raise errors.ConnectionError("Authentication failed: Invalid TOTP") # Generic error fallback print(f"Authentication failed: {short_msg}") @@ -1012,14 +1014,16 @@ def send_startup(totp_value=None): self._logger.error("Invalid TOTP: Cannot be empty.") raise errors.ConnectionError("Invalid TOTP: Cannot be empty.") + # ❌ Normalize: trim whitespace + totp_input = totp_input.strip() # ❌ Validate TOTP format: explicit non-numeric error, then length check if not totp_input.isdigit(): - self._logger.error("Invalid TOTP: contains non-numeric characters") - raise errors.ConnectionError("Invalid TOTP: contains non-numeric characters") + self._logger.error("Authentication failed: Invalid TOTP: contains non-numeric characters") + raise errors.ConnectionError("Authentication failed: Invalid TOTP: contains non-numeric characters") if len(totp_input) != 6: print("Invalid TOTP format. Please enter a 6-digit code.") - self._logger.error("Invalid TOTP format entered.") - raise errors.ConnectionError("Invalid TOTP format: Must be a 6-digit number.") + self._logger.error("Authentication failed: Invalid TOTP: must be 6 digits") + raise errors.ConnectionError("Authentication failed: Invalid TOTP: must be 6 digits") # ✅ Valid TOTP — retry connection totp = totp_input self.close_socket() From d9ac515a8e640bb7d750580bb542a81298d6ba3b Mon Sep 17 00:00:00 2001 From: sharmagot Date: Sun, 14 Dec 2025 07:13:44 -0500 Subject: [PATCH 3/5] Use shared TOTP validator for normalization and consistent error messages --- vertica_python/vertica/connection.py | 81 ++++++++++++++++++---------- 1 file changed, 54 insertions(+), 27 deletions(-) diff --git a/vertica_python/vertica/connection.py b/vertica_python/vertica/connection.py index 33a40a9d..8ac86012 100644 --- a/vertica_python/vertica/connection.py +++ b/vertica_python/vertica/connection.py @@ -313,15 +313,31 @@ def __init__(self, options: Optional[Dict[str, Any]] = None) -> None: if self.totp is not None: if not isinstance(self.totp, str): raise TypeError('The value of connection option "totp" should be a string') - # Normalize: trim surrounding whitespace - self.totp = self.totp.strip() - # Validate TOTP format: must be 6 numeric digits, with explicit non-numeric error - if not self.totp.isdigit(): - self._logger.error('Authentication failed: Invalid TOTP: contains non-numeric characters') - raise errors.ConnectionError('Authentication failed: Invalid TOTP: contains non-numeric characters') - if len(self.totp) != 6: - self._logger.error('Authentication failed: Invalid TOTP: must be 6 digits') - raise errors.ConnectionError('Authentication failed: Invalid TOTP: must be 6 digits') + # Use shared TOTP validator for normalization and precedence checks + try: + from .totp_validation import validate_totp_code, INVALID_TOTP_MSG + except Exception: + validate_totp_code = None + INVALID_TOTP_MSG = 'Invalid TOTP: Please enter a valid 6-digit numeric code.' + + if validate_totp_code is not None: + result = validate_totp_code(self.totp, totp_is_valid=None) + if not result.ok: + msg = result.message or INVALID_TOTP_MSG + self._logger.error(f'Authentication failed: {msg}') + raise errors.ConnectionError(f'Authentication failed: {msg}') + # normalized digits-only code + self.totp = result.code + else: + # Fallback minimal validation + s = self.totp.strip() + if not s.isdigit(): + self._logger.error(INVALID_TOTP_MSG) + raise errors.ConnectionError(INVALID_TOTP_MSG) + if len(s) != 6: + self._logger.error(INVALID_TOTP_MSG) + raise errors.ConnectionError(INVALID_TOTP_MSG) + self.totp = s self._logger.info('TOTP received in connection options') # OAuth authentication setup @@ -983,10 +999,14 @@ def send_startup(totp_value=None): short_msg = match.group(1).strip() if match else error_msg.strip() if "Invalid TOTP" in short_msg: - print("Authentication failed: Invalid TOTP") - self._logger.error("Authentication failed: Invalid TOTP") + try: + from .totp_validation import INVALID_TOTP_MSG + except Exception: + INVALID_TOTP_MSG = "Invalid TOTP: Please enter a valid 6-digit numeric code." + print(f"Authentication failed: {INVALID_TOTP_MSG}") + self._logger.error(f"Authentication failed: {INVALID_TOTP_MSG}") self.close_socket() - raise errors.ConnectionError("Authentication failed: Invalid TOTP") + raise errors.ConnectionError(f"Authentication failed: {INVALID_TOTP_MSG}") # Generic error fallback print(f"Authentication failed: {short_msg}") @@ -1009,21 +1029,28 @@ def send_startup(totp_value=None): if ready: totp_input = sys.stdin.readline().strip() - # ❌ Blank TOTP entered - if not totp_input: - self._logger.error("Invalid TOTP: Cannot be empty.") - raise errors.ConnectionError("Invalid TOTP: Cannot be empty.") - - # ❌ Normalize: trim whitespace - totp_input = totp_input.strip() - # ❌ Validate TOTP format: explicit non-numeric error, then length check - if not totp_input.isdigit(): - self._logger.error("Authentication failed: Invalid TOTP: contains non-numeric characters") - raise errors.ConnectionError("Authentication failed: Invalid TOTP: contains non-numeric characters") - if len(totp_input) != 6: - print("Invalid TOTP format. Please enter a 6-digit code.") - self._logger.error("Authentication failed: Invalid TOTP: must be 6 digits") - raise errors.ConnectionError("Authentication failed: Invalid TOTP: must be 6 digits") + # Validate using shared precedence + try: + from .totp_validation import validate_totp_code, INVALID_TOTP_MSG + except Exception: + validate_totp_code = None + INVALID_TOTP_MSG = "Invalid TOTP: Please enter a valid 6-digit numeric code." + + if validate_totp_code is not None: + result = validate_totp_code(totp_input, totp_is_valid=None) + if not result.ok: + msg = result.message or INVALID_TOTP_MSG + print(msg) + self._logger.error(msg) + raise errors.ConnectionError(msg) + totp_input = result.code + else: + s = totp_input.strip() + if not s.isdigit() or len(s) != 6: + print(INVALID_TOTP_MSG) + self._logger.error(INVALID_TOTP_MSG) + raise errors.ConnectionError(INVALID_TOTP_MSG) + totp_input = s # ✅ Valid TOTP — retry connection totp = totp_input self.close_socket() From 65f56991d59fe3de7162d014bd571a22c313db60 Mon Sep 17 00:00:00 2001 From: sharmagot Date: Fri, 2 Jan 2026 05:40:22 -0500 Subject: [PATCH 4/5] Fix TOTP validation and block invalid authentication requests --- vertica_python/vertica/connection.py | 124 ++++++++++++++++----------- 1 file changed, 72 insertions(+), 52 deletions(-) diff --git a/vertica_python/vertica/connection.py b/vertica_python/vertica/connection.py index 8ac86012..cfd88d09 100644 --- a/vertica_python/vertica/connection.py +++ b/vertica_python/vertica/connection.py @@ -49,6 +49,7 @@ import signal import select import sys +import unicodedata from collections import deque from struct import unpack @@ -88,6 +89,61 @@ warnings.warn(f"Cannot get the login user name: {str(e)}") +# TOTP validation utilities (client-side) +class TotpValidationResult(NamedTuple): + ok: bool + code: str + message: str + + +INVALID_TOTP_MSG = 'Invalid TOTP: Please enter a valid 6-digit numeric code.' + + +def validate_totp_code(raw_code: str, totp_is_valid=None) -> TotpValidationResult: + """Validate and normalize a user-supplied TOTP value. + + Precedence: + 1) Trim & normalize input (strip spaces and separators; normalize full-width digits) + 2) Empty check -> "Enter your 6-digit code" + 3) Length check -> "Code must be 6 digits" + 4) Numeric-only check -> "Code can contain digits only" + + Returns TotpValidationResult(ok, code, message). On success, `code` is a 6-digit ASCII string. + `totp_is_valid` is reserved for optional server-side checks and ignored here. + """ + try: + s = raw_code if raw_code is not None else '' + # Normalize Unicode (convert full-width digits etc. to ASCII) + s = unicodedata.normalize('NFKC', s) + # Strip leading/trailing whitespace + s = s.strip() + # Remove common separators inside the code + # Spaces, hyphens, underscores, dots, and common dash-like characters + separators = {' ', '\t', '\n', '\r', '\f', '\v', '-', '_', '.', + '\u2012', '\u2013', '\u2014', '\u2212', '\u00B7', '\u2027', '\u30FB'} + # Replace all occurrences of separators + for sep in list(separators): + s = s.replace(sep, '') + + # Empty check + if s == '': + return TotpValidationResult(False, '', 'Enter your 6-digit code') + + # Length check + if len(s) != 6: + return TotpValidationResult(False, '', 'Code must be 6 digits') + + # Numeric-only check + if not s.isdigit(): + return TotpValidationResult(False, '', 'Code can contain digits only') + + # All good + return TotpValidationResult(True, s, '') + except Exception: + # Fallback generic error + return TotpValidationResult(False, '', INVALID_TOTP_MSG) + + def connect(**kwargs: Any) -> Connection: """Opens a new connection to a Vertica database.""" return Connection(kwargs) @@ -313,31 +369,14 @@ def __init__(self, options: Optional[Dict[str, Any]] = None) -> None: if self.totp is not None: if not isinstance(self.totp, str): raise TypeError('The value of connection option "totp" should be a string') - # Use shared TOTP validator for normalization and precedence checks - try: - from .totp_validation import validate_totp_code, INVALID_TOTP_MSG - except Exception: - validate_totp_code = None - INVALID_TOTP_MSG = 'Invalid TOTP: Please enter a valid 6-digit numeric code.' - - if validate_totp_code is not None: - result = validate_totp_code(self.totp, totp_is_valid=None) - if not result.ok: - msg = result.message or INVALID_TOTP_MSG - self._logger.error(f'Authentication failed: {msg}') - raise errors.ConnectionError(f'Authentication failed: {msg}') - # normalized digits-only code - self.totp = result.code - else: - # Fallback minimal validation - s = self.totp.strip() - if not s.isdigit(): - self._logger.error(INVALID_TOTP_MSG) - raise errors.ConnectionError(INVALID_TOTP_MSG) - if len(s) != 6: - self._logger.error(INVALID_TOTP_MSG) - raise errors.ConnectionError(INVALID_TOTP_MSG) - self.totp = s + # Validate using local validator + result = validate_totp_code(self.totp, totp_is_valid=None) + if not result.ok: + msg = result.message or INVALID_TOTP_MSG + self._logger.error(f'Authentication failed: {msg}') + raise errors.ConnectionError(f'Authentication failed: {msg}') + # normalized digits-only code + self.totp = result.code self._logger.info('TOTP received in connection options') # OAuth authentication setup @@ -999,10 +1038,6 @@ def send_startup(totp_value=None): short_msg = match.group(1).strip() if match else error_msg.strip() if "Invalid TOTP" in short_msg: - try: - from .totp_validation import INVALID_TOTP_MSG - except Exception: - INVALID_TOTP_MSG = "Invalid TOTP: Please enter a valid 6-digit numeric code." print(f"Authentication failed: {INVALID_TOTP_MSG}") self._logger.error(f"Authentication failed: {INVALID_TOTP_MSG}") self.close_socket() @@ -1022,35 +1057,20 @@ def send_startup(totp_value=None): # ✅ If TOTP not provided initially, prompt only once if not totp: - timeout_seconds = 30 # 5 minutes timeout + timeout_seconds = 300 # 5 minutes timeout try: print("Enter TOTP: ", end="", flush=True) ready, _, _ = select.select([sys.stdin], [], [], timeout_seconds) if ready: totp_input = sys.stdin.readline().strip() - # Validate using shared precedence - try: - from .totp_validation import validate_totp_code, INVALID_TOTP_MSG - except Exception: - validate_totp_code = None - INVALID_TOTP_MSG = "Invalid TOTP: Please enter a valid 6-digit numeric code." - - if validate_totp_code is not None: - result = validate_totp_code(totp_input, totp_is_valid=None) - if not result.ok: - msg = result.message or INVALID_TOTP_MSG - print(msg) - self._logger.error(msg) - raise errors.ConnectionError(msg) - totp_input = result.code - else: - s = totp_input.strip() - if not s.isdigit() or len(s) != 6: - print(INVALID_TOTP_MSG) - self._logger.error(INVALID_TOTP_MSG) - raise errors.ConnectionError(INVALID_TOTP_MSG) - totp_input = s + # Validate using local precedence-based validator + result = validate_totp_code(totp_input, totp_is_valid=None) + if not result.ok: + msg = result.message or INVALID_TOTP_MSG + self._logger.error(msg) + raise errors.ConnectionError(msg) + totp_input = result.code # ✅ Valid TOTP — retry connection totp = totp_input self.close_socket() From a8c0e4b0d5e525595f6fc00e048d4504a2a2b8bd Mon Sep 17 00:00:00 2001 From: sharmagot Date: Fri, 2 Jan 2026 06:38:30 -0500 Subject: [PATCH 5/5] Unify TOTP validation to return a single generic error message --- vertica_python/vertica/connection.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/vertica_python/vertica/connection.py b/vertica_python/vertica/connection.py index cfd88d09..14aedc23 100644 --- a/vertica_python/vertica/connection.py +++ b/vertica_python/vertica/connection.py @@ -104,11 +104,11 @@ def validate_totp_code(raw_code: str, totp_is_valid=None) -> TotpValidationResul Precedence: 1) Trim & normalize input (strip spaces and separators; normalize full-width digits) - 2) Empty check -> "Enter your 6-digit code" - 3) Length check -> "Code must be 6 digits" - 4) Numeric-only check -> "Code can contain digits only" + 2) Check emptiness, length == 6, and numeric-only - Returns TotpValidationResult(ok, code, message). On success, `code` is a 6-digit ASCII string. + Returns TotpValidationResult(ok, code, message). + - Success: `ok=True`, `code` is a 6-digit ASCII string, `message=''`. + - Failure: `ok=False`, `code=''`, `message` is always the generic INVALID_TOTP_MSG. `totp_is_valid` is reserved for optional server-side checks and ignored here. """ try: @@ -125,17 +125,9 @@ def validate_totp_code(raw_code: str, totp_is_valid=None) -> TotpValidationResul for sep in list(separators): s = s.replace(sep, '') - # Empty check - if s == '': - return TotpValidationResult(False, '', 'Enter your 6-digit code') - - # Length check - if len(s) != 6: - return TotpValidationResult(False, '', 'Code must be 6 digits') - - # Numeric-only check - if not s.isdigit(): - return TotpValidationResult(False, '', 'Code can contain digits only') + # Empty / length / numeric checks + if s == '' or len(s) != 6 or not s.isdigit(): + return TotpValidationResult(False, '', INVALID_TOTP_MSG) # All good return TotpValidationResult(True, s, '')