fix(prompts-core): block prompt path traversal

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-05-24 17:50:18 +09:00
parent 74d5f7018d
commit 436c618706
4 changed files with 46 additions and 10 deletions
+23 -2
View File
@@ -1,6 +1,6 @@
import { parseFrontmatter } from "@oh-my-opencode/utils"
import { readFile } from "node:fs/promises"
import { join } from "node:path"
import { isAbsolute, relative, resolve } from "node:path"
import type { LoadedPrompt, LoadPromptInput, RuntimeInjection } from "./types"
export class PromptFileNotFoundError extends Error {
@@ -16,10 +16,21 @@ export class PromptFileNotFoundError extends Error {
}
}
export class PromptPathTraversalError extends Error {
readonly name = "PromptPathTraversalError"
constructor(
readonly promptName: string,
readonly variant: string
) {
super(`Prompt path escapes source directory for ${promptName}/${variant}`)
}
}
export async function loadPrompt<TFrontmatter = Record<string, unknown>>(
input: LoadPromptInput
): Promise<LoadedPrompt<TFrontmatter>> {
const filePath = join(input.source.baseDir, input.name, `${input.variant}.md`)
const filePath = resolvePromptFilePath(input.source.baseDir, input.name, input.variant)
const content = await readPromptFile(input.name, input.variant, filePath)
const parsed = parseFrontmatter<TFrontmatter>(content)
const body = await applyRuntimeInjections(parsed.body, input.inject ?? [])
@@ -33,6 +44,16 @@ export async function loadPrompt<TFrontmatter = Record<string, unknown>>(
}
}
function resolvePromptFilePath(baseDir: string, promptName: string, variant: string): string {
const resolvedBaseDir = resolve(baseDir)
const filePath = resolve(resolvedBaseDir, promptName, `${variant}.md`)
const relativePath = relative(resolvedBaseDir, filePath)
if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
throw new PromptPathTraversalError(promptName, variant)
}
return filePath
}
async function readPromptFile(promptName: string, variant: string, filePath: string): Promise<string> {
try {
return await readFile(filePath, "utf8")