Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d157667
🛡️ Sentinel: [CRITICAL] Fix stored SSRF in webhook URL endpoint
seonghobae Aug 9, 2026
5eddd98
test(security): reject non-string webhook URLs
seonghobae Aug 9, 2026
7bc59a1
test(security): exercise malformed webhook bodies at API boundary
seonghobae Aug 9, 2026
fbc77cb
🛡️ Sentinel: [CRITICAL] Fix stored SSRF and Uncaught Exception in web…
seonghobae Aug 9, 2026
e9d4946
test(scanner): reproduce stored SSRF detection gap
seonghobae Aug 9, 2026
057df0e
security(scanner): detect direct stored webhook SSRF
seonghobae Aug 9, 2026
3d6f57b
test(controlplane): pin webhook input validation boundary
seonghobae Aug 9, 2026
6d55440
fix(controlplane): reject malformed webhook payloads
seonghobae Aug 9, 2026
c9752fa
test(controlplane): prove invalid webhook input preserves state
seonghobae Aug 9, 2026
1689ba7
test(scanner): expose stored SSRF variable-flow blind spot
seonghobae Aug 9, 2026
30af47d
fix(scanner): detect unvalidated stored SSRF variable flow
seonghobae Aug 9, 2026
35578cd
test(scanner): expose SSRF finding metadata misclassification
seonghobae Aug 9, 2026
e051776
fix(scanner): classify CWE-918 findings as SSRF
seonghobae Aug 9, 2026
e65cda8
test(scanner): expose stored SSRF rule evasion paths
seonghobae Aug 9, 2026
2cd9a8f
fix(scanner): close stored SSRF rule evasion paths
seonghobae Aug 9, 2026
2ce25b2
test(scanner): expose non-enforcing SSRF guard bypass
seonghobae Aug 9, 2026
14240fa
test(scanner): pin fail-closed SSRF guard semantics
seonghobae Aug 9, 2026
48a6da5
fix(scanner): require enforcing SSRF validation guards
seonghobae Aug 9, 2026
40b5c63
docs(tests): document stored SSRF regression contracts
seonghobae Aug 9, 2026
40c521e
test(scanner): expose equivalent stored SSRF accessors
seonghobae Aug 9, 2026
999095e
fix(scanner): detect equivalent stored SSRF accessors
seonghobae Aug 9, 2026
5ba9131
docs(tests): document webhook validation helpers
seonghobae Aug 9, 2026
84a2708
security(scanner): preserve stored SSRF detection after endpoint fix
seonghobae Aug 12, 2026
c75987d
perf(scanner): prefilter stored SSRF rule by sink literal
seonghobae Aug 12, 2026
b5c95e3
fix(scanner): evaluate stored SSRF validation per sink
seonghobae Aug 12, 2026
2beb859
fix(scanner): accept none-aware fail-closed SSRF guard
seonghobae Aug 12, 2026
1013819
🎨 Palette: 개선된 파일 업로드 버튼 UX 적용 (#925)
seonghobae Aug 12, 2026
6c6a830
chore: restack stored SSRF scanner on current develop
seonghobae Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,8 @@
**Vulnerability:** DOM XSS via unescaped `severity` string interpolated into `innerHTML` in `scanner/dashboard/index.html`.
**Learning:** Even enum-like or seemingly safe meta-fields like `severity` can contain malicious payloads if sourced from user input (findings file) and directly injected into innerHTML.
**Prevention:** Always use the `esc()` sanitizer for any dynamically rendered property from `findings.json`, regardless of expected schema types.

## 2024-05-24 - [SSRF in Webhook Endpoint]
**Vulnerability:** The `/api/v1/webhook` endpoint accepted arbitrary URLs for the drift alert webhook without any server-side request forgery (SSRF) validation before saving them to the database.
**Learning:** Even if a URL is validated prior to use (e.g., in `_send_alert`), accepting and storing arbitrary URLs without validation introduces a Stored SSRF vulnerability vector and violates the principle of failing fast and securely on untrusted input.
**Prevention:** Always apply security validation functions (like `_is_safe_url`) immediately at the boundary/endpoint level prior to performing database insertion or mutation.
7 changes: 5 additions & 2 deletions appguardrail_core/controlplane.py
Original file line number Diff line number Diff line change
Expand Up @@ -631,8 +631,11 @@ def do_POST(self):
body = self._body()
if body is None:
return self._json(400, {"error": "invalid JSON body"})
set_webhook(conn, org, (body or {}).get("url"))
return self._json(200, {"webhook_url": (body or {}).get("url")})
webhook_url = (body or {}).get("url")
if webhook_url and not _is_safe_url(webhook_url):
return self._json(400, {"error": "unsafe webhook url"})
set_webhook(conn, org, webhook_url)
return self._json(200, {"webhook_url": webhook_url})
Comment thread
seonghobae marked this conversation as resolved.
Resolved

if path == "/api/v1/keys":
if not has_role(role, "owner"):
Expand Down
9 changes: 9 additions & 0 deletions tests/test_controlplane.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,15 @@ def test_api_set_webhook(server):
assert status == 200 and body["webhook_url"] == "http://hook.example/y"


def test_api_set_webhook_unsafe_url(server):
base, key = server
with pytest.raises(urllib.error.HTTPError) as e:
_req("POST", f"{base}/api/v1/webhook", key, {"url": "http://127.0.0.1/"})
assert e.value.code == 400
body = json.loads(e.value.read())
assert body["error"] == "unsafe webhook url"


def test_roles_and_key_scoping():
conn = connect(":memory:")
oid, owner_key = create_org(conn, "Acme")
Expand Down
Loading