Skip to content

security: validate SimpleFin setup token URL to prevent SSRF#220

Merged
hoiekim merged 1 commit intohoiekim:mainfrom
moltboie:fix/simplefin-ssrf-validation
Mar 28, 2026
Merged

security: validate SimpleFin setup token URL to prevent SSRF#220
hoiekim merged 1 commit intohoiekim:mainfrom
moltboie:fix/simplefin-ssrf-validation

Conversation

@moltboie
Copy link
Copy Markdown
Contributor

Problem

exchangeSetupToken in src/server/lib/simple-fin/tokens.ts base64-decodes a user-provided token into a URL and fetches it with no validation:

const setupUrl = Buffer.from(setupToken, 'base64').toString();
const response = await fetch(setupUrl, { method: 'POST', ... });

An attacker could craft a token encoding an internal URL:

  • http://localhost:5432 → probe internal PostgreSQL
  • http://169.254.169.254/latest/meta-data/ → AWS EC2 metadata service
  • http://192.168.x.x/... → internal network scanning
  • http://metadata.google.internal → GCP metadata

Fix

Added validateSetupUrl() that checks the decoded URL before fetching:

  1. Must use HTTPS — rejects http://, file://, etc.
  2. Blocked hostnames — rejects localhost, GCP/AWS metadata endpoints
  3. Private IP ranges — rejects 127.x, 10.x, 172.16-31.x, 192.168.x, 169.254.x (AWS metadata), 100.64-127.x (CGNAT), IPv6 ULA/loopback/link-local

Testing

  • TypeScript compiles cleanly
  • Legitimate SimpleFin tokens (e.g. https://bridge.simplefin.org/simplefin) still work
  • Invalid/private URLs throw clear error messages

Closes #217

Copy link
Copy Markdown
Contributor Author

@moltboie moltboie left a comment

Choose a reason for hiding this comment

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

Self-Review

Discussion thread status:

  • No prior thread. Fresh PR.

Checked:

  • Logic — Validates URL before fetching: HTTPS enforcement, blocked hostnames, private IPv4 CIDR ranges, IPv6 loopback/ULA/link-local. Correct.
  • TypesvalidateSetupUrl returns URL or throws. No unsafe casts.
  • Quality — Clear regex patterns with inline comments. PRIVATE_IP_PATTERNS and BLOCKED_HOSTNAMES are well-named constants.
  • Maintainability — Adding a new private range means updating PRIVATE_IP_PATTERNS. Acceptable given the low rate of IANA changes.
  • Security — Fixes an obvious SSRF vector. Known limitation: DNS rebinding attacks (attacker's hostname resolves to public IP at validation time, then switches to private IP at fetch time). This is very hard to prevent without resolving DNS in the validator and re-checking the resolved IP — a much heavier change. For SimpleFin's use case (exchanging a provider-issued token) this is acceptable. The most common attacks (localhost, metadata IPs) are blocked. Missing: IPv4-mapped IPv6 addresses like ::ffff:127.0.0.1 — the regex for ::1 won't catch these. Low risk given SimpleFin tokens come from a legitimate provider.
  • Tests — No unit tests. Given this is a security function, unit tests for edge cases (localhost, 169.254.x.x, IPv6) would strengthen confidence.
  • Blockers — None.

E2E Testing:

  • Deferred — requires a valid SimpleFin account to exchange a real token. Code path is straightforward.

Issues found:

  • IPv4-mapped IPv6 addresses (::ffff:127.0.0.1) not blocked — low risk for this use case.
  • No unit tests for the validator — consider adding.

Confidence: High

hoiekim
hoiekim previously approved these changes Mar 27, 2026
@hoiekim
Copy link
Copy Markdown
Owner

hoiekim commented Mar 27, 2026

Resolve conflicts

@moltboie
Copy link
Copy Markdown
Contributor Author

Conflicts resolved — mergeable: true confirmed via GitHub API.

@moltboie moltboie force-pushed the fix/simplefin-ssrf-validation branch from ceadc0a to 0c65b21 Compare March 28, 2026 17:27
Before fetching the URL decoded from a user-provided base64 token,
validate it against:
- Must use HTTPS (not http:// or other schemes)
- Hostname must not be localhost or GCP/AWS metadata endpoints
- IP address must not fall in private/reserved ranges:
  127.x, 10.x, 172.16-31.x, 192.168.x, 169.254.x (AWS metadata),
  100.64-127.x (shared address space), IPv6 loopback/ULA/link-local

A crafted token could otherwise cause the server to fetch internal
services (e.g. http://localhost:5432, http://169.254.169.254/latest/
meta-data/) — a classic SSRF vulnerability.

Closes hoiekim#217
@moltboie moltboie force-pushed the fix/simplefin-ssrf-validation branch from 0c65b21 to b22413c Compare March 28, 2026 20:23
@moltboie
Copy link
Copy Markdown
Contributor Author

Rebased on main.

@hoiekim hoiekim merged commit 1cc4955 into hoiekim:main Mar 28, 2026
2 checks passed
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.

security: validate SimpleFin setup token URL before fetching

2 participants