Open proxies and misconfigured relays
The three ways a proxy becomes open by accident, how scanners find it, what it costs you, and the bind, ACL and CONNECT settings that close it.
Key points
- A proxy is open when it accepts requests from sources you did not intend and will connect to destinations you did not intend; both halves are needed, and closing either one is enough.
- The most common accident is not a forward proxy at all, it is a reverse proxy whose catch-all
serverblock contains a variable inproxy_pass. - Bind address, ACL and CONNECT port policy are independent controls; each fails differently, and none substitutes for egress firewalling of the proxy host itself.
- Public "free proxy list" endpoints are unauthenticated intermediaries with full visibility of plaintext traffic and the ability to modify responses, and many are hosts whose owners did not consent.
A proxy is open when two conditions hold at once: it accepts requests from sources you did not intend, and it will connect onward to destinations you did not intend. Both halves are required, which is useful, because it means you have two independent places to close it. A proxy reachable from the whole internet but restricted to three upstream hosts is not an open proxy. A proxy that will connect anywhere but only listens on 127.0.0.1 is not one either. The dangerous configuration is the one where neither control was ever set, and that is almost always an accident rather than a decision.
Openness is not a property of forward proxies specifically. A reverse proxy that forwards to a destination derived from the request is a forward proxy wearing a different name, and it is the more common accident, because nobody audits a reverse proxy for a property they think only applies to Squid.
The three ways it happens by accident#
1. A forward proxy bound to 0.0.0.0#
Squid's http_port takes an optional address. Written as http_port 3128 with no address it listens on every interface on the host, including the public one on a cloud instance with a public IP. The intent was almost always "listen on the internal interface", and on a single-homed development machine the difference is invisible.
# Listens on every interface, including any public address on the host.
http_port 3128
# Listens only where you meant it to.
http_port 10.20.0.9:3128Binding is the cheapest control and the one most worth getting right, because it fails safe: a proxy that is not reachable cannot be abused regardless of what its ACLs say. It is also the control that a change of network topology can silently undo, for example when an instance gains a public IP or a container's port is published with -p 3128:3128 rather than -p 127.0.0.1:3128:3128.
2. A reverse proxy with a catch-all server block and a variable upstream#
This is the one that catches teams who have never deliberately run a forward proxy.
# Vulnerable. This is a fully functional open forward proxy.
server {
listen 80;
resolver 10.0.0.2;
location / {
proxy_pass http://$http_host$request_uri;
}
}nginx routes a request to the server block whose server_name matches the Host header. If none matches, it uses the default server for that listening socket, which is the first server block defined for that address and port unless another is marked default_server. So a block with no server_name at all becomes the catch-all for everything nobody else claimed. Combine that with $http_host in proxy_pass and the result is a proxy that fetches whatever host the client names, from inside your network, using your source address. The onward request is made with the original URI, unnormalised, because nginx passes the request URI as-is when proxy_pass contains variables. The related SSRF exposure is the same defect viewed from the application side.
3. CONNECT enabled without a port restriction#
CONNECT asks the proxy for a raw bidirectional TCP relay to an arbitrary host:port. Without a port restriction, that is a general-purpose TCP relay: port 25 for spam, 22 for SSH pivoting, 3306 or 5432 for database access, and the whole ephemeral range for anything else listening internally. Squid's shipped configuration encodes the standard answer with acl SSL_ports port 443 and http_access deny CONNECT !SSL_ports. Removing or overriding that line while debugging a service on port 8443 is a common way to lose it permanently. The correct fix is adding the specific port to SSL_ports, never widening Safe_ports, which governs ordinary requests rather than tunnels. The mechanics are in HTTP CONNECT tunnelling.
Consequences, in the order they usually arrive#
| Consequence | How it shows up | Timescale |
|---|---|---|
| Egress bandwidth and cloud data-transfer charges | An unexplained rise in outbound transfer on one instance, often before anything else is noticed | Hours to days |
| Credential stuffing and scraping traffic | High-volume POST traffic to third-party login endpoints, from your address | Days |
| Spam relay and DNSBL listing | Your address appears on public blocklists; your own outbound mail starts bouncing | Days to weeks, and slow to reverse |
| Abuse complaints and provider action | Abuse tickets from the destination's operator, then throttling or suspension by your hosting provider | Days to weeks |
| SSRF pivot into your own network | Requests to 169.254.169.254, to internal service names, or CONNECT to internal ports | Immediate, and the most serious |
The ordering matters for detection. The billing signal usually precedes the security signal, so an unexplained egress spike on a proxy host deserves an access-log review before it is written off as traffic growth. The last row is the one that turns a nuisance into an incident: an open proxy inside a network is a machine that will make requests on an attacker's behalf from a trusted position, which is exactly the primitive an external attacker otherwise has to find an application bug to obtain.
How open proxies are found at internet scale#
There is no obscurity budget. The IPv4 address space is small enough to sweep exhaustively, and it is swept continuously by research scanners, commercial attack-surface products and opportunistic actors alike. A newly exposed proxy is found because everything is found, not because anyone was looking for you.
The probing pattern is consistent enough to alert on:
- Port selection. Connections concentrate on the conventional proxy ports: 3128 (Squid's default), 8080, 8888, 1080 (SOCKS), and 80 or 443 on hosts already known to run HTTP.
- Absolute-form request targets. RFC 9112 section 3.2 reserves absolute-form (
GET http://example.com/ HTTP/1.1) for requests to a proxy. An origin server has no legitimate reason to receive one. Its appearance in your access log is close to a definitive signal that something is probing you as a proxy, and is the single best detection rule available. - A
Hostheader that is not yours. The probe names a third-party host it controls or one with a known response body, then checks whether the content came back or whether its own logs recorded a request from your address. CONNECTto a canary host and port. Success is inferred from a200status or from the tunnel actually carrying data.- Source diversity. Probes arrive from many unrelated networks over a short window, because lists of confirmed open proxies are traded and re-verified.
None of this requires special tooling to detect, and none of it needs to be reproduced to be defended against. Log the Host, log the request target form, and alert when either departs from your expected set.
Detection and hardening checklist#
| Misconfiguration | Symptom in logs | Fix |
|---|---|---|
http_port with no bind address | Squid access.log entries with source addresses outside your network ranges | http_port 10.20.0.9:3128, and confirm with ss -lntp that the listener is not on 0.0.0.0 |
| Container port published to all interfaces | Same, with the container's mapped port reachable externally | Publish as 127.0.0.1:3128:3128 or bind to the internal interface address |
http_access allow all, or an allow rule above the denies | Squid logs TCP_MISS/200 for domains you do not own | Restore first-match ordering: denies first, a specific allow, then http_access deny all as the final rule |
| CONNECT unrestricted | CONNECT host:25, CONNECT host:22 or high-numbered ports in access.log | acl SSL_ports port 443 plus http_access deny CONNECT !SSL_ports; add individual ports to SSL_ports only |
nginx catch-all with a variable proxy_pass | $host in the access log is a domain you do not serve; upstream errors naming external hosts | Remove the variable, or gate it behind an exact-match map allowlist |
| No default server block | Requests with an unknown Host land on whichever server block happens to be first | Add an explicit default_server returning 444 |
| Proxy host with unrestricted egress | Outbound connections from the proxy to ports 25, 22, 3306 | Egress security group or firewall on the proxy host permitting only 80 and 443 outbound |
| No authentication on an internal forward proxy | Any host on the internal network can use it, including a compromised workstation | acl authenticated proxy_auth REQUIRED plus an auth_param scheme |
| No connection or rate limits | One source address holding hundreds of concurrent connections | acl toomany maxconn 20 with http_access deny toomany; client_lifetime to cap tunnel duration |
The nginx default server block#
Every listening address should have an explicit default server that answers nothing.
server {
listen 80 default_server;
listen [::]:80 default_server;
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
ssl_reject_handshake on; # nginx 1.19.4 and later
return 444;
}return 444 is an nginx-specific code that closes the connection without sending a response, which is the correct answer to a request for a host you do not serve: it gives a scanner no fingerprint and costs nothing to serve. ssl_reject_handshake on (nginx 1.19.4 and later) rejects the TLS handshake outright for unmatched SNI, so the default block needs no certificate. Without it, nginx must present some certificate before it can read the Host header, which typically means leaking whichever certificate belongs to the first HTTPS server block.
Ordering in Squid ACLs#
http_access rules are evaluated top to bottom and the first match wins, which makes ordering a security property rather than a style preference.
acl SSL_ports port 443
acl Safe_ports port 80 443
acl localnet src 10.20.0.0/16
acl to_localhost dst 127.0.0.0/8 0.0.0.0/32 ::1
acl authenticated proxy_auth REQUIRED
acl toomany maxconn 20
http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports
http_access deny to_localhost
http_access deny toomany
http_access allow localhost manager
http_access deny manager
http_access allow localnet authenticated
http_access deny allSquid's shipped configuration already ends with http_access deny all and denies manager from anywhere but localhost. The classic regression is adding a broad allow above the denies while debugging and never moving it back. Verify the effective policy from a machine outside the intended source range rather than by reading the file.
Why free public proxy lists are dangerous to use#
Readers frequently arrive here looking for a proxy to use rather than one to secure. The relevant facts about those endpoints, stated plainly:
- Plaintext traffic is fully visible to the operator. Anything over
http://can be read and recorded in full: URLs, headers, cookies, form fields, session tokens, API keys. - Responses can be modified. An intermediary carrying plaintext can inject script, advertising or malware into HTML, and can rewrite links or downloads. This is not hypothetical; it is the ordinary capability of any HTTP proxy.
- HTTPS narrows but does not eliminate the exposure. Through
CONNECT, the operator still learns every destination hostname (from the CONNECT authority and the TLS SNI), plus connection timing and byte volumes. If it presents its own certificate instead of tunnelling, browsers will warn, and the entire attack depends on the user dismissing that warning. See TLS interception for what a proxy with a trusted CA can do. - Downgrade is available to the operator. An intermediary can strip or rewrite redirects from
http://tohttps://on the initial navigation, so the user never reaches the encrypted version. - Credentials sent to the proxy itself are exposed. Proxy Basic authentication is base64, not encryption; over a plaintext proxy connection those credentials travel in the clear, as covered in proxy authentication.
- Many listed endpoints are hosts whose owners did not consent. They appear on lists because they are misconfigured or compromised. Using them means routing traffic through someone else's machine without authorisation, and it means your traffic may be traversing a host that is itself under an attacker's control.
- Availability and integrity are unmanaged. Endpoints appear and disappear, and there is no operator to hold accountable for interception, logging or resale of the traffic.
An operator with an actual need for an intermediary should run one, with authentication and an allowlist, or use a provider with a contract and a published policy. There is no configuration of a free public proxy that makes it trustworthy for authenticated traffic.
Failure modes#
403 Forbidden from your own proxy after tightening ACLs. Expected while Safe_ports and SSL_ports are being narrowed. Squid serves ERR_ACCESS_DENIED and logs TCP_DENIED/403; the log line names the ACL that matched, which is the fastest way to find the rule you need.
A scanner's request appearing in application logs with a foreign Host. The default server block is missing or is not marked default_server, so an unmatched Host fell through to a real application. Add the 444 block above.
Your outbound mail bouncing with a blocklist reference. Your address was listed while the proxy was open. Closing the proxy is necessary but not sufficient; delisting is a separate, slower process with each list operator, and the listing usually outlives the misconfiguration by some margin.
A rise in 502 from the proxy with upstream names you do not recognise. Someone is using the proxy for hosts that are refusing or timing out. Treat unrecognised upstream names in gateway errors as an exposure signal, not a capacity signal, and check against 502 vs 503 vs 504 before scaling anything.
Everything looks correct in the config file but the proxy is still reachable. Check the actual listener with ss -lntp and test from outside the intended network. Configuration files describe intent; the socket table describes reality, and container port publishing sits between the two.
Frequently asked questions#
What makes a proxy an "open proxy"?#
It accepts requests from sources it should not (typically anyone on the internet) and will connect onward to destinations it should not (typically anywhere). Both properties must hold for it to be abusable, so restricting either the source with a bind address and ACLs, or the destination with an allowlist, closes it.
Can a reverse proxy be an open proxy?#
Yes, and it is the more common accident. A server block that acts as the default for its listening address, combined with a proxy_pass containing a variable such as $http_host, forwards to whatever host the client names. That is a fully functional open forward proxy even though nothing in the configuration is named "forward proxy".
How do I check whether my proxy is open?#
Test from outside the intended source range, not from a machine already inside it. Confirm the listener address with ss -lntp, then send an absolute-form request and a CONNECT to a host you control and check whether your own destination logs a hit. Grep the access log for Host values and request targets outside your expected set.
What is a safe default CONNECT policy?#
Allow CONNECT only to the ports you have a stated reason for, normally 443 alone. In Squid that is acl SSL_ports port 443 plus http_access deny CONNECT !SSL_ports. Add specific extra ports to SSL_ports; never add ranges, and never widen Safe_ports instead, since that ACL governs ordinary requests rather than tunnels.
Why does nginx need a default server block returning 444?#
Because a request whose Host matches no server_name still has to be handled by something, and nginx picks the first block on that listening address. An explicit default_server returning 444 closes the connection with no response, which gives scanners nothing and stops unmatched hosts reaching a real application.
Are free public proxies safe to use?#
No. The operator sees all plaintext traffic in full, can modify responses, learns every destination hostname even over HTTPS, and is anonymous and unaccountable. Many listed endpoints are misconfigured or compromised machines whose owners never agreed to carry traffic, so using them is also unauthorised use of someone else's system.
Does adding authentication to my proxy make it safe?#
It closes the source half of the problem, so it is a substantial improvement, but it leaves the destination half open: an authenticated user, or anyone who obtains a credential, still reaches anywhere the proxy can route. Pair authentication with a destination allowlist, a CONNECT port restriction, and an egress firewall on the proxy host, as set out in the reverse proxy security checklist.
Primary sources#
Every normative claim on this page is checked against the specification or the vendor documentation listed here. Where behaviour is version dependent, the version is named in the text.
- Squid http_access directive
- Squid http_port directive
- Squid acl directive and ACL types
- nginx server_names and the default server
- nginx ngx_http_ssl_module, ssl_reject_handshake
- nginx ngx_http_proxy_module, proxy_pass
- RFC 9110 HTTP Semantics, section 9.3.6 CONNECT
- RFC 9112 HTTP/1.1, section 3.2 Request Target
Found something wrong, or behaviour that differs on your version? Report it with the version number and a primary source. Anything substantive is fixed in the page and logged on the corrections page. See editorial standards for how pages are researched, sourced and reviewed.