Skip to main content
Glama
jigonyoo

mcp-permission-server

by jigonyoo

mcp-permission-server

MCP 工具前面的权限层,以及一份能证明其决策的日志。

每条审计轨迹都记录发生了什么。几乎没有一条记录的信息足以重新计算 它为什么被允许,而且大多数只记录成功的内容——所以当有人问 "代理是怎么到达那个文件的",日志无法回答。它只能重复"已做出决策"这一断言。

                                 NAIVE     GUARDED
calls in the session                17          17
calls that must be denied            8           8
EXECUTED WITHOUT A GRANT             7           0
LEGITIMATE CALLS REFUSED             0           0      ← no over-blocking
DECISIONS THAT CANNOT BE            16           0      ← the number nobody reports
  RECONSTRUCTED FROM THE LOG
denials that left no trace           1           0
secrets sitting in the log           2           0
entry removable unnoticed            1           0

NAIVE 是 MCP 服务器在权限不是重点时自带的权限模型:一个允许的工具名列表,以及一份记录成功内容的日志。本次会话中的每次调用除了一次之外都通过了它。

python3 demo.py
python3 gate.py --log audit.json     # run the session
python3 gate.py --verify audit.json  # re-decide every entry from the log alone
chain:  OK - the chain is intact
replay: 17/17 entries recompute to the verdict they record
secrets in the log: none

退出码:0 全部允许,1 有拒绝,2 日志无法验证。


一项授权同时是四样东西

也就是一个有效授权仍然无法覆盖一次调用的四种独立方式——也是为什么*"这个调用者是否有此工具的权限"*是错误的问题。

{"id": "G1", "tool": "fs.read", "scope": "/data/reports",
 "purpose": "q3-report", "expires_at": 100, "max_uses": 10}

调用

问题所在

还有什么会让它被允许

fs.read /data/reports-private/salaries.csv

一个以相同字符开头但不同的目录

工具 ✓ 目的 ✓ 过期 ✓

fs.read /data/reports/../reports-private/salaries.csv

同一个目录,但通过被授权的那个目录绕路到达

工具 ✓ 目的 ✓ 过期 ✓

fs.read /data/reports/q4.csv 在 t=140

授权在四十个时钟周期前就过期了。调用本身没有任何问题

工具 ✓ 范围 ✓ 目的 ✓

对只够两次的授权进行的第三次 fs.write

授权既是预算也是权限

工具 ✓ 范围 ✓ 目的 ✓

为 marketing-export 执行 fs.read /data/reports/q3.csv

与本次会话中的第一次调用完全相同,除了发起它的原因

工具 ✓ 范围 ✓ 过期 ✓

render_template /templates/leak.md

模板拉入了 /etc/service-token,调用者对此既没有授权也从未提及

工具 ✓ 范围 ✓ 目的 ✓ 过期 ✓

secrets.get DB_ADMIN_PASSWORD

持有一个密钥的授权不等于持有整个密钥环

工具 ✓ 目的 ✓ 过期 ✓

有一个测试针对以上每一项,只关闭它所指名的检查,并要求该调用能够通过。一个被多个检查同时捕获的拒绝,无法证明其中任何一个检查的必要性。


Related MCP server: SentinelMCP

困惑的代理

render_template 已被授权。/templates/leak.md 在授权范围内。 模板包含 {{include:/etc/service-token}}。

服务器持有调用者没有的权利,并且被要求将这些权利花在一个调用者从未提及的资源上。一个只检查参数中资源的权限层,恰恰在最需要它的工具上检查错了东西。

所以检查会在调用触及某资源之前先查看它——然后仍然对 /templates/report.md 说"是",因为它触及了 /data/reports/q3.csv,而调用者确实持有对该文件的授权。权限检查的一半是那个"是"。 一条因为模板可以包含内容就阻止所有模板的规则,不是规则。


路径包含,这通常是出错的地方

两个错误都只有一行。

path.startswith(scope)     # /data/reports-private is "inside" /data/reports
".." in path               # and this breaks ordinary callers to catch nothing

第二个错误值得单独讨论。/data/reports/../reports/q3.csv 解析后回到授权范围之内,它在语料库中是一个必须被允许的调用。禁止这些字符是廉价的修复;解析路径能捕获禁止字符所能捕获的一切,而且不会误伤任何不该误伤的东西。

