From efc98458a95ac5d82b26c3441bc49f4686255230 Mon Sep 17 00:00:00 2001 From: ricekot Date: Fri, 19 Sep 2025 23:53:46 +0530 Subject: [PATCH] ascanrulesAlpha: Add Suspicious Input Transformation Script Signed-off-by: ricekot --- addOns/ascanrulesAlpha/CHANGELOG.md | 3 +- .../ascanrulesAlpha.gradle.kts | 17 + .../ExtensionAscanRulesAlphaScripts.java | 163 +++++++ .../resources/help/contents/ascanalpha.html | 9 + .../resources/Messages.properties | 5 + .../active/SuspiciousInputTransformation.js | 253 +++++++++++ ...spiciousInputTransformationScriptTest.java | 409 ++++++++++++++++++ ...ctiveDefaultTemplateGraalJsScriptTest.java | 8 - .../GraalJsActiveScriptScanRuleTestUtils.java | 11 + ...GraalJsPassiveScriptScanRuleTestUtils.java | 10 + ...ssiveDefaultTemplateGraalJsScriptTest.java | 8 - 11 files changed, 879 insertions(+), 17 deletions(-) create mode 100644 addOns/ascanrulesAlpha/src/main/java/org/zaproxy/zap/extension/ascanrulesAlpha/scripts/ExtensionAscanRulesAlphaScripts.java create mode 100644 addOns/ascanrulesAlpha/src/main/zapHomeFiles/scripts/scripts/active/SuspiciousInputTransformation.js create mode 100644 addOns/ascanrulesAlpha/src/test/java/org/zaproxy/zap/extension/ascanrulesAlpha/SuspiciousInputTransformationScriptTest.java diff --git a/addOns/ascanrulesAlpha/CHANGELOG.md b/addOns/ascanrulesAlpha/CHANGELOG.md index 33b2b143f89..f09734dc06c 100644 --- a/addOns/ascanrulesAlpha/CHANGELOG.md +++ b/addOns/ascanrulesAlpha/CHANGELOG.md @@ -4,7 +4,8 @@ All notable changes to this add-on will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased - +### Added +- Suspicious Input Transformation Script Scan Rule. ## [51] - 2025-09-18 ### Changed diff --git a/addOns/ascanrulesAlpha/ascanrulesAlpha.gradle.kts b/addOns/ascanrulesAlpha/ascanrulesAlpha.gradle.kts index efb00aff7e0..107efba8be7 100644 --- a/addOns/ascanrulesAlpha/ascanrulesAlpha.gradle.kts +++ b/addOns/ascanrulesAlpha/ascanrulesAlpha.gradle.kts @@ -14,6 +14,20 @@ zapAddOn { } } } + + extensions { + register("org.zaproxy.zap.extension.ascanrulesAlpha.scripts.ExtensionAscanRulesAlphaScripts") { + classnames { + allowed.set(listOf("org.zaproxy.zap.extension.ascanrulesAlpha.scripts")) + } + dependencies { + addOns { + register("scripts") + register("graaljs") + } + } + } + } } } @@ -25,4 +39,7 @@ dependencies { zapAddOn("commonlib") testImplementation(project(":testutils")) + testImplementation(project(":addOns:graaljs")) + testImplementation(project(":addOns:scripts")) + testImplementation(parent!!.childProjects.get("graaljs")!!.sourceSets.test.get().output) } diff --git a/addOns/ascanrulesAlpha/src/main/java/org/zaproxy/zap/extension/ascanrulesAlpha/scripts/ExtensionAscanRulesAlphaScripts.java b/addOns/ascanrulesAlpha/src/main/java/org/zaproxy/zap/extension/ascanrulesAlpha/scripts/ExtensionAscanRulesAlphaScripts.java new file mode 100644 index 00000000000..abe00e1806d --- /dev/null +++ b/addOns/ascanrulesAlpha/src/main/java/org/zaproxy/zap/extension/ascanrulesAlpha/scripts/ExtensionAscanRulesAlphaScripts.java @@ -0,0 +1,163 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2025 The ZAP Development Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zaproxy.zap.extension.ascanrulesAlpha.scripts; + +import java.io.File; +import java.nio.file.Paths; +import java.util.List; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.parosproxy.paros.Constant; +import org.parosproxy.paros.extension.Extension; +import org.parosproxy.paros.extension.ExtensionAdaptor; +import org.zaproxy.zap.extension.ascan.ExtensionActiveScan; +import org.zaproxy.zap.extension.script.ExtensionScript; +import org.zaproxy.zap.extension.script.ScriptEngineWrapper; +import org.zaproxy.zap.extension.script.ScriptType; +import org.zaproxy.zap.extension.script.ScriptWrapper; + +public class ExtensionAscanRulesAlphaScripts extends ExtensionAdaptor { + + private static final List> DEPENDENCIES = + List.of(ExtensionActiveScan.class, ExtensionScript.class); + private static final Logger LOGGER = + LogManager.getLogger(ExtensionAscanRulesAlphaScripts.class); + private static final String SCRIPT_SUSPICIOUS_INPUT_TRANSFORMATION = + "SuspiciousInputTransformation.js"; + + private ExtensionScript extScript; + + @Override + public String getName() { + return ExtensionAscanRulesAlphaScripts.class.getSimpleName(); + } + + @Override + public String getUIName() { + return Constant.messages.getString("ascanalpha.scripts.name"); + } + + @Override + public String getDescription() { + return Constant.messages.getString("ascanalpha.scripts.desc"); + } + + @Override + public List> getDependencies() { + return DEPENDENCIES; + } + + @Override + public void postInit() { + extScript = + org.parosproxy.paros.control.Control.getSingleton() + .getExtensionLoader() + .getExtension(ExtensionScript.class); + addScripts(); + } + + @Override + public boolean canUnload() { + return true; + } + + @Override + public void unload() { + removeScripts(); + } + + private void addScripts() { + addScript( + SCRIPT_SUSPICIOUS_INPUT_TRANSFORMATION, + Constant.messages.getString( + "ascanalpha.scripts.suspiciousInputTransformation.desc"), + extScript.getScriptType(ExtensionActiveScan.SCRIPT_TYPE_ACTIVE), + false); + } + + private void addScript(String name, String description, ScriptType type, boolean isTemplate) { + try { + if (extScript.getScript(name) != null) { + return; + } + ScriptEngineWrapper engine = extScript.getEngineWrapper("Graal.js"); + if (engine == null) { + return; + } + + File file; + if (isTemplate) { + file = + Paths.get( + Constant.getZapHome(), + ExtensionScript.TEMPLATES_DIR, + type.getName(), + name) + .toFile(); + } else { + file = + Paths.get( + Constant.getZapHome(), + ExtensionScript.SCRIPTS_DIR, + ExtensionScript.SCRIPTS_DIR, + type.getName(), + name) + .toFile(); + } + ScriptWrapper script = new ScriptWrapper(name, description, engine, type, true, file); + extScript.loadScript(script); + if (isTemplate) { + extScript.addTemplate(script, false); + } else { + extScript.addScript(script, false); + } + } catch (Exception e) { + LOGGER.warn( + Constant.messages.getString( + "ascanalpha.scripts.warn.couldNotAddScripts", e.getLocalizedMessage())); + } + } + + private void removeScripts() { + if (extScript == null) { + return; + } + removeScript(SCRIPT_SUSPICIOUS_INPUT_TRANSFORMATION, false); + } + + private void removeScript(String name, boolean isTemplate) { + ScriptWrapper script; + if (isTemplate) { + script = extScript.getTreeModel().getTemplate(name); + } else { + script = extScript.getScript(name); + } + + if (script == null) { + return; + } + + if (isTemplate) { + extScript.removeTemplate(script); + } else { + extScript.removeScript(script); + } + } +} diff --git a/addOns/ascanrulesAlpha/src/main/javahelp/org/zaproxy/zap/extension/ascanrulesAlpha/resources/help/contents/ascanalpha.html b/addOns/ascanrulesAlpha/src/main/javahelp/org/zaproxy/zap/extension/ascanrulesAlpha/resources/help/contents/ascanalpha.html index 560431c44e4..f6b648becc4 100644 --- a/addOns/ascanrulesAlpha/src/main/javahelp/org/zaproxy/zap/extension/ascanrulesAlpha/resources/help/contents/ascanalpha.html +++ b/addOns/ascanrulesAlpha/src/main/javahelp/org/zaproxy/zap/extension/ascanrulesAlpha/resources/help/contents/ascanalpha.html @@ -55,5 +55,14 @@

