[{"data":1,"prerenderedAt":690},["ShallowReactive",2],{"blog-how-to/check-mcp-config-exposed":3},{"id":4,"title":5,"body":6,"category":656,"date":657,"dateModified":657,"description":658,"draft":659,"extension":660,"faq":661,"featured":659,"headerVariant":672,"image":673,"keywords":674,"meta":675,"navigation":678,"ogDescription":679,"ogTitle":673,"path":680,"readTime":681,"schemaOrg":682,"schemaType":683,"seo":684,"sitemap":685,"stem":686,"tags":687,"twitterCard":688,"__hash__":689},"blog/blog/how-to/check-mcp-config-exposed.md","Is Your .cursor/mcp.json Exposed? The Obvious Check Lies (2026)",{"type":7,"value":8,"toc":646},"minimark",[9,13,28,31,34,44,49,60,63,168,183,198,202,209,215,224,233,240,270,279,283,294,297,339,356,360,363,372,379,389,402,419,435,452,456,463,471,474,496,500,516,519,522,607,626],[10,11,12],"p",{},"Run this against Vercel's own website right now:",[14,15,17],"code-block",{"label":16},"The check everyone reaches for",[18,19,24],"pre",{"className":20,"code":22,"language":23},[21],"language-text","$ curl -s -o /dev/null -w \"%{http_code}\\n\" https://vercel.com/.cursor/mcp.json\n200\n","text",[25,26,22],"code",{"__ignoreMap":27},"",[10,29,30],{},"Vercel is not leaking its Cursor config. That 200 is 2.5 MB of marketing HTML, served by a catch-all route that answers every unmatched path the same way. We checked it on 2026-08-26 and got the same result twice.",[10,32,33],{},"That's the problem with the advice going around. Scanners really did add AI assistant config paths to their wordlists, the check people reach for really is a one-line curl, and that one-line curl gives a false positive on a large share of the apps this audience ships.",[35,36,37],"tldr",{},[10,38,39,40,43],{},"Scanners now request ",[25,41,42],{},"/.cursor/mcp.json"," and friends, with real MCP protocol handshakes rather than blind guessing. On Vercel and Netlify with a normal framework preset your file almost certainly isn't reachable, for two separate reasons. There's a narrow set of deployment shapes where it is. Checking takes thirty seconds, but only if you look at the content type and the body instead of the status code.",[45,46,48],"h2",{"id":47},"what-was-actually-observed","What was actually observed",[10,50,51,52,59],{},"On 2026-07-13, SANS Internet Storm Center handler Manuel Humberto Santander Peláez published ",[53,54,58],"a",{"href":55,"rel":56},"https://isc.sans.edu/diary/33150",[57],"nofollow","a diary"," built from 14 days of Apache and ModSecurity logs on one small web host. The host ran no AI infrastructure at all. In his words: \"None of the AI-agent infrastructure the scanners were looking for existed on this host. That is the point.\"",[10,61,62],{},"The requested paths:",[64,65,66,79],"table",{},[67,68,69],"thead",{},[70,71,72,76],"tr",{},[73,74,75],"th",{},"Path",[73,77,78],{},"What it would be",[80,81,82,93,106,119,129,142,155],"tbody",{},[70,83,84,90],{},[85,86,87],"td",{},[25,88,89],{},"/.claude/mcp.json",[85,91,92],{},"Claude Code MCP server config",[70,94,95,103],{},[85,96,97,99,100],{},[25,98,42],{},", ",[25,101,102],{},"/.cursor/mcp_config.json",[85,104,105],{},"Cursor MCP server config",[70,107,108,116],{},[85,109,110,99,113],{},[25,111,112],{},"/.vscode/mcp.json",[25,114,115],{},"/.mcp/config.json",[85,117,118],{},"VS Code and generic MCP config",[70,120,121,126],{},[85,122,123],{},[25,124,125],{},"/.claude/settings.local.json",[85,127,128],{},"Claude Code local settings",[70,130,131,139],{},[85,132,133,99,136],{},[25,134,135],{},"/.claude/.credentials.json",[25,137,138],{},"/.config/claude/.credentials.json",[85,140,141],{},"Stored auth token",[70,143,144,152],{},[85,145,146,99,149],{},[25,147,148],{},"/v1/models",[25,150,151],{},"/api/tags",[85,153,154],{},"Unauthenticated local LLM APIs",[70,156,157,165],{},[85,158,159,99,162],{},[25,160,161],{},"POST /mcp",[25,163,164],{},"GET /sse",[85,166,167],{},"A live MCP server",[10,169,170,171,174,175,178,179,182],{},"The genuinely interesting part isn't the path list, it's the method. The scanners send correctly formed JSON-RPC 2.0 handshakes with protocol version ",[25,172,173],{},"2025-03-26",", not blind GETs. The diary puts it plainly: \"The scanner is not blindly requesting a URL. It is speaking the protocol.\" Credential files got ",[25,176,177],{},"HEAD"," requests rather than ",[25,180,181],{},"GET",", which the author reads as checking existence before spending bandwidth, a sign of \"a mature, wide campaign rather than a one-off curiosity probe.\"",[184,185,186],"info-box",{},[10,187,188,192,193,197],{},[189,190,191],"strong",{},"Keep this in proportion."," All four AI categories together came to roughly 200 requests over 14 days, about 14 a day, and the diary explicitly frames that as the ",[194,195,196],"em",{},"small"," bucket: \"Spring Boot Actuator scanning dominates by request volume.\" This is one handler's logs from one host, self-reported, with no published dataset. It's evidence that AI paths are now in commodity wordlists. It isn't evidence that anyone is hammering your site.",[45,199,201],{"id":200},"why-your-file-is-probably-not-reachable","Why your file is probably not reachable",[10,203,204,205,208],{},"Two independent things have to fail before a repo-root ",[25,206,207],{},".cursor/mcp.json"," answers over HTTP. Most people only know about the first.",[10,210,211,214],{},[189,212,213],{},"Layer one: only the output directory is served."," Both major hosts document this in almost identical language.",[10,216,217,218,223],{},"Vercel, in ",[53,219,222],{"href":220,"rel":221},"https://vercel.com/docs/builds/configure-a-build",[57],"Configuring a Build",": \"Only the contents of this Output Directory will be served statically by Vercel.\"",[10,225,226,227,232],{},"Netlify, in ",[53,228,231],{"href":229,"rel":230},"https://docs.netlify.com/build/configure-builds/overview/",[57],"Build configuration overview",": \"Only files in the publish directory are deployed. Files and assets located outside of the publish directory won't be included in site deploys.\"",[10,234,235,236,239],{},"A file being in your git repository has nothing to do with whether it's on the web. What matters is which directory the host serves. With Next.js, Astro, Vite or Nuxt on a standard preset, that's a build output folder, and ",[25,237,238],{},".cursor/"," at the repo root isn't in it.",[10,241,242,245,246,249,250,253,254,257,258,263,264,269],{},[189,243,244],{},"Layer two: dotfiles get special treatment."," Next.js serves ",[25,247,248],{},"public/"," through the ",[25,251,252],{},"send"," library without enabling its ",[25,255,256],{},"dotfiles"," option, so dotfiles return 404 (",[53,259,262],{"href":260,"rel":261},"https://github.com/vercel/next.js/discussions/20282",[57],"open discussion since December 2020","). Netlify staff have ",[53,265,268],{"href":266,"rel":267},"https://answers.netlify.com/t/netlify-deploy-api-removes-files-and-directories-beginning-with-a-period/37728",[57],"confirmed on their forum"," that files and directories beginning with a period are stripped, saying \"I do not believe we are planning any changes here.\"",[271,272,273],"warning-box",{},[10,274,275,278],{},[189,276,277],{},"Don't treat layer two as a guarantee."," Neither vendor documents dotfile handling in their official docs. The evidence is a GitHub discussion and a support thread, and Netlify's own suggested workaround (\"deploy via git or use a redirect\") implies the behaviour differs between git builds and zip or CLI deploys. It's real, staff-acknowledged, undocumented, and method-dependent. Verify rather than assume.",[45,280,282],{"id":281},"where-it-genuinely-can-be-reachable","Where it genuinely can be reachable",[10,284,285,286,289,290,293],{},"Vercel documents the first case itself. For projects with no build step: \"Choose 'Other' as the Framework Preset. This sets the output directory as ",[25,287,288],{},"public"," if it exists or ",[25,291,292],{},"."," (root directory of the project) otherwise.\"",[10,295,296],{},"Output directory equals repository root is exactly the shape where this stops being theoretical. The other shapes:",[298,299,300,311,318,325],"ul",{},[301,302,303,306,307,310],"li",{},[25,304,305],{},"vercel.json"," with ",[25,308,309],{},"\"outputDirectory\": \".\""," set by hand",[301,312,313,314,317],{},"An nginx or Apache ",[25,315,316],{},"root"," pointed straight at a git checkout",[301,319,320,321,324],{},"A Dockerfile that does ",[25,322,323],{},"COPY . ."," and then serves its working directory",[301,326,327,330,331,334,335,338],{},[25,328,329],{},"python -m http.server"," in a project folder, or a Vite dev server left exposed (",[25,332,333],{},"vite",", not ",[25,336,337],{},"vite preview","), both of which serve from the project root",[10,340,341,342,355],{},"There's a real sighting of the general failure on Vercel's own forum: a ",[53,343,346,347,350,351,354],{"href":344,"rel":345},"https://community.vercel.com/t/internal-files-being-exposed-as-static-assets/3266",[57],"thread where ",[25,348,349],{},"README.md"," and ",[25,352,353],{},"requirements.txt"," were served as public static assets"," on an \"Other\" preset project, because filesystem precedence beat the catch-all rewrite. Those aren't dotfiles, which is consistent with the two layers above behaving independently.",[45,357,359],{"id":358},"the-check-that-actually-works","The check that actually works",[10,361,362],{},"Status code alone is useless. Look at what came back.",[14,364,366],{"label":365},"Content-aware check",[18,367,370],{"className":368,"code":369,"language":23},[21],"curl -sS -L --max-time 20 -o /tmp/body -w 'status=%{http_code} type=%{content_type}\\n' \\\n  https://YOURSITE.com/.cursor/mcp.json\n\nhead -c 400 /tmp/body\n",[25,371,369],{"__ignoreMap":27},[10,373,374,375,378],{},"You are only exposed if ",[189,376,377],{},"all three"," are true:",[380,381,383],"step",{"number":382},"1",[10,384,385,386,292],{},"Status is ",[25,387,388],{},"200",[380,390,392],{"number":391},"2",[10,393,394,395,334,398,401],{},"Content type is ",[25,396,397],{},"application/json",[25,399,400],{},"text/html",". An HTML body is your app's catch-all route answering, which is the false positive that fools nearly everyone.",[380,403,405],{"number":404},"3",[10,406,407,408,99,411,414,415,418],{},"The body actually contains config, meaning strings like ",[25,409,410],{},"mcpServers",[25,412,413],{},"command"," or ",[25,416,417],{},"args",". Not a rendered page, not an error object.",[10,420,421,422,99,424,99,426,99,428,99,431,434],{},"Run it against the other paths too: ",[25,423,89],{},[25,425,112],{},[25,427,135],{},[25,429,430],{},"/.env",[25,432,433],{},"/.git/config",". Same three tests each time.",[436,437,438],"tip-box",{},[10,439,440,443,444,447,448,451],{},[25,441,442],{},"404"," isn't the only good answer. A ",[25,445,446],{},"403"," is common when nginx carries ",[25,449,450],{},"location ~ /\\. { deny all; }",", and a Cloudflare or WAF interstitial also means the file isn't being served. Anything that isn't JSON containing your actual config is fine.",[45,453,455],{"id":454},"one-thing-thats-true-for-everyone","One thing that's true for everyone",[10,457,458,459,462],{},"Whatever your hosting does, put these in ",[25,460,461],{},".gitignore",":",[14,464,465],{"label":461},[18,466,469],{"className":467,"code":468,"language":23},[21],".cursor/\n.claude/\n.vscode/\n.mcp/\n",[25,470,468],{"__ignoreMap":27},[10,472,473],{},"This costs nothing and doesn't depend on a vendor's undocumented dotfile behaviour staying the way it is. If those directories are already committed, taking them out means no deployment shape you migrate to later can serve them.",[271,475,476],{},[10,477,478,485,486,491,492,495],{},[189,479,480,481,484],{},"A leaked ",[25,482,483],{},"mcp.json"," isn't automatically a leaked key."," ",[53,487,490],{"href":488,"rel":489},"https://cursor.com/docs/mcp",[57],"Cursor's MCP documentation"," uses ",[25,493,494],{},"${env:NAME}"," indirection, so a well-formed config names servers and commands while the secrets sit in the environment. The diary is careful here too, describing the prize as \"server endpoints and sometimes API keys.\" Server names and internal URLs still tell an attacker what your agent can reach, which is worth having.",[45,497,499],{"id":498},"the-part-that-worries-us-more","The part that worries us more",[10,501,502,503,99,506,99,509,99,512,515],{},"Buried in the same diary is a finding that doesn't depend on an unusual deploy shape at all. If you wired an AI agent with a tool that fetches arbitrary URLs, you built an SSRF primitive. The diary's phrasing: \"An MCP server or an agent tool that fetches arbitrary URLs is a ready-made SSRF primitive.\" The scanners rotate parameter names (",[25,504,505],{},"url",[25,507,508],{},"uri",[25,510,511],{},"path",[25,513,514],{},"dest",") and aim at cloud metadata endpoints.",[10,517,518],{},"That one is live for anyone whose agent can fetch a link, no misconfiguration required. If your app has a \"summarise this URL\" feature, it's the thing to look at first.",[10,520,521],{},"The author's summary of why an exposed MCP server is worse than a leaked config file, from the body of the diary: it is \"a remote, machine-readable menu of everything an agent can touch, offered to anyone who completes the handshake.\"",[523,524,525,538,552,580,589],"faq-section",{},[526,527,529],"faq-item",{"question":528},"Is my .cursor/mcp.json exposed on my deployed site?",[10,530,531,532,534,535,537],{},"On Vercel or Netlify with a normal framework preset, almost certainly not. Both only serve the build output directory, and a repo-root ",[25,533,238],{}," is not in it. The cases where it can be reachable are specific: a no-build static deploy whose output directory is the repository root, an nginx or Apache root pointed at a checkout, or a Docker image built with ",[25,536,323],{}," that serves its working directory.",[526,539,541],{"question":540},"Why does curl return 200 for a file I do not have?",[10,542,543,544,547,548,551],{},"Because most single-page apps ship a catch-all rewrite that serves ",[25,545,546],{},"index.html"," for any unmatched path. The status code is 200 and the body is your app. As of 2026-08-26, ",[25,549,550],{},"https://vercel.com/.cursor/mcp.json"," returns 200 with about 2.5 MB of HTML, and Vercel is obviously not leaking a config file. Check the content type and the body, never the status code alone.",[526,553,555],{"question":554},"What paths are scanners actually requesting?",[10,556,557,558,99,560,99,562,99,564,99,566,99,568,99,570,350,572,574,575,350,577,579],{},"The SANS Internet Storm Center diary of 2026-07-13 lists ",[25,559,89],{},[25,561,42],{},[25,563,102],{},[25,565,112],{},[25,567,115],{},[25,569,125],{},[25,571,135],{},[25,573,138],{},", alongside probes to ",[25,576,148],{},[25,578,151],{},". That is one handler's logs from one host over 14 days, so treat it as a wordlist sighting rather than a measure of how much traffic you should expect.",[526,581,583],{"question":582},"Does a leaked mcp.json contain my API keys?",[10,584,585,586,588],{},"Sometimes, but often not. Cursor's documented format uses ",[25,587,494],{}," indirection, so a well-formed file names servers and commands while the secrets live in the environment. The diary is careful about this too, describing the prize as server endpoints and sometimes API keys. Server names and internal URLs are still worth something to an attacker: they describe what your agent can reach.",[526,590,592],{"question":591},"What should I do regardless of how my site is deployed?",[10,593,594,595,99,597,350,600,603,604,606],{},"Put ",[25,596,238],{},[25,598,599],{},".claude/",[25,601,602],{},".vscode/"," in ",[25,605,461],{},". That is free, correct for every reader, and does not depend on your hosting behaving a particular way. If those directories are already committed, removing them from the repo means they cannot be served by any deployment shape you might move to later.",[608,609,610,616,621],"related-articles",{},[611,612],"related-card",{"description":613,"href":614,"title":615},"The same discipline applied to source, git history, and your browser bundle","/blog/how-to/check-exposed-keys","How to Check for Exposed API Keys",[611,617],{"description":618,"href":619,"title":620},"What an MCP server hands an agent, and how to keep that list short","/blog/best-practices/mcp-server-security","MCP Server Security",[611,622],{"description":623,"href":624,"title":625},"The agent tool that fetches a URL is the primitive attackers are probing for","/blog/vulnerabilities/ssrf","SSRF Explained",[627,628,631,635],"cta-box",{"href":629,"label":630},"/","Scan Your Deployed App",[45,632,634],{"id":633},"let-something-else-run-the-checks","Let something else run the checks",[10,636,637,638,641,642,645],{},"A scan requests the paths that should never answer, follows the redirects, and reads the bodies rather than the status codes. It also checks the things this post did not: exposed ",[25,639,640],{},".env"," files, ",[25,643,644],{},".git"," directories, and endpoints that should require auth.",{"title":27,"searchDepth":647,"depth":647,"links":648},2,[649,650,651,652,653,654,655],{"id":47,"depth":647,"text":48},{"id":200,"depth":647,"text":201},{"id":281,"depth":647,"text":282},{"id":358,"depth":647,"text":359},{"id":454,"depth":647,"text":455},{"id":498,"depth":647,"text":499},{"id":633,"depth":647,"text":634},"how-to","2026-08-26","Scanners now request AI assistant config paths. Here is how to actually tell whether your site serves one, and why a 200 response usually means nothing.",false,"md",[662,664,666,668,670],{"question":528,"answer":663},"On Vercel or Netlify with a normal framework preset, almost certainly not. Both only serve the build output directory, and a repo-root .cursor/ is not in it. The cases where it can be reachable are specific: a no-build static deploy whose output directory is the repository root, an nginx or Apache root pointed at a checkout, or a Docker image built with COPY . . that serves its working directory.",{"question":540,"answer":665},"Because most single-page apps ship a catch-all rewrite that serves index.html for any unmatched path. The status code is 200 and the body is your app. As of 2026-08-26, https://vercel.com/.cursor/mcp.json returns 200 with about 2.5 MB of HTML, and Vercel is obviously not leaking a config file. Check the content type and the body, never the status code alone.",{"question":554,"answer":667},"The SANS Internet Storm Center diary of 2026-07-13 lists /.claude/mcp.json, /.cursor/mcp.json, /.cursor/mcp_config.json, /.vscode/mcp.json, /.mcp/config.json, /.claude/settings.local.json, /.claude/.credentials.json and /.config/claude/.credentials.json, alongside probes to /v1/models and /api/tags. That is one handler's logs from one host over 14 days, so treat it as a wordlist sighting rather than a measure of how much traffic you should expect.",{"question":582,"answer":669},"Sometimes, but often not. Cursor's documented format uses ${env:NAME} indirection, so a well-formed file names servers and commands while the secrets live in the environment. The diary is careful about this too, describing the prize as server endpoints and sometimes API keys. Server names and internal URLs are still worth something to an attacker: they describe what your agent can reach.",{"question":591,"answer":671},"Put .cursor/, .claude/ and .vscode/ in .gitignore. That is free, correct for every reader, and does not depend on your hosting behaving a particular way. If those directories are already committed, removing them from the repo means they cannot be served by any deployment shape you might move to later.","yellow",null,"cursor mcp.json exposed, .claude credentials exposed, is my mcp config public, mcp server scanning, check if dotfile is served, soft 404 catch-all rewrite, ai assistant config leaked",{"trendTrigger":676,"trendDate":677},"sans-isc-diary-33150-ai-config-paths-in-scanner-wordlists","2026-07-13",true,"curl https://vercel.com/.cursor/mcp.json returns 200. It is not exposed. Here is the check that actually works.","/blog/how-to/check-mcp-config-exposed","8 min read","[object Object]","HowTo",{"title":5,"description":658},{"loc":680},"blog/how-to/check-mcp-config-exposed",[],"summary_large_image","dBBAHRwXJ6SRzbEb2LWh3s1U7ywKrjwyRXmnq1RXpbk",1787862046434]