{"record":{"id":"b55626915b274be3","repo":"paperclipai/paperclip","slug":"plugin-migrationsdir-escapes-package-root-migra","errorCode":null,"errorMessage":"Plugin migrationsDir escapes package root: ${migrationsDir}","messagePattern":"Plugin migrationsDir escapes package root: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"server/src/services/plugin-database.ts","lineNumber":341,"sourceCode":"    throw new Error(\"Every ctx.db parameter must be referenced by a $n placeholder\");\n  }\n  return sql.join(chunks, sql.raw(\"\"));\n}\n\nasync function listSqlMigrationFiles(migrationsDir: string): Promise<string[]> {\n  const entries = await readdir(migrationsDir, { withFileTypes: true });\n  return entries\n    .filter((entry) => entry.isFile() && entry.name.endsWith(\".sql\"))\n    .map((entry) => entry.name)\n    .sort((a, b) => a.localeCompare(b));\n}\n\nfunction resolveMigrationsDir(packageRoot: string, migrationsDir: string): string {\n  const resolvedRoot = path.resolve(packageRoot);\n  const resolvedDir = path.resolve(resolvedRoot, migrationsDir);\n  const relative = path.relative(resolvedRoot, resolvedDir);\n  if (relative.startsWith(\"..\") || path.isAbsolute(relative)) {\n    throw new Error(`Plugin migrationsDir escapes package root: ${migrationsDir}`);\n  }\n  return resolvedDir;\n}\n\ntype PluginDatabaseClient = Pick<Db, \"select\" | \"insert\" | \"update\" | \"execute\">;\ntype PluginDatabaseRootClient = PluginDatabaseClient & Partial<Pick<Db, \"transaction\">>;\n\nexport interface ApplyPluginMigrationsOptions {\n  /**\n   * Persist failed migration ledger rows. Fresh install uses false because the\n   * caller owns a larger transaction and must roll back the plugin row and\n   * namespace together.\n   */\n  persistFailure?: boolean;\n}\n\nexport function pluginDatabaseService(db: PluginDatabaseRootClient) {\n  async function getPluginRecord(pluginId: string) {","sourceCodeStart":323,"sourceCodeEnd":359,"githubUrl":"https://github.com/paperclipai/paperclip/blob/120ae5428fa29bee300bcf806491cd4d965fbb7c/server/src/services/plugin-database.ts#L323-L359","documentation":"Path-traversal guard in resolveMigrationsDir: after joining and resolving, the plugin's configured migrationsDir resolves outside the plugin package root. A malicious or misconfigured manifest could otherwise point migrations at arbitrary host directories; the escaping migrationsDir value is at fault.","triggerScenarios":"Thrown at server/src/services/plugin-database.ts:341 when the library encounters an invalid state.","commonSituations":"See trigger scenarios.","solutions":["Set migrationsDir to a path inside the plugin package root."],"exampleFix":null,"handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":[],"tags":[],"backgroundTag":null,"analyzedSha":"120ae5428fa29bee300bcf806491cd4d965fbb7c","analyzedAt":"2026-08-18T22:49:45.177Z","contentChangedAt":"2026-08-18T22:49:45.177Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}