Web Cache Deception


Alert ID: 40039. +

Suspicious Input Transformation

+This is an active script scan rule. It detects various types of suspicious input transformations that may indicate +potential security vulnerabilities such as template injection, expression evaluation, quote consumption, and issues +related to unicode normalization. +

+Latest code: SuspiciousInputTransformation.js +
+Alert ID: 100044. + diff --git a/addOns/ascanrulesAlpha/src/main/resources/org/zaproxy/zap/extension/ascanrulesAlpha/resources/Messages.properties b/addOns/ascanrulesAlpha/src/main/resources/org/zaproxy/zap/extension/ascanrulesAlpha/resources/Messages.properties index f6e6700574c..4202dc7cdb4 100644 --- a/addOns/ascanrulesAlpha/src/main/resources/org/zaproxy/zap/extension/ascanrulesAlpha/resources/Messages.properties +++ b/addOns/ascanrulesAlpha/src/main/resources/org/zaproxy/zap/extension/ascanrulesAlpha/resources/Messages.properties @@ -33,6 +33,11 @@ ascanalpha.mongodb.soln = Do not trust client side input and escape all data on ascanalpha.name = Active Scan Rules - alpha +ascanalpha.scripts.desc = Adds alpha status active scan rule scripts. +ascanalpha.scripts.name = Active Scan Rule Scripts - alpha +ascanalpha.scripts.suspiciousInputTransformation.desc = This script detects suspicious input transformations in web applications. +ascanalpha.scripts.warn.couldNotAddScripts = Could not add alpha active scan rule scripts: {0}. + ascanalpha.webCacheDeception.desc = Web cache deception may be possible. It may be possible for unauthorised user to view sensitive data on this page. ascanalpha.webCacheDeception.name = Web Cache Deception ascanalpha.webCacheDeception.otherinfo = Cached Authorised Response and Unauthorised Response are similar. diff --git a/addOns/ascanrulesAlpha/src/main/zapHomeFiles/scripts/scripts/active/SuspiciousInputTransformation.js b/addOns/ascanrulesAlpha/src/main/zapHomeFiles/scripts/scripts/active/SuspiciousInputTransformation.js new file mode 100644 index 00000000000..c0f65593525 --- /dev/null +++ b/addOns/ascanrulesAlpha/src/main/zapHomeFiles/scripts/scripts/active/SuspiciousInputTransformation.js @@ -0,0 +1,253 @@ +/** + * Hat tip to https://github.com/albinowax/ActiveScanPlusPlus/blob/master/src/burp/SuspectTransform.java. + */ + +const ScanRuleMetadata = Java.type("org.zaproxy.addon.commonlib.scanrules.ScanRuleMetadata"); +const CommonAlertTag = Java.type("org.zaproxy.addon.commonlib.CommonAlertTag"); +const RandomStringUtils = Java.type("org.apache.commons.lang3.RandomStringUtils"); +const RandomUtils = Java.type("org.apache.commons.lang3.RandomUtils"); +const CHECK_CONFIRM_COUNT = 2; +const SCAN_RULE_ID = "100044"; + +function getMetadata() { + return ScanRuleMetadata.fromYaml(` +id: ${SCAN_RULE_ID} +name: Suspicious Input Transformation +description: > + The application performed a suspicious input transformation that may indicate a security vulnerability. + The input was transformed in an unexpected way, suggesting potential issues with input validation, encoding/decoding, + or expression evaluation. This could indicate vulnerabilities such as server-side template injection, + expression language injection, unicode normalization issues, or other input processing flaws that may be exploitable. +solution: > + Review input validation and sanitization mechanisms. Ensure user input is properly escaped and validated + before processing. Consider implementing strict input filtering to prevent injection attacks. +references: [] # Added dynamically based on the specific transformation detected +category: injection +risk: high +confidence: medium +cweId: 20 # CWE-20: Improper Input Validation +wascId: 20 # WASC-20: Improper Input Handling +alertTags: + ${CommonAlertTag.OWASP_2021_A03_INJECTION.getTag()}: ${CommonAlertTag.OWASP_2021_A03_INJECTION.getValue()} + ${CommonAlertTag.OWASP_2017_A01_INJECTION.getTag()}: ${CommonAlertTag.OWASP_2017_A01_INJECTION.getValue()} +status: alpha +codeLink: https://github.com/zaproxy/zap-extensions/blob/main/addOns/ascanrulesAlpha/src/main/zapHomeFiles/scripts/scripts/active/SuspiciousInputTransformation.js +helpLink: https://www.zaproxy.org/docs/desktop/addons/active-scan-rules-alpha/#id-100044 +`); +} + +function generateRandomString(length) { + return RandomStringUtils.secure().nextAlphanumeric(length); +} + +function generateRandomNumbers() { + const x = RandomUtils.secure().randomInt(99, 10000); + const y = RandomUtils.secure().randomInt(99, 10000); + return { x, y, product: x * y }; +} + +const inputTransformationChecks = [ + { + name: "Quote Consumption", + alertRef: SCAN_RULE_ID + "-1", + transformFunction: function (originalValue) { + const leftAnchor = generateRandomString(6); + const rightAnchor = generateRandomString(6); + return { + attackPayload: leftAnchor + "''" + rightAnchor, + expectedTransformedValues: [leftAnchor + "'" + rightAnchor], + }; + }, + references: [], + }, + { + name: "Arithmetic Evaluation", + alertRef: SCAN_RULE_ID + "-2", + transformFunction: function (originalValue) { + const nums = generateRandomNumbers(); + return { + attackPayload: nums.x + "*" + nums.y, + expectedTransformedValues: [nums.product.toString()], + }; + }, + references: [], + }, + { + name: "Expression Evaluation", + alertRef: SCAN_RULE_ID + "-3", + transformFunction: function (originalValue) { + const nums = generateRandomNumbers(); + return { + attackPayload: "${" + nums.x + "*" + nums.y + "}", + expectedTransformedValues: [nums.product.toString()], + }; + }, + references: [ + "https://portswigger.net/research/server-side-template-injection", + ], + }, + { + name: "Template Evaluation", + alertRef: SCAN_RULE_ID + "-4", + transformFunction: function (originalValue) { + const nums = generateRandomNumbers(); + return { + attackPayload: "@(" + nums.x + "*" + nums.y + ")", + expectedTransformedValues: [nums.product.toString()], + }; + }, + references: [ + "https://portswigger.net/research/server-side-template-injection", + ], + }, + { + name: "EL Evaluation", + alertRef: SCAN_RULE_ID + "-5", + transformFunction: function (originalValue) { + const nums = generateRandomNumbers(); + return { + attackPayload: "%{" + nums.x + "*" + nums.y + "}", + expectedTransformedValues: [nums.product.toString()], + }; + }, + references: [ + "https://portswigger.net/research/server-side-template-injection", + ], + }, + { + name: "Unicode Normalisation", + alertRef: SCAN_RULE_ID + "-6", + transformFunction: function (originalValue) { + const leftAnchor = generateRandomString(6); + const rightAnchor = generateRandomString(6); + return { + attackPayload: leftAnchor + "\u212a" + rightAnchor, + expectedTransformedValues: [leftAnchor + "K" + rightAnchor], + }; + }, + references: [ + "https://blog.orange.tw/posts/2025-01-worstfit-unveiling-hidden-transformers-in-windows-ansi", + ], + }, + { + name: "URL Decoding Error", + alertRef: SCAN_RULE_ID + "-7", + transformFunction: function (originalValue) { + const leftAnchor = generateRandomString(6); + const rightAnchor = generateRandomString(6); + return { + attackPayload: leftAnchor + "\u0391" + rightAnchor, + expectedTransformedValues: [leftAnchor + "N\u0011" + rightAnchor], + }; + }, + references: ["https://cwe.mitre.org/data/definitions/172.html"], + }, + { + name: "Unicode Byte Truncation", + alertRef: SCAN_RULE_ID + "-8", + transformFunction: function (originalValue) { + const leftAnchor = generateRandomString(6); + const rightAnchor = generateRandomString(6); + return { + attackPayload: leftAnchor + "\uCF7B" + rightAnchor, + expectedTransformedValues: [leftAnchor + "{" + rightAnchor], + }; + }, + references: [ + "https://portswigger.net/research/bypassing-character-blocklists-with-unicode-overflows", + ], + }, + { + name: "Unicode Case Conversion", + alertRef: SCAN_RULE_ID + "-9", + transformFunction: function (originalValue) { + const leftAnchor = generateRandomString(6); + const rightAnchor = generateRandomString(6); + return { + attackPayload: leftAnchor + "\u0131" + rightAnchor, + expectedTransformedValues: [leftAnchor + "I" + rightAnchor], + }; + }, + references: ["https://www.unicode.org/charts/case/index.html"], + }, + { + name: "Unicode Combining Diacritic", + alertRef: SCAN_RULE_ID + "-10", + transformFunction: function (originalValue) { + const rightAnchor = generateRandomString(6); + return { + attackPayload: "\u0338" + rightAnchor, + expectedTransformedValues: ["\u226F" + rightAnchor], + }; + }, + references: ["https://codepoints.net/combining_diacritical_marks?lang=en"], + }, +]; + +function scan(as, msg, param, value) { + const originalResponse = msg.getResponseBody().toString(); + const checksCount = + as.getAttackStrength() == "LOW" ? 6 : inputTransformationChecks.length; + + for (let i = 0; i < checksCount; i++) { + if (as.isStop()) { + return; + } + + const check = inputTransformationChecks[i]; + let confirmedTransformation = false; + + // Perform multiple attempts to confirm the transformation + for (let attempt = 0; attempt < CHECK_CONFIRM_COUNT; attempt++) { + if (as.isStop()) { + return; + } + + // Generate the attack payload and expected values + const transformFunctionResult = check.transformFunction(value); + const attackPayload = transformFunctionResult.attackPayload; + const expectedValues = transformFunctionResult.expectedTransformedValues; + + // Send the request with the attack payload + const testMsg = msg.cloneRequest(); + as.setParam(testMsg, param, attackPayload); + as.sendAndReceive(testMsg, false, false); + + // Check if the response contains any of the expected values + const attackResponse = testMsg.getResponseBody().toString(); + let responseContainsTransformedValue = false; + for (let j = 0; j < expectedValues.length; j++) { + const expectedValue = expectedValues[j]; + if ( + attackResponse.indexOf(expectedValue) !== -1 && + originalResponse.indexOf(expectedValue) === -1 + ) { + responseContainsTransformedValue = true; + if (attempt === CHECK_CONFIRM_COUNT - 1) { + // Response contained transformed value in all attempts, raise alert + as.newAlert() + .setName(`Suspicious Input Transformation: ${check.name}`) + .setAlertRef(check.alertRef) + .setParam(param) + .setAttack(attackPayload) + .setEvidence(expectedValue) + .setMessage(testMsg) + .setReference(check.references.join("\n")) + .raise(); + confirmedTransformation = true; + } + break; + } + } + if (!responseContainsTransformedValue) { + // Response does not contain any expected value, no need to retry for confirmation + break; + } + } + + if (confirmedTransformation) { + // Matched one check, unlikely that others will match too, so skip them + break; + } + } +} diff --git a/addOns/ascanrulesAlpha/src/test/java/org/zaproxy/zap/extension/ascanrulesAlpha/SuspiciousInputTransformationScriptTest.java b/addOns/ascanrulesAlpha/src/test/java/org/zaproxy/zap/extension/ascanrulesAlpha/SuspiciousInputTransformationScriptTest.java new file mode 100644 index 00000000000..a699b8192c6 --- /dev/null +++ b/addOns/ascanrulesAlpha/src/test/java/org/zaproxy/zap/extension/ascanrulesAlpha/SuspiciousInputTransformationScriptTest.java @@ -0,0 +1,409 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2025 The ZAP Development Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zaproxy.zap.extension.ascanrulesAlpha; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; + +import fi.iki.elonen.NanoHTTPD; +import fi.iki.elonen.NanoHTTPD.IHTTPSession; +import fi.iki.elonen.NanoHTTPD.Response; +import java.nio.file.Path; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.parosproxy.paros.core.scanner.Alert; +import org.parosproxy.paros.core.scanner.Category; +import org.parosproxy.paros.network.HttpMessage; +import org.zaproxy.addon.commonlib.CommonAlertTag; +import org.zaproxy.zap.control.AddOn; +import org.zaproxy.zap.extension.graaljs.GraalJsActiveScriptScanRuleTestUtils; +import org.zaproxy.zap.testutils.NanoServerHandler; + +class SuspiciousInputTransformationScriptTest extends GraalJsActiveScriptScanRuleTestUtils { + @Override + public Path getScriptPath() throws Exception { + return Path.of( + getClass() + .getResource("/scripts/scripts/active/SuspiciousInputTransformation.js") + .toURI()); + } + + @Test + void shouldReturnExpectedMappings() { + assertThat(rule.getId(), is(equalTo(100044))); + assertThat(rule.getName(), is(equalTo("Suspicious Input Transformation"))); + assertThat(rule.getCategory(), is(equalTo(Category.INJECTION))); + assertThat(rule.getRisk(), is(equalTo(Alert.RISK_HIGH))); + assertThat(rule.getCweId(), is(equalTo(20))); + assertThat(rule.getWascId(), is(equalTo(20))); + assertThat( + rule.getAlertTags().keySet(), + containsInAnyOrder( + CommonAlertTag.OWASP_2021_A03_INJECTION.getTag(), + CommonAlertTag.OWASP_2017_A01_INJECTION.getTag())); + assertThat(rule.getStatus(), is(equalTo(AddOn.Status.alpha))); + } + + @Test + void shouldGetExampleAlerts() { + // When + var exampleAlerts = rule.getExampleAlerts(); + // Then + assertThat(exampleAlerts, hasSize(1)); + Alert alert = exampleAlerts.get(0); + assertThat(alert.getPluginId(), is(equalTo(100044))); + assertThat(alert.getName(), containsString("Suspicious Input Transformation")); + assertThat(alert.getRisk(), is(equalTo(Alert.RISK_HIGH))); + assertThat(alert.getConfidence(), is(equalTo(Alert.CONFIDENCE_MEDIUM))); + assertThat(alert.getCweId(), is(equalTo(20))); + assertThat(alert.getWascId(), is(equalTo(20))); + assertThat( + alert.getTags().keySet(), + containsInAnyOrder( + CommonAlertTag.OWASP_2021_A03_INJECTION.getTag(), + CommonAlertTag.OWASP_2017_A01_INJECTION.getTag(), + "CWE-20")); + } + + @Test + void shouldRaiseAlertOnQuoteConsumption() throws Exception { + // Given + String path = "/quoteConsumption"; + nano.addHandler(new TransformationHandler(path)); + HttpMessage msg = getHttpMessage(path + "?param=value"); + rule.init(msg, parent); + // When + rule.scan(); + // Then + assertThat(alertsRaised, hasSize(1)); + Alert alert = alertsRaised.get(0); + assertThat(alert.getName(), containsString("Quote Consumption")); + assertThat(alert.getAlertRef(), is(equalTo("100044-1"))); + assertThat(alert.getParam(), is(equalTo("param"))); + } + + @Test + void shouldRaiseAlertOnArithmeticEvaluation() throws Exception { + // Given + String path = "/arithmeticEvaluation"; + nano.addHandler(new TransformationHandler(path)); + HttpMessage msg = getHttpMessage(path + "?param=value"); + rule.init(msg, parent); + // When + rule.scan(); + // Then + assertThat(alertsRaised, hasSize(1)); + Alert alert = alertsRaised.get(0); + assertThat(alert.getName(), containsString("Arithmetic Evaluation")); + assertThat(alert.getAlertRef(), is(equalTo("100044-2"))); + assertThat(alert.getParam(), is(equalTo("param"))); + } + + @Test + void shouldRaiseAlertOnExpressionEvaluation() throws Exception { + // Given + String path = "/expressionEvaluation"; + nano.addHandler(new TransformationHandler(path)); + HttpMessage msg = getHttpMessage(path + "?param=value"); + rule.init(msg, parent); + // When + rule.scan(); + // Then + assertThat(alertsRaised, hasSize(1)); + Alert alert = alertsRaised.get(0); + assertThat(alert.getName(), containsString("Expression Evaluation")); + assertThat(alert.getAlertRef(), is(equalTo("100044-3"))); + assertThat(alert.getParam(), is(equalTo("param"))); + } + + @Test + void shouldRaiseAlertOnTemplateEvaluation() throws Exception { + // Given + String path = "/templateEvaluation"; + nano.addHandler(new TransformationHandler(path)); + HttpMessage msg = getHttpMessage(path + "?param=value"); + rule.init(msg, parent); + // When + rule.scan(); + // Then + assertThat(alertsRaised, hasSize(1)); + Alert alert = alertsRaised.get(0); + assertThat(alert.getName(), containsString("Template Evaluation")); + assertThat(alert.getAlertRef(), is(equalTo("100044-4"))); + assertThat(alert.getParam(), is(equalTo("param"))); + } + + @Test + void shouldRaiseAlertOnElEvaluation() throws Exception { + // Given + String path = "/elEvaluation"; + nano.addHandler(new TransformationHandler(path)); + HttpMessage msg = getHttpMessage(path + "?param=value"); + rule.init(msg, parent); + // When + rule.scan(); + // Then + assertThat(alertsRaised, hasSize(1)); + Alert alert = alertsRaised.get(0); + assertThat(alert.getName(), containsString("EL Evaluation")); + assertThat(alert.getAlertRef(), is(equalTo("100044-5"))); + assertThat(alert.getParam(), is(equalTo("param"))); + } + + @Test + void shouldRaiseAlertOnUnicodeNormalisation() throws Exception { + // Given + String path = "/unicodeNormalisation"; + nano.addHandler(new TransformationHandler(path)); + HttpMessage msg = getHttpMessage(path + "?param=value"); + rule.init(msg, parent); + // When + rule.scan(); + // Then + assertThat(alertsRaised, hasSize(1)); + Alert alert = alertsRaised.get(0); + assertThat(alert.getName(), containsString("Unicode Normalisation")); + assertThat(alert.getAlertRef(), is(equalTo("100044-6"))); + assertThat(alert.getParam(), is(equalTo("param"))); + } + + @Test + void shouldRaiseAlertOnUrlDecodingError() throws Exception { + // Given + String path = "/urlDecodingError"; + nano.addHandler(new TransformationHandler(path)); + HttpMessage msg = getHttpMessage(path + "?param=value"); + rule.init(msg, parent); + // When + rule.scan(); + // Then + assertThat(alertsRaised, hasSize(1)); + Alert alert = alertsRaised.get(0); + assertThat(alert.getName(), containsString("URL Decoding Error")); + assertThat(alert.getAlertRef(), is(equalTo("100044-7"))); + assertThat(alert.getParam(), is(equalTo("param"))); + } + + @Test + void shouldRaiseAlertOnUnicodeByteTruncation() throws Exception { + // Given + String path = "/unicodeByteTruncation"; + nano.addHandler(new TransformationHandler(path)); + HttpMessage msg = getHttpMessage(path + "?param=value"); + rule.init(msg, parent); + // When + rule.scan(); + // Then + assertThat(alertsRaised, hasSize(1)); + Alert alert = alertsRaised.get(0); + assertThat(alert.getName(), containsString("Unicode Byte Truncation")); + assertThat(alert.getAlertRef(), is(equalTo("100044-8"))); + assertThat(alert.getParam(), is(equalTo("param"))); + } + + @Test + void shouldRaiseAlertOnUnicodeCaseConversion() throws Exception { + // Given + String path = "/unicodeCaseConversion"; + nano.addHandler(new TransformationHandler(path)); + HttpMessage msg = getHttpMessage(path + "?param=value"); + rule.init(msg, parent); + // When + rule.scan(); + // Then + assertThat(alertsRaised, hasSize(1)); + Alert alert = alertsRaised.get(0); + assertThat(alert.getName(), containsString("Unicode Case Conversion")); + assertThat(alert.getAlertRef(), is(equalTo("100044-9"))); + assertThat(alert.getParam(), is(equalTo("param"))); + } + + @Test + void shouldRaiseAlertOnUnicodeCombiningDiacritic() throws Exception { + // Given + String path = "/unicodeCombiningDiacritic"; + nano.addHandler(new TransformationHandler(path)); + HttpMessage msg = getHttpMessage(path + "?param=value"); + rule.init(msg, parent); + // When + rule.scan(); + // Then + assertThat(alertsRaised, hasSize(1)); + Alert alert = alertsRaised.get(0); + assertThat(alert.getName(), containsString("Unicode Combining Diacritic")); + assertThat(alert.getAlertRef(), is(equalTo("100044-10"))); + assertThat(alert.getParam(), is(equalTo("param"))); + } + + @Test + void shouldNotRaiseAlertWhenNoTransformation() throws Exception { + // Given + String path = "/noTransformation"; + nano.addHandler(new TransformationHandler(path)); + HttpMessage msg = getHttpMessage(path + "?param=value"); + rule.init(msg, parent); + // When + rule.scan(); + // Then + assertThat(alertsRaised, is(empty())); + } + + private static class TransformationHandler extends NanoServerHandler { + private static final String DEFAULT_RESPONSE_STRING = ""; + + public TransformationHandler(String path) { + super(path); + } + + @Override + protected Response serve(IHTTPSession session) { + String paramValue = getFirstParamValue(session, "param"); + String path = session.getUri(); + + return handleQuoteConsumption(path, paramValue) + .or(() -> handleArithmeticEvaluation(path, paramValue)) + .or(() -> handleExpressionEvaluation(path, paramValue)) + .or(() -> handleTemplateEvaluation(path, paramValue)) + .or(() -> handleUnicodeNormalisation(path, paramValue)) + .or(() -> handleElEvaluation(path, paramValue)) + .or(() -> handleUrlDecodingError(path, paramValue)) + .or(() -> handleUnicodeByteTruncation(path, paramValue)) + .or(() -> handleUnicodeCaseConversion(path, paramValue)) + .or(() -> handleUnicodeCombiningDiacritic(path, paramValue)) + .orElseGet( + () -> + NanoHTTPD.newFixedLengthResponse( + Response.Status.OK, + NanoHTTPD.MIME_HTML, + DEFAULT_RESPONSE_STRING)); + } + + private Optional handleQuoteConsumption(String path, String paramValue) { + if (path.equals("/quoteConsumption") && paramValue.contains("''")) { + String transformed = paramValue.replace("''", "'"); + return Optional.of(createSuccessResponse(transformed)); + } + return Optional.empty(); + } + + private Optional handleArithmeticEvaluation(String path, String paramValue) { + if (path.equals("/arithmeticEvaluation") && paramValue.contains("*")) { + return evaluateArithmetic(paramValue); + } + return Optional.empty(); + } + + private Optional handleExpressionEvaluation(String path, String paramValue) { + if (path.equals("/expressionEvaluation") + && paramValue.startsWith("${") + && paramValue.endsWith("}")) { + String expression = paramValue.substring(2, paramValue.length() - 1); + return evaluateArithmetic(expression); + } + return Optional.empty(); + } + + private Optional handleTemplateEvaluation(String path, String paramValue) { + if (path.equals("/templateEvaluation") + && paramValue.startsWith("@(") + && paramValue.endsWith(")")) { + String expression = paramValue.substring(2, paramValue.length() - 1); + return evaluateArithmetic(expression); + } + return Optional.empty(); + } + + private Optional handleUnicodeNormalisation(String path, String paramValue) { + if (path.equals("/unicodeNormalisation") && paramValue.contains("\u212a")) { + String transformed = paramValue.replace("\u212a", "K"); + return Optional.of(createSuccessResponse(transformed)); + } + return Optional.empty(); + } + + private Optional handleElEvaluation(String path, String paramValue) { + if (path.equals("/elEvaluation") + && paramValue.startsWith("%{") + && paramValue.endsWith("}")) { + String expression = paramValue.substring(2, paramValue.length() - 1); + return evaluateArithmetic(expression); + } + return Optional.empty(); + } + + private Optional handleUrlDecodingError(String path, String paramValue) { + if (path.equals("/urlDecodingError") && paramValue.contains("\u0391")) { + String transformed = paramValue.replace("\u0391", "N\u0011"); + return Optional.of(createSuccessResponse(transformed)); + } + return Optional.empty(); + } + + private Optional handleUnicodeByteTruncation(String path, String paramValue) { + if (path.equals("/unicodeByteTruncation") && paramValue.contains("\uCF7B")) { + String transformed = paramValue.replace("\uCF7B", "{"); + return Optional.of(createSuccessResponse(transformed)); + } + return Optional.empty(); + } + + private Optional handleUnicodeCaseConversion(String path, String paramValue) { + if (path.equals("/unicodeCaseConversion") && paramValue.contains("\u0131")) { + String transformed = paramValue.replace("\u0131", "I"); + return Optional.of(createSuccessResponse(transformed)); + } + return Optional.empty(); + } + + private Optional handleUnicodeCombiningDiacritic(String path, String paramValue) { + if (path.equals("/unicodeCombiningDiacritic") && paramValue.contains("\u0338")) { + String transformed = paramValue.replace("\u0338", "\u226F"); + return Optional.of(createSuccessResponse(transformed)); + } + return Optional.empty(); + } + + private Optional evaluateArithmetic(String expression) { + if (expression.contains("*")) { + String[] parts = expression.split("\\*"); + if (parts.length == 2) { + try { + int x = Integer.parseInt(parts[0]); + int y = Integer.parseInt(parts[1]); + return Optional.of(createSuccessResponse(String.valueOf(x * y))); + } catch (NumberFormatException e) { + // Ignore + } + } + } + return Optional.empty(); + } + + private Response createSuccessResponse(String content) { + return NanoHTTPD.newFixedLengthResponse( + Response.Status.OK, NanoHTTPD.MIME_HTML, content); + } + } +} diff --git a/addOns/graaljs/src/test/java/org/zaproxy/zap/extension/graaljs/ActiveDefaultTemplateGraalJsScriptTest.java b/addOns/graaljs/src/test/java/org/zaproxy/zap/extension/graaljs/ActiveDefaultTemplateGraalJsScriptTest.java index 6c80e9ccbed..0d85ef676c3 100644 --- a/addOns/graaljs/src/test/java/org/zaproxy/zap/extension/graaljs/ActiveDefaultTemplateGraalJsScriptTest.java +++ b/addOns/graaljs/src/test/java/org/zaproxy/zap/extension/graaljs/ActiveDefaultTemplateGraalJsScriptTest.java @@ -20,15 +20,12 @@ package org.zaproxy.zap.extension.graaljs; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.emptyOrNullString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; import java.nio.file.Path; import java.util.Map; -import java.util.ResourceBundle; import org.junit.jupiter.api.Test; import org.parosproxy.paros.core.scanner.Alert; import org.zaproxy.zap.testutils.AlertReferenceError; @@ -47,11 +44,6 @@ protected boolean isIgnoreAlertsRaisedInSendReasonableNumberOfMessages() { return true; } - @Override - public void shouldHaveI18nNonEmptyName(String name, ResourceBundle extensionResourceBundle) { - assertThat(name, is(not(emptyOrNullString()))); - } - @Override public boolean isAllowedReferenceError( AlertReferenceError.Cause cause, String reference, Object detail) { diff --git a/addOns/graaljs/src/test/java/org/zaproxy/zap/extension/graaljs/GraalJsActiveScriptScanRuleTestUtils.java b/addOns/graaljs/src/test/java/org/zaproxy/zap/extension/graaljs/GraalJsActiveScriptScanRuleTestUtils.java index be1ac6ac8cf..e44c1d1f39e 100644 --- a/addOns/graaljs/src/test/java/org/zaproxy/zap/extension/graaljs/GraalJsActiveScriptScanRuleTestUtils.java +++ b/addOns/graaljs/src/test/java/org/zaproxy/zap/extension/graaljs/GraalJsActiveScriptScanRuleTestUtils.java @@ -19,7 +19,13 @@ */ package org.zaproxy.zap.extension.graaljs; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.emptyOrNullString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + import java.util.List; +import java.util.ResourceBundle; import org.junit.jupiter.api.BeforeEach; import org.zaproxy.addon.commonlib.scanrules.ScanRuleMetadata; import org.zaproxy.addon.commonlib.scanrules.ScanRuleMetadataProvider; @@ -55,6 +61,11 @@ public void setUpMessages() { mockMessages(new ExtensionGraalJs()); } + @Override + public void shouldHaveI18nNonEmptyName(String name, ResourceBundle extensionResourceBundle) { + assertThat(name, is(not(emptyOrNullString()))); + } + @Override protected ActiveScriptScanRule createScanner() { try { diff --git a/addOns/graaljs/src/test/java/org/zaproxy/zap/extension/graaljs/GraalJsPassiveScriptScanRuleTestUtils.java b/addOns/graaljs/src/test/java/org/zaproxy/zap/extension/graaljs/GraalJsPassiveScriptScanRuleTestUtils.java index fb73620e024..94dbfc71813 100644 --- a/addOns/graaljs/src/test/java/org/zaproxy/zap/extension/graaljs/GraalJsPassiveScriptScanRuleTestUtils.java +++ b/addOns/graaljs/src/test/java/org/zaproxy/zap/extension/graaljs/GraalJsPassiveScriptScanRuleTestUtils.java @@ -19,9 +19,14 @@ */ package org.zaproxy.zap.extension.graaljs; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.emptyOrNullString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.mockito.Mockito.mock; import java.util.List; +import java.util.ResourceBundle; import org.junit.jupiter.api.BeforeEach; import org.zaproxy.addon.commonlib.scanrules.ScanRuleMetadataProvider; import org.zaproxy.zap.extension.script.ScriptEngineWrapper; @@ -54,6 +59,11 @@ public void setUpMessages() { mockMessages(new ExtensionGraalJs()); } + @Override + public void shouldHaveI18nNonEmptyName(String name, ResourceBundle extensionResourceBundle) { + assertThat(name, is(not(emptyOrNullString()))); + } + @Override protected PassiveScriptScanRule createScanner() { try { diff --git a/addOns/graaljs/src/test/java/org/zaproxy/zap/extension/graaljs/PassiveDefaultTemplateGraalJsScriptTest.java b/addOns/graaljs/src/test/java/org/zaproxy/zap/extension/graaljs/PassiveDefaultTemplateGraalJsScriptTest.java index 5f3f752f47c..8e5660e0c49 100644 --- a/addOns/graaljs/src/test/java/org/zaproxy/zap/extension/graaljs/PassiveDefaultTemplateGraalJsScriptTest.java +++ b/addOns/graaljs/src/test/java/org/zaproxy/zap/extension/graaljs/PassiveDefaultTemplateGraalJsScriptTest.java @@ -20,15 +20,12 @@ package org.zaproxy.zap.extension.graaljs; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.emptyOrNullString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; import java.nio.file.Path; import java.util.Map; -import java.util.ResourceBundle; import org.apache.commons.httpclient.URI; import org.junit.jupiter.api.Test; import org.parosproxy.paros.core.scanner.Alert; @@ -45,11 +42,6 @@ public Path getScriptPath() throws Exception { .toURI()); } - @Override - public void shouldHaveI18nNonEmptyName(String name, ResourceBundle extensionResourceBundle) { - assertThat(name, is(not(emptyOrNullString()))); - } - @Override public boolean isAllowedReferenceError( AlertReferenceError.Cause cause, String reference, Object detail) {