Merge pull request #13555 from am0o0/amammad-java-bombs

Java: Decompression Bombs
This commit is contained in:
Owen Mansel-Chan
2024-07-31 14:55:28 +01:00
committed by GitHub
82 changed files with 2611 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
package org.example;
import java.nio.file.StandardCopyOption;
import java.util.Enumeration;
import java.io.IOException;
import java.util.zip.*;
import java.util.zip.ZipEntry;
import java.io.File;
import java.nio.file.Files;
class BadExample {
public static void ZipInputStreamUnSafe(String filename) throws IOException {
File f = new File(filename);
try (ZipFile zipFile = new ZipFile(f)) {
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry ze = entries.nextElement();
File out = new File("./tmp/tmp.txt");
Files.copy(zipFile.getInputStream(ze), out.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
}
}
}

View File

@@ -0,0 +1,38 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>Extracting Compressed files with any compression algorithm like gzip can cause a denial of service attack.</p>
<p>Attackers can create a huge file by just repeating a single byte and compress it to a small file.</p>
</overview>
<recommendation>
<p>When decompressing a user-provided compressed file, verify the decompression ratio or decompress the files within a loop byte by byte to be able to manage the decompressed size in each cycle of the loop.</p>
</recommendation>
<example>
<p>
In the following example, the decompressed file size is not checked before decompression, exposing the application to a denial of service.
</p>
<sample src="BadExample.java" />
<p>
A better approach is shown in the following example, where a ZIP file is read within a loop and a size threshold is checked every cycle.
</p>
<sample src="GoodExample.java"/>
</example>
<references>
<li>
<a href="https://github.com/advisories/GHSA-47vx-fqr5-j2gw">CVE-2022-4565</a>
</li>
<li>
David Fifield: <a href="https://www.bamsoftware.com/hacks/zipbomb/">A better zip bomb</a>.
</li>
</references>
</qhelp>

View File

@@ -0,0 +1,21 @@
/**
* @name Uncontrolled file decompression
* @description Decompressing user-controlled files without checking the compression ratio may allow attackers to perform denial-of-service attacks.
* @kind path-problem
* @problem.severity error
* @security-severity 7.8
* @precision high
* @id java/uncontrolled-file-decompression
* @tags security
* experimental
* external/cwe/cwe-409
*/
import java
import experimental.semmle.code.java.security.DecompressionBombQuery
import DecompressionBombsFlow::PathGraph
from DecompressionBombsFlow::PathNode source, DecompressionBombsFlow::PathNode sink
where DecompressionBombsFlow::flowPath(source, sink)
select sink.getNode(), source, sink, "This file extraction depends on a $@.", source.getNode(),
"potentially untrusted source"

View File

@@ -0,0 +1,33 @@
import java.util.zip.*;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.zip.ZipEntry;
public class GoodExample {
public static void ZipInputStreamSafe(String filename) throws IOException {
int UncompressedSizeThreshold = 10 * 1024 * 1024; // 10MB
int BUFFERSIZE = 256;
FileInputStream fis = new FileInputStream(filename);
try (ZipInputStream zis = new ZipInputStream(new BufferedInputStream(fis))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
int count;
byte[] data = new byte[BUFFERSIZE];
FileOutputStream fos = new FileOutputStream(entry.getName());
BufferedOutputStream dest = new BufferedOutputStream(fos, BUFFERSIZE);
int totalRead = 0;
while ((count = zis.read(data, 0, BUFFERSIZE)) != -1) {
totalRead = totalRead + count;
if (totalRead > UncompressedSizeThreshold) {
System.out.println("This Compressed file can be a bomb!");
break;
}
dest.write(data, 0, count);
}
dest.flush();
dest.close();
zis.closeEntry();
}
}
}
}

View File

