Skip to content

Project-scoped tokens can read the global memory scope via memory.search include_global

Moderate
susomejias published GHSA-cc4j-ch4r-9pf5 Aug 1, 2026

Package

npm @rembric/server (npm)

Affected versions

<= 0.25.1

Patched versions

0.25.2

Description

Summary

memory.search's include_global argument widens a project-scoped search to also return global memories without re-authorizing against the global scope. A token deliberately pinned to a single project therefore reads the entire user-wide global scope.

This matters most for OAuth: the scopes minted for a /mcp/<slug> grant are exactly the project-pinned ones (apps/server/src/services/oauth.ts:343, documented at README.md:269). A connector a user consented to for one project can read all of their user-wide memory.

The authorization gap

isAuthorized (apps/server/src/services/tokens.ts:249-271) deliberately denies global reads to project-pinned tokens:

if (scope.startsWith('read:project:')) {
  const id = scope.slice('read:project:'.length);
  return action === 'read' && target.scope === 'project' && target.projectId === id;
}
if (scope.startsWith('project:')) {
  const id = scope.slice('project:'.length);
  return target.scope === 'project' && target.projectId === id;
}

docs/agents.md:25-26 states the intent: a project:<id> token "cannot call … any tool whose effective scope resolves to another project or to global".

But handleSearch (apps/server/src/mcp/memory-tools.ts:944-960) authorizes only against the project scope and then widens:

const { scope } = await resolveEffectiveScope(deps);
assertAuthorized('read', scope);          // scope is {kind:'project', projectId}

includeGlobal: args.include_global,        // widens past what was authorized

includeGlobal threads down through MemoryService.searchhybridSearch, widening both the lexical WHERE (scope-clause.ts::scopeWhere) and the dense branch (hybrid-search.ts:354-357 unions the project and global vec partitions before RRF). There is no second authorization check anywhere on that path.

Proof

Throwaway vitest probe against main (f1aa568), using a real project:<id> token scope rather than the * admin scope every existing test uses:

✓ isAuthorized denies global reads to a project-pinned token
✗ but memory.search with include_global returns global rows to that same token
  token scope 'project:01KYYJ56EZX3JKM3Z2YWJKHE2S' -> 2 rows, 1 global: [ 'SECRET user-wide preference' ]

Same result for read:project:<id>.