这里的解析是词法层面的,绝不触碰文件系统——有一个测试会搜索 realpath、os.stat 和 open(。决策在调用之前做出,基于调用者提供的路径,它不能因为碰巧存在什么而给出不同的答案。


日志才是产品

两个属性使它成为证据而非日记。

它记录拒绝。 一份只记录成功内容的日志无法回答"代理尝试了什么",而这正是事后任何人都会问的问题。在这里,十七条记录中有八条是拒绝,每条都带有做出拒绝的检查以及用平实语言写出的原因。

它可以被重放。 每条记录都带有它据以决策的授权和策略版本,因此可以仅凭日志重新计算决策,并与它记录的结果进行比较:

python3 gate.py --verify audit.json
replay: 17/17 entries recompute to the verdict they record

当 replay 字段关闭时,这个数字是 0/17——记录仍然说明做出了什么决策,但没有人能验证它。

而且记录是哈希链式的,因此删除或修改一条记录是可见的:

entry 6 does not follow the one before it -- something was removed or edited

秘密永远不会进入日志。 一份保存着秘密的审计轨迹就是秘密的第二份副本,存放在一个更多人能读取的文件里。secrets.get 在本次会话中被允许,但它的值没有出现在日志的任何地方;而朴素日志中包含两个。


九项检查,以及每项检查证明自己价值的证据

check         what turning it off lets through
grant          1 more unauthorised, 1 more unreconstructable, 1 more egress
scope          3 more unauthorised, 3 more unreconstructable
expiry         2 more unauthorised, 2 more unreconstructable
purpose        1 more unauthorised, 1 more unreconstructable
deputy         1 more unauthorised, 1 more unreconstructable
redact         1 more secret in the log
deny_log       8 more denials that left no trace
replay        17 more decisions that cannot be reconstructed
chain          1 more entry removable unnoticed
python3 ablate.py

有一个值得记录的修正:scope 消融的第一个版本并没有关闭检查——它替换成了字符串前缀比较,这是一个不同的检查而不是没有检查,而且它继续静默地拒绝一个本应证明检查必要性的调用。关闭一项检查必须意味着真正关闭它,否则消融测量的是一次替换。

同一个会话还暴露了一个授权,其规模使得最后一次调用被拒绝是因为多用了一次,而不是因为命名了一个它未覆盖的密钥。它在测试错误的检查并且通过了。修复在测试夹具中,而发现这个问题的测试正是那个要求每次拒绝都隔离自身检查的测试。


已知限制,明说而非隐藏

  • 这是决策层,不是传输层。 它做决策,针对进程内的工具表执行,并记录日志。将其接入真正的 MCP 传输是普通工作;这里的内容是通常缺失的那部分,而且它在无网络、无密钥的情况下运行。

  • 授权是写下来的,不是询问得来的。 真正的部署会询问一个人并存储答案。那个答案必须包含什么——工具、范围、目的、过期、使用次数——才是这里论证的部分。

  • purpose 由调用者声明并被照单全收。 一个在目的上撒谎的调用者会击败这项检查。它仍然买到了一些真实的东西:声明的目的在日志中,所以谎言被记录在案,而且授权的范围并没有为了迁就它而扩大。

  • 代理检查只知道一个工具的影响范围。 render_template 解析它的包含项。一个服务器无法计算其影响范围的工具无法用这种方式检查,对这种工具诚实的答案是更窄的授权,而不是更宽的检查。

  • 链证明的是顺序和完整性,不是保管权。 任何能重写整个文件的人都能重写整条链。它使选择性编辑可见,这是现实中的威胁;追加式存储或外部锚点才能让其余部分可见,而那是部署工作。


布局

fixtures/spec.py     the session, the grants, the world, and the answer key
build_fixtures.py    renders session.json, policy.json, world.json, truth.json
mcpgate/scope.py     is this path inside that scope
mcpgate/policy.py    a grant is four things at once
mcpgate/tools.py     the tool table, and what a call actually reaches
mcpgate/audit.py     the hash-chained, redacted, replayable log
mcpgate/server.py    naive() and guarded(), and replay()
gate.py              the CLI: run a session, or verify a log
score.py             every number above
ablate.py            each check off, one at a time
tests/               38 tests

运行它

python3 demo.py                        # both servers + ablation
python3 gate.py --log audit.json       # the product
python3 gate.py --verify audit.json    # the proof
python3 -m pytest tests -q             # 38 tests

Docker,关闭网络:

docker compose run --rm gate

测试

一个断言允许列表仍然几乎放行所有内容。一个重放每条记录的决策并要求相同的结果。一个要求每次拒绝仅仅因为其指名的检查而存活,这样消融数字才名副其实。一个在路径解析器中搜索文件系统调用。

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Enforces identity-based access control and audit logging for MCP servers, letting you grant fine-grained tool permissions to users and systems while failing secure by default.
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enforces deterministic security policies as an inline firewall for MCP server tool calls, with AST-based validation, cryptographic audit logging, and CLI-based evaluation and verification.
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    Enables policy-governed MCP interactions with deterministic authorization, tenant isolation, minimized PII exposure, and human approval gates for sensitive mutations, while producing structured audit events.
    3
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables MCP clients to govern downstream tool servers by enforcing default-deny authority and attestation on every tool call, with live monitoring, approval, and mid-session revocation.
    3
    Apache 2.0