Applications often fetch remote resources on behalf of users. Image importers, webhook testers, document converters, link preview services, feed readers, and URL-based upload features all need outbound network access. That capability becomes a security boundary as soon as an untrusted party can influence the destination.

A server can usually reach systems that an internet client cannot. It may have access to loopback services, private subnets, cloud metadata endpoints, internal administration panels, service discovery systems, or trusted network peers. A server-side request forgery flaw, commonly called SSRF, turns the application’s network position into an attack primitive.

The defensive goal is not merely to reject suspicious URL strings. A robust design constrains the full request path: accepted schemes, destination names, DNS results, redirect targets, ports, network routes, response size, and request duration.

Treat outbound fetching as a privileged operation

Consider a feature that accepts an image URL and stores the downloaded bytes:

browser -> application -> supplied URL
                       -> remote server

The browser never contacts the supplied destination. The application does. As a result, the destination sees the application’s source network identity and receives a request from a host with the application’s reachability.

A request such as this can be dangerous even when the response is never shown directly to the caller:

http://127.0.0.1:9000/admin

The request itself may trigger an action. Response timing, status, or size can also reveal information about internal services.

Start by treating every server-side URL fetch as a privileged operation. Put the fetch logic behind one small, reviewed component instead of allowing each feature to create unrestricted HTTP clients.

Prefer destination allowlists when the product permits them

The strongest policy is often a narrow destination allowlist. If a webhook verifier only needs to contact a fixed vendor API, accept that vendor’s expected HTTPS host rather than arbitrary internet URLs.

An allowlist can constrain several dimensions at once:

  • scheme, usually https
  • exact host or controlled host suffix
  • expected port
  • permitted path prefix when appropriate
  • HTTP methods

Compare these two policies:

weak:  accept any URL except strings containing "localhost"
strong: accept HTTPS URLs for api.example.test on port 443

A blocklist has to anticipate every representation of every forbidden destination. An allowlist describes the small set of destinations the feature actually needs.

Some products genuinely need arbitrary public internet destinations. In that case, network-address validation becomes essential.

Parse URLs with a real URL parser

Do not make security decisions with substring checks, regular expressions intended for presentation validation, or string splitting.

URLs contain user information, IPv6 literals, percent encoding, internationalized names, explicit ports, and other syntax that can confuse ad hoc parsers. Parse the input with a standards-aware URL library, then make policy decisions from the parsed components.

A safe sequence is:

input
  -> parse
  -> check scheme
  -> normalize host for comparison
  -> check port policy
  -> resolve host
  -> check every resulting address
  -> connect under network policy

Reject unsupported schemes rather than passing them to a generic transfer library. A feature intended to retrieve web resources rarely needs file:, ftp:, gopher:, or other schemes.

Validate the resolved IP address

Checking only the hostname leaves a major gap. DNS maps names to addresses, and an attacker may control that mapping.

For a public-internet fetcher, reject destinations that resolve to address ranges the application must not contact. The exact policy depends on the deployment, but commonly restricted targets include:

  • loopback addresses
  • private network ranges
  • link-local ranges
  • unspecified addresses
  • multicast ranges
  • infrastructure-specific metadata addresses
  • internal ranges assigned to the organization

Apply the policy to both IPv4 and IPv6. IPv6 must not become a side door around an IPv4-only filter.

Also inspect every address returned by DNS. Accepting a hostname because one result is public while another result is private can create inconsistent behavior across connection attempts.

Keep validation and connection bound together

A subtle race appears when code resolves a hostname for validation and the HTTP library performs a fresh, independent DNS lookup during connection setup.

security check: host -> public address
later lookup:   host -> private address

An attacker who controls DNS can change the answer between those operations. This pattern is often called DNS rebinding in SSRF discussions.

A stronger fetcher resolves the host, validates the selected address, and makes the connection to that validated address while preserving the original hostname for TLS certificate verification and the HTTP Host or authority value.

Conceptually:

URL host: images.example.test
DNS result: 203.0.113.40
policy: address accepted
TCP: connect to validated address
TLS: verify certificate for images.example.test
HTTP: send authority for images.example.test

Do not disable TLS hostname checks to make this binding easier. The network destination check and TLS identity check protect different boundaries.

Production implementations should use networking hooks supplied by the language or HTTP stack rather than reconstructing HTTP and TLS protocols manually.

Reapply policy after every redirect

