Fix java/zipslip CodeQL alerts by validating archive entry paths (#761)
| | |
| | | import java.io.IOException; |
| | | import java.net.JarURLConnection; |
| | | import java.net.URL; |
| | | import java.nio.file.Path; |
| | | import java.util.Enumeration; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.LinkedList; |
| | |
| | | createTransformTask(inputFactory, output, stylesheet, executor, parameters); |
| | | } |
| | | |
| | | /** Resolves a file name against an output directory, refusing names which escape it. */ |
| | | private static String safeOutputPath(final String outputDir, final String fileName) throws IOException { |
| | | final Path outputDirPath = new File(outputDir).toPath().toAbsolutePath().normalize(); |
| | | final Path outputFile = outputDirPath.resolve(fileName).normalize(); |
| | | if (!outputFile.startsWith(outputDirPath)) { |
| | | throw new IOException("File name '" + fileName + "' is outside of the output directory"); |
| | | } |
| | | return outputFile.toString(); |
| | | } |
| | | |
| | | private void executeTransformXMLDefinitions() throws Exception { |
| | | getLog().info("Transforming XML definitions..."); |
| | | |
| | |
| | | |
| | | for (final Map.Entry<String, StreamSourceFactory> entry : componentDescriptors |
| | | .entrySet()) { |
| | | final String meta = metaDir + entry.getKey() + "CfgDefn.java"; |
| | | final String meta = safeOutputPath(metaDir, entry.getKey() + "CfgDefn.java"); |
| | | createTransformTask(entry.getValue(), meta, stylesheetMetaJava, parallelExecutor); |
| | | |
| | | final String server = serverDir + entry.getKey() + "Cfg.java"; |
| | | final String server = safeOutputPath(serverDir, entry.getKey() + "Cfg.java"); |
| | | createTransformTask(entry.getValue(), server, stylesheetServerJava, |
| | | parallelExecutor); |
| | | |
| | | final String client = clientDir + entry.getKey() + "CfgClient.java"; |
| | | final String client = safeOutputPath(clientDir, entry.getKey() + "CfgClient.java"); |
| | | createTransformTask(entry.getValue(), client, stylesheetClientJava, |
| | | parallelExecutor); |
| | | |
| | | final String ldap = ldapProfileDir + entry.getKey() + "CfgDefn.properties"; |
| | | final String ldap = safeOutputPath(ldapProfileDir, entry.getKey() + "CfgDefn.properties"); |
| | | createTransformTask(entry.getValue(), ldap, stylesheetProfileLDAP, parallelExecutor); |
| | | |
| | | final String cli = cliProfileDir + entry.getKey() + "CfgDefn.properties"; |
| | | final String cli = safeOutputPath(cliProfileDir, entry.getKey() + "CfgDefn.properties"); |
| | | createTransformTask(entry.getValue(), cli, stylesheetProfileCLI, parallelExecutor); |
| | | |
| | | final String i18n = i18nDir + entry.getKey() + "CfgDefn.properties"; |
| | | final String i18n = safeOutputPath(i18nDir, entry.getKey() + "CfgDefn.properties"); |
| | | createTransformTask(entry.getValue(), i18n, stylesheetMessages, parallelExecutor); |
| | | |
| | | createTransformTask(entry.getValue(), manifest, stylesheetManifest, |
| | |
| | | } |
| | | } |
| | | }; |
| | | final String profile = javaDir + "/" + entry.getKey() + "/package-info.java"; |
| | | final String profile = safeOutputPath(javaDir, entry.getKey() + "/package-info.java"); |
| | | createTransformTask(sourceFactory, profile, entry.getValue(), parallelExecutor, |
| | | "type", entry.getKey()); |
| | | } |
| | |
| | | * |
| | | * Copyright 2007-2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.quicksetup.util; |
| | | |
| | |
| | | import java.io.FileNotFoundException; |
| | | import java.io.IOException; |
| | | import java.io.InputStream; |
| | | import java.nio.file.Path; |
| | | import java.util.ArrayList; |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | |
| | | */ |
| | | Map<String, List<String>> permissions = new HashMap<>(); |
| | | permissions.put(getProtectedDirectoryPermissionUnix(), newArrayList(destDir)); |
| | | Path destDirPath = new File(destDir).toPath().toAbsolutePath().normalize(); |
| | | try { |
| | | if(application != null) { |
| | | application.checkAbort(); |
| | |
| | | } |
| | | } |
| | | if (name != null && name.length() > 0) { |
| | | Path destination = destDirPath.resolve(name).normalize(); |
| | | if (!destination.startsWith(destDirPath)) { |
| | | throw new IOException("Zip entry '" + entry.getName() |
| | | + "' is outside of the destination directory"); |
| | | } |
| | | try { |
| | | File destination = new File(destDir, name); |
| | | copyZipEntry(entry, destination, zipIn, |
| | | copyZipEntry(entry, destination.toFile(), zipIn, |
| | | ratioBeforeCompleted, ratioWhenCompleted, permissions); |
| | | } catch (IOException ioe) { |
| | | throw new ApplicationException( |
| | |
| | | * |
| | | * Copyright 2006-2009 Sun Microsystems, Inc. |
| | | * Portions Copyright 2013-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.util; |
| | | |
| | |
| | | private void restoreZipEntry(String zipEntryName, ZipInputStream zipStream, Path restoreDir, |
| | | RestoreConfig restoreConfig) throws IOException, DirectoryException |
| | | { |
| | | Path fileToRestore = restoreDir.resolve(zipEntryName); |
| | | Path fileToRestore = restoreDir.resolve(zipEntryName).normalize(); |
| | | if (!fileToRestore.startsWith(restoreDir.normalize())) |
| | | { |
| | | throw new IOException("Zip entry '" + zipEntryName + "' is outside of the restore directory"); |
| | | } |
| | | ensureFileCanBeRestored(fileToRestore); |
| | | |
| | | try (OutputStream outputStream = new FileOutputStream(fileToRestore.toFile())) |
| | |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2016 ForgeRock AS. |
| | | * Portions Copyrighted 2026 3A Systems, LLC. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.util; |
| | | |
| | |
| | | import java.net.ServerSocket; |
| | | import java.net.Socket; |
| | | import java.nio.ByteBuffer; |
| | | import java.nio.file.Path; |
| | | import java.text.ParseException; |
| | | import java.text.SimpleDateFormat; |
| | | import java.util.ArrayList; |
| | |
| | | public static void extractZipArchive(File zipFile, File targetDirectory, List<String> executableDirectories, |
| | | List<String> executableFiles) throws IOException |
| | | { |
| | | Path targetDir = targetDirectory.toPath().toAbsolutePath().normalize(); |
| | | try (ZipInputStream zipStream = new ZipInputStream(new FileInputStream(zipFile))) |
| | | { |
| | | ZipEntry fileEntry; |
| | | while ((fileEntry = zipStream.getNextEntry()) != null) |
| | | { |
| | | File targetFile = new File(targetDirectory.getPath(), fileEntry.getName()); |
| | | 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(); |
| | | |
| | | if (fileEntry.isDirectory()) |
| | | { |
| | |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.util; |
| | | |
| | |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.ListIterator; |
| | | import java.util.zip.ZipEntry; |
| | | import java.util.zip.ZipOutputStream; |
| | | |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.opends.server.DirectoryServerTestCase; |
| | |
| | | import org.opends.server.api.Backupable; |
| | | import org.opends.server.types.BackupConfig; |
| | | import org.opends.server.types.BackupDirectory; |
| | | import org.opends.server.types.DirectoryException; |
| | | import org.opends.server.types.RestoreConfig; |
| | | import org.testng.Reporter; |
| | | import org.testng.annotations.BeforeClass; |
| | |
| | | cleanDirectories(sourceDirectory, backupPath); |
| | | } |
| | | |
| | | /** |
| | | * A restore must reject archive entries which resolve outside of the restore |
| | | * directory (zip slip) and must not write any file outside of it. |
| | | */ |
| | | @Test |
| | | public void testRestoreRejectsZipSlipEntries() throws Exception |
| | | { |
| | | String label = "zipslip"; |
| | | Path sourceDirectory = createSourceDirectory(label); |
| | | Backupable backupable = buildBackupable(sourceDirectory, 1); |
| | | BackupDirectory backupDir = buildBackupDir(label); |
| | | BackupConfig backupConfig = new BackupConfig(backupDir, BACKUP_ID, false); |
| | | RestoreConfig restoreConfig = new RestoreConfig(backupDir, BACKUP_ID, false); |
| | | |
| | | BackupManager backupManager = new BackupManager(BACKEND_ID); |
| | | backupManager.createBackup(backupable, backupConfig); |
| | | |
| | | // replace the archive with one containing an entry escaping the restore directory |
| | | File archiveFile = new File(backupDir.getPath(), getArchiveFileName(BACKUP_ID)); |
| | | try (ZipOutputStream zipOutput = new ZipOutputStream(new FileOutputStream(archiveFile))) |
| | | { |
| | | zipOutput.putNextEntry(new ZipEntry("../evil.txt")); |
| | | zipOutput.write(getBytes("evil")); |
| | | zipOutput.closeEntry(); |
| | | } |
| | | |
| | | File escapedFile = new File(sourceDirectory.toFile().getParentFile(), "evil.txt"); |
| | | try |
| | | { |
| | | backupManager.restoreBackup(backupable, restoreConfig); |
| | | fail("Expected restoreBackup to reject a zip entry escaping the restore directory"); |
| | | } |
| | | catch (DirectoryException expected) |
| | | { |
| | | assertThat(expected.getMessage()).contains("evil.txt"); |
| | | } |
| | | assertThat(escapedFile).doesNotExist(); |
| | | |
| | | cleanDirectories(sourceDirectory, backupDir.getPath()); |
| | | } |
| | | |
| | | @Test |
| | | public void testCreateDirectoryWithNumericSuffix() throws Exception |
| | | { |
| | |
| | | * |
| | | * Copyright 2006-2009 Sun Microsystems, Inc. |
| | | * Portions Copyright 2013-2016 ForgeRock AS. |
| | | * Portions Copyrighted 2026 3A Systems, LLC. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.util; |
| | | |
| | |
| | | import java.io.BufferedReader; |
| | | import java.io.BufferedWriter; |
| | | import java.io.File; |
| | | import java.io.FileOutputStream; |
| | | import java.io.FileReader; |
| | | import java.io.FileWriter; |
| | | import java.io.IOException; |
| | |
| | | import java.nio.channels.FileChannel; |
| | | import java.nio.channels.FileLock; |
| | | import java.text.ParseException; |
| | | import java.util.Collections; |
| | | import java.util.LinkedList; |
| | | import java.util.List; |
| | | import java.util.zip.ZipEntry; |
| | | import java.util.zip.ZipOutputStream; |
| | | |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.opends.server.TestCaseUtils; |
| | |
| | | Assert.assertTrue(StaticUtils.isSelfConnection(socket)); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * {@link StaticUtils#extractZipArchive} must reject archive entries which |
| | | * resolve outside of the target directory (zip slip) and must not write any |
| | | * file outside of it. |
| | | */ |
| | | @Test |
| | | public void testExtractZipArchiveRejectsZipSlipEntries() throws Exception |
| | | { |
| | | File tempDir = TestCaseUtils.createTemporaryDirectory("zipslip"); |
| | | File zipFile = new File(tempDir, "malicious.zip"); |
| | | try (ZipOutputStream zipOutput = new ZipOutputStream(new FileOutputStream(zipFile))) |
| | | { |
| | | zipOutput.putNextEntry(new ZipEntry("../evil.txt")); |
| | | zipOutput.write(StaticUtils.getBytes("evil")); |
| | | zipOutput.closeEntry(); |
| | | } |
| | | File targetDirectory = new File(tempDir, "target"); |
| | | Assert.assertTrue(targetDirectory.mkdirs()); |
| | | |
| | | try |
| | | { |
| | | StaticUtils.extractZipArchive(zipFile, targetDirectory, |
| | | Collections.<String> emptyList(), Collections.<String> emptyList()); |
| | | Assert.fail("Expected extractZipArchive to reject a zip entry escaping the target directory"); |
| | | } |
| | | catch (IOException expected) |
| | | { |
| | | // expected |
| | | } |
| | | Assert.assertFalse(new File(tempDir, "evil.txt").exists()); |
| | | } |
| | | |
| | | /** |
| | | * {@link StaticUtils#extractZipArchive} must accept a relative target |
| | | * directory: EmbeddedDirectoryServer passes the parent of a relative server |
| | | * root, e.g. {@code .} for a server root of {@code ./opendj}. |
| | | */ |
| | | @Test |
| | | public void testExtractZipArchiveSupportsRelativeTargetDirectory() throws Exception |
| | | { |
| | | File tempDir = TestCaseUtils.createTemporaryDirectory("zipslip-relative"); |
| | | File zipFile = new File(tempDir, "archive.zip"); |
| | | String rootName = "zipslip-relative-extract-" + System.nanoTime(); |
| | | try (ZipOutputStream zipOutput = new ZipOutputStream(new FileOutputStream(zipFile))) |
| | | { |
| | | zipOutput.putNextEntry(new ZipEntry(rootName + "/")); |
| | | zipOutput.closeEntry(); |
| | | zipOutput.putNextEntry(new ZipEntry(rootName + "/README")); |
| | | zipOutput.write(StaticUtils.getBytes("content")); |
| | | zipOutput.closeEntry(); |
| | | } |
| | | |
| | | File extractedRoot = new File(rootName); |
| | | try |
| | | { |
| | | StaticUtils.extractZipArchive(zipFile, new File("."), |
| | | Collections.<String> emptyList(), Collections.<String> emptyList()); |
| | | Assert.assertTrue(new File(extractedRoot, "README").exists()); |
| | | } |
| | | finally |
| | | { |
| | | StaticUtils.recursiveDelete(extractedRoot); |
| | | } |
| | | } |
| | | } |