Skip to content
This repository was archived by the owner on Jun 30, 2023. It is now read-only.

Commit

Permalink
Make maximum recursion depth and maximum in memory size configurable
Browse files Browse the repository at this point in the history
Adds tests with large jars and zip bombs to verify that recursion
depth and memory limits are respected as well as to improve
adversarial test coverage.
  • Loading branch information
singlethink authored and ericchiang committed Jan 14, 2022
1 parent 0c8fe23 commit ffa77c8
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 21 deletions.
76 changes: 55 additions & 21 deletions jar/jar.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,6 @@ import (
"rsc.io/binaryregexp"
)

const (
maxZipDepth = 16
maxZipSize = 4 << 30 // 4GiB
)

var exts = map[string]bool{
".jar": true,
".war": true,
Expand All @@ -45,6 +40,50 @@ var exts = map[string]bool{
".jmod": true,
}

// Parser allows tuning paramters of a vulnerable log4j scan. The
// zero value provides reasonable defaults.
type Parser struct {
// MaxDepth is the maximum depth of recursive archives below
// the top level that will be unpacked. Default is 16.
MaxDepth int
// MaxBytes is the maximum size of files that will be
// read into memory during scanning. Default is 4GiB.
MaxBytes int64
}

const (
defaultMaxZipDepth = 16
defaultMaxZipBytes = 4 << 30 // 4GiB
)

func (ch *Parser) maxDepth() int {
if ch.MaxDepth == 0 {
return defaultMaxZipDepth
}
return ch.MaxDepth
}

func (ch *Parser) maxBytes() int64 {
if ch.MaxBytes == 0 {
return defaultMaxZipBytes
}
return ch.MaxBytes
}

// Parse traverses a JAR file, attempting to detect any usages of
// vulnerable log4j versions.
func (ch *Parser) Parse(r *zip.Reader) (*Report, error) {
c := checker{Parser: ch}
if err := c.checkJAR(r, 0, 0); err != nil {
return nil, fmt.Errorf("failed to check JAR: %v", err)
}
return &Report{
Vulnerable: c.bad(),
MainClass: c.mainClass,
Version: c.version,
}, nil
}

// Report contains information about a scanned JAR.
type Report struct {
// Vulnerable reports if a vulnerable version of the log4j is included in the
Expand All @@ -59,18 +98,11 @@ type Report struct {
Version string
}

// Parse traverses a JAR file, attempting to detect any usages of vulnerable
// log4j versions.
// Parse traverses a JAR file, attempting to detect any usages of
// vulnerable log4j versions.
func Parse(r *zip.Reader) (*Report, error) {
var c checker
if err := c.checkJAR(r, 0, 0); err != nil {
return nil, fmt.Errorf("failed to check JAR: %v", err)
}
return &Report{
Vulnerable: c.bad(),
MainClass: c.mainClass,
Version: c.version,
}, nil
c := &Parser{}
return c.Parse(r)
}

// ReadCloser mirrors zip.ReadCloser.
Expand Down Expand Up @@ -140,6 +172,8 @@ func NewReader(ra io.ReaderAt, size int64) (zr *zip.Reader, offset int64, err er
}

type checker struct {
*Parser

// Does the JAR contain JndiLookup.class? This indicates
// log4j >=2.0-beta9 which hasn't been patched by removing
// JndiLookup.class.
Expand Down Expand Up @@ -192,8 +226,8 @@ var bufPool = sync.Pool{
}

func (c *checker) checkJAR(r *zip.Reader, depth int, size int64) error {
if depth > maxZipDepth {
return fmt.Errorf("reached max zip depth of %d", maxZipDepth)
if depth > c.maxDepth() {
return fmt.Errorf("reached max zip depth of %d", c.maxDepth())
}

err := walkZIP(r, func(zf *zip.File) error {
Expand Down Expand Up @@ -240,7 +274,7 @@ func (c *checker) checkJAR(r *zip.Reader, depth int, size int64) error {
if err != nil {
return fmt.Errorf("stat file %s: %v", p, err)
}
if fsize := info.Size(); fsize+size > maxZipSize {
if fsize := info.Size(); fsize+size > c.maxBytes() {
return fmt.Errorf("reading %s would exceed memory limit: %v", p, err)
}
buf := bufPool.Get().([]byte)
Expand Down Expand Up @@ -294,8 +328,8 @@ func (c *checker) checkJAR(r *zip.Reader, depth int, size int64) error {
}
// If we're about to read more than the max size we've configure ahead of time then stop.
// Note that this only applies to embedded ZIPs/JARs. The outer ZIP/JAR can still be larger than the limit.
if size+fi.Size() > maxZipSize {
return fmt.Errorf("archive inside archive at %q is greater than 4GB, skipping", p)
if size+fi.Size() > c.maxBytes() {
return fmt.Errorf("archive inside archive at %q is greater than %d bytes, skipping", p, c.maxBytes())
}
f, err := zf.Open()
if err != nil {
Expand Down
51 changes: 51 additions & 0 deletions jar/jar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ func TestParse(t *testing.T) {
filename string
wantBad bool
}{
{"400mb.jar", false},
{"400mb_jar_in_jar.jar", false},
{"arara.jar", true},
{"arara.jar.patched", false},
{"arara.signed.jar", true},
Expand Down Expand Up @@ -70,6 +72,11 @@ func TestParse(t *testing.T) {
{"helloworld-executable", false},
{"helloworld.jar", false},
{"helloworld.signed.jar", false},

// Ensure robustness to zip bombs from
// https://www.bamsoftware.com/hacks/zipbomb/.
{"zipbombs/zbsm_in_jar.jar", false},
{"zipbombs/zbsm.jar", false},
}
for _, tc := range testCases {
t.Run(tc.filename, func(t *testing.T) {
Expand All @@ -91,6 +98,50 @@ func TestParse(t *testing.T) {
}
}

func TestMaxBytes(t *testing.T) {
p := testdataPath("400mb_jar_in_jar.jar")
zr, _, err := OpenReader(p)
if err != nil {
t.Fatalf("zip.OpenReader failed: %v", err)
}
defer zr.Close()

c := &Parser{MaxBytes: 4 << 20 /* 4MiB */}
if r, err := c.Parse(&zr.Reader); err == nil {
t.Errorf("Parse() = %+v, want error", r)
}
}

func TestMaxDepth(t *testing.T) {
p := testdataPath("bad_jar_in_jar_in_jar.jar")
zr, _, err := OpenReader(p)
if err != nil {
t.Fatalf("zip.OpenReader failed: %v", err)
}
defer zr.Close()

c := &Parser{MaxDepth: 1}
if r, err := c.Parse(&zr.Reader); err == nil {
t.Errorf("Parse() = %+v, want error", r)
}
}

// TestInfiniteRecursion ensures that Parse does not get stuck in an
// infinitely recursive zip.
func TestInfiniteRecursion(t *testing.T) {
// Using infinite r.zip from https://research.swtch.com/zip.
p := testdataPath("zipbombs/r.zip")
zr, _, err := OpenReader(p)
if err != nil {
t.Fatalf("zip.OpenReader failed: %v", err)
}
defer zr.Close()
report, err := Parse(&zr.Reader)
if err == nil {
t.Errorf("Parse() failed to return error on infintely recursive zip, got %+v, want error", report)
}
}

func BenchmarkParse(b *testing.B) {
filename := "safe1.jar"
p := testdataPath(filename)
Expand Down
Binary file added jar/testdata/400mb.jar
Binary file not shown.
Binary file added jar/testdata/400mb_jar_in_jar.jar
Binary file not shown.
7 changes: 7 additions & 0 deletions jar/testdata/generate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,10 @@ exec java -jar $file_path "$@"
' > vuln-class-executable
cat vuln-class.jar >> vuln-class-executable
chmod +x vuln-class-executable

mkdir -p tmp
dd if=/dev/zero of=tmp/400mb bs=1M count=400
zip 400mb.jar tmp/400mb
rm -rf tmp

zip 400mb_jar_in_jar.jar 400mb.jar
Binary file added jar/testdata/zipbombs/r.zip
Binary file not shown.
Binary file added jar/testdata/zipbombs/zbsm.jar
Binary file not shown.
Binary file added jar/testdata/zipbombs/zbsm_in_jar.jar
Binary file not shown.

0 comments on commit ffa77c8

Please sign in to comment.