让用户信任自己的 MCP 客户端,而不打开钓鱼洞
问题
你运行自己的agent:Slack bot,一个服务 VPS,MCP客户端你在一个周末写。 您想要在您的文件中,在流船上意味着您自己的 S3 桶,agent 可以通过 MCP 服务器读写和删除。 agent会说MCP的标准OAuth的签名:它会自己注册,打开浏览器,你点击批准,它会得到一个符号。
它在第一步失败。 在注册时,agent 将它命名为重定向 URI,您的浏览器在您批准后返回地址,并带有一次性的授权代码 agent 交换一个符号。 您的地址是 https://agent. yourhost.example/ callback;答案无效 redirect uri. 服务器仅接受其运营商许可列表上的地址:Claude,ChatGPT,Cursor,VS代码等少数。 获取您的添加意味着要求操作员更改配置文件。
服务器不能简单地接受任何地址。 邮局会把钥匙送到你家没有它,一个陌生人就登记了一个假的“Claude”,把钥匙寄到他们的地方,用一个链接将你的存储器固定起来。 所以服务器要么对您关闭,要么对每个人都不安全。
两种失败都是常见的。 在封闭的一侧,这是人们遇到的:
" 不允许重定向 URI, URL 不属于 Atlassian 白列域中注册的 MCP 客户端。 " — 错误用户,Cursor 在其中,在 Atlassian 开发者社区中报告。
“......每个主机应用程序必须有一个注册的GitHub App(或OAuth App)来代表用户认证。” —— GitHub MCP服务器,政策和治理;否则,你返回个人访问令牌。
在开放方面,2026年5月的一项研究测试了119台OAuth驱动的MCP服务器,发现其中114台的注册端点,95.8%
“接受匿名者任意改变微小数值”。
攻击:一个链接,一个点击,别人的桶
说您的服务器接受任何注册和任何方向地址。 攻击者不记账,可以分四个步骤接管用户的访问:
- 登记一个名为“Claude”的客户,该客户的调整方向是https://evil.example/cb。 公开注册是匿名的,通过设计。
- 发送一个签名用户的正常外观/oaut/授权链接:“将您的存储重新连接到Claude”。
- 用户在真实的会话中看到您真正的同意屏幕,并点击批准。
- 代码获得在邪恶。 攻击者用它来换取一个纪念物,现在是用户。
PKCE无济于事:它只让启动流的客户端完成它,攻击者开始这个。 委托人姓名自述,证明无为。 同意屏幕显示重定向主机显示,但claude-连接。 应用通过一个透视。 这与寄给Microsoft 365用户的假申请批准提示相同:真实的页面和真实的批准;攻击者使用注册的平台的应用,这里的地址服务器不应该接受。
用户的确同意,但有虚假信息。 重定向许可列表是使批准成为用户认为它意味着什么:无论名称称,键只能去列表称的地方。
这不是假设。 2026年1月Obsidian时,Square的MCP服务器上,安全部门报导了这一链条:用研究人员的话来说,“任何先前授权使用Square MCP服务器的用户都是脆弱的。 ” 他们注意到,Square的固定只适用于新注册的客户。
其他人在MCP代理商面前发现了同样的链条,每点击一次,价值24小时访问令牌和14天刷新令牌,并跨过bug-bounty程序,有时被挥舞为社会工程。 SickSec的回答:受害人从未离开过可信赖的域名。
一个比较安静的问题随身携带。 当一个请求在重新定向地址被接受后失败时,错误会被发送到该地址。 因此,接受任何地址的服务器是自己域上的开放重定向器:一个设计出来的授权链接与不良的瞄准镜会弹射到任何网站的访客,在您的主机名下。 RFC 9700描述的正是这个案子。
称的Spec和服务器到底做什么
MCP Spec将风险命名,并将策略留给每个服务器。 它的安全考虑是,授权服务器 MUSST 验证精确的重定向URI,只应该自动重定向到他们信任的URI,而May 转而通知用户,让他们决定。 最后两句是RFC 9700的句子,逐字逐句。 " 信任 " 的含义是空洞的。
登记也正在转移。 2026-07-28客户端注册页面对客户端ID元数据文档(CIMD)的动态客户端注册(一个客户端自己在一个API上注册,没有人参与)进行贬值,其中客户端ID是一个HTTPS URL,用于列出其重定向URI的文件。 发布客户端的CIMD 称,而不是该用户是否有意连接。
OAuth工作组一直在这里。 在IETF OAuth列表中2025年12月的一条线条中,沃伦·帕拉德认为未经认证的注册将MCP授权服务器转变为开放的重定向器。 Aaron Parecki引述的答复指出RFC 9700 §4.11.2
在实际操作中,服务器坐在两个极之一:
loopback表示localhost或127.0.0.1:用户自己的机器上的应用程序,等待浏览器回来。
第一杆会花费真正的用户,客户端不会为您修复。 2026年6月的Claude Code期报道,拥有精确匹配重定向许可列表的组织不能从Claude Code签名,因为其http://localhost:<port>/callback host和路径是固定的。 2026年7月因未按计划关闭。 服务器必须移动。
这不是一个新主意
让用户为自己应用程序的回调提供担保已经过时了。 GitHub有开发者在其召回的URL中创建了OAuth应用程序和类型。 MCP spec预先注册选项描述用户手动注册客户端的服务器页面。 而一个组织可以在Atlassian的MCP服务器中添加可信赖的重定向域。 这些都不是适合匿名注册的客户的:前两个客户需要他人制作的客户ID,Atlassian则决定整个组织。 因此,我们保持了自我注册,让每个用户在注册时决定,为了他们自己,除了整个服务器的列表。
我们的设计:信任每个用户,在登录时决定
我们保留本地客户的许可列表和回路,此外,每个用户可以信任少数准确的https重定向地址供自己使用;我们的封顶是10。 在网络应用的MCP网页上
关键动作是检查运行时。 登记是在任何人签字之前进行的,因此无法知道谁可以查阅名单。 因此注册接受任意的 https URI,并且决定等待授权页面,其中用户已知:
一条规则坐落在每个分支之下:没有代码,也没有错误总是被重定向到地址,除非每个账户或这个用户信任它。 在登录之前,没有用户列表适用,所以即使是OAuth库自己的验证错误(一个糟糕的PKCE挑战,未知范围)也等待登录,拒绝就是我们主机上的一页。 N8n的CVE-2026-42230(2026年4月)直接拒绝同意攻击者的注册地址。
环绕七条路,每条都关上
这些规则大多来自试图打破第一个版本。
- 只有用户自己的浏览器会话更改列表,从不修改应用程序或API令牌,所以失窃令牌无法在用户代码可能去的地方拓宽。
- 从拒绝页中添加意味着输入主机。 页面显示主机,完整地址作为纯文本(绝不是一个链接),它会加入的账户,以及一个警告:如果有人发送这个链接,则关闭页面。 在主机被打出之前,“信任和继续” 仍然被禁用,因此,从 fishing 电子邮件中点击一次是不够的。
- 同意筛选这个登录的重新定位,一个客户端可以同时注册几个地址,这样就可以在自己的地址之外列出claude.ai. 显示屏幕上只有这个签名返回的屏幕,以及它的基础:在许可列表上,回转,或“你在2026年11月3日信任这个地址”。 虽然2026年5月的研究发现60.5%的测试服务器没有;
- 每增加一个字都用电子邮件发送,并在活动日志上进行审核。 电子邮件与主机称在何时何地账户连接,使完整地址不变成链接。 这是费氏用户永远得不到的信号
- 删除一个地址削减的应用程序关闭,甚至中签。 它通过该地址取消所有赠款(用户给予的批准),并放弃已经批准但尚未交换的代码。 一个代码存在大约一分钟;没有这个代码,一个在飞行中的代码成为去除之后的赠款。
- 拒绝浏览器主机和不同阅读的服务器。 URI 解析器可能保留好的。 example% 2eevil. example 作为一个字面主机,而浏览器会进入好的.example.evil.example. 一个为一个主机命名并发送用户到另一个主机的页面会挫败打字主机的检查,因此这样的主机在注册和列表中都会被拒绝。
- 准确的匹配。 没有通配符,没有前缀匹配,没有回转式的端口灵活性。
我们拒绝的东西
它并没有阻止一个坚定的社会工程师:一个用户谈论打入攻击者的宿主,批准是费斯德的,无论他们选择哪一级。 我们捆绑它而不是阻止它:输入的主机、警告、电子邮件、删除时的即时断开,以及用户信任的地址从只读和称开始的同意屏幕。
我们首先在写入访问时封顶用户信任的地址,所以费氏赠款可以更改数据,但不能删除。 我们放下了盖子:用户连接自己的agents想要完全进入,盖子是一个后卫,而不是防御。
在以上两根柱子之外,我们称重:
您自己的 MCP 服务器的检查表
- 在注册时,在已注册用户知道的情况下,决定授权的编码地点。
- 在重定向前对照规则检查每一个答案,包括错误,并拒绝在自己的主机上页。 Cloudflare的工人-oauth供应商于2025年5月运送CVE-2025-4143
- 在同意屏幕上显示此请求的重定向主机,并说为什么允许。
- 将清单扩大为在登录流外或打字确认之后的蓄意行为。
- 每次添加一个地址时都会给用户发邮件。
- 取消赠款和飞行守则。
- 拒绝主机浏览器和你的解析器读数不同的URI.
想试试吗?在您自己的 agent 中,加入 Stream 的 MCP 服务器 https://mcp. streamvessel.com 并签名。 如果你的agent不在我们的名单上,你会在上面显示的拒绝页面上登陆,在那里你可以信任它的地址
本文由 AI图鉴自动抓取并翻译,版权归原作者所有;如需查看原文请点下方链接。
Letting users trust their own MCP clients, without opening a phishing hole
The problem
You run your own agent: a Slack bot, a service on a VPS, an MCP client you wrote over a weekend. You want it on your files, which on Stream Vessel means your own S3 bucket, where an agent can read, write and delete through the MCP server. The agent speaks MCP’s standard OAuth sign-in: it registers itself, opens a browser, you click Approve, it gets a token.
It fails at step one. At registration the agent named its redirect URI, the address your browser returns to after you approve, carrying a one-time authorization code the agent swaps for a token. Yours is https://agent.yourhost.example/callback; the answer is invalid_redirect_uri. The server only accepts addresses on its operator’s allowlist: Claude, ChatGPT, Cursor, VS Code and a few others. Getting yours added means asking the operator to change a config file.
The server cannot simply accept any address. That list is the addresses the post office will deliver your house keys to. Without it, a stranger registers a fake “Claude” that has the keys sent to their place, and phishes your storage with one link. So the server is either closed to you or unsafe for everyone.
Both failures are common. On the closed side, this is what people run into:
“The redirect URI is not allowed, the URL is not part of Atlassian whitelisted domains for registered MCP Clients.” — an error users, Cursor’s among them, reported in the Atlassian developer community
“…each host application must have a registered GitHub App (or OAuth App) to authenticate on behalf of the user.” — GitHub MCP server, policies and governance; otherwise you fall back to a personal access token
On the open side, a May 2026 study that tested 119 OAuth-enabled MCP servers found registration endpoints on 114 of them, 95.8%, that
“accept arbitrary redirect_uri values from anonymous requesters.”
The attack: one link, one click, someone else’s bucket
Say your server accepts any registration and any redirect address. An attacker with no account on it can take over a user’s access in four steps:
- Register a client named “Claude” whose redirect URI is https://evil.example/cb. Open registration is anonymous, by design.
- Send a signed-in user a normal-looking /oauth/authorize link: “Reconnect your storage to Claude”.
- The user sees your real consent screen, on your real domain, in their real session, and clicks Approve.
- The code lands at evil.example. The attacker trades it for a token and is now the user.
PKCE does not help: it lets only the client that started a flow finish it, and the attacker started this one. The client name is self-declared and proves nothing. The consent screen shows the redirect host, but claude-connect.app passes a glance. It is the same trick as the fake-app approval prompts sent to Microsoft 365 users: a real page and a real Approve; there the attacker uses an app the platform registered, here an address the server should never have accepted.
The user does approve, but on false information. A redirect allowlist is what makes an approval mean what the user thinks it means: whatever the name says, the keys can only go where the list says.
This is not hypothetical. In January 2026 Obsidian Security reported this chain against Square’s MCP server: self-registration accepted any redirect address, and one click exposed a merchant’s profile, transactions and bank details. In the researchers’ words, “any user who had previously authorized the Square MCP server was vulnerable.” Square’s fix, they note, only applied to newly registered clients.
Others found the same chain on an MCP proxy in front of a company’s single sign-on, worth a 24-hour access token and a 14-day refresh token per click, and across bug-bounty programs, where it was sometimes waved off as social engineering. SickSec’s answer: the victim never leaves the trusted domain.
A quieter problem rides along. When a request fails a check after its redirect address was accepted, the error is sent to that address. So a server that accepts any address is an open redirector on its own domain: a crafted authorize link with a bad scope bounces visitors to any site, under your hostname. RFC 9700 describes exactly this case.
What the spec says, and what servers actually do
The MCP spec names the risk and leaves the policy to each server. Its security considerations say authorization servers MUST validate exact redirect URIs, SHOULD only redirect automatically to a URI they trust, and MAY instead inform the user and let them decide. The last two are RFC 9700’s sentences, word for word. What “trust” means is left open.
Registration is shifting too. The 2026-07-28 client registration page deprecates Dynamic Client Registration (a client registering itself over an API, no human involved) for Client ID Metadata Documents (CIMD), where the client id is an HTTPS URL to a document listing its redirect URIs. CIMD says who publishes a client, not whether this user meant to connect it.
The OAuth working group has been here. In a December 2025 thread on the IETF OAuth list, Warren Parad argued that unauthenticated registration turns an MCP authorization server into an open redirector. Aaron Parecki’s quoted reply points to RFC 9700 §4.11.2, which starts from exactly that setup.
In practice, servers sit at one of two poles:
Loopback means localhost or 127.0.0.1: an app on the user’s own machine, waiting for the browser to come back.
The first pole costs real users, and clients will not fix it for you. A Claude Code issue from June 2026 reports that organizations with exact-match redirect allowlists cannot sign in from Claude Code, because its http://localhost:<port>/callback host and path are fixed. It was closed as not planned in July 2026. The server has to move.
This is not a new idea
Letting users vouch for their own app’s callback is old. GitHub has developers create an OAuth app and type in its callback URL. The MCP spec’s pre-registration option describes a server page where users register a client by hand. And an organization can add trusted redirect domains to Atlassian’s MCP server. None of these fit a client that registers itself anonymously: the first two need a client id made somewhere else, and Atlassian decides for a whole organization. So we kept self-registration and let each user decide at sign-in, for their own account, beside the server-wide list.
Our design: trust is per user, decided at sign-in
We keep the operator’s allowlist and loopback for local clients, unchanged and open to every account. On top of it, each user can trust a handful of exact https redirect addresses for their own account; ours caps at 10. On Stream Vessel that is “Trusted app addresses” on the MCP page of the web app.
The key move is when the check runs. Registration happens before anyone signs in, so it cannot know whose list to consult. So registration accepts any https URI, and the decision waits for the authorize page, where the user is known:
One rule sits under every branch: no code and no error is ever redirected to an address unless every account or this user trusts it. Before sign-in no user’s list applies, so even the OAuth library’s own validation errors (a bad PKCE challenge, an unknown scope) wait for sign-in, and a refusal is a page on our host. That closes the open-redirect hole along with the phishing one, and it covers the Deny button too: n8n’s CVE-2026-42230 (April 2026) sent users who denied consent straight to an attacker’s registered address.
Seven ways around it, each closed
Most of these rules came from trying to break the first version.
- Only the user’s own browser session changes the list, never an app or API token, so a stolen token cannot widen where the user’s codes may go.
- Adding from the refusal page means typing the host. The page shows the host, the full address as plain text (never a link), the account it would join, and a warning: if someone sent you this link, close the page. “Trust and continue” stays disabled until the host is typed, so one click from a phishing email is not enough.
- The consent screen names this sign-in’s redirect, and why it is allowed. A client can register several addresses at once, so it could list claude.ai beside its own. The screen shows only the one this sign-in returns to, and its basis: on the allowlist, loopback, or “you trusted this address yourself on 3 Nov 2026”. The spec requires showing the host for metadata-document clients, though the May 2026 study found 60.5% of tested servers don’t; the basis is what makes it mean something.
- Every addition is emailed, and audited while the activity log is on. The email leads with the host, says when and to which account, and keeps the full address from turning into a link. It is the signal a phished user would otherwise never get.
- Removing an address cuts the app off, even mid-sign-in. It revokes every grant (an approval the user gave) through that address and drops codes approved but not yet exchanged. A code lives about a minute; without this, a code in flight becomes a grant after the removal.
- Hosts the browser and the server read differently are refused. A URI parser may keep good.example%2eevil.example as one literal host while the browser goes to good.example.evil.example. A page that names one host and sends the user to another defeats the typed-host check, so such hosts are refused at registration and on the list.
- Exact match only. No wildcards, no prefix match, no loopback-style port flexibility.
What it does not stop, and what we turned down
It does not stop a determined social engineer: a user talked into typing an attacker’s host and approving is phished, whichever tier they pick. We bound it rather than prevent it: the typed host, the warnings, the email, instant disconnection on removal, and a consent screen that starts at read-only and says the user trusted the address.
We first capped user-trusted addresses at write access, so a phished grant could change data but not delete it. We dropped the cap: users connecting their own agents wanted full access, and the cap was a backstop, not the defense.
Beyond the two poles above, we weighed:
A checklist for your own MCP server
- Decide where codes may go at authorize, with the signed-in user known, not at registration.
- Check every answer, errors included, against that rule before redirecting, and refuse to a page on your own host. Do it at authorize, not only at token exchange: Cloudflare’s workers-oauth-provider shipped CVE-2025-4143 in May 2025 for checking only the latter.
- Show the redirect host of this request on the consent screen, and say why it is allowed.
- Make widening the list a deliberate act outside the sign-in flow, or behind a typed confirmation.
- Email the user every time an address is added.
- Make removal revoke grants and in-flight codes.
- Refuse URIs whose host the browser and your parser read differently.
Want to try it? In your own agent, add Stream Vessel’s MCP server, https://mcp.streamvessel.com, and sign in. If your agent isn’t on our list, you’ll land on the refusal page shown above, where you can trust its address and carry on.
Auto-fetched and translated by AI Atlas. All rights belong to the original author.