TidyURLs
Go to Tools
BLOG
The TidyURLsJournal

Practical guides, tutorials, and insights about URLs, SEO, web development, and online marketing. Learn, troubleshoot, and get the most of the web.

Back to the Journal
TidyURLs Journal

SSRF-Safe URL Validation: What to Check Before Fetching

Practical insights, clear explanations, and useful ideas for understanding the web.

October 28, 20269 min read6 topics
SSRF-Safe URL Validation: What to Check Before Fetching
Explore the details
#SSRF#URL validation#web security#DNS#backend development#network security
October 28, 20269 min read

A URL can be syntactically valid and still point to a sensitive service on your network. If your backend fetches user-submitted URLs, preventing server-side request forgery (SSRF) takes more than parsing the URL: you need a destination policy, DNS-aware connection controls, and checks on every redirect.

This guide walks through those checks and explains where validation can fail. It focuses on a defensive workflow for backend developers; exact networking APIs depend on your runtime and HTTP client.

Key Takeaways

  • URL parsing checks structure, not whether a destination is safe to contact.
  • Prefer an allowlist of exact hostnames and permitted ports when the feature allows it.
  • If arbitrary public hosts must be supported, check every resolved IPv4 and IPv6 address, then ensure the connection uses an address you approved.
  • Disable automatic redirects or validate every redirect target as a new request.
  • Use network egress controls as an additional boundary. Application-level checks alone are not a complete SSRF defense.

Why URL syntax checks are not enough

A parser can tell you that https://example.test/path has a scheme, hostname, and path. It cannot tell you whether the hostname resolves to a private address, whether DNS changes between validation and connection, or whether the server will redirect your client somewhere else.

For example, a URL may use an ordinary-looking hostname that resolves to loopback, a private network, or a cloud metadata address. An attacker may also use alternate IP representations or exploit a difference between the address your code checked and the address the HTTP client eventually contacted.

Treat validation as a sequence of checks at the point where the network request is made—not as a one-time check on the input string.

Build a destination policy

First decide what destinations the feature actually needs to reach. A service that fetches previews from a fixed set of partner domains should use that set as an allowlist. A feature that fetches arbitrary public pages needs a broader policy and stronger network isolation.

For each request, define rules for:

  • Scheme: Usually permit only https. Allow http only when there is a specific requirement and the risk is understood. Reject other schemes, including file, ftp, and application-specific schemes.
  • Hostname: Prefer exact allowed hostnames. If subdomains are allowed, compare on a DNS-label boundary: host === root || host.endsWith("." + root). A plain suffix check can wrongly accept names such as trusted.example.attacker.test or nottrusted.example.
  • Port: Allow only the ports the feature needs, commonly the default web ports. Do not let arbitrary ports become a way to probe internal services.
  • URL credentials: Reject URLs containing a username or password. They are unnecessary for most public fetching features and can make the displayed and actual destination harder to reason about.
  • Fragments and length: Fragments are not sent in an HTTP request; remove them if they are irrelevant. Limit URL length and reject malformed input rather than trying to repair it permissively.

Parse with a standards-aware URL implementation, then apply policy to the parsed components. Do not make security decisions using ad hoc string searches or regular expressions over the raw URL.

Check DNS results and IP addresses

When the policy permits hostnames beyond a fixed allowlist, resolve the hostname and inspect the resulting addresses before connecting. Account for both address families: query or otherwise handle A (IPv4) and AAAA (IPv6) records.

Reject destinations in prohibited ranges. At a minimum, consider loopback, private, link-local, unspecified, multicast, and other non-public or special-purpose ranges. The exact policy should match your environment: a service may have internal ranges beyond the standard private ranges, and address classifications can change as standards and registries evolve.

Use a maintained IP-address library or the platform’s address parsing facilities. Do not classify addresses with string prefixes. Normalize and parse addresses before applying range checks. Watch for IPv4-mapped IPv6 addresses, such as an IPv4 address represented inside an IPv6 address, and make sure your policy treats them consistently. Reject malformed or ambiguous forms, including zone identifiers unless you have a specific, safe reason to support them.

Do not assume that a hostname resolving to one public address is safe if another answer is prohibited. A conservative policy rejects the hostname if any returned address is disallowed. If you choose one approved address, the connection must actually use an address that was checked.

Avoid time-of-check/time-of-use gaps

A common failure is to resolve and validate a hostname, then pass the original hostname to an HTTP client that performs a fresh DNS lookup. DNS can change between those steps. In a DNS rebinding scenario, an attacker-controlled name may return a public address during validation and a prohibited address when the client connects.

The safer design is to bind the connection to an address that passed validation while retaining the original hostname for HTTP Host handling and TLS certificate verification. After connecting, check the actual peer address where the client or transport allows it. If your HTTP library cannot reliably connect to the vetted address while preserving hostname-based TLS checks, do not assume that a preceding DNS check closes the gap; use a suitable transport, a vetted egress proxy, or a narrower hostname allowlist.

Handle redirects as new requests

An initially safe URL can redirect to an unsafe destination. Many HTTP clients follow redirects automatically, which can bypass checks that run only on the starting URL.

