{"slug": "i-found-3-security-vulnerabilities-in-my-own-ai-agent-s-tool-access", "title": "I Found 3 Security Vulnerabilities in My Own AI Agent's Tool Access", "summary": "A developer who built GeoMart for OpenAI's WebMCP Challenge discovered three security vulnerabilities in the AI agent's tool access, including an unescaped innerHTML injection and an unprotected API endpoint. After fixing those, a retest by Google Antigravity revealed that the Origin header check could be bypassed by manually setting the header in a Node.js fetch call. The developer ultimately implemented Cloudflare Turnstile with a server-side secret to secure the human-approval boundary.", "body_md": "I built GeoMart for OpenAI's WebMCP Challenge: a storefront where a human fills in a live, unsubmitted \"site brief\" and an AI agent uses WebMCP tools to read it, score survey equipment against real physical constraints, and draft a quote the human has to approve. The hackathon's own rule is blunt: every core action has to be reachable only through a WebMCP tool, no REST route allowed to duplicate it.\n\nI thought I'd covered that. Then I asked Google Antigravity to try to break it, gave it the full source code, and it found three problems in about ten minutes.\n\nAntigravity's first pass found that agent-supplied text, the quote reasoning, the product IDs in the comparison tray, was going straight into the page via unescaped `innerHTML`\n\n. It also found something worse: `POST /api/quotes`\n\n, the endpoint that creates a quote request, had zero protection. A bare script from outside any browser could call it directly and submit a quote with no human involved at all.\n\nI fixed both. HTML-escaped every agent-supplied string before it hits the DOM. And added an `isTrusted`\n\ncheck on the submit button's click handler, so a script-simulated click (`element.click()`\n\n, `dispatchEvent`\n\n) can't fire it. Only a hardware-derived click passes.\n\nI redeployed, felt good about it, and asked for a retest.\n\nAntigravity replayed the exact attack that had worked before. This time it got a 403. Good. Then it tried something I hadn't thought to test: it manually set the `Origin`\n\nheader on a bare Node.js `fetch()`\n\ncall to match my site's own domain.\n\nIt got a 201. The quote submitted. No browser involved.\n\n`Origin`\n\nis not a secret. My repo is open source, required by the hackathon's own rules. Anyone reading `worker.ts`\n\ncan see exactly what value my check expects. A real browser can't forge that header, but Node's `fetch()`\n\nisn't a browser and has no such restriction. My \"fix\" only ever checked \"did you bother to copy the domain name,\" not \"are you actually a browser.\"\n\n**If your security check would still pass after you told the attacker exactly how it works, it was never a real check.** It was a filter for people who hadn't read your code yet.\n\nI considered a session-cookie approach next and talked myself out of it in about two minutes, because it fails the same test: a script with full HTTP access can just fetch the page first to grab the cookie, then replay it on the real request. No browser needed there either. Same hole, different shape.\n\nThe fix that worked needed something that never appears in the repo. I added Cloudflare Turnstile, verified server-side against a secret stored only in a Worker secret, never committed, never in the client bundle. I replayed the exact bypass afterward: correct Origin, no token, 403. Correct Origin, fake token, 403. That's the difference between \"a value you can read\" and \"a value you can't.\"\n\nWhile chasing the Origin bypass, the retest surfaced something unrelated: the database table for storing submitted quotes had never been created. I'd written the migration file back when I built the feature and never run it. Every previous test of the \"human clicks submit\" flow had only exercised the client-side draft step, `draft_quote_notes`\n\n, which never touches the server. The whole point of the human-in-the-loop design is that a human's click writes a row to the database. That write had been silently broken the entire time, and nothing caught it until a click produced a 500 error.\n\nThat whole chase left me with one test I now apply to every check I add: does this check depend on something the agent, or anyone reading your public repo, could already know or derive?\n\nOrigin headers: public, in your own URL.\n\nReferer headers: same problem.\n\nA session cookie with no server-side validation: derivable in two requests.\n\nA value only your server computes, that never leaves your server: actually a secret.\n\nAdversarial testing an AI-agent-facing app isn't optional if you're claiming a human-approval boundary. Ask an agent with full source access to try to defeat your own claims, specifically the ones you're proudest of, before a judge or an attacker does it for you.\n\nGeoMart is live at [https://geomart-webmcp.fpl-test.workers.dev](https://geomart-webmcp.fpl-test.workers.dev), the WebMCP tools are documented in the repo at [https://github.com/dannwaneri/geomart-webmcp](https://github.com/dannwaneri/geomart-webmcp), and the commit message on the security-hardening commit walks through every one of these fixes, including the ones that didn't work the first time.", "url": "https://wpnews.pro/news/i-found-3-security-vulnerabilities-in-my-own-ai-agent-s-tool-access", "canonical_source": "https://dev.to/dannwaneri/i-found-3-security-vulnerabilities-in-my-own-ai-agents-tool-access-75m", "published_at": "2026-09-02 13:19:03+00:00", "updated_at": "2026-09-02 13:54:49.940131+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-products", "developer-tools"], "entities": ["OpenAI", "Google Antigravity", "Cloudflare Turnstile", "GeoMart", "WebMCP Challenge"], "alternates": {"html": "https://wpnews.pro/news/i-found-3-security-vulnerabilities-in-my-own-ai-agent-s-tool-access", "markdown": "https://wpnews.pro/news/i-found-3-security-vulnerabilities-in-my-own-ai-agent-s-tool-access.md", "text": "https://wpnews.pro/news/i-found-3-security-vulnerabilities-in-my-own-ai-agent-s-tool-access.txt", "jsonld": "https://wpnews.pro/news/i-found-3-security-vulnerabilities-in-my-own-ai-agent-s-tool-access.jsonld"}}