SafeHttpClient — the safe outbound HTTP path
Any server-side fetch whose destination is chosen by someone below the trusted
operator — a request parameter, a remote party's DNS/SRV record, or a row a
low-privilege user can write — goes through includes/SafeHttpClient.php. It is
the default so that safety is not a per-callsite discipline nobody keeps.
What it guarantees, on every request
- Validation before a socket opens. The URL, and every redirect hop, passes
through
UrlSafetyValidator(scheme allowlist, port policy, hostname-literal rejection, DNS resolution of every A/AAAA record against private / loopback / link-local / reserved ranges). - Connection pinned to the validated IPs via
CURLOPT_RESOLVE— the real hostname is still used for SNI, Host and certificate verification, but the socket cannot be re-resolved to a rebinding target between the check and the connect. - Redirects are never handed to curl. Following is a manual loop that
re-validates and re-pins every hop;
CURLOPT_FOLLOWLOCATIONis always off. - TLS verification is always on — there is no per-call insecure escape hatch.
- Response body and headers are both capped so attacker-controlled content cannot be buffered without bound.
Using it
require_once(PathHelper::getIncludePath('includes/SafeHttpClient.php'));
$client = new SafeHttpClient([
'allowed_ports' => [80, 443], // default; a list, or ['allow' => [443], 'min' => 1024]
'allow_redirects' => false, // default; set true only when a redirect is expected
'timeout' => 15,
]);
$response = $client->get($url, ['Header-Name' => 'value']);
// $response->status, ->body, ->headers (lowercased map), ->header('Content-Type'), ->json()An unsafe URL (or redirect hop) throws UnsafeUrlException; a transport failure
throws SafeHttpException. Catch both at the callsite and treat a refusal as a
failed fetch, never as a reason to fall back to a bare request.
SafeHttpClient::directPortPolicy() returns the port policy Joinery Direct uses
(443, or any port ≥ 1024), because its destination is a remote party's SRV record.
When NOT to use it
A fetch whose host is a compile-time constant no user or admin can move — a
vendor API base (Stripe, PayPal, Mailgun, the DNS driver API_BASE
constants) — is not an SSRF surface and does not need the client. A callsite that
legitimately needs a policy the client forbids (following redirects uncapped, or
skipping TLS verification for a self-signed node cert) keeps its own transport and
documents why at the callsite.