-
-
Notifications
You must be signed in to change notification settings - Fork 254
Add Swagger Secret & Version Detector active scan rule #487
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
thc202
merged 7 commits into
zaproxy:main
from
aasthasahni9:add-swagger-secret-detector
Aug 11, 2025
Merged
Changes from 5 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
eaa6e9e
Add Swagger Secret & Version Detector active scan rule
40a9223
I have removed any duplicate scripts and added updates to this one, w…
0de188d
Remove outdated Swagger secret detector script
aasthasahni9 24d9f9e
Removed extra comments and added the correct codel ink and help link
aasthasahni9 e8cebcf
Added newline character after last brace, to fix gradlew violation. a…
aasthasahni9 9efcc2f
Rename The script name with Pascal Case
aasthasahni9 43a5c13
Added Changelog and deleted duplicate file.
aasthasahni9 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| } | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.