├── .github
└── workflows
│ ├── release.yaml
│ └── test.yaml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── examples
└── parse.go
├── go.mod
├── go.sum
├── jar
├── jar.go
├── jar_test.go
├── rewrite.go
├── rewrite_test.go
├── testdata
│ ├── 400mb.jar
│ ├── 400mb_jar_in_jar.jar
│ ├── arara.jar
│ ├── arara.jar.patched
│ ├── arara.signed.jar
│ ├── arara.signed.jar.patched
│ ├── bad_jar_in_jar.jar
│ ├── bad_jar_in_jar.jar.patched
│ ├── bad_jar_in_jar_in_jar.jar
│ ├── bad_jar_in_jar_in_jar.jar.patched
│ ├── bad_jar_with_invalid_jar.jar
│ ├── bad_jar_with_invalid_jar.jar.patched
│ ├── corrupt.jar
│ ├── corrupt_jar_in_jar.jar
│ ├── corruptjar.go
│ ├── emptydir.zip
│ ├── emptydirs.zip
│ ├── generate.sh
│ ├── good_jar_in_jar.jar
│ ├── good_jar_in_jar_in_jar.jar
│ ├── good_jar_with_invalid_jar.jar
│ ├── helloworld-executable
│ ├── helloworld.jar
│ ├── helloworld.signed.jar
│ ├── log4j-core-2.0-beta9.jar
│ ├── log4j-core-2.1.jar
│ ├── log4j-core-2.1.jar.patched
│ ├── log4j-core-2.12.1.jar
│ ├── log4j-core-2.12.1.jar.patched
│ ├── log4j-core-2.12.2.jar
│ ├── log4j-core-2.14.0.jar
│ ├── log4j-core-2.14.0.jar.patched
│ ├── log4j-core-2.15.0.jar
│ ├── log4j-core-2.15.0.jar.patched
│ ├── log4j-core-2.16.0.jar
│ ├── notarealjar.jar
│ ├── safe1.jar
│ ├── safe1.signed.jar
│ ├── selenium-api-3.141.59.jar
│ ├── shadow-6.1.0.jar
│ ├── similarbutnotvuln.jar
│ ├── vuln-class-executable
│ ├── vuln-class.jar
│ ├── vuln-class.jar.patched
│ └── zipbombs
│ │ ├── r.zip
│ │ ├── zbsm.jar
│ │ └── zbsm_in_jar.jar
├── walker.go
├── walker_other.go
├── walker_test.go
└── walker_unix.go
├── log4jscanner.go
├── log4jscanner_linux.go
├── log4jscanner_other.go
├── pool
├── dynamic.go
└── dynamic_test.go
├── scripts
├── build-release.sh
└── release.sh
└── third_party
└── zip
├── LICENSE
└── zip.go
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: release
2 | on:
3 | release:
4 | types: [created]
5 |
6 | jobs:
7 | release:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Install Go
11 | uses: actions/setup-go@v2
12 | with:
13 | go-version: 1.17.x
14 | - name: Checkout code
15 | uses: actions/checkout@v2
16 | - name: Build Release
17 | run: ./scripts/build-release.sh
18 | - name: Update Dependencies
19 | run: sudo apt-get update
20 | - name: Install Dependencies
21 | run: sudo apt-get install -y jq
22 | - name: Upload
23 | run: ./scripts/release.sh ./bin
24 | env:
25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
26 |
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: test
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - main
9 |
10 | jobs:
11 | test:
12 | strategy:
13 | matrix:
14 | os: [macos-latest, ubuntu-latest, windows-latest]
15 | go-version: [1.17.x]
16 | runs-on: ${{ matrix.os }}
17 | steps:
18 | - name: Install Go
19 | uses: actions/setup-go@v2
20 | with:
21 | go-version: ${{ matrix.go-version }}
22 | - name: Checkout code
23 | uses: actions/checkout@v2
24 | - name: Build
25 | run: go build ./...
26 | - name: Test
27 | run: go test ./...
28 | release:
29 | strategy:
30 | matrix:
31 | go-version: [1.17.x]
32 | runs-on: ubuntu-latest
33 | steps:
34 | - name: Install Go
35 | uses: actions/setup-go@v2
36 | with:
37 | go-version: ${{ matrix.go-version }}
38 | - name: Checkout code
39 | uses: actions/checkout@v2
40 | - name: Build Release
41 | run: ./scripts/build-release.sh v0.0.0+test
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bin/
2 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to Contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement (CLA). You (or your employer) retain the copyright to your
10 | contribution; this simply gives us permission to use and redistribute your
11 | contributions as part of the project. Head over to
12 | to see your current agreements on file or
13 | to sign a new one.
14 |
15 | You generally only need to submit a CLA once, so if you've already submitted one
16 | (even if it was for a different project), you probably don't need to do it
17 | again.
18 |
19 | ## Code Reviews
20 |
21 | All submissions, including submissions by project members, require review. We
22 | use GitHub pull requests for this purpose. Consult
23 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
24 | information on using pull requests.
25 |
26 | ## Community Guidelines
27 |
28 | This project follows
29 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/).
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # log4jscanner
2 |
3 | [](https://pkg.go.dev/github.com/google/log4jscanner/jar)
4 |
5 | A log4j vulnerability filesystem scanner and Go package for analyzing JAR files.
6 |
7 | ## Installing
8 |
9 | Pre-compiled binaries are available as [release assets][releases].
10 |
11 | To install from source with an existing [Go][go] v1.17+ installation, either
12 | use [go install][go-install]:
13 |
14 | ```
15 | go install github.com/google/log4jscanner@latest
16 | ```
17 |
18 | Or build from the repo directly:
19 |
20 | ```
21 | git clone https://github.com/google/log4jscanner.git
22 | cd log4jscanner
23 | go build -o log4jscanner
24 | ```
25 |
26 | [go]: https://go.dev/
27 | [go-install]: https://pkg.go.dev/cmd/go#hdr-Compile_and_install_packages_and_dependencies
28 | [releases]: https://github.com/google/log4jscanner/releases
29 |
30 | ## Command line tool
31 |
32 | This project includes a scanner that walks directory, printing any detected JARs
33 | to stdout.
34 |
35 | ```
36 | $ log4jscanner ./jar/testdata
37 | ./jar/testdata/bad_jar_in_jar.jar
38 | ./jar/testdata/log4j-core-2.1.jar
39 | ./jar/testdata/log4j-core-2.12.1.jar
40 | ./jar/testdata/log4j-core-2.14.0.jar
41 | ./jar/testdata/log4j-core-2.15.0.jar
42 | ./jar/testdata/vuln-class.jar
43 | ```
44 |
45 | Optionally, the `--rewrite` flag can actively remove the vulnerable class from
46 | detected JARs in-place.
47 |
48 | ```
49 | $ zipinfo /tmp/vuln-class.jar | grep Jndi
50 | -rw-r--r-- 3.0 unx 2937 bx defN 20-Nov-06 14:03 lookup/JndiLookup.class
51 | -rw-r--r-- 3.0 unx 5029 bx defN 20-Nov-06 14:03 net/JndiManager.class
52 | -rw-r--r-- 3.0 unx 249 bx defN 20-Nov-06 14:03 net/JndiManager$1.class
53 | -rw-r--r-- 3.0 unx 1939 bx defN 20-Nov-06 14:03 net/JndiManager$JndiManagerFactory.class
54 | $ log4jscanner --rewrite /tmp
55 | /tmp/vuln-class.jar
56 | $ zipinfo /tmp/vuln-class.jar | grep Jndi
57 | -rw-r--r-- 3.0 unx 5029 bx defN 20-Nov-06 14:03 net/JndiManager.class
58 | -rw-r--r-- 3.0 unx 249 bx defN 20-Nov-06 14:03 net/JndiManager$1.class
59 | -rw-r--r-- 3.0 unx 1939 bx defN 20-Nov-06 14:03 net/JndiManager$JndiManagerFactory.class
60 | ```
61 |
62 | On MacOS, you can scan the entire data directory with:
63 |
64 | ```
65 | $ sudo log4jscanner /System/Volumes/Data
66 | ```
67 |
68 | The scanner can also skip directories by passing glob patterns. On Linux, you
69 | may choose to scan the entire root filesystem, but skip site-specific paths
70 | (e.g. the `/data/*` directory). By default log4jscanner will not scan magic
71 | filesystems, such as /proc and /sys.
72 |
73 | ```
74 | $ sudo log4jscanner --skip '/data/*' /
75 | ```
76 |
77 | For heavy customization, such as reporting to external endpoints, much of the
78 | tool's logic is exposed through the [`jar.Walker`][jar-walker] API.
79 |
80 | [jar-walker]: https://pkg.go.dev/github.com/google/log4jscanner/jar#Walker
81 |
82 | ## Package
83 |
84 | Parsing logic is available through the `jar` package, and can be used to scan
85 | assets stored in other code repositories. Because JARs use the ZIP format, this
86 | package operates on [`archive/zip.Reader`][zip-reader].
87 |
88 | [zip-reader]: https://pkg.go.dev/archive/zip#Reader
89 |
90 | ```go
91 | import (
92 | "archive/zip"
93 | // ...
94 |
95 | "github.com/google/log4jscanner/jar"
96 | )
97 |
98 | func main() {
99 | rc, err := zip.OpenReader(pathToJARFile)
100 | if err != nil {
101 | if errors.Is(err, zip.ErrFormat) {
102 | // File isn't a ZIP file.
103 | return
104 | }
105 | log.Fatalf("opening class: %v", err)
106 | }
107 | defer rc.Close()
108 |
109 | if !jar.IsJAR(&rc.Reader) {
110 | // ZIP file isn't a JAR file.
111 | return
112 | }
113 |
114 | result, err := jar.Parse(&rc.Reader)
115 | if err != nil {
116 | log.Fatalf("parsing zip file: %v", err)
117 | }
118 | if result.Vulnerable {
119 | fmt.Println("File is vulnerable")
120 | }
121 | }
122 | ```
123 |
124 | See the `examples/` directory for full programs.
125 |
126 | ## False positives
127 |
128 | False positives have been observed for the scanner. Use caution when rewriting
129 | JARs automatically or taking other mitigations based on scan results.
130 |
131 | If you do hit a false positive, please open an issue.
132 |
133 | Note: This scanner purposefully flags the patched versions of log4j for Java 6
134 | and Java 7 as vulnerable.
135 |
136 | ## Contributors
137 |
138 | We unfortunately had to squash the history when open sourcing. The following
139 | contributors were instrumental in this project's development:
140 |
141 | - David Dworken ([@ddworken](https://github.com/ddworken))
142 | - Eric Chiang ([@ericchiang](https://github.com/ericchiang))
143 | - Julian Bangert
144 | - Mike Gerow ([@gerow](https://github.com/gerow))
145 | - Mit Dalsania
146 | - Tom D'Netto
147 |
--------------------------------------------------------------------------------
/examples/parse.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // The parse tool demonstrates how to use the Parse API.
16 | package main
17 |
18 | import (
19 | "archive/zip"
20 | "errors"
21 | "fmt"
22 | "log"
23 | "path/filepath"
24 | "runtime"
25 |
26 | "github.com/google/log4jscanner/jar"
27 | )
28 |
29 | const pathToJARFile = "../jar/testdata/vuln-class.jar"
30 |
31 | // fileDir returns the directory for this source file.
32 | func fileDir() string {
33 | _, file, _, _ := runtime.Caller(0)
34 | return filepath.Dir(file)
35 | }
36 |
37 | func main() {
38 | rc, err := zip.OpenReader(filepath.Join(fileDir(), pathToJARFile))
39 | if err != nil {
40 | if errors.Is(err, zip.ErrFormat) {
41 | // File isn't a ZIP file.
42 | return
43 | }
44 | log.Fatalf("opening class: %v", err)
45 | }
46 | defer rc.Close()
47 |
48 | if !jar.IsJAR(&rc.Reader) {
49 | // ZIP file isn't a JAR file.
50 | return
51 | }
52 |
53 | result, err := jar.Parse(&rc.Reader)
54 | if err != nil {
55 | log.Fatalf("parsing zip file: %v", err)
56 | }
57 | if result.Vulnerable {
58 | fmt.Println("File is vulnerable")
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/google/log4jscanner
2 |
3 | go 1.17
4 |
5 | require (
6 | github.com/google/go-cmp v0.5.6
7 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e
8 | rsc.io/binaryregexp v0.2.0
9 | )
10 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
2 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
3 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
4 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
5 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
6 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
7 | rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
8 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
9 |
--------------------------------------------------------------------------------
/jar/jar.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // Package jar implements JAR scanning capabilities for log4j.
16 | package jar
17 |
18 | import (
19 | "archive/zip"
20 | "bufio"
21 | "bytes"
22 | "errors"
23 | "fmt"
24 | "io"
25 | "io/fs"
26 | "os"
27 | "path"
28 | "path/filepath"
29 | "strings"
30 | "sync"
31 |
32 | "github.com/google/log4jscanner/pool"
33 | zipfork "github.com/google/log4jscanner/third_party/zip"
34 | "rsc.io/binaryregexp"
35 | )
36 |
37 | var exts = map[string]bool{
38 | ".jar": true,
39 | ".war": true,
40 | ".ear": true,
41 | ".zip": true,
42 | ".jmod": true,
43 | }
44 |
45 | // Names of class files to parse in detail.
46 | const (
47 | jndiManagerClass = "JndiManager.class"
48 | jndiLookupClass = "JndiLookup.class"
49 | )
50 |
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 |
63 | // Parser allows tuning paramters of a vulnerable log4j scan. The
64 | // zero value provides reasonable defaults.
65 | type Parser struct {
66 | // MaxDepth is the maximum depth of recursive archives below
67 | // the top level that will be unpacked. Default is 16.
68 | MaxDepth int
69 | // MaxBytes is the maximum size of files that will be
70 | // read into memory during scanning. Default is 4GiB.
71 | MaxBytes int64
72 | // Name is the name of the file being parsed. Default is "".
73 | Name string
74 | // FileError can be used to handle errors for a JAR file.
75 | // When checking a file returns an error other than
76 | // fs.SkipDir, FileError will be called with the offending
77 | // path and error. If FileError returns nil, checking will
78 | // continue. Otherwise, checking will abort. Default is to
79 | // abort checking whenever err != nil.
80 | FileError func(path string, err error) error
81 | }
82 |
83 | const (
84 | defaultMaxZipDepth = 16
85 | defaultMaxZipBytes = 4 << 30 // 4GiB
86 | )
87 |
88 | func (p *Parser) maxDepth() int {
89 | if p.MaxDepth == 0 {
90 | return defaultMaxZipDepth
91 | }
92 | return p.MaxDepth
93 | }
94 |
95 | func (p *Parser) maxBytes() int64 {
96 | if p.MaxBytes == 0 {
97 | return defaultMaxZipBytes
98 | }
99 | return p.MaxBytes
100 | }
101 |
102 | func (p *Parser) fileError(path string, err error) error {
103 | if p.FileError != nil {
104 | return p.FileError(path, err)
105 | }
106 | return err
107 | }
108 |
109 | // Parse traverses a JAR file, attempting to detect any usages of
110 | // vulnerable log4j versions.
111 | func (p *Parser) Parse(r *zip.Reader) (*Report, error) {
112 | c := checker{Parser: p}
113 | if err := c.checkJAR(r, 0, 0, p.Name); err != nil {
114 | return nil, fmt.Errorf("failed to check JAR: %v", err)
115 | }
116 |
117 | var vs []*Vuln
118 | for _, id := range c.cves() {
119 | vs = append(vs, &Vuln{CVE: id.String()})
120 | }
121 |
122 | return &Report{
123 | Vulnerable: c.bad(),
124 | Vulns: vs,
125 | MainClass: c.mainClass,
126 | Version: c.version,
127 | }, nil
128 | }
129 |
130 | // Report contains information about a scanned JAR.
131 | type Report struct {
132 | // Vulnerable reports if a vulnerable version of the log4j is included in the
133 | // JAR and has been initialized.
134 | //
135 | // Note that this package considers the 2.15.0 versions vulnerable.
136 | Vulnerable bool
137 |
138 | // Vulns gives details on the individual vulnerabilities detected.
139 | Vulns []*Vuln
140 |
141 | // MainClass and Version are information taken from the MANIFEST.MF file.
142 | // Version indicates the version of JAR, NOT the log4j package.
143 | MainClass string
144 | Version string
145 | }
146 |
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 |
153 | // Parse traverses a JAR file, attempting to detect any usages of
154 | // vulnerable log4j versions.
155 | func Parse(r *zip.Reader) (*Report, error) {
156 | c := &Parser{}
157 | return c.Parse(r)
158 | }
159 |
160 | // ReadCloser mirrors zip.ReadCloser.
161 | type ReadCloser struct {
162 | zip.Reader
163 |
164 | f *os.File
165 | }
166 |
167 | // Close closes the underlying file.
168 | func (r *ReadCloser) Close() error {
169 | return r.f.Close()
170 | }
171 |
172 | // OpenReader mirrors zip.OpenReader, loading a JAR from a file, but supports
173 | // self-executable JARs. See NewReader() for details.
174 | func OpenReader(path string) (r *ReadCloser, offset int64, err error) {
175 | f, err := os.Open(path)
176 | if err != nil {
177 | return
178 | }
179 | info, err := f.Stat()
180 | if err != nil {
181 | f.Close()
182 | return
183 | }
184 | zr, offset, err := NewReader(f, info.Size())
185 | if err != nil {
186 | f.Close()
187 | return
188 | }
189 | return &ReadCloser{*zr, f}, offset, nil
190 | }
191 |
192 | // offsetReader is a io.ReaderAt that starts at some offset from the start of
193 | // the file.
194 | type offsetReader struct {
195 | ra io.ReaderAt
196 | offset int64
197 | }
198 |
199 | func (o offsetReader) ReadAt(p []byte, off int64) (n int, err error) {
200 | return o.ra.ReadAt(p, off+o.offset)
201 | }
202 |
203 | // NewReader is a wrapper around zip.NewReader that supports self-executable
204 | // JARs. JAR files with prefixed data, such as a bash script to allow them to
205 | // run directly.
206 | //
207 | // If the ZIP contains a prefix, the returned offset indicates the size of the
208 | // prefix.
209 | //
210 | // See:
211 | // - https://kevinboone.me/execjava.html
212 | // - https://github.com/golang/go/issues/10464
213 | func NewReader(ra io.ReaderAt, size int64) (zr *zip.Reader, offset int64, err error) {
214 | offset, err = zipfork.ReadZIPOffset(ra, size)
215 | if err != nil {
216 | return nil, 0, err
217 | }
218 | if offset > 0 {
219 | ra = offsetReader{ra, offset}
220 | }
221 | zr, err = zip.NewReader(ra, size)
222 | return zr, offset, err
223 | }
224 |
225 | type checker struct {
226 | *Parser
227 |
228 | // Does the JAR contain JndiLookup.class? This indicates
229 | // log4j >=2.0-beta9 which hasn't been patched by removing
230 | // JndiLookup.class.
231 | hasLookupClass bool
232 | // Does JndiLookup have a reference to javax.naming.InitialContext? This
233 | // indicates log4j >=2.0-beta9 and <2.1.
234 | hasInitialContext bool
235 | // Does the JAR contain JndiManager.class, which indicates log4j >=2.1?
236 | hasJndiManagerClass bool
237 | // Does the JAR contain JndiManager with a constructor that
238 | // indicates log4j <2.15?
239 | hasJndiManagerPre215 bool
240 | // Does JndiManager have the isJndiEnabled method, which
241 | // exists in 2.16+ and 2.12.2 (which is not vulnerable to
242 | // log4shell)?
243 | hasIsJndiEnabled bool
244 |
245 | mainClass string
246 | version string
247 | }
248 |
249 | func (c *checker) done() bool {
250 | return c.bad() && c.mainClass != ""
251 | }
252 |
253 | // Vulnerability signatures.
254 | // Note: Care must be taken in the formulae below with respect to the
255 | // !c.hasIsJndiEnabled clause. It is satisfied by default until
256 | // JndiManager.class is encountered. To prevent early termination of
257 | // a scan with an incorrect result, we have to ensure that we have
258 | // already encountered JndiManager.class (e.g. hasJndiManager*) or we
259 | // have encountered positive evidence that it will be absent
260 | // (i.e. log4j <2.1).
261 | var sigs = map[cveID]func(*checker) bool{
262 | // CVE-2021-44228 - Initial log4shell vulnerability affecting
263 | // Log4j2 2.0-beta9 through 2.12.1 (inclusive, 2.12.2 is not
264 | // vulnerable) and 2.13.0 through 2.15.0 (exclusive).
265 | cve_2021_44228: func(c *checker) bool {
266 | return c.hasLookupClass && // unpatched >=2.0-beta9 and
267 | (c.hasInitialContext || // <2.1
268 | c.hasJndiManagerPre215) && // >=2.1 && <2.15 and
269 | !c.hasIsJndiEnabled // <2.16 && !2.12.2
270 | },
271 |
272 | // CVE-2021-45046 - Thread Context Lookup Pattern
273 | // vulnerability affects all Log4j2 versions >=2.0-beta9 and
274 | // <=2.15.0, except for 2.12.2.
275 | // See: https://logging.apache.org/log4j/2.x/security.html
276 | cve_2021_45046: func(c *checker) bool {
277 | return c.hasLookupClass && // unpatched >=2.0-beta9 and
278 | (c.hasInitialContext || // <2.1
279 | c.hasJndiManagerClass) && // >=2.1 and
280 | !c.hasIsJndiEnabled // <2.16 && !2.12.2
281 | },
282 | }
283 |
284 | func (c *checker) bad() bool {
285 | for _, s := range sigs {
286 | if s(c) {
287 | return true
288 | }
289 | }
290 |
291 | return false
292 | }
293 |
294 | func (c *checker) cves() []cveID {
295 | var ids []cveID
296 | for id, sig := range sigs {
297 | if sig(c) {
298 | ids = append(ids, id)
299 | }
300 | }
301 | return ids
302 | }
303 |
304 | const bufSize = 4 << 10 // 4 KiB
305 |
306 | var (
307 | bufPool = sync.Pool{
308 | New: func() interface{} {
309 | return make([]byte, bufSize)
310 | },
311 | }
312 | dynBufPool = pool.Dynamic{
313 | Pool: &sync.Pool{New: func() interface{} { return make([]byte, 0) }},
314 | MinUtility: bufSize,
315 | }
316 | )
317 |
318 | func (c *checker) checkJAR(r *zip.Reader, depth int, size int64, jar string) error {
319 | if depth > c.maxDepth() {
320 | return fmt.Errorf("reached max zip depth of %d", c.maxDepth())
321 | }
322 |
323 | for _, f := range r.File {
324 | if err := c.checkFile(f, depth, size, jar); err != nil {
325 | if errors.Is(err, fs.SkipDir) {
326 | return nil
327 | }
328 | if e := c.fileError(filepath.Join(jar, f.Name), err); e != nil {
329 | return e
330 | }
331 | }
332 | }
333 | return nil
334 | }
335 |
336 | func (c *checker) checkFile(zf *zip.File, depth int, size int64, jar string) error {
337 | d := fs.FileInfoToDirEntry(zf.FileInfo())
338 | p := zf.Name
339 | base := path.Base(p)
340 |
341 | if c.done() {
342 | if d.IsDir() {
343 | return fs.SkipDir
344 | }
345 | return nil
346 | }
347 |
348 | if !d.Type().IsRegular() {
349 | return nil
350 | }
351 | if strings.HasSuffix(p, ".class") {
352 | if c.bad() {
353 | // Already determined that the content is bad, no
354 | // need to check more.
355 | return nil
356 | }
357 |
358 | info := zf.FileInfo()
359 | if fsize := info.Size(); fsize+size > c.maxBytes() {
360 | return fmt.Errorf("reading %s would exceed memory limit", p)
361 | }
362 |
363 | // We only need to check JndiLookup and JndiManager classes. Bail before incurring
364 | // the cost of opening the file if we aren't going to check it.
365 | switch base {
366 | case jndiLookupClass:
367 | if !c.needsJndiLookupCheck() {
368 | return nil
369 | }
370 | case jndiManagerClass:
371 | if !c.needsJndiManagerCheck() {
372 | return nil
373 | }
374 | default:
375 | return nil
376 | }
377 |
378 | f, err := zf.Open()
379 | if err != nil {
380 | return fmt.Errorf("opening file %s: %v", p, err)
381 | }
382 | defer f.Close()
383 |
384 | buf := bufPool.Get().([]byte)
385 | defer bufPool.Put(buf)
386 |
387 | switch base {
388 | case jndiLookupClass:
389 | return c.checkJndiLookup(f, buf)
390 | case jndiManagerClass:
391 | return c.checkJndiManager(f, buf)
392 | }
393 | }
394 | if p == "META-INF/MANIFEST.MF" {
395 | mf, err := zf.Open()
396 | if err != nil {
397 | return fmt.Errorf("opening manifest file %s: %v", p, err)
398 | }
399 | defer mf.Close()
400 |
401 | buf := bufPool.Get().([]byte)
402 | defer bufPool.Put(buf)
403 |
404 | s := bufio.NewScanner(mf)
405 | s.Buffer(buf, bufio.MaxScanTokenSize)
406 | for s.Scan() {
407 | // Use s.Bytes instead of s.Text to avoid a string allocation.
408 | b := s.Bytes()
409 | // Use IndexByte directly instead of strings.Split to avoid allocating a return slice.
410 | i := bytes.IndexByte(b, ':')
411 | if i < 0 {
412 | continue
413 | }
414 | k, v := b[:i], b[i+1:]
415 | if bytes.IndexByte(v, ':') >= 0 {
416 | continue
417 | }
418 | if string(k) == "Main-Class" {
419 | c.mainClass = strings.TrimSpace(string(v))
420 | } else if string(k) == "Implementation-Version" {
421 | c.version = strings.TrimSpace(string(v))
422 | }
423 | }
424 | if err := s.Err(); err != nil {
425 | return fmt.Errorf("scanning manifest file %s: %v", p, err)
426 | }
427 | return nil
428 | }
429 |
430 | // Scan for jars within jars.
431 | if !exts[path.Ext(p)] {
432 | return nil
433 | }
434 | // We've found a jar in a jar. Open it!
435 | fi, err := d.Info()
436 | if err != nil {
437 | return fmt.Errorf("failed to get archive inside of archive %s: %v", p, err)
438 | }
439 | // If we're about to read more than the max size we've configure ahead of time then stop.
440 | // Note that this only applies to embedded ZIPs/JARs. The outer ZIP/JAR can still be larger than the limit.
441 | if size+fi.Size() > c.maxBytes() {
442 | return fmt.Errorf("archive inside archive at %q is greater than %d bytes, skipping", p, c.maxBytes())
443 | }
444 | f, err := zf.Open()
445 | if err != nil {
446 | return fmt.Errorf("open file %s: %v", p, err)
447 | }
448 | buf := dynBufPool.Get().([]byte)
449 | buf, err = readFull(f, fi, buf)
450 | defer dynBufPool.Put(buf, float64(len(buf)), float64(cap(buf)))
451 | f.Close() // Recycle the flate buffer earlier, we're going to recurse.
452 | if err != nil {
453 | return fmt.Errorf("read file %s: %v", p, err)
454 | }
455 | br := bytes.NewReader(buf)
456 | r2, err := zip.NewReader(br, br.Size())
457 | if err != nil {
458 | if err == zip.ErrFormat {
459 | // Not a zip file.
460 | return nil
461 | }
462 | return fmt.Errorf("parsing file %s: %v", p, err)
463 | }
464 | if err := c.checkJAR(r2, depth+1, size+fi.Size(), filepath.Join(jar, p)); err != nil {
465 | return fmt.Errorf("checking sub jar %s: %v", p, err)
466 | }
467 | return nil
468 | }
469 |
470 | func readFull(r io.Reader, fi os.FileInfo, buf []byte) ([]byte, error) {
471 | if !fi.Mode().IsRegular() {
472 | return io.ReadAll(r) // If not a regular file, size may not be accurate.
473 | }
474 | if size := int(fi.Size()); cap(buf) < size {
475 | capacity := size
476 | if capacity < bufSize {
477 | capacity = bufSize // Allocating much smaller buffers could lead to quick re-allocations.
478 | }
479 | buf = make([]byte, size, capacity)
480 | } else {
481 | buf = buf[:size]
482 | }
483 | n, err := io.ReadFull(r, buf)
484 | if err != nil || n != len(buf) {
485 | return buf, err
486 | }
487 | return buf, nil
488 | }
489 |
490 | // needsJndiManagerCheck returns true if there's something that we could learn by checking
491 | // JndiManager bytecode with checkJndiManager.
492 | func (c *checker) needsJndiManagerCheck() bool {
493 | return !c.hasJndiManagerClass || !c.hasJndiManagerPre215 || !c.hasIsJndiEnabled
494 | }
495 |
496 | const (
497 | // Replicate YARA rule:
498 | //
499 | // strings:
500 | // $JndiManagerConstructor = {
501 | // 3c 69 6e 69 74 3e 01 00 2b 28 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69
502 | // 6e 67 3b 4c 6a 61 76 61 78 2f 6e 61 6d 69 6e 67 2f 43 6f 6e 74 65 78 74 3b
503 | // 29 56
504 | // }
505 | //
506 | // https://github.com/darkarnium/Log4j-CVE-Detect/blob/main/rules/vulnerability/log4j/CVE-2021-44228.yar
507 |
508 | log4jYARARule = "\x3c\x69\x6e\x69\x74\x3e\x01\x00\x2b\x28\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x74\x72\x69\x6e\x67\x3b\x4c\x6a\x61\x76\x61\x78\x2f\x6e\x61\x6d\x69\x6e\x67\x2f\x43\x6f\x6e\x74\x65\x78\x74\x3b\x29\x56"
509 |
510 | // Relevant commit: https://github.com/apache/logging-log4j2/commit/44569090f1cf1e92c711fb96dfd18cd7dccc72ea
511 | // In 2.16 the JndiManager class added the method `isJndiEnabled`. This was
512 | // done so the Interpolator could check if JNDI was enabled. We expect the
513 | // existence of this method should be relatively stable over time.
514 | //
515 | // This is definitely a bit brittle and may mean we fail to detect future versions
516 | // correctly (e.g. if there is a 2.17 that changes the name of the method).
517 | // What we really would like is something that was removed (a method, a
518 | // constructor, a string, anything...) in 2.16. But there isn't anything
519 | // so we have to rely on this brittle solution.
520 | //
521 | // Since this is so brittle, we're keeping the above rule that can reliably and
522 | // non-brittle-ey detect <2.15 as a back up.
523 | log4j216Pattern = "isJndiEnabled"
524 | )
525 |
526 | // log4jPattern is a byte-matching regular expression that checks for two
527 | // conditions in a Java class file:
528 | // 1. Does the YARA rule match?
529 | // 2. Have we found the 2.16 pattern?
530 | var log4jPattern *binaryregexp.Regexp
531 |
532 | func init() {
533 | // Since this means we want to check two patterns in parallel we create all
534 | // 4 combinations of how the patterns may appear, given that they do not
535 | // share a matching prefix or a suffix (which they do not).
536 | //
537 | // The four combinations are:
538 | // 1. [216Pattern]
539 | // 2. [YARARulePattern]
540 | // 3. [216Pattern.*YARARulePattern]
541 | // 4. [YARARulePattern.*216Pattern]
542 | //
543 | // By creating submatches for each of these cases, we can identify which
544 | // patterns are actually present. Also, in order to ensure (1) and (2)
545 | // do not shadow (3) and (4), we need to look for the longest match.
546 | yaraRule := binaryregexp.QuoteMeta(log4jYARARule)
547 | log4jPattern = binaryregexp.MustCompile(
548 | fmt.Sprintf("(?P<216>%s)|(?P%s)|(?P<216First>%s.*%s)|(?P%s.*%s)",
549 | log4j216Pattern,
550 | yaraRule,
551 | log4j216Pattern, yaraRule,
552 | yaraRule, log4j216Pattern,
553 | ),
554 | )
555 | log4jPattern.Longest()
556 | }
557 |
558 | // checkJndiManager checks JndiManager class bytecode for presence of the constructor indicating a
559 | // vulnerable pre-2.15 version or the isJndiEnabled method indicating 2.16+ or 2.12.2.
560 | func (c *checker) checkJndiManager(r io.Reader, buf []byte) error {
561 | c.hasJndiManagerClass = true
562 |
563 | br := newByteReader(r, buf)
564 | matches := log4jPattern.FindReaderSubmatchIndex(br)
565 |
566 | // Error reading.
567 | if err := br.Err(); err != nil && err != io.EOF {
568 | return err
569 | }
570 |
571 | // No match.
572 | if matches == nil {
573 | return nil
574 | }
575 |
576 | // We have a match!
577 | switch {
578 | case matches[2] > 0:
579 | // 1. [216Pattern]
580 | c.hasIsJndiEnabled = true
581 | case matches[4] > 0:
582 | // 2. [YARARulePattern]
583 | c.hasJndiManagerPre215 = true
584 | case matches[6] > 0:
585 | // 3. [216Pattern.*YARARulePattern]
586 | fallthrough
587 | case matches[8] > 0:
588 | // 4. [YARARulePattern.*216Pattern]
589 | c.hasIsJndiEnabled = true
590 | c.hasJndiManagerPre215 = true
591 | }
592 | return nil
593 | }
594 |
595 | // needsJndiLookupCheck returns true if there's something that we could learn by checking
596 | // JndiLookup bytecode with checkJndiLookup.
597 | func (c *checker) needsJndiLookupCheck() bool {
598 | return !c.hasLookupClass || !c.hasInitialContext
599 | }
600 |
601 | // The JndiLookup class in log4j >=2.0-beta9 but <2.1 contains a reference to
602 | // javax.naming.InitialContext that was removed in the 2.1 release.
603 | // Relevant commit: https://github.com/apache/logging-log4j2/commit/cc30d6dd629cbf0529ce898d6c25305b2cff9f0e
604 | var initialContextPattern = binaryregexp.MustCompile(binaryregexp.QuoteMeta(`javax/naming/InitialContext`))
605 |
606 | // checkJndiLookup checks JndiLookup class bytecode for a reference to javax/naming/InitialContext,
607 | // indicating log4j >=2.0-beta9 but <2.1.
608 | func (c *checker) checkJndiLookup(r io.Reader, buf []byte) error {
609 | c.hasLookupClass = true
610 |
611 | br := newByteReader(r, buf)
612 | matches := initialContextPattern.MatchReader(br)
613 |
614 | // Error reading.
615 | if err := br.Err(); err != nil && err != io.EOF {
616 | return err
617 | }
618 |
619 | if matches {
620 | c.hasInitialContext = true
621 | }
622 |
623 | return nil
624 | }
625 |
626 | type byteReader struct {
627 | r io.Reader
628 | buf []byte
629 | off int
630 | err error
631 | }
632 |
633 | func newByteReader(r io.Reader, buf []byte) *byteReader {
634 | return &byteReader{r: r, buf: buf[:0]}
635 | }
636 |
637 | func (b *byteReader) ReadByte() (byte, error) {
638 | for b.off == len(b.buf) {
639 | if b.err != nil {
640 | return 0, b.err
641 | }
642 | n, err := b.r.Read(b.buf[:cap(b.buf)])
643 | b.err = err
644 | b.buf = b.buf[:n]
645 | b.off = 0
646 | }
647 | result := b.buf[b.off]
648 | b.off++
649 | return result, nil
650 | }
651 |
652 | func (b *byteReader) Err() error {
653 | return b.err
654 | }
655 |
--------------------------------------------------------------------------------
/jar/jar_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package jar
16 |
17 | import (
18 | "bytes"
19 | "fmt"
20 | "io"
21 | "path/filepath"
22 | "strings"
23 | "testing"
24 |
25 | "github.com/google/go-cmp/cmp"
26 | "github.com/google/go-cmp/cmp/cmpopts"
27 | )
28 |
29 | var testdataPath = func(p string) string {
30 | return filepath.Join("testdata", p)
31 | }
32 |
33 | func TestParse(t *testing.T) {
34 | testCases := []struct {
35 | filename string
36 | wantBad bool
37 | wantCVEs []cveID
38 | }{
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 | },
79 | // log4j 2.12.2 is not affected by log4shell.
80 | // See: https://logging.apache.org/log4j/2.x/security.html
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 | },
124 | // Archive contains a malformed directory that causes archive/zip to
125 | // return an error.
126 | // See https://go.dev/issues/50390
127 | {
128 | filename: "selenium-api-3.141.59.jar",
129 | wantBad: false,
130 | },
131 | // Test case where it contains a JndiLookupOther.class file that shouldn't be detected as vulnerable
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 | },
201 |
202 | // Ensure robustness to zip bombs from
203 | // https://www.bamsoftware.com/hacks/zipbomb/.
204 | {
205 | filename: "zipbombs/zbsm_in_jar.jar",
206 | wantBad: false,
207 | },
208 | {
209 | filename: "zipbombs/zbsm.jar",
210 | wantBad: false,
211 | },
212 | }
213 | for _, tc := range testCases {
214 | t.Run(tc.filename, func(t *testing.T) {
215 | p := testdataPath(tc.filename)
216 | zr, _, err := OpenReader(p)
217 | if err != nil {
218 | t.Fatalf("zip.OpenReader failed: %v", err)
219 | }
220 | defer zr.Close()
221 | report, err := Parse(&zr.Reader)
222 | if err != nil {
223 | t.Fatalf("Parse() returned an unexpected error, got %v, want nil", err)
224 | }
225 | got := report.Vulnerable
226 | if tc.wantBad != got {
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)
232 | }
233 | })
234 | }
235 | }
236 |
237 | func TestMaxBytes(t *testing.T) {
238 | p := testdataPath("400mb_jar_in_jar.jar")
239 | zr, _, err := OpenReader(p)
240 | if err != nil {
241 | t.Fatalf("zip.OpenReader failed: %v", err)
242 | }
243 | defer zr.Close()
244 |
245 | c := &Parser{MaxBytes: 4 << 20 /* 4MiB */}
246 | if r, err := c.Parse(&zr.Reader); err == nil {
247 | t.Errorf("Parse() = %+v, want error", r)
248 | }
249 | }
250 |
251 | func TestMaxDepth(t *testing.T) {
252 | p := testdataPath("bad_jar_in_jar_in_jar.jar")
253 | zr, _, err := OpenReader(p)
254 | if err != nil {
255 | t.Fatalf("zip.OpenReader failed: %v", err)
256 | }
257 | defer zr.Close()
258 |
259 | c := &Parser{MaxDepth: 1}
260 | if r, err := c.Parse(&zr.Reader); err == nil {
261 | t.Errorf("Parse() = %+v, want error", r)
262 | }
263 | }
264 |
265 | // TestFileError verifies that FileError is invoked with correct paths when an error is encountered
266 | // while processing a JAR file. The test then verifies that scanning continues and successfully
267 | // identifies a vulnerable log4j later in the JAR file. corrupt.jar contains a file that will be
268 | // read by checkJAR (decoy/JndiManager.class) before encountering other vulnerable log4j files. The
269 | // decoy file triggers an error because it has an unsupported compression algorithm.
270 | // corrupt_jar_in_jar.jar is similar, except that it contains corrupt.jar before vuln-class.jar.
271 | func TestFileError(t *testing.T) {
272 | t.Parallel()
273 |
274 | testCases := []struct {
275 | file string
276 | // want is the expected path provided to FileError
277 | want string
278 | }{
279 | {
280 | file: "corrupt.jar",
281 | want: filepath.Join("corrupt.jar", "decoy", "JndiManager.class"),
282 | },
283 | {
284 | file: "corrupt_jar_in_jar.jar",
285 | want: filepath.Join("corrupt_jar_in_jar.jar", "corrupt.jar", "decoy", "JndiManager.class"),
286 | },
287 | }
288 |
289 | for _, tc := range testCases {
290 | tc := tc
291 | t.Run(tc.file, func(t *testing.T) {
292 | t.Parallel()
293 | p := testdataPath(tc.file)
294 | zr, _, err := OpenReader(p)
295 | if err != nil {
296 | t.Fatalf("OpenReader failed: %v", err)
297 | }
298 | defer zr.Close()
299 |
300 | var got []string
301 | pr := &Parser{
302 | Name: tc.file,
303 | FileError: func(path string, err error) error {
304 | t.Logf("FileError(%q, %v)", path, err)
305 | got = append(got, path)
306 | return nil
307 | },
308 | }
309 |
310 | r, err := pr.Parse(&zr.Reader)
311 | if err != nil {
312 | t.Fatalf("Parse() = %+v, want nil error", err)
313 | }
314 | if !r.Vulnerable {
315 | t.Error("Parse() returned not vulnerable, want vulnerable")
316 | }
317 |
318 | if diff := cmp.Diff([]string{tc.want}, got, cmpopts.EquateEmpty()); diff != "" {
319 | t.Errorf("Parse() returned diff (-want +got):\n%s", diff)
320 | }
321 | })
322 | }
323 | }
324 |
325 | // TestInfiniteRecursion ensures that Parse does not get stuck in an
326 | // infinitely recursive zip.
327 | func TestInfiniteRecursion(t *testing.T) {
328 | // Using infinite r.zip from https://research.swtch.com/zip.
329 | p := testdataPath("zipbombs/r.zip")
330 | zr, _, err := OpenReader(p)
331 | if err != nil {
332 | t.Fatalf("zip.OpenReader failed: %v", err)
333 | }
334 | defer zr.Close()
335 | report, err := Parse(&zr.Reader)
336 | if err == nil {
337 | t.Errorf("Parse() failed to return error on infintely recursive zip, got %+v, want error", report)
338 | }
339 | }
340 |
341 | func BenchmarkParse(b *testing.B) {
342 | for _, filename := range [...]string{
343 | "400mb_jar_in_jar.jar",
344 | "safe1.jar",
345 | } {
346 | b.Run(filename, func(b *testing.B) {
347 | p := testdataPath(filename)
348 | zr, _, err := OpenReader(p)
349 | if err != nil {
350 | b.Fatalf("zip.OpenReader failed: %v", err)
351 | }
352 | defer zr.Close()
353 | b.ReportAllocs()
354 | for i := 0; i < b.N; i++ {
355 | _, err := Parse(&zr.Reader)
356 | if err != nil {
357 | b.Errorf("Scan() returned an unexpected error, got %v, want nil", err)
358 | }
359 | }
360 | })
361 | }
362 | }
363 |
364 | func BenchmarkParseParallel(b *testing.B) {
365 | for _, filename := range [...]string{
366 | "400mb_jar_in_jar.jar",
367 | "safe1.jar",
368 | } {
369 | b.Run(filename, func(b *testing.B) {
370 | p := testdataPath(filename)
371 | b.ReportAllocs()
372 | b.RunParallel(func(pb *testing.PB) {
373 | zr, _, err := OpenReader(p)
374 | if err != nil {
375 | b.Fatalf("zip.OpenReader failed: %v", err)
376 | }
377 | defer zr.Close()
378 | for pb.Next() {
379 | _, err := Parse(&zr.Reader)
380 | if err != nil {
381 | b.Errorf("Scan() returned an unexpected error, got %v, want nil", err)
382 | }
383 | }
384 | })
385 | })
386 | }
387 | }
388 |
389 | func TestLog4jPattern(t *testing.T) {
390 | tests := []struct {
391 | input []byte
392 | matchType int
393 | }{
394 | {append([]byte{0x0, 0x1}, []byte("isJndiEnabled")...), 0},
395 | {[]byte{
396 | 0x3c, 0x69, 0x6e, 0x69, 0x74, 0x3e,
397 | 0x01, 0x00, 0x2b,
398 | 0x28, 0x4c, 0x6a, 0x61, 0x76, 0x61, 0x2f, 0x6c,
399 | 0x61, 0x6e, 0x67, 0x2f, 0x53, 0x74, 0x72, 0x69,
400 | 0x6e, 0x67, 0x3b, 0x4c, 0x6a, 0x61, 0x76, 0x61,
401 | 0x78, 0x2f, 0x6e, 0x61, 0x6d, 0x69, 0x6e, 0x67,
402 | 0x2f, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74,
403 | 0x3b, 0x29, 0x56,
404 | }, 1},
405 | {append(make([]byte, 1000), []byte{
406 | 0x3c, 0x69, 0x6e, 0x69, 0x74, 0x3e,
407 | 0x01, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x00,
408 | 0x28, 0x4c, 0x6a, 0x61, 0x76, 0x61, 0x2f, 0x6c,
409 | 0x61, 0x6e, 0x67, 0x2f, 0x53, 0x74, 0x72, 0x69,
410 | 0x6e, 0x67, 0x3b, 0x4c, 0x6a, 0x61, 0x76, 0x61,
411 | 0x78, 0x2f, 0x6e, 0x61, 0x6d, 0x69, 0x6e, 0x67,
412 | 0x2f, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74,
413 | 0x3b, 0x29, 0x56,
414 | }...), -1},
415 | {append([]byte("isJndiEnabled"), []byte{
416 | 0x3c, 0x69, 0x6e, 0x69, 0x74, 0x3e,
417 | 0x01, 0x00, 0x2b,
418 | 0x28, 0x4c, 0x6a, 0x61, 0x76, 0x61, 0x2f, 0x6c,
419 | 0x61, 0x6e, 0x67, 0x2f, 0x53, 0x74, 0x72, 0x69,
420 | 0x6e, 0x67, 0x3b, 0x4c, 0x6a, 0x61, 0x76, 0x61,
421 | 0x78, 0x2f, 0x6e, 0x61, 0x6d, 0x69, 0x6e, 0x67,
422 | 0x2f, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74,
423 | 0x3b, 0x29, 0x56,
424 | // Some random bytes.
425 | 0xff, 0xff, 0xff,
426 | }...), 2},
427 | {append([]byte{
428 | 0x3c, 0x69, 0x6e, 0x69, 0x74, 0x3e,
429 | 0x01, 0x00, 0x2b,
430 | 0x28, 0x4c, 0x6a, 0x61, 0x76, 0x61, 0x2f, 0x6c,
431 | 0x61, 0x6e, 0x67, 0x2f, 0x53, 0x74, 0x72, 0x69,
432 | 0x6e, 0x67, 0x3b, 0x4c, 0x6a, 0x61, 0x76, 0x61,
433 | 0x78, 0x2f, 0x6e, 0x61, 0x6d, 0x69, 0x6e, 0x67,
434 | 0x2f, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74,
435 | 0x3b, 0x29, 0x56,
436 | // Some random bytes.
437 | 0x15, 0x7f, 0xa5,
438 | }, []byte("isJndiEnabled")...), 3},
439 | }
440 | for _, test := range tests {
441 | br := newByteReader(bytes.NewReader(test.input), make([]byte, 16))
442 | matches := log4jPattern.FindReaderSubmatchIndex(br)
443 | if matches == nil && test.matchType >= 0 {
444 | t.Error("expected match")
445 | continue
446 | }
447 | switch test.matchType {
448 | case 0:
449 | if matches[(test.matchType+1)*2] < 0 {
450 | t.Error("expected match of 2.16 only")
451 | }
452 | case 1:
453 | if matches[(test.matchType+1)*2] < 0 {
454 | t.Error("expected match of YARA rule only")
455 | }
456 | case 2:
457 | if matches[(test.matchType+1)*2] < 0 {
458 | t.Error("expected match of 2.16 then YARA rule")
459 | }
460 | case 3:
461 | if matches[(test.matchType+1)*2] < 0 {
462 | t.Error("expected match of YARA rule then then 2.16")
463 | }
464 | default:
465 | if matches != nil {
466 | t.Error("unexpected match")
467 | }
468 | }
469 | }
470 | }
471 |
472 | func TestByteReader(t *testing.T) {
473 | check := func(buf []byte, f func() io.Reader, expect []byte, expectErr error) {
474 | t.Helper()
475 |
476 | br := newByteReader(f(), buf)
477 | i := 0
478 | for {
479 | b, err := br.ReadByte()
480 | if err != nil {
481 | if err != expectErr {
482 | t.Errorf("expected error %v, got %v", expectErr, err)
483 | }
484 | if br.Err() != err {
485 | t.Errorf("Err method result %v didn't match final error %v", br.Err(), err)
486 | }
487 | break
488 | }
489 | if b != expect[i] {
490 | t.Errorf("read unexpected value %d at index %d", b, i)
491 | break
492 | }
493 | i++
494 | }
495 | if i != len(expect) {
496 | t.Errorf("expected to read %d bytes, read %d bytes instead", len(expect), i)
497 | }
498 | }
499 | // Intentionally reuse a buffer to see how it deals with
500 | // a dirty buffer.
501 | buf := make([]byte, 8192)
502 |
503 | small := []byte("hello world")
504 | newSmallReader := func() io.Reader {
505 | return bytes.NewReader(small)
506 | }
507 | check(buf[:5], newSmallReader, small, io.EOF)
508 | check(buf[:1], newSmallReader, small, io.EOF)
509 | check(buf[:103], newSmallReader, small, io.EOF)
510 |
511 | large := bytes.Repeat(small, 1001)
512 | newLargeReader := func() io.Reader {
513 | return bytes.NewReader(large)
514 | }
515 | check(buf[:1], newLargeReader, large, io.EOF)
516 | check(buf[:1041], newLargeReader, large, io.EOF)
517 | check(buf[:], newLargeReader, large, io.EOF)
518 |
519 | const failAfter = 105
520 | bad := fmt.Errorf("this is bad")
521 | newBadReader := func() io.Reader {
522 | return newFaultReader(bytes.NewReader(large), bad, failAfter)
523 | }
524 | check(buf[:4], newBadReader, large[:failAfter], bad)
525 | check(buf[:1], newBadReader, large[:failAfter], bad)
526 | check(buf[:971], newBadReader, large[:failAfter], bad)
527 | }
528 |
529 | type faultReader struct {
530 | io.Reader
531 | fault error
532 | after int
533 |
534 | read int
535 | }
536 |
537 | func newFaultReader(r io.Reader, fault error, after int) *faultReader {
538 | return &faultReader{r, fault, after, 0}
539 | }
540 |
541 | func (f *faultReader) Read(b []byte) (int, error) {
542 | if f.read >= f.after {
543 | return 0, f.fault
544 | }
545 | n, err := f.Reader.Read(b)
546 | f.read += n
547 | if f.read >= f.after {
548 | return f.after - (f.read - n), f.fault
549 | }
550 | return n, err
551 | }
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 | }
567 |
--------------------------------------------------------------------------------
/jar/rewrite.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package jar
16 |
17 | import (
18 | "archive/zip"
19 | "bytes"
20 | "fmt"
21 | "io"
22 | "io/ioutil"
23 | "path"
24 | "strings"
25 | )
26 |
27 | var skipSuffixes = [...]string{
28 | // Skip copying the file over to the new jar so that the new jar is immune.
29 | "JndiLookup.class",
30 | // Remove signing keys from the JAR.
31 | ".RSA",
32 | // Remove any signatures from the JAR.
33 | ".SF",
34 | }
35 |
36 | // RewriteJAR is like Rewrite but accounts for self-executable JARs, copying
37 | // any prefixed data that may be included in the JAR.
38 | func RewriteJAR(dest io.Writer, src io.ReaderAt, size int64) error {
39 | zr, offset, err := NewReader(src, size)
40 | if err != nil {
41 | return err
42 | }
43 |
44 | if offset > 0 {
45 | src := io.NewSectionReader(src, 0, offset)
46 | if _, err := io.CopyN(dest, src, offset); err != nil {
47 | return err
48 | }
49 | }
50 | return Rewrite(dest, zr)
51 | }
52 |
53 | // Rewrite attempts to remove any JndiLookup.class files from a JAR.
54 | //
55 | // Rewrite does not account for self-executable JARs and does not preserve the
56 | // file prefix. This must be explicitly handled, or use RewriteJAR() to do so
57 | // automatically.
58 | //
59 | // zr, offset, err := jar.NewReader(ra, size)
60 | // if err != nil {
61 | // // ...
62 | // }
63 | // dest, err := os.CreateTemp("", "")
64 | // if err != nil {
65 | // // ...
66 | // }
67 | // defer dest.Close()
68 | //
69 | // if offset > 0 {
70 | // // Rewrite prefix.
71 | // src := io.NewSectionReader(ra, 0, offset)
72 | // if _, err := io.CopyN(dest, src, offset); err != nil {
73 | // // ...
74 | // }
75 | // }
76 | // if err := jar.Rewrite(dest, zr); err != nil {
77 | // // ...
78 | // }
79 | //
80 | func Rewrite(w io.Writer, zr *zip.Reader) error {
81 | zw := zip.NewWriter(w)
82 | for _, zipItem := range zr.File {
83 | skip := false
84 | for _, suffix := range skipSuffixes {
85 | if strings.HasSuffix(zipItem.Name, suffix) {
86 | skip = true
87 | break
88 | }
89 | }
90 | if skip {
91 | continue
92 | }
93 |
94 | if exts[path.Ext(zipItem.Name)] {
95 | // Nested jar! Recur on it to ensure that nested jars are immune
96 | nestedReader, err := zipItem.Open()
97 | if err != nil {
98 | return fmt.Errorf("failed to open nested zip %q for auto-mitigation: %v; skipping", zipItem.Name, err)
99 | }
100 | b, err := ioutil.ReadAll(nestedReader)
101 | if err != nil {
102 | return fmt.Errorf("failed to read nested zip %q for auto-mitigation: %v; skipping", zipItem.Name, err)
103 | }
104 | nestedReaderAt := bytes.NewReader(b)
105 | nestedZipReader, err := zip.NewReader(nestedReaderAt, int64(len(b)))
106 | if err != nil {
107 | if err == zip.ErrFormat {
108 | // Not a zip file.
109 | goto copyFile
110 | }
111 | return fmt.Errorf("failed to create nested zip %q reader for auto-mitigation: %v; skipping", zipItem.Name, err)
112 | }
113 | writer, err := zw.CreateHeader(&zipItem.FileHeader)
114 | if err != nil {
115 | return fmt.Errorf("failed to create nested zip %q item for auto-mitigation: %v", zipItem.Name, err)
116 | }
117 | if err := Rewrite(writer, nestedZipReader); err != nil {
118 | return fmt.Errorf("rewriting nested zip %s: %v", zipItem.Name, err)
119 | }
120 | continue
121 | }
122 |
123 | copyFile:
124 | if zipItem.Mode().IsDir() {
125 | // Copy() only works on files, so manually create the directory entry
126 | dirHeader := zipItem.FileHeader
127 | // Reset the Extra field which holds the OS-specific metadata that encodes the last
128 | // modified time. This is technically incorrect because it means the mitigated
129 | // zips that we create will have the last modified timestamp updated. But, if we don't
130 | // do this we create invalid zips because `zw.CreateHeader` assumes that `Extra` is empty
131 | // and always appends the modified time to the end of `Extra`. We don't use `zw.CreateRaw`
132 | // because we want the rest of the logic that `zw.CreateHeader` provides.
133 | dirHeader.Extra = make([]byte, 0)
134 | if _, err := zw.CreateHeader(&dirHeader); err != nil {
135 | return fmt.Errorf("failed to copy zip directory %s: %v", zipItem.Name, err)
136 | }
137 | } else {
138 | if err := zw.Copy(zipItem); err != nil {
139 | return fmt.Errorf("failed to copy zip file %s: %v", zipItem.Name, err)
140 | }
141 | }
142 | }
143 | if err := zw.Close(); err != nil {
144 | return fmt.Errorf("finalize writer: %v", err)
145 | }
146 | return nil
147 | }
148 |
--------------------------------------------------------------------------------
/jar/rewrite_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package jar
16 |
17 | import (
18 | "archive/zip"
19 | "bytes"
20 | "fmt"
21 | "io"
22 | "os"
23 | "os/exec"
24 | "path"
25 | "path/filepath"
26 | "regexp"
27 |
28 | "testing"
29 |
30 | "github.com/google/go-cmp/cmp"
31 | )
32 |
33 | func cpFile(t *testing.T, dest, src string) {
34 | t.Helper()
35 | dir := filepath.Dir(dest)
36 | if _, err := os.Stat(dir); err != nil {
37 | if err := os.MkdirAll(dir, 0755); err != nil {
38 | t.Fatalf("creating destination directory: %v", err)
39 | }
40 | }
41 |
42 | r, err := os.Open(src)
43 | if err != nil {
44 | t.Fatalf("open file %s: %v", src, err)
45 | }
46 | defer r.Close()
47 |
48 | ri, err := r.Stat()
49 | if err != nil {
50 | t.Fatalf("stat file %s: %v", src, err)
51 | }
52 | w, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, ri.Mode())
53 | if err != nil {
54 | t.Fatalf("open destination file %s: %v", src, err)
55 | }
56 | defer w.Close()
57 | if _, err := io.Copy(w, r); err != nil {
58 | t.Fatalf("copying file contents: %v", err)
59 | }
60 | }
61 |
62 | func autoMitigateJAR(path string) error {
63 | r, err := os.Open(path)
64 | if err != nil {
65 | return fmt.Errorf("open flie: %v", err)
66 | }
67 | defer r.Close()
68 | info, err := r.Stat()
69 | if err != nil {
70 | return fmt.Errorf("stat file: %v", err)
71 | }
72 |
73 | f, err := os.CreateTemp("", "")
74 | if err != nil {
75 | return fmt.Errorf("create temp: %v", err)
76 | }
77 | defer f.Close()
78 | if err := RewriteJAR(f, r, info.Size()); err != nil {
79 | return fmt.Errorf("rewriting zip: %v", err)
80 | }
81 |
82 | // Files must be closed before rename works on Windows.
83 | r.Close()
84 | f.Close()
85 |
86 | if err := os.Rename(f.Name(), path); err != nil {
87 | return fmt.Errorf("renaming file: %v", err)
88 | }
89 | return nil
90 | }
91 |
92 | func checkJARs(t *testing.T, expectRemoved func(name string) bool, before, after *zip.Reader) {
93 | var i, j int
94 | for i < len(before.File) && j < len(after.File) {
95 | beforeFile := before.File[i]
96 | afterFile := after.File[j]
97 | if expectRemoved(path.Base(beforeFile.Name)) {
98 | // Skip files that are meant to be removed in before.
99 | i++
100 | continue
101 | }
102 | i++
103 | j++
104 | if expectRemoved(path.Base(afterFile.Name)) {
105 | // ensure they were removed in after.
106 | t.Errorf("found class that was meant to be removed at %q", afterFile.Name)
107 | }
108 | if beforeFile.Name != afterFile.Name {
109 | t.Fatalf("found unexpected differing filenames %q %q", beforeFile.Name, afterFile.Name)
110 | }
111 | name := beforeFile.Name
112 | if beforeFile.Mode().IsDir() != afterFile.Mode().IsDir() {
113 | t.Fatalf("filemode for %s did not match, got=%v, want=%v", name, afterFile.Mode(), beforeFile.Mode())
114 | }
115 |
116 | if beforeFile.Mode().IsDir() {
117 | // Don't attempt to read a directory.
118 | continue
119 | }
120 |
121 | bf, err := beforeFile.Open()
122 | if err != nil {
123 | t.Fatalf("failed to open before file: %v", err)
124 | }
125 | bb, err := io.ReadAll(bf)
126 | if err != nil {
127 | t.Fatalf("failed to read all before file: %v", err)
128 | }
129 | bf.Close()
130 | af, err := afterFile.Open()
131 | if err != nil {
132 | t.Fatalf("failed to open after file: %v", err)
133 | }
134 | ab, err := io.ReadAll(af)
135 | if err != nil {
136 | t.Fatalf("failed to read all after file: %v", err)
137 | }
138 | af.Close()
139 |
140 | // If we find zip files make sure we open them up.
141 | if exts[path.Ext(name)] {
142 | var bFailed, aFailed bool
143 | bz, err := zip.NewReader(bytes.NewReader(bb), int64(len(bb)))
144 | if err != nil {
145 | bFailed = true
146 | }
147 | az, err := zip.NewReader(bytes.NewReader(ab), int64(len(ab)))
148 | if err != nil {
149 | aFailed = true
150 | }
151 | if !aFailed && !bFailed {
152 | checkJARs(t, expectRemoved, bz, az)
153 | continue
154 | } else if aFailed && bFailed {
155 | // might not be a valid zip, so carry on
156 | } else {
157 | t.Fatalf("between before and after zip file %q one succeeds but the other fails", name)
158 | }
159 | }
160 | // Finally just compare the files to make sure they match.
161 | if diff := cmp.Diff(beforeFile.FileHeader, afterFile.FileHeader); diff != "" {
162 | t.Fatalf("headers for %q don't match (-before, +after): %s", name, diff)
163 | }
164 | if !bytes.Equal(bb, ab) {
165 | t.Errorf("contents %q for files don't match", name)
166 | }
167 | }
168 |
169 | if i != len(before.File) {
170 | t.Error("files left over in before zip")
171 | }
172 | if j != len(after.File) {
173 | t.Error("files left over in after zip")
174 | }
175 | }
176 |
177 | func TestAutoMitigateJAR(t *testing.T) {
178 | for _, tc := range []string{
179 | "arara.jar",
180 | "bad_jar_in_jar.jar",
181 | "bad_jar_in_jar_in_jar.jar",
182 | "bad_jar_with_invalid_jar.jar",
183 | "vuln-class.jar",
184 | "vuln-class-executable",
185 | } {
186 | tc := tc
187 | t.Run(tc, func(t *testing.T) {
188 | t.Parallel()
189 | src := testdataPath(tc)
190 | dest := filepath.Join(t.TempDir(), tc)
191 |
192 | cpFile(t, dest, src)
193 |
194 | if err := autoMitigateJAR(dest); err != nil {
195 | t.Fatalf("autoMitigateJar(%s) failed: %v", dest, err)
196 | }
197 |
198 | before, _, err := OpenReader(src)
199 | if err != nil {
200 | t.Fatalf("zip.OpenReader(%q) failed: %v", src, err)
201 | }
202 | defer before.Close()
203 | after, _, err := OpenReader(dest)
204 | if err != nil {
205 | t.Fatalf("zip.OpenReader(%q) failed: %v", dest, err)
206 | }
207 | defer after.Close()
208 | checkJARs(t, func(name string) bool {
209 | return path.Base(name) == "JndiLookup.class"
210 | }, &before.Reader, &after.Reader)
211 | })
212 | }
213 | }
214 |
215 | func TestAutoMitigateExecutable(t *testing.T) {
216 | for _, tc := range []string{
217 | "helloworld-executable",
218 | "vuln-class-executable",
219 | } {
220 | tc := tc
221 | t.Run(tc, func(t *testing.T) {
222 | t.Parallel()
223 | src := testdataPath(tc)
224 | dest := filepath.Join(t.TempDir(), tc)
225 |
226 | cpFile(t, dest, src)
227 |
228 | if err := autoMitigateJAR(dest); err != nil {
229 | t.Fatalf("autoMitigateJar(%s) failed: %v", dest, err)
230 | }
231 |
232 | sf, err := os.Open(src)
233 | if err != nil {
234 | t.Fatalf("open file %s: %v", src, err)
235 | }
236 | defer sf.Close()
237 | info, err := sf.Stat()
238 | if err != nil {
239 | t.Fatalf("stat file %s: %v", src, err)
240 | }
241 |
242 | _, offset, err := NewReader(sf, info.Size())
243 | if err != nil {
244 | t.Fatalf("new jar reader %s: %v", src, err)
245 | }
246 | if offset <= 0 {
247 | t.Errorf("expected offset for executable %s: got=%d", src, offset)
248 | }
249 |
250 | df, err := os.Open(dest)
251 | if err != nil {
252 | t.Fatalf("open file %s: %v", dest, err)
253 | }
254 | defer df.Close()
255 |
256 | got := make([]byte, offset)
257 | want := make([]byte, offset)
258 | if _, err := io.ReadFull(sf, want); err != nil {
259 | t.Fatalf("reading prefix from file %s: %v", src, err)
260 | }
261 | if _, err := io.ReadFull(df, got); err != nil {
262 | t.Fatalf("reading prefix from file %s: %v", dest, err)
263 | }
264 | if !bytes.Equal(got, want) {
265 | t.Errorf("prefix did not match after rewrite, got=%q, want=%q", got, want)
266 | }
267 | })
268 | }
269 | }
270 | func TestAutoMitigate(t *testing.T) {
271 | for _, tc := range []string{
272 | "arara.jar",
273 | "bad_jar_in_jar.jar",
274 | "bad_jar_in_jar_in_jar.jar",
275 | "bad_jar_with_invalid_jar.jar",
276 | "vuln-class.jar",
277 | "vuln-class-executable",
278 | } {
279 | tc := tc
280 | t.Run(tc, func(t *testing.T) {
281 | t.Parallel()
282 | src := testdataPath(tc)
283 | dest := filepath.Join(t.TempDir(), tc)
284 |
285 | cpFile(t, dest, src)
286 |
287 | if err := autoMitigateJAR(dest); err != nil {
288 | t.Fatalf("autoMitigateJar(%s) failed: %v", dest, err)
289 | }
290 |
291 | before, _, err := OpenReader(src)
292 | if err != nil {
293 | t.Fatalf("zip.OpenReader(%q) failed: %v", src, err)
294 | }
295 | defer before.Close()
296 | after, _, err := OpenReader(dest)
297 | if err != nil {
298 | t.Fatalf("zip.OpenReader(%q) failed: %v", dest, err)
299 | }
300 | defer after.Close()
301 | checkJARs(t, func(name string) bool {
302 | return path.Base(name) == "JndiLookup.class"
303 | }, &before.Reader, &after.Reader)
304 | })
305 | }
306 | }
307 |
308 | func TestAutoMitigateSignedJAR(t *testing.T) {
309 | testCases := []string{
310 | "arara.signed.jar",
311 | "safe1.signed.jar",
312 | "helloworld.signed.jar",
313 | }
314 | for _, name := range testCases {
315 | t.Run(name, func(t *testing.T) {
316 | src := testdataPath(name)
317 | dest := filepath.Join(t.TempDir(), name)
318 |
319 | cpFile(t, dest, src)
320 |
321 | if err := autoMitigateJAR(dest); err != nil {
322 | t.Fatalf("autoMitigateJar(%s) failed: %v", dest, err)
323 | }
324 |
325 | before, _, err := OpenReader(src)
326 | if err != nil {
327 | t.Fatalf("zip.OpenReader(%q) failed: %v", src, err)
328 | }
329 | defer before.Close()
330 | after, _, err := OpenReader(dest)
331 | if err != nil {
332 | t.Fatalf("zip.OpenReader(%q) failed: %v", dest, err)
333 | }
334 | defer after.Close()
335 | checkJARs(t, func(name string) bool {
336 | return name == "JndiLookup.class" ||
337 | name == "SERVER.SF" ||
338 | name == "SERVER.RSA"
339 | }, &before.Reader, &after.Reader)
340 | })
341 | }
342 | }
343 |
344 | func TestAutoMitigatedJarsAreCorrectlyFormed(t *testing.T) {
345 | if _, err := exec.LookPath("zipinfo"); err != nil {
346 | t.Skip("zipinfo not available, skipping test")
347 | }
348 |
349 | testCases := []string{
350 | "arara.jar",
351 | "shadow-6.1.0.jar",
352 | "arara.signed.jar",
353 | "bad_jar_in_jar_in_jar.jar",
354 | "bad_jar_in_jar.jar",
355 | "good_jar_in_jar_in_jar.jar",
356 | "good_jar_in_jar.jar",
357 | "helloworld.jar",
358 | "helloworld.signed.jar",
359 | "log4j-core-2.12.1.jar",
360 | "log4j-core-2.14.0.jar",
361 | "log4j-core-2.15.0.jar",
362 | "log4j-core-2.16.0.jar",
363 | "log4j-core-2.1.jar",
364 | "safe1.jar",
365 | "safe1.signed.jar",
366 | "emptydir.zip",
367 | "emptydirs.zip",
368 | }
369 | for _, name := range testCases {
370 | t.Run(name, func(t *testing.T) {
371 | // Set up
372 | src := testdataPath(name)
373 | dest := filepath.Join(t.TempDir(), name)
374 |
375 | cpFile(t, dest, src)
376 |
377 | // Mitigate
378 | if err := autoMitigateJAR(dest); err != nil {
379 | t.Fatalf("autoMitigateJar(%s) failed: %v", dest, err)
380 | }
381 |
382 | // Check that the jars were actually mitigated
383 | before, err := zip.OpenReader(src)
384 | if err != nil {
385 | t.Fatalf("zip.OpenReader(%q) failed: %v", src, err)
386 | }
387 | defer before.Close()
388 | after, err := zip.OpenReader(dest)
389 | if err != nil {
390 | t.Fatalf("zip.OpenReader(%q) failed: %v", dest, err)
391 | }
392 | defer after.Close()
393 | checkJARs(t, func(name string) bool {
394 | return name == "JndiLookup.class" ||
395 | name == "SERVER.SF" ||
396 | name == "SERVER.RSA"
397 | }, &before.Reader, &after.Reader)
398 |
399 | // Check that they are well formed
400 | out, err := exec.Command("zipinfo", "-v", dest).Output()
401 | if err != nil {
402 | t.Fatalf("zipinfo command failed for dest %s: %v", dest, err)
403 | }
404 | match, err := regexp.MatchString(`There are an extra -\d+ bytes preceding this file`, string(out))
405 | if err != nil {
406 | t.Fatalf("regex failed: %v", err)
407 | }
408 | if match {
409 | t.Fatalf("mitigated jar %s is malformed:\n%v", dest, string(out))
410 | }
411 | })
412 | }
413 | }
414 |
--------------------------------------------------------------------------------
/jar/testdata/400mb.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/400mb.jar
--------------------------------------------------------------------------------
/jar/testdata/400mb_jar_in_jar.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/400mb_jar_in_jar.jar
--------------------------------------------------------------------------------
/jar/testdata/arara.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/arara.jar
--------------------------------------------------------------------------------
/jar/testdata/arara.jar.patched:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/arara.jar.patched
--------------------------------------------------------------------------------
/jar/testdata/arara.signed.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/arara.signed.jar
--------------------------------------------------------------------------------
/jar/testdata/arara.signed.jar.patched:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/arara.signed.jar.patched
--------------------------------------------------------------------------------
/jar/testdata/bad_jar_in_jar.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/bad_jar_in_jar.jar
--------------------------------------------------------------------------------
/jar/testdata/bad_jar_in_jar.jar.patched:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/bad_jar_in_jar.jar.patched
--------------------------------------------------------------------------------
/jar/testdata/bad_jar_in_jar_in_jar.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/bad_jar_in_jar_in_jar.jar
--------------------------------------------------------------------------------
/jar/testdata/bad_jar_in_jar_in_jar.jar.patched:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/bad_jar_in_jar_in_jar.jar.patched
--------------------------------------------------------------------------------
/jar/testdata/bad_jar_with_invalid_jar.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/bad_jar_with_invalid_jar.jar
--------------------------------------------------------------------------------
/jar/testdata/bad_jar_with_invalid_jar.jar.patched:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/bad_jar_with_invalid_jar.jar.patched
--------------------------------------------------------------------------------
/jar/testdata/corrupt.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/corrupt.jar
--------------------------------------------------------------------------------
/jar/testdata/corrupt_jar_in_jar.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/corrupt_jar_in_jar.jar
--------------------------------------------------------------------------------
/jar/testdata/corruptjar.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "log"
8 | "os"
9 |
10 | "rsc.io/binaryregexp"
11 | )
12 |
13 | const (
14 | // Offset of compression field in LFH record.
15 | // See: https://users.cs.jmu.edu/buchhofp/forensics/formats/pkzip.html
16 | lfhCompOffset = 0x8
17 | // Offset of compression field in CDH record.
18 | cdhCompOffset = 0xa
19 | // Reserved compression scheme.
20 | compReserved = 0xf
21 | )
22 |
23 | func main() {
24 | if len(os.Args) != 2 {
25 | fmt.Fprint(os.Stderr, "usage: go run corruptjar.go CLASS_TO_CORRUPT\n")
26 | os.Exit(1)
27 | }
28 |
29 | path := os.Args[1]
30 |
31 | b, err := io.ReadAll(os.Stdin)
32 | if err != nil {
33 | log.Fatalf("ReadAll(Stdin) = %v", err)
34 | }
35 |
36 | lfh := binaryregexp.MustCompile(
37 | binaryregexp.QuoteMeta("PK\x03\x04") +
38 | `[\x00-\xff]{26}` +
39 | binaryregexp.QuoteMeta(path))
40 |
41 | m := lfh.FindIndex(b)
42 | if len(m) == 0 {
43 | log.Fatalf("Could not find %s Local File Header", path)
44 | }
45 |
46 | b[m[0]+lfhCompOffset] = compReserved
47 |
48 | cdh := binaryregexp.MustCompile(
49 | binaryregexp.QuoteMeta("PK\x01\x02") +
50 | `[\x00-\xff]{42}` +
51 | binaryregexp.QuoteMeta(path))
52 |
53 | m = cdh.FindIndex(b)
54 | if len(m) == 0 {
55 | log.Fatalf("Could not find %s Central Directory Header", path)
56 | }
57 |
58 | b[m[0]+cdhCompOffset] = compReserved
59 |
60 | if n, err := io.Copy(os.Stdout, bytes.NewBuffer(b)); err != nil || n != int64(len(b)) {
61 | log.Fatalf("Copy(Stdout, b) = %d, %v", n, err)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/jar/testdata/emptydir.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/emptydir.zip
--------------------------------------------------------------------------------
/jar/testdata/emptydirs.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/emptydirs.zip
--------------------------------------------------------------------------------
/jar/testdata/generate.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -e
2 |
3 | # Copyright 2021 Google LLC
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # https://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | echo '#!/bin/bash
18 | file_path=`realpath $0`
19 | exec java -jar $file_path "$@"
20 | ' > helloworld-executable
21 | cat helloworld.jar >> helloworld-executable
22 | chmod +x helloworld-executable
23 |
24 | echo '#!/bin/bash
25 | file_path=`realpath $0`
26 | exec java -jar $file_path "$@"
27 | ' > vuln-class-executable
28 | cat vuln-class.jar >> vuln-class-executable
29 | chmod +x vuln-class-executable
30 |
31 | mkdir -p tmp
32 | dd if=/dev/zero of=tmp/400mb bs=1M count=400
33 | zip 400mb.jar tmp/400mb
34 | rm -rf tmp
35 |
36 | zip 400mb_jar_in_jar.jar 400mb.jar
37 |
38 | # Create a JAR file that is corrupted so that it will trigger an error when
39 | # being processed by checkJAR before it encounters vulnerable log4j files. We
40 | # do so by inserting a JndiManager.class file with an unsupported compression
41 | # algorithm.
42 | mkdir -p corrupt/{decoy,vuln}
43 | touch corrupt/decoy/JndiManager.class
44 | unzip vuln-class.jar -d corrupt/vuln
45 | (cd corrupt; jar cf corrupt-orig.jar decoy vuln)
46 | go run corruptjar.go decoy/JndiManager.class corrupt.jar
47 | rm -rf corrupt
48 | jar cf corrupt_jar_in_jar.jar corrupt.jar vuln-class.jar
49 |
--------------------------------------------------------------------------------
/jar/testdata/good_jar_in_jar.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/good_jar_in_jar.jar
--------------------------------------------------------------------------------
/jar/testdata/good_jar_in_jar_in_jar.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/good_jar_in_jar_in_jar.jar
--------------------------------------------------------------------------------
/jar/testdata/good_jar_with_invalid_jar.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/good_jar_with_invalid_jar.jar
--------------------------------------------------------------------------------
/jar/testdata/helloworld-executable:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/helloworld-executable
--------------------------------------------------------------------------------
/jar/testdata/helloworld.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/helloworld.jar
--------------------------------------------------------------------------------
/jar/testdata/helloworld.signed.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/helloworld.signed.jar
--------------------------------------------------------------------------------
/jar/testdata/log4j-core-2.0-beta9.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/log4j-core-2.0-beta9.jar
--------------------------------------------------------------------------------
/jar/testdata/log4j-core-2.1.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/log4j-core-2.1.jar
--------------------------------------------------------------------------------
/jar/testdata/log4j-core-2.1.jar.patched:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/log4j-core-2.1.jar.patched
--------------------------------------------------------------------------------
/jar/testdata/log4j-core-2.12.1.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/log4j-core-2.12.1.jar
--------------------------------------------------------------------------------
/jar/testdata/log4j-core-2.12.1.jar.patched:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/log4j-core-2.12.1.jar.patched
--------------------------------------------------------------------------------
/jar/testdata/log4j-core-2.12.2.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/log4j-core-2.12.2.jar
--------------------------------------------------------------------------------
/jar/testdata/log4j-core-2.14.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/log4j-core-2.14.0.jar
--------------------------------------------------------------------------------
/jar/testdata/log4j-core-2.14.0.jar.patched:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/log4j-core-2.14.0.jar.patched
--------------------------------------------------------------------------------
/jar/testdata/log4j-core-2.15.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/log4j-core-2.15.0.jar
--------------------------------------------------------------------------------
/jar/testdata/log4j-core-2.15.0.jar.patched:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/log4j-core-2.15.0.jar.patched
--------------------------------------------------------------------------------
/jar/testdata/log4j-core-2.16.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/log4j-core-2.16.0.jar
--------------------------------------------------------------------------------
/jar/testdata/notarealjar.jar:
--------------------------------------------------------------------------------
1 | Ha! I tricked you! I'm not actually a real jar!
2 |
--------------------------------------------------------------------------------
/jar/testdata/safe1.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/safe1.jar
--------------------------------------------------------------------------------
/jar/testdata/safe1.signed.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/safe1.signed.jar
--------------------------------------------------------------------------------
/jar/testdata/selenium-api-3.141.59.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/selenium-api-3.141.59.jar
--------------------------------------------------------------------------------
/jar/testdata/shadow-6.1.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/shadow-6.1.0.jar
--------------------------------------------------------------------------------
/jar/testdata/similarbutnotvuln.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/similarbutnotvuln.jar
--------------------------------------------------------------------------------
/jar/testdata/vuln-class-executable:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/vuln-class-executable
--------------------------------------------------------------------------------
/jar/testdata/vuln-class.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/vuln-class.jar
--------------------------------------------------------------------------------
/jar/testdata/vuln-class.jar.patched:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/vuln-class.jar.patched
--------------------------------------------------------------------------------
/jar/testdata/zipbombs/r.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/zipbombs/r.zip
--------------------------------------------------------------------------------
/jar/testdata/zipbombs/zbsm.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/zipbombs/zbsm.jar
--------------------------------------------------------------------------------
/jar/testdata/zipbombs/zbsm_in_jar.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/log4jscanner/3648656830d97e15896a74bad20aa346f6c79b63/jar/testdata/zipbombs/zbsm_in_jar.jar
--------------------------------------------------------------------------------
/jar/walker.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package jar
16 |
17 | import (
18 | "archive/zip"
19 | "fmt"
20 | "io"
21 | "io/fs"
22 | "os"
23 | "path"
24 | "path/filepath"
25 | "strings"
26 | )
27 |
28 | // IsJAR determines if a given ZIP reader is a JAR.
29 | func IsJAR(zr *zip.Reader) bool {
30 | // Optimization: Scan file header for the JAR-specific dir META-INF, bail
31 | // if it's not present (it must not be a jar).
32 | // In practice, JARs seem to have their META-INF directory at the beginning
33 | // of the central directory structure.
34 | // Jar files missing that directory still get loaded, so we also check for
35 | // class files and nested jars.
36 | for _, fh := range zr.File {
37 | isDir := fh.FileInfo().IsDir()
38 | if (isDir && strings.HasPrefix(fh.Name, "META-INF")) ||
39 | (isDir && strings.HasPrefix(fh.Name, "WEB-INF")) ||
40 | (!isDir && strings.HasSuffix(fh.Name, ".class")) ||
41 | (!isDir && strings.HasSuffix(fh.Name, ".jar")) {
42 | return true
43 | }
44 | }
45 | return false
46 | }
47 |
48 | // Walker implements a filesystem walker to scan for log4j vulnerable JARs
49 | // and optional rewrite them.
50 | type Walker struct {
51 | // Rewrite indicates if the Walker should rewrite JARs in place as it
52 | // iterates through the filesystem.
53 | Rewrite bool
54 | // SkipDir, if provided, allows the walker to skip certain directories
55 | // as it scans.
56 | SkipDir func(path string, de fs.DirEntry) bool
57 | // HandleError can be used to handle errors for a given directory or
58 | // JAR file.
59 | HandleError func(path string, err error)
60 | // HandleReport is called when a JAR is determined vulnerable. If Rewrite
61 | // is provided, this is called before the Rewrite occurs.
62 | HandleReport func(path string, r *Report)
63 | // HandleRewrite is called when a JAR is rewritten successfully.
64 | HandleRewrite func(path string, r *Report)
65 | // Parser will be used when checking JARs, if provided. If
66 | // unset, a Parser with sensible defaults will be created.
67 | Parser *Parser
68 | }
69 |
70 | // Walk attempts to scan a directory for vulnerable JARs.
71 | func (w *Walker) Walk(dir string) error {
72 | p := w.Parser
73 | if p == nil {
74 | p = &Parser{}
75 | }
76 |
77 | fsys := os.DirFS(dir)
78 | wk := walker{w, fsys, dir, p}
79 |
80 | return fs.WalkDir(fsys, ".", func(p string, d fs.DirEntry, err error) error {
81 | if err != nil {
82 | wk.handleError(p, err)
83 | return nil
84 | }
85 | if wk.skipDir(p, d) {
86 | return fs.SkipDir
87 | }
88 | if err := wk.visit(p, d); err != nil {
89 | wk.handleError(p, err)
90 | }
91 | return nil
92 | })
93 | }
94 |
95 | type walker struct {
96 | *Walker
97 | fs fs.FS
98 | dir string
99 | // p is the Parser to use for this walk. p is guaranteed to be non-nil.
100 | p *Parser
101 | }
102 |
103 | func (w *walker) filepath(path string) string {
104 | return filepath.Join(w.dir, path)
105 | }
106 |
107 | func (w *walker) handleError(path string, err error) {
108 | if w.HandleError == nil {
109 | return
110 | }
111 | w.HandleError(w.filepath(path), err)
112 | }
113 |
114 | func (w *walker) handleReport(path string, r *Report) {
115 | if w.HandleReport == nil {
116 | return
117 | }
118 | w.HandleReport(w.filepath(path), r)
119 | }
120 |
121 | func (w *walker) handleRewrite(path string, r *Report) {
122 | if w.HandleRewrite == nil {
123 | return
124 | }
125 | w.HandleRewrite(w.filepath(path), r)
126 | }
127 |
128 | func (w *walker) skipDir(path string, d fs.DirEntry) bool {
129 | if w.SkipDir == nil {
130 | return false
131 | }
132 | return w.SkipDir(w.filepath(path), d)
133 | }
134 |
135 | func (w *walker) visit(p string, d fs.DirEntry) error {
136 | if d.IsDir() || !d.Type().IsRegular() {
137 | return nil
138 | }
139 | if !exts[path.Ext(p)] {
140 | return nil
141 | }
142 | f, err := w.fs.Open(p)
143 | if err != nil {
144 | return fmt.Errorf("open: %v", err)
145 | }
146 | defer f.Close()
147 |
148 | info, err := f.Stat()
149 | if err != nil {
150 | return fmt.Errorf("stat: %v", err)
151 | }
152 | ra, ok := f.(io.ReaderAt)
153 | if !ok {
154 | return fmt.Errorf("file doesn't implement reader at: %T", f)
155 | }
156 | zr, _, err := NewReader(ra, info.Size())
157 | if err != nil {
158 | if err == zip.ErrFormat {
159 | // Not a JAR.
160 | return nil
161 | }
162 | return fmt.Errorf("opening file as a ZIP archive: %v", err)
163 | }
164 | if !IsJAR(zr) {
165 | return nil
166 | }
167 | r, err := w.p.Parse(zr)
168 | if err != nil {
169 | return fmt.Errorf("scanning jar: %v", err)
170 | }
171 |
172 | if !r.Vulnerable {
173 | return nil
174 | }
175 | w.handleReport(p, r)
176 |
177 | if !w.Rewrite {
178 | return nil
179 | }
180 |
181 | dest := w.filepath(p)
182 | // Ensure temp file is created in the same directory as the file we want to
183 | // rewrite to improve the chances of ending up on the same filesystem. On
184 | // Linux, os.Rename() doesn't work across filesystems.
185 | //
186 | // https://github.com/google/log4jscanner/issues/18
187 | tf, err := os.CreateTemp(filepath.Dir(dest), ".log4jscanner")
188 | if err != nil {
189 | return fmt.Errorf("creating temp file: %v", err)
190 | }
191 | defer os.Remove(tf.Name()) // Attempt to clean up temp file no matter what.
192 | defer tf.Close()
193 |
194 | if err := RewriteJAR(tf, ra, info.Size()); err != nil {
195 | return fmt.Errorf("failed to rewrite %s: %v", p, err)
196 | }
197 |
198 | // Files must be closed for rewrite to work on Windows.
199 | f.Close()
200 | tf.Close()
201 | if err := os.Chmod(tf.Name(), info.Mode()); err != nil {
202 | return fmt.Errorf("chmod file: %v", err)
203 | }
204 |
205 | uid, gid, ok, err := fileOwner(info)
206 | if err != nil {
207 | return fmt.Errorf("determining file owner: %v", err)
208 | }
209 | if ok {
210 | if err := os.Chown(tf.Name(), int(uid), int(gid)); err != nil {
211 | return fmt.Errorf("changing ownership of temporary file: %v", err)
212 | }
213 | }
214 | if err := os.Rename(tf.Name(), dest); err != nil {
215 | return fmt.Errorf("overwriting %s: %v", p, err)
216 | }
217 | w.handleRewrite(p, r)
218 | return nil
219 | }
220 |
--------------------------------------------------------------------------------
/jar/walker_other.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | //go:build !(linux || darwin)
16 |
17 | package jar
18 |
19 | import "io/fs"
20 |
21 | func fileOwner(fi fs.FileInfo) (uid, gid uint32, ok bool, err error) {
22 | return 0, 0, false, nil
23 | }
24 |
--------------------------------------------------------------------------------
/jar/walker_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package jar
16 |
17 | import (
18 | "path/filepath"
19 | "strings"
20 | "testing"
21 |
22 | "github.com/google/go-cmp/cmp"
23 | )
24 |
25 | func TestWalker(t *testing.T) {
26 | tempDir := t.TempDir()
27 | files := []string{
28 | "arara.jar",
29 | "arara.jar.patched",
30 | "arara.signed.jar",
31 | "arara.signed.jar.patched",
32 | "bad_jar_in_jar_in_jar.jar",
33 | "bad_jar_in_jar_in_jar.jar.patched",
34 | "bad_jar_in_jar.jar",
35 | "bad_jar_in_jar.jar.patched",
36 | "bad_jar_with_invalid_jar.jar",
37 | "bad_jar_with_invalid_jar.jar.patched",
38 | "good_jar_in_jar_in_jar.jar",
39 | "good_jar_in_jar.jar",
40 | "good_jar_with_invalid_jar.jar",
41 | "helloworld.jar",
42 | "helloworld.signed.jar",
43 | "log4j-core-2.12.1.jar",
44 | "log4j-core-2.12.1.jar.patched",
45 | "log4j-core-2.14.0.jar",
46 | "log4j-core-2.14.0.jar.patched",
47 | "log4j-core-2.15.0.jar",
48 | "log4j-core-2.15.0.jar.patched",
49 | "log4j-core-2.16.0.jar",
50 | "log4j-core-2.1.jar",
51 | "log4j-core-2.1.jar.patched",
52 | "notarealjar.jar",
53 | "safe1.jar",
54 | "safe1.signed.jar",
55 | "similarbutnotvuln.jar",
56 | "vuln-class.jar",
57 | "vuln-class.jar.patched",
58 | }
59 | for _, file := range files {
60 | src := testdataPath(file)
61 | dest := filepath.Join(tempDir, file)
62 | cpFile(t, dest, src)
63 | }
64 |
65 | got := []string{}
66 | want := []string{
67 | "arara.jar",
68 | "arara.signed.jar",
69 | "bad_jar_in_jar.jar",
70 | "bad_jar_in_jar_in_jar.jar",
71 | "bad_jar_with_invalid_jar.jar",
72 | "log4j-core-2.1.jar",
73 | "log4j-core-2.12.1.jar",
74 | "log4j-core-2.14.0.jar",
75 | "log4j-core-2.15.0.jar",
76 | "vuln-class.jar",
77 | }
78 | for i, p := range want {
79 | want[i] = filepath.Join(tempDir, p)
80 | }
81 | w := Walker{
82 | HandleError: func(path string, err error) {
83 | t.Errorf("processing %s: %v", path, err)
84 | },
85 | HandleReport: func(path string, r *Report) {
86 | got = append(got, path)
87 | },
88 | }
89 | if err := w.Walk(tempDir); err != nil {
90 | t.Fatalf("walking filesystem: %v", err)
91 | }
92 | if diff := cmp.Diff(want, got); diff != "" {
93 | t.Errorf("walking filesystem returned diff (-want, +got): %s", diff)
94 | }
95 | }
96 |
97 | func TestWalkerRewrite(t *testing.T) {
98 | tempDir := t.TempDir()
99 | files := []string{
100 | "arara.jar",
101 | "arara.jar.patched",
102 | "arara.signed.jar",
103 | "arara.signed.jar.patched",
104 | "bad_jar_in_jar_in_jar.jar",
105 | "bad_jar_in_jar_in_jar.jar.patched",
106 | "bad_jar_in_jar.jar",
107 | "bad_jar_in_jar.jar.patched",
108 | "bad_jar_with_invalid_jar.jar",
109 | "bad_jar_with_invalid_jar.jar.patched",
110 | "good_jar_in_jar_in_jar.jar",
111 | "good_jar_in_jar.jar",
112 | "good_jar_with_invalid_jar.jar",
113 | "helloworld.jar",
114 | "helloworld.signed.jar",
115 | "log4j-core-2.12.1.jar",
116 | "log4j-core-2.12.1.jar.patched",
117 | "log4j-core-2.14.0.jar",
118 | "log4j-core-2.14.0.jar.patched",
119 | "log4j-core-2.15.0.jar",
120 | "log4j-core-2.15.0.jar.patched",
121 | "log4j-core-2.16.0.jar",
122 | "log4j-core-2.1.jar",
123 | "log4j-core-2.1.jar.patched",
124 | "notarealjar.jar",
125 | "safe1.jar",
126 | "safe1.signed.jar",
127 | "similarbutnotvuln.jar",
128 | "vuln-class.jar",
129 | "vuln-class.jar.patched",
130 | }
131 | for _, file := range files {
132 | src := testdataPath(file)
133 | dest := filepath.Join(tempDir, file)
134 | cpFile(t, dest, src)
135 | }
136 |
137 | got := []string{}
138 | want := []string{
139 | "arara.jar",
140 | "arara.signed.jar",
141 | "bad_jar_in_jar.jar",
142 | "bad_jar_in_jar_in_jar.jar",
143 | "bad_jar_with_invalid_jar.jar",
144 | "log4j-core-2.1.jar",
145 | "log4j-core-2.12.1.jar",
146 | "log4j-core-2.14.0.jar",
147 | "log4j-core-2.15.0.jar",
148 | "vuln-class.jar",
149 | }
150 | for i, p := range want {
151 | want[i] = filepath.Join(tempDir, p)
152 | }
153 | w := Walker{
154 | Rewrite: true,
155 | HandleError: func(path string, err error) {
156 | t.Errorf("processing %s: %v", path, err)
157 | },
158 | HandleRewrite: func(path string, r *Report) {
159 | got = append(got, path)
160 | },
161 | }
162 | if err := w.Walk(tempDir); err != nil {
163 | t.Fatalf("walking filesystem: %v", err)
164 | }
165 | if diff := cmp.Diff(want, got); diff != "" {
166 | t.Errorf("walking filesystem returned diff (-want, +got): %s", diff)
167 | }
168 | got = []string{}
169 | want = []string{}
170 | w.HandleError = func(path string, err error) {
171 | t.Errorf("processing after rewrite %s: %v", path, err)
172 | }
173 |
174 | if err := w.Walk(tempDir); err != nil {
175 | t.Fatalf("walking filesystem: %v", err)
176 | }
177 | if diff := cmp.Diff(want, got); diff != "" {
178 | t.Errorf("walking filesystem after rewrite returned diff (-want, +got): %s", diff)
179 | }
180 | }
181 |
182 | // TestNonDefaultParser verifies that Walker can be configured with a
183 | // non-default Parser by scanning a large JAR file with two
184 | // configurations: one where Parser.MaxBytes is larger than the file
185 | // size and one where Parser.MaxBytes is smaller than the file
186 | // size. It ensures that the first case succeeds and the second fails.
187 | func TestNonDefaultParser(t *testing.T) {
188 | jar := "400mb_jar_in_jar.jar"
189 |
190 | tempDir := t.TempDir()
191 | src := testdataPath(jar)
192 | dest := filepath.Join(tempDir, jar)
193 | cpFile(t, dest, src)
194 |
195 | tests := []struct {
196 | desc string
197 | maxBytes int64
198 | wantErr bool
199 | }{
200 | {
201 | desc: "MaxBytes > JAR size",
202 | maxBytes: 4 << 30, // 4GiB
203 | wantErr: false,
204 | },
205 | {
206 | desc: "MaxBytes < JAR size",
207 | maxBytes: 4 << 20, // 4MiB
208 | wantErr: true,
209 | },
210 | }
211 |
212 | for _, tc := range tests {
213 | t.Run(tc.desc, func(t *testing.T) {
214 | var gotErr error
215 |
216 | p := &Parser{MaxBytes: tc.maxBytes}
217 | w := &Walker{
218 | Parser: p,
219 | HandleError: func(path string, err error) {
220 | if err != nil && strings.HasSuffix(path, filepath.FromSlash("/"+jar)) {
221 | gotErr = err
222 | }
223 | },
224 | }
225 | if err := w.Walk(tempDir); err != nil {
226 | t.Errorf("Walk returned unexpected error: %v", err)
227 | }
228 |
229 | if tc.wantErr && gotErr == nil {
230 | t.Error("Parser failed to generate expected error when scanning JARs > MaxBytes, got nil, want error")
231 | } else if !tc.wantErr && gotErr != nil {
232 | t.Errorf("Parser generated unexpected error when scanning JARs <= MaxBytes, got %v, want nil error", gotErr)
233 | }
234 | })
235 | }
236 | }
237 |
--------------------------------------------------------------------------------
/jar/walker_unix.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | //go:build linux || darwin
16 |
17 | package jar
18 |
19 | import (
20 | "fmt"
21 | "io/fs"
22 | "syscall"
23 | )
24 |
25 | func fileOwner(fi fs.FileInfo) (uid, gid uint32, ok bool, err error) {
26 | if fi.Sys() == nil {
27 | err = fmt.Errorf("failed to get system-specific stat info")
28 | return
29 | }
30 | s, ok := fi.Sys().(*syscall.Stat_t)
31 | if !ok {
32 | err = fmt.Errorf("failed to get system-specific stat info: expected *syscall.Stat_t, got %T", fi.Sys())
33 | return
34 | }
35 | return s.Uid, s.Gid, true, nil
36 | }
37 |
--------------------------------------------------------------------------------
/log4jscanner.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // The log4jscanner tool scans a set of directories for log4j vulnerable JARs.
16 | package main
17 |
18 | import (
19 | "flag"
20 | "fmt"
21 | "io/fs"
22 | "log"
23 | "os"
24 | "path/filepath"
25 |
26 | "github.com/google/log4jscanner/jar"
27 | )
28 |
29 | func usage() {
30 | fmt.Fprint(os.Stderr, `Usage: log4jscanner [flag] [directories]
31 |
32 | A log4j vulnerability scanner. The scanner walks the provided directories
33 | attempting to find vulnerable JARs. Paths of vulnerable JARs are printed
34 | to stdout.
35 |
36 | Flags:
37 |
38 | -s, --skip Glob pattern to skip when scanning (e.g. '/var/run/*'). May
39 | be provided multiple times.
40 | -f, --force Don't skip network and userland filesystems. (smb,nfs,afs,fuse)
41 | -w, --rewrite Rewrite vulnerable JARs as they are detected.
42 | -v, --verbose Print verbose logs to stderr.
43 |
44 | `)
45 | }
46 |
47 | var skipDirs = map[string]bool{
48 | ".hg": true,
49 | ".git": true,
50 | "node_modules": true,
51 | ".idea": true,
52 | ".svn": true,
53 | ".p4root": true,
54 |
55 | // TODO(ericchiang): expand
56 | }
57 |
58 | func main() {
59 | var (
60 | rewrite bool
61 | w bool
62 | verbose bool
63 | v bool
64 | force bool
65 | f bool
66 | toSkip []string
67 | )
68 | appendSkip := func(dir string) error {
69 | toSkip = append(toSkip, dir)
70 | return nil
71 | }
72 |
73 | flag.BoolVar(&rewrite, "rewrite", false, "")
74 | flag.BoolVar(&w, "w", false, "")
75 | flag.BoolVar(&verbose, "verbose", false, "")
76 | flag.BoolVar(&v, "v", false, "")
77 | flag.BoolVar(&force, "force", false, "")
78 | flag.BoolVar(&f, "f", false, "")
79 | flag.Func("s", "", appendSkip)
80 | flag.Func("skip", "", appendSkip)
81 | flag.Usage = usage
82 | flag.Parse()
83 | dirs := flag.Args()
84 | if len(dirs) == 0 {
85 | usage()
86 | os.Exit(1)
87 | }
88 | if f {
89 | force = f
90 | }
91 | if v {
92 | verbose = v
93 | }
94 | if w {
95 | rewrite = w
96 | }
97 | log.SetFlags(log.LstdFlags | log.Lshortfile)
98 | logf := func(format string, v ...interface{}) {
99 | if verbose {
100 | log.Printf(format, v...)
101 | }
102 | }
103 | seen := 0
104 | walker := jar.Walker{
105 | Rewrite: rewrite,
106 | SkipDir: func(path string, d fs.DirEntry) bool {
107 | seen++
108 | if seen%5000 == 0 {
109 | logf("Scanned %d files", seen)
110 | }
111 | if !d.IsDir() {
112 | return false
113 | }
114 | for _, pattern := range toSkip {
115 | if ok, err := filepath.Match(pattern, path); err == nil && ok {
116 | return true
117 | }
118 | }
119 | if skipDirs[filepath.Base(path)] {
120 | return true
121 | }
122 | ignore, err := ignoreDir(path, force)
123 | if err != nil {
124 | log.Printf("Error scanning %s: %v", path, err)
125 | }
126 | return ignore
127 | },
128 | HandleError: func(path string, err error) {
129 | log.Printf("Error: scanning %s: %v", path, err)
130 | },
131 | HandleReport: func(path string, r *jar.Report) {
132 | if !rewrite {
133 | fmt.Println(path)
134 | }
135 | },
136 | HandleRewrite: func(path string, r *jar.Report) {
137 | if rewrite {
138 | fmt.Println(path)
139 | }
140 | },
141 | }
142 |
143 | for _, dir := range dirs {
144 | logf("Scanning %s", dir)
145 | if err := walker.Walk(dir); err != nil {
146 | log.Printf("Error: walking %s: %v", dir, err)
147 | }
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/log4jscanner_linux.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | //go:build linux
16 |
17 | package main
18 |
19 | import (
20 | "fmt"
21 |
22 | "golang.org/x/sys/unix"
23 | )
24 |
25 | var toIgnore = map[int64]bool{
26 | unix.CGROUP_SUPER_MAGIC: true,
27 | unix.BPF_FS_MAGIC: true,
28 | unix.DEBUGFS_MAGIC: true,
29 | unix.DEVPTS_SUPER_MAGIC: true,
30 | unix.PROC_SUPER_MAGIC: true,
31 | unix.SECURITYFS_MAGIC: true,
32 | unix.SYSFS_MAGIC: true,
33 | unix.TRACEFS_MAGIC: true,
34 | }
35 |
36 | var networkIgnore = map[int64]bool{
37 | unix.SMB_SUPER_MAGIC: true,
38 | unix.AFS_SUPER_MAGIC: true,
39 | unix.NFS_SUPER_MAGIC: true,
40 | 0x65735546: true, // Fuse_SUPER_MAGIC
41 | 0xff534d42: true, // CIFS_MAGIC_NUMBER
42 | 0xfe534d42: true, // SMB2_MAGIC_NUMBER
43 | }
44 |
45 | func ignoreDir(path string, force bool) (bool, error) {
46 | var stat unix.Statfs_t
47 | if err := unix.Statfs(path, &stat); err != nil {
48 | return false, fmt.Errorf("determining filesystem of %s: %v", path, err)
49 | }
50 | if force {
51 | return toIgnore[stat.Type], nil
52 | }
53 | return toIgnore[stat.Type] || networkIgnore[stat.Type], nil
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/log4jscanner_other.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | //go:build !linux
16 |
17 | package main
18 |
19 | func ignoreDir(path string, force bool) (bool, error) {
20 | return false, nil
21 | }
22 |
--------------------------------------------------------------------------------
/pool/dynamic.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // Package pool provides an object pool that trades off the cost of creation
16 | // versus retention. It is meant to avoid the pessimal behaviour (see [issue
17 | // 23199]) seen when using a regular sync.Pool with objects of dynamic sizes;
18 | // objects that are too large are kept alive by repeat usages that don't need
19 | // such sizes.
20 | //
21 | // [issue 23199]: https://github.com/golang/go/issues/23199
22 | package pool
23 |
24 | import (
25 | "math"
26 | "sync/atomic"
27 | )
28 |
29 | // A Dynamic pool is like a sync.Pool for objects of varying sizes.
30 | //
31 | // It prevents the indefinite retention of (too) large objects by keeping a
32 | // history of required object sizes (utility) and comparing them to the actual
33 | // object size (cost) before accepting an object.
34 | type Dynamic struct {
35 | Pool interface {
36 | Get() interface{}
37 | Put(interface{})
38 | }
39 |
40 | // The utility below which the cost of creating the object is more expensive
41 | // than just keeping it. Set this to the expected object size (or perhaps a
42 | // bit larger to reduce allocations more).
43 | MinUtility float64
44 |
45 | avgUtility uint64 // Actually a float64, but that type does not have atomic ops.
46 | }
47 |
48 | func (p *Dynamic) Get() interface{} {
49 | return p.Pool.Get()
50 | }
51 |
52 | // Put is like sync.Pool.Put, with a few differences. The utility is a measure
53 | // of what part of the object was actually used. The cost is a measure of the
54 | // total "size" of the object. Utility must be smaller than or equal to cost.
55 | func (p *Dynamic) Put(v interface{}, utility, cost float64) bool {
56 | // Update the average utility. Uses atomic load/store, which means that
57 | // values can get lost if Put is called concurrently. That's fine, we're
58 | // just looking for an approximate (weighted) moving average.
59 | avgUtility := math.Float64frombits(atomic.LoadUint64(&p.avgUtility))
60 | avgUtility = decay(avgUtility, utility, p.MinUtility)
61 | atomic.StoreUint64(&p.avgUtility, math.Float64bits(avgUtility))
62 |
63 | if cost > 10*avgUtility {
64 | return false // If the cost is 10x larger than the average utility, drop it.
65 | }
66 | p.Pool.Put(v)
67 | return true
68 | }
69 |
70 | // decay updates returns `val` if `val > `prev`, otherwise it returns an
71 | // exponentially moving average of `prev` and `val` (with factor 0.5. This is
72 | // meant to provide a slower downramp if `val` drops ever lower. The minimum
73 | // value is `min`.
74 | func decay(prev, val, min float64) float64 {
75 | if val < min {
76 | val = min
77 | }
78 | if prev == 0 || val > prev {
79 | return val
80 | }
81 | const factor = 0.5
82 | return (prev * factor) + (val * (1 - factor))
83 | }
84 |
--------------------------------------------------------------------------------
/pool/dynamic_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package pool
16 |
17 | import (
18 | "math"
19 | "sync/atomic"
20 | "testing"
21 | )
22 |
23 | const bufSize = 4096
24 |
25 | // A simplePool is like a sync.Pool, but more determistic for tests. Properties:
26 | //
27 | // - Can not be used concurrently.
28 | // - Can not have crossed Get/Put calls.
29 | type simplePool struct {
30 | New func() interface{}
31 |
32 | val interface{}
33 | }
34 |
35 | func (s *simplePool) Get() interface{} {
36 | if s.val == nil {
37 | return s.New()
38 | }
39 | return s.val
40 | }
41 |
42 | func (s *simplePool) Put(val interface{}) {
43 | s.val = val
44 | }
45 |
46 | // The desired behaviour of the dynamic (buffer) pool is:
47 | // - Don't retain (very) large items indefinitely (check that one is rejected
48 | // at least once).
49 | // - Do retain even large items for a while so their allocation cost is
50 | // amortized.
51 | func TestDynamic(t *testing.T) {
52 | dp := Dynamic{
53 | Pool: &simplePool{New: func() interface{} { return make([]byte, 0) }},
54 | MinUtility: bufSize,
55 | }
56 | var allocs int
57 | // Simulate a sequence of file sizes. This sequence is not based on some
58 | // real-life observed sequence of sizes of jar-in-jars. It might be better
59 | // to use such a sequence, but every organisation will have its own expected
60 | // sizes and this synthetic one conains some fairly extreme samples that
61 | // check whether the algorithm is robust.
62 | //
63 | // For the current algorithm, the worst possible sequence is one that
64 | // rises, then suddenly drops and then rises slowly again. We contend that
65 | // this case is rare.
66 | sizes := [...]int{
67 | 100000, 1, 1, 1, 1, 1, 10, 1, 1, 1, 1, 1, 1, 1, 12, 1, 1, 1, 1, 1, 1, 1,
68 | 1000, 100, 10000, 100000, 1, 100000, 1, 50000, 1, 1, 25000, 1, 1, 1,
69 | 100000, 1, 1, 1, 1, 1, 1, 1, 1, 1, 100, 100, 100, 1, 1, 1, 1, 1, 100,
70 | 200, 300, 100, 50, 50, 50, 50, 50, 1, 1, 1, 1, 100000000, 1000000,
71 | 100000, 10000, 1000, 100, 10, 1, 1, 500, 2020, 400, 3984, 5, 200, 500,
72 | 40000, 35000, 45000, 42000, 38000, 38000, 39000, 41000, 42000, 42000, // Average: 40000
73 | 2000, 4000, 3949, 2011, 4096, 33, 0, 4938, 1, 1, 1200, 2400, 1200, 200,
74 | 400, 600, 700, 100, 400, 500, 700, 600, 900, 1000, 1100, 1200, 1000,
75 | }
76 |
77 | var largeBufferPurged int
78 |
79 | t.Logf("num allocs value target capacity")
80 | for idx, size := range sizes {
81 | buf := dp.Get().([]byte)
82 | if cap(buf) < size {
83 | capacity := size
84 | if capacity < bufSize {
85 | capacity = bufSize // Allocating much smaller buffers could lead to quick re-allocations.
86 | }
87 | buf = make([]byte, size, capacity)
88 | allocs++
89 | } else {
90 | buf = buf[:size]
91 | }
92 | utility := float64(len(buf))
93 | if utility < bufSize {
94 | utility = bufSize
95 | }
96 | if !dp.Put(buf, utility, float64(cap(buf))) && cap(buf) >= 100000 {
97 | largeBufferPurged++
98 | }
99 | avgUtility := math.Float64frombits(atomic.LoadUint64(&dp.avgUtility))
100 | t.Logf("%d %d %d %f %d", idx+1, allocs, size, avgUtility, cap(buf))
101 | }
102 | // Before the amortized buffer optimization, each iteration would've been
103 | // one allocation. We want at least 10x fewer than that.
104 | if got, want := allocs, len(sizes)/10; got > want {
105 | t.Errorf("got %d allocations, wanted %d", got, want)
106 | }
107 | if got, atLeast := largeBufferPurged, 2; got < atLeast {
108 | t.Errorf("buffers >= 100000 have been rejected %d times, expected at least %d", got, atLeast)
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/scripts/build-release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -e
2 |
3 | # Copyright 2021 Google LLC
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # https://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | # A script for building binary releases for various platforms.
18 |
19 | if [[ "$0" != "./scripts/build-release.sh" ]]; then
20 | 1>&2 echo "Script must be run at root of filesystem"
21 | exit 1
22 | fi
23 |
24 | VERSION="$1"
25 |
26 | if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then
27 | VERSION="$GITHUB_REF_NAME"
28 | fi
29 |
30 | if [[ "$VERSION" != v* ]]; then
31 | 1>&2 echo "No version specified as argument"
32 | exit 1
33 | fi
34 |
35 | mkdir -p bin
36 | rm -f bin/*.zip
37 | rm -f bin/*.tar.gz
38 |
39 | function build {
40 | TEMP_DIR="$( mktemp -d )"
41 | GOOS="$1"
42 | GOARCH="$2"
43 |
44 | export CGO_ENABLED=0
45 |
46 | BIN_NAME="log4jscanner"
47 | if [[ "$1" == "windows" ]]; then
48 | BIN_NAME="log4jscanner.exe"
49 | fi
50 |
51 | GOOS="$GOOS" GOARCH="$GOARCH" go build -o "${TEMP_DIR}/log4jscanner/${BIN_NAME}"
52 |
53 | if [[ "$1" == "windows" ]]; then
54 | TARGET="${PWD}/bin/log4jscanner-${VERSION}-${GOOS}-${GOARCH}.zip"
55 | cd "$TEMP_DIR"
56 | zip -r "$TARGET" ./
57 | cd -
58 | else
59 | TARGET="${PWD}/bin/log4jscanner-${VERSION}-${GOOS}-${GOARCH}.tar.gz"
60 | tar \
61 | --group=root \
62 | --owner=root \
63 | -czvf \
64 | "$TARGET" \
65 | -C "$TEMP_DIR" \
66 | "./log4jscanner"
67 | # Print the contents of the TAR so we can visually debug it during CI.
68 | tar -ztvf "$TARGET"
69 | fi
70 | rm -rf "$TEMP_DIR"
71 | }
72 |
73 | # NOTE: When adding new releases, also update .github/workflows/release.yaml.
74 |
75 | build darwin amd64
76 | build darwin arm64
77 | build linux amd64
78 | build linux arm64
79 | build windows amd64
80 |
--------------------------------------------------------------------------------
/scripts/release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -e
2 |
3 | # Copyright 2021 Google LLC
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # https://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | # https://docs.github.com/en/actions/learn-github-actions/environment-variables
18 |
19 | if [[ "$GITHUB_TOKEN" == "" ]]; then
20 | 2>&1 echo "GITHUB_TOKEN not present"
21 | exit 1
22 | fi
23 |
24 | if [[ "$GITHUB_REF_NAME" == "" ]]; then
25 | 2>&1 echo "GITHUB_REF_NAME not present"
26 | exit 1
27 | fi
28 |
29 | if [[ "$GITHUB_API_URL" == "" ]]; then
30 | 2>&1 echo "GITHUB_API_URL not present"
31 | exit 1
32 | fi
33 |
34 | if [[ "$GITHUB_REPOSITORY" == "" ]]; then
35 | 2>&1 echo "GITHUB_REPOSITORY not present"
36 | exit 1
37 | fi
38 |
39 | if [[ "$1" == "" ]]; then
40 | 2>&1 echo "No files to upload"
41 | exit 1
42 | fi
43 |
44 | AUTH_HEADER="Authorization: token ${GITHUB_TOKEN}"
45 |
46 | TEMP_DIR="$( mktemp -d )"
47 |
48 | # https://docs.github.com/en/rest/reference/releases#get-a-release-by-tag-name
49 |
50 | curl -sSL \
51 | --fail \
52 | --header "$AUTH_HEADER" \
53 | -o "${TEMP_DIR}/out" \
54 | "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}"
55 |
56 | RELEASE_ID="$( jq '.id' < "${TEMP_DIR}/out" )"
57 |
58 | echo "Release ID: ${RELEASE_ID}"
59 |
60 | # https://docs.github.com/en/rest/reference/releases#upload-a-release-asset
61 |
62 | for FILE in ${1}/*
63 | do
64 | echo $FILE
65 | NAME="${FILE#$1}"
66 | if [[ "$NAME" != "/" && "$NAME" != "" ]]; then
67 | echo "Uploading: ${NAME}"
68 | RELEASE_URL="https://uploads.github.com/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=${NAME}"
69 | echo "Upload URL: ${RELEASE_URL}"
70 | curl -sSL \
71 | -o - \
72 | -XPOST \
73 | --fail \
74 | --header "$AUTH_HEADER" \
75 | --header "Content-Type: application/octet-stream" \
76 | --upload-file "$FILE" \
77 | "$RELEASE_URL"
78 | fi
79 | done
80 |
81 | rm -rf "$TEMP_DIR"
82 |
--------------------------------------------------------------------------------
/third_party/zip/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2009 The Go Authors. All rights reserved.
2 |
3 | Redistribution and use in source and binary forms, with or without
4 | modification, are permitted provided that the following conditions are
5 | met:
6 |
7 | * Redistributions of source code must retain the above copyright
8 | notice, this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above
10 | copyright notice, this list of conditions and the following disclaimer
11 | in the documentation and/or other materials provided with the
12 | distribution.
13 | * Neither the name of Google Inc. nor the names of its
14 | contributors may be used to endorse or promote products derived from
15 | this software without specific prior written permission.
16 |
17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/third_party/zip/zip.go:
--------------------------------------------------------------------------------
1 | // Copyright 2010 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | // Package zip contains a fork of archive/zip reader.
6 | //
7 | // This package copies logic from archive/zip to read the central directory file
8 | // header from the end of a ZIP file. The only modification is that
9 | // readDirectoryEnd() is exported as ReadZIPOffset(), returning the position of the
10 | // start of the ZIP contents.
11 | //
12 | // https://github.com/golang/go/blob/go1.17.5/src/archive/zip/reader.go
13 | // https://github.com/golang/go/blob/go1.17.5/src/archive/zip/struct.go
14 | //
15 | // See https://go.dev/issues/10464
16 | //
17 | // This package MUST NOT be depended on by external code and may change at any
18 | // time. This is not an "internal" package only because we're required to place
19 | // external code under a top level "third_party/" directory.
20 | package zip
21 |
22 | import (
23 | "archive/zip"
24 | "encoding/binary"
25 | "errors"
26 | "io"
27 | )
28 |
29 | type readBuf []byte
30 |
31 | func (b *readBuf) uint8() uint8 {
32 | v := (*b)[0]
33 | *b = (*b)[1:]
34 | return v
35 | }
36 |
37 | func (b *readBuf) uint16() uint16 {
38 | v := binary.LittleEndian.Uint16(*b)
39 | *b = (*b)[2:]
40 | return v
41 | }
42 |
43 | func (b *readBuf) uint32() uint32 {
44 | v := binary.LittleEndian.Uint32(*b)
45 | *b = (*b)[4:]
46 | return v
47 | }
48 |
49 | func (b *readBuf) uint64() uint64 {
50 | v := binary.LittleEndian.Uint64(*b)
51 | *b = (*b)[8:]
52 | return v
53 | }
54 |
55 | func (b *readBuf) sub(n int) readBuf {
56 | b2 := (*b)[:n]
57 | *b = (*b)[n:]
58 | return b2
59 | }
60 |
61 | const (
62 | directory64LocSignature = 0x07064b50
63 | directory64EndSignature = 0x06064b50
64 |
65 | directoryEndLen = 22 // + comment
66 | directory64LocLen = 20 //
67 | directory64EndLen = 56 // + extra
68 |
69 | )
70 |
71 | type directoryEnd struct {
72 | diskNbr uint32 // unused
73 | dirDiskNbr uint32 // unused
74 | dirRecordsThisDisk uint64 // unused
75 | directoryRecords uint64
76 | directorySize uint64
77 | directoryOffset uint64 // relative to file
78 | commentLen uint16
79 | comment string
80 | }
81 |
82 | // ReadZIPOffset attempts to determine where a ZIP file starts, supporting
83 | // self-executing JARs. JARs concatenated with a bash script.
84 | func ReadZIPOffset(r io.ReaderAt, size int64) (offset int64, err error) {
85 | // look for directoryEndSignature in the last 1k, then in the last 65k
86 | var buf []byte
87 | var directoryEndOffset int64
88 | for i, bLen := range []int64{1024, 65 * 1024} {
89 | if bLen > size {
90 | bLen = size
91 | }
92 | buf = make([]byte, int(bLen))
93 | if _, err := r.ReadAt(buf, size-bLen); err != nil && err != io.EOF {
94 | return 0, err
95 | }
96 | if p := findSignatureInBlock(buf); p >= 0 {
97 | buf = buf[p:]
98 | directoryEndOffset = size - bLen + int64(p)
99 | break
100 | }
101 | if i == 1 || bLen == size {
102 | return 0, zip.ErrFormat
103 | }
104 | }
105 |
106 | // read header into struct
107 | b := readBuf(buf[4:]) // skip signature
108 | d := &directoryEnd{
109 | diskNbr: uint32(b.uint16()),
110 | dirDiskNbr: uint32(b.uint16()),
111 | dirRecordsThisDisk: uint64(b.uint16()),
112 | directoryRecords: uint64(b.uint16()),
113 | directorySize: uint64(b.uint32()),
114 | directoryOffset: uint64(b.uint32()),
115 | commentLen: b.uint16(),
116 | }
117 | l := int(d.commentLen)
118 | if l > len(b) {
119 | return 0, errors.New("zip: invalid comment length")
120 | }
121 | d.comment = string(b[:l])
122 | return directoryEndOffset - int64(d.directorySize) - int64(d.directoryOffset), nil
123 | }
124 |
125 | func findSignatureInBlock(b []byte) int {
126 | for i := len(b) - directoryEndLen; i >= 0; i-- {
127 | // defined from directoryEndSignature in struct.go
128 | if b[i] == 'P' && b[i+1] == 'K' && b[i+2] == 0x05 && b[i+3] == 0x06 {
129 | // n is length of comment
130 | n := int(b[i+directoryEndLen-2]) | int(b[i+directoryEndLen-1])<<8
131 | if n+directoryEndLen+i <= len(b) {
132 | return i
133 | }
134 | }
135 | }
136 | return -1
137 | }
138 |
139 | // readDirectory64End reads the zip64 directory end and updates the
140 | // directory end with the zip64 directory end values.
141 | func readDirectory64End(r io.ReaderAt, offset int64, d *directoryEnd) (err error) {
142 | buf := make([]byte, directory64EndLen)
143 | if _, err := r.ReadAt(buf, offset); err != nil {
144 | return err
145 | }
146 |
147 | b := readBuf(buf)
148 | if sig := b.uint32(); sig != directory64EndSignature {
149 | return zip.ErrFormat
150 | }
151 |
152 | b = b[12:] // skip dir size, version and version needed (uint64 + 2x uint16)
153 | d.diskNbr = b.uint32() // number of this disk
154 | d.dirDiskNbr = b.uint32() // number of the disk with the start of the central directory
155 | d.dirRecordsThisDisk = b.uint64() // total number of entries in the central directory on this disk
156 | d.directoryRecords = b.uint64() // total number of entries in the central directory
157 | d.directorySize = b.uint64() // size of the central directory
158 | d.directoryOffset = b.uint64() // offset of start of central directory with respect to the starting disk number
159 |
160 | return nil
161 | }
162 |
163 | // findDirectory64End tries to read the zip64 locator just before the
164 | // directory end and returns the offset of the zip64 directory end if
165 | // found.
166 | func findDirectory64End(r io.ReaderAt, directoryEndOffset int64) (int64, error) {
167 | locOffset := directoryEndOffset - directory64LocLen
168 | if locOffset < 0 {
169 | return -1, nil // no need to look for a header outside the file
170 | }
171 | buf := make([]byte, directory64LocLen)
172 | if _, err := r.ReadAt(buf, locOffset); err != nil {
173 | return -1, err
174 | }
175 | b := readBuf(buf)
176 | if sig := b.uint32(); sig != directory64LocSignature {
177 | return -1, nil
178 | }
179 | if b.uint32() != 0 { // number of the disk with the start of the zip64 end of central directory
180 | return -1, nil // the file is not a valid zip64-file
181 | }
182 | p := b.uint64() // relative offset of the zip64 end of central directory record
183 | if b.uint32() != 1 { // total number of disks
184 | return -1, nil // the file is not a valid zip64-file
185 | }
186 | return int64(p), nil
187 | }
188 |
--------------------------------------------------------------------------------