├── .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 | [![Go Reference](https://pkg.go.dev/badge/github.com/google/log4jscanner/jar.svg)](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 | --------------------------------------------------------------------------------