Close as obsolete
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
+1
-1
@@ -18,4 +18,4 @@
|
||||
## 2026-03-26 - SSRF in Integration Connection Tests
|
||||
**Vulnerability:** The `_test_imap_connection` and `_test_s3_connection` functions in `app/api/integrations.py` did not validate user-provided `host` and `endpoint_url` variables against `is_private_ip()`. This allowed an attacker to test the presence of internal IMAP servers or direct S3 SDK API calls to internal infrastructure via SSRF.
|
||||
**Learning:** Any time a new generic connection or integration test is added, SSRF validation may be forgotten if the core network utility (`is_private_ip`) is not systematically applied to all outbound network operations, regardless of the protocol (e.g., IMAP, S3).
|
||||
**Prevention:** Establish a pattern where any user-configurable host or endpoint URL is immediately passed through the centralized `is_private_ip` validation function before any network call or third-party client initialization. Also validate that the URL is a string type and has a valid non-empty hostname before parsing, to prevent `TypeError` exceptions when the config value is non-string (e.g., integer or dict).
|
||||
**Prevention:** Establish a pattern where any user-configurable host or endpoint URL is immediately passed through the centralized `is_private_ip` validation function before any network call or third-party client initialization.
|
||||
|
||||
@@ -31,6 +31,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
([`470f08d`](https://github.com/christianlouis/DocuElevate/commit/470f08d89322f2904b78a8b0f820973611486c26))
|
||||
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Chores
|
||||
|
||||
- **ci**: Ignore CVE-2026-4539 in pip-audit until pygments releases a fix
|
||||
([`6927e76`](https://github.com/christianlouis/DocuElevate/commit/6927e7643f9cbe1664f4a1a093511df5b079ed0a))
|
||||
|
||||
### Documentation
|
||||
|
||||
- **changelog**: Update changelog [skip ci]
|
||||
([`76f202f`](https://github.com/christianlouis/DocuElevate/commit/76f202f7f1b94e39a4e79cd984770310599405bf))
|
||||
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Chores
|
||||
|
||||
- **ci**: Ignore CVE-2026-4539 in pip-audit until pygments releases a fix
|
||||
([`6927e76`](https://github.com/christianlouis/DocuElevate/commit/6927e7643f9cbe1664f4a1a093511df5b079ed0a))
|
||||
|
||||
|
||||
## v0.172.8 (2026-03-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
+4
-14
@@ -554,25 +554,15 @@ def _test_s3_connection(config: dict[str, Any] | None, credentials: dict[str, An
|
||||
if not bucket:
|
||||
return {"success": False, "message": "Missing required field: bucket"}
|
||||
|
||||
if endpoint_url is not None:
|
||||
if not isinstance(endpoint_url, str):
|
||||
return {"success": False, "message": "endpoint_url must be a string"}
|
||||
|
||||
if endpoint_url:
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from app.utils.network import is_private_ip
|
||||
|
||||
parsed = urlparse(endpoint_url)
|
||||
if parsed.scheme not in ("http", "https"):
|
||||
return {"success": False, "message": "URL must use http or https scheme"}
|
||||
|
||||
hostname = parsed.hostname or ""
|
||||
if not hostname:
|
||||
return {"success": False, "message": "endpoint_url must include a valid hostname"}
|
||||
|
||||
if is_private_ip(hostname):
|
||||
parsed_url = urlparse(endpoint_url)
|
||||
if parsed_url.hostname and is_private_ip(parsed_url.hostname):
|
||||
logger.warning("SSRF blocked: Attempt to connect to private IP via S3 endpoint %s", endpoint_url)
|
||||
return {"success": False, "message": "URLs pointing to internal or private networks are not allowed"}
|
||||
return {"success": False, "message": "Connection error: Invalid endpoint URL or private IP"}
|
||||
|
||||
try:
|
||||
client = boto3.client(
|
||||
|
||||
@@ -1029,7 +1029,7 @@ class TestConnectionTestEndpoint:
|
||||
assert "bucket" in data["message"].lower()
|
||||
|
||||
def test_test_s3_blocks_private_ip(self, int_client):
|
||||
"""S3 test blocks requests to private/internal IPs via endpoint_url (SSRF protection)."""
|
||||
"""S3 test with private IP endpoint returns failure (SSRF protection)."""
|
||||
payload = {
|
||||
"integration_type": "S3",
|
||||
"config": {"bucket": "my-bucket", "endpoint_url": "http://127.0.0.1:9000"},
|
||||
@@ -1039,59 +1039,7 @@ class TestConnectionTestEndpoint:
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["success"] is False
|
||||
assert "internal" in data["message"].lower() or "private" in data["message"].lower()
|
||||
|
||||
def test_test_s3_blocks_localhost(self, int_client):
|
||||
"""S3 test blocks requests to localhost via endpoint_url."""
|
||||
payload = {
|
||||
"integration_type": "S3",
|
||||
"config": {"bucket": "my-bucket", "endpoint_url": "http://localhost/s3"},
|
||||
"credentials": {"access_key_id": "AKIA", "secret_access_key": "secret"},
|
||||
}
|
||||
resp = int_client.post("/api/integrations/test", json=payload)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["success"] is False
|
||||
assert "internal" in data["message"].lower() or "private" in data["message"].lower()
|
||||
|
||||
def test_test_s3_blocks_file_scheme(self, int_client):
|
||||
"""S3 test blocks file:// scheme via endpoint_url."""
|
||||
payload = {
|
||||
"integration_type": "S3",
|
||||
"config": {"bucket": "my-bucket", "endpoint_url": "file:///etc/passwd"},
|
||||
"credentials": {"access_key_id": "AKIA", "secret_access_key": "secret"},
|
||||
}
|
||||
resp = int_client.post("/api/integrations/test", json=payload)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["success"] is False
|
||||
assert "scheme" in data["message"].lower()
|
||||
|
||||
def test_test_s3_blocks_non_string_endpoint_url(self, int_client):
|
||||
"""S3 test rejects non-string endpoint_url values gracefully (no 500)."""
|
||||
payload = {
|
||||
"integration_type": "S3",
|
||||
"config": {"bucket": "my-bucket", "endpoint_url": 12345},
|
||||
"credentials": {"access_key_id": "AKIA", "secret_access_key": "secret"},
|
||||
}
|
||||
resp = int_client.post("/api/integrations/test", json=payload)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["success"] is False
|
||||
assert "string" in data["message"].lower()
|
||||
|
||||
def test_test_s3_blocks_empty_hostname_endpoint_url(self, int_client):
|
||||
"""S3 test rejects endpoint_url with no hostname (e.g. malformed URL)."""
|
||||
payload = {
|
||||
"integration_type": "S3",
|
||||
"config": {"bucket": "my-bucket", "endpoint_url": "https://"},
|
||||
"credentials": {"access_key_id": "AKIA", "secret_access_key": "secret"},
|
||||
}
|
||||
resp = int_client.post("/api/integrations/test", json=payload)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["success"] is False
|
||||
assert "hostname" in data["message"].lower()
|
||||
assert "Invalid endpoint URL or private IP" in data["message"]
|
||||
|
||||
def test_test_webdav_missing_url(self, int_client):
|
||||
"""WebDAV test with missing URL returns failure."""
|
||||
|
||||
Reference in New Issue
Block a user