chatboxai/chatbox · critical
Unsafe ZIP entry path: ${path}
Error message
Unsafe ZIP entry path: ${path} What it means
Thrown by assertSafeArchivePath during the first (lexical) path-safety check. It rejects paths that are empty, contain a NUL byte or backslash, are absolute (start with '/'), or look like a Windows drive root (e.g. 'C:'). This is a zip-slip / path-traversal guard applied to every entry on both read and write.
Source
Thrown at src/renderer/packages/backup/zip.ts:41
maxTotalUncompressedBytes?: number
maxCompressionRatio?: number
}
export interface ZipReadOptions {
limits?: ZipReadLimits
entryLimits?: (path: string) => Partial<Pick<ZipReadLimits, 'maxEntryUncompressedBytes' | 'maxCompressionRatio'>>
signal?: AbortSignal
}
function throwIfAborted(signal?: AbortSignal) {
if (signal?.aborted) {
throw signal.reason instanceof Error ? signal.reason : new DOMException('Operation canceled', 'AbortError')
}
}
export function assertSafeArchivePath(path: string): void {
if (!path || path.includes('\0') || path.includes('\\') || path.startsWith('/') || /^[a-zA-Z]:/.test(path)) {
throw new Error(`Unsafe ZIP entry path: ${path}`)
}
const segments = path.split('/')
if (segments.some((segment) => segment === '' || segment === '.' || segment === '..')) {
throw new Error(`Unsafe ZIP entry path: ${path}`)
}
}
function* splitBytes(bytes: Uint8Array): Generator<Uint8Array> {
for (let offset = 0; offset < bytes.length; offset += ZIP_INPUT_CHUNK_SIZE) {
yield bytes.subarray(offset, Math.min(offset + ZIP_INPUT_CHUNK_SIZE, bytes.length))
}
}
function toChunks(data: ZipArchiveEntry['data']): Iterable<Uint8Array> | AsyncIterable<Uint8Array> {
return data instanceof Uint8Array ? splitBytes(data) : data
}
export async function* createZipStream(View on GitHub (pinned to 81571269ad)
Solutions
- Reject the archive; do not attempt to extract paths that fail this guard.
- When producing archives, normalize entry paths to relative forward-slash form before adding.
- Sanitize entry names: strip leading slashes, convert backslashes to '/', reject empty names.
Example fix
// before
zip.add(new ZipDeflate(entry.path, { level: 6 }))
// after
const safe = entry.path.replace(/\\/g, '/').replace(/^\/+/, '')
if (!safe || /\0/.test(safe) || /^[a-zA-Z]:/.test(safe)) {
throw new Error(`Refusing unsafe path: ${entry.path}`)
}
zip.add(new ZipDeflate(safe, { level: 6 })) Defensive patterns
Strategy: validation
Validate before calling
function isLexicallySafePath(path: string): boolean {
return !!path && !path.includes('\0') && !path.includes('\\') && !path.startsWith('/') && !/^[a-zA-Z]:/.test(path)
} Type guard
const isSafeArchivePath = (path: string): boolean =>
!!path && !path.includes('\0') && !path.includes('\\') && !path.startsWith('/') && !/^[a-zA-Z]:/.test(path) &&
!path.split('/').some((seg) => seg === '' || seg === '.' || seg === '..') Try / catch
try {
assertSafeArchivePath(entry.name)
} catch (error) {
if (/Unsafe ZIP entry path/.test((error as Error).message)) skipEntry(entry)
throw error
} Prevention
- Normalize entry paths to relative forward-slash form before adding to an archive.
- Reject archives whose entries use absolute or drive-rooted paths.
- Never extract to a filesystem path derived directly from an untrusted entry name.
When it happens
Trigger: A ZIP entry name is '', contains '\\0' or '\\', starts with '/', or matches /^[a-zA-Z]:/.
Common situations: Malicious or malformed archive with absolute entry paths; archive created on Windows with backslash separators; an entry crafted for path traversal.
Related errors
- invalid file path for "${name}": ${file.path}
- Invalid skill name: path traversal not allowed
- Invalid script name: path traversal not allowed
- ZIP archive central directory is invalid
- Script path escapes skills directory
AI-assisted analysis of chatboxai/chatbox@81571269ad (2026-08-12).
Data as JSON: /api/errors/4da49c3c598b1e4b.
Report an issue: GitHub.