@@ -0,0 +1,379 @@
import java
private import semmle.code.java.dataflow.TaintTracking
module DecompressionBomb {
/**
* The Decompression bomb Sink
*
* Extend this class for creating new decompression bomb sinks
*/
abstract class Sink extends DataFlow::Node { }
/**
* The Additional flow steps that help to create a dataflow or taint tracking query
*
* Extend this class for creating new additional taint steps
*/
class AdditionalStep extends Unit {
abstract predicate step(DataFlow::Node n1, DataFlow::Node n2);
}
abstract class BombReadInputStreamCall extends MethodCall { }
private class ReadInputStreamQualifierSink extends DecompressionBomb::Sink {
ReadInputStreamQualifierSink() { this.asExpr() = any(BombReadInputStreamCall r).getQualifier() }
}
}
/**
* Providing Decompression sinks and additional taint steps for `org.xerial.snappy` package
*/
module XerialSnappy {
/**
* A type that is responsible for `SnappyInputStream` Class
*/
class TypeInputStream extends RefType {
TypeInputStream() {
this.getASupertype*().hasQualifiedName("org.xerial.snappy", "SnappyInputStream")
}
}
/**
* The methods that read bytes and belong to `SnappyInputStream` Types
*/
class ReadInputStreamCall extends DecompressionBomb::BombReadInputStreamCall {
ReadInputStreamCall() {
this.getReceiverType() instanceof TypeInputStream and
this.getCallee().hasName(["read", "readNBytes", "readAllBytes"])
}
}
/**
* Gets `n1` and `n2` which `SnappyInputStream n2 = new SnappyInputStream(n1)` or
* `n1.read(n2)`,
* second one is added because of sanitizer, we want to compare return value of each `read` or similar method
* that whether there is a flow to a comparison between total read of decompressed stream and a constant value
*/
private class InputStreamAdditionalTaintStep extends DecompressionBomb::AdditionalStep {
override predicate step(DataFlow::Node n1, DataFlow::Node n2) {
exists(ConstructorCall call |
call.getCallee().getDeclaringType() instanceof TypeInputStream and
call.getArgument(0) = n1.asExpr() and
call = n2.asExpr()
)
}
}
}
/**
* Providing Decompression sinks and additional taint steps for `org.apache.commons.compress` package
*/
module ApacheCommons {
/**
* A type that is responsible for `ArchiveInputStream` Class
*/
class TypeArchiveInputStream extends RefType {
TypeArchiveInputStream() {
this.getASupertype*()
.hasQualifiedName("org.apache.commons.compress.archivers", "ArchiveInputStream")
}
}
/**
* A type that is responsible for `CompressorInputStream` Class
*/
class TypeCompressorInputStream extends RefType {
TypeCompressorInputStream() {
this.getASupertype*()
.hasQualifiedName("org.apache.commons.compress.compressors", "CompressorInputStream")
}
}
/**
* Providing Decompression sinks and additional taint steps for `org.apache.commons.compress.compressors.*` Types
*/
module Compressors {
/**
* The types that are responsible for specific compression format of `CompressorInputStream` Class
*/
class TypeCompressors extends RefType {
TypeCompressors() {
this.getASupertype*()
.hasQualifiedName("org.apache.commons.compress.compressors.gzip",
"GzipCompressorInputStream") or
this.getASupertype*()
.hasQualifiedName("org.apache.commons.compress.compressors.brotli",
"BrotliCompressorInputStream") or
this.getASupertype*()
.hasQualifiedName("org.apache.commons.compress.compressors.bzip2",
"BZip2CompressorInputStream") or
this.getASupertype*()
.hasQualifiedName("org.apache.commons.compress.compressors.deflate",
"DeflateCompressorInputStream") or
this.getASupertype*()
.hasQualifiedName("org.apache.commons.compress.compressors.deflate64",
"Deflate64CompressorInputStream") or
this.getASupertype*()
.hasQualifiedName("org.apache.commons.compress.compressors.lz4",
"BlockLZ4CompressorInputStream") or
this.getASupertype*()
.hasQualifiedName("org.apache.commons.compress.compressors.lzma",
"LZMACompressorInputStream") or
this.getASupertype*()
.hasQualifiedName("org.apache.commons.compress.compressors.pack200",
"Pack200CompressorInputStream") or
this.getASupertype*()
.hasQualifiedName("org.apache.commons.compress.compressors.snappy",
"SnappyCompressorInputStream") or
this.getASupertype*()
.hasQualifiedName("org.apache.commons.compress.compressors.xz",
"XZCompressorInputStream") or
this.getASupertype*()
.hasQualifiedName("org.apache.commons.compress.compressors.z", "ZCompressorInputStream") or
this.getASupertype*()
.hasQualifiedName("org.apache.commons.compress.compressors.zstandard",
"ZstdCompressorInputStream")
}
}
/**
* The methods that read bytes and belong to `*CompressorInputStream` Types
*/
class ReadInputStreamCall extends DecompressionBomb::BombReadInputStreamCall {
ReadInputStreamCall() {
this.getReceiverType() instanceof TypeCompressors and
this.getCallee().hasName(["read", "readNBytes", "readAllBytes"])
}
}
/**
* Gets `n1` and `n2` which `GzipCompressorInputStream n2 = new GzipCompressorInputStream(n1)`
*/
private class CompressorsAndArchiversAdditionalTaintStep extends DecompressionBomb::AdditionalStep
{
override predicate step(DataFlow::Node n1, DataFlow::Node n2) {
exists(ConstructorCall call |
call.getCallee().getDeclaringType() instanceof TypeCompressors and
call.getArgument(0) = n1.asExpr() and
call = n2.asExpr()
)
}
}
}
/**
* Providing Decompression sinks and additional taint steps for Types from `org.apache.commons.compress.archivers.*` packages
*/
module Archivers {
/**
* The types that are responsible for specific compression format of `ArchiveInputStream` Class
*/
class TypeArchivers extends RefType {
TypeArchivers() {
this.getASupertype*()
.hasQualifiedName("org.apache.commons.compress.archivers.ar", "ArArchiveInputStream") or
this.getASupertype*()
.hasQualifiedName("org.apache.commons.compress.archivers.arj", "ArjArchiveInputStream") or
this.getASupertype*()
.hasQualifiedName("org.apache.commons.compress.archivers.cpio", "CpioArchiveInputStream") or
this.getASupertype*()
.hasQualifiedName("org.apache.commons.compress.archivers.ar", "ArArchiveInputStream") or
this.getASupertype*()
.hasQualifiedName("org.apache.commons.compress.archivers.jar", "JarArchiveInputStream") or
this.getASupertype*()
.hasQualifiedName("org.apache.commons.compress.archivers.zip", "ZipArchiveInputStream")
}
}
/**
* The methods that read bytes and belong to `*ArchiveInputStream` Types
*/
class ReadInputStreamCall extends DecompressionBomb::BombReadInputStreamCall {
ReadInputStreamCall() {
this.getReceiverType() instanceof TypeArchivers and
this.getCallee().hasName(["read", "readNBytes", "readAllBytes"])
}
}
/**
* Gets `n1` and `n2` which `CompressorInputStream n2 = new CompressorStreamFactory().createCompressorInputStream(n1)`
* or `ArchiveInputStream n2 = new ArchiveStreamFactory().createArchiveInputStream(n1)` or
* `n1.read(n2)`,
* second one is added because of sanitizer, we want to compare return value of each `read` or similar method
* that whether there is a flow to a comparison between total read of decompressed stream and a constant value
*/
private class CompressorsAndArchiversAdditionalTaintStep extends DecompressionBomb::AdditionalStep
{
override predicate step(DataFlow::Node n1, DataFlow::Node n2) {
exists(ConstructorCall call |
call.getCallee().getDeclaringType() instanceof TypeArchivers and
call.getArgument(0) = n1.asExpr() and
call = n2.asExpr()
)
}
}
}
/**
* Providing Decompression sinks and additional taint steps for `CompressorStreamFactory` and `ArchiveStreamFactory` Types
*/
module Factory {
/**
* A type that is responsible for `ArchiveInputStream` Class
*/
class TypeArchivers extends RefType {
TypeArchivers() {
this.getASupertype*()
.hasQualifiedName("org.apache.commons.compress.archivers", "ArchiveStreamFactory")
}
}
/**
* A type that is responsible for `CompressorStreamFactory` Class
*/
class TypeCompressors extends RefType {
TypeCompressors() {
this.getASupertype*()
.hasQualifiedName("org.apache.commons.compress.compressors", "CompressorStreamFactory")
}
}
/**
* Gets `n1` and `n2` which `ZipInputStream n2 = new ZipInputStream(n1)`
*/
private class CompressorsAndArchiversAdditionalTaintStep extends DecompressionBomb::AdditionalStep
{
override predicate step(DataFlow::Node n1, DataFlow::Node n2) {
exists(MethodCall call |
(
call.getCallee().getDeclaringType() instanceof TypeCompressors
or
call.getCallee().getDeclaringType() instanceof TypeArchivers
) and
call.getArgument(0) = n1.asExpr() and
call = n2.asExpr()
)
}
}
/**
* The methods that read bytes and belong to `CompressorInputStream` or `ArchiveInputStream` Types
*/
class ReadInputStreamCall extends DecompressionBomb::BombReadInputStreamCall {
ReadInputStreamCall() {
(
this.getReceiverType() instanceof TypeArchiveInputStream
or
this.getReceiverType() instanceof TypeCompressorInputStream
) and
this.getCallee().hasName(["read", "readNBytes", "readAllBytes"])
}
}
}
}
/**
* Providing Decompression sinks and additional taint steps for `net.lingala.zip4j.io` package
*/
module Zip4j {
/**
* A type that is responsible for `ZipInputStream` Class
*/
class TypeZipInputStream extends RefType {
TypeZipInputStream() {
this.hasQualifiedName("net.lingala.zip4j.io.inputstream", "ZipInputStream")
}
}
/**
* The methods that read bytes and belong to `CompressorInputStream` or `ArchiveInputStream` Types
*/
class ReadInputStreamCall extends DecompressionBomb::BombReadInputStreamCall {
ReadInputStreamCall() {
this.getReceiverType() instanceof TypeZipInputStream and
this.getMethod().hasName(["read", "readNBytes", "readAllBytes"])
}
}
/**
* Gets `n1` and `n2` which `CompressorInputStream n2 = new CompressorStreamFactory().createCompressorInputStream(n1)`
* or `ArchiveInputStream n2 = new ArchiveStreamFactory().createArchiveInputStream(n1)` or
* `n1.read(n2)`,
* second one is added because of sanitizer, we want to compare return value of each `read` or similar method
* that whether there is a flow to a comparison between total read of decompressed stream and a constant value
*/
private class CompressorsAndArchiversAdditionalTaintStep extends DecompressionBomb::AdditionalStep
{
override predicate step(DataFlow::Node n1, DataFlow::Node n2) {
exists(ConstructorCall call |
call.getCallee().getDeclaringType() instanceof TypeZipInputStream and
call.getArgument(0) = n1.asExpr() and
call = n2.asExpr()
)
}
}
}
/**
* Providing Decompression sinks and additional taint steps for `java.util.zip` package
*/
module Zip {
/**
* The Types that are responsible for `ZipInputStream`, `GZIPInputStream`, `InflaterInputStream` Classes
*/
class TypeInputStream extends RefType {
TypeInputStream() {
this.getASupertype*()
.hasQualifiedName("java.util.zip",
["ZipInputStream", "GZIPInputStream", "InflaterInputStream"])
}
}
/**
* The methods that read bytes and belong to `*InputStream` Types
*/
class ReadInputStreamCall extends DecompressionBomb::BombReadInputStreamCall {
ReadInputStreamCall() {
this.getReceiverType() instanceof TypeInputStream and
this.getCallee().hasName(["read", "readNBytes", "readAllBytes"])
}
}
/**
* A type that is responsible for `Inflater` Class
*/
class TypeInflator extends RefType {
TypeInflator() { this.hasQualifiedName("java.util.zip", "Inflater") }
}
class InflateSink extends DecompressionBomb::Sink {
InflateSink() {
exists(MethodCall ma |
ma.getReceiverType() instanceof TypeInflator and
ma.getCallee().hasName("inflate") and
ma.getArgument(0) = this.asExpr()
or
ma.getReceiverType() instanceof TypeInflator and
ma.getMethod().hasName("setInput") and
ma.getArgument(0) = this.asExpr()
)
}
}
class ZipFileSink extends DecompressionBomb::Sink {
ZipFileSink() {
exists(MethodCall call |
call.getCallee().getDeclaringType() instanceof TypeZipFile and
call.getCallee().hasName("getInputStream") and
call.getQualifier() = this.asExpr()
)
}
}
/**
* A type that is responsible for `ZipFile` Class
*/
class TypeZipFile extends RefType {
TypeZipFile() { this.hasQualifiedName("java.util.zip", "ZipFile") }
}
}

