redgun-security 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scan.js +2 -0
- package/src/remote/browser.js +263 -0
package/package.json
CHANGED
package/scan.js
CHANGED
|
@@ -8,6 +8,7 @@ import { scanSamlRemote, scanLdapRemote, scanMfaBypass, scanWebsocketReplay, sca
|
|
|
8
8
|
import { scanSsrfBypassChains, scanJwtRemoteAdvanced, scanGrpc, scanOpenApi, scanWebrtc, scanStoredDomXss, scanSsiRemote, scanXpathRemote, scanTimingRemote } from './src/remote/complete.js';
|
|
9
9
|
import { scanLlmRemote, scanCssInjectionRemote, scanPostMessageRemote, scanEsiRemote, scanHttp3, scanHpackBomb, scanSmtpRemote, scanDkimReplay } from './src/remote/modern.js';
|
|
10
10
|
import { validateFindings } from './src/core/validator.js';
|
|
11
|
+
import { runBrowserEngine } from './src/remote/browser.js';
|
|
11
12
|
|
|
12
13
|
export async function runRemoteScan(url, spinner, modules = null) {
|
|
13
14
|
const target = new URL(url);
|
|
@@ -15,6 +16,7 @@ export async function runRemoteScan(url, spinner, modules = null) {
|
|
|
15
16
|
const origin = target.origin;
|
|
16
17
|
|
|
17
18
|
const allModules = [
|
|
19
|
+
{ name: 'Browser Engine (Puppeteer)', value: 'browser', fn: () => runBrowserEngine(origin, spinner) },
|
|
18
20
|
{ name: 'Probe & Fingerprint (httpx)', value: 'probe', fn: () => runProbe(origin, spinner) },
|
|
19
21
|
{ name: 'Crawl & Extract (Katana)', value: 'crawl', fn: () => runCrawler(origin, spinner) },
|
|
20
22
|
{ name: 'HTTP Headers', value: 'headers', fn: () => scanHeaders(origin, spinner) },
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { addFinding } from '../core/findings.js';
|
|
2
|
+
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
export async function runBrowserEngine(origin, spinner) {
|
|
6
|
+
spinner.text = '[Browser] Launching headless Chromium...';
|
|
7
|
+
|
|
8
|
+
let puppeteer;
|
|
9
|
+
try {
|
|
10
|
+
puppeteer = (await import('puppeteer')).default;
|
|
11
|
+
} catch {
|
|
12
|
+
spinner.warn('Puppeteer not available — browser tests skipped');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let browser;
|
|
17
|
+
let page;
|
|
18
|
+
const results = {
|
|
19
|
+
alerts: [],
|
|
20
|
+
consoleErrors: [],
|
|
21
|
+
networkRequests: [],
|
|
22
|
+
wsConnections: [],
|
|
23
|
+
localStorage: {},
|
|
24
|
+
sessionStorage: {},
|
|
25
|
+
serviceWorkers: false,
|
|
26
|
+
postMessageListens: false,
|
|
27
|
+
screenshotPath: null,
|
|
28
|
+
xssTested: 0,
|
|
29
|
+
xssConfirmed: 0,
|
|
30
|
+
formsFound: 0,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
browser = await puppeteer.launch({
|
|
35
|
+
headless: 'new',
|
|
36
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-web-security', '--disable-features=IsolateOrigins,site-per-process'],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
page = await browser.newPage();
|
|
40
|
+
await page.setViewport({ width: 1280, height: 800 });
|
|
41
|
+
|
|
42
|
+
page.on('dialog', async (dialog) => {
|
|
43
|
+
results.alerts.push({ message: dialog.message(), type: dialog.type() });
|
|
44
|
+
await dialog.dismiss();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
page.on('console', (msg) => {
|
|
48
|
+
if (msg.type() === 'error') {
|
|
49
|
+
results.consoleErrors.push(msg.text());
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
page.on('request', (req) => {
|
|
54
|
+
if (req.resourceType() === 'websocket') {
|
|
55
|
+
results.wsConnections.push(req.url());
|
|
56
|
+
}
|
|
57
|
+
results.networkRequests.push({
|
|
58
|
+
url: req.url(),
|
|
59
|
+
method: req.method(),
|
|
60
|
+
type: req.resourceType(),
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
spinner.text = '[Browser] Navigating to target...';
|
|
65
|
+
await page.goto(origin, { waitUntil: 'networkidle2', timeout: 30000 });
|
|
66
|
+
|
|
67
|
+
results.serviceWorkers = await page.evaluate(() => 'serviceWorker' in navigator && Boolean(navigator.serviceWorker.controller));
|
|
68
|
+
|
|
69
|
+
results.postMessageListens = await page.evaluate(() => {
|
|
70
|
+
let hasListener = false;
|
|
71
|
+
window.addEventListener('message', () => { hasListener = true; });
|
|
72
|
+
window.postMessage('__redgun_probe__', '*');
|
|
73
|
+
return new Promise(r => setTimeout(() => r(hasListener), 200));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
results.localStorage = await page.evaluate(() => {
|
|
77
|
+
const data = {};
|
|
78
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
79
|
+
const key = localStorage.key(i);
|
|
80
|
+
data[key] = localStorage.getItem(key);
|
|
81
|
+
}
|
|
82
|
+
return data;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
results.sessionStorage = await page.evaluate(() => {
|
|
86
|
+
const data = {};
|
|
87
|
+
try {
|
|
88
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
|
89
|
+
const key = sessionStorage.key(i);
|
|
90
|
+
data[key] = sessionStorage.getItem(key);
|
|
91
|
+
}
|
|
92
|
+
} catch {}
|
|
93
|
+
return data;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
spinner.text = '[Browser] Scanning for forms and inputs...';
|
|
97
|
+
const inputFields = await page.evaluate(() => {
|
|
98
|
+
const fields = [];
|
|
99
|
+
document.querySelectorAll('input, textarea, select').forEach((el) => {
|
|
100
|
+
const name = el.getAttribute('name') || el.getAttribute('id') || el.getAttribute('class') || '';
|
|
101
|
+
const type = el.getAttribute('type') || el.tagName.toLowerCase();
|
|
102
|
+
fields.push({ name: name.substring(0, 60), type });
|
|
103
|
+
});
|
|
104
|
+
return fields;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
results.formsFound = await page.evaluate(() => document.querySelectorAll('form').length);
|
|
108
|
+
|
|
109
|
+
spinner.text = '[Browser] Testing DOM XSS...';
|
|
110
|
+
const xssPayloads = [
|
|
111
|
+
'<img src=x onerror=alert("REDGUN_XSS") />',
|
|
112
|
+
'<svg onload=alert("REDGUN_XSS") />',
|
|
113
|
+
'" onfocus=alert("REDGUN_XSS") autofocus="',
|
|
114
|
+
'"><img src=x onerror=alert("REDGUN_XSS")>',
|
|
115
|
+
'javascript:alert("REDGUN_XSS")',
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
for (const field of inputFields.slice(0, 20)) {
|
|
119
|
+
if (!field.name) continue;
|
|
120
|
+
for (const payload of xssPayloads.slice(0, 3)) {
|
|
121
|
+
try {
|
|
122
|
+
await page.evaluate((name, payload) => {
|
|
123
|
+
const el = document.querySelector(`[name="${name}"], [id="${name}"]`);
|
|
124
|
+
if (el) {
|
|
125
|
+
el.value = payload;
|
|
126
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
127
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
128
|
+
}
|
|
129
|
+
}, field.name, payload);
|
|
130
|
+
results.xssTested++;
|
|
131
|
+
} catch {}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const formFields of inputFields.filter(f => f.name)) {
|
|
136
|
+
try {
|
|
137
|
+
await page.evaluate((name) => {
|
|
138
|
+
const input = document.querySelector(`[name="${name}"], [id="${name}"]`);
|
|
139
|
+
const form = input?.closest('form');
|
|
140
|
+
if (form) form.submit();
|
|
141
|
+
}, formFields.name);
|
|
142
|
+
await new Promise(r => setTimeout(r, 300));
|
|
143
|
+
|
|
144
|
+
if (results.alerts.some(a => a.message.includes('REDGUN_XSS'))) {
|
|
145
|
+
results.xssConfirmed++;
|
|
146
|
+
}
|
|
147
|
+
} catch {}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const currentPageUrl = page.url();
|
|
151
|
+
if (results.alerts.some(a => a.message.includes('REDGUN_XSS')) && currentPageUrl.includes(origin)) {
|
|
152
|
+
results.xssConfirmed = Math.max(results.xssConfirmed, 1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (results.alerts.length > 0 || results.xssConfirmed > 0) {
|
|
156
|
+
const screenshotsDir = './scans';
|
|
157
|
+
if (!existsSync(screenshotsDir)) mkdirSync(screenshotsDir, { recursive: true });
|
|
158
|
+
const ts = Date.now();
|
|
159
|
+
results.screenshotPath = join(screenshotsDir, `redgun-xss-${ts}.png`);
|
|
160
|
+
await page.screenshot({ path: results.screenshotPath, fullPage: true });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
} catch (err) {
|
|
164
|
+
spinner.text = `[Browser] Error: ${err.message}`;
|
|
165
|
+
} finally {
|
|
166
|
+
if (browser) await browser.close();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
reportFindings(origin, results);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function reportFindings(origin, results) {
|
|
173
|
+
if (results.xssConfirmed > 0) {
|
|
174
|
+
addFinding(
|
|
175
|
+
'CRITICAL',
|
|
176
|
+
'Browser XSS',
|
|
177
|
+
`${results.xssConfirmed} DOM/Stored XSS confirmed via browser`,
|
|
178
|
+
`${results.xssTested} payloads injected into ${results.formsFound} forms — ${results.xssConfirmed} alert() triggers detected\nScreenshot: ${results.screenshotPath || 'N/A'}`,
|
|
179
|
+
'Sanitize all user input on both client and server side. Use DOMPurify, React escape, or framework auto-escaping.'
|
|
180
|
+
);
|
|
181
|
+
} else if (results.xssTested > 0) {
|
|
182
|
+
addFinding(
|
|
183
|
+
'INFO',
|
|
184
|
+
'Browser XSS',
|
|
185
|
+
`Tested ${results.xssTested} inputs — no XSS confirmed`,
|
|
186
|
+
`${results.formsFound} forms analyzed with 5 XSS payload types`,
|
|
187
|
+
'DOM/Stored XSS not confirmed — continue manual testing with application-specific payloads'
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const apiRequests = results.networkRequests.filter(r => r.url.includes('/api/'));
|
|
192
|
+
if (apiRequests.length > 0) {
|
|
193
|
+
const uniqueApis = [...new Set(apiRequests.map(r => r.url).filter(u => u.startsWith(origin)))];
|
|
194
|
+
|
|
195
|
+
if (uniqueApis.length > 5) {
|
|
196
|
+
addFinding(
|
|
197
|
+
'INFO',
|
|
198
|
+
'Browser Recon',
|
|
199
|
+
`${uniqueApis.length} API endpoints captured from browser network`,
|
|
200
|
+
`APIs: ${uniqueApis.slice(0, 10).join(', ')}${uniqueApis.length > 10 ? ` +${uniqueApis.length - 10} more` : ''}`,
|
|
201
|
+
'Review captured API endpoints for auth requirements and sensitive data exposure'
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (results.wsConnections.length > 0) {
|
|
207
|
+
addFinding(
|
|
208
|
+
'MEDIUM',
|
|
209
|
+
'Browser WebSocket',
|
|
210
|
+
`${results.wsConnections.length} WebSocket connection(s) detected`,
|
|
211
|
+
`WS: ${results.wsConnections.join(', ')}`,
|
|
212
|
+
'Test WebSocket for CSWSH, missing auth, and message tampering'
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (results.serviceWorkers) {
|
|
217
|
+
addFinding(
|
|
218
|
+
'LOW',
|
|
219
|
+
'Browser Service Worker',
|
|
220
|
+
'Service Worker active on page',
|
|
221
|
+
'SW registered — test for importScripts abuse and fetch listener tampering',
|
|
222
|
+
'Validate Service Worker scope and origin. Use Subresource Integrity for imported scripts.'
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (results.postMessageListens) {
|
|
227
|
+
addFinding(
|
|
228
|
+
'MEDIUM',
|
|
229
|
+
'Browser postMessage',
|
|
230
|
+
'postMessage listener detected (runtime check)',
|
|
231
|
+
'Page has active message event handler',
|
|
232
|
+
'Audit postMessage listeners for missing origin validation. Test cross-origin iframe attacks.'
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const localKeys = Object.keys(results.localStorage || {});
|
|
237
|
+
const sessionKeys = Object.keys(results.sessionStorage || {});
|
|
238
|
+
if (localKeys.length > 0 || sessionKeys.length > 0) {
|
|
239
|
+
const sensitiveStorage = [...localKeys, ...sessionKeys].filter(k =>
|
|
240
|
+
/token|secret|key|password|credential|jwt|auth|session|user/i.test(k)
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
if (sensitiveStorage.length > 0) {
|
|
244
|
+
addFinding(
|
|
245
|
+
'HIGH',
|
|
246
|
+
'Browser Storage',
|
|
247
|
+
`${sensitiveStorage.length} sensitive items in browser storage`,
|
|
248
|
+
`Keys: ${sensitiveStorage.join(', ')}`,
|
|
249
|
+
'Never store tokens, secrets, or credentials in localStorage/sessionStorage. Use httpOnly cookies for session management.'
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (results.consoleErrors.length > 5) {
|
|
255
|
+
addFinding(
|
|
256
|
+
'LOW',
|
|
257
|
+
'Browser Console',
|
|
258
|
+
`${results.consoleErrors.length} console errors detected`,
|
|
259
|
+
`Sample: ${results.consoleErrors.slice(0, 3).join(' | ')}`,
|
|
260
|
+
'Console errors may reveal internal paths, CSP violations, or API error messages'
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|