Fix java/zipslip CodeQL alerts by validating archive entry paths#761
Fix java/zipslip CodeQL alerts by validating archive entry paths#761vharseko wants to merge 1 commit into
Conversation
maximthomas
left a comment
There was a problem hiding this comment.
The normalize-and-startsWith barrier is placed correctly in all four sinks — before any mkdirs() / createDirectories() / FileOutputStream — and Path.startsWith is component-based, so /tmp/restore-evil vs /tmp/restore is handled right. BackupManager is exactly right: it reassigns fileToRestore to the normalized path and writes that. The other three validate one path and write a different one, which leaves a hole and, in one case, breaks a legitimate call. Two one-line fixes below.
Symlink traversal still escapes (Medium)
StaticUtils, ZipExtractor and GenerateConfigMojo check normalize() but then write to the un-normalized File. normalize() is purely lexical, so an entry like link/../evil.txt passes the guard and the OS then resolves link before ... Verified locally:
guard allows = true (norm=sym/target/evil.txt)
actually written to: .../scratchpad/sym/evil.txt <-- outside sym/target
Requires a pre-existing symlink under the destination (a prior install, an upgrade target) — but that is precisely the extraction scenario here. The normalized path is already computed, so just use it:
// opendj-server-legacy/src/main/java/org/opends/server/util/StaticUtils.java
Path targetDir = targetDirectory.toPath().toAbsolutePath().normalize();
Path targetPath = targetDir.resolve(fileEntry.getName()).normalize();
if (!targetPath.startsWith(targetDir))
{
throw new IOException("Zip entry '" + fileEntry.getName() + "' is outside of the target directory");
}
File targetFile = targetPath.toFile();Same shape for ZipExtractor.extract and GenerateConfigMojo.safeOutputPath (return outputFile.toPath().normalize().toString()). BackupManager already does this and needs no change.
Relative target directory rejects every entry (Medium)
StaticUtils.extractZipArchive never absolutizes targetDirectory. Paths.get(".").normalize() is the empty path, and nothing startsWith it:
dir=. name=opendj/bin/start-ds -> norm=opendj/bin/start-ds base=(empty) allowed=false
This is reachable through the public embedded API — EmbeddedDirectoryServer.java:455 does File deployDirectory = serverRoot.getParentFile();, and new File("./opendj").getParentFile() is .. So serverRootDirectory("./opendj") + extractArchiveForSetup() worked before this PR and now throws "Zip entry 'opendj/…' is outside of the target directory". Fixed by the toAbsolutePath() in the snippet above. ZipExtractor is unaffected — Utils.getPath() canonicalizes first (Utils.java:294).
No regression test (Low)
opendj-server-legacy/src/test/java/org/opends/server/util/BackupManagerTestCase.java already builds and restores archives, so a negative case is cheap: archive with a ../evil.txt entry, assert the restore fails and nothing lands outside the restore dir. Without it the guard can be dropped in a refactor silently.
Nits
GenerateConfigMojoguard is defensive-only: keys come fromname.substring(name.lastIndexOf('/') + 1, endPos)(line 486) orFile.getName()(line 464) and always get aCfgDefn.java-style suffix — they can never contain a separator. Fine to keep for the alert, but notejavaDir + "/" + entry.getKey() + "/package-info.java"and the manifest path are left unwrapped, which reads as inconsistent.ZipExtractorerror is mislabelled: the newthrowis inside thetrywhosecatch (IOException)rewrites it asINFO_ERROR_COPYING, so a zip-slip rejection surfaces to the user as a file-copy error. Hoist the check above thetry.- Hoist the base path out of the loop:
targetDirectory.toPath().normalize()/new File(destDir).toPath().normalize()are recomputed for every zip entry. - Hardcoded English messages: these classes otherwise use
LocalizableMessage. Acceptable here sinceBackupManagerwraps intoERR_BACKUP_CANNOT_RESTOREupstream — just a conscious call. - Copyright wording:
Portions Copyrighted 2026(12 files repo-wide) vsPortions Copyright 2026(180, and what the CDDL header template says).
Fixes the four open
java/zipslipcode-scanning alerts (#105, #106, #107, #108) by validating archive entry paths before any file system operation, using the normalize-and-startsWith barrier recommended for this rule:restoreDir.resolve(zipEntryName)and rejects entries outside the restore directory before creating parent directories and writing the restored file.safeOutputPath()helper validates the generated-file paths built from jar entry names before they are used to create output files.