<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>aabidk.dev – Ai</title><link>https://aabidk.dev/tags/ai/</link><description>Recent content in Ai on aabidk.dev</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Mon, 12 Oct 2026 17:00:00 +0530</lastBuildDate><atom:link href="https://aabidk.dev/tags/ai/index.xml" rel="self" type="application/rss+xml"/><item><title>Bypassing iron-proxy's MCP Tool Allowlist With a Content-Type Header</title><link>https://aabidk.dev/blog/bypassing-iron-proxy-mcp-tool-allowlist-with-a-content-type-header/</link><pubDate>Mon, 12 Oct 2026 17:00:00 +0530</pubDate><guid>https://aabidk.dev/blog/bypassing-iron-proxy-mcp-tool-allowlist-with-a-content-type-header/</guid><description>
&lt;p&gt;&lt;a href="https://github.com/paradigmxyz/iron-proxy"target="_blank" rel="noopener"&gt;iron-proxy&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; is an egress proxy to manage traffic for untrusted workloads. It works as a MITM proxy, has a DNS server and can apply an allowlist on MCP tool calls.&lt;/p&gt;
&lt;p&gt;While trying to sandbox an agent with docker, I added iron-proxy as a container to route all the traffic through it. The idea was to have agent and MCP each in their own containers and isolated networks, both communicating through the proxy. The proxy was the only container having outbound internet access. Either all the workloads had to go through the proxy, which only allowed access to domains and MCP tools explicitly whitelisted, or were dropped.&lt;/p&gt;
&lt;p&gt;I discovered that iron-proxy only enforces the MCP tool call allowlist when the &lt;code&gt;Content-Type&lt;/code&gt; header matches &lt;code&gt;application/json&lt;/code&gt;. This means an agent can send a JSON-RPC body for a denied tool with a different &lt;code&gt;Content-Type&lt;/code&gt; header and it can be executed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; AI agents were used to test the behaviour across multiple MCP server implementations, content types and iron-proxy versions.&lt;/p&gt;
&lt;h3&gt;The Test Setup&lt;span class="hx:absolute hx:-mt-20" id="the-test-setup"&gt;&lt;/span&gt;
&lt;a href="#the-test-setup" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;For testing the bypass, I used the following setup:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The workload container: An isolated container, which only has proxy as a way out. In the real workload this was the agent container.&lt;/li&gt;
&lt;li&gt;The proxy container: The iron-proxy itself.&lt;/li&gt;
&lt;li&gt;The upstream container: A simple container with python script that logs the MCP tool calls it receives. In the real workload this was the MCP container.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure&gt;
&lt;img src="test-setup.png" title="The test setup: workload container, proxy container and upstream container" alt="" loading="lazy" /&gt;
&lt;figcaption&gt;The test setup: workload container, proxy container and upstream container&lt;/figcaption&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;The proxy config allowed only one host and one tool:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;proxy.yaml&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;dns&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;enabled&lt;/span&gt;: &lt;span style="color:#fab387"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;proxy&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;http_listen&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#34;:8080&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;tls&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;mode&lt;/span&gt;: sni-only
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;transforms&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#cba6f7"&gt;name&lt;/span&gt;: allowlist
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;config&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;domains&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#a6e3a1"&gt;&amp;#34;mcp.test&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;mcp&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;error&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;code&lt;/span&gt;: -&lt;span style="color:#fab387"&gt;32001&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;message&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#34;blocked by iron-proxy policy&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;servers&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#cba6f7"&gt;name&lt;/span&gt;: demo
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;rules&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#cba6f7"&gt;host&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#34;mcp.test&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;paths&lt;/span&gt;: [&lt;span style="color:#a6e3a1"&gt;&amp;#34;/mcp&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;/mcp/*&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;tools&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#cba6f7"&gt;name&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#34;allowed_tool&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;mcp_gateway&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;routes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#cba6f7"&gt;name&lt;/span&gt;: demo
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;rules&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#cba6f7"&gt;host&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#34;mcp.test&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;paths&lt;/span&gt;: [&lt;span style="color:#a6e3a1"&gt;&amp;#34;/mcp&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;/mcp/*&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;upstream&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#34;http://upstream:8901/mcp&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;log&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;level&lt;/span&gt;: info&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Per this config, only &lt;code&gt;allowed_tool&lt;/code&gt; should be executed when called. Any other tool call should never reach the upstream container, the proxy should record a call with &lt;code&gt;decision:deny&lt;/code&gt; and reason as &lt;code&gt;tool_not_allowed&lt;/code&gt;. The following call works as expected and we see a log entry for it:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# control: denied, upstream untouched&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -s -x http://127.0.0.1:8080 http://mcp.test/mcp &lt;span style="color:#89b4fa"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#89b4fa"&gt;&lt;/span&gt; -H &lt;span style="color:#a6e3a1"&gt;&amp;#39;Content-Type: application/json&amp;#39;&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#89b4fa"&gt;&lt;/span&gt; -d &lt;span style="color:#a6e3a1"&gt;&amp;#39;{&amp;#34;jsonrpc&amp;#34;:&amp;#34;2.0&amp;#34;,&amp;#34;id&amp;#34;:1,&amp;#34;method&amp;#34;:&amp;#34;tools/call&amp;#34;,&amp;#34;params&amp;#34;:{&amp;#34;name&amp;#34;:&amp;#34;denied_tool&amp;#34;,&amp;#34;arguments&amp;#34;:{}}}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# response: blocked by the proxy&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;{&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;jsonrpc&amp;#34;&lt;/span&gt;:&lt;span style="color:#a6e3a1"&gt;&amp;#34;2.0&amp;#34;&lt;/span&gt;,&lt;span style="color:#a6e3a1"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;:1,&lt;span style="color:#a6e3a1"&gt;&amp;#34;error&amp;#34;&lt;/span&gt;:&lt;span style="color:#89dceb;font-weight:bold"&gt;{&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;code&amp;#34;&lt;/span&gt;:-32001,&lt;span style="color:#a6e3a1"&gt;&amp;#34;message&amp;#34;&lt;/span&gt;:&lt;span style="color:#a6e3a1"&gt;&amp;#34;blocked by iron-proxy policy&amp;#34;&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;}}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;But if we replace the header with &lt;code&gt;Content-Type: text/plain&lt;/code&gt;:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -s -x http://127.0.0.1:8080 http://mcp.test/mcp &lt;span style="color:#89b4fa"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#89b4fa"&gt;&lt;/span&gt; -H &lt;span style="color:#a6e3a1"&gt;&amp;#39;Content-Type: text/plain&amp;#39;&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#89b4fa"&gt;&lt;/span&gt; -d &lt;span style="color:#a6e3a1"&gt;&amp;#39;{&amp;#34;jsonrpc&amp;#34;:&amp;#34;2.0&amp;#34;,&amp;#34;id&amp;#34;:2,&amp;#34;method&amp;#34;:&amp;#34;tools/call&amp;#34;,&amp;#34;params&amp;#34;:{&amp;#34;name&amp;#34;:&amp;#34;denied_tool&amp;#34;,&amp;#34;arguments&amp;#34;:{}}}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# response: answered by the upstream&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;{&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;jsonrpc&amp;#34;&lt;/span&gt;:&lt;span style="color:#a6e3a1"&gt;&amp;#34;2.0&amp;#34;&lt;/span&gt;,&lt;span style="color:#a6e3a1"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;:0,&lt;span style="color:#a6e3a1"&gt;&amp;#34;result&amp;#34;&lt;/span&gt;:&lt;span style="color:#89dceb;font-weight:bold"&gt;{&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;tools&amp;#34;&lt;/span&gt;:&lt;span style="color:#89dceb;font-weight:bold"&gt;[]}}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The upstream answers instead of the proxy. If this were a real MCP server, the denied tool would have run.&lt;/p&gt;
&lt;p&gt;The proxy keeps audit logs for all requests. The log records three different shapes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;An allowed call writes a request record and a response record.&lt;/li&gt;
&lt;li&gt;A denied call writes a request record only.&lt;/li&gt;
&lt;li&gt;A bypassed call writes a response record only, because no policy ran on the request. The decision is recorded as &lt;code&gt;allow&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The missing request record is the only way to tell a bypassed call from a truly allowed call, as the decision for both is recorded as &lt;code&gt;allow&lt;/code&gt; in the logs.&lt;/p&gt;
&lt;h3&gt;The Root Cause&lt;span class="hx:absolute hx:-mt-20" id="the-root-cause"&gt;&lt;/span&gt;
&lt;a href="#the-root-cause" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;From the iron-proxy source, the interceptor only inspected a POST when the declared media type parses as &lt;code&gt;application/json&lt;/code&gt;:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;internal/mcp/intercept.go&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;// We only inspect application/json POST requests. GET listeners and other&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;// shapes pass through; their responses are still wrapped if they arrive&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;// over text/event-stream so the listener stream is filtered.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; req.Method &lt;span style="color:#89dceb;font-weight:bold"&gt;!=&lt;/span&gt; http.MethodPost {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; &lt;span style="color:#fab387"&gt;nil&lt;/span&gt;, &lt;span style="color:#fab387"&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; !&lt;span style="color:#89b4fa"&gt;mediaTypeIs&lt;/span&gt;(req.Header.&lt;span style="color:#89b4fa"&gt;Get&lt;/span&gt;(&lt;span style="color:#a6e3a1"&gt;&amp;#34;Content-Type&amp;#34;&lt;/span&gt;), mediaTypeJSON) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; &lt;span style="color:#fab387"&gt;nil&lt;/span&gt;, &lt;span style="color:#fab387"&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;code&gt;mediaTypeIs&lt;/code&gt; parses the header with Go&amp;rsquo;s &lt;code&gt;mime.ParseMediaType&lt;/code&gt; and compares the result to &lt;code&gt;application/json&lt;/code&gt;. If the parse fails, the function returns false and the caller treats that as not being an MCP request. So the request is forwarded without any policy check.&lt;/p&gt;
&lt;p&gt;Every header value that fails this check therefore skips the allowlist check:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;Content-Type&lt;/code&gt; sent&lt;/th&gt;
&lt;th&gt;Proxy&lt;/th&gt;
&lt;th&gt;Upstream&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;application/json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rejected&lt;/td&gt;
&lt;td&gt;untouched&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;application/json; charset=utf-8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rejected&lt;/td&gt;
&lt;td&gt;untouched&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Application/JSON&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rejected&lt;/td&gt;
&lt;td&gt;untouched&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;application/json;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rejected&lt;/td&gt;
&lt;td&gt;untouched&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;text/plain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;forwarded&lt;/td&gt;
&lt;td&gt;logged by the upstream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;application/x-www-form-urlencoded&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;forwarded&lt;/td&gt;
&lt;td&gt;logged by the upstream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;no &lt;code&gt;Content-Type&lt;/code&gt; at all&lt;/td&gt;
&lt;td&gt;forwarded&lt;/td&gt;
&lt;td&gt;logged by the upstream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;text/plain, application/json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;forwarded&lt;/td&gt;
&lt;td&gt;logged by the upstream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;application/json, text/plain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;forwarded&lt;/td&gt;
&lt;td&gt;logged by the upstream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;duplicate &lt;code&gt;Content-Type&lt;/code&gt; headers&lt;/td&gt;
&lt;td&gt;forwarded&lt;/td&gt;
&lt;td&gt;logged by the upstream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;application/jsonx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;forwarded&lt;/td&gt;
&lt;td&gt;logged by the upstream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;application/json-patch+json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;forwarded&lt;/td&gt;
&lt;td&gt;logged by the upstream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;application/json; charset=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;forwarded&lt;/td&gt;
&lt;td&gt;logged by the upstream&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Out of these 13 cases, 4 were rejected and 9 were forwarded.&lt;/p&gt;
&lt;h3&gt;Limitations&lt;span class="hx:absolute hx:-mt-20" id="limitations"&gt;&lt;/span&gt;
&lt;a href="#limitations" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;This bypass in itself is not a full exploit. The other half depends on the MCP server checking for content type headers (the MCP server I originally encountered this with did not). I tested 4 MCP servers behind the same proxy config, each exposing a tool not on the allowlist: the official Python SDK (&lt;code&gt;mcp&lt;/code&gt; &lt;code&gt;2.2.0&lt;/code&gt; and &lt;code&gt;1.30.0&lt;/code&gt;), the official TypeScript SDK (&lt;code&gt;1.30.0&lt;/code&gt;) and &lt;code&gt;mark3labs/mcp-go&lt;/code&gt; (&lt;code&gt;1.1.0&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;The setup is the same as above, and the execution is confirmed when a tool handler prints the call in the upstream log, because the HTTP response alone can be misleading: a server can accept the body and still answer with a JSON-RPC error.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Server&lt;/th&gt;
&lt;th&gt;&lt;code&gt;text/plain&lt;/code&gt;, form-urlencoded, none&lt;/th&gt;
&lt;th&gt;&lt;code&gt;application/json, text/plain&lt;/code&gt; and &lt;code&gt;application/json; charset=&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;python &lt;code&gt;mcp&lt;/code&gt; 2.2.0&lt;/td&gt;
&lt;td&gt;&lt;code&gt;400&lt;/code&gt;, not executed&lt;/td&gt;
&lt;td&gt;executed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;python &lt;code&gt;mcp&lt;/code&gt; 1.30.0&lt;/td&gt;
&lt;td&gt;&lt;code&gt;400&lt;/code&gt;, not executed&lt;/td&gt;
&lt;td&gt;executed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript SDK 1.30.0&lt;/td&gt;
&lt;td&gt;&lt;code&gt;415&lt;/code&gt;, not executed&lt;/td&gt;
&lt;td&gt;&lt;code&gt;415&lt;/code&gt;, not executed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mark3labs/mcp-go&lt;/code&gt; 1.1.0&lt;/td&gt;
&lt;td&gt;&lt;code&gt;400&lt;/code&gt;, not executed&lt;/td&gt;
&lt;td&gt;&lt;code&gt;400&lt;/code&gt;, not executed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Two header values did work: &lt;code&gt;application/json, text/plain&lt;/code&gt; and &lt;code&gt;application/json; charset=&lt;/code&gt;. Go does not parse either of them. It rejects the first because a comma is not valid inside a media type, and the second because the &lt;code&gt;charset&lt;/code&gt; parameter has no value.&lt;/p&gt;
&lt;p&gt;The Python SDK accepts both values. It validates the header in two different places but neither check blocks our call. The first check runs for every POST and compares a prefix:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;mcp/server/transport_security.py&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; content_type &lt;span style="color:#89dceb;font-weight:bold"&gt;is&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;not&lt;/span&gt; &lt;span style="color:#fab387"&gt;None&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;and&lt;/span&gt; content_type&lt;span style="color:#89dceb;font-weight:bold"&gt;.&lt;/span&gt;lower()&lt;span style="color:#89dceb;font-weight:bold"&gt;.&lt;/span&gt;startswith(&lt;span style="color:#a6e3a1"&gt;&amp;#34;application/json&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;A value that fails it returns &lt;code&gt;400&lt;/code&gt; with &lt;code&gt;Invalid Content-Type header&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The second check looks for &lt;code&gt;application/json&lt;/code&gt; among the comma-separated parts:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;mcp/server/streamable_http.py&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;content_type_parts &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; [part&lt;span style="color:#89dceb;font-weight:bold"&gt;.&lt;/span&gt;strip() &lt;span style="color:#cba6f7"&gt;for&lt;/span&gt; part &lt;span style="color:#89dceb;font-weight:bold"&gt;in&lt;/span&gt; content_type&lt;span style="color:#89dceb;font-weight:bold"&gt;.&lt;/span&gt;split(&lt;span style="color:#a6e3a1"&gt;&amp;#34;;&amp;#34;&lt;/span&gt;)[&lt;span style="color:#fab387"&gt;0&lt;/span&gt;]&lt;span style="color:#89dceb;font-weight:bold"&gt;.&lt;/span&gt;split(&lt;span style="color:#a6e3a1"&gt;&amp;#34;,&amp;#34;&lt;/span&gt;)]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; &lt;span style="color:#89dceb"&gt;any&lt;/span&gt;(part &lt;span style="color:#89dceb;font-weight:bold"&gt;==&lt;/span&gt; CONTENT_TYPE_JSON &lt;span style="color:#cba6f7"&gt;for&lt;/span&gt; part &lt;span style="color:#89dceb;font-weight:bold"&gt;in&lt;/span&gt; content_type_parts)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;A value that fails this one returns &lt;code&gt;415 Unsupported Media Type&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Both of our values pass both checks. &lt;code&gt;application/json, text/plain&lt;/code&gt; passes the first on its prefix, and passes the second because the split on &lt;code&gt;,&lt;/code&gt; produces &lt;code&gt;[&amp;quot;application/json&amp;quot;, &amp;quot;text/plain&amp;quot;]&lt;/code&gt;, where one part matches. &lt;code&gt;application/json; charset=&lt;/code&gt; passes the first on its prefix, and passes the second because everything before the &lt;code&gt;;&lt;/code&gt; is exactly &lt;code&gt;application/json&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Possible Exploits&lt;span class="hx:absolute hx:-mt-20" id="possible-exploits"&gt;&lt;/span&gt;
&lt;a href="#possible-exploits" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;h4&gt;A local, trusted MCP server&lt;span class="hx:absolute hx:-mt-20" id="a-local-trusted-mcp-server"&gt;&lt;/span&gt;
&lt;a href="#a-local-trusted-mcp-server" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;Let&amp;rsquo;s say we have a local, trusted MCP server for database, running on the other side of the proxy as the agent. Assume tools like &lt;code&gt;list_tables&lt;/code&gt;, &lt;code&gt;describe_table&lt;/code&gt;, &lt;code&gt;create_table&lt;/code&gt;, &lt;code&gt;drop_table&lt;/code&gt;, etc and that the agent can interact with the database through MCP only. We want to deny access to the destructive tool: &lt;code&gt;drop_table&lt;/code&gt;. With this bypass, the agent can access the &lt;code&gt;drop_table&lt;/code&gt; tool by simply changing the &lt;code&gt;Content-Type&lt;/code&gt; header.&lt;/p&gt;
&lt;h4&gt;A local / remote server that needs authentication&lt;span class="hx:absolute hx:-mt-20" id="a-local--remote-server-that-needs-authentication"&gt;&lt;/span&gt;
&lt;a href="#a-local--remote-server-that-needs-authentication" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;iron-proxy supports injecting credentials into the calls. For MCP servers that need authentication, the gateway attaches route credentials to all the calls it forwards. Let&amp;rsquo;s say we have the proxy config like this:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;proxy.yaml&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;mcp_gateway&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;routes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#cba6f7"&gt;name&lt;/span&gt;: demo
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;rules&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#cba6f7"&gt;host&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#34;mcp.test&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;paths&lt;/span&gt;: [&lt;span style="color:#a6e3a1"&gt;&amp;#34;/mcp&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;/mcp/*&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;upstream&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#34;http://upstream:8901/mcp&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;credentials&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#cba6f7"&gt;source&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt;: env
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;var&lt;/span&gt;: UPSTREAM_TOKEN
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;inject&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;header&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#34;x-upstream-token&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;require&lt;/span&gt;: &lt;span style="color:#fab387"&gt;true&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The bypassed call goes through the same gateway, and reaches the upstream carrying the real token. Note that the agent itself does not hold this credential and this can only be injected by the proxy. The audit line records &lt;code&gt;action: &amp;quot;allow&amp;quot;&lt;/code&gt; and &lt;code&gt;credentials_injected: [&amp;quot;UPSTREAM_TOKEN:header:X-Upstream-Token&amp;quot;]&lt;/code&gt; on a request that has no request record. So a denied tool that needs authentication can still run.&lt;/p&gt;
&lt;h4&gt;Bypassing argument matching&lt;span class="hx:absolute hx:-mt-20" id="bypassing-argument-matching"&gt;&lt;/span&gt;
&lt;a href="#bypassing-argument-matching" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;The proxy also supports limiting the tool calls conditionally, from the docs&amp;rsquo; &lt;a href="https://docs.iron.sh/policies/mcp-interception#argument-matchers"target="_blank" rel="noopener"&gt;own example&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;proxy.yaml&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;tools&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#cba6f7"&gt;name&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#34;allowed_tool&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#cba6f7"&gt;name&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#34;create_issue&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;when&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#cba6f7"&gt;path&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#34;owner&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;equals&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#34;ironsh&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;This should allow the &lt;code&gt;create_issue&lt;/code&gt; tool only when the &lt;code&gt;owner&lt;/code&gt; is &lt;code&gt;ironsh&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Testing the same with different &lt;code&gt;Content-Type&lt;/code&gt; values gives the following results:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Call&lt;/th&gt;
&lt;th&gt;&lt;code&gt;Content-Type&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Proxy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;create_issue&lt;/code&gt; &lt;code&gt;owner: ironsh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;application/json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;forwarded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;create_issue&lt;/code&gt; &lt;code&gt;owner: attacker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;application/json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-32001&lt;/code&gt;, upstream untouched&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;create_issue&lt;/code&gt; &lt;code&gt;owner: attacker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;text/plain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;forwarded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;create_issue&lt;/code&gt; &lt;code&gt;owner: attacker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;form-urlencoded&lt;/td&gt;
&lt;td&gt;forwarded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;create_issue&lt;/code&gt; &lt;code&gt;owner: attacker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;forwarded&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;So the argument matching can be bypassed the same way as the tool calls.&lt;/p&gt;
&lt;h3&gt;Reporting and Fix&lt;span class="hx:absolute hx:-mt-20" id="reporting-and-fix"&gt;&lt;/span&gt;
&lt;a href="#reporting-and-fix" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;I reported the issue privately to the vendor, and they validated and fixed it promptly in &lt;code&gt;v0.50.0&lt;/code&gt; (&lt;a href="https://github.com/paradigmxyz/iron-proxy/pull/254"target="_blank" rel="noopener"&gt;fix(mcp): handle POST requests consistently, PR #254&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;). The conditional check was removed, and every POST request that matches the MCP server is now inspected, so the check does not depend on the header anymore. The same 13 header shapes are now all rejected by the proxy and nothing reaches the upstream MCP.&lt;/p&gt;
&lt;h3&gt;Timeline&lt;span class="hx:absolute hx:-mt-20" id="timeline"&gt;&lt;/span&gt;
&lt;a href="#timeline" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;18 September 2026&lt;/strong&gt;: reported the issue. The fix was merged the same day in &lt;a href="https://github.com/paradigmxyz/iron-proxy/pull/254"target="_blank" rel="noopener"&gt;PR #254&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; and &lt;code&gt;v0.50.0&lt;/code&gt; was published hours later. I re-ran the tests against the new version and the bypass was gone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;9 October 2026&lt;/strong&gt;: &lt;a href="https://github.com/paradigmxyz/iron-proxy/security/advisories/GHSA-vfgh-6jv3-4348"target="_blank" rel="noopener"&gt;advisory published&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;, listing &lt;code&gt;0.23.0&lt;/code&gt; through &lt;code&gt;0.49.0&lt;/code&gt; as affected and &lt;code&gt;0.50.0&lt;/code&gt; as the patched version.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;This was my first security report. I admit I refreshed the advisory page more than once.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If you have any feedback or suggestions, feel free to reach out at &lt;a href="mailto:reply@aabidk.dev"&gt;reply@aabidk.dev&lt;/a&gt;&lt;/p&gt;</description></item></channel></rss>