Reject webhook/destination URLs with embedded credentials (#79)#312
Merged
icebergai-review-bot[bot] merged 1 commit intoJul 20, 2026
Merged
Conversation
A target like https://user:pass@host/hook validated at create time, but the IP-pinning rebuild in send_pinned_request replaces the whole netloc and silently dropped the user:pass@ at send time (confusing 401s). Worse, AlertDestination.target is persisted and returned by GET /api/alerts/destinations / rendered in the UI, so a URL credential was stored in plaintext and exposed. validate_webhook_url now refuses any userinfo with a clear 422 — at create/update and again at send time, for every sender kind — matching how the outbound-proxy URL already rejects userinfo. Credentials belong in an env-only mechanism (a sender secret_ref), never in the target URL. Tests: API-level 422 (user:pass@ / user@ / :pass@) with nothing stored, plus a direct validate_webhook_url unit contract. CHANGELOG + architecture rule updated. Closes #79 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CRVkWHQZ1r8hhPKDzX4nVk
Contributor
There was a problem hiding this comment.
IcebergAutoReview
Verdict: approve
The change correctly rejects credential-bearing webhook URLs through the shared validator used at create/update and send time. It is secure, scoped, documented, and adequately tested.
Findings
- No blocking findings.
Validation
- Inspected complete main...HEAD diff and affected route/sender context
- git diff --check main...HEAD passed
- Direct userinfo rejection probe passed, including percent-encoded credentials
- Targeted pytest could not start because the workspace prevents creating .venv; current-head CI is green
Residual risks / optional notes
- None identified.
Automated review by Codex 2d3d6d51c934 using IcebergAutoReview.
icebergai-review-bot
Bot
deleted the
claude/issue-79-reject-webhook-userinfo
branch
July 20, 2026 08:12
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A webhook/destination URL with userinfo —
https://user:pass@host/hook— passed validation at create time, but two things went wrong afterwards:send_pinned_request(app/webhooks.py) replaces the whole netloc with the pinned IP, so theuser:pass@was dropped and the destination returned confusing 401s (the original report).AlertDestination.targetis persisted in the DB and returned byGET /api/alerts/destinations(and rendered in the account UI), so a credential embedded in the URL was stored in the clear and handed back over the API — the "breaks secret storage" problem.Per the issue's own preferred option, this rejects userinfo rather than trying to preserve it.
validate_webhook_urlnow raises a clearWebhookValidationErrorfor any URL containing a username or password, which surfaces as a 422 at create/update and — because validation runs again insidesend_pinned_request— is also refused at send time, for every sender kind (webhook/Slack/Teams/Jira/ServiceNow). This matches how the outbound-proxy URL already rejects userinfo (validate_proxy_settings); credentials belong in an env-only mechanism (a sendersecret_ref, #37), never in the target URL.Closes #79
Test evidence
New coverage (
tests/test_alerts.py):test_create_destination_rejects_embedded_credentials— API returns 422 foruser:pass@,user@, and:pass@forms, and nothing is stored (the listing never contains the target).test_validate_webhook_url_rejects_userinfo— direct unit contract that userinfo never validates, covering the send-time path shared by all kinds.Checklist
pytest,ruff check+ruff format --check,mypy app,bandit -c pyproject.toml -r appCHANGELOG.mdupdated under the unreleased Security sectionvalidate_webhook_urlbullet in.claude/rules/architecture.mdnow records the userinfo rejectionhelp.htmlneeds no changeuv lockneededGenerated by Claude Code