The delivery engine's shared HTTP client uses an SSRF-safe dialer (NewSSRFSafeTransport, internal/delivery/ssrf.go, wired onto e.client). However clientForConfig() (internal/delivery/engine.go) constructs a fresh http.Client that does not carry that Transport when a per-target timeout is configured, which bypasses the request-time private-IP guard for that path.
This is currently latent — the UI does not expose a per-target timeout — but it is a real bypass if a target config ever carries one, so it should be closed before 1.0.
Definition of done:
clientForConfig() builds its client on top of the SSRF-safe Transport, overriding only the timeout
a test asserts that a client returned for a config with a per-target timeout still refuses a private/reserved destination
Part of the road to 1.0 (see #33).
The delivery engine's shared HTTP client uses an SSRF-safe dialer (`NewSSRFSafeTransport`, `internal/delivery/ssrf.go`, wired onto `e.client`). However `clientForConfig()` (`internal/delivery/engine.go`) constructs a fresh `http.Client` that does not carry that Transport when a per-target timeout is configured, which bypasses the request-time private-IP guard for that path.
This is currently latent — the UI does not expose a per-target timeout — but it is a real bypass if a target config ever carries one, so it should be closed before 1.0.
Definition of done:
- `clientForConfig()` builds its client on top of the SSRF-safe Transport, overriding only the timeout
- a test asserts that a client returned for a config with a per-target timeout still refuses a private/reserved destination
clawbot
added this to the 1.0.0 milestone 2026-08-07 13:16:10 +02:00
Confine this change to internal/delivery/ ONLY (engine.go plus a delivery-package test). Do not touch handlers or server code.
The problem: the engine's shared client uses an SSRF-safe transport (NewSSRFSafeTransport in internal/delivery/ssrf.go). clientForConfig() in internal/delivery/engine.go builds a fresh http.Client WITHOUT that transport when a per-target timeout is configured, bypassing the request-time private-IP guard for that path.
Fix:
make clientForConfig() build its client on top of the SSRF-safe transport (reuse the same transport the shared e.client uses), overriding only the Timeout
ensure every client the engine uses for outbound target requests carries the SSRF-safe transport — there must be no code path that makes an outbound target request without it
Definition of done:
a client returned by clientForConfig() for a config that sets a per-target timeout still refuses connections to private/reserved/link-local addresses (covered by a test)
behaviour is unchanged when no per-target timeout is set
the SSRF-safe transport is not duplicated in a way that drops its dialer
Gates and process:
make fmt before committing
validate with docker build . (must exit 0; host Go 1.25 vs go.mod 1.26, so Docker is the gate)
branch from main named issue-69-ssrf-client-timeout; commit subject ends with (closes #69)
open a PR (base main) and comment on it with the diff summary and the docker build . result
no AI-assistant/tooling references anywhere
## Implementation instructions
Confine this change to `internal/delivery/` ONLY (`engine.go` plus a delivery-package test). Do not touch handlers or server code.
The problem: the engine's shared client uses an SSRF-safe transport (`NewSSRFSafeTransport` in `internal/delivery/ssrf.go`). `clientForConfig()` in `internal/delivery/engine.go` builds a fresh `http.Client` WITHOUT that transport when a per-target timeout is configured, bypassing the request-time private-IP guard for that path.
Fix:
- make `clientForConfig()` build its client on top of the SSRF-safe transport (reuse the same transport the shared `e.client` uses), overriding only the `Timeout`
- ensure every client the engine uses for outbound target requests carries the SSRF-safe transport — there must be no code path that makes an outbound target request without it
Definition of done:
- a client returned by `clientForConfig()` for a config that sets a per-target timeout still refuses connections to private/reserved/link-local addresses (covered by a test)
- behaviour is unchanged when no per-target timeout is set
- the SSRF-safe transport is not duplicated in a way that drops its dialer
Gates and process:
- `make fmt` before committing
- validate with `docker build .` (must exit 0; host Go 1.25 vs go.mod 1.26, so Docker is the gate)
- branch from `main` named `issue-69-ssrf-client-timeout`; commit subject ends with ` (closes #69)`
- open a PR (base `main`) and comment on it with the diff summary and the `docker build .` result
- no AI-assistant/tooling references anywhere
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Part of the road to 1.0 (see #33).
The delivery engine's shared HTTP client uses an SSRF-safe dialer (
NewSSRFSafeTransport,internal/delivery/ssrf.go, wired ontoe.client). HoweverclientForConfig()(internal/delivery/engine.go) constructs a freshhttp.Clientthat does not carry that Transport when a per-target timeout is configured, which bypasses the request-time private-IP guard for that path.This is currently latent — the UI does not expose a per-target timeout — but it is a real bypass if a target config ever carries one, so it should be closed before 1.0.
Definition of done:
clientForConfig()builds its client on top of the SSRF-safe Transport, overriding only the timeoutImplementation instructions
Confine this change to
internal/delivery/ONLY (engine.goplus a delivery-package test). Do not touch handlers or server code.The problem: the engine's shared client uses an SSRF-safe transport (
NewSSRFSafeTransportininternal/delivery/ssrf.go).clientForConfig()ininternal/delivery/engine.gobuilds a freshhttp.ClientWITHOUT that transport when a per-target timeout is configured, bypassing the request-time private-IP guard for that path.Fix:
clientForConfig()build its client on top of the SSRF-safe transport (reuse the same transport the sharede.clientuses), overriding only theTimeoutDefinition of done:
clientForConfig()for a config that sets a per-target timeout still refuses connections to private/reserved/link-local addresses (covered by a test)Gates and process:
make fmtbefore committingdocker build .(must exit 0; host Go 1.25 vs go.mod 1.26, so Docker is the gate)mainnamedissue-69-ssrf-client-timeout; commit subject ends with(closes #69)main) and comment on it with the diff summary and thedocker build .result