From a57766ed7e2f8b8c2563d7a9724c793638591b07 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 04:00:55 +0000 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[HIGH]?= =?UTF-8?q?=20Fix=20SSRF=20in=20integrations=20connection=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds validation using `is_private_ip()` for user-provided hosts in `_test_imap_connection` and `_test_s3_connection` to prevent Server-Side Request Forgery vulnerabilities. Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- .jules/sentinel.md | 4 ++++ app/api/integrations.py | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index f3b5481a..6a6144c5 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -15,3 +15,7 @@ **Vulnerability:** The `is_private_ip` function in `app/utils/network.py` failed open (returned `False`) when a hostname could not be resolved (`socket.gaierror`). **Learning:** This fail-open pattern was originally added to allow external domains in tests, but in production, it created a severe SSRF risk. An attacker could bypass SSRF protections by providing a URL that fails to resolve during the security check but resolves later (DNS rebinding), or by exploiting internal routing behaviors via unresolvable addresses. **Prevention:** Always fail securely in network authorization functions. If a domain cannot be resolved to verify its safety, the request must be blocked (`return True` / default-deny). Tests should mock DNS resolution correctly instead of compromising production security logic. +## 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. diff --git a/app/api/integrations.py b/app/api/integrations.py index f5b11a16..99e83bc2 100644 --- a/app/api/integrations.py +++ b/app/api/integrations.py @@ -515,6 +515,12 @@ def _test_imap_connection(config: dict[str, Any] | None, credentials: dict[str, if not host or not username or not password: return {"success": False, "message": "Missing required fields: host, username, and password"} + from app.utils.network import is_private_ip + + if is_private_ip(host): + logger.warning("SSRF blocked: Attempt to connect to private IP %s", host) + return {"success": False, "message": "Connection error: Invalid hostname or IP address"} + try: if use_ssl: mail = imaplib.IMAP4_SSL(host, port) @@ -543,17 +549,28 @@ def _test_s3_connection(config: dict[str, Any] | None, credentials: dict[str, An creds = credentials or {} bucket = cfg.get("bucket", "") region = cfg.get("region", "us-east-1") + endpoint_url = cfg.get("endpoint_url") if not bucket: return {"success": False, "message": "Missing required field: bucket"} + if endpoint_url: + from urllib.parse import urlparse + + from app.utils.network import is_private_ip + + 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": "Connection error: Invalid endpoint URL or private IP"} + try: client = boto3.client( "s3", region_name=region, aws_access_key_id=creds.get("access_key_id", ""), aws_secret_access_key=creds.get("secret_access_key", ""), - endpoint_url=cfg.get("endpoint_url"), + endpoint_url=endpoint_url, ) client.head_bucket(Bucket=bucket) return {"success": True, "message": f"S3 bucket '{bucket}' is accessible"} From 470f08d89322f2904b78a8b0f820973611486c26 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 04:18:24 +0000 Subject: [PATCH 2/5] test: add tests for SSRF validation in integrations Adds missing unit tests for `_test_imap_connection` and `_test_s3_connection` to cover the new `is_private_ip()` SSRF blocking logic and satisfy Codecov checks. Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- tests/test_api_integrations.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_api_integrations.py b/tests/test_api_integrations.py index 958a1a86..561ac177 100644 --- a/tests/test_api_integrations.py +++ b/tests/test_api_integrations.py @@ -998,6 +998,23 @@ class TestConnectionTestEndpoint: assert data["success"] is False assert "Missing" in data["message"] + def test_test_imap_blocks_private_ip(self, int_client): + """IMAP test with private IP returns failure (SSRF protection).""" + payload = { + "integration_type": "IMAP", + "config": { + "host": "127.0.0.1", + "port": 993, + "username": "user", + }, + "credentials": {"password": "pass"}, + } + resp = int_client.post("/api/integrations/test", json=payload) + assert resp.status_code == 200 + data = resp.json() + assert data["success"] is False + assert "Invalid hostname or IP address" in data["message"] + def test_test_s3_missing_bucket(self, int_client): """S3 test with missing bucket returns failure.""" payload = { @@ -1011,6 +1028,19 @@ class TestConnectionTestEndpoint: assert data["success"] is False assert "bucket" in data["message"].lower() + def test_test_s3_blocks_private_ip(self, int_client): + """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"}, + "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 "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.""" payload = { From 1625896e30da50fa12b40fd8dd4334b30b48ec77 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 08:41:48 +0000 Subject: [PATCH 3/5] Bump picomatch in /frontend Bumps and [picomatch](https://github.com/micromatch/picomatch). These dependencies needed to be updated together. Updates `picomatch` from 2.3.1 to 2.3.2 - [Release notes](https://github.com/micromatch/picomatch/releases) - [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2) Updates `picomatch` from 4.0.3 to 4.0.4 - [Release notes](https://github.com/micromatch/picomatch/releases) - [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2) --- updated-dependencies: - dependency-name: picomatch dependency-version: 2.3.2 dependency-type: indirect - dependency-name: picomatch dependency-version: 4.0.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2e41cbf5..95059ed8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -536,9 +536,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -974,9 +974,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { From 69053bfb08d3e2f12a86878044667ac500888837 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 27 Mar 2026 14:21:42 +0000 Subject: [PATCH 4/5] docs(changelog): update changelog [skip ci] --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6057b04f..489ff118 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## 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 From 9b9882c4d62691d0ddd20444e3b77bfe6eecc8c3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 27 Mar 2026 14:24:59 +0000 Subject: [PATCH 5/5] docs(changelog): update changelog [skip ci] --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 489ff118..1d14d963 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## 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] + ([`69053bf`](https://github.com/christianlouis/DocuElevate/commit/69053bfb08d3e2f12a86878044667ac500888837)) + +- **changelog**: Update changelog [skip ci] + ([`76f202f`](https://github.com/christianlouis/DocuElevate/commit/76f202f7f1b94e39a4e79cd984770310599405bf)) + +### Testing + +- Add tests for SSRF validation in integrations + ([`470f08d`](https://github.com/christianlouis/DocuElevate/commit/470f08d89322f2904b78a8b0f820973611486c26)) + + ## Unreleased ### Chores