{"record":{"id":"23356cf23c872363","repo":"NousResearch/hermes-agent","slug":"caller-illegal-path-traversal-in-path","errorCode":null,"errorMessage":"${caller}: illegal path traversal in \"${path}\"","messagePattern":"(.+?): illegal path traversal in \"(.+?)\"","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"apps/desktop/src/hermes.ts","lineNumber":285,"sourceCode":"/** Options for a plugin REST call — mirrors the app's own `hermesDesktop.api`\n *  shape, minus the path (which is namespace-derived). */\nexport interface PluginRestOptions {\n  method?: string\n  body?: unknown\n  /** Single-file multipart upload (see HermesApiRequest.upload). */\n  upload?: { filename: string; contentType?: string; bytes: ArrayBuffer }\n  timeoutMs?: number\n}\n\n// Normalize `path` to a leading-slash suffix relative to `/api/plugins/<id>`.\n// The namespace is the boundary — reject `..` so a relative segment can't\n// normalize out into another plugin's API or a core route. Check the path\n// portion only (before any query/hash).\nfunction pluginPathSuffix(caller: string, path: string): string {\n  const suffix = path.startsWith('/') ? path : `/${path}`\n\n  if (suffix.split(/[?#]/, 1)[0].split('/').includes('..')) {\n    throw new Error(`${caller}: illegal path traversal in \"${path}\"`)\n  }\n\n  return suffix\n}\n\n/** The plugin REST door. Every call is scoped BY CONSTRUCTION to the plugin's\n *  own backend namespace — `path` is relative to `/api/plugins/<pluginId>`\n *  ('/board' → `/api/plugins/kanban/board`), so a plugin can't address another\n *  plugin's API or a core route through it. Profile-aware like every desktop\n *  REST call. Broader reach (core endpoints, another namespace) is the future\n *  declared-capability seam; today the namespace IS the boundary. */\nexport async function pluginRest<T>(pluginId: string, path: string, opts: PluginRestOptions = {}): Promise<T> {\n  if (!window.hermesDesktop?.api) {\n    throw new Error('Hermes desktop bridge unavailable')\n  }\n\n  const suffix = pluginPathSuffix('pluginRest', path)\n","sourceCodeStart":267,"sourceCodeEnd":303,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/apps/desktop/src/hermes.ts#L267-L303","documentation":"Thrown by pluginPathSuffix() in apps/desktop/src/hermes.ts:285 when a `path` passed to the plugin REST door contains a `..` segment. pluginRest() scopes every request by construction to `/api/plugins/<pluginId>`; the `..` rejection is what prevents a relative path from normalizing up into another plugin's API namespace or a core route. Only the path portion (before any `?query` or `#hash`) is checked.","triggerScenarios":"Calling pluginRest(pluginId, '../other-plugin/board'); building a path by concatenation that yields '/a/../b'; passing a user-supplied relative path like 'items/../../admin' into a plugin REST helper; `..` appearing as a whole segment (`'a/..b'` is fine, `'a/..'` is not).","commonSituations":"A plugin naively joining ids into a URL: `${base}/${id}` where id contains traversal; UI code forwarding a typed route that includes parent-directory shorthand; attempting to reach a sibling plugin's endpoint or a core endpoint through the namespaced door.","solutions":["Keep pluginRest paths strictly inside the plugin's own namespace and always start them with '/' (e.g. '/board', '/items/42').","Sanitize user/agent-supplied segments before interpolation: reject or encodeURIComponent anything containing '..' as a segment.","If you genuinely need another plugin's API or a core route, do not use pluginRest — call the appropriate dedicated API helper; the namespace IS the boundary.","Strip traversal at the source: normalize the path first and assert no '..' segment remains before calling."],"exampleFix":"// before\nawait pluginRest('kanban', `../settings/all`)\nawait pluginRest('kanban', `/board/${userInput}`) // userInput = '../../x'\n\n// after\nawait hermesApi('/api/settings/all')          // core route via its own helper\nawait pluginRest('kanban', `/board/${encodeURIComponent(userInput)}`)","handlingStrategy":"validation","validationCode":"function safePluginPath(path: string): string {\n  const suffix = path.startsWith('/') ? path : `/${path}`\n  const pathOnly = suffix.split(/[?#]/, 1)[0]\n  if (pathOnly.split('/').includes('..')) throw new Error(`illegal path: ${path}`)\n  return suffix\n}\nconst clean = `/items/${encodeURIComponent(userSuppliedId)}`\nsafePluginPath(clean)","typeGuard":null,"tryCatchPattern":"try { await pluginRest(id, path) } catch (e) { if (e instanceof Error && e.message.includes('illegal path traversal')) { /* fix path construction; do NOT strip '..' blindly and retry */ } }","preventionTips":["Build plugin paths from a fixed table, not string concatenation of user input","encodeURIComponent every interpolated segment","Never attempt cross-plugin or core routes through pluginRest","Treat hitting this error as a security signal, log it — don't silently normalize"],"tags":["desktop","plugins","security","path-traversal","rest"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}