View File

@@ -0,0 +1,14 @@
import experimental.semmle.code.java.security.FileAndFormRemoteSource
import experimental.semmle.code.java.security.DecompressionBomb::DecompressionBomb
module DecompressionBombsConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
predicate isAdditionalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
any(AdditionalStep ads).step(nodeFrom, nodeTo)
}
}
module DecompressionBombsFlow = TaintTracking::Global<DecompressionBombsConfig>;

View File

@@ -0,0 +1,118 @@
import java
import semmle.code.java.dataflow.FlowSources
class CommonsFileUploadAdditionalTaintStep extends Unit {
abstract predicate step(DataFlow::Node n1, DataFlow::Node n2);
}
module ApacheCommonsFileUpload {
module RemoteFlowSource {
class TypeServletFileUpload extends RefType {
TypeServletFileUpload() {
this.hasQualifiedName("org.apache.commons.fileupload.servlet", "ServletFileUpload")
}
}
class TypeFileUpload extends RefType {
TypeFileUpload() {
this.getAStrictAncestor*().hasQualifiedName("org.apache.commons.fileupload", "FileItem")
}
}
class TypeFileItemStream extends RefType {
TypeFileItemStream() {
this.getAStrictAncestor*()
.hasQualifiedName("org.apache.commons.fileupload", "FileItemStream")
}
}
class ServletFileUpload extends RemoteFlowSource {
ServletFileUpload() {
exists(MethodCall ma |
ma.getReceiverType() instanceof TypeServletFileUpload and
ma.getCallee().hasName("parseRequest") and
this.asExpr() = ma
)
}
override string getSourceType() { result = "Apache Commons Fileupload" }
}
private class FileItemRemoteSource extends RemoteFlowSource {
FileItemRemoteSource() {
exists(MethodCall ma |
ma.getReceiverType() instanceof TypeFileUpload and
ma.getCallee()
.hasName([
"getInputStream", "getFieldName", "getContentType", "get", "getName", "getString"
]) and
this.asExpr() = ma
)
}
override string getSourceType() { result = "Apache Commons Fileupload" }
}
private class FileItemStreamRemoteSource extends RemoteFlowSource {
FileItemStreamRemoteSource() {
exists(MethodCall ma |
ma.getReceiverType() instanceof TypeFileItemStream and
ma.getCallee().hasName(["getContentType", "getFieldName", "getName", "openStream"]) and
this.asExpr() = ma
)
}
override string getSourceType() { result = "Apache Commons Fileupload" }
}
}
module Util {
class TypeStreams extends RefType {
TypeStreams() { this.hasQualifiedName("org.apache.commons.fileupload.util", "Streams") }
}
private class AsStringAdditionalTaintStep extends CommonsFileUploadAdditionalTaintStep {
override predicate step(DataFlow::Node n1, DataFlow::Node n2) {
exists(Call call |
call.getCallee().getDeclaringType() instanceof TypeStreams and
call.getArgument(0) = n1.asExpr() and
call = n2.asExpr() and
call.getCallee().hasName("asString")
)
}
}
private class CopyAdditionalTaintStep extends CommonsFileUploadAdditionalTaintStep {
override predicate step(DataFlow::Node n1, DataFlow::Node n2) {
exists(Call call |
call.getCallee().getDeclaringType() instanceof TypeStreams and
call.getArgument(0) = n1.asExpr() and
call.getArgument(1) = n2.asExpr() and
call.getCallee().hasName("copy")
)
}
}
}
}
module ServletRemoteMultiPartSources {
class TypePart extends RefType {
TypePart() { this.hasQualifiedName(["javax.servlet.http", "jakarta.servlet.http"], "Part") }
}
private class ServletPartCalls extends RemoteFlowSource {
ServletPartCalls() {
exists(MethodCall ma |
ma.getReceiverType() instanceof TypePart and
ma.getCallee()
.hasName([
"getInputStream", "getName", "getContentType", "getHeader", "getHeaders",
"getHeaderNames", "getSubmittedFileName", "write"
]) and
this.asExpr() = ma
)
}
override string getSourceType() { result = "Javax Servlet Http" }
}
}