nexu-io/open-design · error · Error
path escapes project dir
Error message
path escapes project dir
What it means
safeJoin resolves path.resolve(root, relPath) and requires the result to start with root + path.sep (or equal root). This catches `..` traversal that resolves outside the project directory — a defense-in-depth guard that runs after validateProjectPath has already normalized the name. Maps to HTTP 400.
Source
Thrown at apps/daemon/src/design/claude-design-import.ts:279
return validateProjectPath(name);
}
function chooseEntryFile(paths: string[]): string | null {
const html = paths.filter((p) => /\.html?$/i.test(p));
if (html.length === 0) return null;
const lower = new Map(html.map((p) => [p.toLowerCase(), p]));
return (
lower.get('index.html') ??
html.find((p) => !p.includes('/')) ??
html[0] ??
null
);
}
function safeJoin(root: string, relPath: string): string {
const target = path.resolve(root, relPath);
if (!target.startsWith(root + path.sep) && target !== root) {
throw new Error('path escapes project dir');
}
return target;
}
View on GitHub (pinned to 5be4028344)
Solutions
- Treat this as a security signal — do not import the archive as-is.
- Audit the entry names with `unzip -l` for traversal patterns.
- Re-create the zip from trusted sources with a flat or cleanly-relative structure.
- If the project dir contains symlinks, remove them before importing.
Defensive patterns
Strategy: try-catch
Validate before calling
// Defense-in-depth: ensure the project dir has no symlinks that an entry
// could resolve through to escape.
import { readdirSync, lstatSync } from 'node:fs';
import { join } from 'node:path';
function dirHasSymlinks(dir: string): boolean {
for (const entry of readdirSync(dir)) {
if (lstatSync(join(dir, entry)).isSymbolicLink()) return true;
}
return false;
} Try / catch
try {
await importClaudeDesignZip(zipPath, projectDir);
} catch (err) {
if (String(err).includes('path escapes project dir')) {
// Security signal: treat as untrusted archive.
return res.status(400).json({ error: 'archive refused for safety' });
}
throw err;
} Prevention
- Keep the project import directory free of symlinks.
- Treat this error as a security signal; do not silently retry.
- Re-create archives from trusted sources with flat/relative layouts.
When it happens
Trigger: An entry name that, after normalization, still resolves outside the project root — e.g. via platform-specific path resolution, a symlink in the project dir, or a regression in validateProjectPath's FORBIDDEN_SEGMENT check.
Common situations: Symlink-based escapes inside the project directory; crafted archives from untrusted sources; edge cases in Windows path normalization; a legitimate entry that happens to collide with a reserved segment path.
Related errors
- invalid zip file name
- absolute zip paths are not allowed
- encrypted zip entries are not supported
- invalid brand id: ${input.brandId}
- invalid design system id: ${designSystemId}
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/b996bf05a6d8aa16.
Report an issue: GitHub.