{"record":{"id":"a6b32b53fccca134","repo":"router-for-me/CLIProxyAPI","slug":"zip-entry-s-escapes-archive-root","errorCode":null,"errorMessage":"zip entry %s escapes archive root","messagePattern":"zip entry (.+?) escapes archive root","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/pluginstore/install.go","lineNumber":387,"sourceCode":"}\n\nfunc versionedPluginFileName(id string, version string, goos string) string {\n\treturn strings.TrimSpace(id) + \"-v\" + normalizeVersion(version) + pluginExtension(goos)\n}\n\nfunc cleanZipName(name string) (string, error) {\n\tif strings.TrimSpace(name) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"zip entry has empty name\")\n\t}\n\tif strings.Contains(name, `\\`) {\n\t\treturn \"\", fmt.Errorf(\"zip entry %s uses backslash path separators\", name)\n\t}\n\tif path.IsAbs(name) {\n\t\treturn \"\", fmt.Errorf(\"zip entry %s is absolute\", name)\n\t}\n\tcleaned := path.Clean(name)\n\tif cleaned == \".\" || cleaned == \"..\" || strings.HasPrefix(cleaned, \"../\") {\n\t\treturn \"\", fmt.Errorf(\"zip entry %s escapes archive root\", name)\n\t}\n\treturn cleaned, nil\n}\n\nfunc regularZipFile(file *zip.File) bool {\n\tmode := file.FileInfo().Mode()\n\treturn mode.IsRegular() || mode.Type() == 0\n}\n\nfunc hasDynamicLibraryExtension(name string) bool {\n\tlowerName := strings.ToLower(name)\n\treturn strings.HasSuffix(lowerName, \".dylib\") || strings.HasSuffix(lowerName, \".so\") || strings.HasSuffix(lowerName, \".dll\")\n}\n\ntype pluginFileInfo struct {\n\tID      string\n\tPath    string\n\tVersion string","sourceCodeStart":369,"sourceCodeEnd":405,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/pluginstore/install.go#L369-L405","documentation":"Returned by cleanZipName (install.go:385-388) when an entry, after path.Clean, is '.', '..', or starts with '../' — i.e. it would resolve outside the archive root. This is the core zip-slip defense: such a name could escape the plugins directory when extracted, so install.go refuses the archive entirely.","triggerScenarios":"InstallArchive on a zip with entries named '..', '../..', 'dir/../../escape.so', or constructions like 'a/./../..' that clean to a parent escape. The check fires for every entry during the scan, regardless of file type.","commonSituations":"Malicious archives targeting zip-slip (CVE-2018-12689-style); corrupted zip central directories producing garbage names; rare archiver bugs writing dot components.","solutions":["Do not install the artifact — treat it as untrusted and discard it","Rebuild the archive from trusted sources using relative, root-level entry names","If you archive files yourself, sanitize names before adding entries (strip leading '../' segments)"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"func zipEntriesContained(archiveData []byte) error {\n    r, err := zip.NewReader(bytes.NewReader(archiveData), int64(len(archiveData)))\n    if err != nil { return err }\n    for _, f := range r.File {\n        c := path.Clean(f.Name)\n        if c == \".\" || c == \"..\" || strings.HasPrefix(c, \"../\") {\n            return fmt.Errorf(\"entry %q escapes archive root\", f.Name)\n        }\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":"if err := zipEntriesContained(data); err != nil {\n    // hostile or corrupt artifact: drop it and alert; never retry\n} else {\n    res, err := store.InstallArchive(data, plugin, opts)\n}","preventionTips":["Never install archives from untrusted origins — the store hard-rejects traversal names by design","Audit third-party plugin sources before adding them to a registry","Keep the plugins dir path fixed and predictable so escapes are detectable in audits"],"tags":["pluginstore","zip","security","zip-slip","path-traversal"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}