{"record":{"id":"17fb2a74c19b1483","repo":"affaan-m/ECC","slug":"unsafe-nasiko-archive-expected-exactly-one-bounde","errorCode":null,"errorMessage":"Unsafe Nasiko archive: expected exactly one bounded regular binary file.","messagePattern":"Unsafe Nasiko archive: expected exactly one bounded regular binary file\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"scripts/lib/nasiko-release.js","lineNumber":103,"sourceCode":"  let binary = null;\n  while (offset + 512 <= tar.length) {\n    const header = tar.subarray(offset, offset + 512);\n    if (header.every(byte => byte === 0)) break;\n    const name = readTarString(header, 0, 100);\n    const prefix = readTarString(header, 345, 155);\n    const type = String.fromCharCode(header[156] || 48);\n    const rawSize = readTarString(header, 124, 12).trim();\n    const size = Number.parseInt(rawSize || '0', 8);\n    const start = offset + 512;\n    const end = start + size;\n    if (!Number.isSafeInteger(size) || size < 0 || end > tar.length) throw new Error('Nasiko archive is truncated.');\n    const payload = tar.subarray(start, end);\n    const isBinary = !prefix && name === expectedName && (type === '0' || type === '\\0');\n    const isAppleDouble = !prefix && name === `._${expectedName}` && type === '0' && size <= 1024 * 1024;\n    const isPaxMetadata = !prefix && name === `PaxHeader/${expectedName}` && type === 'x' && size <= 64 * 1024\n      && !/(?:^|\\n)(?:path|linkpath)=/i.test(payload.toString('utf8'));\n    if (isBinary && !binary && size > 0 && size <= MAX_BINARY_BYTES) binary = Buffer.from(payload);\n    else if (!isAppleDouble && !isPaxMetadata) throw new Error('Unsafe Nasiko archive: expected exactly one bounded regular binary file.');\n    offset = start + Math.ceil(size / 512) * 512;\n  }\n  if (!binary) throw new Error('Unsafe Nasiko archive: expected exactly one bounded regular binary file.');\n  return binary;\n}\n\nfunction fetchBytes(url, options = {}) {\n  const parsed = new URL(url);\n  if (parsed.origin !== REGISTRY_ORIGIN || parsed.protocol !== 'https:') return Promise.reject(new Error('Nasiko download origin is not allowed.'));\n  const maxBytes = options.maxBytes || MAX_ARCHIVE_BYTES;\n  return new Promise((resolve, reject) => {\n    const request = https.get(parsed, { headers: options.accept ? { Accept: options.accept } : {} }, response => {\n      if (response.statusCode >= 300 && response.statusCode < 400) { response.resume(); reject(new Error('Nasiko registry redirects are not allowed.')); return; }\n      if (response.statusCode !== 200) { response.resume(); reject(new Error(`Nasiko registry returned HTTP ${response.statusCode}.`)); return; }\n      const chunks = [];\n      let total = 0;\n      response.on('data', chunk => {\n        total += chunk.length;","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/affaan-m/ECC/blob/06c5e118c4d3e6c3b7f9445f973a2194c82de193/scripts/lib/nasiko-release.js#L85-L121","documentation":"extractQualifiedTarGzip enforces a strict allowlist while walking the tar: each entry must be the expected regular binary file (no path prefix, type '0' or NUL, size 1..64 MiB, at most once), or one of two tolerated metadata entries - a macOS AppleDouble '._name' file (<=1 MiB) or a bounded PaxHeader entry (type 'x', <=64 KiB, containing no path/linkpath overrides). Any other entry - extra files, directories, symlinks, hardlinks, devices, or Pax records that would relocate the binary - aborts extraction.","triggerScenarios":"An entry in the qualified archive is not the expected binary and not whitelisted metadata: name !== expectedName (or a ustar prefix is set), type is '5' (directory), '1'/'2' (hard/symlink), or a second regular file appears; or a PaxHeader entry contains 'path=' / 'linkpath=' records that could rename or redirect the extracted file.","commonSituations":"Repackaging scripts that add a LICENSE or README into the archive; macOS builds leaking extra AppleDouble files beyond the bounded ones; upstream changing packaging to include directories; adversarial archives attempting path traversal or binary substitution via Pax path records.","solutions":["Repack the release archive with exactly one regular file named nasiko (or nasiko.exe) and nothing else","Strip extra entries from the tar (for example `tar --delete` or rebuild with `tar -cf archive nasiko`)","Remove path/linkpath records from PaxHeaders in the packaging pipeline","Treat unexpected entries in a digest-pinned archive as a security incident and report it upstream"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await installNasiko({ version: 'v0.1.0' });\n} catch (error) {\n  if (/Unsafe Nasiko archive/.test(String(error.message))) {\n    // Fail closed. Never loosen the entry allowlist. Inspect the archive with\n    // `tar -tvf` to find the offending entry and fix the packaging (exactly one\n    // regular binary, no symlinks/extra files/path-rewriting Pax records).\n  }\n  throw error;\n}","preventionTips":["Build release archives with `tar -cf archive nasiko` and nothing else in the tree","Never allow path/linkpath Pax records in packaged artifacts","Treat any unexpected entry under a pinned digest as a security incident, not a bug to patch around"],"tags":["tar","security","path-traversal","archive"],"backgroundTag":"tar-archive-validation-failed","analyzedSha":"06c5e118c4d3e6c3b7f9445f973a2194c82de193","analyzedAt":"2026-08-18T11:27:13.915Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}