can1357/oh-my-pi · error · ArchiveError
Cannot write unsafe ZIP member path '${inputName}'
Error message
Cannot write unsafe ZIP member path '${inputName}' What it means
Thrown by the ZIP writer when an input member path is deemed unsafe: it fails normalization (empty, does not round-trip through './'-stripping, escapes via '..', is absolute, is a Windows drive path, or contains NUL). The writer only accepts paths that are safe, relative, portable archive member names.
Source
Thrown at packages/utils/src/ar/zip.ts:627
/** Encode deterministic stored/deflated ZIP bytes, emitting ZIP64 end records when the entry count requires them. */
export async function encodeZip(members: Iterable<readonly [string, Uint8Array]>): Promise<Uint8Array> {
try {
const localParts: Uint8Array[] = [];
const centralParts: Uint8Array[] = [];
let localSize = 0;
let centralSize = 0;
let count = 0;
for (const [inputName, data] of members) {
const portableName = inputName.replace(/\\/g, "/");
const normalizedName = normalizeArchiveEntryPath(portableName);
if (
!normalizedName ||
normalizedName !== portableName.replace(/^\.\//, "") ||
portableName.startsWith("/") ||
/^[A-Za-z]:/.test(portableName) ||
portableName.includes("\0")
) {
throw new ArchiveError(`Cannot write unsafe ZIP member path '${inputName}'`);
}
const name = normalizedName;
const nameBytes = TEXT_ENCODER.encode(name);
if (nameBytes.byteLength > U16_MAX) throw new ArchiveError(`ZIP member path '${name}' is too long to write`);
if (data.byteLength >= U32_MAX) throw new ArchiveError(`ZIP member '${name}' is too large to write`);
const deflated = data.byteLength === 0 ? undefined : zlib.deflateRawSync(data);
const payload = deflated && deflated.byteLength < data.byteLength ? deflated : data;
const method = payload === data ? 0 : 8;
if (payload.byteLength >= U32_MAX || localSize >= U32_MAX) {
throw new ArchiveError("ZIP archive is too large to write member offsets safely");
}
const checksum = crc32(data);
const local = new Uint8Array(30 + nameBytes.byteLength);
writeUInt32LE(local, 0, LOCAL_HEADER_SIGNATURE);
writeUInt16LE(local, 4, 20);
writeUInt16LE(local, 6, UTF8_FLAG);
writeUInt16LE(local, 8, method);
writeUInt16LE(local, 10, 0);View on GitHub (pinned to 9690622007)
Solutions
- Normalize the path and strip unsafe components before writing: resolve to a relative path without '..' segments
- Convert backslashes to forward slashes and drop drive letters / leading slashes
- Reject or rename members containing NUL bytes
- Sanitize user-supplied filenames with an allowlist (alphanumerics, dash, dot, slash)
Example fix
// before
writeZip([{ name: userInput + '/' + file, data }]);
// after
const safe = path.posix.normalize(userInput.replace(/\\/g, '/')).replace(/^(\.\.\/)+/, '').replace(/^\.\//, '');
if (!safe || safe.startsWith('/') || safe.includes('\\0')) throw new Error('unsafe name');
writeZip([{ name: safe, data }]); Defensive patterns
Strategy: validation
Validate before calling
function isSafeZipName(name: string): boolean {
const portable = name.replaceAll('\\', '/');
const normalized = path.posix.normalize(portable).replace(/^\.\//, '');
return Boolean(normalized) &&
normalized === portable.replace(/^\.\//, '') &&
!portable.startsWith('/') &&
!/^[A-Za-z]:/.test(portable) &&
!portable.includes('\0');
} Type guard
function isSafeZipName(name: string): boolean {
const portable = name.replaceAll('\\', '/');
const normalized = path.posix.normalize(portable).replace(/^\.\//, '');
return normalized.length > 0 && normalized === portable.replace(/^\.\//, '') &&
!portable.startsWith('/') && !/^[A-Za-z]:/.test(portable) && !portable.includes('\0');
} Try / catch
try {
await writeZip(members);
} catch (err) {
if (err instanceof ArchiveError && err.message.startsWith('Cannot write unsafe ZIP member path')) {
throw new Error(`Rejected unsafe member name (possible path traversal): ${err.message}`, { cause: err });
}
throw err;
} Prevention
- Never pass raw user input as member names; normalize first
- Convert Windows paths to relative forward-slash form
- Strip leading './' and any '..' segments before writing
When it happens
Trigger: Calling the zip write/create API with member names like '../secrets.txt', '/etc/passwd', 'C:\\foo', 'a\0b', or names whose normalized form differs from the input (e.g. redundant '../' or './' segments that do not collapse cleanly).
Common situations: Archiving files built from untrusted user input; concatenating path segments without normalization; Windows paths passed directly instead of converted to forward-slash relative paths; path traversal attempts in upload pipelines.
Related errors
- Destination paths cannot contain parent traversal or NUL byt
- Shared-folder destination escapes its configured root
- Absolute paths are not allowed in ${scheme}:// URLs: ${rawPa
- Path traversal (..) is not allowed in ${scheme}:// URLs: ${r
- Unsafe #{scheme}:// path (absolute or traversal): #{path}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/ab46dff668847019.
Report an issue: GitHub.