| Description | # Agent Teams AI Local Fastify Sidecar Unauthenticated Sensitive Information Disclosure
**Affected product**: Agent Teams AI desktop application (Electron), verified on version 2.1.2 (Windows x64)
**Vulnerability type**: Missing authentication / local sensitive information disclosure (CWE-306, CWE-200)
**Threat scenario**: Any process on the same machine, including browser extensions and local contexts reachable from a web page
## 1. Vulnerability Description
While Agent Teams AI is running, it starts a Fastify-based local HTTP service (a sidecar) that serves configuration, project, session, search, team, SSH, notification, and token-usage data to the renderer process. The service binds to `127.0.0.1` and picks a port starting at `3456` (incrementing up to `3466` on collision). It runs under two conditions: when `httpServer.enabled` is set to `true` in the configuration, the server starts together with the application; when team control functionality needs a control URL, the server is started on demand. Users are generally unaware that this service exists.
The problem is that **none of the `/api/*` routes have any authentication, session validation, or request-token verification**. The only line of defense is a CORS origin check, but CORS only restricts read access for "cross-origin pages inside a browser." It has no effect on non-browser local processes: browser extensions (which can bypass the same-origin policy and read responses once host permissions are granted), any local program running with low privileges, or even a malicious script on the same machine can send ordinary HTTP GET requests to `127.0.0.1:3456-3466` and read the full responses without any credential.
Without any user interaction, an attacker can silently read:
- The full application configuration: the Claude root directory, custom project paths, SSH profiles (host, username, port, identity-file path), the most recent SSH connection, provider endpoint settings, and notification/runtime settings;
- Project inventories and session data: project paths, session IDs/titles/message counts, and **complete session message contents** (including user prompts, assistant replies, tool calls with their parameters, and subagent records);
- Global session search results, including matched text and surrounding context;
- Team configurations, members, runtime state, and diagnostics (process PIDs, command lines, working directories);
- Notification history, token usage and cost data, and organization information.
This amounts to broad local privacy and intellectual-property disclosure. Although the API does not directly return provider API keys, SSH passwords, or browser cookies, the exposed SSH host/username/identity-file paths and project paths provide precise intelligence for follow-up targeted attacks such as private-key theft or tailored phishing.
## 2. Vulnerability Analysis
### 2.1 The server has no authentication layer at all
In `src/main/services/infrastructure/HttpServer.ts`, the `doStart` method creates the Fastify instance, registers only the CORS plugin, and then registers all API routes. No authentication hook of any kind is mounted in between: there is no `onRequest` verification, no bearer-token check, and no session validation:
```ts
this.app = Fastify({ logger: false });
const corsOrigin = process.env.CORS_ORIGIN;
if (corsOrigin === '*') {
await this.app.register(cors, { origin: true, credentials: true });
} else if (corsOrigin) {
const origins = corsOrigin.split(',').map((o) => o.trim());
await this.app.register(cors, { origin: origins, credentials: true });
} else {
const localhostPattern = /^https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?$/;
await this.app.register(cors, {
origin: (origin, cb) => {
if (!origin) {
cb(null, true); // <- requests without an Origin header pass directly
return;
}
if (localhostPattern.test(origin)) {
cb(null, true);
return;
}
cb(new Error('Not allowed by CORS'), false);
},
credentials: true,
});
}
```
There is a critical misunderstanding here: **CORS is not an authentication mechanism**. The `origin` callback is only exercised for browser cross-origin requests. Requests issued by local Python scripts, `curl`, or any other local process carry no `Origin` header at all, and hit `if (!origin) { cb(null, true); }`, which allows them through immediately. In other words, the CORS check only blocks one scenario - "a public web page using a browser to read the response cross-origin" - while providing zero protection against the most relevant attacker in this threat model: a local process such as a browser extension or local malware.
### 2.2 The port range is fixed and predictable
```ts
preferredPort: number = 3456,
...
for (let attempt = 0; attempt <= 10; attempt++) {
const tryPort = preferredPort + attempt;
try {
await this.app.listen({ host, port: tryPort });
this.port = tryPort;
...
```
The server always tries ports `3456` through `3466`. An attacker only needs to probe these 11 ports to locate the service; no full port scan is required.
### 2.3 Sensitive routes return internal data directly
Every route handler reads internal services and returns the complete data without any notion of caller identity. Taking `src/main/http/config.ts` as an example:
```ts
app.get('/api/config', async () => {
const config = configManager.getConfig();
return { success: true, data: config };
});
```
`getConfig()` returns the **entire application configuration object** held by `ConfigManager`, including `ssh.profiles` (SSH hosts, usernames, ports, auth methods, identity-file paths), `ssh.lastConnection`, `providerConnections` (per-provider endpoint settings), and `general.customProjectPaths`. All of it is returned verbatim to the caller.
Session routes (`src/main/http/sessions.ts`) behave the same way:
```ts
app.get<{ Params: { projectId: string } }>(
'/api/projects/:projectId/sessions',
async (request) => {
const validated = validateProjectId(request.params.projectId);
if (!validated.valid) return [];
const sessions = await services.projectScanner.listSessions(validated.value!);
return sessions;
}
);
```
`GET /api/projects/:id/sessions/:id` goes further and returns the fully parsed session content: user messages, assistant replies, tool-call inputs and outputs, subagent records, and metrics. Sessions contain the complete history of user-AI interaction plus project code context, which is highly sensitive.
### 2.4 Attack chain summary
1. The user starts Agent Teams AI normally. The sidecar listens on `127.0.0.1:3456` (or an incremented port) once enabled by configuration or triggered by team functionality;
2. A local attacker (browser extension or local process) sends an HTTP GET to `127.0.0.1:3456` without any credential;
3. The request has no `Origin` header, so the CORS callback passes it through; the route handler executes and returns the full JSON;
4. The attacker reads the response and completes the exfiltration. No alert is logged and the user notices nothing.
## 3. Proof of Concept
### Reproduction steps
1. Start the Agent Teams AI desktop application and make sure the sidecar is running (either `httpServer.enabled` is `true` in the configuration, or team control functionality has been triggered so the server started on demand);
2. Save the script below as `poc.py` and run `python poc.py` in any environment with Python 3.10+ (optional arguments are documented via `--help`; for example `--query "keyword"` tests global search, and `--project-id <id> --session-id <id>` reads a complete session);
3. The script first discovers the service on ports `3456`-`3466` via `/api/version`, then requests each sensitive endpoint and prints the returned content. In testing, every endpoint returned `HTTP 200` with full JSON data, and no token or credential was ever sent.
Sample output (excerpt):
```text
[+] Found Fastify sidecar: http://127.0.0.1:3456 (version 2.1.2)
=== full config: /api/config [HTTP 200] ===
{
"success": true,
"data": {
"general": { "customProjectPaths": ["E:\\work\\secret-project"], ... },
"ssh": { "profiles": [{ "host": "10.0.0.5", "user": "deploy", ... }], "lastConnection": { ... } },
"providerConnections": { ... }
}
}
=== ssh last connection: /api/ssh/last-connection [HTTP 200] ===
{ "success": true, "data": { "host": "10.0.0.5", "port": 22, "user": "deploy", "authMethod": "key", "identityFile": "C:\\Users\\xx\\.ssh\\id_ed25519" } }
=== teams: /api/teams [HTTP 200] ===
...
```
|
|---|