Skip to content

fix(interface): prevent SSRF in host-side git-repo probe - #1321

Open
siundu254 wants to merge 2 commits into
usestrix:mainfrom
siundu254:fix/ssrf-git-repo-probe
Open

siundu254 wants to merge 2 commits into
usestrix:mainfrom
siundu254:fix/ssrf-git-repo-probe

Conversation

@siundu254

@siundu254 siundu254 commented Sep 16, 2026

Copy link
Copy Markdown

Summary

  • _is_http_git_repo probes a scan target URL from the Strix host itself (not the sandbox) to infer whether it's a git repo. It followed HTTP redirects by default and treated any 401 response as a positive "yes, this is a git repo" signal.
  • Since the probe runs from Strix's own infrastructure, an attacker-influenced target string (e.g. read from a poisoned file, or forwarded by an automated CI/pipeline flow) could make Strix's host issue real outbound requests to internal/private services and use the 401-as-positive-signal plus followed redirects to reach otherwise-unreachable internal resources — a classic SSRF pattern.
  • Adds _is_ssrf_safe_host, which rejects hosts that resolve (directly or via DNS) to loopback/private/link-local/reserved/multicast addresses, applied before the probe request is made.
  • Disables redirect-following on the probe (allow_redirects=False).
  • Only a genuine 200 response with the expected x-git-upload-pack-advertisement content-type now counts as confirmation; 401/403/other 4xx no longer do.

Fixes

Fixes #1132

Test plan

  • Added tests/test_ssrf_guard.py covering: rejection of loopback/private/link-local/reserved/multicast IP literals, rejection of hostnames that resolve to those ranges (mocked DNS), rejection of unresolvable hostnames, that _is_http_git_repo never issues a request for a disallowed host, that 401 no longer counts as a positive signal, that redirects are not followed, and that a genuine 200 git response is still accepted.
  • uv run ruff format --check / uv run ruff check on changed files — clean
  • uv run mypy strix/interface/utils.py — clean
  • uv run pyright strix/interface/utils.py — no new findings (diffed against main; all 82 pre-existing findings unrelated to this change)
  • uv run bandit -c pyproject.toml strix/interface/utils.py — no issues
  • uv run pytest tests/test_ssrf_guard.py tests/test_local_sources.py tests/test_api_spec_targets.py — 58 passed
  • Broader related-test sweep (pytest tests/ -k "target or ssrf or interface or local_source or api_spec") — 135 passed

_is_http_git_repo runs from the Strix host itself (not the sandbox) to
check whether a target URL looks like a git repo. It followed redirects
and treated HTTP 401 as a positive "is a git repo" signal, so an
attacker-influenced target string could make Strix's own infrastructure
probe internal/private services (a classic SSRF pattern), including via
a 401-gated redirect chain.

Add _is_ssrf_safe_host to reject targets that resolve to
loopback/private/link-local/reserved/multicast addresses before probing,
disable redirect-following on the probe request, and only treat a
genuine 200 response with the expected git content-type as a positive
signal.

Fixes usestrix#1132

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 3/5

The PR is not yet safe to merge because an attacker-controlled hostname can bypass the new SSRF guard through DNS rebinding.

Findings

  1. P1 Security DNS Rebinding Bypasses Guard
Fix with agent prompt
### Issue 1
strix/interface/utils.py:1169
The safety check resolves the hostname but keeps only a boolean result. `requests.get` then connects using the original hostname, causing a separate DNS lookup. An attacker-controlled hostname can return a public address during validation and rebind to a private or link-local address for the connection, allowing this host-side probe to reach internal infrastructure. The request must connect to a validated address while preserving the original hostname for HTTP and TLS, or enforce the address policy when connecting.

**How this was verified:** Attacker-controlled scan targets reach this function, and the validated addresses are neither returned nor pinned before the subsequent request resolves the hostname again.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

This PR hardens the host-side HTTP Git-repository probe by rejecting non-public destination addresses, disabling redirects, and requiring a successful Git advertisement response.

  • Adds IP-literal and DNS-result filtering for private, loopback, link-local, reserved, multicast, and unspecified addresses.
  • Stops treating authentication responses as evidence of a Git repository.
  • Adds focused tests for disallowed destinations and response handling.
  • The validation remains vulnerable to DNS rebinding because the checked address is not pinned to the subsequent connection.

Reviews (1) · Last reviewed commit: "fix(interface): prevent SSRF in host-sid..."

Comment thread strix/interface/utils.py Outdated
with requests.get(check_url, headers={"User-Agent": "git/2.43.0"}, timeout=10) as resp:
if resp.status_code >= 400:
return resp.status_code == 401
with requests.get(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 security DNS rebinding bypasses guard

The safety check resolves the hostname but keeps only a boolean result. requests.get then connects using the original hostname, causing a separate DNS lookup. An attacker-controlled hostname can return a public address during validation and rebind to a private or link-local address for the connection, allowing this host-side probe to reach internal infrastructure. The request must connect to a validated address while preserving the original hostname for HTTP and TLS, or enforce the address policy when connecting.

How this was verified: Attacker-controlled scan targets reach this function, and the validated addresses are neither returned nor pinned before the subsequent request resolves the hostname again.

Prompt To Fix With AI
This is a comment left during a code review.
Path: strix/interface/utils.py
Line: 1169

Comment:
**DNS rebinding bypasses guard**

The safety check resolves the hostname but keeps only a boolean result. `requests.get` then connects using the original hostname, causing a separate DNS lookup. An attacker-controlled hostname can return a public address during validation and rebind to a private or link-local address for the connection, allowing this host-side probe to reach internal infrastructure. The request must connect to a validated address while preserving the original hostname for HTTP and TLS, or enforce the address policy when connecting.

**How this was verified:** Attacker-controlled scan targets reach this function, and the validated addresses are neither returned nor pinned before the subsequent request resolves the hostname again.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

…g bypass

The previous SSRF guard validated the target hostname and then let
requests.get() resolve and connect to it separately. That leaves a
DNS-rebinding window: a malicious DNS server can answer the validation
lookup with a public address and the connection's own lookup, moments
later, with a private one, since nothing pins the two lookups together.

Resolve the hostname once via _resolve_pinned_probe_ip, validate that
address, and connect directly to it with urllib3's connection pools
(passing server_hostname/assert_hostname for TLS SNI and certificate
hostname verification, and an explicit Host header) instead of handing
the hostname back to an HTTP client that would re-resolve it.

Adds a regression test that simulates a rebinding DNS server (public
address on the first lookup, private on any later one) and asserts the
probe still connects to the address it already validated.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@siundu254

Copy link
Copy Markdown
Author

Thanks for the review — good catch. The original guard validated the hostname and then let requests.get() resolve and connect to it separately, which left a DNS-rebinding window (validation lookup answered with a public address, the connection's own lookup answered with a private one).

Pushed a follow-up commit that closes this: the hostname is now resolved exactly once via _resolve_pinned_probe_ip, and the probe connects directly to that validated address using urllib3's connection pools (server_hostname/assert_hostname handle TLS SNI and certificate hostname verification against the original hostname, with an explicit Host header), instead of handing the hostname back to an HTTP client that would re-resolve it.

Added test_is_http_git_repo_pins_the_connection_to_the_validated_address, which simulates a rebinding DNS server (public address on the first lookup, private on any subsequent one) and asserts the probe still connects to the address it already validated.

Full check-all equivalent (ruff format/check, mypy, pyright, bandit, pytest) re-run clean on the updated diff.

This branch has not been deployed

No deployments
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.

SSRF: target-type inference treats HTTP 401 as "is a git repo", probing from host with redirects followed

1 participant