Skip to content

Commit 6ab2761

Browse files
author
Aastha Sahni
committed
Add Swagger Secret & Version Detector active scan rule
Signed-off-by: Aastha Sahni <[email protected]>
1 parent 951c881 commit 6ab2761

File tree

1 file changed

+282
-0
lines changed

1 file changed

+282
-0
lines changed

active/swagger-secret-detector.js

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
// Note that new active scripts will initially be disabled
2+
// -------------------------------------------------------------------
3+
// Swagger Secrets & Version Detector - ZAP Active Scan Rule Script
4+
// -------------------------------------------------------------------
5+
// Modern ZAP registration using getMetadata() function
6+
// Import required ZAP Java types for modern registration
7+
8+
var URI = Java.type('org.apache.commons.httpclient.URI');
9+
var ScanRuleMetadata = Java.type("org.zaproxy.addon.commonlib.scanrules.ScanRuleMetadata");
10+
var CommonAlertTag = Java.type("org.zaproxy.addon.commonlib.CommonAlertTag");
11+
function getMetadata() {
12+
return ScanRuleMetadata.fromYaml(`
13+
id: 100001
14+
name: Swagger UI Secret & Vulnerability Detector
15+
description: >
16+
Detects exposed Swagger UI and OpenAPI endpoints that leak sensitive secrets such as API keys,
17+
OAuth client secrets, access tokens, or run vulnerable versions. This scanner performs comprehensive
18+
detection of sensitive information disclosure in API documentation.
19+
solution: >
20+
Remove hardcoded secrets from API documentation, restrict access to API documentation endpoints,
21+
and upgrade Swagger UI to a secure version. Ensure proper authentication is required to access documentation.
22+
references:
23+
- https://swagger.io/docs/
24+
- https://owasp.org/www-project-api-security/
25+
category: info_gather
26+
risk: high
27+
confidence: medium
28+
cweId: 200 # CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
29+
wascId: 13 # WASC-13: Information Leakage
30+
alertTags:
31+
${CommonAlertTag.OWASP_2021_A05_SEC_MISCONFIG.getTag()}: ${CommonAlertTag.OWASP_2021_A05_SEC_MISCONFIG.getValue()}
32+
${CommonAlertTag.OWASP_2017_A06_SEC_MISCONFIG.getTag()}: ${CommonAlertTag.OWASP_2017_A06_SEC_MISCONFIG.getValue()}
33+
status: alpha
34+
codeLink: https://example.com/swagger-ui-detector.js
35+
helpLink: https://www.example.com/
36+
`);
37+
}
38+
39+
// -------------------------------------------------------------------
40+
// 1. List of commonly exposed Swagger/OpenAPI documentation paths
41+
// -------------------------------------------------------------------
42+
var SWAGGER_PATHS = [
43+
"/swagger", "/swagger/", "/swagger/index.html", "/swagger/ui", "/swagger/ui/",
44+
"/swagger/ui/index", "/swagger/ui/index.html", "/swagger-ui", "/swagger-ui/",
45+
"/swagger-ui/index.html", "/swagger-ui/index", "/docs", "/docs/",
46+
"/api-docs", "/v2/api-docs", "/v3/api-docs", "/swagger.json",
47+
"/swagger.yaml", "/openapi.json", "/openapi.yaml"
48+
];
49+
50+
// -------------------------------------------------------------------
51+
// 2. Regex matchers for path filtering (more flexible than exact matches)
52+
// -------------------------------------------------------------------
53+
var SWAGGER_REGEX_PATHS = [
54+
/\/swagger\/?$/i,
55+
/\/swagger\/index\.html$/i,
56+
/\/swagger\/ui\/?$/i,
57+
/\/swagger\/ui\/index(\.html)?$/i,
58+
/\/swagger-ui\/?$/i,
59+
/\/swagger-ui\/index(\.html)?$/i,
60+
/\/docs\/?$/i,
61+
/\/api-docs$/i,
62+
/\/v2\/api-docs$/i,
63+
/\/v3\/api-docs$/i,
64+
/\/swagger\.(json|yaml)$/i,
65+
/\/openapi\.(json|yaml)$/i,
66+
/\/api(\/v[0-9]+)?\/.*$/i,
67+
/\/v[0-9]+\/swagger.*$/i,
68+
/\/v[0-9]+\/openapi.*$/i,
69+
/\/nswag\/?$/i,
70+
/\/redoc\/?$/i,
71+
/\/admin\/?$/i,
72+
/\/config(\.json|\.yaml|\.yml|\.php)?$/i,
73+
/\/debug(\.log|\.txt)?$/i,
74+
/\/\.env$/i,
75+
/\/\.git\/config$/i,
76+
/\/login\/?$/i,
77+
/\/signin\/?$/i,
78+
/\/upload\/.*$/i,
79+
/\/graphql$/i,
80+
/\/graphiql$/i,
81+
/\/phpinfo\.php$/i,
82+
/\/server-status$/i,
83+
/\/actuator\/.*$/i,
84+
/\/\.git\/HEAD$/i,
85+
/\/backup\.zip$/i,
86+
/\/db\.sql$/i
87+
];
88+
89+
// -------------------------------------------------------------------
90+
// 3. Regex patterns to detect likely secrets in Swagger responses
91+
// -------------------------------------------------------------------
92+
var SECRET_REGEXES = [
93+
/["']?clientId["']?\s*:\s*["'](?!client_id|""|.{0,6}$).*?["']/gi,
94+
/["']?clientSecret["']?\s*:\s*["'](?!client_secret|""|.{0,6}$).*?["']/gi,
95+
/["']?oAuth2ClientId["']?\s*:\s*["'](?!client_id|""|.{0,6}$).*?["']/gi,
96+
/["']?oAuth2ClientSecret["']?\s*:\s*["'](?!client_secret|""|.{0,6}$).*?["']/gi,
97+
/["']?api_key["']?\s*:\s*["'](?!your_api_key_here|""|.{0,6}$).*?["']/gi,
98+
/["']?access_token["']?\s*:\s*["'](?!""|.{0,6}$).*?["']/gi,
99+
/["']?authorization["']?\s*:\s*["']Bearer\s+(?!""|.{0,6}$).*?["']/gi
100+
];
101+
102+
// -------------------------------------------------------------------
103+
// 4. Known dummy/test values that should be ignored
104+
// -------------------------------------------------------------------
105+
var FALSE_POSITIVES = [
106+
"clientid", "clientsecret", "string", "n/a", "null", "na", "true", "false",
107+
"value_here", "your_key", "your_api_key_here", "demo_token", "test1234",
108+
"dummysecret", "{token}", "bearer{token}", "placeholder", "insert_value"
109+
];
110+
111+
// -------------------------------------------------------------------
112+
// 5. False positive filter: heuristic to skip known dummy/test data
113+
// -------------------------------------------------------------------
114+
function isFalsePositiveKV(kvString) {
115+
if (!kvString || kvString.length < 1) return true;
116+
117+
var kvMatch = kvString.match(/["']?([^"']+)["']?\s*:\s*["']?([^"']+)["']?/);
118+
if (!kvMatch || kvMatch.length < 3) return false;
119+
120+
var key = kvMatch[1].toLowerCase().trim();
121+
var value = kvMatch[2].toLowerCase().trim();
122+
value = value.replace(/[\s"'{}]/g, '');
123+
124+
if (value.length < 8) return true;
125+
126+
var contextKeys = ["example", "description", "title", "note"];
127+
for (var i = 0; i < contextKeys.length; i++) {
128+
if (key.indexOf(contextKeys[i]) !== -1) return true;
129+
}
130+
131+
var junkTokens = ["test", "sample", "dummy", "mock", "try", "placeholder", "your", "insert"];
132+
for (var i = 0; i < junkTokens.length; i++) {
133+
if (value.indexOf(junkTokens[i]) !== -1 || key.indexOf(junkTokens[i]) !== -1) return true;
134+
}
135+
136+
for (var i = 0; i < FALSE_POSITIVES.length; i++) {
137+
if (value === FALSE_POSITIVES[i]) return true;
138+
}
139+
140+
return false;
141+
}
142+
143+
// -------------------------------------------------------------------
144+
// 6. Redact secret values in evidence (show only first 5 chars)
145+
// -------------------------------------------------------------------
146+
function redactSecret(secret) {
147+
var parts = secret.split(':');
148+
if (parts.length < 2) return secret;
149+
var value = parts.slice(1).join(':').trim().replace(/^"|"$/g, '');
150+
return parts[0] + ': "' + value.substring(0, 5) + '..."';
151+
}
152+
153+
// -------------------------------------------------------------------
154+
// 7. Detect Swagger UI version in HTML/JS
155+
// -------------------------------------------------------------------
156+
function detectSwaggerVersion(body) {
157+
if (body.indexOf('SwaggerUIBundle') !== -1) return 3;
158+
if (body.indexOf('SwaggerUi') !== -1 || body.indexOf('window.swaggerUi') !== -1 || body.indexOf('swashbuckleConfig') !== -1) return 2;
159+
if (body.indexOf('NSwag') !== -1 || body.indexOf('nswagui') !== -1) return 4;
160+
return 0;
161+
}
162+
163+
function extractVersion(body) {
164+
var versionRegex = /version\s*[:=]\s*["']?(\d+\.\d+\.\d+)["']?/i;
165+
var match = body.match(versionRegex);
166+
return match ? match[1] : null;
167+
}
168+
169+
function versionToInt(v) {
170+
var parts = v.split(".");
171+
return (parseInt(parts[0], 10) * 10000) + (parseInt(parts[1], 10) * 100) + parseInt(parts[2], 10);
172+
}
173+
174+
// -------------------------------------------------------------------
175+
// 8. Main scan logic: runs once per node
176+
// -------------------------------------------------------------------
177+
function scanNode(as, msg) {
178+
var origUri = msg.getRequestHeader().getURI();
179+
var scheme = origUri.getScheme();
180+
var host = origUri.getHost();
181+
var port = origUri.getPort();
182+
var base = scheme + "://" + host + ((port !== -1 && port !== 80 && port !== 443) ? ":" + port : "");
183+
184+
// --- Pass 1: Check static Swagger paths ---
185+
for (var i = 0; i < SWAGGER_PATHS.length; i++) {
186+
scanPath(as, msg, scheme, host, port, SWAGGER_PATHS[i], base + SWAGGER_PATHS[i]);
187+
}
188+
189+
// --- Pass 2: Check current request path if it matches any regex ---
190+
var currentPath = origUri.getPath();
191+
for (var r = 0; r < SWAGGER_REGEX_PATHS.length; r++) {
192+
if (SWAGGER_REGEX_PATHS[r].test(currentPath)) {
193+
scanPath(as, msg, scheme, host, port, currentPath, base + currentPath);
194+
}
195+
}
196+
}
197+
198+
// -------------------------------------------------------------------
199+
// 9. Scan a single path (version + secret detection reused)
200+
// -------------------------------------------------------------------
201+
function scanPath(as, origMsg, scheme, host, port, pathOnly, fullPath) {
202+
var requestMsg = origMsg.cloneRequest();
203+
204+
try {
205+
requestMsg.getRequestHeader().setMethod("GET");
206+
var newUri = new URI(scheme, null, host, port, pathOnly);
207+
requestMsg.getRequestHeader().setURI(newUri);
208+
requestMsg.getRequestHeader().setContentLength(0);
209+
210+
var origHeaders = origMsg.getRequestHeader();
211+
["User-Agent", "Cookie", "Authorization"].forEach(function (header) {
212+
var val = origHeaders.getHeader(header);
213+
if (val) requestMsg.getRequestHeader().setHeader(header, val);
214+
});
215+
216+
as.sendAndReceive(requestMsg, false, false);
217+
} catch (err) {
218+
return;
219+
}
220+
221+
var body = requestMsg.getResponseBody().toString();
222+
var version = detectSwaggerVersion(body);
223+
var semver = extractVersion(body);
224+
225+
if (semver && (version === 2 || version === 3)) {
226+
var vInt = versionToInt(semver);
227+
if ((version === 2 && vInt < 20210) || (version === 3 && vInt < 32403)) {
228+
var cveReference = (version === 2)
229+
? "https://nvd.nist.gov/vuln/detail/CVE-2019-17495"
230+
: "https://github.com/swagger-api/swagger-ui/releases/tag/v3.24.3";
231+
232+
as.newAlert()
233+
.setRisk(3)
234+
.setConfidence(2)
235+
.setName("Vulnerable Swagger UI version detected (v" + semver + ")")
236+
.setAlertRef("100001-1")
237+
.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")
238+
.setOtherInfo("Discovered at: " + fullPath)
239+
.setSolution("Upgrade to the latest version of Swagger UI. Regularly review and patch known issues.")
240+
.setReference(cveReference)
241+
.setMessage(requestMsg)
242+
.raise();
243+
}
244+
}
245+
246+
detectSecrets(as, requestMsg, fullPath, body);
247+
}
248+
249+
function detectSecrets(as, requestMsg, fullPath, body) {
250+
var matches = {};
251+
for (var j = 0; j < SECRET_REGEXES.length; j++) {
252+
var found = body.match(SECRET_REGEXES[j]);
253+
if (found) {
254+
for (var f = 0; f < found.length; f++) {
255+
var match = found[f];
256+
if (!isFalsePositiveKV(match)) {
257+
matches[match] = true;
258+
}
259+
}
260+
}
261+
}
262+
263+
var evidenceRaw = Object.keys(matches);
264+
var redactedEvidence = evidenceRaw.map(redactSecret);
265+
var foundClientId = evidenceRaw.some(e => /clientId/i.test(e));
266+
var foundSecret = evidenceRaw.some(e => /clientSecret|api_key|access_token|authorization/i.test(e));
267+
268+
if (foundClientId && foundSecret) {
269+
as.newAlert()
270+
.setRisk(3)
271+
.setConfidence(2)
272+
.setName("Exposed secrets in Swagger/OpenAPI path")
273+
.setAlertRef("100001-2")
274+
.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.")
275+
.setEvidence(redactedEvidence.join("\n"))
276+
.setOtherInfo("Discovered at: " + fullPath)
277+
.setSolution("Remove hardcoded secrets from documentation and ensure the endpoint is protected with authentication.")
278+
.setReference("https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/")
279+
.setMessage(requestMsg)
280+
.raise();
281+
}
282+
}

0 commit comments

Comments
 (0)