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

Accepting User-Supplied Callback URLs Without Creating an SSRF Risk

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

October 28, 20269 min read6 topics
Accepting User-Supplied Callback URLs Without Creating an SSRF Risk
Explore the details
#SSRF#URL validation#webhooks#backend security#DNS security#network security
October 28, 20269 min read

A callback feature can make your server an unwitting network client. If an attacker can choose the destination URL, they may try to make your application contact internal services, cloud metadata endpoints, or other destinations that should not be reachable from the public internet. This guide explains layered ways to reduce that risk when accepting callback URLs.

Key Takeaways

  • Prefer a fixed destination or a strict hostname allowlist when the product allows it. Arbitrary URLs require stronger controls.
  • Parse URLs with a reputable library, allow only the schemes and ports you need, and validate the destination's resolved IP addresses.
  • Prevent time-of-check/time-of-use gaps: connect to an address you validated, or use a controlled egress proxy that enforces the same policy.
  • Disable automatic redirects, or validate every redirect destination before following it.
  • Treat application checks as one layer. Network egress controls should prevent access to internal and metadata networks even if application validation fails.

Why callback URLs create an SSRF risk

In a server-side request forgery (SSRF) attack, a user influences a request that your server makes. The request may originate from a network location with access the user does not have. A URL that looks like an ordinary public callback can instead point to a loopback service, a private network address, or a cloud metadata endpoint. A vulnerable server might then expose internal responses or trigger actions on those services.

The risk is not limited to a user entering a literal private IP address. A hostname can resolve to a prohibited address, a DNS answer can change between validation and connection, and an HTTP client can follow a redirect to a different destination. URL parsing alone does not establish that a network destination is safe.

Choose the narrowest callback model

Before accepting arbitrary URLs, decide whether you need them:

  1. Fixed destination: Store a server-configured URL and do not let the user choose the host. This is the simplest option to secure.
  2. Hostname allowlist: Permit only specific, controlled hosts. Compare normalized hostnames exactly; avoid checks such as host.endsWith("example.com"), which can also match notexample.com. If subdomains are intended, check the label boundary, such as host === "example.com" || host.endsWith(".example.com").
  3. Arbitrary public destinations: If users must provide any callback host, combine strict URL and IP validation, safe connection behavior, redirect controls, and network-level egress restrictions. A hostname allowlist may not be available, but the other layers still apply.

An ownership check or a verification request can help establish that a user controls a callback endpoint. It does not prove that the endpoint will always resolve to a safe network destination.

Validate the URL and its destination

Apply policy to the parsed URL, not to a string assembled with ad hoc checks. A typical policy for an HTTP callback might:

  • Accept only https (or http only if the feature genuinely requires it).
  • Reject user information in the URL, such as https://user:password@host/.
  • Allow only necessary ports, commonly 443 for HTTPS and, if explicitly required, 80 for HTTP.
  • Reject malformed URLs, unsupported schemes, and hostnames that are empty or ambiguous under your URL parser.
  • Resolve the hostname and inspect every returned IPv4 and IPv6 address. Reject the destination if any address is prohibited by your policy, rather than selecting a public answer from a mixed set.
  • Use a maintained IP-address library to classify addresses. Include loopback, private, link-local, unspecified, multicast, and other non-public or special-purpose ranges appropriate to your environment.

Do not rely only on a list of familiar strings. For example, blocking 127.0.0.1 does not address all loopback representations, IPv6 loopback, IPv4-mapped IPv6 addresses, or alternate numeric forms that different parsers may interpret differently. Parse and canonicalize with the same standards-compliant URL and IP libraries used by the request path, then apply policy to the resulting address objects. Reject IPv6 zone identifiers and other forms your application does not explicitly support.

A hostname allowlist and an IP policy solve different problems. An allowlisted hostname can still resolve to a prohibited address, so check the resolved destination too. Conversely, rejecting private IPs does not restrict which public host a user can choose.

Close the DNS and connection gap

A check such as “resolve hostname, verify it is public, then ask the HTTP client to fetch the hostname” can be vulnerable to a time-of-check/time-of-use gap. The HTTP client may perform its own DNS lookup later, and the answer could differ. This can enable DNS rebinding: a hostname may return a public address during validation and a private address when the connection is made.

Use a request path that connects to an address covered by your validation. Options include:

  • Pin the validated address for the connection. Resolve the hostname, validate the complete answer set, and configure the transport to connect to a selected validated IP without performing a second uncontrolled lookup. For HTTPS, preserve the original hostname for TLS certificate verification and SNI, and preserve the correct HTTP Host value. Do not disable certificate verification to make IP pinning work.
  • Use a controlled egress proxy. Have the proxy resolve destinations and enforce hostname, IP, port, and redirect policy at connection time. Ensure the application cannot bypass the proxy through environment settings or another network route.

