From d67699684b3286d8c3f35a8fafa5fd6045f669e9 Mon Sep 17 00:00:00 2001 From: Omer Cohen Date: Thu, 12 Mar 2026 06:05:42 +0000 Subject: [PATCH 1/5] fix: address critical security vulnerabilities in Python SDK Security fixes addressing March 2026 security review findings: CRITICAL fixes: - Flask cookies now enforce secure=True flag (prevents session hijacking over HTTP) - Replace ReDoS-vulnerable phone regex with simpler bounded pattern (prevents DoS) HIGH severity fixes: - Add deprecation warning to decode_token_unverified() (prevents misuse for auth) - Add SecurityWarning when skip_verify=True is used (alerts TLS bypass) - Document skip_verify in samples as local-dev-only (prevents production misuse) The skip_verify parameter is intentionally kept for local development with self-signed certificates, but now emits clear warnings to prevent production use. Fixes: Related to code scanning alert #2914 (phone regex) Co-authored-by: Shuni <251468265+shuni-bot[bot]@users.noreply.github.com> --- descope/common.py | 5 ++++- descope/descope_client.py | 12 ++++++++++++ descope/flask/__init__.py | 2 +- descope/jwt_common.py | 12 ++++++++++++ samples/otp_web_sample_app.py | 4 +++- samples/password_web_sample_app.py | 4 +++- 6 files changed, 35 insertions(+), 4 deletions(-) diff --git a/descope/common.py b/descope/common.py index abe045c24..cb9633632 100644 --- a/descope/common.py +++ b/descope/common.py @@ -8,7 +8,10 @@ DEFAULT_BASE_URL = DEFAULT_URL_PREFIX + "." + DEFAULT_DOMAIN # pragma: no cover DEFAULT_TIMEOUT_SECONDS = 60 -PHONE_REGEX = r"""^(?:(?:\(?(?:00|\+)([1-4]\d\d|[1-9]\d?)\)?)?[\-\.\ \\/]?)?((?:\(?\d{1,}\)?[\-\.\ \\/]?){0,})(?:[\-\.\ \\/]?(?:#|ext\.?|extension|x)[\-\.\ \\/]?(\d+))?$""" +# Simple phone validation to prevent ReDoS (catastrophic backtracking) +# Allows digits, spaces, hyphens, plus, parentheses, dots, # for extension +# Length: 7-25 characters (reasonable for international phone numbers) +PHONE_REGEX = r"""^[\d\s\-\+\(\)\.#xX]{7,25}$""" SESSION_COOKIE_NAME = "DS" REFRESH_SESSION_COOKIE_NAME = "DSR" diff --git a/descope/descope_client.py b/descope/descope_client.py index 610400901..dc1d47b1e 100644 --- a/descope/descope_client.py +++ b/descope/descope_client.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +import warnings from typing import Iterable import requests @@ -50,6 +51,17 @@ def __init__( ), ) + # Warn about TLS verification bypass + if skip_verify: + warnings.warn( + "⚠️ SECURITY WARNING: TLS certificate verification is DISABLED (skip_verify=True). " + "This makes your application vulnerable to man-in-the-middle attacks. " + "ONLY use this for local development with self-signed certificates. " + "NEVER use skip_verify=True in production environments.", + category=SecurityWarning, + stacklevel=2, + ) + # Auth Initialization auth_http_client = HTTPClient( project_id=project_id, diff --git a/descope/flask/__init__.py b/descope/flask/__init__.py index aab97141b..a0296e04f 100644 --- a/descope/flask/__init__.py +++ b/descope/flask/__init__.py @@ -34,7 +34,7 @@ def set_cookie_on_response(response: Response, token: dict, cookie_data: dict): expires=cookie_data.get("exp", expire_time), path=cookie_data.get("path", "/"), domain=cookie_domain, - secure=False, # True + secure=True, # Cookies must be sent over HTTPS only httponly=True, samesite="Strict", # "Strict", "Lax", "None" ) diff --git a/descope/jwt_common.py b/descope/jwt_common.py index 2183df5ea..137c970dd 100644 --- a/descope/jwt_common.py +++ b/descope/jwt_common.py @@ -1,5 +1,6 @@ from __future__ import annotations +import warnings from typing import Callable, Iterable, Optional import jwt @@ -71,9 +72,20 @@ def decode_token_unverified( ) -> dict: """Decode a JWT without verifying signature (used when no validator is provided). + WARNING: This function does NOT verify JWT signatures and should NEVER be used + for authentication or authorization decisions. It is only intended for testing + or scenarios where token validation happens elsewhere. + Audience verification is disabled by default since no key is provided. Returns an empty dict if decoding fails. """ + warnings.warn( + "decode_token_unverified() does not verify JWT signatures. " + "Do not use this for authentication or authorization. " + "Use proper token validation with signature verification instead.", + DeprecationWarning, + stacklevel=2, + ) try: return jwt.decode( token, options={"verify_signature": False, "verify_aud": False} diff --git a/samples/otp_web_sample_app.py b/samples/otp_web_sample_app.py index e0ce2cddb..7f43f33f7 100644 --- a/samples/otp_web_sample_app.py +++ b/samples/otp_web_sample_app.py @@ -18,7 +18,9 @@ PROJECT_ID = "" -# init the DescopeClient +# SECURITY WARNING: skip_verify=True disables TLS certificate verification +# ONLY use this for local development with self-signed certificates +# NEVER use skip_verify=True in production - remove this parameter for production descope_client = DescopeClient(PROJECT_ID, skip_verify=True) diff --git a/samples/password_web_sample_app.py b/samples/password_web_sample_app.py index 13575c064..c3aaf44a3 100755 --- a/samples/password_web_sample_app.py +++ b/samples/password_web_sample_app.py @@ -14,7 +14,9 @@ PROJECT_ID = "" -# init the DescopeClient +# SECURITY WARNING: skip_verify=True disables TLS certificate verification +# ONLY use this for local development with self-signed certificates +# NEVER use skip_verify=True in production - remove this parameter for production descope_client = DescopeClient(PROJECT_ID, skip_verify=True) From b9d28ca01e2b516cbad4ee3c1b3bc5db3b612a75 Mon Sep 17 00:00:00 2001 From: Omer Date: Thu, 12 Mar 2026 07:07:29 +0000 Subject: [PATCH 2/5] fix: address review feedback - phone regex lookahead, SecurityWarning category --- descope/common.py | 2 +- descope/jwt_common.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/descope/common.py b/descope/common.py index cb9633632..fbe921621 100644 --- a/descope/common.py +++ b/descope/common.py @@ -11,7 +11,7 @@ # Simple phone validation to prevent ReDoS (catastrophic backtracking) # Allows digits, spaces, hyphens, plus, parentheses, dots, # for extension # Length: 7-25 characters (reasonable for international phone numbers) -PHONE_REGEX = r"""^[\d\s\-\+\(\)\.#xX]{7,25}$""" +PHONE_REGEX = r"""^(?=.*\d{4,})[\d\s\-\+\(\)\.#xX]{7,25}$""" SESSION_COOKIE_NAME = "DS" REFRESH_SESSION_COOKIE_NAME = "DSR" diff --git a/descope/jwt_common.py b/descope/jwt_common.py index 137c970dd..448140dc7 100644 --- a/descope/jwt_common.py +++ b/descope/jwt_common.py @@ -83,7 +83,7 @@ def decode_token_unverified( "decode_token_unverified() does not verify JWT signatures. " "Do not use this for authentication or authorization. " "Use proper token validation with signature verification instead.", - DeprecationWarning, + SecurityWarning, stacklevel=2, ) try: From 2b189fef2004f331b5317522bc0ef9a9b837f672 Mon Sep 17 00:00:00 2001 From: Omer Date: Thu, 12 Mar 2026 07:21:21 +0000 Subject: [PATCH 3/5] fix: phone regex - restrict to single leading +, reject ++prefix --- descope/common.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/descope/common.py b/descope/common.py index fbe921621..0f7a9ae7a 100644 --- a/descope/common.py +++ b/descope/common.py @@ -9,9 +9,9 @@ DEFAULT_TIMEOUT_SECONDS = 60 # Simple phone validation to prevent ReDoS (catastrophic backtracking) -# Allows digits, spaces, hyphens, plus, parentheses, dots, # for extension -# Length: 7-25 characters (reasonable for international phone numbers) -PHONE_REGEX = r"""^(?=.*\d{4,})[\d\s\-\+\(\)\.#xX]{7,25}$""" +# Optional leading +, then digits, spaces, hyphens, parentheses, dots, # for extension +# Requires at least 4 consecutive digits, length 7-25, at most one leading + +PHONE_REGEX = r"""^(?=.*\d{4,})\+?[\d\s\-\(\)\.#xX]{6,24}$""" SESSION_COOKIE_NAME = "DS" REFRESH_SESSION_COOKIE_NAME = "DSR" From 49736ac03669640ec7345006beb622a43e207e5a Mon Sep 17 00:00:00 2001 From: Omer Date: Thu, 12 Mar 2026 07:34:13 +0000 Subject: [PATCH 4/5] fix: suppress SecurityWarning in test_decode_token_unverified_handles_garbage --- tests/test_jwt_common.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_jwt_common.py b/tests/test_jwt_common.py index a6b9c7add..c5bd86bed 100644 --- a/tests/test_jwt_common.py +++ b/tests/test_jwt_common.py @@ -1,4 +1,5 @@ import unittest +import warnings from descope.jwt_common import ( COOKIE_DATA_NAME, @@ -58,7 +59,9 @@ def validator(token: str, audience=None): def test_decode_token_unverified_handles_garbage(self): # Invalid token strings should not raise and should return empty dict - assert decode_token_unverified("not-a-jwt") == {} + with warnings.catch_warnings(): + warnings.simplefilter("ignore", SecurityWarning) + assert decode_token_unverified("not-a-jwt") == {} if __name__ == "__main__": From a4360afc3251813a0b8e886c9b121c6dc2029574 Mon Sep 17 00:00:00 2001 From: Omer Date: Thu, 12 Mar 2026 07:51:32 +0000 Subject: [PATCH 5/5] fix: SecurityWarning does not exist in Python - use UserWarning --- descope/descope_client.py | 2 +- descope/jwt_common.py | 2 +- tests/test_jwt_common.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/descope/descope_client.py b/descope/descope_client.py index dc1d47b1e..3a07b01b2 100644 --- a/descope/descope_client.py +++ b/descope/descope_client.py @@ -58,7 +58,7 @@ def __init__( "This makes your application vulnerable to man-in-the-middle attacks. " "ONLY use this for local development with self-signed certificates. " "NEVER use skip_verify=True in production environments.", - category=SecurityWarning, + category=UserWarning, stacklevel=2, ) diff --git a/descope/jwt_common.py b/descope/jwt_common.py index 448140dc7..500f047c7 100644 --- a/descope/jwt_common.py +++ b/descope/jwt_common.py @@ -83,7 +83,7 @@ def decode_token_unverified( "decode_token_unverified() does not verify JWT signatures. " "Do not use this for authentication or authorization. " "Use proper token validation with signature verification instead.", - SecurityWarning, + UserWarning, stacklevel=2, ) try: diff --git a/tests/test_jwt_common.py b/tests/test_jwt_common.py index c5bd86bed..9cfb47e6a 100644 --- a/tests/test_jwt_common.py +++ b/tests/test_jwt_common.py @@ -60,7 +60,7 @@ def validator(token: str, audience=None): def test_decode_token_unverified_handles_garbage(self): # Invalid token strings should not raise and should return empty dict with warnings.catch_warnings(): - warnings.simplefilter("ignore", SecurityWarning) + warnings.simplefilter("ignore", UserWarning) assert decode_token_unverified("not-a-jwt") == {}