VYPR
Moderate severityNVD Advisory· Published Jul 7, 2025· Updated Jul 7, 2025

ReDoS in fastapi-guard's penetration attempts detector

CVE-2025-53539

Description

FastAPI Guard is a security library for FastAPI that provides middleware to control IPs, log requests, and detect penetration attempts. fastapi-guard's penetration attempts detection uses regex to scan incoming requests. However, some of the regex patterns used in detection are extremely inefficient and can cause polynomial complexity backtracks when handling specially crafted inputs. This vulnerability is fixed in 3.0.1.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
fastapi-guardPyPI
< 3.0.13.0.1

Affected products

1

Patches

1
d9d50e8130b7

Merge commit from fork

12 files changed · +120 107
  • CHANGELOG.md+11 0 modified
    @@ -3,6 +3,17 @@ Release Notes
     
     ___
     
    +v3.0.1 (2025-07-07)
    +-------------------
    +
    +Security Fixes (v3.0.1)
    +------------
    +
    +- **IMPORTANT**: Prevented ReDoS (Regular Expression Denial of Service - CWE-1333) attacks by replacing unbounded regex quantifiers with bounded ones. (GHSA-j47q-rc62-w448)
    +- **CVE ID**: (TBD)
    +
    +___
    +
     v3.0.0 (2025-06-21)
     -------------------
     
    
  • docs/index.md+1 1 modified
    @@ -75,7 +75,7 @@ You can also download the example app as a Docker container from [GitHub Contain
     docker pull ghcr.io/rennf93/fastapi-guard-example:latest
     
     # Or pull a specific version (matches library releases)
    -docker pull ghcr.io/rennf93/fastapi-guard-example:v3.0.0
    +docker pull ghcr.io/rennf93/fastapi-guard-example:v3.0.1
     ```
     
     ___
    
  • docs/release-notes.md+11 0 modified
    @@ -10,6 +10,17 @@ Release Notes
     
     ___
     
    +v3.0.1 (2025-07-07)
    +-------------------
    +
    +Security Fixes (v3.0.1)
    +------------
    +
    +- **IMPORTANT**: Prevented ReDoS (Regular Expression Denial of Service - CWE-1333) attacks by replacing unbounded regex quantifiers with bounded ones. (GHSA-j47q-rc62-w448)
    +- **CVE ID**: (TBD)
    +
    +___
    +
     v3.0.0 (2025-06-21)
     -------------------
     
    
  • docs/versions/versions.json+2 1 modified
    @@ -1,4 +1,5 @@
     {
    +    "3.0.1": "3.0.1",
         "3.0.0": "3.0.0",
         "2.1.3": "2.1.3",
         "2.1.2": "2.1.2",
    @@ -18,5 +19,5 @@
         "0.3.4": "0.3.4",
         "0.3.3": "0.3.3",
         "0.3.2": "0.3.2",
    -    "latest": "3.0.0"
    +    "latest": "3.0.1"
     }
    \ No newline at end of file
    
  • .github/ISSUE_TEMPLATE/bug_report.md+1 1 modified
    @@ -36,7 +36,7 @@ ___
     
     Environment
     ===========
    -- FastAPI Guard version: [e.g. 3.0.0]
    +- FastAPI Guard version: [e.g. 3.0.1]
     - 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]
    
  • guard/handlers/cloud_handler.py+1 1 modified
    @@ -53,7 +53,7 @@ def fetch_azure_ip_ranges() -> set[ipaddress.IPv4Network | ipaddress.IPv6Network
             response.raise_for_status()
     
             decoded_html = html.unescape(response.text)
    -        pattern = r'href=["\'](https://download\.microsoft\.com/' r'.*?\.json)["\']'
    +        pattern = r'href=["\'](https://download\.microsoft\.com/.{1,500}?\.json)["\']'
             match = re.search(pattern, decoded_html)
     
             if not match:
    
  • guard/handlers/ipinfo_handler.py+17 17 modified
    @@ -4,7 +4,7 @@
     from pathlib import Path
     from typing import Any
     
    -import aiohttp
    +import httpx
     import maxminddb
     from maxminddb import Reader
     
    @@ -76,24 +76,24 @@ async def _download_database(self) -> None:
             retries = 3
             backoff = 1
     
    -        async with aiohttp.ClientSession() as session:
    +        async with httpx.AsyncClient() as session:
                 for attempt in range(retries):
                     try:
    -                    async with session.get(url) as response:
    -                        response.raise_for_status()
    -                        with open(self.db_path, "wb") as f:
    -                            f.write(await response.read())
    -
    -                        if self.redis_handler is not None:
    -                            with open(self.db_path, "rb") as f:
    -                                db_content = f.read().decode("latin-1")
    -                            await self.redis_handler.set_key(
    -                                "ipinfo",
    -                                "database",
    -                                db_content,
    -                                ttl=86400,  # 24 hours
    -                            )
    -                        return
    +                    response = await session.get(url)
    +                    response.raise_for_status()
    +                    with open(self.db_path, "wb") as f:
    +                        f.write(response.content)
    +
    +                    if self.redis_handler is not None:
    +                        with open(self.db_path, "rb") as f:
    +                            db_content = f.read().decode("latin-1")
    +                        await self.redis_handler.set_key(
    +                            "ipinfo",
    +                            "database",
    +                            db_content,
    +                            ttl=86400,  # 24 hours
    +                        )
    +                    return
                     except Exception:
                         if attempt == retries - 1:
                             raise
    
  • guard/handlers/suspatterns_handler.py+60 60 modified
    @@ -18,98 +18,98 @@ class SusPatternsManager:
         custom_patterns: set[str] = set()
     
         patterns: list[str] = [
    -        # XSS - Enhanced patterns
    -        r"<script[^>]*>[^<]*<\/script\s*>",  # Basic script tag
    -        r"javascript:\s*[^\s]+",  # javascript: protocol
    +        # XSS
    +        r"<script[^>]{0,100}>[^<]{0,1000}<\/script\s{0,10}>",  # Basic script tag
    +        r"javascript:\s{0,10}[^\s]{1,200}",  # javascript: protocol
             # Event handlers
             r"(?:on(?:error|load|click|mouseover|submit|mouse|unload|change|focus|"
    -        r"blur|drag))=[\"\']?[^\"\'>\s]+",
    +        r"blur|drag))=(?:[\"'][^\"']{1,100}[\"']|[^\s>]{1,100})",
             # Malicious attributes
    -        r"(?:<[^>]*\s+(?:href|src|data|action)\s*=[\s\"\']*(?:javascript|"
    +        r"(?:<[^>]{1,200}\s{1,20}(?:href|src|data|action)\s{0,10}=[\s\"\']{0,3}(?:javascript|"
             r"vbscript|data):)",
             # CSS expressions
    -        r"(?:<[^>]*\s+style\s*=[\s\"\']*[^>]*(?:expression|behavior|url)\s*\("
    -        r"[^)]*\))",
    -        r"(?:<object[^>]*>[\s\S]*?<\/object\s*>)",  # Suspicious objects
    -        r"(?:<embed[^>]*>[\s\S]*?<\/embed\s*>)",  # Suspicious embeds
    -        r"(?:<applet[^>]*>[\s\S]*?<\/applet\s*>)",  # Java applets
    -        # SQL Injection - Enhanced patterns
    +        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
    +        # SQL Injection
             # Basic SELECT statements
    -        r"(?i)SELECT\s+[\w\s,\*]+\s+FROM\s+[\w\s\._]+",
    +        r"(?i)SELECT\s{1,20}[\w\s,\*]{1,200}\s{1,20}FROM\s{1,20}[\w\s\._]{1,100}",
             # UNION-based queries
    -        r"(?i)UNION\s+(?:ALL\s+)?SELECT",
    +        r"(?i)UNION\s{1,20}(?:ALL\s{1,20})?SELECT",
             # Logic-based
    -        r"(?i)('\s*(?:OR|AND)\s*[\(\s]*'?[\d\w]+\s*(?:=|LIKE|<|>|<=|>=)\s*"
    -        r"[\(\s]*'?[\d\w]+)",
    -        # UNION-based (original pattern)
    -        r"(?i)(UNION\s+(?:ALL\s+)?SELECT\s+(?:NULL[,\s]*)+|\(\s*SELECT\s+"
    +        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})",
    +        # 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"(?:@@|VERSION))",
    -        r"(?i)(?:INTO\s+(?:OUTFILE|DUMPFILE)\s+'[^']+')",  # File operations
    -        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
    +        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
             # Comment-based
    -        r"(?i)(?:\/\*![0-9]*\s*(?:OR|AND|UNION|SELECT|INSERT|DELETE|DROP|"
    +        r"(?i)(?:\/\*![0-9]{0,10}\s{0,10}(?:OR|AND|UNION|SELECT|INSERT|DELETE|DROP|"
             r"CONCAT|CHAR|UPDATE)\b)",
    -        # Directory Traversal - Enhanced patterns
    -        r"(?:\.\./|\.\\/){2,}",  # Multiple traversal
    +        # Directory Traversal
    +        r"(?:\.\./|\.\\/){2,10}",  # 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*$",  # Windows files
    +        r"(?:boot\.ini|win\.ini|system\.ini|config\.sys)\s{0,10}$",  # Windows files
             r"(?:\/proc\/self\/environ$)",  # Process information
    -        r"(?:\/var\/log\/[^\/]+$)",  # Log files
    -        # Command Injection - Enhanced patterns
    +        r"(?:\/var\/log\/[^\/]{1,100}$)",  # Log files
    +        # Command Injection
             # Basic commands
    -        r";\s*(?:ls|cat|rm|chmod|chown|wget|curl|nc|netcat|ping|telnet)\s+"
    -        r"-[a-zA-Z]+\s+",
    +        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}",
             # Download commands
    -        r"\|\s*(?:wget|curl|fetch|lwp-download|lynx|links|GET)\s+",
    +        r"\|\s{0,10}(?:wget|curl|fetch|lwp-download|lynx|links|GET)\s{1,20}",
             # Command substitution
    -        r"(?:[;&|`]\s*(?:\$\([^)]+\)|\$\{[^}]+\}))",
    +        r"(?:[;&|`]\s{0,10}(?:\$\([^)]{1,100}\)|\$\{[^}]{1,100}\}))",
             # Shell execution
    -        r"(?:^|;)\s*(?:bash|sh|ksh|csh|tsch|zsh|ash)\s+-[a-zA-Z]+",
    +        r"(?:^|;)\s{0,10}(?:bash|sh|ksh|csh|tsch|zsh|ash)\s{1,20}-[a-zA-Z]{1,20}",
             # PHP functions
    -        r"\b(?:eval|system|exec|shell_exec|passthru|popen|proc_open)\s*\(",
    -        # File Inclusion - Enhanced patterns
    +        r"\b(?:eval|system|exec|shell_exec|passthru|popen|proc_open)\s{0,10}\(",
    +        # File Inclusion
             # Protocols
             r"(?:php|data|zip|rar|file|glob|expect|input|phpinfo|zlib|phar|ssh2|"
    -        r"rar|ogg|expect)://[^\s]+",
    +        r"rar|ogg|expect)://[^\s]{1,200}",
             # URLs
    -        r"(?:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(?:\/?)(?:"
    -        r"[a-zA-Z0-9\-\.\?,'/\\\+&amp;%\$#_]*)?)",
    -        # LDAP Injection - Enhanced patterns
    -        r"\(\s*[|&]\s*\(\s*[^)]+=[*]",  # Wildcards
    -        r"(?:\*(?:[\s\d\w]+\s*=|=\s*[\d\w\s]+))",  # Attribute matching
    -        r"(?:\(\s*[&|]\s*)",  # Logic operations
    -        # XML Injection - Enhanced patterns
    -        r"<!(?:ENTITY|DOCTYPE)[^>]+SYSTEM[^>]+>",  # XXE
    -        r"(?:<!\[CDATA\[.*?\]\]>)",  # CDATA sections
    -        r"(?:<\?xml.*?\?>)",  # XML declarations
    -        # SSRF - Enhanced patterns
    +        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})?)",
    +        # 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
    +        # XML Injection
    +        r"<!(?:ENTITY|DOCTYPE)[^>]{1,200}SYSTEM[^>]{1,200}>",  # XXE
    +        r"(?:<!\[CDATA\[.{0,1000}?\]\]>)",  # CDATA sections
    +        r"(?:<\?xml.{0,200}?\?>)",  # XML declarations
    +        # SSRF
             # Local addresses
    -        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 - Enhanced patterns
    +        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
    +        # NoSQL Injection
             # MongoDB
    -        r"\{\s*\$(?:where|gt|lt|ne|eq|regex|in|nin|all|size|exists|type|mod|"
    +        r"\{\s{0,10}\$(?:where|gt|lt|ne|eq|regex|in|nin|all|size|exists|type|mod|"
             r"options):",
    -        r"(?:\{\s*\$[a-zA-Z]+\s*:\s*(?:\{|\[))",  # Nested operators
    -        # File Upload - Enhanced patterns
    -        r"(?i)filename=[\"'].*?\.(?:php\d*|phar|phtml|exe|jsp|asp|aspx|sh|"
    +        r"(?:\{\s{0,10}\$[a-zA-Z]{1,20}\s{0,10}:\s{0,10}(?:\{|\[))",  # Nested operators
    +        # File Upload
    +        r"(?i)filename=[\"'].{0,200}?\.(?:php\d{0,5}|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 - Enhanced patterns
    +        # Path Traversal
             # Encoded traversal
             r"(?:%2e%2e|%252e%252e|%uff0e%uff0e|%c0%ae%c0%ae|%e0%40%ae|%c0%ae"
             r"%e0%80%ae|%25c0%25ae)/",
    -        # Template Injection - New category
    +        # Template Injection
             # Basic template injection
    -        r"\{\{\s*[^\}]*(?:system|exec|popen|eval|require|include)\s*\}\}",
    +        r"\{\{\s{0,10}[^\}]{1,200}(?:system|exec|popen|eval|require|include)\s{0,10}\}\}",
             # Alternative syntax
    -        r"\{\%\s*[^\%]*(?:system|exec|popen|eval|require|include)\s*\%\}",
    -        # HTTP Response Splitting - New category
    -        r"[\r\n]\s*(?:HTTP\/[0-9.]+|Location:|Set-Cookie:)",
    +        r"\{\%\s{0,10}[^\%]{1,200}(?:system|exec|popen|eval|require|include)\s{0,10}\%\}",
    +        # HTTP Response Splitting
    +        r"[\r\n]\s{0,10}(?:HTTP\/[0-9.]{1,10}|Location:|Set-Cookie:)",
         ]
     
         compiled_patterns: list[re.Pattern]
    
  • .mike.yml+2 1 modified
    @@ -2,6 +2,7 @@ version_selector: true
     title_switch: true
     versions_file: docs/versions/versions.json
     versions:
    +  - 3.0.1
       - 3.0.0
       - 2.1.3
       - 2.1.2
    @@ -22,4 +23,4 @@ versions:
       - 0.3.2
       - latest
     aliases:
    -  latest: 3.0.0
    \ No newline at end of file
    +  latest: 3.0.1
    \ No newline at end of file
    
  • pyproject.toml+2 7 modified
    @@ -1,6 +1,6 @@
     [project]
     name = "fastapi_guard"
    -version = "3.0.0"
    +version = "3.0.1"
     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"}
    @@ -22,9 +22,9 @@ classifiers = [
         "Programming Language :: Python :: 3.13",
     ]
     dependencies = [
    -    "aiohttp",
         "cachetools",
         "fastapi",
    +    "httpx",
         "ipaddress",
         "maxminddb",
         "redis",
    @@ -35,7 +35,6 @@ dependencies = [
     [project.optional-dependencies]
     dev = [
         "black",
    -    "httpx",
         "matplotlib",
         "mkdocs",
         "mkdocstrings",
    @@ -107,10 +106,6 @@ warn_unreachable = true
     module = "pydantic.*"
     follow_imports = "skip"
     
    -[[tool.mypy.overrides]]
    -module = "aiohttp.*"
    -follow_imports = "skip"
    -
     [[tool.mypy.overrides]]
     module = "redis.*"
     follow_imports = "skip"
    
  • README.md+0 1 modified
    @@ -622,7 +622,6 @@ Acknowledgements
     
     - [FastAPI](https://fastapi.tiangolo.com/)
     - [IPInfo](https://ipinfo.io/)
    -- [aiohttp](https://docs.aiohttp.org/)
     - [cachetools](https://cachetools.readthedocs.io/)
     - [requests](https://docs.python-requests.org/)
     - [Redis](https://redis.io/)
    
  • tests/test_ipinfo/test_ipinfo.py+12 17 modified
    @@ -16,12 +16,10 @@ async def test_ipinfo_db(tmp_path: Path) -> None:
     
         mock_response = Mock()
         mock_response.raise_for_status = Mock()
    -    mock_response.__aenter__ = AsyncMock(return_value=mock_response)
    -    mock_response.__aexit__ = AsyncMock()
    -    mock_response.read = AsyncMock()
    +    mock_response.content = b"test data"
     
         with (
    -        patch("aiohttp.ClientSession.get", return_value=mock_response),
    +        patch("httpx.AsyncClient.get", return_value=mock_response),
             patch("maxminddb.open_database"),
             patch("builtins.open", Mock()),
             patch("os.makedirs"),
    @@ -41,7 +39,7 @@ def test_ipinfo_missing_token() -> None:
     async def test_ipinfo_download_failure(tmp_path: Path) -> None:
         db = IPInfoManager(token="test", db_path=tmp_path / "test.mmdb")
         with (
    -        patch("aiohttp.ClientSession.get", side_effect=Exception("Download failed")),
    +        patch("httpx.AsyncClient.get", side_effect=Exception("Download failed")),
             patch.object(IPInfoManager, "_is_db_outdated", return_value=True),
         ):
             await db.initialize()
    @@ -53,7 +51,7 @@ async def test_ipinfo_download_failure(tmp_path: Path) -> None:
     async def test_db_initialization_retry(tmp_path: Path) -> None:
         db = IPInfoManager(token="test", db_path=tmp_path / "test.mmdb")
         with (
    -        patch("aiohttp.ClientSession.get", side_effect=Exception("First fail")),
    +        patch("httpx.AsyncClient.get", side_effect=Exception("First fail")),
             patch("asyncio.sleep") as mock_sleep,
             patch("builtins.open", Mock()),
         ):
    @@ -68,14 +66,12 @@ async def test_database_retry_success(tmp_path: Path) -> None:
         db = IPInfoManager(token="test", db_path=tmp_path / "test.mmdb")
         mock_response = Mock()
         mock_response.raise_for_status = Mock()
    -    mock_response.__aenter__ = AsyncMock(return_value=mock_response)
    -    mock_response.__aexit__ = AsyncMock()
    -    mock_response.read = AsyncMock(return_value=b"test data")
    +    mock_response.content = b"test data"
     
         # Use a closure to track the number of calls
         call_count = 0
     
    -    def side_effect_function(*args: Any, **kwargs: Any) -> AsyncMock:
    +    async def side_effect_function(*args: Any, **kwargs: Any) -> Mock:
             nonlocal call_count
             call_count += 1
             if call_count == 1:
    @@ -89,7 +85,7 @@ def side_effect_function(*args: Any, **kwargs: Any) -> AsyncMock:
         mock_open = Mock(return_value=mock_file_context)
     
         with (
    -        patch("aiohttp.ClientSession.get", side_effect=side_effect_function),
    +        patch("httpx.AsyncClient.get", side_effect=side_effect_function),
             patch("builtins.open", mock_open),
             patch("os.makedirs"),
             patch("asyncio.sleep") as mock_sleep,
    @@ -194,7 +190,7 @@ async def test_corrupted_db_removal(tmp_path: Path) -> None:
         db.db_path.touch()
     
         with (
    -        patch("aiohttp.ClientSession.get", side_effect=Exception("Download failed")),
    +        patch("httpx.AsyncClient.get", side_effect=Exception("Download failed")),
             patch.object(IPInfoManager, "_is_db_outdated", return_value=True),
         ):
             await db.initialize()
    @@ -207,7 +203,7 @@ async def test_download_exhausts_retries(tmp_path: Path) -> None:
         db = IPInfoManager(token="test", db_path=tmp_path / "test.mmdb")
     
         with (
    -        patch("aiohttp.ClientSession.get", side_effect=Exception("Download failed")),
    +        patch("httpx.AsyncClient.get", side_effect=Exception("Download failed")),
             patch("asyncio.sleep"),
         ):
             with pytest.raises(Exception, match="Download failed"):
    @@ -256,13 +252,12 @@ async def test_redis_cache_update(tmp_path: Path) -> None:
         db = IPInfoManager(token="test", db_path=tmp_path / "test.mmdb")
         db.redis_handler = AsyncMock()
     
    -    mock_response = AsyncMock()
    -    mock_response.__aenter__.return_value = mock_response
    +    mock_response = Mock()
         mock_response.raise_for_status = Mock()
    -    mock_response.read.return_value = b"new_db_data"
    +    mock_response.content = b"new_db_data"
     
         with (
    -        patch("aiohttp.ClientSession.get", return_value=mock_response),
    +        patch("httpx.AsyncClient.get", return_value=mock_response),
             patch("maxminddb.open_database"),
         ):
             await db._download_database()
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.