Independently corroborated by an external report on a real v0.25.1 deployment (public issue #299), where memory.search({include_global: true}) on /mcp/cortex-ai returned a global-scoped row.

Why it was never caught

No test exercises include_global with a restricted token. memory-tools.test.ts:628 ("path-scoped: returns only memories in the bound project — no globals leak") calls handlers.search({}) — without the argument — and every context helper in that file uses ADMIN_TOKEN_SCOPE = '*', which legitimately authorizes everything.

Companion defect: the path-scoped isolation contract is also unenforced

Same call site, same fix location, but a separate failure. openspec/specs/mcp-api/spec.md:24 says verbatim:

memory.search SHALL return only memories whose scope = 'project' and project_id equals the bound project; global memories SHALL NOT be returned. The includeGlobal argument SHALL be ignored on path-scoped connections.

with a scenario at :34-40 covering includeGlobal=true explicitly. That scenario fails today:

✗ mcp-api spec: path-scoped search must ignore includeGlobal
  admin token on /mcp/<slug> -> [ 'project:project-A preference', 'global:SECRET user-wide preference' ]

This one is not an authorization break (the token above is *), but it is the more important half operationally: the shipped plugin only ever opens path-scoped connections (one MCP entry per client, apps/plugin/.claude-plugin/mcp.json; the bridge path-scopes whenever a .rembric exists, apps/plugin/bin/rembric-bridge.mjs:56-72). Enforcing mcp-api:24 therefore closes the authorization gap by construction for every plugin user, because the argument never takes effect on their connections at all.

Fix (shipped)

Fixed in 1f070bc, released in server v0.25.2. Two guards at the read path, mirroring gates that already existed:

Two guards, both at the read path, mirroring gates that already exist:

  1. include_global is ignored on path-scoped connections. Discriminator is ctx.requestedSlug !== null — the same one the write-side scope_locked gate already uses (memory-tools.ts:786, whose comment spells out that "path-scoped" means the URL carried a slug regardless of whether it resolved). Ignore silently per the spec's wording, rather than erroring.
  2. The widening is gated on global authorization for the remaining case (/mcp with an active project.use, where memory/spec.md:77 legitimately allows it): isAuthorized(ctx.scope, 'read', { scope: 'global' }). On denial, drop the widening and serve the project-only result rather than throwing — a legitimate client passing the flag with a restricted token should degrade, not break.

Note the two specs are not in conflict once read by layer: mcp-api:24 constrains the MCP transport when path-scoped, memory/spec.md:77 constrains the service layer generally. The code simply never enforced the transport-layer clause. Neither spec's text needs to change.

Affected versions and how to upgrade

Rembric is distributed as a Docker image, not as an npm package. @rembric/server is private: true and is published to no registry, so the structured package field on this advisory is informational only and will not raise a Dependabot alert for anyone. What you actually run is:

Affected ghcr.io/susomejias/rembric up to and including 0.25.1
Fixed in ghcr.io/susomejias/rembric:0.25.2

Upgrade with the TUI installer (Server -> update), with the server's own self-update action, or by setting REMBRIC_VERSION=0.25.2 and running docker compose up -d.

There is no workaround short of upgrading. If you cannot upgrade immediately, revoke any project:<id> / read:project:<id> token you do not fully trust and revoke OAuth connector grants scoped to a single project — those are the only credentials that gain anything from this defect.

Impact assessment

  • Confidentiality, single axis. No write path is affected: the scope_locked gate already blocks global writes on path-scoped connections, and isAuthorized is correctly consulted for writes.
  • Requires a token whose scope is project:<id> or read:project:<id>, plus a deliberate include_global: true. An * or read:* token is authorized for global anyway, so nothing is escalated there.
  • Self-hosted single-operator deployments — the common case — are largely unaffected in practice, since the operator owns every scope. The exposure is real for multi-project instances that mint per-project tokens, and for any instance using OAuth connectors, where consent is per-project by design.
  • Not remotely triggerable without a valid token.

Tests (landed with the fix)

  1. A project:<id> and a read:project:<id> token get zero global rows from memory.search({include_global: true}). Was failing; now covered, and mutation-checked so the guard cannot be weakened silently.
  2. The mcp-api/spec.md:34-40 scenario, with include_global: true on a path-scoped connection. Was uncovered; now covered.
  3. A * token on /mcp with an active project.use still gets globals with the flag — i.e. the legitimate memory/spec.md:77 case is not regressed.
  4. Write paths unchanged.

Reported alongside public issues #298/#299/#300 from @ESJavadex; #299's repro is what led here, though it was filed as a retrieval-quality issue rather than an authorization one.

How the gap was introduced (provenance)

An internal audit dated 2026-07-11 recorded include_global as not implemented at all, flagged as spec drift against memory/spec.md:77-82 ("the biggest recall gap; scopeCondition is project-only XOR global-only"). It was implemented the next day by the improve-recall-and-plugin-parity change, whose design.md:103 accepted the two-partition kNN scan cost specifically because "it's opt-in (include_global defaults to false)".

So the feature was built to satisfy memory/spec.md:77 and shipped without ever being cross-checked against mcp-api/spec.md:24 — which had constrained it since before it existed — or against isAuthorized. That is why the drift closed in one direction and opened in two others, and why no test covers the combination.

Practical consequence for the fix: the widening's cost mitigation was "it's opt-in". Enforcing mcp-api:24 makes it a no-op on path-scoped connections, which is where all four plugin clients live — so the fix also removes that accepted cost for the overwhelming majority of traffic, rather than adding any.

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

CVE ID

No known CVE

Weaknesses

Incorrect Authorization

The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. Learn more on MITRE.