A server that accepts a hostname from a user can make a careful security decision and still connect somewhere it never intended. The common mistake is validating the hostname first, then assuming that the later network connection will reach the same kind of destination.
Hostnames are names, not network locations. DNS turns a hostname into one or more IP addresses, and those answers can change. If your security rule says “public destinations only,” checking the text of the hostname is not enough. The connection must also be constrained to an address that satisfies that rule.
This matters in features such as webhook testing, URL previews, importers, document fetchers, and callback verification. If an attacker can influence the destination, a weak check may let the application reach internal services that are not directly exposed to that attacker.
This article explains the defensive mental model: resolve the destination, validate the actual addresses, keep the validation tied to the connection, and repeat the decision whenever a redirect or other step selects a new destination.
A hostname is an instruction to resolve, not proof of location
Suppose an application accepts a URL and intends to fetch only resources on the public Internet. A first implementation might reject obvious internal-looking hostnames and accept everything else:
https://files.example.test/report
|
v
hostname looks acceptable
|
v
HTTP client connectsThe missing step is DNS resolution. The HTTP client does not send packets to the hostname string. It resolves that name to an address and connects to the resulting destination.
A hostname that looks harmless can resolve to an address your policy should reject. It can also have several address records, some acceptable and some not. DNS answers can change over time, so a result observed during validation may differ from the result used later by the networking library.
The security decision therefore belongs closer to the actual connection:
hostname
|
v
resolve
|
v
validate every candidate address
|
v
connect using an approved resultThe important invariant is simple: the address that receives the connection must satisfy the destination policy that authorized the request.
Define the network boundary before writing the check
“Block private IPs” sounds precise, but a production destination policy usually needs more thought. The application may need to exclude loopback addresses, link-local ranges, private-use networks, local infrastructure, or other destinations that are reachable from the workload but are not intended for user-directed traffic.
The exact policy depends on the environment. A service that is allowed to fetch from a fixed partner network has a different boundary from a general URL previewer. An internal administration tool may intentionally reach private services, while a public webhook tester usually should not gain that authority from arbitrary user input.
Start by stating what the feature is allowed to reach. For example:
allowed: public HTTPS services
not allowed: loopback, link-local, private networks,
internal service ranges, management networksThat policy is more useful than a list copied from a generic security guide because it reflects the application’s real trust boundary. Network teams may also have organization-specific ranges that are sensitive even though they are not special-purpose addresses on the public Internet.
Treat both IPv4 and IPv6 as part of the same decision. A policy that carefully classifies IPv4 but ignores IPv6 leaves the network boundary dependent on which address family the resolver or client chooses.
Validate resolved addresses, not just the original string
The smallest useful defensive flow has four steps:
- Parse the URL with a well-tested URL parser and enforce the schemes the feature actually needs.
- Resolve the hostname using the application’s normal trusted resolver path.
- Classify every returned address against the destination policy.
- Connect only if the address used for the connection is one that passed that policy.
The third step deserves emphasis. A hostname can return multiple addresses. Accepting the hostname because one address is public while another is forbidden creates ambiguity about which destination the client will choose.
A conservative design rejects the request if any candidate address falls outside the allowed boundary, unless the networking layer gives you a reliable way to select and connect only to a specific validated address. The right choice depends on the client library and availability requirements, but the security property must remain clear: an unvalidated candidate must not become the eventual peer.
Pseudocode makes the relationship easier to see:
url = parse(input)
require url.scheme == "https"
addresses = resolve(url.hostname)
require addresses is not empty
require every address satisfies destination_policy
connect using a validated addressThis is a teaching model, not a drop-in networking implementation. Real clients also handle proxies, retries, IPv4 and IPv6 selection, TLS, connection pools, and redirects. Each of those can affect which peer receives the request.
Avoid a gap between checking and connecting
A subtle failure appears when validation and connection perform separate DNS lookups:
validation lookup -> acceptable address
time passes
connection lookup -> different addressThis is a check-then-use problem. The application authorized one resolution result, but the HTTP client later resolved the hostname again and may use a different result. An attacker who controls DNS for the hostname may be able to influence that change. This family of problems is often described as DNS rebinding when a name is made to resolve to different addresses so that an earlier trust decision no longer matches the later destination.
The defensive goal is not to make DNS immutable. It is to keep authorization tied to what is actually used.
Where the networking stack allows it, resolve and validate the addresses, then make the connection using that validated result while preserving the original hostname for protocol semantics that need it, such as TLS server-name verification. Do not disable certificate or hostname verification to make this work. The TLS identity check and the network-destination check answer different questions and both may be necessary.
If your HTTP library hides resolution and does not let you bind the connection to a validated result, do not pretend an earlier lookup gives a strong guarantee. Prefer a library or architecture that exposes the needed control, or enforce the destination boundary at another layer such as an outbound proxy or network policy.
Revalidate every new destination
Redirects create a second common gap. The original URL may resolve to an allowed address and then return a redirect to a different hostname.
If the client follows that redirect automatically, the security decision made for the first destination says nothing about the second one:
approved.example
|
v
302 redirect
|
v
new destination -> must be evaluated againFor user-influenced fetches, either disable automatic redirects or intercept each redirect and apply the full destination policy again: parse the new URL, check the scheme, resolve its hostname, validate the resulting addresses, and connect only to an approved destination.
Apply the same reasoning to any feature that can select another network peer after the initial check. A retry that chooses a different resolved address, a proxy configuration, or application logic that follows a URL found inside a document can create a new destination decision.
The mental model is more reliable than memorizing special cases: when the peer can change, authorization must follow the change.
Keep TLS verification separate from address authorization
Developers sometimes run into a practical issue after deciding to connect to a validated IP address: the TLS certificate is issued for the hostname, not for the numeric address. The wrong response is to disable TLS verification.
There are two independent questions:
- Is this network address an allowed place for the application to connect?
- Does the TLS endpoint prove that it represents the hostname the application intended to contact?
Destination validation answers the first. Normal TLS hostname verification answers the second. Passing one does not replace the other.
A capable HTTP or TLS stack can connect to a selected address while still using the original hostname for Server Name Indication and certificate identity verification. The exact API is platform-specific, so use the supported mechanism in your networking library rather than building custom TLS handling.
This separation also explains why a valid certificate does not make an internal destination acceptable. A service can have perfectly valid TLS and still be outside the authority you meant to give a user-controlled fetcher.
Use network controls as a backstop
Application validation is valuable because it understands the user’s requested destination and can reject bad requests early. It should not be the only barrier when a workload has access to sensitive internal networks.
Outbound network controls can reduce the damage from parser mistakes, resolver surprises, library behavior, or a missed code path. For example, an egress proxy or network policy can prevent the workload from reaching management or internal service ranges even if application validation fails.
The two controls solve different parts of the problem:
application validation -> decides whether this request is allowed
network restriction -> limits where the workload can go at allFor a low-risk service with access only to a narrow, non-sensitive network, strong network isolation may make elaborate application-side address logic less valuable. For an Internet-facing feature running in a network with sensitive reachable services, using both layers is easier to justify.
Neither layer makes arbitrary remote content trustworthy. A permitted public server can still return malicious, oversized, or unexpected content. Content validation, response-size limits, timeouts, and isolated processing remain separate decisions.
Common implementations that look safer than they are
Checking only whether the hostname string contains localhost or resembles an IP address is weak because DNS can map an ordinary-looking name to a forbidden address.
Resolving once during validation and letting the HTTP client resolve again later creates a gap between the checked destination and the used destination.
Checking only the first DNS answer is also fragile. Resolvers may return several IPv4 or IPv6 addresses, and the networking stack may choose a different candidate when a connection fails or when address-family preference changes.
Allowing redirects without revalidation moves the request to a destination that never passed the original policy.
Disabling TLS verification to support a custom connection path trades one security problem for another. Keep certificate identity verification intact.
Finally, maintaining a generic blocklist without understanding the workload’s real network reach can miss organization-specific sensitive destinations. The policy should be derived from the authority the feature actually needs.
Verify the control with boundary-focused tests
Tests should exercise the decision points rather than only a few example hostnames. Confirm that the application rejects addresses from every network class your policy forbids, across both IPv4 and IPv6. Confirm that a hostname returning a mixture of allowed and forbidden candidates cannot cause a connection to the forbidden one.
Test redirects to a disallowed destination and verify that the second request is never sent. If the client can retry across several resolved addresses, verify that retries remain limited to validated candidates.
Also test failure behavior. If DNS resolution fails, returns no usable address, or produces a result the classifier cannot understand, a user-directed fetch should normally fail closed rather than bypass the check. Log enough structured context to diagnose the rejection without placing credentials or sensitive URL data into logs.
Network-level tests add useful evidence. From the deployed workload, attempt connections to representative forbidden destinations and verify that the egress layer blocks them independently of application code. This confirms that defense in depth exists in the environment where the application actually runs.
Keep the authorization attached to the peer
The main mistake in hostname-based security checks is authorizing a name and then forgetting that the network connection is made to an address chosen later. DNS is part of the path from user input to network authority, so its result belongs inside the security decision.
For any server-side feature that connects to user-influenced hostnames, write down the allowed network boundary, validate every resolved candidate, ensure the connection uses an approved result, and repeat the decision whenever the destination changes. Then use outbound network restrictions where the consequence of a mistake justifies another layer.
That design does not make remote fetching risk-free. It does make one critical property explicit and testable: user-controlled names cannot silently expand the set of network peers your application is authorized to reach.