{"record":{"id":"6dc26bfb0d7221e8","repo":"router-for-me/CLIProxyAPI","slug":"zip-entry-s-is-absolute","errorCode":null,"errorMessage":"zip entry %s is absolute","messagePattern":"zip entry (.+?) is absolute","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/pluginstore/install.go","lineNumber":383,"sourceCode":"\tif mode == 0 {\n\t\tmode = 0o755\n\t}\n\treturn data, mode, nil\n}\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","sourceCodeStart":365,"sourceCodeEnd":401,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/pluginstore/install.go#L365-L401","documentation":"Returned by cleanZipName (install.go:382-384) when an entry name is an absolute path — it starts with '/' (on any OS) or a Windows drive letter that path.IsAbs recognizes. Absolute entry names would let an archive claim an arbitrary location outside the install directory, so they are rejected before path.Clean runs.","triggerScenarios":"InstallArchive on a zip containing entries like '/usr/lib/myplugin.so' or 'C:\\\\Windows\\\\myplugin.dll'. Any single such entry fails the entire install during the scan loop at install.go:322-326.","commonSituations":"Zipping with absolute paths from the command line (zip stores what it is given), hostile archives crafted for zip-slip, or naive archivers that preserve the source path root.","solutions":["Rebuild the zip with relative entry paths from a working directory (cd into the folder, then zip)","Inspect offenders first: unzip -l artifact.zip | grep '^/'","Treat repeated occurrences from a third-party source as a red flag about that artifact's provenance"],"exampleFix":"# before: zipped from /\nzip plugin.zip /build/out/myplugin.so   # entry '/build/out/myplugin.so'\n# after: zipped from the output dir\ncd build/out && zip plugin.zip myplugin.so","handlingStrategy":"validation","validationCode":"func zipEntriesRelative(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        if path.IsAbs(f.Name) || strings.Contains(f.Name, `\\`) {\n            return fmt.Errorf(\"entry %q is not a safe relative path\", f.Name)\n        }\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always zip from inside the directory containing the artifact (cd build/out && zip ...)","Sanitize names when producing archives programmatically","Reject absolute-entry archives at any ingestion boundary — they are malformed at best, hostile at worst"],"tags":["pluginstore","zip","security","path-traversal"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}