{"id":"GHSA-3jqg-r5pp-rmp2","kind":"vulnerability","aliases":["GHSA-3jqg-r5pp-rmp2"],"related":[],"title":"robots.txt enforcement bypass via HTTP redirects in mcp-server-fetch","description":"### Summary\n\n`mcp-server-fetch` checks robots.txt for the URL the model asks for, then lets httpx follow redirects without checking robots.txt again. A page on a host that allows autonomous fetching can redirect to a URL that robots.txt forbids, on another host or on the same one, and the server fetches it and returns the content to the model. This is the default configuration and applies to fetches made through the `fetch` tool, which is where the README says robots.txt is obeyed. The attacker needs the model to fetch a URL they control, or a site that redirects on its own.\n\n### Details\n\nrobots.txt is checked once, for the URL passed to the `fetch` tool, in the tool handler ([server.py#L452-L465](https://github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/src/mcp_server_fetch/server.py#L452-L465)):\r\n\r\n```python\r\npage_url = str(url)\r\n\r\nif not ignore_robots_txt:\r\n    await check_may_autonomously_fetch_url(\r\n        page_url, user_agent_autonomous, proxy_url, allow_private_ips\r\n    )\r\n\r\ncontent, prefix = await fetch_url(\r\n    page_url,\r\n    user_agent_autonomous,\r\n    force_raw=raw,\r\n    proxy_url=proxy_url,\r\n    allow_private_ips=allow_private_ips,\r\n)\r\n```\r\n\r\n`check_may_autonomously_fetch_url` ([L247-L293](https://github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/src/mcp_server_fetch/server.py#L247-L293)) downloads `/robots.txt` from that URL's host and calls `can_fetch()` for that one URL. `fetch_url` ([L296-L317](https://github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/src/mcp_server_fetch/server.py#L296-L317)) then fetches it with redirects on:\r\n\r\n```python\r\nasync with make_client(proxy_url) as client:\r\n    if not allow_private_ips:\r\n        client.event_hooks = {\"request\": [_refuse_private_destination]}\r\n    try:\r\n        response = await client.get(\r\n            url,\r\n            follow_redirects=True,\r\n            headers={\"User-Agent\": user_agent},\r\n            timeout=30,\r\n        )\r\n```\r\n\r\nhttpx follows the 3xx chain inside that `get()`. The only thing that runs per hop is `_refuse_private_destination` (unless `--allow-private-ips` is set), the private-address hook from #5033. Nothing reads robots.txt for a hop's URL, so the server fetches wherever the chain ends. robots.txt belongs to one origin (RFC 9309 section 2.3 gives its URI as `scheme:[//authority]/robots.txt`), which leaves two cases open:\r\n\r\n- The requested URL is on a host that allows it (or has no robots.txt) and redirects to another host whose robots.txt says `Disallow: /`, or answers 401/403, which this server treats as \"not allowed\" ([L274-L277](https://github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/src/mcp_server_fetch/server.py#L274-L277)). The second host's robots.txt is never requested.\r\n- The requested URL redirects within the same host to a path that the host's own robots.txt disallows. The rules were downloaded, but `can_fetch()` only ran for the original path.\r\n\r\nThe private-address guard still runs on every hop, so this is only about robots.txt.\r\n\r\nThe `server.py` in the PyPI 2026.10.19 wheel is identical to this commit's. The same script also reproduces on 2026.8.18, 2025.4.7, 2025.1.14 and 0.6.2, with `--allow-private-ips` dropped (it doesn't exist before 2026.10.10) and `isError` for `is_error` (SDK 1.x client). The last three ran with `mcp==1.9.0` and `pydantic==2.10.6`. On 0.6.2 and 2025.1.14 the control calls fail with an unrelated `McpError` constructor error under that SDK and return no content.\r\n\r\n### The existing test\r\n\r\n`tests/test_protocol.py` pins the current behaviour, inside `test_allow_private_ips_follows_redirect_to_private_address` ([L845-L861](https://github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/tests/test_protocol.py#L845-L861)):\r\n\r\n```python\r\n    # robots.txt is only consulted for the original host, never the target.\r\n    assert web.urls() == [ROBOTS, PAGE, metadata, metadata]\r\n```\r\n\r\nI read that as a record of what the code does today, in a test whose subject is `--allow-private-ips`, not as a decision that redirects are exempt. The line came in with #4971 (merged 2026-10-04), whose description lists it under \"Characterized (pinned, not fixed)\" and says each pinned item carries a comment \"so the fixing PR knows which assertion to change\". There the item is filed under #4838, and #5033, which closed #4838, only dealt with the private-address half.\r\n\r\nThe README's robots.txt section ([README.md#L168-L172](https://github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/README.md?plain=1#L168-L172)) says:\r\n\r\n> By default, the server will obey a websites robots.txt file if the request came from the model (via a tool), but not if the request was user initiated (via a prompt).\r\n\r\nand nothing about redirects, while the CAUTION at the top of the same file ([L14](https://github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/README.md?plain=1#L14)) says the private-address guard \"checks every redirect hop the same way\". SECURITY.md ([L63-L65](https://github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/SECURITY.md?plain=1#L63-L65)) names `robots.txt` among the boundaries a server claims to enforce: \"A way around a boundary a server claims to enforce (its allowed directories, the client's Roots, or `robots.txt`, for example) is a vulnerability\". From the site's side, a model-initiated request for its URL is an autonomous fetch however the request got there. If redirects are meant to be exempt, the README should say so; if not, that assertion is what changes, and the patch updates it.\r\n\r\n### Related work\r\n\r\n#4838 was fixed by #5033, which registered `_refuse_private_destination` as a request hook so every hop goes through the address guard. robots.txt was not part of that change. Three older open PRs for the same address problem (#4497, #4773, #4803) replace automatic redirects with a manual loop; none of them checks robots.txt inside the loop either. I found no issue, PR or published advisory in this repo about robots.txt and redirects. #5197 is about 5xx responses on robots.txt, a different bug.\r\n\r\n### Suggested fix\r\n\r\nCheck robots.txt in a second request hook, for every request after the first (the handler has already checked the first one), and only when the caller asked for robots.txt enforcement:\r\n\r\n```diff\r\ndiff --git a/src/fetch/src/mcp_server_fetch/server.py b/src/fetch/src/mcp_server_fetch/server.py\r\nindex bdac272..9618251 100644\r\n--- a/src/fetch/src/mcp_server_fetch/server.py\r\n+++ b/src/fetch/src/mcp_server_fetch/server.py\r\n@@ -299,15 +299,31 @@ async def fetch_url(\r\n     force_raw: bool = False,\r\n     proxy_url: str | None = None,\r\n     allow_private_ips: bool = False,\r\n+    check_robots_txt: bool = False,\r\n ) -> Tuple[str, str]:\r\n     \"\"\"\r\n     Fetch the URL and return the content in a form ready for the LLM, as well as a prefix string with status information.\r\n     \"\"\"\r\n     from httpx import HTTPError\r\n \r\n+    initial_url = str(httpx.URL(url))\r\n+\r\n+    async def _check_redirect_robots(request: httpx.Request) -> None:\r\n+        # The caller has already checked robots.txt for the requested URL;\r\n+        # this checks the URL of every redirect hop after it.\r\n+        if str(request.url) != initial_url:\r\n+            await check_may_autonomously_fetch_url(\r\n+                str(request.url), user_agent, proxy_url, allow_private_ips\r\n+            )\r\n+\r\n     async with make_client(proxy_url) as client:\r\n+        hooks = []\r\n         if not allow_private_ips:\r\n-            client.event_hooks = {\"request\": [_refuse_private_destination]}\r\n+            hooks.append(_refuse_private_destination)\r\n+        if check_robots_txt:\r\n+            hooks.append(_check_redirect_robots)\r\n+        if hooks:\r\n+            client.event_hooks = {\"request\": hooks}\r\n         try:\r\n             response = await client.get(\r\n                 url,\r\n@@ -462,6 +478,7 @@ def create_server(\r\n             force_raw=raw,\r\n             proxy_url=proxy_url,\r\n             allow_private_ips=allow_private_ips,\r\n+            check_robots_txt=not ignore_robots_txt,\r\n         )\r\n         original_length = len(content)\r\n         if start_index >= original_length:\r\n```\r\n\r\nThe `fetch` prompt and `--ignore-robots-txt` don't pass the flag, so they behave as before. Behaviour change to be aware of: one extra robots.txt request per redirect hop, same-host hops included (I didn't add caching, to keep the diff small), and a redirect target whose robots.txt can't be fetched or answers 401/403 now fails the fetch, the same way it already does for the requested URL.\r\n\r\nThe full patch also adds four tests to `tests/test_protocol.py` and changes the assertion quoted above. I can post it here in full or open a PR against the private fork.\n\n### Proof of concept\n\nTested at `51b6382a3e827aceb6bce430b1208d69b93d95ea` (`mcp-server-fetch` 2026.10.19) on Linux with Python 3.11.14, `mcp` 2.3.0 and `httpx` 0.27.2 (the last two as locked in `src/fetch/uv.lock`). The server is started as `python -m mcp_server_fetch --allow-private-ips`, with no other arguments and no proxy. robots.txt enforcement is on (the default); `--allow-private-ips` is only there because the two test sites listen on 127.0.0.1. The client is the Python MCP SDK's stdio client, not a desktop client.\r\n\r\n```bash\r\ngit clone https://github.com/modelcontextprotocol/servers.git\r\ncd servers && git checkout 51b6382a3e827aceb6bce430b1208d69b93d95ea\r\ncd src/fetch && uv sync --frozen\r\n# save the script below as poc_redirect_robots.py in this directory, then:\r\nuv run --frozen python poc_redirect_robots.py\r\n```\r\n\r\nThe script starts two local sites on different ports (so they are different origins). Site B's robots.txt disallows everything. Site A's disallows `/private`; A has `/go`, which redirects to B, and `/public`, which redirects to A's own `/private/report`. It calls the `fetch` tool on B and on A's `/private/report` directly (controls), then on the two redirecting URLs.\r\n\r\n```python\r\n\"\"\"mcp-server-fetch: robots.txt is checked for the requested URL, not for redirect targets.\r\n\r\nTwo local sites on different ports (so two origins). Run from an environment where\r\nmcp-server-fetch is installed:  python poc_redirect_robots.py [server args...]\r\nWith no arguments the server is started with --allow-private-ips (needed only\r\nbecause the sites are on 127.0.0.1).\r\n\"\"\"\r\n\r\nimport asyncio\r\nimport os\r\nimport sys\r\nfrom http.server import BaseHTTPRequestHandler, ThreadingHTTPServer\r\nfrom threading import Thread\r\n\r\nfrom mcp import ClientSession, StdioServerParameters\r\nfrom mcp.client.stdio import stdio_client\r\n\r\nHITS = []  # \"site path\" for every request the two sites receive\r\n\r\n\r\ndef serve(name, routes):\r\n    class Handler(BaseHTTPRequestHandler):\r\n        def do_GET(self):\r\n            HITS.append(f\"{name}{self.path}\")\r\n            status, headers, body = routes(self.path)\r\n            self.send_response(status)\r\n            self.send_header(\"Content-Type\", \"text/plain\")\r\n            for k, v in headers.items():\r\n                self.send_header(k, v)\r\n            self.send_header(\"Content-Length\", str(len(body)))\r\n            self.end_headers()\r\n            self.wfile.write(body)\r\n\r\n        def log_message(self, *args):\r\n            pass\r\n\r\n    httpd = ThreadingHTTPServer((\"127.0.0.1\", 0), Handler)\r\n    Thread(target=httpd.serve_forever, daemon=True).start()\r\n    return httpd.server_port\r\n\r\n\r\n# Site B disallows everything.\r\ndef site_b(path):\r\n    if path == \"/robots.txt\":\r\n        return 200, {}, b\"User-agent: *\\nDisallow: /\\n\"\r\n    if path == \"/secret\":\r\n        return 200, {}, b\"SECRET-FROM-B\"\r\n    return 404, {}, b\"not found\"\r\n\r\n\r\nport_b = serve(\"B\", site_b)\r\n\r\n\r\n# Site A allows everything except /private, and has two redirects.\r\ndef site_a(path):\r\n    if path == \"/robots.txt\":\r\n        return 200, {}, b\"User-agent: *\\nDisallow: /private\\n\"\r\n    if path == \"/go\":  # cross-origin: -> site B\r\n        return 302, {\"Location\": f\"http://127.0.0.1:{port_b}/secret\"}, b\"\"\r\n    if path == \"/public\":  # same origin: -> a disallowed path\r\n        return 302, {\"Location\": \"/private/report\"}, b\"\"\r\n    if path == \"/private/report\":\r\n        return 200, {}, b\"PRIVATE-REPORT\"\r\n    return 404, {}, b\"not found\"\r\n\r\n\r\nport_a = serve(\"A\", site_a)\r\n\r\nCASES = [\r\n    # (label, url, redirects?)\r\n    (\"control   B/secret          (robots Disallow: /)\", f\"http://127.0.0.1:{port_b}/secret\", False),\r\n    (\"control   A/private/report  (robots Disallow: /private)\", f\"http://127.0.0.1:{port_a}/private/report\", False),\r\n    (\"redirect  A/go     -> B/secret\", f\"http://127.0.0.1:{port_a}/go\", True),\r\n    (\"redirect  A/public -> A/private/report\", f\"http://127.0.0.1:{port_a}/public\", True),\r\n]\r\n\r\n\r\nasync def main():\r\n    args = sys.argv[1:] or [\"--allow-private-ips\"]\r\n    params = StdioServerParameters(\r\n        command=sys.executable, args=[\"-m\", \"mcp_server_fetch\", *args]\r\n    )\r\n    leaked = False\r\n    with open(os.devnull, \"w\") as devnull:\r\n        async with stdio_client(params, errlog=devnull) as (read, write):\r\n            async with ClientSession(read, write) as session:\r\n                await session.initialize()\r\n                for label, url, is_redirect in CASES:\r\n                    result = await session.call_tool(\"fetch\", {\"url\": url})\r\n                    text = result.content[0].text\r\n                    if not result.is_error:\r\n                        outcome = \"FETCHED\"\r\n                    elif \"robots.txt\" in text and \"not allowed\" in text:\r\n                        outcome = \"BLOCKED by robots.txt\"\r\n                    else:\r\n                        outcome = f\"ERROR: {text[:80]}\"\r\n                    print(f\"{label:58} -> {outcome}\")\r\n                    if not result.is_error:\r\n                        print(f\"    {text.splitlines()[-1]}\")\r\n                    leaked |= is_redirect and not result.is_error\r\n    print(\"requests seen by the sites:\", HITS)\r\n    print(\"VULNERABLE\" if leaked else \"OK: redirect targets are checked\")\r\n    sys.exit(1 if leaked else 0)\r\n\r\n\r\nasyncio.run(main())\r\n```\r\n\r\nExpected: all four calls refused. Actual at that commit:\r\n\r\n```text\r\ncontrol   B/secret          (robots Disallow: /)           -> BLOCKED by robots.txt\r\ncontrol   A/private/report  (robots Disallow: /private)    -> BLOCKED by robots.txt\r\nredirect  A/go     -> B/secret                             -> FETCHED\r\n    SECRET-FROM-B\r\nredirect  A/public -> A/private/report                     -> FETCHED\r\n    PRIVATE-REPORT\r\nrequests seen by the sites: ['B/robots.txt', 'A/robots.txt', 'A/robots.txt', 'A/go', 'B/secret', 'A/robots.txt', 'A/public', 'A/private/report']\r\nVULNERABLE\r\n```\r\n\r\nIn the request log, B's robots.txt is requested for the direct call only; after `A/go` the server goes straight to `B/secret`. With the patch from the Details section applied the script exits 0:\r\n\r\n```text\r\ncontrol   B/secret          (robots Disallow: /)           -> BLOCKED by robots.txt\r\ncontrol   A/private/report  (robots Disallow: /private)    -> BLOCKED by robots.txt\r\nredirect  A/go     -> B/secret                             -> BLOCKED by robots.txt\r\nredirect  A/public -> A/private/report                     -> BLOCKED by robots.txt\r\nrequests seen by the sites: ['B/robots.txt', 'A/robots.txt', 'A/robots.txt', 'A/go', 'B/robots.txt', 'A/robots.txt', 'A/public', 'A/robots.txt']\r\nOK: redirect targets are checked\r\n```\r\n\r\nThe same scenario is also in the patch as in-process tests, which run with the default private-address guard on (no `--allow-private-ips`). The fetch suite is 145 passed at HEAD and 149 with the patch. With only the test changes applied on HEAD, 4 tests fail: three of the new ones and the updated assertion (the fourth new test guards `--ignore-robots-txt`). `ruff check`, `ruff format --check` and `pyright` are clean with the patch.\n\n### Impact\n\nAnyone running `mcp-server-fetch` with robots.txt enforcement on (the default), for fetches the model makes through the `fetch` tool. The `fetch` prompt skips robots.txt by design and is not affected.\r\n\r\nThe attacker needs a URL the model will fetch and that the robots.txt check lets through: a page they host on a host with no robots.txt or a permissive one, answering with a redirect. A site can also trip it without any attacker, with an ordinary redirect from an allowed path to a disallowed one. In both cases the final URL is never checked against the robots.txt of the host it ends up on.\r\n\r\nWhat they get is content from URLs whose site asked autonomous agents to stay away, including hosts whose robots.txt answers 401/403. I'm not claiming access to anything non-public: robots.txt is a request to crawlers, not access control, and the private-address guard still applies on every hop. The harm is to the person running the server, who has been told robots.txt is obeyed, and to the site owners whose policy gets ignored.","summary":"mcp-server-fetch checks robots.txt only for the URL the model requested, then follows HTTP redirects without checking robots.txt for the redirect targets. A page can redirect the server to a path or host that robots.txt forbids, and the server returns that content to the model.","severity":"low","cvss":3.1,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"mcp-server-fetch","fixed":["2026.10.24"],"affected":">= 0.6.1, <= 2026.10.19","ecosystem":"pypi"}],"ecosystems":["pypi"],"tags":["cwe-693","mcp","mcp-server-fetch","package","python"],"not_affected_if":["ignore-robots-txt","trusted-urls-only"],"how_to_check":"- Check the installed version: `pip show mcp-server-fetch` and compare against the affected range 0.6.1 to 2026.10.19.\n- Look for `mcp-server-fetch` in your Python environments or lockfiles (`uv.lock`, `requirements.txt`) that the fetch tool runs under.","fix":"- `mcp-server-fetch` `2026.10.24`","status":"open","published_at":"2026-10-25T00:15:37+00:00","first_seen_at":"2026-11-09T22:53:25+00:00","triaged_at":"2026-11-09T22:58:27+00:00","updated_at":"2026-11-09T22:58:27+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-11-09T22:58:27Z","model":"claude-haiku-5-5","output":{"fix":"- `mcp-server-fetch` `2026.10.24`","reach":"internet","title":"mcp-server-fetch: robots.txt not rechecked on redirect targets, bypassing site rules","impact":"low","summary":"mcp-server-fetch checks robots.txt only for the URL the model requested, then follows HTTP redirects without checking robots.txt for the redirect targets. A page can redirect the server to a path or host that robots.txt forbids, and the server returns that content to the model.","audience":"developers","category":"package","nicknames":[],"who_cares":["mcp","python","mcp-server-fetch"],"confidence":0.8,"fix_status":"available","needs_auth":"no","worst_case":"Server fetches and returns content from paths robots.txt forbids","analyst_note":"","how_to_check":"- Check the installed version: `pip show mcp-server-fetch` and compare against the affected range 0.6.1 to 2026.10.19.\n- Look for `mcp-server-fetch` in your Python environments or lockfiles (`uv.lock`, `requirements.txt`) that the fetch tool runs under.","plain_action":"","product_name":"","needs_analyst":false,"not_affected_if":["ignore-robots-txt","trusted-urls-only"],"who_is_affected":"Teams running mcp-server-fetch with default robots.txt enforcement, where the model fetches URLs that may redirect. Servers started with ignore-robots-txt, or where fetches go only to trusted URLs without redirects, are less exposed. The sources do not say how much a site's robots.txt is relied on for access control.","user_interaction":"no","product_ecosystem":"","needs_special_setup":"no","severity_if_unknown":"unknown"},"cost_usd":0.001207,"input_tokens":9087,"output_tokens":597,"severity_before":"low"},"analyst_status":null,"analyst_note":null,"search":"'-693':27B '/authority':396C '/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/src/mcp_server_fetch/server.py#l247-l293))':267C '/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/src/mcp_server_fetch/server.py#l274-l277)).':441C '/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/src/mcp_server_fetch/server.py#l296-l317))':290C '/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/src/mcp_server_fetch/server.py#l452-l465)):':207C '/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/tests/test_protocol.py#l845-l861)):':596C '/robots.txt':269C,397C '0.6.2':520C,553C '1':539C '1.9.0':548C '2.10.6':551C '2.3':390C '2025.1.14':518C,555C '2025.4.7':517C '2026.10.10':532C '2026.10.19':502C '2026.8.18':516C '30':332C '3jqg':14A,19A '3xx':336C '401/403':428C '5033':365C '9309':388C 'address':362C,484C,590C 'agent':227C,242C,328C,330C 'allow':111C,231C,251C,254C,308C,354C,411C,435C,523C,583C 'allow-private-ip':353C,522C 'also':513C 'anoth':123C,420C 'answer':427C 'appli':148C 'ask':94C 'async':298C 'attack':166C 'autonom':112C,221C,228C,243C,259C 'await':218C,236C,319C 'behaviour':580C 'belong':383C 'bypass':3A 'call':276C,558C 'case':401C 'chain':337C,380C 'check':39B,53B,87C,102C,187C,219C,257C 'client':301C,305C,541C 'client.event':311C 'client.get':320C 'commit':508C 'configur':146C 'constructor':564C 'consult':601C 'content':78B,138C,234C,572C 'control':175C,557C 'current':579C 'cwe':26B 'default':145C 'destin':316C,351C 'detail':184C 'disallow':425C,467C 'doesn':528C 'download':268C,471C 'drop':526C 'end':381C 'enforc':2A 'error':537C,565C 'everi':489C 'exist':530C,574C 'fail':559C 'fetch':11A,25A,32B,38B,86C,113C,133C,150C,154C,171C,195C,222C,237C,260C,278C,283C,292C,377C,474C 'follow':49B,99C,322C,334C,586C 'forbid':72B,121C 'forc':244C 'get':340C 'ghsa':13A,18A 'ghsa-3jqg-r5pp-rmp2':12A,17A 'github.com':206C,266C,289C,440C,595C 'github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/src/mcp_server_fetch/server.py#l247-l293))':265C 'github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/src/mcp_server_fetch/server.py#l274-l277)).':439C 'github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/src/mcp_server_fetch/server.py#l296-l317))':288C 'github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/src/mcp_server_fetch/server.py#l452-l465)):':205C 'github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/tests/test_protocol.py#l845-l861)):':594C 'give':391C 'guard':485C 'handler':200C 'header':325C 'hook':312C,363C 'hop':347C,371C,490C 'host':69B,109C,124C,274C,409C,421C,444C,457C,463C 'http':5A,50B 'httpx':98C,333C 'ident':505C 'ignor':215C 'insid':338C,581C 'ip':233C,253C,256C,310C,356C,525C,585C 'iserror':534C 'l247':263C 'l247-l293':262C 'l274':437C 'l274-l277':436C 'l277':438C 'l293':264C 'l296':286C 'l296-l317':285C 'l317':287C 'l452':203C 'l452-l465':202C 'l465':204C 'l845':592C 'l845-l861':591C 'l861':593C 'last':543C 'leav':399C 'let':97C 'made':151C 'make':300C 'may':220C,258C 'mcp':9A,23A,28B,30B,36B,84C,547C 'mcp-server-fetch':8A,22A,29B,35B,83C 'mcperror':563C 'model':46B,81B,93C,141C,169C 'need':167C 'never':448C 'noth':366C 'obey':164C 'one':129C,281C,385C 'open':402C 'origin':386C,479C 'origina':604C 'packag':33B 'page':60B,106C,209C,224C,239C 'pass':192C 'path':67B,460C,480C 'per':346C 'pin':577C 'prefix':235C 'privat':232C,252C,255C,309C,315C,350C,355C,361C,483C,524C,584C,589C 'private-address':360C,482C 'proxi':229C,247C,249C,302C 'pydant':550C 'pypi':501C 'python':34B,208C,297C,597C 'r5pp':15A,20A 'ran':476C,545C 'raw':245C,246C 'read':367C 'readm':160C 'redirect':6A,51B,57B,62B,100C,115C,180C,295C,323C,418C,453C,587C 'refus':314C,349C 'reproduc':514C 'request':47B,313C,404C,449C,451C 'respons':318C 'return':76B,136C,570C 'rfc':387C 'rmp2':16A,21A 'robot':216C 'robots.txt':1A,40B,54B,71B,88C,103C,120C,162C,185C,368C,382C,416C,423C,446C,466C,496C,598C 'rule':469C 'run':345C,487C 'say':161C,424C 'scheme':395C 'script':512C 'sdk':538C,568C 'second':443C 'section':389C 'server':10A,24A,31B,37B,64B,75B,85C,132C,376C,431C 'server.py':201C,498C 'set':358C 'site':178C 'still':486C 'str':211C 'summari':82C 'target':58B 'test':575C,582C 'tests/test_protocol.py':576C 'thing':343C 'three':544C 'timeout':331C 'tool':155C,196C,199C 'treat':432C 'tri':317C 'true':324C 'two':400C 'txt':217C 'unless':352C 'unrel':562C 'uri':393C 'url':44B,91C,118C,173C,191C,210C,212C,223C,225C,230C,238C,240C,248C,250C,261C,272C,282C,284C,303C,321C,373C,405C,452C 'user':226C,241C,327C,329C 'user-ag':326C 'via':4A 'wheel':503C 'wherev':378C 'whose':422C 'within':454C 'without':52B,101C 'x':540C","nicknames":[],"mitigations":[],"who_affected":"Teams running mcp-server-fetch with default robots.txt enforcement, where the model fetches URLs that may redirect. Servers started with ignore-robots-txt, or where fetches go only to trusted URLs without redirects, are less exposed. The sources do not say how much a site's robots.txt is relied on for access control.","tier":"C","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"developers","plain_action":null,"pkeys":["mcpserverfetch"],"ptokens":["fetch","mcp","server"],"labels":["cwe693","low","mcp","mcpserverfetch","package","pypi","python"],"campaign_type":null,"search_a":"'3jqg':14A,19A 'bypass':3A 'enforc':2A 'fetch':11A,25A 'ghsa':13A,18A 'ghsa-3jqg-r5pp-rmp2':12A,17A 'http':5A 'mcp':9A,23A 'mcp-server-fetch':8A,22A 'r5pp':15A,20A 'redirect':6A 'rmp2':16A,21A 'robots.txt':1A 'server':10A,24A 'via':4A","search_b":"'-693':27B 'check':39B,53B 'content':78B 'cwe':26B 'fetch':32B,38B 'follow':49B 'forbid':72B 'host':69B 'http':50B 'mcp':28B,30B,36B 'mcp-server-fetch':29B,35B 'model':46B,81B 'packag':33B 'page':60B 'path':67B 'python':34B 'redirect':51B,57B,62B 'request':47B 'return':76B 'robots.txt':40B,54B,71B 'server':31B,37B,64B,75B 'target':58B 'url':44B 'without':52B","search_c":"'/authority':396C '/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/src/mcp_server_fetch/server.py#l247-l293))':267C '/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/src/mcp_server_fetch/server.py#l274-l277)).':441C '/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/src/mcp_server_fetch/server.py#l296-l317))':290C '/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/src/mcp_server_fetch/server.py#l452-l465)):':207C '/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/tests/test_protocol.py#l845-l861)):':596C '/robots.txt':269C,397C '0.6.2':520C,553C '1':539C '1.9.0':548C '2.10.6':551C '2.3':390C '2025.1.14':518C,555C '2025.4.7':517C '2026.10.10':532C '2026.10.19':502C '2026.8.18':516C '30':332C '3xx':336C '401/403':428C '5033':365C '9309':388C 'address':362C,484C,590C 'agent':227C,242C,328C,330C 'allow':111C,231C,251C,254C,308C,354C,411C,435C,523C,583C 'allow-private-ip':353C,522C 'also':513C 'anoth':123C,420C 'answer':427C 'appli':148C 'ask':94C 'async':298C 'attack':166C 'autonom':112C,221C,228C,243C,259C 'await':218C,236C,319C 'behaviour':580C 'belong':383C 'call':276C,558C 'case':401C 'chain':337C,380C 'check':87C,102C,187C,219C,257C 'client':301C,305C,541C 'client.event':311C 'client.get':320C 'commit':508C 'configur':146C 'constructor':564C 'consult':601C 'content':138C,234C,572C 'control':175C,557C 'current':579C 'default':145C 'destin':316C,351C 'detail':184C 'disallow':425C,467C 'doesn':528C 'download':268C,471C 'drop':526C 'end':381C 'error':537C,565C 'everi':489C 'exist':530C,574C 'fail':559C 'fetch':86C,113C,133C,150C,154C,171C,195C,222C,237C,260C,278C,283C,292C,377C,474C 'follow':99C,322C,334C,586C 'forbid':121C 'forc':244C 'get':340C 'github.com':206C,266C,289C,440C,595C 'github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/src/mcp_server_fetch/server.py#l247-l293))':265C 'github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/src/mcp_server_fetch/server.py#l274-l277)).':439C 'github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/src/mcp_server_fetch/server.py#l296-l317))':288C 'github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/src/mcp_server_fetch/server.py#l452-l465)):':205C 'github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/fetch/tests/test_protocol.py#l845-l861)):':594C 'give':391C 'guard':485C 'handler':200C 'header':325C 'hook':312C,363C 'hop':347C,371C,490C 'host':109C,124C,274C,409C,421C,444C,457C,463C 'httpx':98C,333C 'ident':505C 'ignor':215C 'insid':338C,581C 'ip':233C,253C,256C,310C,356C,525C,585C 'iserror':534C 'l247':263C 'l247-l293':262C 'l274':437C 'l274-l277':436C 'l277':438C 'l293':264C 'l296':286C 'l296-l317':285C 'l317':287C 'l452':203C 'l452-l465':202C 'l465':204C 'l845':592C 'l845-l861':591C 'l861':593C 'last':543C 'leav':399C 'let':97C 'made':151C 'make':300C 'may':220C,258C 'mcp':84C,547C 'mcp-server-fetch':83C 'mcperror':563C 'model':93C,141C,169C 'need':167C 'never':448C 'noth':366C 'obey':164C 'one':129C,281C,385C 'open':402C 'origin':386C,479C 'origina':604C 'page':106C,209C,224C,239C 'pass':192C 'path':460C,480C 'per':346C 'pin':577C 'prefix':235C 'privat':232C,252C,255C,309C,315C,350C,355C,361C,483C,524C,584C,589C 'private-address':360C,482C 'proxi':229C,247C,249C,302C 'pydant':550C 'pypi':501C 'python':208C,297C,597C 'ran':476C,545C 'raw':245C,246C 'read':367C 'readm':160C 'redirect':100C,115C,180C,295C,323C,418C,453C,587C 'refus':314C,349C 'reproduc':514C 'request':313C,404C,449C,451C 'respons':318C 'return':136C,570C 'rfc':387C 'robot':216C 'robots.txt':88C,103C,120C,162C,185C,368C,382C,416C,423C,446C,466C,496C,598C 'rule':469C 'run':345C,487C 'say':161C,424C 'scheme':395C 'script':512C 'sdk':538C,568C 'second':443C 'section':389C 'server':85C,132C,376C,431C 'server.py':201C,498C 'set':358C 'site':178C 'still':486C 'str':211C 'summari':82C 'test':575C,582C 'tests/test_protocol.py':576C 'thing':343C 'three':544C 'timeout':331C 'tool':155C,196C,199C 'treat':432C 'tri':317C 'true':324C 'two':400C 'txt':217C 'unless':352C 'unrel':562C 'uri':393C 'url':91C,118C,173C,191C,210C,212C,223C,225C,230C,238C,240C,248C,250C,261C,272C,282C,284C,303C,321C,373C,405C,452C 'user':226C,241C,327C,329C 'user-ag':326C 'wheel':503C 'wherev':378C 'whose':422C 'within':454C 'without':101C 'x':540C","cause_entry":null,"cause_path":null,"disclosures_checked_at":null,"first_public_at":"2026-10-25T00:15:37+00:00","kev":null,"cvss_vector":null,"cvss_by":null,"poc_count":0,"sources":[{"source":"ghsa-repo","external_id":"GHSA-3jqg-r5pp-rmp2","url":"https://github.com/modelcontextprotocol/servers/security/advisories/GHSA-3jqg-r5pp-rmp2","severity":"low","published_at":"2026-10-25T00:15:37+00:00","modified_at":"2026-10-25T00:15:38+00:00","first_seen_at":"2026-11-09T22:53:25+00:00","fetched_at":"2026-11-09T22:53:25+00:00","note":null}],"documents":[{"id":110623,"url":"https://github.com/modelcontextprotocol/servers/security/advisories/GHSA-3jqg-r5pp-rmp2","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa-repo","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"}],"disclosures":[],"exploit_sources":[],"notice":"Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai"}