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
+33
View File
@@ -0,0 +1,33 @@
import { existsSync, realpathSync } from "fs"
import { basename, dirname, isAbsolute, join, normalize, relative, resolve } from "path"
function toCanonicalPath(pathToNormalize: string): string {
const resolvedPath = resolve(pathToNormalize)
if (existsSync(resolvedPath)) {
try {
return normalize(realpathSync.native(resolvedPath))
} catch {
return normalize(resolvedPath)
}
}
const parentDirectory = dirname(resolvedPath)
const canonicalParentDirectory = existsSync(parentDirectory)
? realpathSync.native(parentDirectory)
: parentDirectory
return normalize(join(canonicalParentDirectory, basename(resolvedPath)))
}
export function containsPath(rootPath: string, candidatePath: string): boolean {
const canonicalRootPath = toCanonicalPath(rootPath)
const canonicalCandidatePath = toCanonicalPath(candidatePath)
const relativePath = relative(canonicalRootPath, canonicalCandidatePath)
return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath))
}
export function isWithinProject(candidatePath: string, projectRoot: string): boolean {
return containsPath(projectRoot, candidatePath)
}