Keep the SSRF-safe transport in clientForConfig (closes #69) #74

Merged
sneak merged 2 commits from issue-69-ssrf-client-timeout into main 2026-08-07 14:03:38 +02:00
Collaborator

clientForConfig() in internal/delivery/engine.go built a fresh http.Client without a Transport when a per-target timeout was configured, dropping the request-time private-IP guard for that path.

It now reuses the shared client's SSRF-safe transport (e.client.Transport, the same NewSSRFSafeTransport instance), overriding only the Timeout. Behaviour is unchanged when no per-target timeout is set (the shared client is returned as before), so no engine code path makes an outbound target request with a client lacking the SSRF-safe transport.

Adds a delivery-package test proving a client from clientForConfig() with a per-target timeout still refuses private/reserved/link-local destinations, that the timeout is applied, that the SSRF-safe transport is reused (not duplicated), and that the no-timeout path returns the shared client unchanged.

Confined to internal/delivery/ only; handlers and server code untouched.

Closes #69

`clientForConfig()` in `internal/delivery/engine.go` built a fresh `http.Client` without a Transport when a per-target timeout was configured, dropping the request-time private-IP guard for that path. It now reuses the shared client's SSRF-safe transport (`e.client.Transport`, the same `NewSSRFSafeTransport` instance), overriding only the `Timeout`. Behaviour is unchanged when no per-target timeout is set (the shared client is returned as before), so no engine code path makes an outbound target request with a client lacking the SSRF-safe transport. Adds a delivery-package test proving a client from `clientForConfig()` with a per-target timeout still refuses private/reserved/link-local destinations, that the timeout is applied, that the SSRF-safe transport is reused (not duplicated), and that the no-timeout path returns the shared client unchanged. Confined to `internal/delivery/` only; handlers and server code untouched. Closes #69
clawbot added 1 commit 2026-08-07 14:00:10 +02:00
Keep the SSRF-safe transport in clientForConfig (closes #69)
Some checks failed
check / check (push) Has been cancelled
6b14fc847d
Author
Collaborator

What changed

  • internal/delivery/engine.goclientForConfig(): when a per-target timeout is set, the returned http.Client now sets Transport: e.client.Transport (the shared NewSSRFSafeTransport instance), overriding only Timeout. Previously it returned a transport-less client, silently dropping the SSRF dialer for that path. No-timeout path is unchanged (still returns the shared e.client).
  • internal/delivery/export_test.go — added ExportClientForConfig and ExportClient helpers for the delivery-package test.
  • internal/delivery/client_ssrf_test.go (new) — asserts a client from clientForConfig() with a per-target timeout still refuses 127.0.0.1, 169.254.169.254, and [fe80::1]; that the timeout is applied; that the SSRF-safe transport is reused (same pointer), not duplicated; and that the no-timeout path returns the shared client unchanged.

Diffstat: 3 files changed, 129 insertions(+).

Validation

make fmt clean. Docker build gate (runs fmt-check, lint, test, build):

docker build . exit code: 0
### What changed - `internal/delivery/engine.go` — `clientForConfig()`: when a per-target timeout is set, the returned `http.Client` now sets `Transport: e.client.Transport` (the shared `NewSSRFSafeTransport` instance), overriding only `Timeout`. Previously it returned a transport-less client, silently dropping the SSRF dialer for that path. No-timeout path is unchanged (still returns the shared `e.client`). - `internal/delivery/export_test.go` — added `ExportClientForConfig` and `ExportClient` helpers for the delivery-package test. - `internal/delivery/client_ssrf_test.go` (new) — asserts a client from `clientForConfig()` with a per-target timeout still refuses `127.0.0.1`, `169.254.169.254`, and `[fe80::1]`; that the timeout is applied; that the SSRF-safe transport is reused (same pointer), not duplicated; and that the no-timeout path returns the shared client unchanged. Diffstat: 3 files changed, 129 insertions(+). ### Validation `make fmt` clean. Docker build gate (runs fmt-check, lint, test, build): ``` docker build . exit code: 0 ```
sneak added 1 commit 2026-08-07 14:01:21 +02:00
Merge branch 'main' into issue-69-ssrf-client-timeout
All checks were successful
check / check (push) Successful in 2m40s
de7103889d
Author
Collaborator

Independent review — PASS (merge-ready)

Adversarial review against the issue spec and repo policies. This one is security-sensitive, so I scrutinized it closely.

  • internal/delivery/engine.go clientForConfig(): the per-target-timeout branch now sets Transport: e.client.Transport, reusing the shared SSRF-safe transport and overriding only Timeout. The no-timeout branch still returns the shared e.client. So no outbound-request path drops the request-time private-IP guard, and the transport is shared rather than duplicated. Correct.
  • internal/delivery/client_ssrf_test.go is a genuine functional test, not just pointer assertions: for 127.0.0.1, 169.254.169.254, and [fe80::1] it builds a per-target-timeout client and actually calls client.Do, asserting the request is blocked by the guard (error contains "blocked"), the per-target timeout is applied, and the transport pointer matches the shared client's. A second test asserts the no-timeout path returns the shared client unchanged. The cases are offline and deterministic — private/reserved addresses are rejected before any dial.
  • Scope confined to internal/delivery/; make fmt clean; docker build . green; no AI/tooling references; commit subject closes the issue.

Verdict: meets the bar, with strong test coverage. Marking merge-ready and handing to @sneak for final review.

## Independent review — PASS (merge-ready) Adversarial review against the issue spec and repo policies. This one is security-sensitive, so I scrutinized it closely. - `internal/delivery/engine.go` `clientForConfig()`: the per-target-timeout branch now sets `Transport: e.client.Transport`, reusing the shared SSRF-safe transport and overriding only `Timeout`. The no-timeout branch still returns the shared `e.client`. So no outbound-request path drops the request-time private-IP guard, and the transport is shared rather than duplicated. Correct. - `internal/delivery/client_ssrf_test.go` is a genuine functional test, not just pointer assertions: for `127.0.0.1`, `169.254.169.254`, and `[fe80::1]` it builds a per-target-timeout client and actually calls `client.Do`, asserting the request is blocked by the guard (error contains "blocked"), the per-target timeout is applied, and the transport pointer matches the shared client's. A second test asserts the no-timeout path returns the shared client unchanged. The cases are offline and deterministic — private/reserved addresses are rejected before any dial. - Scope confined to `internal/delivery/`; `make fmt` clean; `docker build .` green; no AI/tooling references; commit subject closes the issue. Verdict: meets the bar, with strong test coverage. Marking merge-ready and handing to @sneak for final review.
sneak was assigned by clawbot 2026-08-07 14:02:23 +02:00
sneak merged commit b1f43c9520 into main 2026-08-07 14:03:38 +02:00
sneak deleted branch issue-69-ssrf-client-timeout 2026-08-07 14:03:38 +02:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/webhooker#74