修复 Zip 路径遍历漏洞

This commit is contained in:
kunfei 2023-03-13 14:44:46 +08:00
parent b11f6ffdd4
commit 84c57c28ac
10 changed files with 150 additions and 91 deletions

View File

@ -21,6 +21,7 @@ import io.legado.app.lib.webdav.WebDavFile
import io.legado.app.model.remote.RemoteBookWebDav
import io.legado.app.ui.widget.dialog.WaitDialog
import io.legado.app.utils.*
import io.legado.app.utils.compress.ZipUtils
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.ensureActive

View File

@ -13,6 +13,7 @@ import io.legado.app.help.DefaultData
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.utils.*
import io.legado.app.utils.compress.ZipUtils
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext
import splitties.init.appCtx

View File

@ -14,6 +14,7 @@ import io.legado.app.help.config.ReadBookConfig
import io.legado.app.help.config.ThemeConfig
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.utils.*
import io.legado.app.utils.compress.ZipUtils
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext

View File

@ -20,6 +20,7 @@ import io.legado.app.help.config.ReadBookConfig
import io.legado.app.help.config.ThemeConfig
import io.legado.app.model.localBook.LocalBook
import io.legado.app.utils.*
import io.legado.app.utils.compress.ZipUtils
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.delay

View File

@ -34,6 +34,7 @@ import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.document.HandleFileContract
import io.legado.app.ui.widget.seekbar.SeekBarChangeListener
import io.legado.app.utils.*
import io.legado.app.utils.compress.ZipUtils
import io.legado.app.utils.viewbindingdelegate.viewBinding
import splitties.init.appCtx

View File

