spring-projects/spring-boot · error · ReportableException

Entry '{entry.getName()}' would be written to '{canonicalEnt

Error message

Entry '{entry.getName()}' would be written to '{canonicalEntryPath}'. This is outside the output location of '{canonicalOutputPath}'. Verify your target server configuration.

What it means

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.

Source

Thrown at cli/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/ProjectGenerator.java:119

		byte[] content = entity.getContent();
		Assert.state(content != null, "'content' must not be null");
		try (ZipInputStream zipStream = new ZipInputStream(new ByteArrayInputStream(content))) {
			extractFromStream(zipStream, overwrite, outputDirectory);
			fixExecutableFlag(outputDirectory, "mvnw");
			fixExecutableFlag(outputDirectory, "gradlew");
			Log.info("Project extracted to '" + outputDirectory.getAbsolutePath() + "'");
		}
	}

	private void extractFromStream(ZipInputStream zipStream, boolean overwrite, File outputDirectory)
			throws IOException {
		ZipEntry entry = zipStream.getNextEntry();
		String canonicalOutputPath = outputDirectory.getCanonicalPath() + File.separator;
		while (entry != null) {
			File file = new File(outputDirectory, entry.getName());
			String canonicalEntryPath = file.getCanonicalPath();
			if (!canonicalEntryPath.startsWith(canonicalOutputPath)) {
				throw new ReportableException("Entry '" + entry.getName() + "' would be written to '"
						+ canonicalEntryPath + "'. This is outside the output location of '" + canonicalOutputPath
						+ "'. Verify your target server configuration.");
			}
			if (file.exists() && !overwrite) {
				throw new ReportableException((file.isDirectory() ? "Directory" : "File") + " '" + file.getName()
						+ "' already exists. Use --force if you want to overwrite or "
						+ "specify an alternate location.");
			}
			if (!entry.isDirectory()) {
				FileCopyUtils.copy(StreamUtils.nonClosing(zipStream), new FileOutputStream(file));
			}
			else {
				file.mkdir();
			}
			zipStream.closeEntry();
			entry = zipStream.getNextEntry();
		}
	}

View on GitHub (pinned to 5b2dbdbb8b)

Solutions

  1. Do not disable this check. Verify the target Initializr service (--target) is trusted and its archive generation is correct.
  2. If you run a private Initializr, fix its archiving so entries are relative to the project root with no ../ components.
  3. If the entry name is legitimately unusual, inspect the zip locally (unzip -l) to confirm contents before trusting the server.

Example fix

// before: server emits entry '../../etc/foo'
$ spring init --target https://untrusted-start/ --output myapp/
// after
$ spring init --target https://start.spring.io --output myapp/   # use a trusted service
Defensive patterns

Strategy: validation

Validate before calling

// Before extracting, sanity-check archive entry names against the output dir
Path base = outputDirectory.getCanonicalFile().toPath();
try (ZipInputStream z = new ZipInputStream(new ByteArrayInputStream(content))) {
    ZipEntry e; while ((e = z.getNextEntry()) != null) {
        Path resolved = base.resolve(e.getName()).normalize();
        if (!resolved.startsWith(base)) throw new IOException("Unsafe entry: " + e.getName());
    }
}

Type guard

// Zip-slip guard: entry is safe iff resolved path stays under base
boolean safe(Path base, String entryName) {
    return base.resolve(entryName).normalize().startsWith(base);
}

Try / catch

// This is a security check; do NOT catch-and-ignore. Treat as a hard failure
try { generator.generateProject(request, force); }
catch (ReportableException ex) {
    if (ex.getMessage().contains("outside the output location")) {
        // stop, audit the service, do not retry against the same untrusted server
        throw ex;
    }
    throw ex;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of spring-projects/spring-boot@5b2dbdbb8b (2026-08-04). Data as JSON: /data/errors/5cd534b769ca14f5.json. Report an issue: GitHub.