Skip to content

fix(api): block private/loopback URLs in scrape targets, alerts, ClickHouse + cross-tenant label override - #32

Merged
Makisuo merged 2 commits into
mainfrom
security/ssrf-and-cross-tenant
May 8, 2026
Merged

Makisuo merged 2 commits into
mainfrom
security/ssrf-and-cross-tenant

Conversation

@Makisuo

@Makisuo Makisuo commented May 8, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Closes the 3 CRITICAL ssrf findings and the bulk of the 13 HIGH ssrf cluster, plus 2 HIGH cross-tenant-id.

  • New apps/api/src/lib/url-validator.ts: validateExternalUrlSync, Effect wrapper, and safeFetch. Rejects non-http(s) schemes plus loopback / RFC1918 / link-local / cloud-metadata / unique-local / IPv4-mapped IPv6 ranges. safeFetch issues with redirect: \"manual\" and re-validates each Location target (max 5 hops).
  • Wired into ScrapeTargetsService (validateUrl + probe), AlertsService (destination URL persistence), AlertDeliveryDispatch (slack/webhook/hazel/hazel-oauth fetches), NotificationDispatcher, OrgClickHouseSettingsService (URL validation + blank-password reuse only when URL/user/db unchanged).
  • Cross-tenant fix: routes/sd.http.ts now applies user labelsJson BEFORE canonical labels so maple_org_id / maple_* / __* / job cannot be overridden via service discovery. ScrapeTargetsService.validateLabelsJson also rejects reserved keys at write time.
  • Caught during verification (commit bd6d81ab): URL parsers canonicalise ::ffff:127.0.0.1 to ::ffff:7f00:1 (hex form, leading zeros stripped). The original regex matched only the dotted-quad form, so an IPv4-mapped IPv6 attack URL bypassed the check. Replaced with a decoder that parses the hex form back to dotted-quad and reuses the IPv4 check.

Note on Cloudflare Workers

The API runs on Workers, where dns.lookup is unavailable. This validator is string-based: it catches naive literal-IP and known-hostname SSRF and is paired with validate-on-write so stored URLs cannot point at internal targets in the first place. DNS-rebinding is a known limitation; the customer-side collector that consumes service discovery is the bigger residual risk and should be addressed via egress network policy.

Test plan

  • 35 unit tests in url-validator.test.ts (scheme allowlist, IPv4/IPv6 private ranges, IPv4-mapped IPv6 in both dotted-quad and hex form, redirect re-validation).
  • 18 existing AlertsService tests still pass.
  • 26-case attack matrix all blocked, 4-case public URL set all accepted.
  • bun turbo typecheck passes across all 18 packages.

🤖 Generated with Claude Code


View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

Makisuo and others added 2 commits May 8, 2026 22:59
…ations, ClickHouse settings

Addresses DeepSec CRITICAL `ssrf` and HIGH `ssrf` / `cross-tenant-id`
findings. Authenticated tenants previously had several paths to make
the Maple worker (or the customer-side collector) issue requests at
attacker-chosen internal hosts and metadata endpoints, and to poison
another tenant's telemetry by overriding the `maple_org_id` label.

Changes:

- `apps/api/src/lib/url-validator.ts` (new): `validateExternalUrlSync`,
  Effect wrapper, and `safeFetch`. Rejects non-http(s) schemes, plus
  loopback / RFC1918 / link-local / cloud-metadata / unique-local /
  IPv4-mapped IPv6 ranges by hostname/IP literal. `safeFetch` issues
  with `redirect: "manual"` and re-validates each Location target
  (max 5 hops). String-based by design — the API runs on Cloudflare
  Workers where `dns.lookup` is not available; this catches naive
  literal-IP and known-hostname SSRF and is paired with
  validate-on-write so stored URLs cannot point at internal targets
  in the first place.
- `ScrapeTargetsService.validateUrl` now delegates to the shared
  validator; `probe` uses `safeFetch` instead of `fetch(... redirect:
  "follow")`. `validateLabelsJson` now rejects reserved label keys
  (`maple_*`, `__*`, `job`, `instance`).
- `routes/sd.http.ts` applies user-supplied `labelsJson` BEFORE
  canonical system labels, so `maple_org_id` (and other `maple_*` /
  `__*` / `job` keys) cannot be overridden at the discovery layer
  even if a stale row pre-dates the new write-time validation.
- `AlertsService.createDestination` and `updateDestination` now call
  `validateDestinationUrl` for slack / webhook / hazel URLs before
  persisting them.
- `AlertDeliveryDispatch` swaps the four user-controlled-URL fetches
  (slack, webhook, hazel, hazel-oauth) for `safeFetch`. PagerDuty
  uses a hardcoded URL and is unchanged.
- `OrgClickHouseSettingsService.normalizeHttpUrl` now blocks
  private/loopback hosts. The blank-password reuse path now requires
  the URL, user, and database to match the stored row before reusing
  the encrypted password — otherwise an admin could redirect the
  stored credential at a different host (`secrets-exposure` HIGH).

Tests: `apps/api/src/lib/url-validator.test.ts` (30 cases covering
scheme allowlist, IPv4/IPv6 private ranges, blocked hostnames, and
`safeFetch` redirect re-validation). Existing `AlertsService` tests
continue to pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
URL parsers canonicalise `::ffff:a.b.c.d` to the hex form `::ffff:HHHH:HHHH`
with leading zeros stripped per group (e.g. `10.0.0.1` →
`::ffff:a00:1`, `127.0.0.1` → `::ffff:7f00:1`). The original regex set
matched only the dotted-quad form, leaving canonicalised mapped
addresses unblocked.

Replace the per-range regexes with a decoder that parses
`::ffff:HHHH:HHHH` back to dotted-quad and reuses the IPv4 private
range check. Add 5 new mapped-address regression cases.

Found while exhaustively testing PR #1 against an attack matrix —
URL parser canonicalisation is something I should have anticipated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Makisuo
Makisuo merged commit fed920f into main May 8, 2026
2 of 3 checks passed

This branch was previously deployed

1 inactive deployment
pr-preview — bd6d81ab Deployed May 8, 2026 by Makisuo via deploy-pr-preview #108
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.

1 participant