redgun-security 1.2.0 → 1.4.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/README.md +1 -1
- package/package.json +1 -1
- package/scan.js +18 -0
- package/src/local/client-proto.js +67 -0
- package/src/local/css-injection.js +67 -0
- package/src/local/csti.js +71 -0
- package/src/local/electron.js +68 -0
- package/src/local/index.js +26 -0
- package/src/local/jwt-advanced.js +70 -0
- package/src/local/llm-ai.js +67 -0
- package/src/local/padding-oracle.js +66 -0
- package/src/local/postmessage.js +66 -0
- package/src/local/service-worker.js +64 -0
- package/src/local/supply-chain-advanced.js +71 -0
- package/src/local/timing.js +66 -0
- package/src/local/webauthn.js +66 -0
- package/src/local/xpath-ssi.js +67 -0
- package/src/remote/complete.js +240 -0
- package/src/remote/modern.js +181 -0
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
## What is RedGun?
|
|
25
25
|
|
|
26
|
-
RedGun is a security auditing CLI tool that finds vulnerabilities in your web applications. It includes **51 security modules** covering techniques from
|
|
26
|
+
RedGun is a security auditing CLI tool that finds vulnerabilities in your web applications. It includes **51 security modules** covering techniques from modern techniques. Two modes:
|
|
27
27
|
|
|
28
28
|
**Remote scan** (black-box): Give it a URL. It crawls with Katana-style JS parsing, fingerprints with httpx-style probing, then tests — XSS, SQLi, SSRF, CORS, XXE, OAuth, IDOR, cache deception, DOM-based, HTTP smuggling, CRLF, parameter pollution, file upload, and more.
|
|
29
29
|
|
package/package.json
CHANGED
package/scan.js
CHANGED
|
@@ -5,6 +5,8 @@ import { runCrawler } from './src/remote/crawler.js';
|
|
|
5
5
|
import { runProbe } from './src/remote/probe.js';
|
|
6
6
|
import { scanXxeRemote, scanOauthRemote, scanAccessControlRemote, scanWebCacheDeception, scanParameterPollution, scanFileUpload, scanDomBased, scanHttp2 } from './src/remote/portswigger.js';
|
|
7
7
|
import { scanSamlRemote, scanLdapRemote, scanMfaBypass, scanWebsocketReplay, scanPasswordReset, scanCsrfRemote, scanDanglingDns, scanCloudRemote } from './src/remote/advanced.js';
|
|
8
|
+
import { scanSsrfBypassChains, scanJwtRemoteAdvanced, scanGrpc, scanOpenApi, scanWebrtc, scanStoredDomXss, scanSsiRemote, scanXpathRemote, scanTimingRemote } from './src/remote/complete.js';
|
|
9
|
+
import { scanLlmRemote, scanCssInjectionRemote, scanPostMessageRemote, scanEsiRemote, scanHttp3, scanHpackBomb, scanSmtpRemote, scanDkimReplay } from './src/remote/modern.js';
|
|
8
10
|
|
|
9
11
|
export async function runRemoteScan(url, spinner, modules = null) {
|
|
10
12
|
const target = new URL(url);
|
|
@@ -55,6 +57,22 @@ export async function runRemoteScan(url, spinner, modules = null) {
|
|
|
55
57
|
{ name: 'CSRF Token Analysis (remote)', value: 'csrf', fn: () => scanCsrfRemote(origin, spinner) },
|
|
56
58
|
{ name: 'Subdomain Takeover (Dangling DNS)', value: 'takeover', fn: () => scanDanglingDns(hostname, spinner) },
|
|
57
59
|
{ name: 'Cloud Metadata SSRF', value: 'cloudmeta', fn: () => scanCloudRemote(origin, spinner) },
|
|
60
|
+
{ name: 'SSRF Bypass Chains', value: 'ssrfbypass', fn: () => scanSsrfBypassChains(origin, spinner) },
|
|
61
|
+
{ name: 'JWT Advanced (kid/none/JWK)', value: 'jwtadv', fn: () => scanJwtRemoteAdvanced(origin, spinner) },
|
|
62
|
+
{ name: 'gRPC Reflection', value: 'grpc', fn: () => scanGrpc(origin, spinner) },
|
|
63
|
+
{ name: 'OpenAPI/Swagger Fuzz', value: 'openapi', fn: () => scanOpenApi(origin, spinner) },
|
|
64
|
+
{ name: 'WebRTC IP Leak', value: 'webrtc', fn: () => scanWebrtc(origin, spinner) },
|
|
65
|
+
{ name: 'Stored/DOM XSS Auto', value: 'storedxss', fn: () => scanStoredDomXss(origin, spinner) },
|
|
66
|
+
{ name: 'SSI Injection Remote', value: 'ssi', fn: () => scanSsiRemote(origin, spinner) },
|
|
67
|
+
{ name: 'XPath Injection Remote', value: 'xpath', fn: () => scanXpathRemote(origin, spinner) },
|
|
68
|
+
{ name: 'Timing Side-Channel', value: 'timing', fn: () => scanTimingRemote(origin, spinner) },
|
|
69
|
+
{ name: 'AI/LLM Prompt Injection', value: 'llmai', fn: () => scanLlmRemote(origin, spinner) },
|
|
70
|
+
{ name: 'CSS Injection/Exfiltration', value: 'css', fn: () => scanCssInjectionRemote(origin, spinner) },
|
|
71
|
+
{ name: 'PostMessage/BroadcastChannel', value: 'postmsg', fn: () => scanPostMessageRemote(origin, spinner) },
|
|
72
|
+
{ name: 'ESI Injection (CDN)', value: 'esi', fn: () => scanEsiRemote(origin, spinner) },
|
|
73
|
+
{ name: 'HTTP/3 QUIC (Modern)', value: 'h3', fn: () => scanHttp3(origin, spinner) },
|
|
74
|
+
{ name: 'HPACK Bomb / Header Overflow', value: 'hpack', fn: () => scanHpackBomb(origin, spinner) },
|
|
75
|
+
{ name: 'SMTP / DKIM Replay', value: 'smtp', fn: () => scanSmtpRemote(origin, spinner) },
|
|
58
76
|
];
|
|
59
77
|
|
|
60
78
|
const toRun = modules ? allModules.filter((m) => modules.includes(m.value)) : allModules;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
|
+
import { addFinding } from '../core/findings.js';
|
|
4
|
+
|
|
5
|
+
const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.html', '.htm'];
|
|
6
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
|
|
7
|
+
|
|
8
|
+
export async function auditClientProto(projectPath, spinner) {
|
|
9
|
+
spinner.text = 'Scanning for client-side prototype pollution gadget chains...';
|
|
10
|
+
const files = getFiles(projectPath);
|
|
11
|
+
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
try {
|
|
14
|
+
const content = readFileSync(file, 'utf-8');
|
|
15
|
+
const relativePath = file.replace(projectPath, '.');
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
|
|
18
|
+
const patterns = [
|
|
19
|
+
{ pattern: /Object\.assign\s*\(\s*\{\}\s*,\s*(?:req|request|params|query|body|input|data)/gi, name: 'Object.assign with user data (proto sink)', severity: 'HIGH' },
|
|
20
|
+
{ pattern: /\{\s*\.\.\.(?:req|request|params|query|body|input|data)\s*\}/gi, name: 'Spread operator from user input (proto sink)', severity: 'HIGH' },
|
|
21
|
+
{ pattern: /lodash\.merge|_.merge|deepmerge|merge\(.*,\s*(?:req|request|params|body)/gi, name: 'Deep merge with user-controlled data (proto sink)', severity: 'CRITICAL' },
|
|
22
|
+
{ pattern: /\$\.extend\s*\(\s*true\s*,/gi, name: 'jQuery $.extend(deep=true) with user data', severity: 'HIGH' },
|
|
23
|
+
{ pattern: /angular\.merge|angular\.extend/gi, name: 'AngularJS merge/extend with user data', severity: 'HIGH' },
|
|
24
|
+
{ pattern: /Object\.create\s*\(\s*(?:req|request|params|query)/gi, name: 'Object.create with user-controlled prototype', severity: 'MEDIUM' },
|
|
25
|
+
{ pattern: /(?:ajax|xhr|fetch)\s*\.(?:response|send|data).*\.(?:extend|merge|assign)/gi, name: 'AJAX response merged into objects', severity: 'MEDIUM' },
|
|
26
|
+
{ pattern: /JSON\.parse\s*\([^)]*\)\s*\..*(?:extend|merge|assign)/gi, name: 'Parsed JSON merged into objects', severity: 'HIGH' },
|
|
27
|
+
{ pattern: /(?:location\.hash|location\.search|document\.cookie|window\.name)\s*.*(?:extend|merge|assign|parse)/gi, name: 'DOM source merged into objects (proto via URL/cookie)', severity: 'CRITICAL' },
|
|
28
|
+
{ pattern: /(?:window|global|self|globalThis)\.Object\.defineProperty/gi, name: 'Object.defineProperty on global (proto tamper)', severity: 'CRITICAL' },
|
|
29
|
+
{ pattern: /sanitize-html|dompurify|xss-filters/gi, name: 'XSS filter bypass via proto (gadget target)', severity: 'MEDIUM' },
|
|
30
|
+
{ pattern: /Object\.freeze\s*\(\s*Object\.prototype\s*\)/gi, name: 'Object.freeze on prototype (mitigation)', severity: 'INFO' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
for (const { pattern, name, severity } of patterns) {
|
|
34
|
+
let match;
|
|
35
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
36
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
37
|
+
const line = lines[lineNum - 1]?.trim() || '';
|
|
38
|
+
if (line.startsWith('//') || line.startsWith('#') || line.startsWith('*')) continue;
|
|
39
|
+
|
|
40
|
+
addFinding(
|
|
41
|
+
severity,
|
|
42
|
+
'Client-Side Proto Pollution',
|
|
43
|
+
name,
|
|
44
|
+
`File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
|
|
45
|
+
'Strip __proto__ and constructor.prototype properties before merging. Use Object.create(null) for dictionaries. Freeze Object.prototype if needing global protection. Use JSON schema validation instead of raw merging. Avoid deep merge from untrusted sources.'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getFiles(dir, files = []) {
|
|
54
|
+
try {
|
|
55
|
+
const entries = readdirSync(dir);
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
|
|
58
|
+
const fullPath = join(dir, entry);
|
|
59
|
+
try {
|
|
60
|
+
const stat = statSync(fullPath);
|
|
61
|
+
if (stat.isDirectory()) getFiles(fullPath, files);
|
|
62
|
+
else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
} catch {}
|
|
66
|
+
return files;
|
|
67
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
|
+
import { addFinding } from '../core/findings.js';
|
|
4
|
+
|
|
5
|
+
const SCAN_EXTENSIONS = ['.js', '.ts', '.css', '.scss', '.less', '.html', '.htm', '.jsx', '.tsx', '.vue', '.svelte', '.astro'];
|
|
6
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
|
|
7
|
+
|
|
8
|
+
export async function auditCssInjection(projectPath, spinner) {
|
|
9
|
+
spinner.text = 'Scanning for CSS injection/exfiltration vectors...';
|
|
10
|
+
const files = getFiles(projectPath);
|
|
11
|
+
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
try {
|
|
14
|
+
const content = readFileSync(file, 'utf-8');
|
|
15
|
+
const relativePath = file.replace(projectPath, '.');
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
|
|
18
|
+
const patterns = [
|
|
19
|
+
{ pattern: /style\s*=\s*(?:req|request|params|query|body|user|input)/gi, name: 'Inline style from user input', severity: 'HIGH' },
|
|
20
|
+
{ pattern: /(<style[^>]*>).*\$\{.*(?:req|params|body|input)/gi, name: 'Style tag with user input', severity: 'HIGH' },
|
|
21
|
+
{ pattern: /css\s*[(`]\$\{/gi, name: 'CSS template literal with user input', severity: 'MEDIUM' },
|
|
22
|
+
{ pattern: /(\[class(?:\^|\*|\$)?=(["']?)[^\]]*["']?\]|attr\(|content:\s*attr)/gi, name: 'CSS attribute selector (data exfiltration)', severity: 'MEDIUM' },
|
|
23
|
+
{ pattern: /@font-face\s*\{[^}]*src:\s*url\s*\(/gi, name: '@font-face with external URL (char-by-char exfil)', severity: 'HIGH' },
|
|
24
|
+
{ pattern: /@import\s+url\s*\(.*\);/gi, name: 'CSS @import (external CSS injection)', severity: 'MEDIUM' },
|
|
25
|
+
{ pattern: /background(?:-image)?:\s*url\s*\(/gi, name: 'CSS background-image (exfiltration channel)', severity: 'MEDIUM' },
|
|
26
|
+
{ pattern: /::?value|::-webkit-input-placeholder|::-moz-placeholder/gi, name: 'CSS pseudo-element selectors', severity: 'LOW' },
|
|
27
|
+
{ pattern: /unicode-range|U\+/gi, name: 'CSS unicode-range (char exfiltration via font)', severity: 'HIGH' },
|
|
28
|
+
{ pattern: /animation-name|keyframes|animation-duration/gi, name: 'CSS animations (timing-based exfiltration)', severity: 'LOW' },
|
|
29
|
+
{ pattern: /content\s*:\s*attr\s*\([^)]*\)/gi, name: 'CSS content: attr() (attribute exfiltration)', severity: 'HIGH' },
|
|
30
|
+
{ pattern: /input\[type=(["']?)password["']?\].*background-image|input\[type=(["']?)password["']?\].*@font-face/gi, name: 'CSS keylogger pattern (password exfiltration)', severity: 'CRITICAL' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
for (const { pattern, name, severity } of patterns) {
|
|
34
|
+
let match;
|
|
35
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
36
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
37
|
+
const line = lines[lineNum - 1]?.trim() || '';
|
|
38
|
+
if (line.startsWith('//') || line.startsWith('#') || line.startsWith('*')) continue;
|
|
39
|
+
|
|
40
|
+
addFinding(
|
|
41
|
+
severity,
|
|
42
|
+
'CSS Injection/Exfiltration',
|
|
43
|
+
name,
|
|
44
|
+
`File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
|
|
45
|
+
'Sanitize user input reflected in CSS/style attributes. Use strict CSP with nonce/hash (not unsafe-inline). For CSS exfiltration: limit input length, use content-based exfil detection, disable @font-face loading from external sources.'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getFiles(dir, files = []) {
|
|
54
|
+
try {
|
|
55
|
+
const entries = readdirSync(dir);
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
|
|
58
|
+
const fullPath = join(dir, entry);
|
|
59
|
+
try {
|
|
60
|
+
const stat = statSync(fullPath);
|
|
61
|
+
if (stat.isDirectory()) getFiles(fullPath, files);
|
|
62
|
+
else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
} catch {}
|
|
66
|
+
return files;
|
|
67
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
|
+
import { addFinding } from '../core/findings.js';
|
|
4
|
+
|
|
5
|
+
const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.py', '.rb', '.php', '.go', '.java'];
|
|
6
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
|
|
7
|
+
|
|
8
|
+
export async function auditCsti(projectPath, spinner) {
|
|
9
|
+
spinner.text = 'Scanning for client-side template injection...';
|
|
10
|
+
const files = getFiles(projectPath);
|
|
11
|
+
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
try {
|
|
14
|
+
const content = readFileSync(file, 'utf-8');
|
|
15
|
+
const relativePath = file.replace(projectPath, '.');
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
|
|
18
|
+
const patterns = [
|
|
19
|
+
{ pattern: /\{\{.+\}\}/g, name: 'AngularJS double-curly expression', severity: 'INFO' },
|
|
20
|
+
{ pattern: /ng-app|ng-controller|ng-bind-html|ng-non-bindable/gi, name: 'AngularJS bindings detected', severity: 'INFO' },
|
|
21
|
+
{ pattern: /\$sce\.trustAsHtml|\$sceProvider\.enabled\s*\(\s*false/gi, name: 'AngularJS SCE disabled/untrusted HTML', severity: 'HIGH' },
|
|
22
|
+
{ pattern: /ng-bind-html\s*=\s*(?:req|request|params|query|body)/gi, name: 'AngularJS ng-bind-html with user input', severity: 'HIGH' },
|
|
23
|
+
{ pattern: /angular\.module.*run\s*\(/gi, name: 'AngularJS module detected', severity: 'INFO' },
|
|
24
|
+
{ pattern: /v-html\s*=\s*(?:req|params|query|body)/gi, name: 'Vue v-html with user input', severity: 'HIGH' },
|
|
25
|
+
{ pattern: /v-bind:src|:src\s*=\s*['"`]\{\{/gi, name: 'Vue dynamic src binding', severity: 'MEDIUM' },
|
|
26
|
+
{ pattern: /Vue\.compile\s*\(|new\s+Vue\s*\(|createApp\s*\(/gi, name: 'Vue instance detected', severity: 'INFO' },
|
|
27
|
+
{ pattern: /React\.createElement|ReactDOM\.render|createRoot\s*\(/gi, name: 'React rendering detected', severity: 'INFO' },
|
|
28
|
+
{ pattern: /dangerouslySetInnerHTML\s*:\s*\{\s*__html\s*:\s*(?:req|params|query|body)/gi, name: 'React dangerouslySetInnerHTML with user input', severity: 'HIGH' },
|
|
29
|
+
{ pattern: /Svelte.*\$set|Svelte.*\$\$invalidate|Svelte.*dangerously/gi, name: 'Svelte dynamic updates', severity: 'MEDIUM' },
|
|
30
|
+
{ pattern: /TemplateRef|ViewContainerRef|ComponentFactoryResolver/gi, name: 'Angular dynamic component (XSS surface)', severity: 'MEDIUM' },
|
|
31
|
+
{ pattern: /bypassSecurityTrustHtml\s*\(/gi, name: 'Angular bypassSecurityTrustHtml', severity: 'HIGH' },
|
|
32
|
+
{ pattern: /bypassSecurityTrustScript|bypassSecurityTrustResourceUrl/gi, name: 'Angular bypassSecurityTrust (unsafe)', severity: 'HIGH' },
|
|
33
|
+
{ pattern: /ElementRef\.nativeElement\.innerHTML/gi, name: 'Angular ElementRef innerHTML', severity: 'MEDIUM' },
|
|
34
|
+
{ pattern: /sanitizeHtml|DOMPurify\.sanitize/gi, name: 'HTML sanitization used (good)', severity: 'INFO' },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
for (const { pattern, name, severity } of patterns) {
|
|
38
|
+
let match;
|
|
39
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
40
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
41
|
+
const line = lines[lineNum - 1]?.trim() || '';
|
|
42
|
+
if (line.startsWith('//') || line.startsWith('#') || line.startsWith('*')) continue;
|
|
43
|
+
|
|
44
|
+
addFinding(
|
|
45
|
+
severity,
|
|
46
|
+
'Client-Side Template Injection (CSTI)',
|
|
47
|
+
name,
|
|
48
|
+
`File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
|
|
49
|
+
'Never bind user input to v-html/dangerouslySetInnerHTML/ng-bind-html. Use DOMPurify for sanitization. Avoid bypassSecurityTrust* APIs. Use Angular default sanitizer. For Vue, use v-text instead of v-html.'
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getFiles(dir, files = []) {
|
|
58
|
+
try {
|
|
59
|
+
const entries = readdirSync(dir);
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
|
|
62
|
+
const fullPath = join(dir, entry);
|
|
63
|
+
try {
|
|
64
|
+
const stat = statSync(fullPath);
|
|
65
|
+
if (stat.isDirectory()) getFiles(fullPath, files);
|
|
66
|
+
else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
} catch {}
|
|
70
|
+
return files;
|
|
71
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
|
+
import { addFinding } from '../core/findings.js';
|
|
4
|
+
|
|
5
|
+
const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.html', '.htm', '.json', '.env'];
|
|
6
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
|
|
7
|
+
|
|
8
|
+
export async function auditElectron(projectPath, spinner) {
|
|
9
|
+
spinner.text = 'Scanning for Electron/ReactNative security issues...';
|
|
10
|
+
const files = getFiles(projectPath);
|
|
11
|
+
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
try {
|
|
14
|
+
const content = readFileSync(file, 'utf-8');
|
|
15
|
+
const relativePath = file.replace(projectPath, '.');
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
|
|
18
|
+
const patterns = [
|
|
19
|
+
{ pattern: /contextIsolation\s*:\s*false/gi, name: 'contextIsolation disabled (preload bridge exposed)', severity: 'CRITICAL' },
|
|
20
|
+
{ pattern: /nodeIntegration\s*:\s*true/gi, name: 'nodeIntegration enabled (Node API in renderer)', severity: 'CRITICAL' },
|
|
21
|
+
{ pattern: /sandbox\s*:\s*false/gi, name: 'Electron sandbox disabled', severity: 'HIGH' },
|
|
22
|
+
{ pattern: /webSecurity\s*:\s*false/gi, name: 'Electron webSecurity disabled (CORS/navigation bypass)', severity: 'HIGH' },
|
|
23
|
+
{ pattern: /allowRunningInsecureContent\s*:\s*true/gi, name: 'Electron mixed content allowed', severity: 'HIGH' },
|
|
24
|
+
{ pattern: /preload\s*:\s*path\.join\s*\(\s*__dirname\s*,\s*['"][^'"]*['"]\s*\)/gi, name: 'preload script from user-controllable path', severity: 'CRITICAL' },
|
|
25
|
+
{ pattern: /nodeIntegrationInSubFrames\s*:\s*true/gi, name: 'Node integration in iframes (XSS→RCE)', severity: 'CRITICAL' },
|
|
26
|
+
{ pattern: /shell\.openExternal\s*\(\s*(?:req|request|params|user|input)/gi, name: 'shell.openExternal with user input (command injection)', severity: 'CRITICAL' },
|
|
27
|
+
{ pattern: /electron\.ipcRenderer|ipcRenderer\.(?:on|send|invoke)/gi, name: 'Electron IPC usage (check handler auth)', severity: 'MEDIUM' },
|
|
28
|
+
{ pattern: /dialog\.showOpenDialog|dialog\.showSaveDialog/gi, name: 'Electron file dialog (check path traversal)', severity: 'MEDIUM' },
|
|
29
|
+
{ pattern: /(?:React\.Native|react-native).*(?:DEBUG|dev_mode)\s*=\s*true/gi, name: 'ReactNative debug mode enabled', severity: 'HIGH' },
|
|
30
|
+
{ pattern: /enableJSCWrapper|enableHermesDebugger/gi, name: 'RN JS debugger enabled', severity: 'MEDIUM' },
|
|
31
|
+
{ pattern: /(?:WebView|WKWebView).*originWhitelist|allowsInlineMediaPlayback/gi, name: 'RN WebView config (check allowlist)', severity: 'MEDIUM' },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
for (const { pattern, name, severity } of patterns) {
|
|
35
|
+
let match;
|
|
36
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
37
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
38
|
+
const line = lines[lineNum - 1]?.trim() || '';
|
|
39
|
+
if (line.startsWith('//') || line.startsWith('#') || line.startsWith('*')) continue;
|
|
40
|
+
|
|
41
|
+
addFinding(
|
|
42
|
+
severity,
|
|
43
|
+
'Electron / React Native',
|
|
44
|
+
name,
|
|
45
|
+
`File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
|
|
46
|
+
'Enable contextIsolation and sandbox. Disable nodeIntegration in renderer. Sanitize all ipcRenderer.send arguments. Validate shell.openExternal URLs. Use allowlist for originWhitelist in RN WebView. Disable debug mode in production builds.'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} catch {}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getFiles(dir, files = []) {
|
|
55
|
+
try {
|
|
56
|
+
const entries = readdirSync(dir);
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
|
|
59
|
+
const fullPath = join(dir, entry);
|
|
60
|
+
try {
|
|
61
|
+
const stat = statSync(fullPath);
|
|
62
|
+
if (stat.isDirectory()) getFiles(fullPath, files);
|
|
63
|
+
else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
|
|
64
|
+
} catch {}
|
|
65
|
+
}
|
|
66
|
+
} catch {}
|
|
67
|
+
return files;
|
|
68
|
+
}
|
package/src/local/index.js
CHANGED
|
@@ -24,6 +24,19 @@ import { auditCloud } from './cloud.js';
|
|
|
24
24
|
import { auditCicd } from './cicd.js';
|
|
25
25
|
import { auditMobile } from './mobile.js';
|
|
26
26
|
import { auditWeb3 } from './web3.js';
|
|
27
|
+
import { auditXpathSsi } from './xpath-ssi.js';
|
|
28
|
+
import { auditTiming } from './timing.js';
|
|
29
|
+
import { auditJwtAdvanced } from './jwt-advanced.js';
|
|
30
|
+
import { auditCsti } from './csti.js';
|
|
31
|
+
import { auditServiceWorker } from './service-worker.js';
|
|
32
|
+
import { auditPaddingOracle } from './padding-oracle.js';
|
|
33
|
+
import { auditLlmAi } from './llm-ai.js';
|
|
34
|
+
import { auditCssInjection } from './css-injection.js';
|
|
35
|
+
import { auditPostMessage } from './postmessage.js';
|
|
36
|
+
import { auditElectron } from './electron.js';
|
|
37
|
+
import { auditWebauthn } from './webauthn.js';
|
|
38
|
+
import { auditSupplyChainAdvanced } from './supply-chain-advanced.js';
|
|
39
|
+
import { auditClientProto } from './client-proto.js';
|
|
27
40
|
|
|
28
41
|
export const LOCAL_MODULES = [
|
|
29
42
|
{ name: 'Code Secrets', value: 'secrets', fn: auditSecrets },
|
|
@@ -52,6 +65,19 @@ export const LOCAL_MODULES = [
|
|
|
52
65
|
{ name: 'CI/CD Pipeline', value: 'cicd', fn: auditCicd },
|
|
53
66
|
{ name: 'Mobile Security', value: 'mobile', fn: auditMobile },
|
|
54
67
|
{ name: 'Web3 / Smart Contracts', value: 'web3', fn: auditWeb3 },
|
|
68
|
+
{ name: 'XPath / SSI Injection', value: 'xpath', fn: auditXpathSsi },
|
|
69
|
+
{ name: 'Timing Side-Channels', value: 'timing', fn: auditTiming },
|
|
70
|
+
{ name: 'JWT Advanced (kid, JWK, jku)', value: 'jwtadv', fn: auditJwtAdvanced },
|
|
71
|
+
{ name: 'Client-Side Template Injection (CSTI)', value: 'csti', fn: auditCsti },
|
|
72
|
+
{ name: 'Service Worker / WebRTC', value: 'sworker', fn: auditServiceWorker },
|
|
73
|
+
{ name: 'Padding / Compression Oracle', value: 'padding', fn: auditPaddingOracle },
|
|
74
|
+
{ name: 'AI/LLM Prompt Injection', value: 'llmai', fn: auditLlmAi },
|
|
75
|
+
{ name: 'CSS Injection/Exfiltration', value: 'css', fn: auditCssInjection },
|
|
76
|
+
{ name: 'PostMessage / BroadcastChannel', value: 'postmsg', fn: auditPostMessage },
|
|
77
|
+
{ name: 'Electron / React Native', value: 'electron', fn: auditElectron },
|
|
78
|
+
{ name: 'WebAuthn / Passkeys', value: 'passkey', fn: auditWebauthn },
|
|
79
|
+
{ name: 'Supply Chain (dep confusion, lockfile)', value: 'supply', fn: auditSupplyChainAdvanced },
|
|
80
|
+
{ name: 'Client-Side Proto Pollution Gadgets', value: 'cproto', fn: auditClientProto },
|
|
55
81
|
];
|
|
56
82
|
|
|
57
83
|
export async function runLocalAudit(projectPath, spinner, modules = null) {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
|
+
import { addFinding } from '../core/findings.js';
|
|
4
|
+
|
|
5
|
+
const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.py', '.rb', '.php', '.go', '.java', '.json', '.env'];
|
|
6
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
|
|
7
|
+
|
|
8
|
+
export async function auditJwtAdvanced(projectPath, spinner) {
|
|
9
|
+
spinner.text = 'Scanning for advanced JWT attacks (kid, JWK, jku)...';
|
|
10
|
+
const files = getFiles(projectPath);
|
|
11
|
+
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
try {
|
|
14
|
+
const content = readFileSync(file, 'utf-8');
|
|
15
|
+
const relativePath = file.replace(projectPath, '.');
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
|
|
18
|
+
const patterns = [
|
|
19
|
+
{ pattern: /(?:kid|keyId|key_id)\s*[:=]\s*(?:req|request|params|query|body|input)/gi, name: 'Key ID (kid) from user input - path traversal/LFI attack', severity: 'CRITICAL' },
|
|
20
|
+
{ pattern: /jwk\s*[:=]\s*(?:req|request|params|query|body)/gi, name: 'JWK from user input - key injection', severity: 'CRITICAL' },
|
|
21
|
+
{ pattern: /jku\s*[:=]\s*(?:req|request|params|query|body)/gi, name: 'jku (JWK Set URL) from user input - SSRF', severity: 'CRITICAL' },
|
|
22
|
+
{ pattern: /x5u\s*[:=]\s*(?:req|request|params|query|body)/gi, name: 'x5u URL from user input', severity: 'CRITICAL' },
|
|
23
|
+
{ pattern: /x5c\s*[:=]\s*(?:req|request|params|query|body)/gi, name: 'x5c certificate from user input', severity: 'CRITICAL' },
|
|
24
|
+
{ pattern: /algorithm\s*[:=]\s*['"]none['"]|alg\s*:\s*['"]none['"]/gi, name: 'Algorithm "none" accepted', severity: 'CRITICAL' },
|
|
25
|
+
{ pattern: /algorithms\s*:\s*\[.*['"]HS256['"].*\]|algorithm\s*:\s*['"]HS256['"]/gi, name: 'HS256 algorithm (key confusion vector)', severity: 'HIGH' },
|
|
26
|
+
{ pattern: /(?:verify|validate)\s*\(\s*(?:token|jwt)\s*,\s*(?:secret|key)\s*,?\s*\{[^}]*algorithms?\s*:\s*\[/gi, name: 'JWT verify with algorithm whitelist', severity: 'INFO' },
|
|
27
|
+
{ pattern: /jwt\.(?:decode|sign|verify).*complete\s*:\s*true/gi, name: 'jwt.io format (complete decode)', severity: 'LOW' },
|
|
28
|
+
{ pattern: /jsonwebtoken|jose|jwk-to-pem|jwt-simple|njwt/gi, name: 'JWT library usage', severity: 'INFO' },
|
|
29
|
+
{ pattern: /(?:publicKey|public_key|pubkey)\s*[:=]\s*(?:req|request|params|query|body)/gi, name: 'Public key from user input (key confusion)', severity: 'CRITICAL' },
|
|
30
|
+
{ pattern: /(?:jwt|token|bearer).*(?:header|payload)\s*.*(?:decode|parse|split|extract)\s*\(/gi, name: 'JWT header/payload parsing (check algorithm handling)', severity: 'MEDIUM' },
|
|
31
|
+
{ pattern: /jwt\.sign\s*\(\s*[^,]*,\s*['"][a-zA-Z0-9]{1,16}['"]/gi, name: 'Weak JWT signing secret (<16 chars)', severity: 'HIGH' },
|
|
32
|
+
{ pattern: /(?:secret|jwtSecret|JWT_SECRET)\s*=\s*['"]?[a-zA-Z0-9]{1,24}['"]?/gi, name: 'Short JWT secret in code', severity: 'HIGH' },
|
|
33
|
+
{ pattern: /crypto\.createPublicKey|crypto\.createPrivateKey/gi, name: 'crypto key creation (check source)', severity: 'INFO' },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
for (const { pattern, name, severity } of patterns) {
|
|
37
|
+
let match;
|
|
38
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
39
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
40
|
+
const line = lines[lineNum - 1]?.trim() || '';
|
|
41
|
+
if (line.startsWith('//') || line.startsWith('#') || line.startsWith('*')) continue;
|
|
42
|
+
|
|
43
|
+
addFinding(
|
|
44
|
+
severity,
|
|
45
|
+
'JWT Advanced Attacks',
|
|
46
|
+
name,
|
|
47
|
+
`File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
|
|
48
|
+
'Whitelist allowed algorithms (e.g., ["RS256"]). Never use "none" algorithm. Do not accept kid/jwk/jku/x5u from untrusted input. Use asymmetric signing (RS256/ES256). Validate kid against a trusted key store, not a file path. Check for key confusion: if using RS256 ensure public key is not accepted as HMAC secret.'
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch {}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getFiles(dir, files = []) {
|
|
57
|
+
try {
|
|
58
|
+
const entries = readdirSync(dir);
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
|
|
61
|
+
const fullPath = join(dir, entry);
|
|
62
|
+
try {
|
|
63
|
+
const stat = statSync(fullPath);
|
|
64
|
+
if (stat.isDirectory()) getFiles(fullPath, files);
|
|
65
|
+
else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
|
|
66
|
+
} catch {}
|
|
67
|
+
}
|
|
68
|
+
} catch {}
|
|
69
|
+
return files;
|
|
70
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
|
+
import { addFinding } from '../core/findings.js';
|
|
4
|
+
|
|
5
|
+
const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.py', '.rb', '.php', '.go', '.java'];
|
|
6
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
|
|
7
|
+
|
|
8
|
+
export async function auditLlmAi(projectPath, spinner) {
|
|
9
|
+
spinner.text = 'Scanning for AI/LLM prompt injection vectors...';
|
|
10
|
+
const files = getFiles(projectPath);
|
|
11
|
+
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
try {
|
|
14
|
+
const content = readFileSync(file, 'utf-8');
|
|
15
|
+
const relativePath = file.replace(projectPath, '.');
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
|
|
18
|
+
const patterns = [
|
|
19
|
+
{ pattern: /system.?prompt|systemPrompt|system_message/gi, name: 'System prompt defined (extraction target)', severity: 'INFO' },
|
|
20
|
+
{ pattern: /(?:chat|completion|completions|generate|ask|query).*(?:req|request|params|query|body|user|input|message)/gi, name: 'LLM completion with user input', severity: 'HIGH' },
|
|
21
|
+
{ pattern: /(?:tools|functions|function_call|tool_choice).*(?:req|request|params|query|body)/gi, name: 'LLM tool call from user input', severity: 'CRITICAL' },
|
|
22
|
+
{ pattern: /(?:ignore|forget|disregard).*(?:previous|above|instructions|rules|prompt)/gi, name: 'No prompt injection guard (ignore instructions)', severity: 'HIGH' },
|
|
23
|
+
{ pattern: /fetch_url|open_url|read_file|execute_code|run_shell/gi, name: 'LLM tool that can fetch/execute', severity: 'CRITICAL' },
|
|
24
|
+
{ pattern: /(?:RAG|retrieval|embedding|vector_store|pinecone|chroma|weaviate|qdrant)/gi, name: 'RAG pipeline (indirect injection target)', severity: 'MEDIUM' },
|
|
25
|
+
{ pattern: /(?:anthropic|openai|cohere|googleai|gemini|vertex.?ai|claude|gpt)/gi, name: 'AI provider library usage', severity: 'INFO' },
|
|
26
|
+
{ pattern: /(?:function_call|functionCall|tool_use|toolUse).*(?:auto|any)/gi, name: 'LLM auto-invokes tools (dangerous)', severity: 'HIGH' },
|
|
27
|
+
{ pattern: /(?:system|instruction|prompt)\s*[:=]\s*['"`][^'"`]{20,}/gi, name: 'System prompt hardcoded in source', severity: 'MEDIUM' },
|
|
28
|
+
{ pattern: /(?:max_tokens|maxTokens|max_completion_tokens).*(?:req|request|body)/gi, name: 'LLM token limit from user', severity: 'LOW' },
|
|
29
|
+
{ pattern: /(?:assistant|agent|copilot|chatbot|llm).*(?:reply|respond|say|write|tell|send)/gi, name: 'LLM agent output pipeline', severity: 'INFO' },
|
|
30
|
+
{ pattern: /\\\\u[eE][0-9a-fA-F]|unicode.*tag.*block|U\+E[0-9A-F]{4}/gi, name: 'Unicode/ASCII smuggling tech reference', severity: 'LOW' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
for (const { pattern, name, severity } of patterns) {
|
|
34
|
+
let match;
|
|
35
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
36
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
37
|
+
const line = lines[lineNum - 1]?.trim() || '';
|
|
38
|
+
if (line.startsWith('//') || line.startsWith('#') || line.startsWith('*')) continue;
|
|
39
|
+
|
|
40
|
+
addFinding(
|
|
41
|
+
severity,
|
|
42
|
+
'AI/LLM Prompt Injection',
|
|
43
|
+
name,
|
|
44
|
+
`File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
|
|
45
|
+
'Use structured input/output separation. Do not concatenate user input into system prompts. Restrict tool-use to safe operations. Rate-limit LLM interactions. Sanitize RAG document inputs (indirect injection). Never let LLM execute code directly.'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getFiles(dir, files = []) {
|
|
54
|
+
try {
|
|
55
|
+
const entries = readdirSync(dir);
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
|
|
58
|
+
const fullPath = join(dir, entry);
|
|
59
|
+
try {
|
|
60
|
+
const stat = statSync(fullPath);
|
|
61
|
+
if (stat.isDirectory()) getFiles(fullPath, files);
|
|
62
|
+
else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
} catch {}
|
|
66
|
+
return files;
|
|
67
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
|
+
import { addFinding } from '../core/findings.js';
|
|
4
|
+
|
|
5
|
+
const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.py', '.rb', '.php', '.go', '.java', '.c', '.cpp', '.cs'];
|
|
6
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
|
|
7
|
+
|
|
8
|
+
export async function auditPaddingOracle(projectPath, spinner) {
|
|
9
|
+
spinner.text = 'Scanning for padding/comression oracle vulnerabilities...';
|
|
10
|
+
const files = getFiles(projectPath);
|
|
11
|
+
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
try {
|
|
14
|
+
const content = readFileSync(file, 'utf-8');
|
|
15
|
+
const relativePath = file.replace(projectPath, '.');
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
|
|
18
|
+
const patterns = [
|
|
19
|
+
{ pattern: /(?:decrypt|decipher|unpad)\s*\([^)]*\)\s*(?:catch|if.*error)/gi, name: 'Decryption with error handling (padding oracle)', severity: 'HIGH' },
|
|
20
|
+
{ pattern: /(?:padding|decrypt)\s*(?:bad|invalid|wrong|error|fail)/gi, name: 'Decryption error message (padding oracle indicator)', severity: 'HIGH' },
|
|
21
|
+
{ pattern: /CBC|cipher\.final|cipher\.update/gi, name: 'CBC mode encryption (padding oracle vector)', severity: 'MEDIUM' },
|
|
22
|
+
{ pattern: /createDecipheriv\s*\(\s*['"](?:aes|des)-.*-(?:cbc|ecb)/gi, name: 'CBC/ECB mode decrypt (potential padding oracle)', severity: 'MEDIUM' },
|
|
23
|
+
{ pattern: /(?:error|exception|err)\s*\(\s*['"](?:bad|invalid|wrong) (?:padding|decrypt|cipher)/gi, name: 'Padding error in response', severity: 'HIGH' },
|
|
24
|
+
{ pattern: /OAEP|RSASSA-PSS|RSAES-OAEP/gi, name: 'RSA OAEP padding (good)', severity: 'INFO' },
|
|
25
|
+
{ pattern: /PKCS|pkcs/i, name: 'PKCS padding reference', severity: 'INFO' },
|
|
26
|
+
{ pattern: /gzip|deflate|compress|zlib|Content-Encoding/gi, name: 'Compression usage (CRIME/BREACH vector)', severity: 'LOW' },
|
|
27
|
+
{ pattern: /(?:token|session|cookie|jwt).*(?:compress|gzip|deflate)/gi, name: 'Compression of secrets/tokens (CRIME/BREACH)', severity: 'MEDIUM' },
|
|
28
|
+
{ pattern: /(?:https|tls|ssl).*(?:compress|gzip|deflate)/gi, name: 'TLS compression reference (CRIME)', severity: 'MEDIUM' },
|
|
29
|
+
{ pattern: /\bcrypto\.createDecipheriv\b/gi, name: 'Node.js createDecipheriv (check error handling)', severity: 'MEDIUM' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
for (const { pattern, name, severity } of patterns) {
|
|
33
|
+
let match;
|
|
34
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
35
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
36
|
+
const line = lines[lineNum - 1]?.trim() || '';
|
|
37
|
+
if (line.startsWith('//') || line.startsWith('#') || line.startsWith('*')) continue;
|
|
38
|
+
|
|
39
|
+
addFinding(
|
|
40
|
+
severity,
|
|
41
|
+
'Padding / Compression Oracle',
|
|
42
|
+
name,
|
|
43
|
+
`File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
|
|
44
|
+
'Use authenticated encryption (AES-GCM, ChaCha20-Poly1305) instead of CBC. Never leak decryption errors to clients. Add random MAC before encrypting. Disable TLS compression. Avoid compressing response bodies that contain secrets or CSRF tokens.'
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch {}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getFiles(dir, files = []) {
|
|
53
|
+
try {
|
|
54
|
+
const entries = readdirSync(dir);
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
|
|
57
|
+
const fullPath = join(dir, entry);
|
|
58
|
+
try {
|
|
59
|
+
const stat = statSync(fullPath);
|
|
60
|
+
if (stat.isDirectory()) getFiles(fullPath, files);
|
|
61
|
+
else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
} catch {}
|
|
65
|
+
return files;
|
|
66
|
+
}
|