修复 Android 中的 Zip 路径遍历漏洞

Fixing a Zip Path Traversal Vulnerability In Android

我已将我的应用程序上传到 Google Play 商店,Google 已发出警告,即 "Android Security"。

在应用程序中,我们下载了 Zip 文件夹并将此 Zip 文件夹保存在内部存储中,然后将该文件夹解压缩到设备的内部存储中。

这里是解压文件夹代码:

public static void doUnzip(String inputZipFile, String 
   destinationDirectory, ZipProgressListener zipProgressListener) throws 
    IOException, RuntimeException {

 Log.e(TAG, "doUnzip:inputZipFile: " + inputZipFile);
 Log.e(TAG, "doUnzip:destinationDirectory: " + destinationDirectory);

int BUFFER = 6 * 1024;
List zipFiles = new ArrayList();
  File sourceZipFile = FileUtils.createValidFile(inputZipFile);
   File unzipDestinationDirectory = 
  FileUtils.createValidFile(destinationDirectory);
unzipDestinationDirectory.mkdir();
   String newPath = unzipDestinationDirectory.getAbsolutePath() + 
 File.separator + 
     FileUtils.getFileNameWithoutExtension(sourceZipFile.getName());
  new File(newPath).mkdir();

 ZipFile zipFile;
// Open Zip file for reading
zipFile = new ZipFile(sourceZipFile, ZipFile.OPEN_READ);
int entries = zipFile.size();
int total = 0;
Log.e(TAG, "doUnzip: entries Found !!" + entries);

// Create an enumeration of the entries in the zip file
Enumeration zipFileEntries = zipFile.entries();

if (zipProgressListener != null) {
    zipProgressListener.onZipStart();
}

// Process each entry
while (zipFileEntries.hasMoreElements()) {
    // grab a zip file entry
    ZipEntry entry = (ZipEntry) zipFileEntries.nextElement();

    String currentEntry = entry.getName();
    Log.i(TAG, "[doUnzip] " + currentEntry);

    File file = new File(newPath);

    File destFile = new File(newPath, currentEntry);
    Log.i(TAG, "doUnzip getCanonicalPath : " + 
destFile.getCanonicalPath());

    if (Build.VERSION.SDK_INT <= VERSION_CODES.LOLLIPOP) {
        Log.i(TAG, "doUnzip: LOLLIPOP");
        if 
  (!destFile.getCanonicalPath().startsWith(destinationDirectory)) {
            throw new RuntimeException(destFile.getCanonicalPath() + 
  " is outside of targetDirectory: " + destinationDirectory);
        }
    } else {
        Log.i(TAG, "doUnzip: Above ");
        if(!destFile.getCanonicalPath().contains(file.getName()) && 
    !destFile.getCanonicalPath().contains("/")){
            throw new RuntimeException(destFile.getCanonicalPath() + 
     " is outside of targetDirectory: " + destinationDirectory);
          }
      }

      if (currentEntry.endsWith(".zip")) {
        zipFiles.add(destFile.getAbsolutePath());
    }

    // grab file's parent directory structure
    File destinationParent = destFile.getParentFile();
    // create the parent directory structure if needed
    destinationParent.mkdirs();

       try {
        // extract file if not a directory
        if (!entry.isDirectory()) {
            BufferedInputStream is = new 
       BufferedInputStream(zipFile.getInputStream(entry));
            int currentByte;
            // establish buffer for writing file
            byte data[] = new byte[BUFFER];

            // write the current file to disk
            FileOutputStream fos = new FileOutputStream(destFile);
            BufferedOutputStream dest = new BufferedOutputStream(fos, 
        BUFFER);

            // read and write until last byte is encountered
            while ((currentByte = is.read(data, 0, BUFFER)) != -1) {
                dest.write(data, 0, currentByte);
            }

            Log.e(TAG, "unzip:outPath: =>" + 
        destFile.getAbsolutePath() + "\nFile size: " + destFile.length() 
         / 1024);
            dest.flush();
            dest.close();
            is.close();
        }

        int progress = 0;
        if (zipProgressListener != null) {
            progress = (total++ * 100 / entries);
            zipProgressListener.onZipProgressUpdate(progress);
        }
        Log.e(TAG, "unzip: PROGRESS::" + progress);
      } catch (IOException ioe) {
        ioe.printStackTrace();
      }
       }
       zipFile.close();

      for (Object zipFile1 : zipFiles) {
         String zipName = (String) zipFile1;
         Log.i(TAG, "doUnzip: ");
         doUnzip(zipName, destinationDirectory + File.separator + 
         zipName.substring(0, zipName.lastIndexOf(".zip")),
            zipProgressListener);
     }

   if (zipProgressListener != null) {
    Log.i(TAG, "doUnzip: " + sourceZipFile.getName());

    zipProgressListener.onZipCompleted(destinationDirectory +
            File.separatorChar + sourceZipFile.getName().substring(0, 
  sourceZipFile.getName().lastIndexOf(".zip")));
  }

   }

这里是 Google 警告:

此信息适用于应用包含不安全解压缩模式的开发人员,这些模式可能会导致 Zip Path Traversal 攻击。包含不安全解压缩模式的易受攻击应用 类 的位置可以在您应用的 Play 管理中心通知中找到。

其他详细信息

Zip 文件可以包含名称中包含路径遍历字符(“../”)的条目(文件或目录)。如果开发人员在未验证其名称的情况下解压缩此类 zip 文件条目,则可能会导致路径遍历攻击,从而导致写入任意目录甚至覆盖应用程序私有文件夹中的文件。

我们建议通过检查解压缩文件的规范路径是否位于预期目录下来解决您应用中的此问题。具体来说,在使用使用 ZipEntry 的 getName() 方法的 return 值创建的文件对象之前,始终检查 File.GetCanonicalPath() 的 return 值是否属于预期的目录路径。例如:

InputStream is = new InputStream(untrustedFileName);
ZipInputStream zis = new ZipInputStream(new BufferedInputStream(is));
while((ZipEntry ze = zis.getNextEntry()) != null) {
  File f = new File(DIR, ze.getName());
  String canonicalPath = f.getCanonicalPath();
  if (!canonicalPath.startsWith(DIR)) {
    // SecurityException
  }
  // Finish unzipping…
}

如何解决上面的警告 Android OS-6?

像这样检查漏洞

InputStream is = new InputStream(untrustedFileName);
ZipInputStream zis = new ZipInputStream(new BufferedInputStream(is));
while((ZipEntry ze = zis.getNextEntry()) != null) {
    File outputFile = new File(outputDir, ze.getName());
    try {
        ensureZipPathSafety(outputFile, outputDir);
    } catch (Exception e) {
        e.printStackTrace();
        return;
    }
    // Finish unzipping…
}


private void ensureZipPathSafety(final File outputFile, final String destDirectory) throws Exception {
    String destDirCanonicalPath = (new File(destDirectory)).getCanonicalPath();
    String outputFilecanonicalPath = outputFile.getCanonicalPath();
    if (!outputFileCanonicalPath.startsWith(destDirCanonicalPath)) {
        throw new Exception(String.format("Found Zip Path Traversal Vulnerability with %s", canonicalPath));
    }
}