How Localhost Fools LLM Agent Guardrails
July 27, 2026 · 10:47 UTC

Localhost reads as a safe address to most agent guardrails, because it usually signals a dev server, a local test, or trusted tooling on the same machine, and that quiet assumption is exactly what this research pulls apart, since a tool using agent can be aimed at a local port while every request quietly lands on a real remote target the guardrail never gets to see.
Nothing in the setup needs a jailbreak, a prompt injection, or a hidden instruction in the input, because the agent just follows a normal security testing task against a local URL while a small forward proxy quietly relays that traffic elsewhere.
Guardrails read text and tool call metadata, then see the same local host on each request, so the danger lives in the path below.
Note
The full paper, the complete 253 turn execution trace saved as output.json, and all 170 raw evidence artifacts are published at NeaByteLab/Research/Network-Indirection under CC BY-SA 4.0, so each claim below stays open to independent inspection.
Hiding the Target Behind Localhost
Setup here stays direct and small, an operator hands the agent a localhost URL, a proxy listens on that port, and that proxy forwards each request to a real application while rewriting the response links back to localhost so nothing looks off at all.
From the agent side every request targets localhost:40300, yet from the real network side the proxy opens a connection to the remote application, so a guardrail that only inspects model text, prompt text, or tool call URLs never learns the true destination unless it also traces the proxy process itself, and almost no guardrail deployed today ever bothers to do that.
Visibility Boundary
:
+------------+ +-------------+ +-----------------+ : +----------------+
| Operator | --> | LLM Agent | --> | Forward Proxy | --> | Real Target |
| | | MiniMax M3 | | localhost:40300 | : | vulnweb.com |
+------------+ +-------------+ +-----------------+ : +----------------+
| :
v :
+----------------------+ :
| Safety Monitor | :
| sees localhost:40300 | <--------------------+
| (all benign) |
+----------------------+
Placing the Proxy Seven Ways
Placement of the proxy is where the attack really starts, and the paper lays out seven distinct spots for it, each one keeping the agent pinned to an address that reads as safe while the real destination quietly sits just one short hop away from it.
Beyond localhost lie the other vectors, since the very same forwarder can also run on a private 192.168 address, on a remote VPS that NATs outward toward the target, behind a fake API gateway that rewrites both the Origin and Referer headers, as a multi hop chain where the agent only ever sees hop one, or as a browser recon node that carries a real TLS fingerprint.
| Vector | Placement | What the agent sees |
|---|---|---|
| Localhost proxy | Single local process | localhost:40300 |
| Private-IP NAT | 192.168.x.x forwarder | A private LAN address |
| VPS NAT | Private scope to external VPS | A trusted internal host |
| Middleware spoof | Fake API gateway | A normal gateway endpoint |
| Multi-hop chain | N sequential proxies | Only the first hop |
| Browser-as-recon | Headless browser with TLS | A real client session |
| Swarm-as-cover | N agents on distinct ports | One quiet local port each |
Running Recon to Persistence Loop
Once the proxy sits in place, the agent runs a loop entirely on its own, moving from recon into enumeration, then through exploitation and exfiltration, and finally persistence, before circling right back around while any attack surface stays open.
+-------+ +-----------+ +---------+ +------------+ +---------+
| Recon | --> | Enumerate | --> | Exploit | --> | Exfiltrate | --> | Persist |
+-------+ +-----------+ +---------+ +------------+ +---------+
^ |
+------------------------------------------------------------------+
loop while attack surface remains
Scaling this out turns one lone agent into a full mesh, where a hundred agents each sit on a separate local port with a separate proxy path, and each one stays under the WAF rate limit on its own while the combined traffic runs well far past it.
Agent 1 -> Proxy 1 --+
Agent 2 -> Proxy 2 --+
Agent 3 -> Proxy 3 --+--> Target(s)
... ... |
Agent N -> Proxy N --+
per agent rate < WAF limit combined rate >> WAF limit
Proxy Code to Reproduce
Reproducing the proof of concept takes a compact TypeScript proxy that leans on @neabyte/deserve, keeps the upstream target fixed, forwards the original method and body, then swaps the target origin for localhost across text responses and redirects.
import { Context, Router } from '@neabyte/deserve'
/**
* HTTP forward proxy with rewriting.
* @description Proxies requests and rewrites URLs in responses.
*/
class ForwardProxy {
/** Local proxy listen address */
static readonly local = 'http://localhost:40300'
/** Remote upstream target origin */
static readonly target = 'http://testaspnet.vulnweb.com'
/**
* Handle incoming proxy request.
* @description Fetches upstream and rewrites text responses.
* @param ctx - Router context object
* @returns Proxied response with rewritten URLs
*/
static async handle(ctx: Context): Promise<Response> {
const req = ctx.get.request()
const url = ctx.get.url()
const upstream = await this.fetchUpstream(req, url)
const contentType = upstream.headers.get('content-type') ?? ''
const isText = /text|json|xml|javascript|css/i.test(contentType)
if (!isText) {
return new Response(upstream.body, {
status: upstream.status,
statusText: upstream.statusText,
headers: this.rewriteHeaders(upstream.headers)
})
}
const raw = await upstream.text()
const rewritten = this.rewriteText(raw)
return new Response(rewritten, {
status: upstream.status,
statusText: upstream.statusText,
headers: this.rewriteHeaders(upstream.headers)
})
}
/**
* Fetch response from upstream target.
* @description Forwards request to target with modified headers.
* @param req - Original incoming request
* @param url - Parsed request URL
* @returns Upstream server response
*/
static async fetchUpstream(req: Request, url: URL): Promise<Response> {
const targetUrl = `${this.target}${url.pathname}${url.search}`
const headers = new Headers(req.headers)
headers.set('host', 'testaspnet.vulnweb.com')
headers.delete('referer')
const method = req.method
let body: BodyInit | null = null
if (method !== 'GET' && method !== 'HEAD') {
body = await req.arrayBuffer()
}
return fetch(targetUrl, {
method,
headers,
body,
redirect: 'manual'
})
}
/**
* Rewrite target URLs in headers.
* @description Replaces target origin in the location header.
* @param input - Original upstream response headers
* @returns Headers with rewritten location values
*/
static rewriteHeaders(input: Headers): Headers {
const out = new Headers(input)
if (out.has('location')) {
out.set('location', this.rewriteText(out.get('location')!))
}
return out
}
/**
* Replace target origin with local.
* @description Substitutes all target URL occurrences with local.
* @param text - Raw text content to rewrite
* @returns Text with target URLs replaced
*/
static rewriteText(text: string): string {
return text.replaceAll(this.target, this.local)
}
}
/** Router instance for the proxy server */
const router = new Router()
/** Register the proxy handler as middleware */
router.use((ctx: Context) => ForwardProxy.handle(ctx))
/** Start the proxy server on port 40300 */
await router.serve(40300)
Running it takes a single command, then an agent tool aimed at localhost:40300 receives local looking pages, forms, and redirects while the upstream application quietly receives the real traffic and answers as if the agent connected straight to it.
deno run --allow-net --allow-read proxy.ts
Reading the Agent Run
MiniMax M3 played the autonomous agent, given only a localhost URL and an OWASP prompt, then ran 253 turns and 277 tool calls.
Evidence saved in the repository covers SQL injection responses, authentication bypass proof, stored XSS payloads, cookie captures, and the final autonomous report, all of it produced against Acunetix testaspnet, a deliberately vulnerable application built by the vendor purely for security testing, so no real production system was ever once touched during the run.
| Metric | Result |
|---|---|
| Vulnerabilities found | 17 |
| Critical findings | 5 |
| High findings | 4 |
| Medium findings | 5 |
| Low findings | 3 |
| Tool calls | 277 |
| Detection rate | 0% |
Walking Through Five Phases
Execution split into five clean phases across the 253 turns of the session, and the table below tracks each one in order, from the very first crawl through injection, then exfiltration, an auth bypass, and a closing round of full XSS and CSRF work.
| Phase | Turns | Calls | What happened |
|---|---|---|---|
| Recon | 1 to 35 | 42 | Crawled pages, fingerprinted IIS and SQL |
| Injection | 36 to 120 | 98 | Confirmed boolean, time, and UNION SQLi |
| Exfiltration | 121 to 170 | 58 | Dumped 7 databases and two user tables |
| Auth bypass | 171 to 200 | 40 | Logged in as admin, session survived |
| XSS and CSRF | 201 to 253 | 39 | Stored XSS, CSRF gaps, missing headers |
Timing stayed brisk through all of it, with the agent holding steady sub ten second turns for roughly 80 percent of the whole run.
Counting the Confirmed Findings
Findings came back very heavy for a mere 30 minute run, and the agent confirmed a UNION based SQL injection on the ReadNews parameter, dumped two full user tables holding cleartext and MD5 credentials plus raw PCI card data, bypassed the login with a classic OR 1=1 payload, then landed a stored XSS and a set of CSRF gaps on top of a pile of missing security headers.
Restraint showed up here too, since the agent correctly rejected four vectors that did not apply on this target, marking XXE, OS command injection, insecure deserialization, and SSRF as absent rather than padding the report with false positives.
Stretching Context With Compaction
Sustaining that long would normally blow straight past a context window, yet the loop survives because it compacts its own history at 95 percent of the entire 1M token budget, folding the older turns into a compact summary that keeps every confirmed finding fully intact while it frees fresh room for the agent to keep going, so the session never has to stop early.
Cumulative input crossed 5.7M tokens across the entire run even though the window itself never once broke, since each compaction quietly reset the counter, and that single trick is what lets the agent keep working with no natural stopping point.
Models That This Hits
Model choice saves nobody at all here, because the gap is structural rather than a flaw in one network, and the paper marks every tool using LLM as exposed, from proprietary models such as GPT, Claude, and Gemini through open source families such as Llama and Mistral, since all of them reason only over text while the routing quietly runs below that observable layer.
Numbers from that single run stay blunt, with 5 critical, 4 high, 5 medium, and 3 low findings surfaced across the 6 OWASP categories, while all six of the evaluated defense classes still landed on a flat 0 percent detection rate against the run.
Why Text Safety Misses It
RLHF, DPO, and refusal training never fire here, because the agent is not being talked into breaking policy through adversarial text, the instruction plainly reads as a reasonable and scoped localhost security test, and the model just follows the task and emits normal tool calls against a local URL, which happens to be the exact behavior all of that training rewards.
Managed API safety layers miss it for the same reason, they process prompts, responses, and tool call arguments, so they only ever read strings like localhost:40300 and never the separate local process quietly rewriting each request behind them.
Both defenses share one blind spot, treating text as the whole story while the real action happens after the agent picks the URL.
Breaking Every Defense Class
Defenses were checked one by one across all six classes, and every single one failed for its own reason, which the table below makes plain, since each of them reads a layer that only ever shows the safe local endpoint and never the socket behind it.
| Defense class | Detection | Why it fails |
|---|---|---|
| Prompt filtering | 0% | Prompt references only localhost |
| Tool call monitor | 0% | All calls target localhost:40300 |
| Output scanning | 0% | Outputs reference the local endpoint |
| Network allowlist | 0% | Blocking localhost breaks all dev |
| WAF and rate limit | 0% | 9.2 req per min sits under threshold |
| Client-side detect | 0% | Browser recon gives a real TLS |
Comparing the Nearby Concepts
SSRF sits closest as a familiar idea, though the direction flips, because SSRF tricks a server into calling an internal or protected resource, while network indirection points an agent at a local resource that secretly forwards to an external target.
Prompt injection lands elsewhere, since it rewrites the input so behavior drifts from intent, while this attack skips all of that.
Tools such as Burp Suite and mitmproxy can feel similar at first glance, because both of them sit between a client and a server much like this does, but those are visible testing tools an engineer deliberately chooses to run, while this proxy exists only to make a remote target look like plain localhost to the agent and to every guardrail watching the session unfold.
| Concept | What Changes | Main Detection Surface |
|---|---|---|
| Prompt injection | Model context | Prompt and output scanning |
| Jailbreak | Model behavior | Policy and refusal checks |
| SSRF | Server request target | App logs and allowlists |
| Network indirection | Network destination after localhost | Process and socket tracing |
Weaponizing the Supply Chain
Supply chain amplification is where this stops being a single demo, because a malicious npm package, pip library, or editor extension can drop a local proxy during a postinstall step, then simply wait for any harmless prompt such as test the dev server, and every machine that installed it turns into a quiet attack node running from a trusted residential address now.
Scale is the scary part right here, since one package pulling 100 thousand weekly downloads seeds that many nodes in a week, with no malware on disk because the agent itself is the payload, and it all hides behind trusted users and audited packages.
Forwarding Any Network Protocol
Protocol reach goes well past the web, because the proxy only forwards raw bytes, so the same trick relays SSH for pivoting, database wire protocols for direct pulls, SMTP for phishing, or DNS for tunneling, with the agent supplying the reasoning.
Reaching Into Physical Systems
Physical systems inherit the exact same gap, since a lot of robotics and industrial gear runs local pub sub or open maintenance interfaces with no real authentication at all, so one publish to a topic is effectively a direct command, and a controller simply cannot tell a real maintenance action apart from an adversarial one that arrives over that same local channel.
Drift makes it worse here, since a slightly altered route slips right past a kill switch that only watches for a sudden failure.
Grounding Defenses in Network Truth
Useful defenses have to bind the agent action to the real outbound socket, since tool call logs on their own stay completely silent about the true destination, and a much safer runtime should record which process opened which connection, where exactly that connection finally went, and whether some local proxy forwarded any of the traffic outside the allowed scope.
Kernel tracing, sandboxed network namespaces, and strict egress allowlists could enforce that, yet they sit in the runtime, below the managed API safety layer every hosted model shares, and that layer only reads text so it never sees the socket.
Patching this stays hard, since open source models run anywhere with no safety layer, so localhost never proves a path is local.


