{"record":{"id":"aa7ce7d0cbb01982","repo":"golang/go","slug":"zip-file-contains-more-than-one-top-level-director","errorCode":null,"errorMessage":"zip file contains more than one top-level directory","messagePattern":"zip file contains more than one top-level directory","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/cmd/go/internal/modfetch/coderepo.go","lineNumber":1154,"sourceCode":"\n\tvar files []modzip.File\n\tif subdir != \"\" {\n\t\tsubdir += \"/\"\n\t}\n\thaveLICENSE := false\n\ttopPrefix := \"\"\n\tfor _, zf := range zr.File {\n\t\tif topPrefix == \"\" {\n\t\t\ti := strings.Index(zf.Name, \"/\")\n\t\t\tif i < 0 {\n\t\t\t\treturn fmt.Errorf(\"missing top-level directory prefix\")\n\t\t\t}\n\t\t\ttopPrefix = zf.Name[:i+1]\n\t\t}\n\t\tvar name string\n\t\tvar found bool\n\t\tif name, found = strings.CutPrefix(zf.Name, topPrefix); !found {\n\t\t\treturn fmt.Errorf(\"zip file contains more than one top-level directory\")\n\t\t}\n\n\t\tif name, found = strings.CutPrefix(name, subdir); !found {\n\t\t\tcontinue\n\t\t}\n\n\t\tif name == \"\" || strings.HasSuffix(name, \"/\") {\n\t\t\tcontinue\n\t\t}\n\t\tfiles = append(files, zipFile{name: name, f: zf})\n\t\tif name == \"LICENSE\" {\n\t\t\thaveLICENSE = true\n\t\t}\n\t}\n\n\tif !haveLICENSE && subdir != \"\" {\n\t\tdata, err := r.code.ReadFile(ctx, rev, \"LICENSE\", codehost.MaxLICENSE)\n\t\tif err == nil {","sourceCodeStart":1136,"sourceCodeEnd":1172,"githubUrl":"https://github.com/golang/go/blob/b6b368adc57c96c3151d224d172029f233ead2c3/src/cmd/go/internal/modfetch/coderepo.go#L1136-L1172","documentation":"Companion to 1004. Once the top-level prefix is locked in from the first entry, every subsequent entry must start with that prefix. Finding an entry that does not means the archive contains more than one top-level directory, which would let a module smuggle files outside its namespace.","triggerScenarios":"strings.CutPrefix(zf.Name, topPrefix) returns found=false for some entry — e.g. archive holds both projectA/... and projectB/... at the top level.","commonSituations":"Multi-project repos packaged into one zip by a misbehaving proxy; corrupted download; cache tampering.","solutions":["Purge the suspect cache entry and re-download.","Point GOPROXY at a compliant proxy (https://proxy.golang.org,direct) that always emits a single top-level dir.","If self-hosting the proxy, validate that the upstream archive contains exactly one root directory before serving."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"func zipSingleTopDir(p string) error {\n    r, err := zip.OpenReader(p)\n    if err != nil { return err }\n    defer r.Close()\n    top := \"\"\n    for _, zf := range r.File {\n        i := strings.Index(zf.Name, \"/\")\n        if i < 0 { return fmt.Errorf(\"no top dir\") }\n        if top == \"\" { top = zf.Name[:i+1] }\n        if !strings.HasPrefix(zf.Name, top) {\n            return fmt.Errorf(\"multiple top dirs: %q vs %q\", top, zf.Name)\n        }\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Validate served archives with the single-top-dir invariant.","Periodically verify your modcache with `go mod verify`."],"tags":["zip","archive","structure","proxy","security"],"backgroundTag":null,"analyzedSha":"b6b368adc57c96c3151d224d172029f233ead2c3","analyzedAt":"2026-08-12T00:22:02.250Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}