| Mô tả | ## 1. Vulnerability Description
While the Memmy desktop application is running, it starts a local Memory Service that listens by default on:
```text
http://127.0.0.1:18960
```
After the user starts Memmy and creates or starts an Agent, the application initializes the Agent workspace and its managed local runtime services, including the Memory Service. If the user then visits a malicious web page, that page can send the following request to the local service without any credentials:
```http
POST /api/v1/admin/shutdown
```
After the request succeeds, the Memory Service stops. Because this service is a core internal component responsible for memory storage, retrieval, and runtime data support, stopping it makes related application functionality unavailable and causes a local application denial-of-service condition.
The default conditions required for this vulnerability are:
1. The Memory Service has no configured token.
2. The service enables anonymous access when no token is present.
3. The anonymous principal receives the wildcard permission `scopes: ["*"]`.
4. The service returns `Access-Control-Allow-Origin: *`, allowing any web page to read the response.
5. The packaged Memory child process provides a managed shutdown callback.
This issue is a denial-of-service vulnerability caused by the combination of missing authentication on a local administrative endpoint and an overly permissive CORS policy:
- CWE-306: Missing Authentication for Critical Function
- CWE-284: Improper Access Control
- CWE-942: Permissive Cross-domain Policy
- CWE-400: Uncontrolled Resource Consumption
Recommended severity: **High**
CVSS 3.1 reference vector:
```text
AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H
Score: 6.5 (Medium)
```
Although the isolated numeric score is medium-to-high, this endpoint is an internal application management interface that can be triggered reliably by an arbitrary web page without reading a complex response or exploiting a race condition. The practical risk should be treated as high.
## 2. Vulnerability Root Cause
### 2.1 The Desktop Runtime Starts a Managed Memory Child Process
The packaged Electron application manages local child processes through `App/shell/desktop/src/main/runtime-services.ts`. During runtime startup, it creates an `AgentGatewaySupervisor` and calls `ensureMemoryService()`:
```ts
const gatewaySupervisor = new AgentGatewaySupervisor(
entries,
runtimeConfig,
children,
options,
{},
browserPreparationAttemptId
);
...
const memoryReady = ensureMemoryService(
entries,
runtimeConfig,
children,
options
);
```
`ensureMemoryService()` starts the Memory child process:
```ts
const memoryChild = spawnNodeService(
"memory",
entries.memoryEntry,
[
"--config",
runtimeConfig.configPath,
"--host",
runtimeConfig.memoryListenHost,
"--port",
String(runtimeConfig.memoryListenPort),
"--db",
runtimeConfig.memoryDatabasePath
],
{
MEMMY_CONFIG: runtimeConfig.configPath,
MEMMY_MEMORY_URL: runtimeConfig.memoryBaseUrl,
MEMMY_MEMORY_TOKEN: runtimeConfig.memoryToken,
...
},
...
);
```
In the normal workflow where a user creates or starts an Agent, this managed runtime is initialized. The target user does not need to open any debugging tool. When the application and Agent runtime are in their normal working state, the local Memory Service may already be listening on `127.0.0.1:18960`.
### 2.2 Anonymous Access Is Enabled by Default When No Token Is Configured
`Memory/src/server/index.ts` selects an authentication mode when starting the Memory HTTP service:
```ts
const listening = await listenMemoryHttpServer({
service,
host,
port,
timeZone: config.timeZone,
onShutdownRequested: () => requestShutdown?.(),
auth: config.storage.token
? { localServiceToken: config.storage.token }
: { allowAnonymous: true }
});
```
When no token exists in the configuration, the service passes:
```ts
{ allowAnonymous: true }
```
The desktop runtime resolves the Memory token as follows:
```ts
const memoryToken = stringValue(env.MEMMY_MEMORY_TOKEN) ??
stringValue(env.MEMMY_SERVICE_TOKEN) ??
stringValue(storage.token) ??
"";
```
Without an environment variable or configured token, `memoryToken` is an empty string. The service does not automatically generate a high-entropy random token. Therefore, in a default installation or an environment that has not explicitly configured a token, the Memory API is anonymously accessible.
### 2.3 The Anonymous Principal Receives Wildcard Permissions
The `authenticate()` function in `Memory/src/server/http.ts` handles anonymous requests:
```ts
if (!localToken && (!auth || auth.allowAnonymous === true)) {
return {
kind: "anonymous",
namespace: namespaceFromRequest(request, url),
scopes: ["*"]
};
}
```
The critical issue is that the anonymous principal receives:
```ts
scopes: ["*"]
```
The shutdown route requires admin write permission:
```ts
if (
method === "POST" &&
path === "/api/v1/admin/shutdown"
) {
requireAdminWrite(principal);
...
}
```
`requireAdminWrite()` eventually delegates to `requireAnyScope()`:
```ts
function requireAnyScope(
principal: AuthPrincipal,
allowed: string[]
): void {
if (principal.scopes.includes("*")) {
return;
}
...
}
```
Because the anonymous principal has `"*"`, `requireAdminWrite()` returns immediately. An administrative shutdown endpoint is therefore callable by a request with no credentials.
### 2.4 CORS Allows Any Web Page to Send the Request and Read the Response
The HTTP server calls `setCors(response)` before handling a request:
```ts
const server = createServer(async (request, response) => {
...
setCors(response);
if (request.method === "OPTIONS") {
response.writeHead(204);
response.end();
return;
}
...
});
```
The implementation of `setCors()` is:
```ts
function setCors(response: ServerResponse): void {
response.setHeader(
"access-control-allow-origin",
"*"
);
response.setHeader(
"access-control-allow-methods",
"GET,POST,DELETE,OPTIONS"
);
response.setHeader(
"access-control-allow-headers",
[
"content-type",
"authorization",
"x-api-key",
...
].join(",")
);
}
```
This has two consequences:
1. A page from any origin can send `POST /api/v1/admin/shutdown` to local `127.0.0.1:18960`.
2. The response contains `Access-Control-Allow-Origin: *`, so page JavaScript can read the HTTP status and response body.
Binding to `127.0.0.1` does not prevent a web page from accessing the local service. The browser's same-origin policy compares the page origin with the target URL origin; it does not automatically trust a target merely because it is an internal local process.
### 2.5 The Shutdown Route Causes the Managed Process to Exit
In `createMemoryHttpServer()`, `canShutdown` is determined by whether an `onShutdownRequested` callback is provided:
```ts
const result = await routeRequest(
options.service,
autoWorker,
request.method,
url,
body,
principal,
Boolean(options.onShutdownRequested),
pluginRuntimeAnalytics,
requestId
);
```
The shutdown route is:
```ts
if (
method === "POST" &&
path === "/api/v1/admin/shutdown"
) {
requireAdminWrite(principal);
if (!canShutdown) {
throw new MemoryServiceError(
"conflict",
"memory service restart is not managed by this server"
);
}
return {
accepted: true,
serverTime: new Date().toISOString()
};
}
```
When the desktop runtime manages the service, `onShutdownRequested` exists and `canShutdown` is true. After the response finishes, the HTTP server invokes the callback:
```ts
if (
request.method === "POST" &&
url.pathname === "/api/v1/admin/shutdown"
) {
response.once("finish", () =>
options.onShutdownRequested?.()
);
}
```
In `Memory/src/server/index.ts`, the callback resolves the shutdown promise:
```ts
let requestShutdown: (() => void) | undefined;
const shutdownRequested = new Promise<void>(
(resolveShutdown) => {
requestShutdown = resolveShutdown;
}
);
...
const listening = await listenMemoryHttpServer({
...
onShutdownRequested: () => requestShutdown?.(),
...
});
...
await shutdownRequested;
```
After `shutdownRequested` resolves, the main function enters its `finally` block:
```ts
finally {
process.off("SIGINT", handleShutdownSignal);
process.off("SIGTERM", handleShutdownSignal);
if (server) {
await closeHttpServer(server);
}
backend?.close();
serverLock?.release();
} |
|---|