Connection pooling, retries, proxy configuration, and DNS caching can affect which address a client actually uses. Confirm that the client or proxy enforces your policy for each new connection and that pooled connections cannot bypass a changed policy. If your chosen HTTP library cannot reliably pin or constrain the connection, use a network proxy or a library with a suitable transport hook rather than assuming that a preflight DNS check is sufficient.

Handle redirects as new destinations

A safe initial URL can respond with a redirect to a prohibited host. Many HTTP clients follow redirects automatically, so configure the callback client not to do so. If redirects are a product requirement, process them manually: resolve each Location relative to the current URL, parse it, apply the full scheme, hostname, port, and IP policy again, and connect only to an approved destination. Set a small redirect limit and reject redirect loops.

Do not forward credentials, authorization headers, cookies, or other sensitive headers to a different host. Prefer not to include secrets in callback URLs at all; URLs can be recorded in application logs and intermediary systems.

Put network controls behind application checks

Run callback delivery from a dedicated worker or service with narrowly scoped network access. At the network layer, deny routes to internal address ranges, loopback, link-local networks, and cloud metadata services unless a specific, reviewed dependency requires otherwise. Apply the policy to both IPv4 and IPv6, and account for proxy paths and other ways the worker could reach protected networks.

Cloud metadata services are especially sensitive because they may expose instance or workload credentials. Provider-specific protections can reduce risk—for example, AWS documents its Instance Metadata Service and IMDSv2—but metadata settings are defense in depth, not permission to let arbitrary user URLs reach metadata endpoints. Keep callback workers away from privileged workloads when possible, and verify the metadata and network controls for the cloud and deployment you use.

Also bound the request: set connection and total timeouts, cap response bytes, limit concurrency and retries, and accept only the methods and response behavior the feature needs. These controls reduce resource-exhaustion risk; they do not replace destination validation.

A practical request workflow

Use a single component to enforce the policy so that initial requests, retries, and redirects cannot take different paths. The following is illustrative pseudocode, not a drop-in implementation; the details for pinning an address depend on your HTTP client or egress proxy.

function deliverCallback(inputUrl):
    url = parseWithSupportedUrlLibrary(inputUrl)
    require url.scheme is allowed
    require url.hasNoUserInfo
    require url.port is allowed

    if hostnameAllowlistIsConfigured:
        require hostnameMatchesAllowlistExactly(url.hostname)

    addresses = resolveAAndAAAA(url.hostname)
    require addresses is not empty
    require every address is permitted by IP policy

    response = makeRequest(
        url,
        connectOnlyTo=validatedAddress(addresses),
        preserveTlsHostname=url.hostname,
        followRedirects=false,
        timeout=shortTimeout,
        maximumResponseBytes=smallLimit
    )

    if response is a redirect:
        require redirectCount is below limit
        nextUrl = resolveLocation(url, response.location)
        return deliverCallback(nextUrl)  // re-parse, re-resolve, and revalidate

    return handleExpectedResponse(response)

In production, make the network enforcement difficult to bypass: isolate callback delivery, restrict outbound routes, avoid accepting arbitrary proxy settings from request data, and log the decision and outcome without logging credentials or sensitive response bodies. Keep logs useful for investigation, but treat callback URLs and response data as potentially sensitive.

Common Mistakes

  • Blocking only localhost or one metadata IP. Attackers can use other prohibited ranges, address families, and representations. Use parsed IP classification and network controls.
  • Checking DNS once, then fetching by hostname. The HTTP client may resolve again. Bind the connection to a validated address or enforce policy in a controlled proxy.
  • Validating only the first DNS answer. A hostname can have multiple A or AAAA records. Apply policy to the complete answer set and fail closed when resolution is ambiguous or incomplete.
  • Trusting the initial URL when redirects are enabled. Every redirect is a new destination and needs the same checks.
  • Checking the URL but not the actual network path. Proxies, retries, connection reuse, and IPv6 can change where a request goes. Validate the behavior of the actual transport and restrict egress.
  • Assuming metadata protections eliminate SSRF. Provider controls reduce exposure but do not secure other internal services or make unrestricted outbound requests safe.

Best Practices

  • Prefer fixed destinations or exact host allowlists; support arbitrary hosts only when product requirements justify the added complexity.
  • Centralize URL parsing, DNS/IP policy, connection behavior, and redirect handling in one callback-delivery component.
  • Use established URL and IP libraries, and test IPv4, IPv6, mapped addresses, multiple DNS answers, redirects, and failures.
  • Enforce outbound restrictions at the network layer, including IPv6 and any proxy or alternate egress path.
  • Keep the callback worker isolated from sensitive internal services and credentials.
  • Review policy when changing HTTP clients, proxies, DNS configuration, cloud metadata settings, or supported URL schemes.

Conclusion

Accepting a user-supplied callback URL is not just a URL-validation problem; it is control over a server-side network request. Restrict destinations where possible, validate the addresses used for connections, re-check redirects, and back up application policy with egress controls. The safest design is one where a missed application check still cannot reach a sensitive network.

References