{"record":{"id":"a48c26d976cc654d","repo":"midudev/autoskills","slug":"refusing-to-download-disallowed-skill-archive-normalizedrel","errorCode":null,"errorMessage":"refusing to download disallowed skill archive: ${normalizedRel}","messagePattern":"refusing to download disallowed skill archive: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"packages/autoskills/installer.ts","lineNumber":281,"sourceCode":"    headers.Authorization = `Bearer ${GITHUB_TOKEN}`;\n  }\n  return headers;\n}\n\nfunction isDisallowedSkillFile(rel: string): boolean {\n  return rel.toLowerCase().endsWith(\".zip\");\n}\n\nasync function downloadRegistryFile(\n  skillName: string,\n  entry: RegistryEntry,\n  rel: string,\n  opts: InstallOptions,\n): Promise<{ buf: Buffer; url: string }> {\n  const normalizedRel = normalizeRegistryRelPath(rel);\n\n  if (isDisallowedSkillFile(normalizedRel)) {\n    throw new Error(`refusing to download disallowed skill archive: ${normalizedRel}`);\n  }\n\n  const expected = entry.sha256[rel] || entry.sha256[normalizedRel];\n  if (!expected) {\n    throw new Error(`no recorded hash for ${normalizedRel}`);\n  }\n\n  const fetchFile = opts.fetchImpl || fetch;\n  const errors = [];\n  for (const baseUrl of getRegistryRawBaseUrls(opts)) {\n    const url = `${baseUrl}/${encodeRawPath(skillName, normalizedRel)}`;\n    opts.onTrace?.(`GET ${url}`);\n    const res = await fetchFile(url, {\n      headers: githubDownloadHeaders(url),\n    });\n    if (!res.ok) {\n      const resetAt = Number(res.headers.get(\"x-ratelimit-reset\") || 0) * 1000;\n      const resetSuffix = resetAt ? ` (resets ${new Date(resetAt).toISOString()})` : \"\";","sourceCodeStart":263,"sourceCodeEnd":299,"githubUrl":"https://github.com/midudev/autoskills/blob/0ec725320d2137253ab2e68e7ba8a072148e741a/packages/autoskills/installer.ts#L263-L299","documentation":"Before downloading any file from the skills registry, downloadRegistryFile normalizes the relative path and checks it against isDisallowedSkillFile(). This is a security guard: archives (e.g. .zip/.tgz) and other forbidden file types must never be fetched, even if the registry manifest lists them. Throwing here prevents a malicious or corrupted registry entry from pulling executable archives onto the machine.","triggerScenarios":"Requesting a registry rel path that passes normalizeRegistryRelPath but matches the disallowed-skill-file patterns (e.g. a skill tarball or zip referenced by a RegistryEntry), via downloadRegistryFile -> downloadRegistryEntry.","commonSituations":"A tampered or hand-edited skills-registry.json listing a skill as an archive; a typo in a rel path that accidentally resolves to a forbidden filename; testing against a custom registry that uses archives while the installer only permits plain skill files.","solutions":["Inspect the registry entry and remove/replace any disallowed archive file references with the plain skill file paths the installer expects.","Verify the registry manifest comes from a trusted source (check its hash/signature or re-sync it) in case it was tampered with.","Fix the rel path you requested — it should point to a permitted skill file, not an archive.","If you control a custom registry, repackage skills as individual files instead of archives."],"exampleFix":"// registry entry (before)\n{ \"files\": [\"skills/foo/foo.zip\"] }\n// after\n{ \"files\": [\"skills/foo/SKILL.md\", \"skills/foo/scripts/run.sh\"] }","handlingStrategy":"validation","validationCode":"import { statSync } from \"node:fs\";\nconst FORBIDDEN = [/\\.zip$/i, /\\.(t|tar)\\.gz$/i, /\\.tgz$/i, /\\.tar$/i];\nfunction registryEntryIsSafe(entry) {\n  return Object.keys(entry.sha256).every(rel => !FORBIDDEN.some(re => re.test(rel)));\n}\n// before installing: if (!registryEntryIsSafe(entry)) throw new Error(\"registry entry contains archives\");","typeGuard":"const isSafeRelPath = (rel) => typeof rel === \"string\" && !rel.startsWith(\"/\") && !rel.includes(\"..\") && !/\\.(zip|tgz|tar|gz)$/i.test(rel);","tryCatchPattern":"try {\n  await downloadRegistryEntry(name, entry, dest);\n} catch (e) {\n  if (e.message.startsWith(\"refusing to download disallowed skill archive\")) {\n    reportSecurityEvent(e.message); // never bypass; fix the registry\n  } else throw e;\n}","preventionTips":["Never hand-edit the registry manifest to add archive files.","Consume the registry only from the trusted upstream source and verify its integrity.","Run a manifest lint step in CI that rejects disallowed file types before publishing.","Keep the disallowed-file patterns in sync with your security policy."],"tags":["security","path-validation","download-blocked"],"backgroundTag":"path-traversal-blocked","analyzedSha":"0ec725320d2137253ab2e68e7ba8a072148e741a","analyzedAt":"2026-09-15T14:12:31.090Z","contentChangedAt":"2026-09-15T14:12:31.090Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}