fix(security): confine file resolution to project roots

Block traversal, out-of-root absolute path, and symlink escapes for @file references, file:// URIs, and config skill file loading while logging rejected attempts.

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-04-02 14:55:35 +09:00
parent a637cca702
commit 98659783c0
8 changed files with 287 additions and 15 deletions
+17 -3
View File
@@ -1,5 +1,7 @@
import { existsSync, readFileSync, statSync } from "fs"
import { join, isAbsolute } from "path"
import { isAbsolute, resolve } from "path"
import { isWithinProject } from "./contains-path"
import { log } from "./logger"
interface FileMatch {
fullMatch: string
@@ -30,9 +32,10 @@ function findFileReferences(text: string): FileMatch[] {
function resolveFilePath(filePath: string, cwd: string): string {
if (isAbsolute(filePath)) {
return filePath
return resolve(filePath)
}
return join(cwd, filePath)
return resolve(cwd, filePath)
}
function readFileContent(resolvedPath: string): string {
@@ -68,6 +71,17 @@ export async function resolveFileReferencesInText(
for (const match of matches) {
const resolvedPath = resolveFilePath(match.filePath, cwd)
if (!isWithinProject(resolvedPath, cwd)) {
log("[file-reference-resolver] Rejected file reference outside project root", {
filePath: match.filePath,
resolvedPath,
projectRoot: cwd,
})
replacements.set(match.fullMatch, `[path rejected: ${match.filePath}]`)
continue
}
const content = readFileContent(resolvedPath)
replacements.set(match.fullMatch, content)
}