Skip to content

security(backends): reject SSRF-prone backend URLs (H1)#53

Merged
lewiswigmore merged 1 commit into
mainfrom
security/backend-url-allowlist
May 29, 2026
Merged

security(backends): reject SSRF-prone backend URLs (H1)#53
lewiswigmore merged 1 commit into
mainfrom
security/backend-url-allowlist

Conversation

@lewiswigmore
Copy link
Copy Markdown
Owner

Summary

Closes H1 from the security review (issue #51).

A hostile config/backends.yaml entry could point the health monitor or cleanup client at the cloud metadata service (169.254.169.254), link-local hosts, multicast ranges, or unsupported schemes (file://, gopher://) that httpx will still happily dispatch. None of those are legitimate cleanup backends, and the metadata-service path is the most common SSRF post-exploitation step on cloud-hosted machines.

Approach

Single chokepoint: dictate/safety.py::validate_backend_url called from BackendSpec.__post_init__. Every site that obtains a spec (health pings, dashboard probe, cleanup POSTs) is now validated by construction.

Allowed

  • http / https only
  • Loopback (127.0.0.0/8, ::1, localhost)
  • RFC1918 (10/8, 172.16/12, 192.168/16) — users legitimately host Ollama on the LAN

Blocked

  • Link-local (169.254.0.0/16, fe80::/10) — catches AWS/GCP IMDS
  • Multicast (224.0.0.0/4)
  • Unspecified (0.0.0.0, ::)
  • IANA-reserved
  • CGNAT (100.64.0.0/10)
  • Hostnames ending .internal or .local (cloud service-meshes, mDNS)
  • Non-http(s) schemes

UX

health.py catches the ValueError and surfaces backend url rejected: <reason> as a per-backend status entry so the menubar shows the failure instead of silently dying inside the monitor thread.

Tests

  • 20 new unit tests in tests/test_safety.py (parametrised allow/block + a BackendSpec-level smoke test)
  • pytest: 322 passed
  • ruff check + ruff format --check on touched files: clean

A malicious config/backends.yaml entry could point the health monitor or
cleanup client at the AWS/GCP metadata service (169.254.169.254), link-local
hosts, multicast ranges, or unsupported schemes (file://, gopher://) that
httpx would still try to dispatch. None of those are legitimate cleanup
backends.

Adds dictate/safety.py::validate_backend_url and wires it into
BackendSpec.__post_init__ so every callsite (health.py, webui dashboard
probe, cleanup.py POST) is validated at the single source of truth.

Allowed: http/https; loopback; RFC1918 (Ollama on LAN is a real use case).
Blocked: link-local (169.254/16, fe80::/10), multicast (224/4), 0.0.0.0,
reserved ranges, CGNAT, .internal / .local suffixes, non-http(s) schemes.

health.py surfaces validation errors as a per-backend status entry so the
menubar shows 'backend url rejected' instead of silently dying.

20 unit tests in tests/test_safety.py covering allow/block parametrised
sets plus a BackendSpec-level test. Full suite green (322 passed).

Tracks H1 in issue #51.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 29, 2026 09:07
@sebastionai
Copy link
Copy Markdown

sebastionai Bot commented May 29, 2026

Pre-merge checks · ✅ 2 · ⚠ 0 · ❌ 0 · ⏭ 1
Check Status Reason
PR title The title clearly describes the specific security change being made.
Description The body thoroughly explains what, why and how including the threat model and testing details.
Linked issue No linked issues were provided.

@sebastionai
Copy link
Copy Markdown

sebastionai Bot commented May 29, 2026

Walkthrough

Adds SSRF protection by validating backend URLs against blocked IP ranges and schemes. A new safety module rejects link-local, multicast, unspecified, reserved and non-HTTP URLs while allowing loopback and RFC1918 addresses. Validation is enforced at BackendSpec construction so all consumers are protected. The health monitor surfaces rejection errors in its status output.

Changes

File Summary
SSRF validation dictate/safety.py, dictate/config.py Introduces validate_backend_url and calls it from BackendSpec.post_init to block dangerous URLs at construction time.
Health monitor error handling dictate/health.py Catches ValueError from backend lookup and reports a descriptive rejection status instead of crashing the monitor thread.
Tests tests/test_safety.py Adds parametrised tests covering allowed and blocked URLs plus a BackendSpec integration smoke test.

🎯 Effort: 2 (Simple) · ⏱ ~12 minutes

Generated by Sebastion AI · docs

@sebastionai
Copy link
Copy Markdown

sebastionai Bot commented May 29, 2026

🔒 Sebastion AI — security audit complete.

No exploitable findings on this diff.

Audited by Sebastion AI · docs · install on more repos

Copy link
Copy Markdown
Collaborator

@sebastiondev sebastiondev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. Closes H1. Single validation chokepoint in BackendSpec.post_init covers health.py, dashboard probe, and cleanup POSTs. Allow/block matrix is reasonable (loopback + RFC1918 yes, link-local + multicast + .internal no). 20 unit tests, full suite green.

@lewiswigmore lewiswigmore merged commit 8af79d9 into main May 29, 2026
3 of 4 checks passed
@lewiswigmore lewiswigmore deleted the security/backend-url-allowlist branch May 29, 2026 09:08
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants