Skip to content

Manus/multitenant remote mcp audit - #5

Open
pooyamachine1-ctrl wants to merge 5 commits into
Danialsamadi:mainfrom
pooyamachine1-ctrl:manus/multitenant-remote-mcp-audit
Open

pooyamachine1-ctrl wants to merge 5 commits into
Danialsamadi:mainfrom
pooyamachine1-ctrl:manus/multitenant-remote-mcp-audit

Conversation

@pooyamachine1-ctrl

Copy link
Copy Markdown

No description provided.

@Danialsamadi

Copy link
Copy Markdown
Owner

Hello i will take a look at it today, Thanks for your contribution

@Danialsamadi

Danialsamadi commented Sep 13, 2026 •

Copy link
Copy Markdown
Owner

Code review

Found 3 issues:

  1. The memory_write link-attach loop still uses unscoped repo.get(l.targetId) while every other object-ID path in this PR was switched to repo.getVisible(id, principal.userId, principal.teamIds). Any authenticated principal can probe whether arbitrary memory IDs exist (existence-disclosure oracle) and create cross-tenant graph edges — contradicting this PR's own docs/architecture.md policy that object-ID operations must 404 for objects outside the caller's visibility.

// Links attach even on dedup — relating existing knowledge is still useful.
for (const l of links ?? []) {
if (repo.get(l.targetId)) repo.addLink(memory.id, l.targetId, l.rel);
}

  1. The /mcp handler constructs a fresh MCP server per HTTP request via createSynapseMcpServer(repo, principal), which unconditionally runs startup side effects at construction: repo.addAudit("startup", ...) and a full runDecay(repo) sweep. The stdio server calls this once per process (with decay on a 24h interval); here every request from every tenant triggers a redundant decay sweep plus an audit row.

app.all("/mcp", async (c) => {
if (c.req.method !== "POST" && c.req.method !== "GET" && c.req.method !== "DELETE") return c.json({ error: "method_not_allowed" }, 405);
const principal = c.get("principal");
const server = createSynapseMcpServer(repo, principal);
const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });

  1. Team-scope dedup is dead code: writeMemory reads (input as CreateMemoryInput & { teamIds?: string[] }).teamIds ?? [], but CreateMemoryInput only has a singular teamId and no caller ever sets teamIds, so the expression is always []. Semantic dedup for scope: "team" writes never considers existing team memories. Likely intended input.teamId ? [input.teamId] : [] (or threading the principal's team memberships).

const candidates = repo
.listVisible(userId, (input as CreateMemoryInput & { teamIds?: string[] }).teamIds ?? [], { status: "active" })
.filter((m) => m.type === input.type);

@Danialsamadi

Copy link
Copy Markdown
Owner

@pooyamachine1-ctrl
If you explain for me what was the thought process behind the code changes here I would be really happy to discuss about this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants