Compare commits

...

22 Commits

Author SHA1 Message Date
google-labs-jules[bot] 5b41d32f90 🛡️ Sentinel: [HIGH] Fix Server-Side Request Forgery in webhooks
Adds validation to webhook URLs before attempting to deliver them to prevent
SSRF attacks targeting private IP ranges, local host, and cloud metadata endpoints.
Validates URL schema, hostname, and applies `is_private_ip()`. Also resolved ruff linting
errors. Tests have been expanded to ensure validation covers all cases.

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
2026-05-17 15:04:09 +00:00
github-actions[bot] 58b14ae769 docs(changelog): update changelog [skip ci] 2026-04-07 09:35:53 +00:00
Christian Krakau-Louis 23c5bac666 Merge pull request #838 from christianlouis/dependabot/pip/pip-licenses-5.5.5
Bump pip-licenses from 5.5.1 to 5.5.5
2026-04-07 11:35:15 +02:00
Christian Krakau-Louis d925dc5cd3 Merge pull request #839 from christianlouis/dependabot/pip/stripe-gte-7.0.0-and-lt-16.0.0
Update stripe requirement from <15.0.0,>=7.0.0 to >=7.0.0,<16.0.0
2026-04-07 11:35:04 +02:00
github-actions[bot] b8ddd2f8d2 chore(release): update build metadata files [skip ci] 2026-04-07 09:34:57 +00:00
semantic-release 301ca9d186 0.172.9
Automatically generated by python-semantic-release
2026-04-07 09:34:54 +00:00
Christian Krakau-Louis 3bd8a52ea2 Merge pull request #836 from christianlouis/sentinel/fix-s3-ssrf-vulnerability-8738702433604673313
🛡️ Sentinel: [HIGH] Fix SSRF in S3 connection test endpoint_url
2026-04-07 11:34:28 +02:00
Christian Krakau-Louis 789e8c6236 Merge pull request #840 from christianlouis/sentinel/ssrf-redirect-bypass-15997970627137004397
🛡️ Sentinel: [HIGH] Fix SSRF bypass via HTTP redirects in url_upload
2026-04-07 11:34:11 +02:00
Christian Krakau-Louis a3ea215a1c Merge branch 'main' into sentinel/ssrf-redirect-bypass-15997970627137004397 2026-04-07 11:33:59 +02:00
github-actions[bot] c6e0b80bec docs(changelog): update changelog [skip ci] 2026-04-07 09:33:42 +00:00
Christian Krakau-Louis e86e1b9f13 Merge pull request #845 from christianlouis/sentinel-ssrf-httpx-redirect-fix-3175117423198883580
🛡️ Sentinel: [HIGH] Fix SSRF bypass via httpx redirects in /process-url
2026-04-07 11:33:11 +02:00
google-labs-jules[bot] 46a9a30af0 🛡️ Sentinel: [HIGH] Fix SSRF bypass via httpx redirects
🚨 Severity: HIGH
💡 Vulnerability: The `/process-url` endpoint used `httpx.AsyncClient` with `follow_redirects=True`. While the initial user-provided URL was validated against SSRF protections (blocking private/internal IPs), the client implicitly followed subsequent HTTP redirects without validating their target locations. This allowed an attacker to bypass the initial check by supplying a valid URL that redirected to an internal IP or cloud metadata endpoint.
🎯 Impact: An attacker could potentially access internal network services or cloud metadata endpoints.
🔧 Fix: Implemented an `event_hooks` listener (`validate_redirect`) on the `httpx.AsyncClient` that intercepts responses, extracts the `Location` header, resolves the absolute target URL, and applies the same `validate_url_safety` check before allowing the redirect to be followed.
 Verification: Ran `pytest tests/test_url_upload.py`, formatting checks via `ruff format` and linting via `ruff check`.

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
2026-04-06 02:55:58 +00:00
google-labs-jules[bot] bdfa3ba1e0 style: sort imports in test_url_upload.py
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
2026-03-30 03:31:23 +00:00
github-actions[bot] 8295279ec9 style: apply ruff auto-fix
- Auto-formatted code with ruff format
- Applied ruff linting fixes with --fix

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-30 03:24:20 +00:00
google-labs-jules[bot] 152ee15b06 test: add coverage for url_upload redirect SSRF bypass prevention hook
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
2026-03-30 03:23:56 +00:00
google-labs-jules[bot] a75e8b9297 🛡️ Sentinel: [HIGH] Fix SSRF bypass via HTTP redirects in url_upload
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
2026-03-30 03:02:05 +00:00
dependabot[bot] ee664f83fb Update stripe requirement from <15.0.0,>=7.0.0 to >=7.0.0,<16.0.0
Updates the requirements on [stripe](https://github.com/stripe/stripe-python) to permit the latest version.
- [Release notes](https://github.com/stripe/stripe-python/releases)
- [Changelog](https://github.com/stripe/stripe-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stripe/stripe-python/compare/v7.0.0...v15.0.0)

---
updated-dependencies:
- dependency-name: stripe
  dependency-version: 15.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-29 23:04:45 +00:00
dependabot[bot] 91ef089aa7 Bump pip-licenses from 5.5.1 to 5.5.5
Bumps [pip-licenses](https://github.com/raimon49/pip-licenses) from 5.5.1 to 5.5.5.
- [Release notes](https://github.com/raimon49/pip-licenses/releases)
- [Changelog](https://github.com/raimon49/pip-licenses/blob/master/CHANGELOG.md)
- [Commits](https://github.com/raimon49/pip-licenses/compare/v-5.5.1...v-5.5.5)

---
updated-dependencies:
- dependency-name: pip-licenses
  dependency-version: 5.5.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-29 23:04:35 +00:00
google-labs-jules[bot] 925864ddca Close as obsolete
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
2026-03-27 14:51:28 +00:00
copilot-swe-agent[bot] 57db4c7c82 fix(api): resolve merge conflicts, add type safety for endpoint_url in S3 connection test
Agent-Logs-Url: https://github.com/christianlouis/DocuElevate/sessions/8a9f717e-a6cb-45f0-8f2a-0e5d1d404657

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
2026-03-27 14:50:51 +00:00
copilot-swe-agent[bot] 35752c9092 fix(api): resolve merge conflicts, add type safety for endpoint_url in S3 connection test
- Resolve merge conflicts with main (PR #834 also fixed S3 SSRF)
- Add isinstance(endpoint_url, str) type check before urlparse to prevent TypeError on non-string values
- Reject endpoint_url with empty/missing hostname after parsing (malformed URLs like 'https://')
- Keep scheme validation (http/https only) and private IP blocking via is_private_ip()
- Add logger.warning for SSRF block events
- Add regression tests: non-string endpoint_url and empty hostname cases
- Update sentinel.md with consolidated SSRF entry

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
2026-03-27 14:48:52 +00:00
google-labs-jules[bot] c547ad1acc 🛡️ Sentinel: [HIGH] Fix SSRF vulnerability in S3 connection test endpoint_url
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
2026-03-27 04:18:28 +00:00
14 changed files with 292 additions and 34 deletions
+4 -21
View File
@@ -1,21 +1,4 @@
## 2024-05-24 - SSRF in WebDAV connection test
**Vulnerability:** The `_test_webdav_connection` function had a custom SSRF check that failed to resolve DNS names, allowing attackers to bypass the check by providing a domain that resolves to an internal IP (e.g., `127.0.0.1`).
**Learning:** DNS resolution is required for robust SSRF protection when validating URLs provided by users.
**Prevention:** Use a centralized `is_private_ip` function (now in `app/utils/network.py`) that resolves the hostname to its IPs and checks if any are private.
## 2026-03-22 - B310: urllib.request.urlopen replaced with httpx
**Vulnerability:** The `_test_webdav_connection` function used `urllib.request.urlopen`, which natively supports dangerous schemes like `file://` or `ftp://` and follows redirects by default, potentially allowing SSRF bypasses or Local File Inclusion.
**Learning:** `urllib.request` should be avoided for user-supplied URLs. Even when URL schemes are manually validated, `urllib`'s default redirect following behavior can bypass SSRF protections (e.g. redirecting to `127.0.0.1`).
**Prevention:** Use a modern, safer HTTP client like `httpx` with `follow_redirects=False` when testing user-provided URLs.
## 2026-03-20 - Safe Path Traversal Prevention in Low-Level Utilities
**Vulnerability:** The generic file utility `hash_file` in `app/utils/file_operations.py` accepted any file path and was vulnerable to reading arbitrary files via path traversal (e.g., `../../../etc/passwd`) or absolute paths if an attacker could control the `filepath` argument.
**Learning:** Naively checking for `".." in path` breaks legitimate relative paths used internally by the application. Blocking absolute paths entirely also breaks functionality. Input validation should occur at the API boundary, but for defense-in-depth, low-level utilities must enforce expected boundaries (e.g., the application's `workdir`).
**Prevention:** Use `pathlib.Path.resolve()` on both the target path and the allowed base directory (`settings.workdir`). Ensure the resolved target path is strictly within the allowed boundary using `filepath_obj.relative_to(workdir_obj)`, catching the `ValueError` that is raised when the path is out of bounds. This safely blocks both relative traversal attacks and arbitrary absolute paths.
## 2025-05-18 - [SSRF Bypass via DNS Resolution Failure]
**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.
## 2025-02-14 - SSRF vulnerability in webhook outgoing requests
**Vulnerability:** Found an SSRF vulnerability where outgoing webhook requests could hit private IPs or metadata endpoints (e.g. 169.254.169.254).
**Learning:** This existed because the `url` parameter provided for webhooks (`app/utils/webhook.py` and `app/utils/user_notification.py`) was not being checked before being passed to `requests.post()` or `httpx.post()`.
**Prevention:** Make sure to always validate URL scheme and hostname with `is_private_ip()` and block known cloud metadata endpoints before doing outgoing network requests based on dynamic values.
+1 -1
View File
@@ -1 +1 @@
2026-03-25T07:54:28Z
2026-04-07T09:34:53Z
+70
View File
@@ -12,6 +12,76 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
## v0.172.9 (2026-04-07)
### Bug Fixes
- **api**: Resolve merge conflicts, add type safety for endpoint_url in S3 connection test
([`57db4c7`](https://github.com/christianlouis/DocuElevate/commit/57db4c7c82f4a8df2e7e5e5505e1d5c01768fc16))
### Chores
- **ci**: Ignore CVE-2026-4539 in pip-audit until pygments releases a fix
([`6927e76`](https://github.com/christianlouis/DocuElevate/commit/6927e7643f9cbe1664f4a1a093511df5b079ed0a))
### Code Style
- Apply ruff auto-fix
([`8295279`](https://github.com/christianlouis/DocuElevate/commit/8295279ec93570da4eb0445ede8084d1eb2aba99))
- Sort imports in test_url_upload.py
([`bdfa3ba`](https://github.com/christianlouis/DocuElevate/commit/bdfa3ba1e0a5702414e3b449fbde6a6d3149557a))
### Documentation
- **changelog**: Update changelog [skip ci]
([`c6e0b80`](https://github.com/christianlouis/DocuElevate/commit/c6e0b80becab81a75aea4ee78f5aaf8b6ac54854))
- **changelog**: Update changelog [skip ci]
([`9b9882c`](https://github.com/christianlouis/DocuElevate/commit/9b9882c4d62691d0ddd20444e3b77bfe6eecc8c3))
- **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 coverage for url_upload redirect SSRF bypass prevention hook
([`152ee15`](https://github.com/christianlouis/DocuElevate/commit/152ee15b06ebf7beb6216423b4c8d93ec2243165))
- Add tests for SSRF validation in integrations
([`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]
([`9b9882c`](https://github.com/christianlouis/DocuElevate/commit/9b9882c4d62691d0ddd20444e3b77bfe6eecc8c3))
- **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
- **ci**: Ignore CVE-2026-4539 in pip-audit until pygments releases a fix
+1 -1
View File
@@ -1 +1 @@
12a35f9
3bd8a52
+6 -6
View File
@@ -1,10 +1,10 @@
DocuElevate Build Information
==============================
Version: 0.172.8
Build Date: 2026-03-25T07:54:28Z
Git Commit: 12a35f9b301a5a4430265f32f6552bd131e0d5e2
Git Short SHA: 12a35f9
Version: 0.172.9
Build Date: 2026-04-07T09:34:53Z
Git Commit: 3bd8a52ea201b33d6071c9b3a7fdace582e65fd5
Git Short SHA: 3bd8a52
Git Branch: main
Commit Date: 2026-03-25T08:54:06+01:00
Build Timestamp: 2026-03-25T07:54:28Z
Commit Date: 2026-04-07T11:34:28+02:00
Build Timestamp: 2026-04-07T09:34:53Z
==============================
+1 -1
View File
@@ -1 +1 @@
0.172.8
0.172.9
+20
View File
@@ -106,6 +106,25 @@ def validate_file_type(content_type: str, filename: str) -> bool:
return False
async def verify_redirect(response: httpx.Response) -> None:
"""
Event hook to intercept redirects and validate the new destination URL.
Prevents SSRF bypasses via redirects to internal networks or metadata endpoints.
"""
if response.status_code in (301, 302, 303, 307, 308):
location = response.headers.get("Location")
if location:
# Resolve relative redirects
new_url = str(response.url.join(location))
# Validate the new URL
try:
validate_url_safety(new_url)
except HTTPException as e:
# Map the validation error to an httpx exception so it can be handled
# properly by the caller, avoiding raw HTTPExceptions escaping the client scope
raise httpx.RequestError(f"Redirect to unsafe URL blocked: {e.detail}", request=response.request) from e
@router.post("/process-url")
@require_login
async def process_url(
@@ -165,6 +184,7 @@ async def process_url(
headers={
"User-Agent": "DocuElevate/1.0", # Identify ourselves
},
event_hooks={"response": [verify_redirect]},
) as client:
async with client.stream("GET", url) as response:
response.raise_for_status()
+26
View File
@@ -12,6 +12,7 @@ import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import Any
from urllib.parse import urlparse
import httpx
@@ -128,6 +129,31 @@ def _send_webhook_notification(target_config: dict[str, Any], event_type: str, t
logger.warning("Webhook notification target missing url")
return False
from app.utils.network import is_private_ip
parsed_url = urlparse(url)
if parsed_url.scheme not in ("http", "https"):
logger.warning("Webhook notification to %s blocked: Invalid scheme %s", url, parsed_url.scheme)
return False
hostname = parsed_url.hostname
if not hostname:
logger.warning("Webhook notification to %s blocked: No hostname", url)
return False
if is_private_ip(hostname):
logger.warning("Webhook notification to %s blocked: Private IP", url)
return False
metadata_endpoints = [
"169.254.169.254", # AWS, Azure, GCP metadata
"metadata.google.internal", # GCP
"169.254.169.253", # AWS link-local
]
if hostname in metadata_endpoints:
logger.warning("Webhook notification to %s blocked: Metadata endpoint", url)
return False
payload = {
"event": event_type,
"title": title,
+26
View File
@@ -18,6 +18,7 @@ import json
import logging
import time
from typing import Any
from urllib.parse import urlparse
import requests
@@ -67,6 +68,31 @@ def deliver_webhook(url: str, payload: dict[str, Any], secret: str | None = None
Returns:
``True`` when the remote server responds with a 2xx status.
"""
from app.utils.network import is_private_ip
parsed_url = urlparse(url)
if parsed_url.scheme not in ("http", "https"):
logger.warning("Webhook to %s blocked: Invalid scheme %s", url, parsed_url.scheme)
return False
hostname = parsed_url.hostname
if not hostname:
logger.warning("Webhook to %s blocked: No hostname", url)
return False
if is_private_ip(hostname):
logger.warning("Webhook to %s blocked: Private IP", url)
return False
metadata_endpoints = [
"169.254.169.254", # AWS, Azure, GCP metadata
"metadata.google.internal", # GCP
"169.254.169.253", # AWS link-local
]
if hostname in metadata_endpoints:
logger.warning("Webhook to %s blocked: Metadata endpoint", url)
return False
body = json.dumps(payload, default=str, sort_keys=True)
body_bytes = body.encode("utf-8")
+1 -1
View File
@@ -34,7 +34,7 @@ pip-audit>=2.7.0 # Dependency vulnerability scanning against OSV/PyPA advisory
pre-commit>=3.6.0
# License compliance
pip-licenses==5.5.1 # For license compliance checking
pip-licenses==5.5.5 # For license compliance checking
# Release automation
python-semantic-release>=9.0.0
+1 -1
View File
@@ -51,7 +51,7 @@ pytesseract>=0.3.10 # Python wrapper for Tesseract OCR
pdf2image>=1.17.0 # Convert PDF pages to images (used by Tesseract and EasyOCR providers)
ocrmypdf>=16.0.0,<18.0.0 # Post-processing: embeds searchable text layers into PDFs via Tesseract
meilisearch>=0.31.0 # Full-text search engine client
stripe>=7.0.0,<15.0.0 # Stripe billing SDK (MIT license)
stripe>=7.0.0,<16.0.0 # Stripe billing SDK (MIT license)
# Error and performance monitoring
sentry-sdk[fastapi,celery,sqlalchemy]>=2.20.0,<3.0.0
+71
View File
@@ -879,3 +879,74 @@ class TestURLUploadCoverageGaps:
# Generic exception (not HTTPException/OSError/RequestException) is caught and returns 500
assert response.status_code == 500
assert "Unexpected error" in response.json()["detail"]
@pytest.mark.asyncio
async def test_verify_redirect_allows_safe_url(self):
"""Test verify_redirect allows safe redirects (lines 115, 118, 120-121)"""
import httpx
from app.api.url_upload import verify_redirect
req = httpx.Request("GET", "http://example.com")
resp = httpx.Response(301, headers={"Location": "https://google.com"}, request=req)
# Should not raise any exception
await verify_redirect(resp)
@pytest.mark.asyncio
@patch("app.api.url_upload.validate_url_safety")
async def test_verify_redirect_blocks_unsafe_url(self, mock_validate):
"""Test verify_redirect blocks unsafe redirects (lines 122-125)"""
import httpx
from fastapi import HTTPException
from app.api.url_upload import verify_redirect
mock_validate.side_effect = HTTPException(status_code=400, detail="Unsafe URL")
req = httpx.Request("GET", "http://example.com")
resp = httpx.Response(301, headers={"Location": "http://127.0.0.1"}, request=req)
with pytest.raises(httpx.RequestError) as exc_info:
await verify_redirect(resp)
assert "Redirect to unsafe URL blocked" in str(exc_info.value)
@pytest.mark.asyncio
async def test_verify_redirect_ignores_non_redirects(self):
"""Test verify_redirect ignores 200 OK responses"""
import httpx
from app.api.url_upload import verify_redirect
req = httpx.Request("GET", "http://example.com")
resp = httpx.Response(200, request=req)
# Should not raise any exception and should ignore missing Location header
await verify_redirect(resp)
@pytest.mark.asyncio
async def test_verify_redirect_coverage():
from app.api.url_upload import verify_redirect
response = MagicMock(spec=httpx.Response)
response.status_code = 301
response.headers = httpx.Headers({"Location": "ftp://example.com"})
response.url = httpx.URL("http://test.com")
response.request = MagicMock(spec=httpx.Request)
with pytest.raises(httpx.RequestError):
await verify_redirect(response)
@pytest.mark.asyncio
async def test_verify_redirect_coverage2():
from app.api.url_upload import verify_redirect
response = MagicMock(spec=httpx.Response)
response.status_code = 301
response.headers = httpx.Headers({"Location": "http://example.com"})
response.url = httpx.URL("http://test.com")
response.request = MagicMock(spec=httpx.Request)
# Should be fine
await verify_redirect(response)
+34 -2
View File
@@ -227,7 +227,8 @@ class TestSendEmailNotification:
class TestSendWebhookNotification:
"""Tests for _send_webhook_notification()."""
def test_success_with_secret_header(self):
def test_success_with_secret_header(self, mocker):
mocker.patch("app.utils.network.is_private_ip", return_value=False)
"""Webhook sent and X-DocuElevate-Secret header set when secret provided."""
from app.utils.user_notification import _send_webhook_notification
@@ -248,7 +249,8 @@ class TestSendWebhookNotification:
assert kwargs["headers"]["X-DocuElevate-Secret"] == "mysecret"
assert kwargs["json"]["event"] == "document.processed"
def test_success_without_secret(self):
def test_success_without_secret(self, mocker):
mocker.patch("app.utils.network.is_private_ip", return_value=False)
"""Webhook sent without X-DocuElevate-Secret header when no secret."""
from app.utils.user_notification import _send_webhook_notification
@@ -633,3 +635,33 @@ class TestNotifyUserDocumentHelpers:
assert "broken.pdf" in notifs[0].title
assert "Timeout" in notifs[0].message
assert notifs[0].event_type == "document.failed"
def test_webhook_coverage():
from app.utils.user_notification import _send_webhook_notification
assert _send_webhook_notification({"url": "http://169.254.169.254"}, "test", "test", "test") == False
assert _send_webhook_notification({"url": "http://localhost"}, "test", "test", "test") == False
assert _send_webhook_notification({"url": "ftp://example.com"}, "test", "test", "test") == False
assert _send_webhook_notification({"url": "http://"}, "test", "test", "test") == False
assert _send_webhook_notification({"url": "http://foo.bar.baz"}, "test", "test", "test") == False
assert _send_webhook_notification({"url": ""}, "test", "test", "test") == False
def test_webhook_coverage2(mocker):
from app.utils.user_notification import _send_webhook_notification
mocker.patch("app.utils.network.is_private_ip", return_value=False)
assert _send_webhook_notification({"url": "http://127.0.0.1"}, "test", "test", "test") == False
def test_webhook_coverage3(mocker):
mocker.patch("app.utils.network.is_private_ip", return_value=False)
from app.utils.user_notification import _send_webhook_notification
assert _send_webhook_notification({"url": "http://127.0.0.1"}, "test", "test", "test") == False
assert _send_webhook_notification({"url": "http://169.254.169.253"}, "test", "test", "test") == False
assert _send_webhook_notification({"url": "http://metadata.google.internal"}, "test", "test", "test") == False
def test_webhook_coverage5():
from app.utils.user_notification import _send_webhook_notification
assert _send_webhook_notification({}, "test", "test", "test") == False
def test_webhook_coverage4(mocker):
from app.utils.user_notification import _send_webhook_notification
mocker.patch("app.utils.network.is_private_ip", return_value=True)
assert _send_webhook_notification({"url": "http://127.0.0.1"}, "test", "test", "test") == False
+30
View File
@@ -438,6 +438,7 @@ class TestDeliverWebhookTask:
"""Tests for the Celery webhook delivery task."""
def test_success_returns_status_dict(self, mocker):
mocker.patch("app.utils.network.is_private_ip", return_value=False)
"""Task returns a dict on successful delivery."""
mocker.patch("app.tasks.webhook_tasks.deliver_webhook", return_value=True)
@@ -460,3 +461,32 @@ class TestDeliverWebhookTask:
with pytest.raises(RuntimeError, match="Webhook delivery.*failed"):
deliver_webhook_task.__wrapped__("https://example.com/hook", {"event": "test"}, None)
def test_webhook_ssrf_coverage():
from app.utils.webhook import deliver_webhook
assert deliver_webhook("ftp://example.com", {"data": 1}) == False
assert deliver_webhook("http://", {"data": 1}) == False
assert deliver_webhook("http://localhost", {"data": 1}) == False
assert deliver_webhook("http://169.254.169.254", {"data": 1}) == False
assert deliver_webhook("", {"data": 1}) == False
def test_webhook_ssrf_coverage2(mocker):
from app.utils.webhook import deliver_webhook
mocker.patch("app.utils.network.is_private_ip", return_value=False)
assert deliver_webhook("http://127.0.0.1", {"data": 1}) == False
def test_webhook_ssrf_coverage3(mocker):
mocker.patch("app.utils.network.is_private_ip", return_value=False)
from app.utils.webhook import deliver_webhook
assert deliver_webhook("http://127.0.0.1", {"data": 1}) == False
assert deliver_webhook("http://169.254.169.253", {"data": 1}) == False
assert deliver_webhook("http://metadata.google.internal", {"data": 1}) == False
def test_webhook_ssrf_coverage5():
from app.utils.webhook import deliver_webhook
assert deliver_webhook("http://example.com/test", {"data": 1}) == False
def test_webhook_ssrf_coverage4(mocker):
from app.utils.webhook import deliver_webhook
mocker.patch("app.utils.network.is_private_ip", return_value=True)
assert deliver_webhook("http://127.0.0.1", {"data": 1}) == False