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

jar: iterate over zip file headers instead of using fs.WalkDir #20

Merged
merged 1 commit into from Dec 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
72 changes: 23 additions & 49 deletions jar/jar.go
Expand Up @@ -56,9 +56,9 @@ type Report struct {

// Parse traverses a JAR file, attempting to detect any usages of vulnerable
// log4j versions.
func Parse(r fs.FS) (*Report, error) {
func Parse(r *zip.Reader) (*Report, error) {
var c checker
if err := c.checkJAR(&zipFS{r}, 0, 0); err != nil {
if err := c.checkJAR(r, 0, 0); err != nil {
return nil, fmt.Errorf("failed to check JAR: %v", err)
}
return &Report{
Expand All @@ -68,35 +68,6 @@ func Parse(r fs.FS) (*Report, error) {
}, nil
}

// zipFS exists because of bugs hit in encoding/zip that causes reading "."
// to return "." as one of the entries. This in turn, makes fs.WalkDir()
// recurse infinitely.
//
// See: https://go.dev/issue/50179
type zipFS struct {
fs.FS
}

// ReadDir overrides the buggy zip.Reader behavior and removes any DirEntry
// values that match the provided name.
func (z *zipFS) ReadDir(name string) ([]fs.DirEntry, error) {
entries, err := fs.ReadDir(z.FS, name)
if err != nil {
return nil, err
}

j := 0
for i := 0; i < len(entries); i++ {
if entries[i].Name() == name {
continue
}
entries[j] = entries[i]
j++
}
entries = entries[:j]
return entries, nil
}

type checker struct {
// Does the JAR contain the JNDI lookup class?
hasLookupClass bool
Expand All @@ -119,20 +90,26 @@ func (c *checker) bad() bool {
return (c.hasLookupClass && c.hasOldJndiManagerConstructor) || (c.hasLookupClass && c.seenJndiManagerClass && !c.isAtLeastTwoDotSixteen)
}

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

err := fs.WalkDir(r, ".", func(p string, d fs.DirEntry, err error) error {
if err != nil {
// Workaround for http://go.dev/issues/50390.
var perr *fs.PathError
if errors.As(err, &perr) && d.IsDir() && !strings.HasSuffix(p, "/") && perr.Op == "readdir" {
func walkZIP(r *zip.Reader, fn func(f *zip.File) error) error {
for _, f := range r.File {
if err := fn(f); err != nil {
if errors.Is(err, fs.SkipDir) {
return nil
}
return err
}
}
return nil
}

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)
}

err := walkZIP(r, func(zf *zip.File) error {
d := fs.FileInfoToDirEntry(zf.FileInfo())
p := zf.Name
if c.done() {
if d.IsDir() {
return fs.SkipDir
Expand All @@ -151,16 +128,13 @@ func (c *checker) checkJAR(r fs.FS, depth int, size int64) error {
return nil
}

f, err := r.Open(p)
f, err := zf.Open()
if err != nil {
return fmt.Errorf("opening file %s: %v", p, err)
}
defer f.Close()

info, err := f.Stat()
if err != nil {
return fmt.Errorf("stat file %s: %v", p, err)
}
info := zf.FileInfo()
var r io.Reader = f
if fsize := info.Size(); fsize > 0 {
if fsize+size > maxZipSize {
Expand Down Expand Up @@ -188,7 +162,7 @@ func (c *checker) checkJAR(r fs.FS, depth int, size int64) error {
return nil
}
if p == "META-INF/MANIFEST.MF" {
mf, err := r.Open(p)
mf, err := zf.Open()
if err != nil {
return fmt.Errorf("opening manifest file %s: %v", p, err)
}
Expand Down Expand Up @@ -232,7 +206,7 @@ func (c *checker) checkJAR(r fs.FS, depth int, size int64) error {
if size+fi.Size() > maxZipSize {
return fmt.Errorf("archive inside archive at %q is greater than 4GB, skipping", p)
}
f, err := r.Open(p)
f, err := zf.Open()
if err != nil {
return fmt.Errorf("open file %s: %v", p, err)
}
Expand All @@ -250,7 +224,7 @@ func (c *checker) checkJAR(r fs.FS, depth int, size int64) error {
}
return fmt.Errorf("parsing file %s: %v", p, err)
}
if err := c.checkJAR(&zipFS{r2}, depth+1, size+fi.Size()); err != nil {
if err := c.checkJAR(r2, depth+1, size+fi.Size()); err != nil {
return fmt.Errorf("checking sub jar %s: %v", p, err)
}
return nil
Expand Down
4 changes: 2 additions & 2 deletions jar/jar_test.go
Expand Up @@ -70,7 +70,7 @@ func TestParse(t *testing.T) {
t.Fatalf("zip.OpenReader failed: %v", err)
}
defer zr.Close()
report, err := Parse(zr)
report, err := Parse(&zr.Reader)
if err != nil {
t.Fatalf("Scan() returned an unexpected error, got %v, want nil", err)
}
Expand All @@ -92,7 +92,7 @@ func BenchmarkParse(b *testing.B) {
defer zr.Close()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, err := Parse(zr)
_, err := Parse(&zr.Reader)
if err != nil {
b.Errorf("Scan() returned an unexpected error, got %v, want nil", err)
}
Expand Down