A defensive redirect workflow is:

  1. Disable automatic redirects.
  2. Send the request to the validated destination.
  3. If the response is a redirect, resolve its Location against the current URL, as required for relative locations.
  4. Parse and validate the resulting URL from the beginning: scheme, hostname, port, DNS answers, and address policy.
  5. Connect only to an approved address, then repeat for each redirect.
  6. Set a small redirect limit and fail closed on malformed locations, unsupported redirect behavior, or a limit breach.

Do not forward credentials, cookies, authorization headers, or other sensitive request headers to a new host unless the feature explicitly requires it and the destination is authorized. Redirect policy should also account for method changes and request-body handling in the HTTP client.

A practical request flow

Use this as a design checklist around your HTTP client. It is intentionally language-neutral: the crucial detail is that the transport must connect to the address you validated, rather than silently resolving the hostname again.

fetchUserURL(input):
    url = parseWithStandardsBasedParser(input)
    reject unless url.scheme is permitted
    reject unless url has no username or password
    reject unless url.hostname and url.port satisfy destination policy

    for redirectCount from 0 through MAX_REDIRECTS:
        addresses = resolveAAndAAAA(url.hostname)
        reject if resolution fails or returns no usable addresses
        reject if any address violates the IP destination policy

        approvedAddress = choose an address from the checked results
        response = requestUsingPinnedAddress(
            address = approvedAddress,
            tlsServerName = url.hostname,
            hostHeader = url.hostname,
            followRedirects = false,
            timeout = REQUEST_TIMEOUT,
            responseSizeLimit = MAX_RESPONSE_BYTES
        )
        reject if connected peer address is not approved

        if response is not a redirect:
            return response

        reject if redirectCount equals MAX_REDIRECTS
        url = resolveLocationAgainstCurrentURL(response.location, url)
        repeat all URL, DNS, and address checks

The names in the pseudocode describe required behavior, not drop-in APIs. Confirm that your chosen runtime and HTTP client can pin the connection address without disabling normal TLS hostname verification. A custom DNS lookup hook or transport may help, but its behavior is library- and version-specific. If you cannot verify that behavior, put a controlled egress proxy between the application and the network or restrict fetching to hosts you can safely allowlist.

Add network-level controls

Application validation is one layer. Run URL-fetching workers with least privilege and restrict their outbound network access so they cannot reach internal services or sensitive infrastructure. Use firewall rules, network policies, or an egress proxy appropriate to your deployment. Keep secrets and privileged credentials out of the fetcher’s environment where possible.

Also set connection and total-request timeouts, response-size limits, redirect limits, and concurrency limits. These controls reduce resource-exhaustion risk; they do not replace destination validation. Log enough to investigate rejected requests without storing sensitive URL query parameters unnecessarily.

Common Mistakes

  • Treating a successful parse as a security check. Parsing establishes URL structure, not destination trust.
  • Checking only the hostname string. A hostname can resolve to a prohibited IP address, and a seemingly allowed suffix may match the wrong domain.
  • Checking only IPv4. IPv6 and IPv4-mapped IPv6 can bypass policies that consider only IPv4.
  • Using a blocklist as the whole policy. Blocklists are easy to make incomplete, and network layouts vary. Prefer an allowlist when possible; if arbitrary public destinations are required, combine comprehensive address classification with connection pinning and egress restrictions.
  • Validating DNS once, then letting the client resolve again. That leaves a time-of-check/time-of-use gap and can enable DNS rebinding.
  • Validating only the first URL. Every redirect target is a new destination and needs the same checks.
  • Assuming HTTPS makes a destination safe. TLS protects a connection to a host; it does not make that host an authorized or public destination.
  • Assuming an online URL parser is an SSRF defense. A parser can help inspect URL components, but it cannot enforce server-side DNS, connection, redirect, or network-egress policy.

Best Practices

  1. Prefer exact hostname and port allowlists where product requirements permit them.
  2. Parse once with a standards-based implementation and reject ambiguous or unsupported input.
  3. Resolve both address families, apply a maintained IP-range policy, and reject mixed safe/unsafe answers.
  4. Pin the outbound connection to a checked address while preserving TLS hostname verification; verify the peer address when possible.
  5. Disable automatic redirects and re-run the complete policy for every hop.
  6. Restrict egress at the network layer and run fetchers with minimal privileges.
  7. Add timeouts, response-size and concurrency limits, and tests for private IPv4/IPv6 destinations, mapped addresses, DNS changes, and redirect chains.

Research note: I can’t retrieve live documentation in this environment, so I can’t confirm that the linked guidance or platform documentation reflects the latest changes. Verify the current OWASP guidance and your runtime’s DNS and networking APIs before deploying an implementation. In particular, connection pinning and address classification are client- and version-dependent.

Conclusion

To validate URLs to prevent SSRF, check more than syntax: enforce a destination policy, inspect DNS results, make the network connection use an approved address, and validate every redirect. Pair those controls with restricted egress and resource limits; no single parser or hostname check provides complete protection.

References