nexu-io/open-design · critical
project_files.read_json path escapes project dir
Error message
project_files.read_json path escapes project dir
What it means
executeProjectFilesReadJson (refresh.ts:593-596) computes the realpath of both the project directory and the target file, and requires the target's realpath to be the project dir itself or begin with `<projectReal>${path.sep}`. This blocks path-traversal attempts (../, absolute paths, or symlinks that escape the project root).
Source
Thrown at apps/daemon/src/live-artifacts/refresh.ts:595
mime: file.mime ?? 'application/octet-stream',
};
if (preview !== undefined) result.preview = preview;
matches.push(result);
}
return asBoundedRefreshOutput({ toolName: 'project_files.search', query: query ?? '', count: matches.length, truncated: allFiles.length > matches.length && matches.length >= maxResults, matches });
}
async function executeProjectFilesReadJson(options: ExecuteLocalDaemonRefreshSourceOptions): Promise<BoundedJsonObject> {
const filePath = selectJsonPath(options.source.input as ProjectFilesReadJsonInput);
if (!filePath.endsWith('.json')) throw new Error('project_files.read_json only supports .json files');
const dir = projectDir(options.projectsRoot, options.projectId);
const target = path.resolve(dir, filePath);
const [dirReal, targetLinkStat] = await Promise.all([realpath(dir), lstat(target)]);
if (targetLinkStat.isSymbolicLink()) throw new Error('project_files.read_json does not follow symlinks');
const targetReal = await realpath(target);
if (!targetReal.startsWith(`${dirReal}${path.sep}`) && targetReal !== dirReal) {
throw new Error('project_files.read_json path escapes project dir');
}
const entryStat = await stat(targetReal);
if (!entryStat.isFile()) throw new Error('project_files.read_json path must be a file');
if (entryStat.size > 256 * 1024) throw new Error('project_files.read_json file exceeds 256KB');
if (options.signal?.aborted === true) throw options.signal.reason;
let parsed: BoundedJsonValue;
try {
parsed = JSON.parse(await readFile(targetReal, 'utf8')) as BoundedJsonValue;
} catch {
throw new Error(`project_files.read_json could not parse JSON at ${filePath}`);
}
return asBoundedRefreshOutput({ toolName: 'project_files.read_json', path: filePath, size: entryStat.size, json: parsed });
}
function compactExecOutput(value: string): string[] {
return value.split('\n').map((line) => line.trimEnd()).filter(Boolean).slice(0, 100);
}
View on GitHub (pinned to 5be4028344)
Solutions
- Use a path that stays inside the project directory (relative, no '..' escapes).
- Verify projectsRoot is the realpath of the project dir; if it is a symlink, resolve it before calling refresh.
- Reject paths containing '..' or absolute paths at the input boundary.
Example fix
// before
const input = { path: '../../../shared/config.json' };
// throws `project_files.read_json path escapes project dir`
// after: keep the file inside the project
const input = { path: 'config.json' }; Defensive patterns
Strategy: validation
Validate before calling
import { realpath } from 'node:fs/promises';
import path from 'node:path';
async function assertInsideProject(dir: string, target: string): Promise<void> {
const [dirReal, targetReal] = await Promise.all([realpath(dir), realpath(target)]);
if (targetReal !== dirReal && !targetReal.startsWith(dirReal + path.sep)) {
throw new Error('path escapes project dir');
}
} Type guard
function isRelativeInside(p: string): boolean {
return !path.isAbsolute(p) && !p.split(path.sep).includes('..');
} Prevention
- Reject absolute paths and '..' segments at the input boundary.
- Ensure projectsRoot is the realpath of the project dir before refresh.
- Treat any user-supplied file path as untrusted and confine it to the project root.
When it happens
Trigger: input.path resolves to a real location outside the project directory, e.g. '../../../etc/passwd', an absolute path like '/etc/secrets.json', or a path that (after realpath) lands outside the project.
Common situations: Relative traversal in the path; an absolute path supplied by mistake or malice; a directory layout where the project dir is itself a symlink whose realpath differs from the supplied projectsRoot.
Related errors
- project_files.read_json does not follow symlinks
- invalid brand id: ${input.brandId}
- invalid design system id: ${designSystemId}
- ${RESOURCE_ROOT_ENV} must be under the workspace root or app
- invalid zip file name
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/86282fb588bcc8c0.
Report an issue: GitHub.