{"id":"5cd534b769ca14f5","repo":"spring-projects/spring-boot","slug":"entry-entry-getname-would-be-written-to-ca","errorCode":null,"errorMessage":"Entry '{entry.getName()}' would be written to '{canonicalEntryPath}'. This is outside the output location of '{canonicalOutputPath}'. Verify your target server configuration.","messagePattern":"Entry '(.+?)' would be written to '(.+?)'\\. This is outside the output location of '(.+?)'\\. Verify your target server configuration\\.","errorType":"exception","errorClass":"ReportableException","httpStatus":null,"severity":"error","filePath":"cli/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/ProjectGenerator.java","lineNumber":119,"sourceCode":"\t\tbyte[] content = entity.getContent();\n\t\tAssert.state(content != null, \"'content' must not be null\");\n\t\ttry (ZipInputStream zipStream = new ZipInputStream(new ByteArrayInputStream(content))) {\n\t\t\textractFromStream(zipStream, overwrite, outputDirectory);\n\t\t\tfixExecutableFlag(outputDirectory, \"mvnw\");\n\t\t\tfixExecutableFlag(outputDirectory, \"gradlew\");\n\t\t\tLog.info(\"Project extracted to '\" + outputDirectory.getAbsolutePath() + \"'\");\n\t\t}\n\t}\n\n\tprivate void extractFromStream(ZipInputStream zipStream, boolean overwrite, File outputDirectory)\n\t\t\tthrows IOException {\n\t\tZipEntry entry = zipStream.getNextEntry();\n\t\tString canonicalOutputPath = outputDirectory.getCanonicalPath() + File.separator;\n\t\twhile (entry != null) {\n\t\t\tFile file = new File(outputDirectory, entry.getName());\n\t\t\tString canonicalEntryPath = file.getCanonicalPath();\n\t\t\tif (!canonicalEntryPath.startsWith(canonicalOutputPath)) {\n\t\t\t\tthrow new ReportableException(\"Entry '\" + entry.getName() + \"' would be written to '\"\n\t\t\t\t\t\t+ canonicalEntryPath + \"'. This is outside the output location of '\" + canonicalOutputPath\n\t\t\t\t\t\t+ \"'. Verify your target server configuration.\");\n\t\t\t}\n\t\t\tif (file.exists() && !overwrite) {\n\t\t\t\tthrow new ReportableException((file.isDirectory() ? \"Directory\" : \"File\") + \" '\" + file.getName()\n\t\t\t\t\t\t+ \"' already exists. Use --force if you want to overwrite or \"\n\t\t\t\t\t\t+ \"specify an alternate location.\");\n\t\t\t}\n\t\t\tif (!entry.isDirectory()) {\n\t\t\t\tFileCopyUtils.copy(StreamUtils.nonClosing(zipStream), new FileOutputStream(file));\n\t\t\t}\n\t\t\telse {\n\t\t\t\tfile.mkdir();\n\t\t\t}\n\t\t\tzipStream.closeEntry();\n\t\t\tentry = zipStream.getNextEntry();\n\t\t}\n\t}","sourceCodeStart":101,"sourceCodeEnd":137,"githubUrl":"https://github.com/spring-projects/spring-boot/blob/5b2dbdbb8be64415eb6552f81ff7c449c8d251e6/cli/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/ProjectGenerator.java#L101-L137","documentation":"A Zip-Slip guard in ProjectGenerator.extractFromStream: for each ZipEntry it computes the canonical path of the target File and verifies it starts with the canonical output directory. If not, the entry would escape the output location (path traversal) and generation is aborted to prevent writing files outside the intended directory.","triggerScenarios":"The downloaded zip from the Initializr service contains an entry whose name uses ../ sequences or an absolute path, so new File(outputDirectory, entry.getName()).getCanonicalPath() resolves outside outputDirectory.getCanonicalPath()+separator. The check at line 118 fails and the exception is thrown.","commonSituations":"A malicious or misconfigured Initializr service; a tampered proxy response; a custom service that packages entries with absolute or parent-relative names. This is a security control - the message directs you to verify the target server configuration.","solutions":["Do not disable this check. Verify the target Initializr service (--target) is trusted and its archive generation is correct.","If you run a private Initializr, fix its archiving so entries are relative to the project root with no ../ components.","If the entry name is legitimately unusual, inspect the zip locally (unzip -l) to confirm contents before trusting the server."],"exampleFix":"// before: server emits entry '../../etc/foo'\n$ spring init --target https://untrusted-start/ --output myapp/\n// after\n$ spring init --target https://start.spring.io --output myapp/   # use a trusted service","handlingStrategy":"validation","validationCode":"// Before extracting, sanity-check archive entry names against the output dir\nPath base = outputDirectory.getCanonicalFile().toPath();\ntry (ZipInputStream z = new ZipInputStream(new ByteArrayInputStream(content))) {\n    ZipEntry e; while ((e = z.getNextEntry()) != null) {\n        Path resolved = base.resolve(e.getName()).normalize();\n        if (!resolved.startsWith(base)) throw new IOException(\"Unsafe entry: \" + e.getName());\n    }\n}","typeGuard":"// Zip-slip guard: entry is safe iff resolved path stays under base\nboolean safe(Path base, String entryName) {\n    return base.resolve(entryName).normalize().startsWith(base);\n}","tryCatchPattern":"// This is a security check; do NOT catch-and-ignore. Treat as a hard failure\ntry { generator.generateProject(request, force); }\ncatch (ReportableException ex) {\n    if (ex.getMessage().contains(\"outside the output location\")) {\n        // stop, audit the service, do not retry against the same untrusted server\n        throw ex;\n    }\n    throw ex;\n}","preventionTips":["Only target trusted Initializr services (--target).","If you host the service, generate archives with entries relative to a single root and reject ../ in entry names.","Treat this error as a potential security incident, not a nuisance."],"tags":["spring-boot-cli","initializr","security","zip-slip","path-traversal"],"analyzedSha":"5b2dbdbb8be64415eb6552f81ff7c449c8d251e6","analyzedAt":"2026-08-04T18:53:14.967Z","schemaVersion":2}