From eaa6e9e7bbf589c2b34bea4103f15ea9784bdd71 Mon Sep 17 00:00:00 2001 From: Aastha Sahni Date: Mon, 28 Jul 2025 11:30:14 -0400 Subject: [PATCH 1/7] Add Swagger Secret & Version Detector active scan rule Signed-off-by: Aastha Sahni --- active/swagger-secret-detector.js | 279 ++++++++++++++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 active/swagger-secret-detector.js diff --git a/active/swagger-secret-detector.js b/active/swagger-secret-detector.js new file mode 100644 index 00000000..d181e835 --- /dev/null +++ b/active/swagger-secret-detector.js @@ -0,0 +1,279 @@ +// Note that new active scripts will initially be disabled +// ------------------------------------------------------------------- +// Swagger Secrets & Version Detector - ZAP Active Scan Rule Script +// ------------------------------------------------------------------- +// Modern ZAP registration using getMetadata() function +// Import required ZAP Java types for modern registration + +var URI = Java.type('org.apache.commons.httpclient.URI'); +var ScanRuleMetadata = Java.type("org.zaproxy.addon.commonlib.scanrules.ScanRuleMetadata"); +var CommonAlertTag = Java.type("org.zaproxy.addon.commonlib.CommonAlertTag"); +function getMetadata() { +return ScanRuleMetadata.fromYaml(` +id: 100043 +name: Swagger UI Secret & Vulnerability Detector +description: > + Detects exposed Swagger UI and OpenAPI endpoints that leak sensitive secrets such as API keys, + OAuth client secrets, access tokens, or run vulnerable versions. This scanner performs comprehensive + detection of sensitive information disclosure in API documentation. +solution: > + Remove hardcoded secrets from API documentation, restrict access to API documentation endpoints, + and upgrade Swagger UI to a secure version. Ensure proper authentication is required to access documentation. +category: info_gather +risk: high +confidence: medium +cweId: 522 # Insufficiently Protected Credentials +alertTags: + ${CommonAlertTag.OWASP_2021_A05_SEC_MISCONFIG.getTag()}: ${CommonAlertTag.OWASP_2021_A05_SEC_MISCONFIG.getValue()} + ${CommonAlertTag.OWASP_2017_A06_SEC_MISCONFIG.getTag()}: ${CommonAlertTag.OWASP_2017_A06_SEC_MISCONFIG.getValue()} +status: alpha +codeLink: https://example.com/swagger-ui-detector.js +helpLink: https://www.example.com/ +`); +} + +// ------------------------------------------------------------------- +// 1. List of commonly exposed Swagger/OpenAPI documentation paths +// ------------------------------------------------------------------- +var SWAGGER_PATHS = [ + "/swagger", "/swagger/", "/swagger/index.html", "/swagger/ui", "/swagger/ui/", + "/swagger/ui/index", "/swagger/ui/index.html", "/swagger-ui", "/swagger-ui/", + "/swagger-ui/index.html", "/swagger-ui/index", "/docs", "/docs/", + "/api-docs", "/v2/api-docs", "/v3/api-docs", "/swagger.json", + "/swagger.yaml", "/openapi.json", "/openapi.yaml" +]; + +// ------------------------------------------------------------------- +// 2. Regex matchers for path filtering (more flexible than exact matches) +// ------------------------------------------------------------------- +var SWAGGER_REGEX_PATHS = [ + /\/swagger\/?$/i, + /\/swagger\/index\.html$/i, + /\/swagger\/ui\/?$/i, + /\/swagger\/ui\/index(\.html)?$/i, + /\/swagger-ui\/?$/i, + /\/swagger-ui\/index(\.html)?$/i, + /\/docs\/?$/i, + /\/api-docs$/i, + /\/v2\/api-docs$/i, + /\/v3\/api-docs$/i, + /\/swagger\.(json|yaml)$/i, + /\/openapi\.(json|yaml)$/i, + /\/api(\/v[0-9]+)?\/.*$/i, + /\/v[0-9]+\/swagger.*$/i, + /\/v[0-9]+\/openapi.*$/i, + /\/nswag\/?$/i, + /\/redoc\/?$/i, + /\/admin\/?$/i, + /\/config(\.json|\.yaml|\.yml|\.php)?$/i, + /\/debug(\.log|\.txt)?$/i, + /\/\.env$/i, + /\/\.git\/config$/i, + /\/login\/?$/i, + /\/signin\/?$/i, + /\/upload\/.*$/i, + /\/graphql$/i, + /\/graphiql$/i, + /\/phpinfo\.php$/i, + /\/server-status$/i, + /\/actuator\/.*$/i, + /\/\.git\/HEAD$/i, + /\/backup\.zip$/i, + /\/db\.sql$/i +]; + +// ------------------------------------------------------------------- +// 3. Regex patterns to detect likely secrets in Swagger responses +// ------------------------------------------------------------------- +var SECRET_REGEXES = [ + /["']?clientId["']?\s*:\s*["'](?!client_id|""|.{0,6}$).*?["']/gi, + /["']?clientSecret["']?\s*:\s*["'](?!client_secret|""|.{0,6}$).*?["']/gi, + /["']?oAuth2ClientId["']?\s*:\s*["'](?!client_id|""|.{0,6}$).*?["']/gi, + /["']?oAuth2ClientSecret["']?\s*:\s*["'](?!client_secret|""|.{0,6}$).*?["']/gi, + /["']?api_key["']?\s*:\s*["'](?!your_api_key_here|""|.{0,6}$).*?["']/gi, + /["']?access_token["']?\s*:\s*["'](?!""|.{0,6}$).*?["']/gi, + /["']?authorization["']?\s*:\s*["']Bearer\s+(?!""|.{0,6}$).*?["']/gi +]; + +// ------------------------------------------------------------------- +// 4. Known dummy/test values that should be ignored +// ------------------------------------------------------------------- +var FALSE_POSITIVES = [ + "clientid", "clientsecret", "string", "n/a", "null", "na", "true", "false", + "value_here", "your_key", "your_api_key_here", "demo_token", "test1234", + "dummysecret", "{token}", "bearer{token}", "placeholder", "insert_value" +]; + +// ------------------------------------------------------------------- +// 5. False positive filter: heuristic to skip known dummy/test data +// ------------------------------------------------------------------- +function isFalsePositiveKV(kvString) { + if (!kvString || kvString.length < 1) return true; + + var kvMatch = kvString.match(/["']?([^"']+)["']?\s*:\s*["']?([^"']+)["']?/); + if (!kvMatch || kvMatch.length < 3) return false; + + var key = kvMatch[1].toLowerCase().trim(); + var value = kvMatch[2].toLowerCase().trim(); + value = value.replace(/[\s"'{}]/g, ''); + + if (value.length < 8) return true; + + var contextKeys = ["example", "description", "title", "note"]; + for (var i = 0; i < contextKeys.length; i++) { + if (key.indexOf(contextKeys[i]) !== -1) return true; + } + + var junkTokens = ["test", "sample", "dummy", "mock", "try", "placeholder", "your", "insert"]; + for (var i = 0; i < junkTokens.length; i++) { + if (value.indexOf(junkTokens[i]) !== -1 || key.indexOf(junkTokens[i]) !== -1) return true; + } + + for (var i = 0; i < FALSE_POSITIVES.length; i++) { + if (value === FALSE_POSITIVES[i]) return true; + } + + return false; +} + +// ------------------------------------------------------------------- +// 6. Redact secret values in evidence (show only first 5 chars) +// ------------------------------------------------------------------- +function redactSecret(secret) { + var parts = secret.split(':'); + if (parts.length < 2) return secret; + var value = parts.slice(1).join(':').trim().replace(/^"|"$/g, ''); + return parts[0] + ': "' + value.substring(0, 5) + '..."'; +} + +// ------------------------------------------------------------------- +// 7. Detect Swagger UI version in HTML/JS +// ------------------------------------------------------------------- +function detectSwaggerVersion(body) { + if (body.indexOf('SwaggerUIBundle') !== -1) return 3; + if (body.indexOf('SwaggerUi') !== -1 || body.indexOf('window.swaggerUi') !== -1 || body.indexOf('swashbuckleConfig') !== -1) return 2; + if (body.indexOf('NSwag') !== -1 || body.indexOf('nswagui') !== -1) return 4; + return 0; +} + +function extractVersion(body) { + var versionRegex = /version\s*[:=]\s*["']?(\d+\.\d+\.\d+)["']?/i; + var match = body.match(versionRegex); + return match ? match[1] : null; +} + +function versionToInt(v) { + var parts = v.split("."); + return (parseInt(parts[0], 10) * 10000) + (parseInt(parts[1], 10) * 100) + parseInt(parts[2], 10); +} + +// ------------------------------------------------------------------- +// 8. Main scan logic: runs once per node +// ------------------------------------------------------------------- +function scanNode(as, msg) { + var origUri = msg.getRequestHeader().getURI(); + var scheme = origUri.getScheme(); + var host = origUri.getHost(); + var port = origUri.getPort(); + var base = scheme + "://" + host + ((port !== -1 && port !== 80 && port !== 443) ? ":" + port : ""); + + // --- Pass 1: Check static Swagger paths --- + for (var i = 0; i < SWAGGER_PATHS.length; i++) { + scanPath(as, msg, scheme, host, port, SWAGGER_PATHS[i], base + SWAGGER_PATHS[i]); + } + + // --- Pass 2: Check current request path if it matches any regex --- + var currentPath = origUri.getPath(); + for (var r = 0; r < SWAGGER_REGEX_PATHS.length; r++) { + if (SWAGGER_REGEX_PATHS[r].test(currentPath)) { + scanPath(as, msg, scheme, host, port, currentPath, base + currentPath); + } + } +} + +// ------------------------------------------------------------------- +// 9. Scan a single path (version + secret detection reused) +// ------------------------------------------------------------------- +function scanPath(as, origMsg, scheme, host, port, pathOnly, fullPath) { + var requestMsg = origMsg.cloneRequest(); + + try { + requestMsg.getRequestHeader().setMethod("GET"); + var newUri = new URI(scheme, null, host, port, pathOnly); + requestMsg.getRequestHeader().setURI(newUri); + requestMsg.getRequestHeader().setContentLength(0); + + var origHeaders = origMsg.getRequestHeader(); + ["User-Agent", "Cookie", "Authorization"].forEach(function (header) { + var val = origHeaders.getHeader(header); + if (val) requestMsg.getRequestHeader().setHeader(header, val); + }); + + as.sendAndReceive(requestMsg, false, false); + } catch (err) { + return; + } + + var body = requestMsg.getResponseBody().toString(); + var version = detectSwaggerVersion(body); + var semver = extractVersion(body); + + if (semver && (version === 2 || version === 3)) { + var vInt = versionToInt(semver); + if ((version === 2 && vInt < 20210) || (version === 3 && vInt < 32403)) { + var cveReference = (version === 2) + ? "https://nvd.nist.gov/vuln/detail/CVE-2019-17495" + : "https://github.com/swagger-api/swagger-ui/releases/tag/v3.24.3"; + + as.newAlert() + .setRisk(3) + .setConfidence(2) + .setName("Vulnerable Swagger UI Version Detected (v" + semver + ")") + .setAlertRef("100043-1") + .setDescription("This Swagger UI version is known to contain vulnerabilities. Exploitation may allow unauthorized access, XSS, or token theft.\n\nAffected versions:\n- Swagger UI v2 < 2.2.10\n- Swagger UI v3 < 3.24.3") + .setOtherInfo("Discovered at: " + fullPath) + .setSolution("Upgrade to the latest version of Swagger UI. Regularly review and patch known issues.") + .setReference(cveReference) + .setMessage(requestMsg) + .raise(); + } + } + + detectSecrets(as, requestMsg, fullPath, body); +} + +function detectSecrets(as, requestMsg, fullPath, body) { + var matches = {}; + for (var j = 0; j < SECRET_REGEXES.length; j++) { + var found = body.match(SECRET_REGEXES[j]); + if (found) { + for (var f = 0; f < found.length; f++) { + var match = found[f]; + if (!isFalsePositiveKV(match)) { + matches[match] = true; + } + } + } + } + + var evidenceRaw = Object.keys(matches); + var redactedEvidence = evidenceRaw.map(redactSecret); + // var evidenceString = redactedEvidence.length > 0 ? redactedEvidence[0] : null; + var foundClientId = evidenceRaw.some(e => /clientId/i.test(e)); + var foundSecret = evidenceRaw.some(e => /clientSecret|api_key|access_token|authorization/i.test(e)); + + if (foundClientId && foundSecret) { + as.newAlert() + .setRisk(3) + .setConfidence(2) + .setName("Exposed Secrets in Swagger/OpenAPI Path") + .setAlertRef("100043-2") + .setDescription("Swagger UI endpoint exposes sensitive secrets such as client secrets, API keys, or OAuth tokens. These secrets may be accessible in the HTML source and should not be exposed publicly, as this can lead to compromise.") + .setEvidence(redactedEvidence[0]) + .setOtherInfo("All secrets exposed:\n" + redactedEvidence.join("\n")) + .setSolution("Remove hardcoded secrets from documentation and ensure the endpoint is protected with authentication.") + .setReference("https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/") + .setMessage(requestMsg) + .raise(); + } +} \ No newline at end of file From 40a922380d6e5d0fbc5d1a2af1eb8c8aa3c97015 Mon Sep 17 00:00:00 2001 From: Aastha Sahni Date: Tue, 29 Jul 2025 13:14:37 -0400 Subject: [PATCH 2/7] I have removed any duplicate scripts and added updates to this one, which includes recent comments. Signed-off-by: Aastha Sahni --- active/swagger-secret-detector .js | 358 +++++++++++++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100644 active/swagger-secret-detector .js diff --git a/active/swagger-secret-detector .js b/active/swagger-secret-detector .js new file mode 100644 index 00000000..d478ceff --- /dev/null +++ b/active/swagger-secret-detector .js @@ -0,0 +1,358 @@ +// Note that new active scripts will initially be disabled +// ------------------------------------------------------------------- +// Swagger Secrets & Version Detector - ZAP Active Scan Rule Script +// ------------------------------------------------------------------- +// Modern ZAP registration using getMetadata() function +// Import required ZAP Java types for modern registration + +var URI = Java.type("org.apache.commons.httpclient.URI"); +var ScanRuleMetadata = Java.type( + "org.zaproxy.addon.commonlib.scanrules.ScanRuleMetadata" +); +var CommonAlertTag = Java.type("org.zaproxy.addon.commonlib.CommonAlertTag"); +function getMetadata() { + return ScanRuleMetadata.fromYaml(` +id: 100043 +name: Swagger UI Secret & Vulnerability Detector +description: > + Detects exposed Swagger UI and OpenAPI endpoints that leak sensitive secrets such as API keys, + OAuth client secrets, access tokens, or run vulnerable versions. This scanner performs comprehensive + detection of sensitive information disclosure in API documentation. +solution: > + Remove hardcoded secrets from API documentation, restrict access to API documentation endpoints, + and upgrade Swagger UI to a secure version. Ensure proper authentication is required to access documentation. +category: info_gather +risk: high +confidence: medium +cweId: 522 # Insufficiently Protected Credentials +alertTags: + ${CommonAlertTag.OWASP_2021_A05_SEC_MISCONFIG.getTag()}: ${CommonAlertTag.OWASP_2021_A05_SEC_MISCONFIG.getValue()} + ${CommonAlertTag.OWASP_2017_A06_SEC_MISCONFIG.getTag()}: ${CommonAlertTag.OWASP_2017_A06_SEC_MISCONFIG.getValue()} +status: alpha +codeLink: https://example.com/swagger-ui-detector.js +helpLink: https://www.example.com/ +`); +} + +// ------------------------------------------------------------------- +// 1. List of commonly exposed Swagger/OpenAPI documentation paths +// ------------------------------------------------------------------- +var SWAGGER_PATHS = [ + "/swagger", + "/swagger/", + "/swagger/index.html", + "/swagger/ui", + "/swagger/ui/", + "/swagger/ui/index", + "/swagger/ui/index.html", + "/swagger-ui", + "/swagger-ui/", + "/swagger-ui/index.html", + "/swagger-ui/index", + "/docs", + "/docs/", + "/api-docs", + "/v2/api-docs", + "/v3/api-docs", + "/swagger.json", + "/swagger.yaml", + "/openapi.json", + "/openapi.yaml", +]; + +// ------------------------------------------------------------------- +// 2. Regex matchers for path filtering (more flexible than exact matches) +// ------------------------------------------------------------------- +var SWAGGER_REGEX_PATHS = [ + /\/swagger\/?$/i, + /\/swagger\/index\.html$/i, + /\/swagger\/ui\/?$/i, + /\/swagger\/ui\/index(\.html)?$/i, + /\/swagger-ui\/?$/i, + /\/swagger-ui\/index(\.html)?$/i, + /\/docs\/?$/i, + /\/api-docs$/i, + /\/v2\/api-docs$/i, + /\/v3\/api-docs$/i, + /\/swagger\.(json|yaml)$/i, + /\/openapi\.(json|yaml)$/i, + /\/api(\/v[0-9]+)?\/.*$/i, + /\/v[0-9]+\/swagger.*$/i, + /\/v[0-9]+\/openapi.*$/i, + /\/nswag\/?$/i, + /\/redoc\/?$/i, + /\/admin\/?$/i, + /\/config(\.json|\.yaml|\.yml|\.php)?$/i, + /\/debug(\.log|\.txt)?$/i, + /\/\.env$/i, + /\/\.git\/config$/i, + /\/login\/?$/i, + /\/signin\/?$/i, + /\/upload\/.*$/i, + /\/graphql$/i, + /\/graphiql$/i, + /\/phpinfo\.php$/i, + /\/server-status$/i, + /\/actuator\/.*$/i, + /\/\.git\/HEAD$/i, + /\/backup\.zip$/i, + /\/db\.sql$/i, +]; + +// ------------------------------------------------------------------- +// 3. Regex patterns to detect likely secrets in Swagger responses +// ------------------------------------------------------------------- +var SECRET_REGEXES = [ + /["']?clientId["']?\s*:\s*["'](?!client_id|""|.{0,6}$).*?["']/gi, + /["']?clientSecret["']?\s*:\s*["'](?!client_secret|""|.{0,6}$).*?["']/gi, + /["']?oAuth2ClientId["']?\s*:\s*["'](?!client_id|""|.{0,6}$).*?["']/gi, + /["']?oAuth2ClientSecret["']?\s*:\s*["'](?!client_secret|""|.{0,6}$).*?["']/gi, + /["']?api_key["']?\s*:\s*["'](?!your_api_key_here|""|.{0,6}$).*?["']/gi, + /["']?access_token["']?\s*:\s*["'](?!""|.{0,6}$).*?["']/gi, + /["']?authorization["']?\s*:\s*["']Bearer\s+(?!""|.{0,6}$).*?["']/gi, +]; + +// ------------------------------------------------------------------- +// 4. Known dummy/test values that should be ignored +// ------------------------------------------------------------------- +var FALSE_POSITIVES = [ + "clientid", + "clientsecret", + "string", + "n/a", + "null", + "na", + "true", + "false", + "value_here", + "your_key", + "your_api_key_here", + "demo_token", + "test1234", + "dummysecret", + "{token}", + "bearer{token}", + "placeholder", + "insert_value", +]; + +// ------------------------------------------------------------------- +// 5. False positive filter: heuristic to skip known dummy/test data +// ------------------------------------------------------------------- +function isFalsePositiveKV(kvString) { + if (!kvString || kvString.length < 1) return true; + + var kvMatch = kvString.match(/["']?([^"']+)["']?\s*:\s*["']?([^"']+)["']?/); + if (!kvMatch || kvMatch.length < 3) return false; + + var key = kvMatch[1].toLowerCase().trim(); + var value = kvMatch[2].toLowerCase().trim(); + value = value.replace(/[\s"'{}]/g, ""); + + if (value.length < 8) return true; + + var contextKeys = ["example", "description", "title", "note"]; + for (var i = 0; i < contextKeys.length; i++) { + if (key.indexOf(contextKeys[i]) !== -1) return true; + } + + var junkTokens = [ + "test", + "sample", + "dummy", + "mock", + "try", + "placeholder", + "your", + "insert", + ]; + for (var i = 0; i < junkTokens.length; i++) { + if ( + value.indexOf(junkTokens[i]) !== -1 || + key.indexOf(junkTokens[i]) !== -1 + ) + return true; + } + + for (var i = 0; i < FALSE_POSITIVES.length; i++) { + if (value === FALSE_POSITIVES[i]) return true; + } + + return false; +} + +// ------------------------------------------------------------------- +// 6. Redact secret values in evidence (show only first 5 chars) +// ------------------------------------------------------------------- +function redactSecret(secret) { + var parts = secret.split(":"); + if (parts.length < 2) return secret; + var value = parts.slice(1).join(":").trim().replace(/^"|"$/g, ""); + return parts[0] + ': "' + value.substring(0, 5) + '..."'; +} + +// ------------------------------------------------------------------- +// 7. Detect Swagger UI version in HTML/JS +// ------------------------------------------------------------------- +function detectSwaggerVersion(body) { + if (body.indexOf("SwaggerUIBundle") !== -1) return 3; + if ( + body.indexOf("SwaggerUi") !== -1 || + body.indexOf("window.swaggerUi") !== -1 || + body.indexOf("swashbuckleConfig") !== -1 + ) + return 2; + if (body.indexOf("NSwag") !== -1 || body.indexOf("nswagui") !== -1) return 4; + return 0; +} + +function extractVersion(body) { + var versionRegex = /version\s*[:=]\s*["']?(\d+\.\d+\.\d+)["']?/i; + var match = body.match(versionRegex); + return match ? match[1] : null; +} + +function versionToInt(v) { + var parts = v.split("."); + return ( + parseInt(parts[0], 10) * 10000 + + parseInt(parts[1], 10) * 100 + + parseInt(parts[2], 10) + ); +} + +// ------------------------------------------------------------------- +// 8. Main scan logic: runs once per node +// ------------------------------------------------------------------- +function scanNode(as, msg) { + var origUri = msg.getRequestHeader().getURI(); + var scheme = origUri.getScheme(); + var host = origUri.getHost(); + var port = origUri.getPort(); + var base = + scheme + + "://" + + host + + (port !== -1 && port !== 80 && port !== 443 ? ":" + port : ""); + + // --- Pass 1: Check static Swagger paths --- + for (var i = 0; i < SWAGGER_PATHS.length; i++) { + if (as.isStop()) return; // ← inserted check + scanPath( + as, + msg, + scheme, + host, + port, + SWAGGER_PATHS[i], + base + SWAGGER_PATHS[i] + ); + } + + // --- Pass 2: Check current request path if it matches any regex --- + var currentPath = origUri.getPath(); + for (var r = 0; r < SWAGGER_REGEX_PATHS.length; r++) { + if (as.isStop()) return; // ← inserted check + if (SWAGGER_REGEX_PATHS[r].test(currentPath)) { + scanPath(as, msg, scheme, host, port, currentPath, base + currentPath); + } + } +} + +// ------------------------------------------------------------------- +// 9. Scan a single path (version + secret detection reused) +// ------------------------------------------------------------------- +function scanPath(as, origMsg, scheme, host, port, pathOnly, fullPath) { + var requestMsg = origMsg.cloneRequest(); + + try { + requestMsg.getRequestHeader().setMethod("GET"); + var newUri = new URI(scheme, null, host, port, pathOnly); + requestMsg.getRequestHeader().setURI(newUri); + requestMsg.getRequestHeader().setContentLength(0); + + var origHeaders = origMsg.getRequestHeader(); + ["User-Agent", "Cookie", "Authorization"].forEach(function (header) { + var val = origHeaders.getHeader(header); + if (val) requestMsg.getRequestHeader().setHeader(header, val); + }); + + as.sendAndReceive(requestMsg, false, false); + } catch (err) { + return; + } + + var body = requestMsg.getResponseBody().toString(); + var version = detectSwaggerVersion(body); + var semver = extractVersion(body); + + if (semver && (version === 2 || version === 3)) { + var vInt = versionToInt(semver); + if ((version === 2 && vInt < 20210) || (version === 3 && vInt < 32403)) { + var cveReference = + version === 2 + ? "https://nvd.nist.gov/vuln/detail/CVE-2019-17495" + : "https://github.com/swagger-api/swagger-ui/releases/tag/v3.24.3"; + + as.newAlert() + .setName("Vulnerable Swagger UI Version Detected (v" + semver + ")") + .setAlertRef("100043-1") + .setDescription( + "This Swagger UI version is known to contain vulnerabilities. Exploitation may allow unauthorized access, XSS, or token theft.\n\nAffected versions:\n- Swagger UI v2 < 2.2.10\n- Swagger UI v3 < 3.24.3" + ) + .setOtherInfo("Discovered at: " + fullPath) + .setSolution( + "Upgrade to the latest version of Swagger UI. Regularly review and patch known issues." + ) + .setReference(cveReference) + .setMessage(requestMsg) + .raise(); + } + } + + detectSecrets(as, requestMsg, fullPath, body); +} + +function detectSecrets(as, requestMsg, fullPath, body) { + var matches = {}; + for (var j = 0; j < SECRET_REGEXES.length; j++) { + var found = body.match(SECRET_REGEXES[j]); + if (found) { + for (var f = 0; f < found.length; f++) { + var match = found[f]; + if (!isFalsePositiveKV(match)) { + matches[match] = true; + } + } + } + } + + var evidenceRaw = Object.keys(matches); + var redactedEvidence = evidenceRaw.map(redactSecret); + // var evidenceString = redactedEvidence.length > 0 ? redactedEvidence[0] : null; + var foundClientId = evidenceRaw.some((e) => /clientId/i.test(e)); + var foundSecret = evidenceRaw.some((e) => + /clientSecret|api_key|access_token|authorization/i.test(e) + ); + + if (foundClientId && foundSecret) { + as.newAlert() + .setName("Exposed Secrets in Swagger/OpenAPI Path") + .setAlertRef("100043-2") + .setDescription( + "Swagger UI endpoint exposes sensitive secrets such as client secrets, API keys, or OAuth tokens. These secrets may be accessible in the HTML source and should not be exposed publicly, as this can lead to compromise." + ) + // .setEvidence(redactedEvidence.join("\n")) + .setEvidence(redactedEvidence[0]) + .setOtherInfo("All secrets exposed:\n" + redactedEvidence.join("\n")) + //.setOtherInfo("Discovered at: " + fullPath) + .setSolution( + "Remove hardcoded secrets from documentation and ensure the endpoint is protected with authentication." + ) + .setReference( + "https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/" + ) + .setMessage(requestMsg) + .raise(); + } +} From 0de188d28d4b2761a936f21d316edfdaf28d9ed2 Mon Sep 17 00:00:00 2001 From: Aastha Sahni Date: Tue, 29 Jul 2025 15:14:19 -0400 Subject: [PATCH 3/7] Remove outdated Swagger secret detector script Signed-off-by: Aastha Sahni --- active/swagger-secret-detector.js | 279 ------------------------------ 1 file changed, 279 deletions(-) delete mode 100644 active/swagger-secret-detector.js diff --git a/active/swagger-secret-detector.js b/active/swagger-secret-detector.js deleted file mode 100644 index d181e835..00000000 --- a/active/swagger-secret-detector.js +++ /dev/null @@ -1,279 +0,0 @@ -// Note that new active scripts will initially be disabled -// ------------------------------------------------------------------- -// Swagger Secrets & Version Detector - ZAP Active Scan Rule Script -// ------------------------------------------------------------------- -// Modern ZAP registration using getMetadata() function -// Import required ZAP Java types for modern registration - -var URI = Java.type('org.apache.commons.httpclient.URI'); -var ScanRuleMetadata = Java.type("org.zaproxy.addon.commonlib.scanrules.ScanRuleMetadata"); -var CommonAlertTag = Java.type("org.zaproxy.addon.commonlib.CommonAlertTag"); -function getMetadata() { -return ScanRuleMetadata.fromYaml(` -id: 100043 -name: Swagger UI Secret & Vulnerability Detector -description: > - Detects exposed Swagger UI and OpenAPI endpoints that leak sensitive secrets such as API keys, - OAuth client secrets, access tokens, or run vulnerable versions. This scanner performs comprehensive - detection of sensitive information disclosure in API documentation. -solution: > - Remove hardcoded secrets from API documentation, restrict access to API documentation endpoints, - and upgrade Swagger UI to a secure version. Ensure proper authentication is required to access documentation. -category: info_gather -risk: high -confidence: medium -cweId: 522 # Insufficiently Protected Credentials -alertTags: - ${CommonAlertTag.OWASP_2021_A05_SEC_MISCONFIG.getTag()}: ${CommonAlertTag.OWASP_2021_A05_SEC_MISCONFIG.getValue()} - ${CommonAlertTag.OWASP_2017_A06_SEC_MISCONFIG.getTag()}: ${CommonAlertTag.OWASP_2017_A06_SEC_MISCONFIG.getValue()} -status: alpha -codeLink: https://example.com/swagger-ui-detector.js -helpLink: https://www.example.com/ -`); -} - -// ------------------------------------------------------------------- -// 1. List of commonly exposed Swagger/OpenAPI documentation paths -// ------------------------------------------------------------------- -var SWAGGER_PATHS = [ - "/swagger", "/swagger/", "/swagger/index.html", "/swagger/ui", "/swagger/ui/", - "/swagger/ui/index", "/swagger/ui/index.html", "/swagger-ui", "/swagger-ui/", - "/swagger-ui/index.html", "/swagger-ui/index", "/docs", "/docs/", - "/api-docs", "/v2/api-docs", "/v3/api-docs", "/swagger.json", - "/swagger.yaml", "/openapi.json", "/openapi.yaml" -]; - -// ------------------------------------------------------------------- -// 2. Regex matchers for path filtering (more flexible than exact matches) -// ------------------------------------------------------------------- -var SWAGGER_REGEX_PATHS = [ - /\/swagger\/?$/i, - /\/swagger\/index\.html$/i, - /\/swagger\/ui\/?$/i, - /\/swagger\/ui\/index(\.html)?$/i, - /\/swagger-ui\/?$/i, - /\/swagger-ui\/index(\.html)?$/i, - /\/docs\/?$/i, - /\/api-docs$/i, - /\/v2\/api-docs$/i, - /\/v3\/api-docs$/i, - /\/swagger\.(json|yaml)$/i, - /\/openapi\.(json|yaml)$/i, - /\/api(\/v[0-9]+)?\/.*$/i, - /\/v[0-9]+\/swagger.*$/i, - /\/v[0-9]+\/openapi.*$/i, - /\/nswag\/?$/i, - /\/redoc\/?$/i, - /\/admin\/?$/i, - /\/config(\.json|\.yaml|\.yml|\.php)?$/i, - /\/debug(\.log|\.txt)?$/i, - /\/\.env$/i, - /\/\.git\/config$/i, - /\/login\/?$/i, - /\/signin\/?$/i, - /\/upload\/.*$/i, - /\/graphql$/i, - /\/graphiql$/i, - /\/phpinfo\.php$/i, - /\/server-status$/i, - /\/actuator\/.*$/i, - /\/\.git\/HEAD$/i, - /\/backup\.zip$/i, - /\/db\.sql$/i -]; - -// ------------------------------------------------------------------- -// 3. Regex patterns to detect likely secrets in Swagger responses -// ------------------------------------------------------------------- -var SECRET_REGEXES = [ - /["']?clientId["']?\s*:\s*["'](?!client_id|""|.{0,6}$).*?["']/gi, - /["']?clientSecret["']?\s*:\s*["'](?!client_secret|""|.{0,6}$).*?["']/gi, - /["']?oAuth2ClientId["']?\s*:\s*["'](?!client_id|""|.{0,6}$).*?["']/gi, - /["']?oAuth2ClientSecret["']?\s*:\s*["'](?!client_secret|""|.{0,6}$).*?["']/gi, - /["']?api_key["']?\s*:\s*["'](?!your_api_key_here|""|.{0,6}$).*?["']/gi, - /["']?access_token["']?\s*:\s*["'](?!""|.{0,6}$).*?["']/gi, - /["']?authorization["']?\s*:\s*["']Bearer\s+(?!""|.{0,6}$).*?["']/gi -]; - -// ------------------------------------------------------------------- -// 4. Known dummy/test values that should be ignored -// ------------------------------------------------------------------- -var FALSE_POSITIVES = [ - "clientid", "clientsecret", "string", "n/a", "null", "na", "true", "false", - "value_here", "your_key", "your_api_key_here", "demo_token", "test1234", - "dummysecret", "{token}", "bearer{token}", "placeholder", "insert_value" -]; - -// ------------------------------------------------------------------- -// 5. False positive filter: heuristic to skip known dummy/test data -// ------------------------------------------------------------------- -function isFalsePositiveKV(kvString) { - if (!kvString || kvString.length < 1) return true; - - var kvMatch = kvString.match(/["']?([^"']+)["']?\s*:\s*["']?([^"']+)["']?/); - if (!kvMatch || kvMatch.length < 3) return false; - - var key = kvMatch[1].toLowerCase().trim(); - var value = kvMatch[2].toLowerCase().trim(); - value = value.replace(/[\s"'{}]/g, ''); - - if (value.length < 8) return true; - - var contextKeys = ["example", "description", "title", "note"]; - for (var i = 0; i < contextKeys.length; i++) { - if (key.indexOf(contextKeys[i]) !== -1) return true; - } - - var junkTokens = ["test", "sample", "dummy", "mock", "try", "placeholder", "your", "insert"]; - for (var i = 0; i < junkTokens.length; i++) { - if (value.indexOf(junkTokens[i]) !== -1 || key.indexOf(junkTokens[i]) !== -1) return true; - } - - for (var i = 0; i < FALSE_POSITIVES.length; i++) { - if (value === FALSE_POSITIVES[i]) return true; - } - - return false; -} - -// ------------------------------------------------------------------- -// 6. Redact secret values in evidence (show only first 5 chars) -// ------------------------------------------------------------------- -function redactSecret(secret) { - var parts = secret.split(':'); - if (parts.length < 2) return secret; - var value = parts.slice(1).join(':').trim().replace(/^"|"$/g, ''); - return parts[0] + ': "' + value.substring(0, 5) + '..."'; -} - -// ------------------------------------------------------------------- -// 7. Detect Swagger UI version in HTML/JS -// ------------------------------------------------------------------- -function detectSwaggerVersion(body) { - if (body.indexOf('SwaggerUIBundle') !== -1) return 3; - if (body.indexOf('SwaggerUi') !== -1 || body.indexOf('window.swaggerUi') !== -1 || body.indexOf('swashbuckleConfig') !== -1) return 2; - if (body.indexOf('NSwag') !== -1 || body.indexOf('nswagui') !== -1) return 4; - return 0; -} - -function extractVersion(body) { - var versionRegex = /version\s*[:=]\s*["']?(\d+\.\d+\.\d+)["']?/i; - var match = body.match(versionRegex); - return match ? match[1] : null; -} - -function versionToInt(v) { - var parts = v.split("."); - return (parseInt(parts[0], 10) * 10000) + (parseInt(parts[1], 10) * 100) + parseInt(parts[2], 10); -} - -// ------------------------------------------------------------------- -// 8. Main scan logic: runs once per node -// ------------------------------------------------------------------- -function scanNode(as, msg) { - var origUri = msg.getRequestHeader().getURI(); - var scheme = origUri.getScheme(); - var host = origUri.getHost(); - var port = origUri.getPort(); - var base = scheme + "://" + host + ((port !== -1 && port !== 80 && port !== 443) ? ":" + port : ""); - - // --- Pass 1: Check static Swagger paths --- - for (var i = 0; i < SWAGGER_PATHS.length; i++) { - scanPath(as, msg, scheme, host, port, SWAGGER_PATHS[i], base + SWAGGER_PATHS[i]); - } - - // --- Pass 2: Check current request path if it matches any regex --- - var currentPath = origUri.getPath(); - for (var r = 0; r < SWAGGER_REGEX_PATHS.length; r++) { - if (SWAGGER_REGEX_PATHS[r].test(currentPath)) { - scanPath(as, msg, scheme, host, port, currentPath, base + currentPath); - } - } -} - -// ------------------------------------------------------------------- -// 9. Scan a single path (version + secret detection reused) -// ------------------------------------------------------------------- -function scanPath(as, origMsg, scheme, host, port, pathOnly, fullPath) { - var requestMsg = origMsg.cloneRequest(); - - try { - requestMsg.getRequestHeader().setMethod("GET"); - var newUri = new URI(scheme, null, host, port, pathOnly); - requestMsg.getRequestHeader().setURI(newUri); - requestMsg.getRequestHeader().setContentLength(0); - - var origHeaders = origMsg.getRequestHeader(); - ["User-Agent", "Cookie", "Authorization"].forEach(function (header) { - var val = origHeaders.getHeader(header); - if (val) requestMsg.getRequestHeader().setHeader(header, val); - }); - - as.sendAndReceive(requestMsg, false, false); - } catch (err) { - return; - } - - var body = requestMsg.getResponseBody().toString(); - var version = detectSwaggerVersion(body); - var semver = extractVersion(body); - - if (semver && (version === 2 || version === 3)) { - var vInt = versionToInt(semver); - if ((version === 2 && vInt < 20210) || (version === 3 && vInt < 32403)) { - var cveReference = (version === 2) - ? "https://nvd.nist.gov/vuln/detail/CVE-2019-17495" - : "https://github.com/swagger-api/swagger-ui/releases/tag/v3.24.3"; - - as.newAlert() - .setRisk(3) - .setConfidence(2) - .setName("Vulnerable Swagger UI Version Detected (v" + semver + ")") - .setAlertRef("100043-1") - .setDescription("This Swagger UI version is known to contain vulnerabilities. Exploitation may allow unauthorized access, XSS, or token theft.\n\nAffected versions:\n- Swagger UI v2 < 2.2.10\n- Swagger UI v3 < 3.24.3") - .setOtherInfo("Discovered at: " + fullPath) - .setSolution("Upgrade to the latest version of Swagger UI. Regularly review and patch known issues.") - .setReference(cveReference) - .setMessage(requestMsg) - .raise(); - } - } - - detectSecrets(as, requestMsg, fullPath, body); -} - -function detectSecrets(as, requestMsg, fullPath, body) { - var matches = {}; - for (var j = 0; j < SECRET_REGEXES.length; j++) { - var found = body.match(SECRET_REGEXES[j]); - if (found) { - for (var f = 0; f < found.length; f++) { - var match = found[f]; - if (!isFalsePositiveKV(match)) { - matches[match] = true; - } - } - } - } - - var evidenceRaw = Object.keys(matches); - var redactedEvidence = evidenceRaw.map(redactSecret); - // var evidenceString = redactedEvidence.length > 0 ? redactedEvidence[0] : null; - var foundClientId = evidenceRaw.some(e => /clientId/i.test(e)); - var foundSecret = evidenceRaw.some(e => /clientSecret|api_key|access_token|authorization/i.test(e)); - - if (foundClientId && foundSecret) { - as.newAlert() - .setRisk(3) - .setConfidence(2) - .setName("Exposed Secrets in Swagger/OpenAPI Path") - .setAlertRef("100043-2") - .setDescription("Swagger UI endpoint exposes sensitive secrets such as client secrets, API keys, or OAuth tokens. These secrets may be accessible in the HTML source and should not be exposed publicly, as this can lead to compromise.") - .setEvidence(redactedEvidence[0]) - .setOtherInfo("All secrets exposed:\n" + redactedEvidence.join("\n")) - .setSolution("Remove hardcoded secrets from documentation and ensure the endpoint is protected with authentication.") - .setReference("https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/") - .setMessage(requestMsg) - .raise(); - } -} \ No newline at end of file From 24d9f9e88fd951ee2d0c2244e5744d66c88ec102 Mon Sep 17 00:00:00 2001 From: Aastha Sahni Date: Tue, 29 Jul 2025 17:44:32 -0400 Subject: [PATCH 4/7] Removed extra comments and added the correct codel ink and help link Signed-off-by: Aastha Sahni --- active/swagger-secret-detector .js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/active/swagger-secret-detector .js b/active/swagger-secret-detector .js index d478ceff..e2664505 100644 --- a/active/swagger-secret-detector .js +++ b/active/swagger-secret-detector .js @@ -2,9 +2,6 @@ // ------------------------------------------------------------------- // Swagger Secrets & Version Detector - ZAP Active Scan Rule Script // ------------------------------------------------------------------- -// Modern ZAP registration using getMetadata() function -// Import required ZAP Java types for modern registration - var URI = Java.type("org.apache.commons.httpclient.URI"); var ScanRuleMetadata = Java.type( "org.zaproxy.addon.commonlib.scanrules.ScanRuleMetadata" @@ -29,8 +26,8 @@ alertTags: ${CommonAlertTag.OWASP_2021_A05_SEC_MISCONFIG.getTag()}: ${CommonAlertTag.OWASP_2021_A05_SEC_MISCONFIG.getValue()} ${CommonAlertTag.OWASP_2017_A06_SEC_MISCONFIG.getTag()}: ${CommonAlertTag.OWASP_2017_A06_SEC_MISCONFIG.getValue()} status: alpha -codeLink: https://example.com/swagger-ui-detector.js -helpLink: https://www.example.com/ +codeLink: https://github.com/zaproxy/community-scripts/blob/main/active/swagger-secret-detector.js +helpLink: https://www.zaproxy.org/docs/desktop/addons/community-scripts/ `); } @@ -342,10 +339,8 @@ function detectSecrets(as, requestMsg, fullPath, body) { .setDescription( "Swagger UI endpoint exposes sensitive secrets such as client secrets, API keys, or OAuth tokens. These secrets may be accessible in the HTML source and should not be exposed publicly, as this can lead to compromise." ) - // .setEvidence(redactedEvidence.join("\n")) .setEvidence(redactedEvidence[0]) .setOtherInfo("All secrets exposed:\n" + redactedEvidence.join("\n")) - //.setOtherInfo("Discovered at: " + fullPath) .setSolution( "Remove hardcoded secrets from documentation and ensure the endpoint is protected with authentication." ) @@ -355,4 +350,4 @@ function detectSecrets(as, requestMsg, fullPath, body) { .setMessage(requestMsg) .raise(); } -} +} \ No newline at end of file From e8cebcfc645f217b208e7411dff6c010ad7da26b Mon Sep 17 00:00:00 2001 From: Aastha Sahni Date: Wed, 30 Jul 2025 10:29:26 -0400 Subject: [PATCH 5/7] Added newline character after last brace, to fix gradlew violation. and remove extra space from file name. Signed-off-by: Aastha Sahni --- .../{swagger-secret-detector .js => swagger-secret-detector.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename active/{swagger-secret-detector .js => swagger-secret-detector.js} (99%) diff --git a/active/swagger-secret-detector .js b/active/swagger-secret-detector.js similarity index 99% rename from active/swagger-secret-detector .js rename to active/swagger-secret-detector.js index e2664505..d5ec9c95 100644 --- a/active/swagger-secret-detector .js +++ b/active/swagger-secret-detector.js @@ -350,4 +350,4 @@ function detectSecrets(as, requestMsg, fullPath, body) { .setMessage(requestMsg) .raise(); } -} \ No newline at end of file +} From 9efcc2f1d268fc9fed67da43e3b67bf98c711c5b Mon Sep 17 00:00:00 2001 From: Aastha Sahni Date: Wed, 30 Jul 2025 12:46:54 -0400 Subject: [PATCH 6/7] Rename The script name with Pascal Case Signed-off-by: Aastha Sahni --- active/SwaggerSecretDetector.js | 353 ++++++++++++++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 active/SwaggerSecretDetector.js diff --git a/active/SwaggerSecretDetector.js b/active/SwaggerSecretDetector.js new file mode 100644 index 00000000..d5ec9c95 --- /dev/null +++ b/active/SwaggerSecretDetector.js @@ -0,0 +1,353 @@ +// Note that new active scripts will initially be disabled +// ------------------------------------------------------------------- +// Swagger Secrets & Version Detector - ZAP Active Scan Rule Script +// ------------------------------------------------------------------- +var URI = Java.type("org.apache.commons.httpclient.URI"); +var ScanRuleMetadata = Java.type( + "org.zaproxy.addon.commonlib.scanrules.ScanRuleMetadata" +); +var CommonAlertTag = Java.type("org.zaproxy.addon.commonlib.CommonAlertTag"); +function getMetadata() { + return ScanRuleMetadata.fromYaml(` +id: 100043 +name: Swagger UI Secret & Vulnerability Detector +description: > + Detects exposed Swagger UI and OpenAPI endpoints that leak sensitive secrets such as API keys, + OAuth client secrets, access tokens, or run vulnerable versions. This scanner performs comprehensive + detection of sensitive information disclosure in API documentation. +solution: > + Remove hardcoded secrets from API documentation, restrict access to API documentation endpoints, + and upgrade Swagger UI to a secure version. Ensure proper authentication is required to access documentation. +category: info_gather +risk: high +confidence: medium +cweId: 522 # Insufficiently Protected Credentials +alertTags: + ${CommonAlertTag.OWASP_2021_A05_SEC_MISCONFIG.getTag()}: ${CommonAlertTag.OWASP_2021_A05_SEC_MISCONFIG.getValue()} + ${CommonAlertTag.OWASP_2017_A06_SEC_MISCONFIG.getTag()}: ${CommonAlertTag.OWASP_2017_A06_SEC_MISCONFIG.getValue()} +status: alpha +codeLink: https://github.com/zaproxy/community-scripts/blob/main/active/swagger-secret-detector.js +helpLink: https://www.zaproxy.org/docs/desktop/addons/community-scripts/ +`); +} + +// ------------------------------------------------------------------- +// 1. List of commonly exposed Swagger/OpenAPI documentation paths +// ------------------------------------------------------------------- +var SWAGGER_PATHS = [ + "/swagger", + "/swagger/", + "/swagger/index.html", + "/swagger/ui", + "/swagger/ui/", + "/swagger/ui/index", + "/swagger/ui/index.html", + "/swagger-ui", + "/swagger-ui/", + "/swagger-ui/index.html", + "/swagger-ui/index", + "/docs", + "/docs/", + "/api-docs", + "/v2/api-docs", + "/v3/api-docs", + "/swagger.json", + "/swagger.yaml", + "/openapi.json", + "/openapi.yaml", +]; + +// ------------------------------------------------------------------- +// 2. Regex matchers for path filtering (more flexible than exact matches) +// ------------------------------------------------------------------- +var SWAGGER_REGEX_PATHS = [ + /\/swagger\/?$/i, + /\/swagger\/index\.html$/i, + /\/swagger\/ui\/?$/i, + /\/swagger\/ui\/index(\.html)?$/i, + /\/swagger-ui\/?$/i, + /\/swagger-ui\/index(\.html)?$/i, + /\/docs\/?$/i, + /\/api-docs$/i, + /\/v2\/api-docs$/i, + /\/v3\/api-docs$/i, + /\/swagger\.(json|yaml)$/i, + /\/openapi\.(json|yaml)$/i, + /\/api(\/v[0-9]+)?\/.*$/i, + /\/v[0-9]+\/swagger.*$/i, + /\/v[0-9]+\/openapi.*$/i, + /\/nswag\/?$/i, + /\/redoc\/?$/i, + /\/admin\/?$/i, + /\/config(\.json|\.yaml|\.yml|\.php)?$/i, + /\/debug(\.log|\.txt)?$/i, + /\/\.env$/i, + /\/\.git\/config$/i, + /\/login\/?$/i, + /\/signin\/?$/i, + /\/upload\/.*$/i, + /\/graphql$/i, + /\/graphiql$/i, + /\/phpinfo\.php$/i, + /\/server-status$/i, + /\/actuator\/.*$/i, + /\/\.git\/HEAD$/i, + /\/backup\.zip$/i, + /\/db\.sql$/i, +]; + +// ------------------------------------------------------------------- +// 3. Regex patterns to detect likely secrets in Swagger responses +// ------------------------------------------------------------------- +var SECRET_REGEXES = [ + /["']?clientId["']?\s*:\s*["'](?!client_id|""|.{0,6}$).*?["']/gi, + /["']?clientSecret["']?\s*:\s*["'](?!client_secret|""|.{0,6}$).*?["']/gi, + /["']?oAuth2ClientId["']?\s*:\s*["'](?!client_id|""|.{0,6}$).*?["']/gi, + /["']?oAuth2ClientSecret["']?\s*:\s*["'](?!client_secret|""|.{0,6}$).*?["']/gi, + /["']?api_key["']?\s*:\s*["'](?!your_api_key_here|""|.{0,6}$).*?["']/gi, + /["']?access_token["']?\s*:\s*["'](?!""|.{0,6}$).*?["']/gi, + /["']?authorization["']?\s*:\s*["']Bearer\s+(?!""|.{0,6}$).*?["']/gi, +]; + +// ------------------------------------------------------------------- +// 4. Known dummy/test values that should be ignored +// ------------------------------------------------------------------- +var FALSE_POSITIVES = [ + "clientid", + "clientsecret", + "string", + "n/a", + "null", + "na", + "true", + "false", + "value_here", + "your_key", + "your_api_key_here", + "demo_token", + "test1234", + "dummysecret", + "{token}", + "bearer{token}", + "placeholder", + "insert_value", +]; + +// ------------------------------------------------------------------- +// 5. False positive filter: heuristic to skip known dummy/test data +// ------------------------------------------------------------------- +function isFalsePositiveKV(kvString) { + if (!kvString || kvString.length < 1) return true; + + var kvMatch = kvString.match(/["']?([^"']+)["']?\s*:\s*["']?([^"']+)["']?/); + if (!kvMatch || kvMatch.length < 3) return false; + + var key = kvMatch[1].toLowerCase().trim(); + var value = kvMatch[2].toLowerCase().trim(); + value = value.replace(/[\s"'{}]/g, ""); + + if (value.length < 8) return true; + + var contextKeys = ["example", "description", "title", "note"]; + for (var i = 0; i < contextKeys.length; i++) { + if (key.indexOf(contextKeys[i]) !== -1) return true; + } + + var junkTokens = [ + "test", + "sample", + "dummy", + "mock", + "try", + "placeholder", + "your", + "insert", + ]; + for (var i = 0; i < junkTokens.length; i++) { + if ( + value.indexOf(junkTokens[i]) !== -1 || + key.indexOf(junkTokens[i]) !== -1 + ) + return true; + } + + for (var i = 0; i < FALSE_POSITIVES.length; i++) { + if (value === FALSE_POSITIVES[i]) return true; + } + + return false; +} + +// ------------------------------------------------------------------- +// 6. Redact secret values in evidence (show only first 5 chars) +// ------------------------------------------------------------------- +function redactSecret(secret) { + var parts = secret.split(":"); + if (parts.length < 2) return secret; + var value = parts.slice(1).join(":").trim().replace(/^"|"$/g, ""); + return parts[0] + ': "' + value.substring(0, 5) + '..."'; +} + +// ------------------------------------------------------------------- +// 7. Detect Swagger UI version in HTML/JS +// ------------------------------------------------------------------- +function detectSwaggerVersion(body) { + if (body.indexOf("SwaggerUIBundle") !== -1) return 3; + if ( + body.indexOf("SwaggerUi") !== -1 || + body.indexOf("window.swaggerUi") !== -1 || + body.indexOf("swashbuckleConfig") !== -1 + ) + return 2; + if (body.indexOf("NSwag") !== -1 || body.indexOf("nswagui") !== -1) return 4; + return 0; +} + +function extractVersion(body) { + var versionRegex = /version\s*[:=]\s*["']?(\d+\.\d+\.\d+)["']?/i; + var match = body.match(versionRegex); + return match ? match[1] : null; +} + +function versionToInt(v) { + var parts = v.split("."); + return ( + parseInt(parts[0], 10) * 10000 + + parseInt(parts[1], 10) * 100 + + parseInt(parts[2], 10) + ); +} + +// ------------------------------------------------------------------- +// 8. Main scan logic: runs once per node +// ------------------------------------------------------------------- +function scanNode(as, msg) { + var origUri = msg.getRequestHeader().getURI(); + var scheme = origUri.getScheme(); + var host = origUri.getHost(); + var port = origUri.getPort(); + var base = + scheme + + "://" + + host + + (port !== -1 && port !== 80 && port !== 443 ? ":" + port : ""); + + // --- Pass 1: Check static Swagger paths --- + for (var i = 0; i < SWAGGER_PATHS.length; i++) { + if (as.isStop()) return; // ← inserted check + scanPath( + as, + msg, + scheme, + host, + port, + SWAGGER_PATHS[i], + base + SWAGGER_PATHS[i] + ); + } + + // --- Pass 2: Check current request path if it matches any regex --- + var currentPath = origUri.getPath(); + for (var r = 0; r < SWAGGER_REGEX_PATHS.length; r++) { + if (as.isStop()) return; // ← inserted check + if (SWAGGER_REGEX_PATHS[r].test(currentPath)) { + scanPath(as, msg, scheme, host, port, currentPath, base + currentPath); + } + } +} + +// ------------------------------------------------------------------- +// 9. Scan a single path (version + secret detection reused) +// ------------------------------------------------------------------- +function scanPath(as, origMsg, scheme, host, port, pathOnly, fullPath) { + var requestMsg = origMsg.cloneRequest(); + + try { + requestMsg.getRequestHeader().setMethod("GET"); + var newUri = new URI(scheme, null, host, port, pathOnly); + requestMsg.getRequestHeader().setURI(newUri); + requestMsg.getRequestHeader().setContentLength(0); + + var origHeaders = origMsg.getRequestHeader(); + ["User-Agent", "Cookie", "Authorization"].forEach(function (header) { + var val = origHeaders.getHeader(header); + if (val) requestMsg.getRequestHeader().setHeader(header, val); + }); + + as.sendAndReceive(requestMsg, false, false); + } catch (err) { + return; + } + + var body = requestMsg.getResponseBody().toString(); + var version = detectSwaggerVersion(body); + var semver = extractVersion(body); + + if (semver && (version === 2 || version === 3)) { + var vInt = versionToInt(semver); + if ((version === 2 && vInt < 20210) || (version === 3 && vInt < 32403)) { + var cveReference = + version === 2 + ? "https://nvd.nist.gov/vuln/detail/CVE-2019-17495" + : "https://github.com/swagger-api/swagger-ui/releases/tag/v3.24.3"; + + as.newAlert() + .setName("Vulnerable Swagger UI Version Detected (v" + semver + ")") + .setAlertRef("100043-1") + .setDescription( + "This Swagger UI version is known to contain vulnerabilities. Exploitation may allow unauthorized access, XSS, or token theft.\n\nAffected versions:\n- Swagger UI v2 < 2.2.10\n- Swagger UI v3 < 3.24.3" + ) + .setOtherInfo("Discovered at: " + fullPath) + .setSolution( + "Upgrade to the latest version of Swagger UI. Regularly review and patch known issues." + ) + .setReference(cveReference) + .setMessage(requestMsg) + .raise(); + } + } + + detectSecrets(as, requestMsg, fullPath, body); +} + +function detectSecrets(as, requestMsg, fullPath, body) { + var matches = {}; + for (var j = 0; j < SECRET_REGEXES.length; j++) { + var found = body.match(SECRET_REGEXES[j]); + if (found) { + for (var f = 0; f < found.length; f++) { + var match = found[f]; + if (!isFalsePositiveKV(match)) { + matches[match] = true; + } + } + } + } + + var evidenceRaw = Object.keys(matches); + var redactedEvidence = evidenceRaw.map(redactSecret); + // var evidenceString = redactedEvidence.length > 0 ? redactedEvidence[0] : null; + var foundClientId = evidenceRaw.some((e) => /clientId/i.test(e)); + var foundSecret = evidenceRaw.some((e) => + /clientSecret|api_key|access_token|authorization/i.test(e) + ); + + if (foundClientId && foundSecret) { + as.newAlert() + .setName("Exposed Secrets in Swagger/OpenAPI Path") + .setAlertRef("100043-2") + .setDescription( + "Swagger UI endpoint exposes sensitive secrets such as client secrets, API keys, or OAuth tokens. These secrets may be accessible in the HTML source and should not be exposed publicly, as this can lead to compromise." + ) + .setEvidence(redactedEvidence[0]) + .setOtherInfo("All secrets exposed:\n" + redactedEvidence.join("\n")) + .setSolution( + "Remove hardcoded secrets from documentation and ensure the endpoint is protected with authentication." + ) + .setReference( + "https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/" + ) + .setMessage(requestMsg) + .raise(); + } +} From 43a5c13851da70411f8c201961f14ed61510fd49 Mon Sep 17 00:00:00 2001 From: Aastha Sahni Date: Wed, 30 Jul 2025 18:00:17 -0400 Subject: [PATCH 7/7] Added Changelog and deleted duplicate file. Signed-off-by: Aastha Sahni --- CHANGELOG.md | 1 + active/swagger-secret-detector.js | 353 ------------------------------ 2 files changed, 1 insertion(+), 353 deletions(-) delete mode 100644 active/swagger-secret-detector.js diff --git a/CHANGELOG.md b/CHANGELOG.md index e59c74c5..b41177c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Standalone script 'PrivateMethodAccess.js' - Variant script 'AddUrlParams.js' - Extender script 'ScanMonitor.js' +- Active script 'SwaggerSecretDetector.js' ### Changed - Update minimum ZAP version to 2.16.0 and compile with Java 17. diff --git a/active/swagger-secret-detector.js b/active/swagger-secret-detector.js deleted file mode 100644 index d5ec9c95..00000000 --- a/active/swagger-secret-detector.js +++ /dev/null @@ -1,353 +0,0 @@ -// Note that new active scripts will initially be disabled -// ------------------------------------------------------------------- -// Swagger Secrets & Version Detector - ZAP Active Scan Rule Script -// ------------------------------------------------------------------- -var URI = Java.type("org.apache.commons.httpclient.URI"); -var ScanRuleMetadata = Java.type( - "org.zaproxy.addon.commonlib.scanrules.ScanRuleMetadata" -); -var CommonAlertTag = Java.type("org.zaproxy.addon.commonlib.CommonAlertTag"); -function getMetadata() { - return ScanRuleMetadata.fromYaml(` -id: 100043 -name: Swagger UI Secret & Vulnerability Detector -description: > - Detects exposed Swagger UI and OpenAPI endpoints that leak sensitive secrets such as API keys, - OAuth client secrets, access tokens, or run vulnerable versions. This scanner performs comprehensive - detection of sensitive information disclosure in API documentation. -solution: > - Remove hardcoded secrets from API documentation, restrict access to API documentation endpoints, - and upgrade Swagger UI to a secure version. Ensure proper authentication is required to access documentation. -category: info_gather -risk: high -confidence: medium -cweId: 522 # Insufficiently Protected Credentials -alertTags: - ${CommonAlertTag.OWASP_2021_A05_SEC_MISCONFIG.getTag()}: ${CommonAlertTag.OWASP_2021_A05_SEC_MISCONFIG.getValue()} - ${CommonAlertTag.OWASP_2017_A06_SEC_MISCONFIG.getTag()}: ${CommonAlertTag.OWASP_2017_A06_SEC_MISCONFIG.getValue()} -status: alpha -codeLink: https://github.com/zaproxy/community-scripts/blob/main/active/swagger-secret-detector.js -helpLink: https://www.zaproxy.org/docs/desktop/addons/community-scripts/ -`); -} - -// ------------------------------------------------------------------- -// 1. List of commonly exposed Swagger/OpenAPI documentation paths -// ------------------------------------------------------------------- -var SWAGGER_PATHS = [ - "/swagger", - "/swagger/", - "/swagger/index.html", - "/swagger/ui", - "/swagger/ui/", - "/swagger/ui/index", - "/swagger/ui/index.html", - "/swagger-ui", - "/swagger-ui/", - "/swagger-ui/index.html", - "/swagger-ui/index", - "/docs", - "/docs/", - "/api-docs", - "/v2/api-docs", - "/v3/api-docs", - "/swagger.json", - "/swagger.yaml", - "/openapi.json", - "/openapi.yaml", -]; - -// ------------------------------------------------------------------- -// 2. Regex matchers for path filtering (more flexible than exact matches) -// ------------------------------------------------------------------- -var SWAGGER_REGEX_PATHS = [ - /\/swagger\/?$/i, - /\/swagger\/index\.html$/i, - /\/swagger\/ui\/?$/i, - /\/swagger\/ui\/index(\.html)?$/i, - /\/swagger-ui\/?$/i, - /\/swagger-ui\/index(\.html)?$/i, - /\/docs\/?$/i, - /\/api-docs$/i, - /\/v2\/api-docs$/i, - /\/v3\/api-docs$/i, - /\/swagger\.(json|yaml)$/i, - /\/openapi\.(json|yaml)$/i, - /\/api(\/v[0-9]+)?\/.*$/i, - /\/v[0-9]+\/swagger.*$/i, - /\/v[0-9]+\/openapi.*$/i, - /\/nswag\/?$/i, - /\/redoc\/?$/i, - /\/admin\/?$/i, - /\/config(\.json|\.yaml|\.yml|\.php)?$/i, - /\/debug(\.log|\.txt)?$/i, - /\/\.env$/i, - /\/\.git\/config$/i, - /\/login\/?$/i, - /\/signin\/?$/i, - /\/upload\/.*$/i, - /\/graphql$/i, - /\/graphiql$/i, - /\/phpinfo\.php$/i, - /\/server-status$/i, - /\/actuator\/.*$/i, - /\/\.git\/HEAD$/i, - /\/backup\.zip$/i, - /\/db\.sql$/i, -]; - -// ------------------------------------------------------------------- -// 3. Regex patterns to detect likely secrets in Swagger responses -// ------------------------------------------------------------------- -var SECRET_REGEXES = [ - /["']?clientId["']?\s*:\s*["'](?!client_id|""|.{0,6}$).*?["']/gi, - /["']?clientSecret["']?\s*:\s*["'](?!client_secret|""|.{0,6}$).*?["']/gi, - /["']?oAuth2ClientId["']?\s*:\s*["'](?!client_id|""|.{0,6}$).*?["']/gi, - /["']?oAuth2ClientSecret["']?\s*:\s*["'](?!client_secret|""|.{0,6}$).*?["']/gi, - /["']?api_key["']?\s*:\s*["'](?!your_api_key_here|""|.{0,6}$).*?["']/gi, - /["']?access_token["']?\s*:\s*["'](?!""|.{0,6}$).*?["']/gi, - /["']?authorization["']?\s*:\s*["']Bearer\s+(?!""|.{0,6}$).*?["']/gi, -]; - -// ------------------------------------------------------------------- -// 4. Known dummy/test values that should be ignored -// ------------------------------------------------------------------- -var FALSE_POSITIVES = [ - "clientid", - "clientsecret", - "string", - "n/a", - "null", - "na", - "true", - "false", - "value_here", - "your_key", - "your_api_key_here", - "demo_token", - "test1234", - "dummysecret", - "{token}", - "bearer{token}", - "placeholder", - "insert_value", -]; - -// ------------------------------------------------------------------- -// 5. False positive filter: heuristic to skip known dummy/test data -// ------------------------------------------------------------------- -function isFalsePositiveKV(kvString) { - if (!kvString || kvString.length < 1) return true; - - var kvMatch = kvString.match(/["']?([^"']+)["']?\s*:\s*["']?([^"']+)["']?/); - if (!kvMatch || kvMatch.length < 3) return false; - - var key = kvMatch[1].toLowerCase().trim(); - var value = kvMatch[2].toLowerCase().trim(); - value = value.replace(/[\s"'{}]/g, ""); - - if (value.length < 8) return true; - - var contextKeys = ["example", "description", "title", "note"]; - for (var i = 0; i < contextKeys.length; i++) { - if (key.indexOf(contextKeys[i]) !== -1) return true; - } - - var junkTokens = [ - "test", - "sample", - "dummy", - "mock", - "try", - "placeholder", - "your", - "insert", - ]; - for (var i = 0; i < junkTokens.length; i++) { - if ( - value.indexOf(junkTokens[i]) !== -1 || - key.indexOf(junkTokens[i]) !== -1 - ) - return true; - } - - for (var i = 0; i < FALSE_POSITIVES.length; i++) { - if (value === FALSE_POSITIVES[i]) return true; - } - - return false; -} - -// ------------------------------------------------------------------- -// 6. Redact secret values in evidence (show only first 5 chars) -// ------------------------------------------------------------------- -function redactSecret(secret) { - var parts = secret.split(":"); - if (parts.length < 2) return secret; - var value = parts.slice(1).join(":").trim().replace(/^"|"$/g, ""); - return parts[0] + ': "' + value.substring(0, 5) + '..."'; -} - -// ------------------------------------------------------------------- -// 7. Detect Swagger UI version in HTML/JS -// ------------------------------------------------------------------- -function detectSwaggerVersion(body) { - if (body.indexOf("SwaggerUIBundle") !== -1) return 3; - if ( - body.indexOf("SwaggerUi") !== -1 || - body.indexOf("window.swaggerUi") !== -1 || - body.indexOf("swashbuckleConfig") !== -1 - ) - return 2; - if (body.indexOf("NSwag") !== -1 || body.indexOf("nswagui") !== -1) return 4; - return 0; -} - -function extractVersion(body) { - var versionRegex = /version\s*[:=]\s*["']?(\d+\.\d+\.\d+)["']?/i; - var match = body.match(versionRegex); - return match ? match[1] : null; -} - -function versionToInt(v) { - var parts = v.split("."); - return ( - parseInt(parts[0], 10) * 10000 + - parseInt(parts[1], 10) * 100 + - parseInt(parts[2], 10) - ); -} - -// ------------------------------------------------------------------- -// 8. Main scan logic: runs once per node -// ------------------------------------------------------------------- -function scanNode(as, msg) { - var origUri = msg.getRequestHeader().getURI(); - var scheme = origUri.getScheme(); - var host = origUri.getHost(); - var port = origUri.getPort(); - var base = - scheme + - "://" + - host + - (port !== -1 && port !== 80 && port !== 443 ? ":" + port : ""); - - // --- Pass 1: Check static Swagger paths --- - for (var i = 0; i < SWAGGER_PATHS.length; i++) { - if (as.isStop()) return; // ← inserted check - scanPath( - as, - msg, - scheme, - host, - port, - SWAGGER_PATHS[i], - base + SWAGGER_PATHS[i] - ); - } - - // --- Pass 2: Check current request path if it matches any regex --- - var currentPath = origUri.getPath(); - for (var r = 0; r < SWAGGER_REGEX_PATHS.length; r++) { - if (as.isStop()) return; // ← inserted check - if (SWAGGER_REGEX_PATHS[r].test(currentPath)) { - scanPath(as, msg, scheme, host, port, currentPath, base + currentPath); - } - } -} - -// ------------------------------------------------------------------- -// 9. Scan a single path (version + secret detection reused) -// ------------------------------------------------------------------- -function scanPath(as, origMsg, scheme, host, port, pathOnly, fullPath) { - var requestMsg = origMsg.cloneRequest(); - - try { - requestMsg.getRequestHeader().setMethod("GET"); - var newUri = new URI(scheme, null, host, port, pathOnly); - requestMsg.getRequestHeader().setURI(newUri); - requestMsg.getRequestHeader().setContentLength(0); - - var origHeaders = origMsg.getRequestHeader(); - ["User-Agent", "Cookie", "Authorization"].forEach(function (header) { - var val = origHeaders.getHeader(header); - if (val) requestMsg.getRequestHeader().setHeader(header, val); - }); - - as.sendAndReceive(requestMsg, false, false); - } catch (err) { - return; - } - - var body = requestMsg.getResponseBody().toString(); - var version = detectSwaggerVersion(body); - var semver = extractVersion(body); - - if (semver && (version === 2 || version === 3)) { - var vInt = versionToInt(semver); - if ((version === 2 && vInt < 20210) || (version === 3 && vInt < 32403)) { - var cveReference = - version === 2 - ? "https://nvd.nist.gov/vuln/detail/CVE-2019-17495" - : "https://github.com/swagger-api/swagger-ui/releases/tag/v3.24.3"; - - as.newAlert() - .setName("Vulnerable Swagger UI Version Detected (v" + semver + ")") - .setAlertRef("100043-1") - .setDescription( - "This Swagger UI version is known to contain vulnerabilities. Exploitation may allow unauthorized access, XSS, or token theft.\n\nAffected versions:\n- Swagger UI v2 < 2.2.10\n- Swagger UI v3 < 3.24.3" - ) - .setOtherInfo("Discovered at: " + fullPath) - .setSolution( - "Upgrade to the latest version of Swagger UI. Regularly review and patch known issues." - ) - .setReference(cveReference) - .setMessage(requestMsg) - .raise(); - } - } - - detectSecrets(as, requestMsg, fullPath, body); -} - -function detectSecrets(as, requestMsg, fullPath, body) { - var matches = {}; - for (var j = 0; j < SECRET_REGEXES.length; j++) { - var found = body.match(SECRET_REGEXES[j]); - if (found) { - for (var f = 0; f < found.length; f++) { - var match = found[f]; - if (!isFalsePositiveKV(match)) { - matches[match] = true; - } - } - } - } - - var evidenceRaw = Object.keys(matches); - var redactedEvidence = evidenceRaw.map(redactSecret); - // var evidenceString = redactedEvidence.length > 0 ? redactedEvidence[0] : null; - var foundClientId = evidenceRaw.some((e) => /clientId/i.test(e)); - var foundSecret = evidenceRaw.some((e) => - /clientSecret|api_key|access_token|authorization/i.test(e) - ); - - if (foundClientId && foundSecret) { - as.newAlert() - .setName("Exposed Secrets in Swagger/OpenAPI Path") - .setAlertRef("100043-2") - .setDescription( - "Swagger UI endpoint exposes sensitive secrets such as client secrets, API keys, or OAuth tokens. These secrets may be accessible in the HTML source and should not be exposed publicly, as this can lead to compromise." - ) - .setEvidence(redactedEvidence[0]) - .setOtherInfo("All secrets exposed:\n" + redactedEvidence.join("\n")) - .setSolution( - "Remove hardcoded secrets from documentation and ensure the endpoint is protected with authentication." - ) - .setReference( - "https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/" - ) - .setMessage(requestMsg) - .raise(); - } -}