@ -2,6 +2,9 @@ package io.legado.app.utils
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import io.legado.app.utils.compress.RarUtils
import io.legado.app.utils.compress.SevenZipUtils
import io.legado.app.utils.compress.ZipUtils
import splitties.init.appCtx
import java.io.File
@ -52,7 +55,7 @@ object ArchiveUtils {
val workPath = workPathFileDoc.toString()
archiveFileDoc.uri.inputStream(appCtx).getOrThrow().use {
when {
name.endsWith(".zip", ignoreCase = true) -> ZipUtils.unZipToPath(it, workPath)
name.endsWith(".zip", ignoreCase = true) -> ZipUtils.unZipToPath(it, workPath)
name.endsWith(".rar", ignoreCase = true) -> RarUtils.unRarToPath(it, workPath)
name.endsWith(".7z", ignoreCase = true) -> SevenZipUtils.un7zToPath(it, workPath)
else -> throw IllegalArgumentException("Unexpected archive format")

View File

@ -1,75 +0,0 @@
package io.legado.app.utils
import android.annotation.SuppressLint
import android.os.ParcelFileDescriptor
import androidx.annotation.Keep
import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry
import org.apache.commons.compress.archivers.sevenz.SevenZFile
import org.apache.commons.compress.utils.SeekableInMemoryByteChannel
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.nio.channels.FileChannel
@Keep
@Suppress("unused","MemberVisibilityCanBePrivate")
object SevenZipUtils {
fun un7zToPath(inputStream: InputStream, path:String){
un7zToPath(inputStream,File(path))
}
fun un7zToPath(byteArray: ByteArray, path:String){
un7zToPath(byteArray,File(path))
}
fun un7zToPath(pfd: ParcelFileDescriptor, path:String){
un7zToPath(pfd,File(path))
}
fun un7zToPath(fileChannel: FileChannel, path:String){
un7zToPath(fileChannel,File(path))
}
fun un7zToPath(inputStream: InputStream, destDir: File?){
un7zToPath(SevenZFile(SeekableInMemoryByteChannel(inputStream.readBytes())),destDir)
}
fun un7zToPath(byteArray: ByteArray, destDir: File?){
un7zToPath(SevenZFile(SeekableInMemoryByteChannel(byteArray)),destDir)
}
fun un7zToPath(pfd: ParcelFileDescriptor, destDir: File?){
un7zToPath(SevenZFile(ParcelFileDescriptorChannel(pfd)),destDir)
}
@SuppressLint("NewApi")
fun un7zToPath(fileChannel: FileChannel, destDir: File?){
un7zToPath(SevenZFile(fileChannel),destDir)
}
fun un7zToPath(file: File, destDir: File?){
un7zToPath(SevenZFile(file),destDir)
}
fun un7zToPath(filePath: String, destDir: File?){
un7zToPath(SevenZFile(File(filePath)),destDir)
}
fun un7zToPath(sevenZFile:SevenZFile, destDir: File?){
var entry: SevenZArchiveEntry?
while (sevenZFile.nextEntry.also { entry=it }!=null) {
val entryFile = File(destDir, entry!!.name)
if (entry!!.isDirectory) {
if (!entryFile.exists()) {
entryFile.mkdirs()
}
continue
}
if (entryFile.parentFile?.exists() != true) {
entryFile.parentFile?.mkdirs()
}
if (!entryFile.exists()) {
entryFile.createNewFile()
entryFile.setReadable(true)
entryFile.setExecutable(true)
}
FileOutputStream(entryFile).use {
sevenZFile.getInputStream(entry).copyTo(it)
}
}
}
}

View File

@ -1,4 +1,4 @@
package io.legado.app.utils
package io.legado.app.utils.compress
import androidx.annotation.Keep
import com.github.junrar.Archive
@ -9,20 +9,30 @@ import java.io.FileOutputStream
import java.io.InputStream
@Keep
@Suppress("unused","MemberVisibilityCanBePrivate")
@Suppress("unused", "MemberVisibilityCanBePrivate")
object RarUtils {
fun unRarToPath(inputStream: InputStream,path:String){
@Throws(NullPointerException::class, SecurityException::class)
fun unRarToPath(inputStream: InputStream, path: String) {
unRarToPath(inputStream, File(path))
}
fun unRarToPath(byteArray: ByteArray,path:String){
@Throws(NullPointerException::class, SecurityException::class)
fun unRarToPath(byteArray: ByteArray, path: String) {
unRarToPath(byteArray, File(path))
}
fun unRarToPath(zipPath:String,path:String){
@Throws(NullPointerException::class, SecurityException::class)
fun unRarToPath(zipPath: String, path: String) {
unRarToPath(zipPath, File(path))
}
fun unRarToPath(file: File,path:String){
@Throws(NullPointerException::class, SecurityException::class)
fun unRarToPath(file: File, path: String) {
unRarToPath(file, File(path))
}
@Throws(NullPointerException::class, SecurityException::class)
fun unRarToPath(inputStream: InputStream, destDir: File?) {
Archive(inputStream).use {
unRarToPath(it, destDir)
@ -30,28 +40,36 @@ object RarUtils {
inputStream.close()
}
@Throws(NullPointerException::class, SecurityException::class)
fun unRarToPath(byteArray: ByteArray, destDir: File?) {
Archive(ByteArrayInputStream(byteArray)).use {
unRarToPath(it, destDir)
}
}
fun unRarToPath(filePath:String, destDir: File?) {
@Throws(NullPointerException::class, SecurityException::class)
fun unRarToPath(filePath: String, destDir: File?) {
Archive(File(filePath)).use {
unRarToPath(it, destDir)
}
}
@Throws(NullPointerException::class, SecurityException::class)
fun unRarToPath(file: File, destDir: File?) {
Archive(file).use {
unRarToPath(it, destDir)
}
}
@Throws(NullPointerException::class, SecurityException::class)
fun unRarToPath(archive: Archive, destDir: File?) {
destDir ?: throw NullPointerException("解决路径不能为空")
var entry: FileHeader?
while (archive.nextFileHeader().also { entry = it } != null) {
val entryFile = File(destDir, entry!!.fileName)
if (!entryFile.canonicalPath.startsWith(destDir.canonicalPath)) {
throw SecurityException("压缩文件只能解压到指定路径")
}
if (entry!!.isDirectory) {
if (!entryFile.exists()) {
entryFile.mkdirs()

View File

@ -0,0 +1,98 @@
package io.legado.app.utils.compress
import android.annotation.SuppressLint
import android.os.ParcelFileDescriptor
import androidx.annotation.Keep
import io.legado.app.utils.ParcelFileDescriptorChannel
import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry
import org.apache.commons.compress.archivers.sevenz.SevenZFile
import org.apache.commons.compress.utils.SeekableInMemoryByteChannel
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.nio.channels.FileChannel
@Keep
@Suppress("unused", "MemberVisibilityCanBePrivate")
object SevenZipUtils {
@Throws(NullPointerException::class, SecurityException::class)
fun un7zToPath(inputStream: InputStream, path: String) {
un7zToPath(inputStream, File(path))
}
@Throws(NullPointerException::class, SecurityException::class)
fun un7zToPath(byteArray: ByteArray, path: String) {
un7zToPath(byteArray, File(path))
}
@Throws(NullPointerException::class, SecurityException::class)
fun un7zToPath(pfd: ParcelFileDescriptor, path: String) {
un7zToPath(pfd, File(path))
}
@Throws(NullPointerException::class, SecurityException::class)
fun un7zToPath(fileChannel: FileChannel, path: String) {
un7zToPath(fileChannel, File(path))
}
@Throws(NullPointerException::class, SecurityException::class)
fun un7zToPath(inputStream: InputStream, destDir: File?) {
un7zToPath(SevenZFile(SeekableInMemoryByteChannel(inputStream.readBytes())), destDir)
}
@Throws(NullPointerException::class, SecurityException::class)
fun un7zToPath(byteArray: ByteArray, destDir: File?) {
un7zToPath(SevenZFile(SeekableInMemoryByteChannel(byteArray)), destDir)
}
@Throws(NullPointerException::class, SecurityException::class)
fun un7zToPath(pfd: ParcelFileDescriptor, destDir: File?) {
un7zToPath(SevenZFile(ParcelFileDescriptorChannel(pfd)), destDir)
}
@SuppressLint("NewApi")
@Throws(NullPointerException::class, SecurityException::class)
fun un7zToPath(fileChannel: FileChannel, destDir: File?) {
un7zToPath(SevenZFile(fileChannel), destDir)
}
@Throws(NullPointerException::class, SecurityException::class)
fun un7zToPath(file: File, destDir: File?) {
un7zToPath(SevenZFile(file), destDir)
}
@Throws(NullPointerException::class, SecurityException::class)
fun un7zToPath(filePath: String, destDir: File?) {
un7zToPath(SevenZFile(File(filePath)), destDir)
}
@Throws(NullPointerException::class, SecurityException::class)
fun un7zToPath(sevenZFile: SevenZFile, destDir: File?) {
destDir ?: throw NullPointerException("解决路径不能为空")
var entry: SevenZArchiveEntry?
while (sevenZFile.nextEntry.also { entry = it } != null) {
val entryFile = File(destDir, entry!!.name)
if (!entryFile.canonicalPath.startsWith(destDir.canonicalPath)) {
throw SecurityException("压缩文件只能解压到指定路径")
}
if (entry!!.isDirectory) {
if (!entryFile.exists()) {
entryFile.mkdirs()
}
continue
}
if (entryFile.parentFile?.exists() != true) {
entryFile.parentFile?.mkdirs()
}
if (!entryFile.exists()) {
entryFile.createNewFile()
entryFile.setReadable(true)
entryFile.setExecutable(true)
}
FileOutputStream(entryFile).use {
sevenZFile.getInputStream(entry).copyTo(it)
}
}
}
}

View File

@ -1,5 +1,7 @@
package io.legado.app.utils
package io.legado.app.utils.compress
import io.legado.app.utils.DebugLog
import io.legado.app.utils.printOnDebug
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext
import java.io.*
@ -174,15 +176,21 @@ object ZipUtils {
return true
}
@Throws(SecurityException::class)
fun unZipToPath(inputStream: InputStream, path: String) {
val zipInputStream = ZipInputStream(inputStream)
unZipToPath(zipInputStream, path)
ZipInputStream(inputStream).use {
unZipToPath(it, path)
}
}
@Throws(SecurityException::class)
fun unZipToPath(zipInputStream: ZipInputStream, path: String) {
var entry: ZipEntry?
while (zipInputStream.nextEntry.also { entry = it } != null) {
val entryFile = File(path, entry!!.name)
if (!entryFile.canonicalPath.startsWith(path)) {
throw SecurityException("压缩文件只能解压到指定路径")
}
if (entry!!.isDirectory) {
if (!entryFile.exists()) {
entryFile.mkdirs()
@ -201,7 +209,6 @@ object ZipUtils {
zipInputStream.copyTo(it)
}
}
zipInputStream.close()
}
/**
@ -302,7 +309,7 @@ object ZipUtils {
return files
}
@Throws(IOException::class)
@Throws(IOException::class, SecurityException::class)
private fun unzipChildFile(
destDir: File,
files: MutableList<File>,
@ -311,14 +318,17 @@ object ZipUtils {
name: String
): Boolean {
val file = File(destDir, name)
if (!file.canonicalPath.startsWith(destDir.canonicalPath)) {
throw SecurityException("压缩文件只能解压到指定路径")
}
files.add(file)
if (entry.isDirectory) {
return createOrExistsDir(file)
} else {
if (!createOrExistsFile(file)) return false
BufferedInputStream(zip.getInputStream(entry)).use { `in` ->
BufferedOutputStream(FileOutputStream(file)).use { out ->
out.write(`in`.readBytes())
zip.getInputStream(entry).use { input ->
FileOutputStream(file).use { out ->
input.copyTo(out)
}
}
}