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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user