Security advisory GHSA-rrf6-pxg8-684g: FastAPI Guard has a regex bypass
Package: fastapi-guard (pypi), vulnerable version: 3.0.1, fixed in: 3.0.2
CWE: CWE-1333
CWE-185
CWE-20
Files changed by the fix:
guard/handlers/suspatterns_handler.py
guard/middleware.py
guard/models.py
guard/utils.py

Fix diff:
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 75beceb7..3e55fa5a 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -36,7 +36,7 @@ ___
 
 Environment
 ===========
-- FastAPI Guard version: [e.g. 3.0.1]
+- FastAPI Guard version: [e.g. 3.0.2]
 - Python version: [e.g. 3.11.10]
 - FastAPI version: [e.g. 0.115.0]
 - OS: [e.g. Ubuntu 22.04, Windows 11, MacOS 15.4]
diff --git a/.gitignore b/.gitignore
index ac8ccb1d..67010077 100644
--- a/.gitignore
+++ b/.gitignore
@@ -178,4 +178,5 @@ cython_debug/
 #.idea/
 .DS_Store
 manual_testing/
-.cursor/
\ No newline at end of file
+.kiro/
+.claude/
\ No newline at end of file
diff --git a/.mike.yml b/.mike.yml
index 808194c1..3e17de01 100644
--- a/.mike.yml
+++ b/.mike.yml
@@ -2,6 +2,7 @@ version_selector: true
 title_switch: true
 versions_file: docs/versions/versions.json
 versions:
+  - 3.0.2
   - 3.0.1
   - 3.0.0
   - 2.1.3
@@ -23,4 +24,4 @@ versions:
   - 0.3.2
   - latest
 aliases:
-  latest: 3.0.1
\ No newline at end of file
+  latest: 3.0.2
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9efa257d..dbc5a7bb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,19 @@ Release Notes
 
 ___
 
+v3.0.2 (2025-07-22)
+-------------------
+
+Security Fixes (v3.0.2)
+------------
+
+- **IMPORTANT**: Enhanced ReDoS prevention - Prevent regex bypass due to length limitations on pattern regex. (GHSA-rrf6-pxg8-684g)
+- **CVE ID**: CVE-2025-54365
+- Added timeout to avoid catastrophical backtracking and/or regex bypass by length limitation expression.
+- Added new `regex_timeout` parameter to `SecurityConfig` to allow for custom timeout for regex pattern matching.
+
+___
+
 v3.0.1 (2025-07-07)
 -------------------
 
diff --git a/docs/api/utilities.md b/docs/api/utilities.md
index d37c2bec..8081b81c 100644
--- a/docs/api/utilities.md
+++ b/docs/api/utilities.md
@@ -118,7 +118,8 @@ detect_penetration_attempt
 
 ```python
 async def detect_penetration_attempt(
-    request: Request
+    request: Request,
+    regex_timeout: float = 2.0
 ) -> tuple[bool, str]
 ```
 
@@ -126,11 +127,18 @@ Detect potential penetration attempts in the request.
 
 This function checks various parts of the request (query params, body, path, headers) against a list of suspicious patterns to identify potential security threats.
 
+Parameters:
+
+- `request`: The FastAPI request object to analyze
+- `regex_timeout`: Timeout in seconds for each regex check to prevent ReDoS attacks (default: 2.0 seconds)
+
 Returns a tuple where:
 
 - First element is a boolean: `True` if a potential attack is detected, `False` otherwise
 - Second element is a string with details about what triggered the detection, or empty string if no attack detected
 
+The regex timeout mechanism protects against ReDoS (Regular Expression Denial of Service) attacks by limiting the execution time of each pattern match. If a pattern match exceeds the timeout, it's considered non-matching and a warning is logged.
+
 Example usage:
 
 ```python
@@ -139,12 +147,21 @@ from guard.utils import detect_penetration_attempt
 
 @app.post("/api/submit")
 async def submit_data(request: Request):
+    # Use default timeout of 2.0 seconds
     is_suspicious, trigger_info = await detect_penetration_attempt(request)
     if is_suspicious:
         # Log the detection with details
         logger.warning(f"Attack detected: {trigger_info}")
         return {"error": "Suspicious activity detected"}
     return {"success": True}
+
+@app.post("/api/critical")
+async def critical_endpoint(request: Request):
+    # Use shorter timeout for critical endpoints
+    is_suspicious, trigger_info = await detect_penetration_attempt(request, regex_timeout=0.5)
+    if is_suspicious:
+        return {"error": "Security check failed"}
+    return {"success": True}
 ```
 
 extract_client_ip
