{"record":{"id":"83d36824a45fc460","repo":"actualbudget/actual","slug":"unsafe-zip-entry-name-name","errorCode":null,"errorMessage":"Unsafe zip entry name: ${name}","messagePattern":"Unsafe zip entry name: (.+?)","errorType":"exception","errorClass":"UnsafeZipError","httpStatus":null,"severity":"critical","filePath":"packages/loot-core/src/server/util/zip.ts","lineNumber":30,"sourceCode":"  readonly meta: UnsafeZipMeta;\n\n  constructor(message: string, meta: UnsafeZipMeta) {\n    super(message);\n    this.meta = meta;\n  }\n}\n\nfunction assertSafeEntryName(name: string) {\n  const isTraversal = name.split('/').some(segment => segment === '..');\n\n  if (\n    name.includes('\\0') ||\n    name.includes('\\\\') ||\n    /^[a-zA-Z]:/.test(name) ||\n    name.startsWith('/') ||\n    isTraversal\n  ) {\n    throw new UnsafeZipError(`Unsafe zip entry name: ${name}`, {\n      zipReason: 'unsafe-entry-name',\n      entryName: name,\n    });\n  }\n}\n\ntype SafeUnzipOptions = {\n  maxArchiveSize?: number;\n  maxEntrySize?: number;\n  maxTotalUncompressedSize?: number;\n};\n\nexport function safeUnzip(\n  data: Uint8Array,\n  {\n    maxArchiveSize = MAX_ZIP_SIZE,\n    maxEntrySize = MAX_ZIP_SIZE,\n    maxTotalUncompressedSize = MAX_ZIP_SIZE,","sourceCodeStart":12,"sourceCodeEnd":48,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/util/zip.ts#L12-L48","documentation":"assertSafeEntryName defends against zip-slip and related path-traversal attacks. It rejects entry names containing '..' traversal sequences, null bytes, backslashes, Windows drive prefixes (C:), absolute paths, or other unsafe patterns, throwing an UnsafeZipError with zipReason 'unsafe-entry-name'.","triggerScenarios":"Extracting a zip whose entry names include '../' sequences, absolute paths ('/etc/passwd'), Windows paths ('C:\\\\evil'), null bytes, or backslash separators — typically a maliciously crafted archive uploaded by a user.","commonSituations":"Importing backup zips from untrusted sources; attacker-supplied archives in a public server deployment attempting path traversal to write outside the extraction directory.","solutions":["This is a security guard — do not bypass it. Inspect the archive: the error's entryName field names the offending entry.","Obtain the zip from a trusted source or re-create it with clean, relative, forward-slash entry names.","Scan incoming archives with a security tool before importing if untrusted uploads are expected.","Ensure any zip-producing code you control uses normalized relative paths for entries."],"exampleFix":"// before\nconst files = safeUnzip(untrustedBuffer); // throws on malicious names\n// after\nlet files;\ntry {\n  files = safeUnzip(untrustedBuffer);\n} catch (e) {\n  if (e instanceof UnsafeZipError && e.zipReason === 'unsafe-entry-name') {\n    throw new Error(`Rejected malicious archive entry: ${e.entryName}`);\n  }\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"// Inspect entry names before extraction\nconst preview = unzipSync(buf, { filter: () => false }); // or read central directory\nconst dangerous = Object.keys(preview).filter(n =>\n  n.includes('..') || n.startsWith('/') || /^[a-zA-Z]:/.test(n) || n.includes('\\\\') || n.includes('\\0'));\nif (dangerous.length) throw new Error(`Unsafe entries: ${dangerous.join(', ')}`);","typeGuard":"function isSafeEntryName(name: string): boolean {\n  return !name.includes('..') && !name.includes('\\0') && !name.includes('\\\\') &&\n    !/^[a-zA-Z]:/.test(name) && !name.startsWith('/');\n}","tryCatchPattern":"try {\n  const entries = safeUnzip(buf);\n} catch (e) {\n  if (e instanceof UnsafeZipError && e.zipReason === 'unsafe-entry-name') {\n    rejectUpload(`Archive rejected: unsafe entry \"${e.entryName}\"`);\n  } else { throw e; }\n}","preventionTips":["Never bypass or widen this check on untrusted archives","Only import zips from trusted sources; treat user uploads as hostile","Ensure zip-producing code writes normalized relative paths with forward slashes"],"tags":["security","zip","path-traversal","validation"],"backgroundTag":"zip-slip-path-traversal","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}