Search by

bahdan / symfony-safe-http-client

bhal

SSRF-protected HTTP Client wrapper for Symfony HttpClient with DNS resolution pinning and subnet validation

Package info

github.com/bahdanhal/symfony-safe-http-client

pkg:composer/bahdan/symfony-safe-http-client

Statistics

Installs: 502

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.2 2026-10-05 07:21 UTC

This package is auto-updated.

Last update: 2026-10-05 18:36:34 UTC


README

An SSRF-safe HTTP client wrapper on top of Symfony HttpClient with DNS resolution pinning and subnet validation.

Packagist · GitHub

Used in production by Stackhal.

Features

  • SSRF Defense: Validates URLs against private IP ranges (RFC 1918, RFC 4193), link-local addresses, and cloud instance metadata (169.254.169.254).
  • DNS Resolution Pinning: Resolves hostnames before request dispatch and pins the IP to prevent DNS rebinding attacks.
  • Concurrent DNS: Resolves unique batch hostnames concurrently through Amp instead of serial native DNS calls, and falls back to blocking system lookups inside fibers the event loop does not own.
  • Safety Limits: Configurable body size limits, redirect limits, and request timeouts.
  • Port Allowlist: Allows only HTTP ports 80 and 443 by default to prevent cross-protocol SSRF.
  • Batch Requests: Concurrently fetch and safely resolve multiple URLs.
  • DI-Friendly Contract: Type-hint SafeHttpFetcherInterface in application services.

Installation

composer require bahdan/symfony-safe-http-client

Usage

use Bahdan\SafeHttpClient\SafeHttpFetcher;
use Bahdan\SafeHttpClient\UrlGuard;
use Symfony\Component\HttpClient\HttpClient;

$guard = new UrlGuard();
$fetcher = new SafeHttpFetcher(HttpClient::create(), $guard);

$result = $fetcher->fetch('https://example.com');
echo $result['body'];

Implement DnsResolverInterface and pass it to UrlGuard to use a custom resolver while retaining subnet validation and connection pinning.

Applications that intentionally fetch from another HTTP port can opt in explicitly:

$guard = new UrlGuard(allowedPorts: [80, 443, 8080, 8443]);

DNS resolvers

UrlGuard uses AsyncDnsResolver by default. It resolves through the Amp event loop when called outside fibers or from a fiber the event loop started. Inside any other fiber, for example a tool handler run by the PHP MCP SDK, awaiting would suspend a fiber the event loop cannot resume, so the resolver uses NativeDnsResolver there instead.

NativeDnsResolver performs blocking dns_get_record() lookups and needs no event loop. Pass it to UrlGuard to always resolve that way:

use Bahdan\SafeHttpClient\NativeDnsResolver;

$guard = new UrlGuard(new NativeDnsResolver());

How DNS rebinding protection works

The guard resolves every hostname before a request, rejects the request if any returned address belongs to a private, loopback, link-local, reserved, multicast, documentation, or cloud metadata range, and returns one validated public address. It prefers an IPv4 address when the hostname has both IPv4 and IPv6 records, so the pinned address stays reachable from hosts without an IPv6 route. SafeHttpFetcher passes that address through Symfony HttpClient's resolve option. The socket therefore connects to the exact address that was validated while the original hostname remains in the URL for TLS SNI and certificate verification. Redirect targets repeat the full validation and pinning process.

Canonical IPv4 and IPv6 literals are checked directly. Ambiguous legacy numeric forms such as dotted octal and hexadecimal addresses are rejected instead of being delegated to platform-dependent DNS parsing.

Symfony dependency injection

The package is framework-agnostic, so it does not force a bundle into applications that only need the library. Register it with standard Symfony service configuration:

# config/services.yaml
services:
  Bahdan\SafeHttpClient\UrlGuard: ~

  Bahdan\SafeHttpClient\SafeHttpFetcher:
    arguments:
      $httpClient: '@http_client'

  Bahdan\SafeHttpClient\SafeHttpFetcherInterface:
    alias: Bahdan\SafeHttpClient\SafeHttpFetcher

Application services can now depend on Bahdan\SafeHttpClient\SafeHttpFetcherInterface and replace the implementation in tests.

License

MIT