Redirects create a second destination decision. A public server can return:

HTTP/1.1 302 Found
Location: http://127.0.0.1:9000/admin

If the HTTP client follows redirects automatically, a carefully validated initial URL can still lead to a forbidden target.

Disable automatic redirect following or install a redirect callback that sends each new target through the same policy as the original URL. Recheck scheme, host, port, DNS results, and address ranges on every hop.

Set a small redirect limit as well. This prevents loops and keeps request cost bounded.

Put network controls beneath application checks

Application validation is valuable, but it should not be the only barrier. Egress controls can make forbidden destinations unreachable even if a parser bug or policy mistake reaches production.

Useful controls include:

  • firewall or network-policy rules that deny sensitive internal ranges
  • a dedicated outbound proxy with destination policy
  • separate runtime identities for fetch workers
  • isolated subnets with no route to management networks
  • explicit denial of cloud metadata services when they are not required

This is defense in depth. The application decides what it intends to contact; the network limits what it is physically able to contact.

For high-risk fetch features, moving retrieval into a small isolated worker can substantially reduce impact. The worker can have no database credentials, no internal service access, and only the minimum outbound connectivity needed for its task.

Bound resource consumption

SSRF protection also needs operational limits. A permitted public endpoint can respond slowly, stream data indefinitely, or return a huge object.

Set limits for:

  • connection establishment time
  • total request duration
  • response headers
  • response body size
  • redirect count
  • concurrent fetches per caller or tenant

Read response bodies through a hard byte limit. A Content-Length header can support an early rejection, but it is not sufficient because a server can omit it or send a different amount of data.

If the feature expects an image, document, or feed, validate the downloaded content after transport limits have been applied. Content-type validation does not replace destination controls.

Avoid forwarding ambient credentials

A generic server-side HTTP client may carry credentials, cookies, client certificates, proxy authorization, or tracing headers that are appropriate for trusted internal calls. Do not reuse such a client for attacker-influenced destinations.

Create a dedicated client with an intentionally small header set. Do not forward the caller’s Authorization header. Do not attach internal service credentials based only on a destination supplied in the request.

This separation also makes code review simpler: the fetcher has one security profile and one destination policy.

Handle hostnames and ports as structured values

A host policy should compare parsed host values, not textual suffixes with ambiguous boundaries.

For example, a policy intended for subdomains of example.test must not accept example.test.attacker.test. Exact matching or label-aware suffix matching avoids that mistake.

Ports need explicit treatment too. If a feature requires standard HTTPS, allowing arbitrary ports expands the reachable service set. Restrict ports to the product requirement.

When internationalized domain names are accepted, normalize them consistently before policy comparison. Keep the normalization rules centralized in the fetch component so every caller receives the same behavior.

Test the policy as an adversarial boundary

Unit tests should cover more than ordinary public URLs. Build a table of cases that exercise parser and network boundaries:

https public host                 -> accept
http when HTTPS is required       -> reject
loopback IPv4                     -> reject
loopback IPv6                     -> reject
private IPv4                      -> reject
link-local destination            -> reject
hostname resolving to private IP  -> reject
mixed public/private DNS answers  -> reject
redirect to private destination   -> reject
unsupported scheme                -> reject
forbidden port                    -> reject

Also test the connection binding itself. A mock resolver can return one address during validation and another later; the test should confirm that the fetcher connects only to an address that passed policy.

Keep tests for redirects separate from initial-request tests. Redirect handling is easy to regress when HTTP client configuration changes.

A practical review checklist

When reviewing a server-side URL feature, trace the complete route from input to socket:

  1. Identify every value the caller can influence.
  2. Confirm that a real URL parser handles the input.
  3. Restrict schemes and ports to the feature’s needs.
  4. Prefer an explicit destination allowlist when possible.
  5. Resolve hostnames and reject forbidden address ranges for both IP families.
  6. Bind the actual connection to an address that passed validation.
  7. Revalidate every redirect destination.
  8. Keep ambient credentials and internal headers out of the request.
  9. Enforce time, size, redirect, and concurrency limits.
  10. Add network-level egress restrictions beneath the application.
  11. Test hostile URL forms, DNS answers, and redirect chains.

The central principle is simple: a URL supplied by an untrusted party is not just text. In a server-side fetch feature, it can become a network-routing instruction. Treat that instruction as a security-sensitive capability and constrain every stage from parsing through connection establishment.