nexu-io/open-design · error · Error
invalid zip file name
Error message
invalid zip file name
What it means
sanitizeZipPath rejects any entry name containing a null byte (\0). Null bytes can terminate strings early in C-based path handlers and are a classic path-injection vector. This guard fires before validateProjectPath. Maps to HTTP 400.
Source
Thrown at apps/daemon/src/design/claude-design-import.ts:257
const bodyStart = offset + 30 + nameLen + extraLen;
const bodyEnd = bodyStart + entry.compressedSize;
if (bodyEnd > zip.length) throw new Error(`zip entry exceeds archive: ${entry.name}`);
const compressed = zip.slice(bodyStart, bodyEnd);
if (entry.method === 0) return Buffer.from(compressed);
// A genuinely empty deflate payload would still occupy at least the BFINAL
// marker; an entirely missing payload cannot be inflated, so treat it as
// empty rather than handing a zero-length buffer to zlib.
if (compressed.length === 0) return Buffer.alloc(0);
// When the central directory advertises 0 (streaming zips with data
// descriptors), fall back to the per-file ceiling so legitimate non-empty
// payloads decode instead of being silently truncated. The post-decode
// checks in the caller enforce MAX_FILE_BYTES and total-bytes limits.
const cap = entry.uncompressedSize > 0 ? entry.uncompressedSize : MAX_FILE_BYTES;
return inflateRawSync(compressed, { maxOutputLength: cap });
}
function sanitizeZipPath(name: string): string {
if (name.includes('\0')) throw new Error('invalid zip file name');
if (/^[A-Za-z]:/.test(name) || name.startsWith('/')) {
throw new Error('absolute zip paths are not allowed');
}
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
);
}
View on GitHub (pinned to 5be4028344)
Solutions
- Do not import untrusted archives.
- Inspect the zip with `unzip -l` for suspicious/garbled entry names.
- Re-create the archive from trusted source files with a standard tool.
Defensive patterns
Strategy: validation
Validate before calling
// Reject archives whose entry names contain null bytes.
import { execFileSync } from 'node:child_process';
function zipHasNullByteNames(file: string): boolean {
try {
const out = execFileSync('unzip', ['-l', file], { encoding: 'utf8' });
return out.includes('\0');
} catch {
return false;
}
} Try / catch
try {
await importClaudeDesignZip(zipPath, projectDir);
} catch (err) {
if (String(err).includes('invalid zip file name')) {
return res.status(400).json({ error: 'zip contains an invalid entry name' });
}
throw err;
} Prevention
- Never import archives from untrusted sources.
- Inspect `unzip -l` output for garbled or truncated names.
- Re-create archives from trusted source files only.
When it happens
Trigger: A zip entry whose name field embeds \0 — typically a maliciously crafted archive (e.g. 'safe.txt\0../../etc/passwd') or one produced by a buggy encoder.
Common situations: Security testing / fuzzing; an archive received from an untrusted source; rare encoder bugs that leak null bytes into name fields.
Related errors
- absolute zip paths are not allowed
- path escapes project dir
- 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/ec3f5824099b7dcf.
Report an issue: GitHub.