@@ -194,4 +211,7 @@ await log_activity(
 
 # Check for penetration attempts
 is_suspicious, trigger_info = await detect_penetration_attempt(request)
+
+# Or with custom timeout
+is_suspicious, trigger_info = await detect_penetration_attempt(request, regex_timeout=1.0)
 ```
diff --git a/docs/release-notes.md b/docs/release-notes.md
index 3461f38f..9a4a3220 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -10,6 +10,19 @@ Release Notes
 
 ___
 
+v3.0.2 (2025-07-22)
+-------------------
+
+Security Fixes (v3.0.2)
+------------
+
+- **IMPORTANT**: Enhanced ReDoS prevention - Prevent regex bypass due to length limitations on pattern regex. (GHSA-rrf6-pxg8-684g)
+- **CVE ID**: CVE-2025-54365
+- Added timeout to avoid catastrophical backtracking and/or regex bypass by length limitation expression.
+- Added new `regex_timeout` parameter to `SecurityConfig` to allow for custom timeout for regex pattern matching.
+
+___
+
 v3.0.1 (2025-07-07)
 -------------------
 
diff --git a/docs/tutorial/security/penetration-detection.md b/docs/tutorial/security/penetration-detection.md
index daca22a1..b946164e 100644
--- a/docs/tutorial/security/penetration-detection.md
+++ b/docs/tutorial/security/penetration-detection.md
@@ -21,7 +21,8 @@ Enable penetration detection:
 config = SecurityConfig(
     enable_penetration_detection=True,
     auto_ban_threshold=5,  # Ban after 5 suspicious requests
-    auto_ban_duration=3600  # Ban duration in seconds
+    auto_ban_duration=3600,  # Ban duration in seconds
+    regex_timeout=2.0  # Timeout for regex pattern matching (default: 2.0 seconds)
 )
 ```
 
@@ -45,6 +46,30 @@ The system checks for various attack patterns including:
 
 ___
 
+Regex Timeout Protection
+------------------------
+
+To prevent ReDoS (Regular Expression Denial of Service) attacks, FastAPI Guard implements a timeout mechanism for regex pattern matching:
+
+```python
+config = SecurityConfig(
+    regex_timeout=2.0  # Default: 2.0 seconds
+)
+```
+
+The `regex_timeout` parameter:
+- Prevents malicious inputs from causing excessive CPU usage through regex backtracking
+- Can be configured between 0.1 and 30.0 seconds
+- Defaults to 2.0 seconds for balanced security and performance
+- If a regex match exceeds the timeout, it's considered non-matching and logged as a potential ReDoS attempt
+
+When a timeout occurs, you'll see a warning in the logs:
+```text
+WARNING - Regex timeout exceeded for pattern '<pattern>' - Potential ReDoS attack blocked.
+```
+
+___
+
 Custom Detection Logic
 ----------------------
 
@@ -55,7 +80,8 @@ from guard.utils import detect_penetration_attempt
 
 @app.post("/api/data")
 async def submit_data(request: Request):
-    is_suspicious, trigger_info = await detect_penetration_attempt(request)
+    # Use custom timeout if needed (default is 2.0 seconds)
+    is_suspicious, trigger_info = await detect_penetration_attempt(request, regex_timeout=1.5)
     if is_suspicious:
         return JSONResponse(
             status_code=400,
diff --git a/docs/versions/versions.json b/docs/versions/versions.json
index 3e7749b6..2e580ef8 100644
--- a/docs/versions/versions.json
+++ b/docs/versions/versions.json
@@ -1,4 +1,5 @@
 {
+    "3.0.2": "3.0.2",
     "3.0.1": "3.0.1",
     "3.0.0": "3.0.0",
     "2.1.3": "2.1.3",
@@ -19,5 +20,5 @@
     "0.3.4": "0.3.4",
     "0.3.3": "0.3.3",
     "0.3.2": "0.3.2",
-    "latest": "3.0.1"
+    "latest": "3.0.2"
 }
\ No newline at end of file
diff --git a/guard/handlers/suspatterns_handler.py b/guard/handlers/suspatterns_handler.py
index 3d116324..a78e1381 100644
--- a/guard/handlers/suspatterns_handler.py
+++ b/guard/handlers/suspatterns_handler.py
@@ -19,85 +19,85 @@ class SusPatternsManager:
 
     patterns: list[str] = [
         # XSS
-        r"<script[^>]{0,100}>[^<]{0,1000}<\/script\s{0,10}>",  # Basic script tag
-        r"javascript:\s{0,10}[^\s]{1,200}",  # javascript: protocol
+        r"<script[^>]*>[^<]*<\/script\s*>",  # Basic script tag
+        r"javascript:\s*[^\s]+",  # javascript: protocol
         # Event handlers
         r"(?:on(?:error|load|click|mouseover|submit|mouse|unload|change|focus|"
-        r"blur|drag))=(?:[\"'][^\"']{1,100}[\"']|[^\s>]{1,100})",
+        r"blur|drag))=(?:[\"'][^\"']*[\"']|[^\s>]+)",
         # Malicious attributes
-        r"(?:<[^>]{1,200}\s{1,20}(?:href|src|data|action)\s{0,10}=[\s\"\']{0,3}(?:javascript|"
+        r"(?:<[^>]+\s+(?:href|src|data|action)\s*=[\s\"\']*(?:javascript|"
         r"vbscript|data):)",
         # CSS expressions
-        r"(?:<[^>]{1,200}style\s{0,10}=[\s\"\']{0,3}[^>\"\']{1,200}(?:expression|behavior|url)\s{0,10}\("
-        r"[^)]{1,200}\))",
-        r"(?:<object[^>]{1,200}>[\s\S]{1,1000}<\/object\s{0,10}>)",  # Suspicious obj
-        r"(?:<embed[^>]{1,200}>[\s\S]{1,1000}<\/embed\s{0,10}>)",  # Suspicious embeds
-        r"(?:<applet[^>]{1,200}>[\s\S]{1,1000}<\/applet\s{0,10}>)",  # Java applets
+        r"(?:<[^>]+style\s*=[\s\"\']*[^>\"\']*(?:expression|behavior|url)\s*\("
+        r"[^)]*\))",
+        r"(?:<object[^>]*>[\s\S]*<\/object\s*>)",  # Suspicious obj
+        r"(?:<embed[^>]*>[\s\S]*<\/embed\s*>)",  # Suspicious embeds
+        r"(?:<applet[^>]*>[\s\S]*<\/applet\s*>)",  # Java applets
         # SQL Injection
         # Basic SELECT statements
-        r"(?i)SELECT\s{1,20}[\w\s,\*]{1,200}\s{1,20}FROM\s{1,20}[\w\s\._]{1,100}",
+        r"(?i)SELECT\s+[\w\s,\*]+\s+FROM\s+[\w\s\._]+",
         # UNION-based queries
-        r"(?i)UNION\s{1,20}(?:ALL\s{1,20})?SELECT",
+        r"(?i)UNION\s+(?:ALL\s+)?SELECT",
         # Logic-based
-        r"(?i)('\s{0,5}(?:OR|AND)\s{0,5}[\(\s]{0,5}'?[\d\w]{1,50}\s{0,5}(?:=|LIKE|<|>|<=|>=)\s{0,5}"
-        r"[\(\s]{0,5}'?[\d\w]{1,50})",
+        r"(?i)('\s*(?:OR|AND)\s*[\(\s]*'?[\d\w]+\s*(?:=|LIKE|<|>|<=|>=)\s*"
+        r"[\(\s]*'?[\d\w]+)",
         # UNION-based
-        r"(?i)(UNION\s{1,20}(?:ALL\s{1,20})?SELECT\s{1,20}(?:NULL[,\s]{0,10}){1,20}|\(\s{0,10}SELECT\s{1,20}"
+        r"(?i)(UNION\s+(?:ALL\s+)?SELECT\s+(?:NULL[,\s]*)+|\(\s*SELECT\s+"
         r"(?:@@|VERSION))",
-        r"(?i)(?:INTO\s{1,20}(?:OUTFILE|DUMPFILE)\s{1,20}'[^']{1,200}')",  # File ops
-        r"(?i)(?:LOAD_FILE\s{0,10}\([^)]{1,200}\))",  # File reading
-        r"(?i)(?:BENCHMARK\s{0,10}\(\s{0,10}\d{1,10}\s{0,10},)",  # Time-based
-        r"(?i)(?:SLEEP\s{0,10}\(\s{0,10}\d{1,10}\s{0,10}\))",  # Time-based
+        r"(?i)(?:INTO\s+(?:OUTFILE|DUMPFILE)\s+'[^']+')",  # File ops
+        r"(?i)(?:LOAD_FILE\s*\([^)]+\))",  # File reading
+        r"(?i)(?:BENCHMARK\s*\(\s*\d+\s*,)",  # Time-based
+        r"(?i)(?:SLEEP\s*\(\s*\d+\s*\))",  # Time-based
         # Comment-based
-        r"(?i)(?:\/\*![0-9]{0,10}\s{0,10}(?:OR|AND|UNION|SELECT|INSERT|DELETE|DROP|"
+        r"(?i)(?:\/\*![0-9]*\s*(?:OR|AND|UNION|SELECT|INSERT|DELETE|DROP|"
         r"CONCAT|CHAR|UPDATE)\b)",
         # Directory Traversal
-        r"(?:\.\./|\.\\/){2,10}",  # Multiple traversal
+        r"(?:\.\.\/|\.\.\\)(?:\.\.\/|\.\.\\)+",  # Multiple traversal
         # Sensitive files
         r"(?:/etc/(?:passwd|shadow|group|hosts|motd|issue|mysql/my.cnf|ssh/"
         r"ssh_config)$)",
-        r"(?:boot\.ini|win\.ini|system\.ini|config\.sys)\s{0,10}$",  # Windows files
+        r"(?:boot\.ini|win\.ini|system\.ini|config\.sys)\s*$",  # Windows files
         r"(?:\/proc\/self\/environ$)",  # Process information
-        r"(?:\/var\/log\/[^\/]{1,100}$)",  # Log files
+        r"(?:\/var\/log\/[^\/]+$)",  # Log files
         # Command Injection
         # Basic commands
-        r";\s{0,10}(?:ls|cat|rm|chmod|chown|wget|curl|nc|netcat|ping|telnet)\s{1,20}"
-        r"-[a-zA-Z]{1,20}\s{1,20}",
+        r";\s*(?:ls|cat|rm|chmod|chown|wget|curl|nc|netcat|ping|telnet)\s+"
+        r"-[a-zA-Z]+\s+",
         # Download commands
-        r"\|\s{0,10}(?:wget|curl|fetch|lwp-download|lynx|links|GET)\s{1,20}",
+        r"\|\s*(?:wget|curl|fetch|lwp-download|lynx|links|GET)\s+",
         # Command substitution
-        r"(?:[;&|`]\s{0,10}(?:\$\([^)]{1,100}\)|\$\{[^}]{1,100}\}))",
+        r"(?:[;&|`]\s*(?:\$\([^)]+\)|\$\{[^}]+\}))",
         # Shell execution
-        r"(?:^|;)\s{0,10}(?:bash|sh|ksh|csh|tsch|zsh|ash)\s{1,20}-[a-zA-Z]{1,20}",
+        r"(?:^|;)\s*(?:bash|sh|ksh|csh|tsch|zsh|ash)\s+-[a-zA-Z]+",
         # PHP functions
-        r"\b(?:eval|system|exec|shell_exec|passthru|popen|proc_open)\s{0,10}\(",
+        r"\b(?:eval|system|exec|shell_exec|passthru|popen|proc_open)\s*\(",
         # File Inclusion
         # Protocols
         r"(?:php|data|zip|rar|file|glob|expect|input|phpinfo|zlib|phar|ssh2|"
-        r"rar|ogg|expect)://[^\s]{1,200}",
+        r"rar|ogg|expect)://[^\s]+",
         # URLs
-        r"(?:\/\/[0-9a-zA-Z]([-.\w]{0,50}[0-9a-zA-Z]){0,10}(:[0-9]{0,10}){0,1}(?:\/?)(?:"
-        r"[a-zA-Z0-9\-\.\?,'/\\\+&amp;%\$#_]{0,500})?)",
+        r"(?:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:[0-9]+)?(?:\/?)(?:"
+        r"[a-zA-Z0-9\-\.\?,'/\\\+&amp;%\$#_]*)?)",
         # LDAP Injection
-        r"\(\s{0,10}[|&]\s{0,10}\(\s{0,10}[^)]{1,100}=[*]",  # Wildcards
-        r"(?:\*(?:[\s\d\w]{1,50}\s{0,10}=|=\s{0,10}[\d\w\s]{1,50}))",  # Attribute match
-        r"(?:\(\s{0,10}[&|]\s{0,10})",  # Logic operations
+        r"\(\s*[|&]\s*\(\s*[^)]+=[*]",  # Wildcards
+        r"(?:\*(?:[\s\d\w]+\s*=|=\s*[\d\w\s]+))",  # Attribute match
+        r"(?:\(\s*[&|]\s*)",  # Logic operations
         # XML Injection
-        r"<!(?:ENTITY|DOCTYPE)[^>]{1,200}SYSTEM[^>]{1,200}>",  # XXE
-        r"(?:<!\[CDATA\[.{0,1000}?\]\]>)",  # CDATA sections
-        r"(?:<\?xml.{0,200}?\?>)",  # XML declarations
+        r"<!(?:ENTITY|DOCTYPE)[^>]+SYSTEM[^>]+>",  # XXE
+        r"(?:<!\[CDATA\[.*?\]\]>)",  # CDATA sections
+        r"(?:<\?xml.*?\?>)",  # XML declarations
         # SSRF
         # Local addresses
-        r"(?:^|\s|/)(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::(?:\d{0,10})\]|(?:169\.254|192\.168|10\.|"
-        r"172\.(?:1[6-9]|2[0-9]|3[01]))\.\d{1,3})(?:\s|$|/)",
-        r"(?:file|dict|gopher|jar|tftp)://[^\s]{1,200}",  # Dangerous protocols
+        r"(?:^|\s|/)(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::(?:\d*)\]|(?:169\.254|192\.168|10\.|"
+        r"172\.(?:1[6-9]|2[0-9]|3[01]))\.\d+)(?:\s|$|/)",
+        r"(?:file|dict|gopher|jar|tftp)://[^\s]+",  # Dangerous protocols
         # NoSQL Injection
         # MongoDB
-        r"\{\s{0,10}\$(?:where|gt|lt|ne|eq|regex|in|nin|all|size|exists|type|mod|"
+        r"\{\s*\$(?:where|gt|lt|ne|eq|regex|in|nin|all|size|exists|type|mod|"
         r"options):",
-        r"(?:\{\s{0,10}\$[a-zA-Z]{1,20}\s{0,10}:\s{0,10}(?:\{|\[))",  # Nested operators
+        r"(?:\{\s*\$[a-zA-Z]+\s*:\s*(?:\{|\[))",  # Nested operators
         # File Upload
-        r"(?i)filename=[\"'].{0,200}?\.(?:php\d{0,5}|phar|phtml|exe|jsp|asp|aspx|sh|"
+        r"(?i)filename=[\"'].*?\.(?:php\d*|phar|phtml|exe|jsp|asp|aspx|sh|"
         r"bash|rb|py|pl|cgi|com|bat|cmd|vbs|vbe|js|ws|wsf|msi|hta)[\"\']",
         # Path Traversal
         # Encoded traversal
@@ -105,11 +105,11 @@ class SusPatternsManager:
         r"%e0%80%ae|%25c0%25ae)/",
         # Template Injection
         # Basic template injection
-        r"\{\{\s{0,10}[^\}]{1,200}(?:system|exec|popen|eval|require|include)\s{0,10}\}\}",
+        r"\{\{\s*[^\}]+(?:system|exec|popen|eval|require|include)\s*\}\}",
         # Alternative syntax
-        r"\{\%\s{0,10}[^\%]{1,200}(?:system|exec|popen|eval|require|include)\s{0,10}\%\}",
+        r"\{\%\s*[^\%]+(?:system|exec|popen|eval|require|include)\s*\%\}",
         # HTTP Response Splitting
-        r"[\r\n]\s{0,10}(?:HTTP\/[0-9.]{1,10}|Location:|Set-Cookie:)",
+        r"[\r\n]\s*(?:HTTP\/[0-9.]+|Location:|Set-Cookie:)",
     ]
 
     compiled_patterns: list[re.Pattern]
diff --git a/guard/middleware.py b/guard/middleware.py
index 1d0fb02f..13b4f7c4 100644
--- a/guard/middleware.py
+++ b/guard/middleware.py
@@ -337,7 +337,9 @@ async def dispatch(
         if penetration_enabled and not self._should_bypass_check(
             "penetration", route_config
         ):
-            detection_result, trigger_info = await detect_penetration_attempt(request)
+            detection_result, trigger_info = await detect_penetration_attempt(
+                request, self.config.regex_timeout
+            )
             sus_specs = f"{client_ip} - {trigger_info}"
             if detection_result:
                 self.suspicious_request_counts[client_ip] = (
diff --git a/guard/models.py b/guard/models.py
index a4d75ac0..d4d4b76c 100644
--- a/guard/models.py
+++ b/guard/models.py
@@ -376,6 +376,19 @@ class SecurityConfig(BaseModel):
         Whether to enable penetration attempt detection.
     """
 
+    regex_timeout: float = Field(
+        default=2.0,
+        description="Timeout for regex pattern matching to prevent ReDoS attacks",
+        ge=0.1,
+        le=30.0,
+    )
+    """
+    float:
+        Timeout in seconds for regex pattern matching.
+        This prevents ReDoS (Regular Expression Denial of Service) attacks.
+        Must be between 0.1 and 30.0 seconds. Default is 2.0 seconds.
+    """
+
     ipinfo_token: str | None = Field(
         default=None,
         description="IPInfo API token for IP geolocation. Deprecated. "
diff --git a/guard/utils.py b/guard/utils.py
index 33ff7733..cd6e1cdf 100644
--- a/guard/utils.py
+++ b/guard/utils.py
@@ -1,4 +1,5 @@
 # fastapi_guard/utils.py
+import concurrent.futures
 import logging
 import re
 from ipaddress import ip_address, ip_network
@@ -349,7 +350,9 @@ async def is_ip_allowed(
         return True
 
 
-async def detect_penetration_attempt(request: Request) -> tuple[bool, str]:
+async def detect_penetration_attempt(
+    request: Request, regex_timeout: float = 2.0
+) -> tuple[bool, str]:
     """
     Detect potential penetration
     attempts in the request.
@@ -363,6 +366,8 @@ async def detect_penetration_attempt(request: Request) -> tuple[bool, str]:
     Args:
         request (Request):
             The FastAPI request object to analyze.
+        regex_timeout (float):
+            Timeout in seconds for each regex check. Defaults to 2.0 seconds.
 
     Returns:
         tuple[bool, str]:
@@ -372,6 +377,30 @@ async def detect_penetration_attempt(request: Request) -> tuple[bool, str]:
 
     suspicious_patterns = await sus_patterns_handler.get_all_compiled_patterns()
 
+    def _regex_search_with_timeout(
+        pattern: re.Pattern, text: str, timeout: float
+    ) -> bool:
+        """Execute regex search with a timeout to prevent ReDoS."""
+
+        def _search() -> bool:
+            return pattern.search(text) is not None
+
+        with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
+            future = executor.submit(_search)
+            try:
+                return future.result(timeout=timeout)
+            except concurrent.futures.TimeoutError:
+                logging.warning(
+                    f"Regex timeout exceeded for pattern '{pattern.pattern}' - "
+                    f"Potential ReDoS attack blocked."
+                )
+                # Cancel the future to clean up
+                future.cancel()
+                return False
+            except Exception as e:
+                logging.error(f"Error in regex search: {str(e)}")
+                return False
+
     async def check_value(value: str) -> tuple[bool, str]:
         try:
             import json
@@ -380,7 +409,11 @@ async def check_value(value: str) -> tuple[bool, str]:
             if isinstance(data, dict):
                 for pattern in suspicious_patterns:
                     for k, v in data.items():
-                        if isinstance(v, str) and pattern.search(v):
+                        if isinstance(v, str) and _regex_search_with_timeout(
+                            pattern,
+                            v,
+                            regex_timeout,
+                        ):
                             return (
                                 True,
                                 f"JSON field '{k}' matched pattern '{pattern.pattern}'",
@@ -388,7 +421,7 @@ async def check_value(value: str) -> tuple[bool, str]:
                 return False, ""
         except json.JSONDecodeError:
             for pattern in suspicious_patterns:
-                if pattern.search(value):
+                if _regex_search_with_timeout(pattern, value, regex_timeout):
                     return True, f"Value matched pattern '{pattern.pattern}'"
         return False, ""
 
diff --git a/pyproject.toml b/pyproject.toml
index f35223c1..2c30e065 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [project]
 name = "fastapi_guard"
-version = "3.0.1"
+version = "3.0.2"
 description = "A security library for FastAPI to control IPs, log requests, and detect penetration attempts."
 authors = [
     {name = "Renzo Franceschini", email = "rennf93@users.noreply.github.com"}
diff --git a/tests/test_middleware/test_security_middleware.py b/tests/test_middleware/test_security_middleware.py
index 3c5e626a..04c88c29 100644
--- a/tests/test_middleware/test_security_middleware.py
+++ b/tests/test_middleware/test_security_middleware.py
@@ -1198,7 +1198,7 @@ async def receive() -> dict[str, str | bytes]:
         assert response.status_code == status.HTTP_200_OK
         assert call_next_called, "call_next should be called in passive mode"
 
-        mock_detect.assert_called_once_with(request)
+        mock_detect.assert_called_once_with(request, 2.0)
 
         mock_log.assert_any_call(
             request,
diff --git a/tests/test_utils/test_request_checks.py b/tests/test_utils/test_request_checks.py
index f099d1ac..6d093fab 100644
--- a/tests/test_utils/test_request_checks.py
+++ b/tests/test_utils/test_request_checks.py
@@ -740,3 +740,100 @@ async def test_is_ip_allowed_blocked_country(mocker: MockerFixture) -> None:
 
     result = await is_ip_allowed("192.168.1.1", config, mock_ipinfo)
     assert not result
+
+
+@pytest.mark.asyncio
+async def test_detect_penetration_attempt_regex_timeout() -> None:
+    """Test regex timeout handling in detect_penetration_attempt."""
+    import concurrent.futures
+    from unittest.mock import MagicMock
+
+    async def receive() -> dict[str, str | bytes]:
+        return {"type": "http.request", "body": b""}
+
+    request = Request(
+        scope={
+            "type": "http",
+            "method": "GET",
+            "path": "/",
+            "headers": [],
+            "query_string": b"param=test",
+            "client": ("127.0.0.1", 12345),
+        },
+        receive=receive,
+    )
+
+    # Mock the ThreadPoolExecutor and Future
+    mock_future = MagicMock()
+    mock_future.result.side_effect = concurrent.futures.TimeoutError()
+    mock_future.cancel = MagicMock()
+
+    mock_executor = MagicMock()
+    mock_executor.submit.return_value = mock_future
+    mock_executor.__enter__ = MagicMock(return_value=mock_executor)
+    mock_executor.__exit__ = MagicMock(return_value=None)
+
+    with (
+        patch("concurrent.futures.ThreadPoolExecutor", return_value=mock_executor),
+        patch("logging.warning") as mock_warning,
+    ):
+        result, trigger = await detect_penetration_attempt(request, regex_timeout=0.1)
+
+        # Should not detect as attack when timeout occurs
+        assert not result
+        assert trigger == ""
+
+        # Check that warning was logged
+        mock_warning.assert_called()
+        warning_msg = mock_warning.call_args[0][0]
+        assert "Regex timeout exceeded" in warning_msg
+        assert "Potential ReDoS attack blocked" in warning_msg
+
+        # Check that future was cancelled (multiple times for multiple patterns)
+        assert mock_future.cancel.call_count > 0
+
+
+@pytest.mark.asyncio
+async def test_detect_penetration_attempt_regex_exception() -> None:
+    """Test general exception handling in regex search."""
+    from unittest.mock import MagicMock
+
+    async def receive() -> dict[str, str | bytes]:
+        return {"type": "http.request", "body": b""}
+
+    request = Request(
+        scope={
+            "type": "http",
+            "method": "GET",
+            "path": "/",
+            "headers": [],
+            "query_string": b"param=test",
+            "client": ("127.0.0.1", 12345),
+        },
+        receive=receive,
+    )
+
+    # Mock the ThreadPoolExecutor and Future to raise a general exception
+    mock_future = MagicMock()
+    mock_future.result.side_effect = Exception("Unexpected regex error")
+
+    mock_executor = MagicMock()
+    mock_executor.submit.return_value = mock_future
+    mock_executor.__enter__ = MagicMock(return_value=mock_executor)
+    mock_executor.__exit__ = MagicMock(return_value=None)
+
+    with (
+        patch("concurrent.futures.ThreadPoolExecutor", return_value=mock_executor),
+        patch("logging.error") as mock_error,
+    ):
+        result, trigger = await detect_penetration_attempt(request, regex_timeout=2.0)
+
+        # Should not detect as attack when exception occurs
+        assert not result
+        assert trigger == ""
+
+        # Check that error was logged
+        mock_error.assert_called()
+        error_msg = mock_error.call_args[0][0]
+        assert "Error in regex search" in error_msg
+        assert "Unexpected regex error" in str(mock_error.call_args[0])