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

Commit 48dbe0d

Browse files
singlethinkericchiang
authored andcommitted
Report CVEs identified
Updates jar.Parse to return the specific CVEs identified during the scan. To ensure that refactoring of detection logic is correct, I've tested this on a corpus of all log4j2 releases through 2.16.0 and verified that there are no false positives or false negatives.
1 parent eb7610e commit 48dbe0d

File tree

2 files changed

+259
-58
lines changed

2 files changed

+259
-58
lines changed

jar/jar.go

+72-18
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ const (
4848
jndiLookupClass = "JndiLookup.class"
4949
)
5050

51+
// CVEs detected. We define them as constants to catch typos.
52+
const (
53+
cve_2021_44228 cveID = "CVE-2021-44228" // JNDI
54+
cve_2021_45046 cveID = "CVE-2021-45046" // Thread Context Lookup
55+
)
56+
57+
type cveID string
58+
59+
func (id cveID) String() string {
60+
return string(id)
61+
}
62+
5163
// Parser allows tuning paramters of a vulnerable log4j scan. The
5264
// zero value provides reasonable defaults.
5365
type Parser struct {
@@ -101,8 +113,15 @@ func (p *Parser) Parse(r *zip.Reader) (*Report, error) {
101113
if err := c.checkJAR(r, 0, 0, p.Name); err != nil {
102114
return nil, fmt.Errorf("failed to check JAR: %v", err)
103115
}
116+
117+
var vs []*Vuln
118+
for _, id := range c.cves() {
119+
vs = append(vs, &Vuln{CVE: id.String()})
120+
}
121+
104122
return &Report{
105123
Vulnerable: c.bad(),
124+
Vulns: vs,
106125
MainClass: c.mainClass,
107126
Version: c.version,
108127
}, nil
@@ -116,12 +135,21 @@ type Report struct {
116135
// Note that this package considers the 2.15.0 versions vulnerable.
117136
Vulnerable bool
118137

138+
// Vulns gives details on the individual vulnerabilities detected.
139+
Vulns []*Vuln
140+
119141
// MainClass and Version are information taken from the MANIFEST.MF file.
120142
// Version indicates the version of JAR, NOT the log4j package.
121143
MainClass string
122144
Version string
123145
}
124146

147+
// Vuln reports details of a vulnerability detected.
148+
type Vuln struct {
149+
// CVE is the CVE ID of the vulnerability.
150+
CVE string
151+
}
152+
125153
// Parse traverses a JAR file, attempting to detect any usages of
126154
// vulnerable log4j versions.
127155
func Parse(r *zip.Reader) (*Report, error) {
@@ -223,29 +251,55 @@ func (c *checker) done() bool {
223251
return c.bad() && c.mainClass != ""
224252
}
225253

226-
func (c *checker) bad() bool {
227-
// Care must be taken in the formulae below with respect to the
228-
// !c.hasIsJndiEnabled clause. It is satisfied by default until
229-
// JndiManager.class is encountered. To prevent early termination of a
230-
// scan with an incorrect result, we have to ensure that we have already
231-
// encountered JndiManager.class (e.g. hasJndiManager*) or we have
232-
// encountered positive evidence that it will be absent
233-
// (i.e. log4j <2.1).
234-
254+
// Vulnerability signatures.
255+
// Note: Care must be taken in the formulae below with respect to the
256+
// !c.hasIsJndiEnabled clause. It is satisfied by default until
257+
// JndiManager.class is encountered. To prevent early termination of
258+
// a scan with an incorrect result, we have to ensure that we have
259+
// already encountered JndiManager.class (e.g. hasJndiManager*) or we
260+
// have encountered positive evidence that it will be absent
261+
// (i.e. log4j <2.1).
262+
var sigs = map[cveID]func(*checker) bool{
235263
// CVE-2021-44228 - Initial log4shell vulnerability affecting
236264
// Log4j2 2.0-beta9 through 2.12.1 (inclusive, 2.12.2 is not
237265
// vulnerable) and 2.13.0 through 2.15.0 (exclusive).
238-
vulnerablePre215 := c.hasLookupClass && // unpatched >=2.0-beta9 and
239-
(c.hasInitialContext || // <2.1
240-
c.hasJndiManagerPre215) && // >=2.1 && <2.15 and
241-
!c.hasIsJndiEnabled // <2.16 && !2.12.2
266+
cve_2021_44228: func(c *checker) bool {
267+
return c.hasLookupClass && // unpatched >=2.0-beta9 and
268+
(c.hasInitialContext || // <2.1
269+
c.hasJndiManagerPre215) && // >=2.1 && <2.15 and
270+
!c.hasIsJndiEnabled // <2.16 && !2.12.2
271+
},
272+
273+
// CVE-2021-45046 - Thread Context Lookup Pattern
274+
// vulnerability affects all Log4j2 versions >=2.0-beta9 and
275+
// <=2.15.0, except for 2.12.2.
276+
// See: https://logging.apache.org/log4j/2.x/security.html
277+
cve_2021_45046: func(c *checker) bool {
278+
return c.hasLookupClass && // unpatched >=2.0-beta9 and
279+
(c.hasInitialContext || // <2.1
280+
c.hasJndiManagerClass) && // >=2.1 and
281+
!c.hasIsJndiEnabled // <2.16 && !2.12.2
282+
},
283+
}
242284

243-
// CVE-2021-45046 - Log4j2 2.15.0
244-
vulnerable215 := c.hasLookupClass && // unpatched >=2.0-beta9 and
245-
c.hasJndiManagerClass && // >=2.1 and
246-
!c.hasIsJndiEnabled // <2.16 && !2.12.2
285+
func (c *checker) bad() bool {
286+
for _, s := range sigs {
287+
if s(c) {
288+
return true
289+
}
290+
}
291+
292+
return false
293+
}
247294

248-
return vulnerablePre215 || vulnerable215
295+
func (c *checker) cves() []cveID {
296+
var ids []cveID
297+
for id, sig := range sigs {
298+
if sig(c) {
299+
ids = append(ids, id)
300+
}
301+
}
302+
return ids
249303
}
250304

251305
const bufSize = 4 << 10 // 4 KiB

jar/jar_test.go

+187-40
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"fmt"
2020
"io"
2121
"path/filepath"
22+
"strings"
2223
"testing"
2324

2425
"github.com/google/go-cmp/cmp"
@@ -33,54 +34,181 @@ func TestParse(t *testing.T) {
3334
testCases := []struct {
3435
filename string
3536
wantBad bool
37+
wantCVEs []cveID
3638
}{
37-
{"400mb.jar", false},
38-
{"400mb_jar_in_jar.jar", false},
39-
{"arara.jar", true},
40-
{"arara.jar.patched", false},
41-
{"arara.signed.jar", true},
42-
{"arara.signed.jar.patched", false},
43-
{"log4j-core-2.0-beta9.jar", true},
44-
{"log4j-core-2.12.1.jar", true},
45-
{"log4j-core-2.12.1.jar.patched", false},
39+
{
40+
filename: "400mb.jar",
41+
wantBad: false,
42+
},
43+
{
44+
filename: "400mb_jar_in_jar.jar",
45+
wantBad: false,
46+
},
47+
{
48+
filename: "arara.jar",
49+
wantBad: true,
50+
wantCVEs: []cveID{cve_2021_44228, cve_2021_45046},
51+
},
52+
{
53+
filename: "arara.jar.patched",
54+
wantBad: false,
55+
},
56+
{
57+
filename: "arara.signed.jar",
58+
wantBad: true,
59+
wantCVEs: []cveID{cve_2021_44228, cve_2021_45046},
60+
},
61+
{
62+
filename: "arara.signed.jar.patched",
63+
wantBad: false,
64+
},
65+
{
66+
filename: "log4j-core-2.0-beta9.jar",
67+
wantBad: true,
68+
wantCVEs: []cveID{cve_2021_44228, cve_2021_45046},
69+
},
70+
{
71+
filename: "log4j-core-2.12.1.jar",
72+
wantBad: true,
73+
wantCVEs: []cveID{cve_2021_44228, cve_2021_45046},
74+
},
75+
{
76+
filename: "log4j-core-2.12.1.jar.patched",
77+
wantBad: false,
78+
},
4679
// log4j 2.12.2 is not affected by log4shell.
4780
// See: https://logging.apache.org/log4j/2.x/security.html
48-
{"log4j-core-2.12.2.jar", false},
49-
{"log4j-core-2.14.0.jar", true},
50-
{"log4j-core-2.14.0.jar.patched", false},
51-
{"log4j-core-2.15.0.jar", true},
52-
{"log4j-core-2.15.0.jar.patched", false},
53-
{"log4j-core-2.16.0.jar", false},
54-
{"log4j-core-2.1.jar", true},
55-
{"log4j-core-2.1.jar.patched", false},
56-
{"safe1.jar", false},
57-
{"safe1.signed.jar", false},
81+
{
82+
filename: "log4j-core-2.12.2.jar",
83+
wantBad: false,
84+
},
85+
{
86+
filename: "log4j-core-2.14.0.jar",
87+
wantBad: true,
88+
wantCVEs: []cveID{cve_2021_44228, cve_2021_45046},
89+
},
90+
{
91+
filename: "log4j-core-2.14.0.jar.patched",
92+
wantBad: false,
93+
},
94+
{
95+
filename: "log4j-core-2.15.0.jar",
96+
wantBad: true,
97+
wantCVEs: []cveID{cve_2021_45046},
98+
},
99+
{
100+
filename: "log4j-core-2.15.0.jar.patched",
101+
wantBad: false,
102+
},
103+
{
104+
filename: "log4j-core-2.16.0.jar",
105+
wantBad: false,
106+
},
107+
{
108+
filename: "log4j-core-2.1.jar",
109+
wantBad: true,
110+
wantCVEs: []cveID{cve_2021_44228, cve_2021_45046},
111+
},
112+
{
113+
filename: "log4j-core-2.1.jar.patched",
114+
wantBad: false,
115+
},
116+
{
117+
filename: "safe1.jar",
118+
wantBad: false,
119+
},
120+
{
121+
filename: "safe1.signed.jar",
122+
wantBad: false,
123+
},
58124
// Archive contains a malformed directory that causes archive/zip to
59125
// return an error.
60126
// See https://go.dev/issues/50390
61-
{"selenium-api-3.141.59.jar", false},
127+
{
128+
filename: "selenium-api-3.141.59.jar",
129+
wantBad: false,
130+
},
62131
// Test case where it contains a JndiLookupOther.class file that shouldn't be detected as vulnerable
63-
{"similarbutnotvuln.jar", false},
64-
{"vuln-class.jar", true},
65-
{"vuln-class-executable", true},
66-
{"vuln-class.jar.patched", false},
67-
{"good_jar_in_jar.jar", false},
68-
{"good_jar_in_jar_in_jar.jar", false},
69-
{"bad_jar_in_jar.jar", true},
70-
{"bad_jar_in_jar.jar.patched", false},
71-
{"bad_jar_in_jar_in_jar.jar", true},
72-
{"bad_jar_in_jar_in_jar.jar.patched", false},
73-
{"bad_jar_with_invalid_jar.jar", true},
74-
{"bad_jar_with_invalid_jar.jar.patched", false},
75-
{"good_jar_with_invalid_jar.jar", false},
76-
{"helloworld-executable", false},
77-
{"helloworld.jar", false},
78-
{"helloworld.signed.jar", false},
132+
{
133+
filename: "similarbutnotvuln.jar",
134+
wantBad: false,
135+
},
136+
{
137+
filename: "vuln-class.jar",
138+
wantBad: true,
139+
wantCVEs: []cveID{cve_2021_44228, cve_2021_45046},
140+
},
141+
{
142+
filename: "vuln-class-executable",
143+
wantBad: true,
144+
wantCVEs: []cveID{cve_2021_44228, cve_2021_45046},
145+
},
146+
{
147+
filename: "vuln-class.jar.patched",
148+
wantBad: false,
149+
},
150+
{
151+
filename: "good_jar_in_jar.jar",
152+
wantBad: false,
153+
},
154+
{
155+
filename: "good_jar_in_jar_in_jar.jar",
156+
wantBad: false,
157+
},
158+
{
159+
filename: "bad_jar_in_jar.jar",
160+
wantBad: true,
161+
wantCVEs: []cveID{cve_2021_44228, cve_2021_45046},
162+
},
163+
{
164+
filename: "bad_jar_in_jar.jar.patched",
165+
wantBad: false,
166+
},
167+
{
168+
filename: "bad_jar_in_jar_in_jar.jar",
169+
wantBad: true,
170+
wantCVEs: []cveID{cve_2021_44228, cve_2021_45046},
171+
},
172+
{
173+
filename: "bad_jar_in_jar_in_jar.jar.patched",
174+
wantBad: false,
175+
},
176+
{
177+
filename: "bad_jar_with_invalid_jar.jar",
178+
wantBad: true,
179+
wantCVEs: []cveID{cve_2021_44228, cve_2021_45046},
180+
},
181+
{
182+
filename: "bad_jar_with_invalid_jar.jar.patched",
183+
wantBad: false,
184+
},
185+
{
186+
filename: "good_jar_with_invalid_jar.jar",
187+
wantBad: false,
188+
},
189+
{
190+
filename: "helloworld-executable",
191+
wantBad: false,
192+
},
193+
{
194+
filename: "helloworld.jar",
195+
wantBad: false,
196+
},
197+
{
198+
filename: "helloworld.signed.jar",
199+
wantBad: false,
200+
},
79201

80202
// Ensure robustness to zip bombs from
81203
// https://www.bamsoftware.com/hacks/zipbomb/.
82-
{"zipbombs/zbsm_in_jar.jar", false},
83-
{"zipbombs/zbsm.jar", false},
204+
{
205+
filename: "zipbombs/zbsm_in_jar.jar",
206+
wantBad: false,
207+
},
208+
{
209+
filename: "zipbombs/zbsm.jar",
210+
wantBad: false,
211+
},
84212
}
85213
for _, tc := range testCases {
86214
t.Run(tc.filename, func(t *testing.T) {
@@ -92,11 +220,15 @@ func TestParse(t *testing.T) {
92220
defer zr.Close()
93221
report, err := Parse(&zr.Reader)
94222
if err != nil {
95-
t.Fatalf("Scan() returned an unexpected error, got %v, want nil", err)
223+
t.Fatalf("Parse() returned an unexpected error, got %v, want nil", err)
96224
}
97225
got := report.Vulnerable
98226
if tc.wantBad != got {
99-
t.Errorf("checkJAR() returned unexpected value, got bad=%t, want bad=%t", got, tc.wantBad)
227+
t.Errorf("Parse() returned unexpected value, got bad=%t, want bad=%t", got, tc.wantBad)
228+
}
229+
230+
if diff := cmp.Diff(tc.wantCVEs, vulnIDs(report.Vulns), cmpopts.EquateEmpty(), cmpopts.SortSlices(cveIDLess)); diff != "" {
231+
t.Errorf("Parse() returned unexpected Vulns, diff (-want +got):\n%s", diff)
100232
}
101233
})
102234
}
@@ -417,3 +549,18 @@ func (f *faultReader) Read(b []byte) (int, error) {
417549
}
418550
return n, err
419551
}
552+
553+
// vulnIDs extracts the cveIDs from an array of Vulns.
554+
func vulnIDs(vs []*Vuln) []cveID {
555+
var ids []cveID
556+
for _, v := range vs {
557+
ids = append(ids, cveID(v.CVE))
558+
}
559+
return ids
560+
}
561+
562+
// cveIDLess returns true if a comes lexically before b. It can be
563+
// used with cmpopts.SortSlices.
564+
func cveIDLess(a, b cveID) bool {
565+
return strings.Compare(a.String(), b.String()) < 0
566+
}

0 commit comments

Comments
 (0)