xploitscan 1.0.0 → 1.0.2
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/dist/{api-Z7VNGPT2.js → api-ZNWEMMEL.js} +8 -2
- package/dist/{chunk-CBDFSACC.js → chunk-IHRV7UHG.js} +57 -2
- package/dist/chunk-IHRV7UHG.js.map +1 -0
- package/dist/index.js +298 -3252
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
- package/dist/chunk-CBDFSACC.js.map +0 -1
- /package/dist/{api-Z7VNGPT2.js.map → api-ZNWEMMEL.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
checkUsage,
|
|
4
|
+
clearProRulesCache,
|
|
4
5
|
clearToken,
|
|
6
|
+
downloadProRulesBundle,
|
|
5
7
|
getStoredToken,
|
|
6
8
|
incrementUsage,
|
|
7
9
|
isAuthenticated,
|
|
10
|
+
loadCachedProRules,
|
|
8
11
|
storeToken,
|
|
9
12
|
syncUser,
|
|
10
13
|
uploadScanResults
|
|
11
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-IHRV7UHG.js";
|
|
12
15
|
|
|
13
16
|
// src/index.ts
|
|
14
17
|
import { Command } from "commander";
|
|
@@ -666,58 +669,6 @@ var nextPublicSecret = {
|
|
|
666
669
|
return matches;
|
|
667
670
|
}
|
|
668
671
|
};
|
|
669
|
-
var firebaseClientConfig = {
|
|
670
|
-
id: "VC012",
|
|
671
|
-
title: "Firebase Config with API Key in Client Code",
|
|
672
|
-
severity: "medium",
|
|
673
|
-
category: "Configuration",
|
|
674
|
-
description: "Firebase config objects in client code expose your API key. While Firebase API keys aren't secret, they should be restricted in the Firebase console.",
|
|
675
|
-
check(content, filePath) {
|
|
676
|
-
if (!/firebase/i.test(content)) return [];
|
|
677
|
-
const patterns = [
|
|
678
|
-
/firebaseConfig\s*=\s*\{[^}]*apiKey\s*:/gi,
|
|
679
|
-
/initializeApp\s*\(\s*\{[^}]*apiKey\s*:/gi
|
|
680
|
-
];
|
|
681
|
-
const matches = [];
|
|
682
|
-
for (const p of patterns) {
|
|
683
|
-
matches.push(...findMatches(
|
|
684
|
-
content,
|
|
685
|
-
p,
|
|
686
|
-
firebaseClientConfig,
|
|
687
|
-
filePath,
|
|
688
|
-
() => "Move Firebase config to environment variables. Restrict the API key in Firebase Console > Project Settings > API restrictions."
|
|
689
|
-
));
|
|
690
|
-
}
|
|
691
|
-
return matches;
|
|
692
|
-
}
|
|
693
|
-
};
|
|
694
|
-
var supabaseAnonAdmin = {
|
|
695
|
-
id: "VC013",
|
|
696
|
-
title: "Supabase Anon Key Used for Admin Operations",
|
|
697
|
-
severity: "high",
|
|
698
|
-
category: "Authorization",
|
|
699
|
-
description: "Using the Supabase anon key for operations that require elevated privileges is insecure.",
|
|
700
|
-
check(content, filePath) {
|
|
701
|
-
if (!/supabase/i.test(content)) return [];
|
|
702
|
-
if (!/anon/i.test(content)) return [];
|
|
703
|
-
if (/service_role/i.test(content)) return [];
|
|
704
|
-
const patterns = [
|
|
705
|
-
/supabase[^.]*\.auth\.admin/gi,
|
|
706
|
-
/supabase[^.]*\.rpc\s*\(/gi
|
|
707
|
-
];
|
|
708
|
-
const matches = [];
|
|
709
|
-
for (const p of patterns) {
|
|
710
|
-
matches.push(...findMatches(
|
|
711
|
-
content,
|
|
712
|
-
p,
|
|
713
|
-
supabaseAnonAdmin,
|
|
714
|
-
filePath,
|
|
715
|
-
() => "Use the service_role key on the server side for admin operations. Never expose it to the client."
|
|
716
|
-
));
|
|
717
|
-
}
|
|
718
|
-
return matches;
|
|
719
|
-
}
|
|
720
|
-
};
|
|
721
672
|
var envNotGitignored = {
|
|
722
673
|
id: "VC014",
|
|
723
674
|
title: ".env File Not in .gitignore",
|
|
@@ -874,46 +825,6 @@ var exposedAuthSecret = {
|
|
|
874
825
|
return matches;
|
|
875
826
|
}
|
|
876
827
|
};
|
|
877
|
-
var insecureElectronWindow = {
|
|
878
|
-
id: "VC019",
|
|
879
|
-
title: "Insecure Electron BrowserWindow Configuration",
|
|
880
|
-
severity: "high",
|
|
881
|
-
category: "Configuration",
|
|
882
|
-
description: "Electron BrowserWindow with nodeIntegration enabled, contextIsolation disabled, or sandbox disabled allows renderer processes to access Node.js APIs, enabling remote code execution.",
|
|
883
|
-
check(content, filePath) {
|
|
884
|
-
if (isTestFile(filePath)) return [];
|
|
885
|
-
if (!/BrowserWindow/i.test(content)) return [];
|
|
886
|
-
const matches = [];
|
|
887
|
-
const patterns = [
|
|
888
|
-
/nodeIntegration\s*:\s*true/g,
|
|
889
|
-
/contextIsolation\s*:\s*false/g,
|
|
890
|
-
/sandbox\s*:\s*false/g,
|
|
891
|
-
/webSecurity\s*:\s*false/g,
|
|
892
|
-
/allowRunningInsecureContent\s*:\s*true/g
|
|
893
|
-
];
|
|
894
|
-
for (const p of patterns) {
|
|
895
|
-
matches.push(...findMatches(
|
|
896
|
-
content,
|
|
897
|
-
p,
|
|
898
|
-
insecureElectronWindow,
|
|
899
|
-
filePath,
|
|
900
|
-
(m) => `Set ${m[0].split(":")[0].trim()}: ${m[0].includes("true") ? "false" : "true"}. Enable contextIsolation, sandbox, and webSecurity; disable nodeIntegration and allowRunningInsecureContent.`
|
|
901
|
-
));
|
|
902
|
-
}
|
|
903
|
-
if (/new\s+BrowserWindow\s*\(/g.test(content)) {
|
|
904
|
-
if (!/sandbox\s*:/i.test(content)) {
|
|
905
|
-
matches.push(...findMatches(
|
|
906
|
-
content,
|
|
907
|
-
/new\s+BrowserWindow\s*\(/g,
|
|
908
|
-
{ ...insecureElectronWindow, title: "Electron BrowserWindow Missing sandbox:true" },
|
|
909
|
-
filePath,
|
|
910
|
-
() => "Add sandbox: true to BrowserWindow webPreferences for defense in depth."
|
|
911
|
-
));
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
return matches;
|
|
915
|
-
}
|
|
916
|
-
};
|
|
917
828
|
var missingCSP = {
|
|
918
829
|
id: "VC020",
|
|
919
830
|
title: "Missing Content Security Policy (CSP)",
|
|
@@ -938,311 +849,6 @@ var missingCSP = {
|
|
|
938
849
|
return [];
|
|
939
850
|
}
|
|
940
851
|
};
|
|
941
|
-
var ipcPathTraversal = {
|
|
942
|
-
id: "VC021",
|
|
943
|
-
title: "IPC/File Handler Without Path Validation",
|
|
944
|
-
severity: "medium",
|
|
945
|
-
category: "Injection",
|
|
946
|
-
description: "IPC handlers that read or write files based on renderer-supplied paths without validation allow path traversal attacks, potentially exposing sensitive files like .ssh keys or .env files.",
|
|
947
|
-
check(content, filePath) {
|
|
948
|
-
if (!/ipcMain\.handle|ipcMain\.on/i.test(content)) return [];
|
|
949
|
-
const matches = [];
|
|
950
|
-
const hasFileOps = /readFile|writeFile|readFileSync|writeFileSync|createReadStream|createWriteStream/i.test(content);
|
|
951
|
-
if (!hasFileOps) return [];
|
|
952
|
-
const hasPathValidation = /(?:path\.resolve|path\.normalize|startsWith|isAbsolute|\.includes\s*\(\s*["'`]\.\.["'`]\s*\)|allowedPaths|safePath|validatePath|sanitizePath)/i.test(content);
|
|
953
|
-
if (!hasPathValidation) {
|
|
954
|
-
matches.push(...findMatches(
|
|
955
|
-
content,
|
|
956
|
-
/ipcMain\.(?:handle|on)\s*\(\s*["'`][^"'`]*(?:read|write|file|save|load|open|export)[^"'`]*["'`]/gi,
|
|
957
|
-
ipcPathTraversal,
|
|
958
|
-
filePath,
|
|
959
|
-
() => "Validate file paths in IPC handlers: ensure paths are within an allowed directory (e.g., app.getPath('userData')), reject paths containing '..', and block access to sensitive directories (.ssh, .env, etc)."
|
|
960
|
-
));
|
|
961
|
-
}
|
|
962
|
-
return matches;
|
|
963
|
-
}
|
|
964
|
-
};
|
|
965
|
-
var unsanitizedHTMLExport = {
|
|
966
|
-
id: "VC022",
|
|
967
|
-
title: "HTML Export/Render Without Sanitization",
|
|
968
|
-
severity: "critical",
|
|
969
|
-
category: "Injection",
|
|
970
|
-
description: "Generating HTML from user content without sanitization (e.g., DOMPurify) allows stored XSS attacks. Malicious content saved in documents could execute scripts when exported or previewed.",
|
|
971
|
-
check(content, filePath) {
|
|
972
|
-
if (isTestFile(filePath)) return [];
|
|
973
|
-
if (/DOMPurify|dompurify/i.test(content)) return [];
|
|
974
|
-
if (filePath.match(/\.(jsx|tsx|vue|svelte)$/) && !/innerHTML|document\.write|\.html\s*=/i.test(content)) return [];
|
|
975
|
-
const hasSanitizer = /sanitize|escapeHtml|escapeHTML|xss|htmlEncode|purify/i.test(content);
|
|
976
|
-
if (hasSanitizer) return [];
|
|
977
|
-
if (!/(?:export|download|save|write|send).*(?:html|HTML)|\.innerHTML\s*=|document\.write|res\.send\s*\(/i.test(content)) return [];
|
|
978
|
-
const matches = [];
|
|
979
|
-
const htmlBuildPatterns = [
|
|
980
|
-
/`<[^`]*\$\{[^}]*(?:content|title|body|text|name|message|description|input|value|data)[^}]*\}[^`]*>`/gi,
|
|
981
|
-
/["']<[^"']*['"]\s*\+\s*(?:content|title|body|text|message|data|doc\.|post\.|article\.)/gi
|
|
982
|
-
];
|
|
983
|
-
for (const p of htmlBuildPatterns) {
|
|
984
|
-
matches.push(...findMatches(
|
|
985
|
-
content,
|
|
986
|
-
p,
|
|
987
|
-
unsanitizedHTMLExport,
|
|
988
|
-
filePath,
|
|
989
|
-
() => "Sanitize user content before embedding in HTML. Use DOMPurify: DOMPurify.sanitize(content). For plain text, use a function to escape HTML entities (<, >, &, quotes)."
|
|
990
|
-
));
|
|
991
|
-
}
|
|
992
|
-
return matches;
|
|
993
|
-
}
|
|
994
|
-
};
|
|
995
|
-
var prototypePollution = {
|
|
996
|
-
id: "VC023",
|
|
997
|
-
title: "Prototype Pollution Risk",
|
|
998
|
-
severity: "high",
|
|
999
|
-
category: "Injection",
|
|
1000
|
-
description: "Parsing JSON from localStorage, URL params, or external sources and merging it into objects without validation can lead to prototype pollution, allowing attackers to inject __proto__ or constructor properties.",
|
|
1001
|
-
check(content, filePath) {
|
|
1002
|
-
const matches = [];
|
|
1003
|
-
const storageParsePatterns = [
|
|
1004
|
-
/JSON\.parse\s*\(\s*(?:localStorage|sessionStorage)\.getItem/g,
|
|
1005
|
-
/JSON\.parse\s*\(\s*window\.localStorage/g
|
|
1006
|
-
];
|
|
1007
|
-
const hasValidation = /schema|validate|sanitize|whitelist|allowedKeys|pick\(|Object\.freeze|zod|yup|joi|ajv/i.test(content);
|
|
1008
|
-
if (hasValidation) return [];
|
|
1009
|
-
const hasUnsafeMerge = /Object\.assign\s*\([^)]*JSON\.parse|\.\.\.JSON\.parse|\{.*\.\.\.(?:stored|saved|cached|parsed|data)/i.test(content);
|
|
1010
|
-
if (hasUnsafeMerge) {
|
|
1011
|
-
matches.push(...findMatches(
|
|
1012
|
-
content,
|
|
1013
|
-
/Object\.assign\s*\([^)]*JSON\.parse|\.\.\.JSON\.parse/g,
|
|
1014
|
-
prototypePollution,
|
|
1015
|
-
filePath,
|
|
1016
|
-
() => "Validate parsed data against an expected schema before merging into objects. Use Object.freeze(), a validation library (Zod, Yup), or manually check for __proto__ and constructor keys."
|
|
1017
|
-
));
|
|
1018
|
-
}
|
|
1019
|
-
for (const p of storageParsePatterns) {
|
|
1020
|
-
matches.push(...findMatches(
|
|
1021
|
-
content,
|
|
1022
|
-
p,
|
|
1023
|
-
prototypePollution,
|
|
1024
|
-
filePath,
|
|
1025
|
-
() => "Validate localStorage data against an expected schema before using it. Malicious extensions or XSS can modify localStorage values."
|
|
1026
|
-
));
|
|
1027
|
-
}
|
|
1028
|
-
return matches;
|
|
1029
|
-
}
|
|
1030
|
-
};
|
|
1031
|
-
var missingFileSizeLimits = {
|
|
1032
|
-
id: "VC024",
|
|
1033
|
-
title: "File Write/Save Without Size Limit",
|
|
1034
|
-
severity: "medium",
|
|
1035
|
-
category: "Availability",
|
|
1036
|
-
description: "File save or upload handlers without size validation can lead to denial-of-service via disk exhaustion or memory exhaustion.",
|
|
1037
|
-
check(content, filePath) {
|
|
1038
|
-
if (!/(?:writeFile|save|upload|export)/i.test(filePath) && !/(?:writeFile|writeFileSync|createWriteStream)/i.test(content)) return [];
|
|
1039
|
-
const hasWriteOps = /(?:ipcMain|app\.(?:post|put)|router\.(?:post|put)).*(?:writeFile|save|export)/is.test(content) || /(?:writeFile|writeFileSync)\s*\(/g.test(content);
|
|
1040
|
-
if (!hasWriteOps) return [];
|
|
1041
|
-
const hasSizeCheck = /(?:size|length|byteLength|bytes)\s*(?:>|>=|<|<=|===)\s*\d|maxSize|MAX_SIZE|sizeLimit|content-length/i.test(content);
|
|
1042
|
-
if (hasSizeCheck) return [];
|
|
1043
|
-
return findMatches(
|
|
1044
|
-
content,
|
|
1045
|
-
/(?:writeFile|writeFileSync)\s*\(/g,
|
|
1046
|
-
missingFileSizeLimits,
|
|
1047
|
-
filePath,
|
|
1048
|
-
() => "Add file size validation before writing. Check content.length or Buffer.byteLength() against a maximum (e.g., 10MB) to prevent disk exhaustion."
|
|
1049
|
-
);
|
|
1050
|
-
}
|
|
1051
|
-
};
|
|
1052
|
-
var unsanitizedFilenames = {
|
|
1053
|
-
id: "VC025",
|
|
1054
|
-
title: "Unsanitized Filename in File Operations",
|
|
1055
|
-
severity: "medium",
|
|
1056
|
-
category: "Injection",
|
|
1057
|
-
description: "Using user-supplied filenames without sanitization in file operations can enable path traversal, overwriting system files, or executing commands via special characters.",
|
|
1058
|
-
check(content, filePath) {
|
|
1059
|
-
const matches = [];
|
|
1060
|
-
const patterns = [
|
|
1061
|
-
/(?:writeFile|writeFileSync|createWriteStream|rename|copyFile)\s*\(\s*(?:`[^`]*\$\{|[^"'`\s,]+\s*\+)/g,
|
|
1062
|
-
/(?:dialog\.showSaveDialog|saveDialog).*(?:defaultPath|fileName)\s*:\s*(?!["'`])/g,
|
|
1063
|
-
/\.download\s*=\s*(?!["'`])/g
|
|
1064
|
-
];
|
|
1065
|
-
const hasSanitization = /sanitize|cleanFilename|safeFilename|replace\s*\(\s*\/\[.*\]\//i.test(content);
|
|
1066
|
-
if (hasSanitization) return [];
|
|
1067
|
-
for (const p of patterns) {
|
|
1068
|
-
matches.push(...findMatches(
|
|
1069
|
-
content,
|
|
1070
|
-
p,
|
|
1071
|
-
unsanitizedFilenames,
|
|
1072
|
-
filePath,
|
|
1073
|
-
() => "Sanitize filenames before use: strip path separators (/ \\), special chars, and '..' sequences. Example: name.replace(/[^a-zA-Z0-9._-]/g, '_')"
|
|
1074
|
-
));
|
|
1075
|
-
}
|
|
1076
|
-
return matches;
|
|
1077
|
-
}
|
|
1078
|
-
};
|
|
1079
|
-
var electronNavigationUnrestricted = {
|
|
1080
|
-
id: "VC026",
|
|
1081
|
-
title: "Electron: External Navigation Not Blocked",
|
|
1082
|
-
severity: "medium",
|
|
1083
|
-
category: "Configuration",
|
|
1084
|
-
description: "Electron apps that don't block navigation to external URLs or new window creation are vulnerable to phishing and drive-by downloads. Malicious links in app content can redirect the entire app to an attacker's site.",
|
|
1085
|
-
check(content, filePath) {
|
|
1086
|
-
if (!/BrowserWindow|electron/i.test(content)) return [];
|
|
1087
|
-
if (!/main|index/i.test(filePath)) return [];
|
|
1088
|
-
const hasNavBlock = /will-navigate|new-window|setWindowOpenHandler|webContents\.on.*navigate/i.test(content);
|
|
1089
|
-
if (hasNavBlock) return [];
|
|
1090
|
-
if (/new\s+BrowserWindow/i.test(content)) {
|
|
1091
|
-
return findMatches(
|
|
1092
|
-
content,
|
|
1093
|
-
/new\s+BrowserWindow\s*\(/g,
|
|
1094
|
-
electronNavigationUnrestricted,
|
|
1095
|
-
filePath,
|
|
1096
|
-
() => "Block external navigation: win.webContents.on('will-navigate', (e, url) => { if (!url.startsWith('file://')) e.preventDefault(); }); and use setWindowOpenHandler to block new windows."
|
|
1097
|
-
);
|
|
1098
|
-
}
|
|
1099
|
-
return [];
|
|
1100
|
-
}
|
|
1101
|
-
};
|
|
1102
|
-
var missingSecurityMeta = {
|
|
1103
|
-
id: "VC027",
|
|
1104
|
-
title: "Missing Security Meta Tags / Headers",
|
|
1105
|
-
severity: "medium",
|
|
1106
|
-
category: "Configuration",
|
|
1107
|
-
description: "HTML pages without X-Content-Type-Options, referrer policy, or other security meta tags are more susceptible to MIME-sniffing attacks and information leakage.",
|
|
1108
|
-
check(content, filePath) {
|
|
1109
|
-
if (!filePath.match(/\.(html|htm)$/)) return [];
|
|
1110
|
-
const matches = [];
|
|
1111
|
-
if (!/X-Content-Type-Options/i.test(content) && !/<meta[^>]*nosniff/i.test(content)) {
|
|
1112
|
-
matches.push({
|
|
1113
|
-
rule: "VC027",
|
|
1114
|
-
title: "Missing X-Content-Type-Options Header",
|
|
1115
|
-
severity: "medium",
|
|
1116
|
-
category: "Configuration",
|
|
1117
|
-
file: filePath,
|
|
1118
|
-
line: 1,
|
|
1119
|
-
snippet: getSnippet(content, 1),
|
|
1120
|
-
fix: 'Add <meta http-equiv="X-Content-Type-Options" content="nosniff"> to prevent MIME-type sniffing.'
|
|
1121
|
-
});
|
|
1122
|
-
}
|
|
1123
|
-
if (!/referrer/i.test(content)) {
|
|
1124
|
-
matches.push({
|
|
1125
|
-
rule: "VC027",
|
|
1126
|
-
title: "Missing Referrer Policy",
|
|
1127
|
-
severity: "medium",
|
|
1128
|
-
category: "Configuration",
|
|
1129
|
-
file: filePath,
|
|
1130
|
-
line: 1,
|
|
1131
|
-
snippet: getSnippet(content, 1),
|
|
1132
|
-
fix: 'Add <meta name="referrer" content="no-referrer"> or "strict-origin-when-cross-origin" to limit referrer leakage.'
|
|
1133
|
-
});
|
|
1134
|
-
}
|
|
1135
|
-
return matches;
|
|
1136
|
-
}
|
|
1137
|
-
};
|
|
1138
|
-
var unvalidatedAPIParams = {
|
|
1139
|
-
id: "VC028",
|
|
1140
|
-
title: "Unvalidated API Request Parameters",
|
|
1141
|
-
severity: "high",
|
|
1142
|
-
category: "Injection",
|
|
1143
|
-
description: "API requests constructed with unvalidated user input (API keys, model names, URLs) can be exploited for injection attacks or unauthorized access to different API models/endpoints.",
|
|
1144
|
-
check(content, filePath) {
|
|
1145
|
-
const matches = [];
|
|
1146
|
-
const apiKeyPatterns = [
|
|
1147
|
-
/(?:apiKey|api_key|authorization)\s*[:=]\s*(?:req\.body|req\.query|params|input|formData|body)\./gi,
|
|
1148
|
-
/headers\s*:\s*\{[^}]*Authorization\s*:\s*(?!["'`]Bearer\s)/gi
|
|
1149
|
-
];
|
|
1150
|
-
const hasValidation = /validate|sanitize|regex|test\(|match\(|pattern|allowList|whitelist|enum|includes\(/i.test(content);
|
|
1151
|
-
if (hasValidation) return [];
|
|
1152
|
-
if (/model\s*[:=]\s*(?:req\.body|params|input|body)\./i.test(content) || /model\s*[:=]\s*(?!["'`])[a-z]/i.test(content)) {
|
|
1153
|
-
const hasModelValidation = /allowedModels|validModels|models\s*\.\s*includes|model.*(?:===|!==|includes)/i.test(content);
|
|
1154
|
-
if (!hasModelValidation && /(?:openai|anthropic|claude|gpt|llm)/i.test(content)) {
|
|
1155
|
-
matches.push(...findMatches(
|
|
1156
|
-
content,
|
|
1157
|
-
/model\s*[:=]\s*(?:req\.body|params|input|body)\./gi,
|
|
1158
|
-
unvalidatedAPIParams,
|
|
1159
|
-
filePath,
|
|
1160
|
-
() => "Validate model selection against an allowlist of approved models. Example: const ALLOWED_MODELS = ['gpt-4', 'claude-3']; if (!ALLOWED_MODELS.includes(model)) throw new Error('Invalid model');"
|
|
1161
|
-
));
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
for (const p of apiKeyPatterns) {
|
|
1165
|
-
matches.push(...findMatches(
|
|
1166
|
-
content,
|
|
1167
|
-
p,
|
|
1168
|
-
unvalidatedAPIParams,
|
|
1169
|
-
filePath,
|
|
1170
|
-
() => "Validate API key format before using it (e.g., check prefix and length). Never pass user-supplied API keys directly to third-party services without validation."
|
|
1171
|
-
));
|
|
1172
|
-
}
|
|
1173
|
-
return matches;
|
|
1174
|
-
}
|
|
1175
|
-
};
|
|
1176
|
-
var unvalidatedEventData = {
|
|
1177
|
-
id: "VC029",
|
|
1178
|
-
title: "Unvalidated Event or PostMessage Data",
|
|
1179
|
-
severity: "medium",
|
|
1180
|
-
category: "Injection",
|
|
1181
|
-
description: "Custom events, postMessage, or IPC message data used without type-checking can lead to injection attacks or unexpected behavior when malicious data is sent through event channels.",
|
|
1182
|
-
check(content, filePath) {
|
|
1183
|
-
const matches = [];
|
|
1184
|
-
if (/addEventListener\s*\(\s*["'`]message["'`]/i.test(content)) {
|
|
1185
|
-
if (!/event\.origin|e\.origin|message\.origin/i.test(content)) {
|
|
1186
|
-
matches.push(...findMatches(
|
|
1187
|
-
content,
|
|
1188
|
-
/addEventListener\s*\(\s*["'`]message["'`]/g,
|
|
1189
|
-
unvalidatedEventData,
|
|
1190
|
-
filePath,
|
|
1191
|
-
() => "Always verify event.origin in message event handlers to prevent cross-origin attacks. Example: if (event.origin !== 'https://trusted.com') return;"
|
|
1192
|
-
));
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
if (/new\s+CustomEvent\s*\(/i.test(content) || /ipcRenderer\.send/i.test(content)) {
|
|
1196
|
-
const hasTypeCheck = /typeof\s|instanceof|z\.|schema|validate|Number\.isFinite|parseInt|parseFloat/i.test(content);
|
|
1197
|
-
if (!hasTypeCheck) {
|
|
1198
|
-
matches.push(...findMatches(
|
|
1199
|
-
content,
|
|
1200
|
-
/new\s+CustomEvent\s*\(/g,
|
|
1201
|
-
unvalidatedEventData,
|
|
1202
|
-
filePath,
|
|
1203
|
-
() => "Type-check custom event data before using it. Validate that data.detail contains expected types to prevent injection."
|
|
1204
|
-
));
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
return matches;
|
|
1208
|
-
}
|
|
1209
|
-
};
|
|
1210
|
-
var insecureDeserialization = {
|
|
1211
|
-
id: "VC030",
|
|
1212
|
-
title: "Insecure Deserialization",
|
|
1213
|
-
severity: "critical",
|
|
1214
|
-
category: "Injection",
|
|
1215
|
-
description: "Deserializing untrusted data (pickle, unserialize, yaml.load) can execute arbitrary code. Attackers craft malicious payloads to gain remote code execution.",
|
|
1216
|
-
check(content, filePath) {
|
|
1217
|
-
const matches = [];
|
|
1218
|
-
const patterns = [
|
|
1219
|
-
// Python pickle
|
|
1220
|
-
/pickle\.loads?\s*\(/g,
|
|
1221
|
-
/cPickle\.loads?\s*\(/g,
|
|
1222
|
-
// PHP unserialize
|
|
1223
|
-
/unserialize\s*\(/g,
|
|
1224
|
-
// Ruby Marshal
|
|
1225
|
-
/Marshal\.load\s*\(/g,
|
|
1226
|
-
// YAML unsafe load (Python)
|
|
1227
|
-
/yaml\.load\s*\([^)]*(?!Loader\s*=\s*yaml\.SafeLoader)/g,
|
|
1228
|
-
/yaml\.unsafe_load\s*\(/g,
|
|
1229
|
-
// Java ObjectInputStream
|
|
1230
|
-
/ObjectInputStream\s*\(/g,
|
|
1231
|
-
// Node.js node-serialize
|
|
1232
|
-
/serialize\.unserialize\s*\(/g
|
|
1233
|
-
];
|
|
1234
|
-
for (const p of patterns) {
|
|
1235
|
-
matches.push(...findMatches(
|
|
1236
|
-
content,
|
|
1237
|
-
p,
|
|
1238
|
-
insecureDeserialization,
|
|
1239
|
-
filePath,
|
|
1240
|
-
() => "Never deserialize untrusted data. Use JSON instead of pickle/Marshal/unserialize. For YAML, use yaml.safe_load(). Validate and sanitize all input before deserialization."
|
|
1241
|
-
));
|
|
1242
|
-
}
|
|
1243
|
-
return matches;
|
|
1244
|
-
}
|
|
1245
|
-
};
|
|
1246
852
|
var hardcodedJWTSecret = {
|
|
1247
853
|
id: "VC031",
|
|
1248
854
|
title: "Hardcoded JWT Secret",
|
|
@@ -1359,33 +965,6 @@ var insecureRandomness = {
|
|
|
1359
965
|
return matches;
|
|
1360
966
|
}
|
|
1361
967
|
};
|
|
1362
|
-
var openRedirectParams = {
|
|
1363
|
-
id: "VC035",
|
|
1364
|
-
title: "Open Redirect via URL Parameters",
|
|
1365
|
-
severity: "high",
|
|
1366
|
-
category: "Injection",
|
|
1367
|
-
description: "Redirect parameters like ?redirect_url=, ?return_to=, ?next= passed directly to redirects enable phishing attacks.",
|
|
1368
|
-
check(content, filePath) {
|
|
1369
|
-
const matches = [];
|
|
1370
|
-
const patterns = [
|
|
1371
|
-
// Reading redirect-like query params and using in redirect
|
|
1372
|
-
/(?:redirect_url|redirect_uri|return_to|return_url|next|callback_url|continue|goto|target|dest|destination|forward|redir)\s*(?:=|:)\s*(?:req\.query|req\.params|searchParams|query|params)\./gi,
|
|
1373
|
-
/redirect\s*\(\s*(?:req\.query|req\.params|searchParams\.get)\s*\(\s*["'`](?:redirect|return|next|callback|url|goto)/gi
|
|
1374
|
-
];
|
|
1375
|
-
const hasValidation = /allowedUrls|allowedDomains|allowedHosts|validUrl|safeDomain|whitelist|startsWith.*https|new URL.*hostname/i.test(content);
|
|
1376
|
-
if (hasValidation) return [];
|
|
1377
|
-
for (const p of patterns) {
|
|
1378
|
-
matches.push(...findMatches(
|
|
1379
|
-
content,
|
|
1380
|
-
p,
|
|
1381
|
-
openRedirectParams,
|
|
1382
|
-
filePath,
|
|
1383
|
-
() => "Validate redirect URLs against an allowlist of trusted domains. Use: const url = new URL(input); if (!ALLOWED_HOSTS.includes(url.hostname)) reject."
|
|
1384
|
-
));
|
|
1385
|
-
}
|
|
1386
|
-
return matches;
|
|
1387
|
-
}
|
|
1388
|
-
};
|
|
1389
968
|
var missingErrorBoundary = {
|
|
1390
969
|
id: "VC036",
|
|
1391
970
|
title: "React App Missing Error Boundary",
|
|
@@ -1449,29 +1028,6 @@ var exposedStackTraces = {
|
|
|
1449
1028
|
return matches;
|
|
1450
1029
|
}
|
|
1451
1030
|
};
|
|
1452
|
-
var insecureFileUpload = {
|
|
1453
|
-
id: "VC038",
|
|
1454
|
-
title: "Insecure File Upload Validation",
|
|
1455
|
-
severity: "high",
|
|
1456
|
-
category: "Injection",
|
|
1457
|
-
description: "File uploads validated only by extension (not MIME type or content) allow attackers to upload executable files disguised as images or documents.",
|
|
1458
|
-
check(content, filePath) {
|
|
1459
|
-
if (!/upload|multer|formidable|busboy|multipart/i.test(content)) return [];
|
|
1460
|
-
const matches = [];
|
|
1461
|
-
const hasExtCheck = /\.(?:endsWith|match|test)\s*\([^)]*(?:\.jpg|\.png|\.pdf|\.doc|ext)/i.test(content);
|
|
1462
|
-
const hasMimeCheck = /mimetype|content-type|file\.type|mime|magic\.detect|file-type/i.test(content);
|
|
1463
|
-
if (hasExtCheck && !hasMimeCheck) {
|
|
1464
|
-
matches.push(...findMatches(
|
|
1465
|
-
content,
|
|
1466
|
-
/upload|multer|formidable|busboy/gi,
|
|
1467
|
-
insecureFileUpload,
|
|
1468
|
-
filePath,
|
|
1469
|
-
() => "Validate file uploads by MIME type AND magic bytes, not just extension. Use the 'file-type' package to detect actual file type from content. Also enforce size limits."
|
|
1470
|
-
));
|
|
1471
|
-
}
|
|
1472
|
-
return matches;
|
|
1473
|
-
}
|
|
1474
|
-
};
|
|
1475
1031
|
var missingLockFile = {
|
|
1476
1032
|
id: "VC039",
|
|
1477
1033
|
title: "Missing Dependency Lock File",
|
|
@@ -1493,2711 +1049,332 @@ var missingLockFile = {
|
|
|
1493
1049
|
return [];
|
|
1494
1050
|
}
|
|
1495
1051
|
};
|
|
1496
|
-
var
|
|
1497
|
-
id: "
|
|
1498
|
-
title: "
|
|
1499
|
-
severity: "critical",
|
|
1500
|
-
category: "Information Leakage",
|
|
1501
|
-
description: "Web server configs that don't block access to .git directories expose your entire source code, commit history, secrets, and credentials.",
|
|
1502
|
-
check(content, filePath) {
|
|
1503
|
-
if (!filePath.match(/(?:nginx|apache|httpd|caddy|\.htaccess|vercel\.json|netlify\.toml|server\.[jt]s)/i)) return [];
|
|
1504
|
-
if (/(?:static|serve|express\.static|serveStatic|public)/i.test(content)) {
|
|
1505
|
-
const blocksGit = /\.git|dotfiles|hidden/i.test(content);
|
|
1506
|
-
if (!blocksGit) {
|
|
1507
|
-
return findMatches(
|
|
1508
|
-
content,
|
|
1509
|
-
/(?:static|serve|express\.static|serveStatic)\s*\(/g,
|
|
1510
|
-
exposedGitDir,
|
|
1511
|
-
filePath,
|
|
1512
|
-
() => "Block access to .git and other dotfiles in your static file server config. For Express: app.use('/.git', (req, res) => res.status(403).end()). For Nginx: location ~ /\\.git { deny all; }"
|
|
1513
|
-
);
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
return [];
|
|
1517
|
-
}
|
|
1518
|
-
};
|
|
1519
|
-
var ssrfVulnerability = {
|
|
1520
|
-
id: "VC041",
|
|
1521
|
-
title: "Potential Server-Side Request Forgery (SSRF)",
|
|
1052
|
+
var weakHashing = {
|
|
1053
|
+
id: "VC060",
|
|
1054
|
+
title: "Weak Hashing Algorithm for Passwords",
|
|
1522
1055
|
severity: "critical",
|
|
1523
|
-
category: "
|
|
1524
|
-
description: "
|
|
1056
|
+
category: "Cryptography",
|
|
1057
|
+
description: "MD5 and SHA1/SHA256 are too fast for password hashing \u2014 they can be brute-forced at billions of attempts per second. Use bcrypt, scrypt, or argon2 instead.",
|
|
1525
1058
|
check(content, filePath) {
|
|
1526
|
-
if (
|
|
1527
|
-
|
|
1528
|
-
if (!isServerFile) return [];
|
|
1059
|
+
if (filePath.includes("test") || filePath.includes("mock")) return [];
|
|
1060
|
+
if (/bcrypt|scrypt|argon2/i.test(content)) return [];
|
|
1529
1061
|
const matches = [];
|
|
1530
1062
|
const patterns = [
|
|
1531
|
-
/(?:
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
const rawMatches = findMatches(
|
|
1537
|
-
content,
|
|
1538
|
-
p,
|
|
1539
|
-
ssrfVulnerability,
|
|
1540
|
-
filePath,
|
|
1541
|
-
() => "Validate URLs against an allowlist before fetching. Block internal IPs: 127.0.0.1, 10.x, 172.16-31.x, 192.168.x, 169.254.169.254 (cloud metadata). Use: const url = new URL(input); if (!ALLOWED_HOSTS.includes(url.hostname)) throw new Error('Blocked');"
|
|
1542
|
-
);
|
|
1543
|
-
for (const rm3 of rawMatches) {
|
|
1544
|
-
const lineText = content.split("\n")[rm3.line - 1] || "";
|
|
1545
|
-
const varMatch = lineText.match(/(?:fetch|axios\.\w+|got|request|https?\.get)\s*\(\s*(\w+)/);
|
|
1546
|
-
if (varMatch) {
|
|
1547
|
-
const varName = varMatch[1];
|
|
1548
|
-
const constDef = new RegExp(`const\\s+${varName}\\s*=\\s*["'\`]https?://`, "i");
|
|
1549
|
-
if (constDef.test(content)) continue;
|
|
1550
|
-
}
|
|
1551
|
-
matches.push(rm3);
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
return matches;
|
|
1555
|
-
}
|
|
1556
|
-
};
|
|
1557
|
-
var massAssignment = {
|
|
1558
|
-
id: "VC042",
|
|
1559
|
-
title: "Mass Assignment Vulnerability",
|
|
1560
|
-
severity: "high",
|
|
1561
|
-
category: "Authorization",
|
|
1562
|
-
description: "Spreading or assigning request body directly into database models allows attackers to set fields they shouldn't (e.g., isAdmin, role, verified).",
|
|
1563
|
-
check(content, filePath) {
|
|
1564
|
-
const isApiFile = /(?:\/api\/|routes?\/|controllers?\/|server\.|handler)/i.test(filePath);
|
|
1565
|
-
if (!isApiFile) return [];
|
|
1566
|
-
const matches = [];
|
|
1567
|
-
const patterns = [
|
|
1568
|
-
// Object.assign(model, req.body)
|
|
1569
|
-
/Object\.assign\s*\(\s*(?:user|account|profile|record|doc|model|entity)[^,]*,\s*(?:req\.body|body|input|data)\s*\)/gi,
|
|
1570
|
-
// Spread req.body into create/update
|
|
1571
|
-
/(?:create|update|insert|save|findOneAndUpdate|updateOne|upsert)\s*\(\s*\{[^}]*\.\.\.(?:req\.body|body|input|data)/gi,
|
|
1572
|
-
// Direct req.body into DB
|
|
1573
|
-
/(?:create|insert|save)\s*\(\s*(?:req\.body|body)\s*\)/gi
|
|
1063
|
+
/(?:md5|sha1|sha256|sha512)\s*\([^)]*(?:password|passwd|pwd)/gi,
|
|
1064
|
+
/createHash\s*\(\s*["'`](?:md5|sha1|sha256)["'`]\).*(?:password|passwd|pwd)/gi,
|
|
1065
|
+
/(?:password|passwd|pwd).*createHash\s*\(\s*["'`](?:md5|sha1|sha256)["'`]\)/gi,
|
|
1066
|
+
/hashlib\.(?:md5|sha1|sha256)\s*\([^)]*(?:password|passwd|pwd)/gi,
|
|
1067
|
+
/Digest::(?:MD5|SHA1|SHA256).*(?:password|passwd|pwd)/gi
|
|
1574
1068
|
];
|
|
1575
|
-
const hasSanitization = /pick\(|omit\(|allowedFields|sanitize|whitelist|permit|strong_params/i.test(content);
|
|
1576
|
-
if (hasSanitization) return [];
|
|
1577
1069
|
for (const p of patterns) {
|
|
1578
1070
|
matches.push(...findMatches(
|
|
1579
1071
|
content,
|
|
1580
1072
|
p,
|
|
1581
|
-
|
|
1073
|
+
weakHashing,
|
|
1582
1074
|
filePath,
|
|
1583
|
-
() => "
|
|
1075
|
+
() => "Use bcrypt, scrypt, or argon2 for password hashing \u2014 they're intentionally slow. Example: const hash = await bcrypt.hash(password, 12);"
|
|
1584
1076
|
));
|
|
1585
1077
|
}
|
|
1586
1078
|
return matches;
|
|
1587
1079
|
}
|
|
1588
1080
|
};
|
|
1589
|
-
var
|
|
1590
|
-
id: "
|
|
1591
|
-
title: "
|
|
1592
|
-
severity: "
|
|
1081
|
+
var disabledTLSVerification = {
|
|
1082
|
+
id: "VC061",
|
|
1083
|
+
title: "Disabled TLS Certificate Verification",
|
|
1084
|
+
severity: "critical",
|
|
1593
1085
|
category: "Cryptography",
|
|
1594
|
-
description: "
|
|
1086
|
+
description: "Disabling TLS certificate verification (NODE_TLS_REJECT_UNAUTHORIZED=0 or rejectUnauthorized:false) makes all HTTPS connections vulnerable to man-in-the-middle attacks.",
|
|
1595
1087
|
check(content, filePath) {
|
|
1596
|
-
if (filePath.includes("test") || filePath.includes("mock")) return [];
|
|
1597
1088
|
const matches = [];
|
|
1598
1089
|
const patterns = [
|
|
1599
|
-
/
|
|
1600
|
-
/
|
|
1090
|
+
/NODE_TLS_REJECT_UNAUTHORIZED\s*[:=]\s*["'`]?0["'`]?/g,
|
|
1091
|
+
/rejectUnauthorized\s*:\s*false/g,
|
|
1092
|
+
/verify\s*[:=]\s*false.*(?:ssl|tls|cert|https)/gi,
|
|
1093
|
+
/PYTHONHTTPSVERIFY\s*[:=]\s*["'`]?0["'`]?/g,
|
|
1094
|
+
/ssl_verify\s*[:=]\s*false/gi,
|
|
1095
|
+
/InsecureSkipVerify\s*:\s*true/g
|
|
1601
1096
|
];
|
|
1602
|
-
const hasTimingSafe = /timingSafeEqual|constantTimeEqual|safeCompare|secureCompare/i.test(content);
|
|
1603
|
-
if (hasTimingSafe) return [];
|
|
1604
1097
|
for (const p of patterns) {
|
|
1605
1098
|
matches.push(...findMatches(
|
|
1606
1099
|
content,
|
|
1607
1100
|
p,
|
|
1608
|
-
|
|
1101
|
+
disabledTLSVerification,
|
|
1609
1102
|
filePath,
|
|
1610
|
-
() => "
|
|
1103
|
+
() => "Never disable TLS certificate verification in production. Fix the root cause: install the correct CA certificate, or use NODE_EXTRA_CA_CERTS for custom CAs."
|
|
1611
1104
|
));
|
|
1612
1105
|
}
|
|
1613
1106
|
return matches;
|
|
1614
1107
|
}
|
|
1615
1108
|
};
|
|
1616
|
-
var
|
|
1617
|
-
id: "
|
|
1618
|
-
title: "
|
|
1619
|
-
severity: "
|
|
1109
|
+
var dangerousInnerHTML = {
|
|
1110
|
+
id: "VC063",
|
|
1111
|
+
title: "Unsanitized dangerouslySetInnerHTML",
|
|
1112
|
+
severity: "critical",
|
|
1620
1113
|
category: "Injection",
|
|
1621
|
-
description: "
|
|
1622
|
-
check(content, filePath) {
|
|
1623
|
-
if (filePath.includes("test") || filePath.includes("mock")) return [];
|
|
1624
|
-
const isServerFile = /(?:\/api\/|routes?\/|controllers?\/|server\.|middleware|handler)/i.test(filePath);
|
|
1625
|
-
if (!isServerFile) return [];
|
|
1626
|
-
const matches = [];
|
|
1627
|
-
const patterns = [
|
|
1628
|
-
/console\.(?:log|warn|error|info)\s*\([^)]*(?:req\.body|req\.query|req\.params|req\.headers)\s*\)/gi,
|
|
1629
|
-
/(?:logger|log)\.(?:info|warn|error|debug)\s*\([^)]*(?:req\.body|req\.query|req\.params)\s*\)/gi
|
|
1630
|
-
];
|
|
1631
|
-
const hasSanitization = /sanitize|escape|JSON\.stringify|replace.*\\n/i.test(content);
|
|
1632
|
-
if (hasSanitization) return [];
|
|
1633
|
-
for (const p of patterns) {
|
|
1634
|
-
matches.push(...findMatches(
|
|
1635
|
-
content,
|
|
1636
|
-
p,
|
|
1637
|
-
logInjection,
|
|
1638
|
-
filePath,
|
|
1639
|
-
() => "Sanitize user input before logging: strip newlines and control characters. Use JSON.stringify() or a structured logger (e.g., pino, winston) that escapes values automatically."
|
|
1640
|
-
));
|
|
1641
|
-
}
|
|
1642
|
-
return matches;
|
|
1643
|
-
}
|
|
1644
|
-
};
|
|
1645
|
-
var weakPasswordRequirements = {
|
|
1646
|
-
id: "VC045",
|
|
1647
|
-
title: "Weak Password Requirements",
|
|
1648
|
-
severity: "high",
|
|
1649
|
-
category: "Authentication",
|
|
1650
|
-
description: "Registration or password-change endpoints without minimum length or complexity validation allow weak passwords that are easily brute-forced.",
|
|
1651
|
-
check(content, filePath) {
|
|
1652
|
-
if (isTestFile(filePath)) return [];
|
|
1653
|
-
if (!/(?:password|passwd|pwd)/i.test(content)) return [];
|
|
1654
|
-
if (!/(?:register|signup|sign.up|createUser|create.user|changePassword|resetPassword|set.password)/i.test(content) && !/(?:\/api\/|routes?\/|controllers?\/)/i.test(filePath)) return [];
|
|
1655
|
-
const hasValidation = /(?:password|pwd).*(?:\.length|minLength|minlength|min_length)\s*(?:>=?|<|>)\s*\d|(?:password|pwd).*(?:match|test|regex|pattern)|zxcvbn|password-validator|passwordStrength|isStrongPassword|joi\.|yup\.|zod\.|validate|schema/i.test(content);
|
|
1656
|
-
if (hasValidation) return [];
|
|
1657
|
-
const hasPasswordHandling = /(?:password|pwd)\s*[:=]\s*(?:req\.body|body|input|params|args)\./i.test(content);
|
|
1658
|
-
if (!hasPasswordHandling) return [];
|
|
1659
|
-
const rawMatches = findMatches(
|
|
1660
|
-
content,
|
|
1661
|
-
/(?:password|pwd)\s*[:=]\s*(?:req\.body|body|input|params|args)\./gi,
|
|
1662
|
-
weakPasswordRequirements,
|
|
1663
|
-
filePath,
|
|
1664
|
-
() => "Enforce minimum password requirements: at least 8 characters, mix of letters/numbers/symbols. Use a library like zxcvbn for strength estimation."
|
|
1665
|
-
);
|
|
1666
|
-
const lines = content.split("\n");
|
|
1667
|
-
const validationPattern = /\.length|minLength|minlength|min_length|match|test|regex|pattern|validate|schema|zxcvbn|isStrongPassword/i;
|
|
1668
|
-
return rawMatches.filter((rm3) => {
|
|
1669
|
-
const start = Math.max(0, rm3.line - 1 - 10);
|
|
1670
|
-
const end = Math.min(lines.length, rm3.line - 1 + 10);
|
|
1671
|
-
const nearby = lines.slice(start, end).join("\n");
|
|
1672
|
-
return !validationPattern.test(nearby);
|
|
1673
|
-
});
|
|
1674
|
-
}
|
|
1675
|
-
};
|
|
1676
|
-
var sessionFixation = {
|
|
1677
|
-
id: "VC046",
|
|
1678
|
-
title: "Session Fixation Risk",
|
|
1679
|
-
severity: "high",
|
|
1680
|
-
category: "Authentication",
|
|
1681
|
-
description: "Not regenerating session IDs after login allows attackers to pre-set a session ID and hijack the authenticated session.",
|
|
1682
|
-
check(content, filePath) {
|
|
1683
|
-
if (!/(?:login|signin|sign.in|authenticate)/i.test(content)) return [];
|
|
1684
|
-
if (!/session/i.test(content)) return [];
|
|
1685
|
-
const hasRegenerate = /regenerate|destroy.*create|req\.session\.id\s*=|session\.regenerateId|rotateSession/i.test(content);
|
|
1686
|
-
if (hasRegenerate) return [];
|
|
1687
|
-
const hasLogin = /(?:login|signin|authenticate)\s*(?:=|:|\()/i.test(content);
|
|
1688
|
-
if (!hasLogin) return [];
|
|
1689
|
-
return findMatches(
|
|
1690
|
-
content,
|
|
1691
|
-
/(?:login|signin|authenticate)\s*(?:=|:|\()/gi,
|
|
1692
|
-
sessionFixation,
|
|
1693
|
-
filePath,
|
|
1694
|
-
() => "Regenerate the session ID after successful login: req.session.regenerate() (Express) or equivalent. This prevents session fixation attacks."
|
|
1695
|
-
);
|
|
1696
|
-
}
|
|
1697
|
-
};
|
|
1698
|
-
var missingBruteForce = {
|
|
1699
|
-
id: "VC047",
|
|
1700
|
-
title: "Login Without Brute Force Protection",
|
|
1701
|
-
severity: "high",
|
|
1702
|
-
category: "Authentication",
|
|
1703
|
-
description: "Login endpoints without rate limiting, account lockout, or progressive delays are vulnerable to credential stuffing and brute force attacks.",
|
|
1114
|
+
description: "Using dangerouslySetInnerHTML without sanitization (DOMPurify) enables XSS attacks. User-controlled content injected as raw HTML can execute arbitrary JavaScript.",
|
|
1704
1115
|
check(content, filePath) {
|
|
1705
|
-
|
|
1706
|
-
if (
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
if (hasBruteForce) return [];
|
|
1116
|
+
if (!filePath.match(/\.(jsx|tsx)$/)) return [];
|
|
1117
|
+
if (!/dangerouslySetInnerHTML/i.test(content)) return [];
|
|
1118
|
+
const hasSanitize = /DOMPurify|sanitize|purify|xss|sanitizeHtml|isomorphic-dompurify/i.test(content);
|
|
1119
|
+
if (hasSanitize) return [];
|
|
1710
1120
|
return findMatches(
|
|
1711
1121
|
content,
|
|
1712
|
-
|
|
1713
|
-
|
|
1122
|
+
/dangerouslySetInnerHTML\s*=\s*\{\s*\{/g,
|
|
1123
|
+
dangerousInnerHTML,
|
|
1714
1124
|
filePath,
|
|
1715
|
-
() => "
|
|
1125
|
+
() => "Sanitize HTML before using dangerouslySetInnerHTML: dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }}. Install: npm install dompurify"
|
|
1716
1126
|
);
|
|
1717
1127
|
}
|
|
1718
1128
|
};
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
if (
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
/\$where\s*:\s*(?!["'`])/g,
|
|
1734
|
-
// Direct variable in query without sanitization
|
|
1735
|
-
/\.(?:findOne|findById|deleteOne|updateOne|findOneAndUpdate)\s*\(\s*\{[^}]*:\s*(?:req\.(?:body|query|params))\./gi
|
|
1736
|
-
];
|
|
1737
|
-
const hasSanitization = /sanitize|escape|mongo-sanitize|express-mongo-sanitize|validator|typeof.*===.*string/i.test(content);
|
|
1738
|
-
if (hasSanitization) return [];
|
|
1739
|
-
for (const p of patterns) {
|
|
1740
|
-
matches.push(...findMatches(
|
|
1741
|
-
content,
|
|
1742
|
-
p,
|
|
1743
|
-
nosqlInjection,
|
|
1744
|
-
filePath,
|
|
1745
|
-
() => "Sanitize MongoDB query inputs: use express-mongo-sanitize, validate types (ensure strings aren't objects), and avoid $where. Example: if (typeof input !== 'string') throw new Error('Invalid input');"
|
|
1746
|
-
));
|
|
1747
|
-
}
|
|
1748
|
-
return matches;
|
|
1749
|
-
}
|
|
1750
|
-
};
|
|
1751
|
-
var exposedDBCredentials = {
|
|
1752
|
-
id: "VC049",
|
|
1753
|
-
title: "Database Credentials in Config File",
|
|
1754
|
-
severity: "critical",
|
|
1755
|
-
category: "Secrets",
|
|
1756
|
-
description: "Database connection strings with embedded usernames and passwords in committed config files expose credentials to anyone with repo access.",
|
|
1757
|
-
check(content, filePath) {
|
|
1758
|
-
if (filePath.endsWith(".example") || filePath.endsWith(".template")) return [];
|
|
1759
|
-
if (!filePath.match(/(?:config|setting|database|db|knexfile|sequelize|drizzle|prisma)/i) && !filePath.match(/\.(json|yaml|yml|toml|js|ts)$/)) return [];
|
|
1760
|
-
if (filePath.match(/\.env/)) return [];
|
|
1761
|
-
const patterns = [
|
|
1762
|
-
// Connection strings with credentials
|
|
1763
|
-
/(?:host|server|database|db).*(?:password|passwd|pwd)\s*[:=]\s*["'`][^"'`]{3,}["'`]/gi,
|
|
1764
|
-
// Inline connection URLs with credentials
|
|
1765
|
-
/(?:connection|database|db).*(?:postgres|mysql|mongodb|redis):\/\/[^:]+:[^@]+@/gi
|
|
1766
|
-
];
|
|
1767
|
-
const matches = [];
|
|
1768
|
-
for (const p of patterns) {
|
|
1769
|
-
matches.push(...findMatches(
|
|
1770
|
-
content,
|
|
1771
|
-
p,
|
|
1772
|
-
exposedDBCredentials,
|
|
1773
|
-
filePath,
|
|
1774
|
-
() => "Move database credentials to environment variables. Use: process.env.DATABASE_URL instead of hardcoding connection strings in config files."
|
|
1775
|
-
));
|
|
1776
|
-
}
|
|
1777
|
-
return matches;
|
|
1778
|
-
}
|
|
1779
|
-
};
|
|
1780
|
-
var missingDBEncryption = {
|
|
1781
|
-
id: "VC050",
|
|
1782
|
-
title: "Database Connection Without SSL/TLS",
|
|
1783
|
-
severity: "high",
|
|
1784
|
-
category: "Configuration",
|
|
1785
|
-
description: "Database connections without SSL/TLS encryption transmit credentials and data in plaintext, allowing eavesdropping on the network.",
|
|
1786
|
-
check(content, filePath) {
|
|
1787
|
-
if (!/(?:createConnection|createPool|createClient|connect|new.*Client|knex|sequelize|drizzle)/i.test(content)) return [];
|
|
1788
|
-
if (!/(?:postgres|mysql|mariadb|pg|mongo)/i.test(content)) return [];
|
|
1789
|
-
const matches = [];
|
|
1790
|
-
const sslDisabled = [
|
|
1791
|
-
/ssl\s*:\s*false/gi,
|
|
1792
|
-
/sslmode\s*[:=]\s*["'`]?disable["'`]?/gi,
|
|
1793
|
-
/rejectUnauthorized\s*:\s*false/gi
|
|
1794
|
-
];
|
|
1795
|
-
for (const p of sslDisabled) {
|
|
1796
|
-
matches.push(...findMatches(
|
|
1797
|
-
content,
|
|
1798
|
-
p,
|
|
1799
|
-
missingDBEncryption,
|
|
1800
|
-
filePath,
|
|
1801
|
-
() => "Enable SSL/TLS for database connections: { ssl: { rejectUnauthorized: true } }. In production, always verify server certificates."
|
|
1802
|
-
));
|
|
1803
|
-
}
|
|
1804
|
-
return matches;
|
|
1805
|
-
}
|
|
1806
|
-
};
|
|
1807
|
-
var graphqlIntrospection = {
|
|
1808
|
-
id: "VC051",
|
|
1809
|
-
title: "GraphQL Introspection Enabled in Production",
|
|
1810
|
-
severity: "medium",
|
|
1811
|
-
category: "Information Leakage",
|
|
1812
|
-
description: "GraphQL introspection exposes your entire API schema, types, queries, and mutations to attackers, making it easy to find attack vectors.",
|
|
1813
|
-
check(content, filePath) {
|
|
1814
|
-
if (!/graphql/i.test(content) && !/graphql/i.test(filePath)) return [];
|
|
1815
|
-
const matches = [];
|
|
1816
|
-
if (/introspection\s*:\s*true/i.test(content)) {
|
|
1817
|
-
matches.push(...findMatches(
|
|
1818
|
-
content,
|
|
1819
|
-
/introspection\s*:\s*true/gi,
|
|
1820
|
-
graphqlIntrospection,
|
|
1821
|
-
filePath,
|
|
1822
|
-
() => "Disable GraphQL introspection in production: introspection: process.env.NODE_ENV !== 'production'. This prevents schema exposure."
|
|
1823
|
-
));
|
|
1824
|
-
}
|
|
1825
|
-
if (/(?:ApolloServer|GraphQLServer|createYoga|buildSchema|makeExecutableSchema)\s*\(/i.test(content)) {
|
|
1826
|
-
if (!/introspection/i.test(content)) {
|
|
1827
|
-
matches.push(...findMatches(
|
|
1828
|
-
content,
|
|
1829
|
-
/(?:ApolloServer|GraphQLServer|createYoga)\s*\(/gi,
|
|
1830
|
-
graphqlIntrospection,
|
|
1831
|
-
filePath,
|
|
1832
|
-
() => "Explicitly disable introspection in production: new ApolloServer({ introspection: process.env.NODE_ENV !== 'production' })"
|
|
1833
|
-
));
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
1836
|
-
return matches;
|
|
1837
|
-
}
|
|
1838
|
-
};
|
|
1839
|
-
var missingRequestSizeLimit = {
|
|
1840
|
-
id: "VC052",
|
|
1841
|
-
title: "Missing Request Body Size Limit",
|
|
1842
|
-
severity: "medium",
|
|
1843
|
-
category: "Availability",
|
|
1844
|
-
description: "Express/Hono/Fastify servers without request body size limits are vulnerable to denial-of-service via oversized payloads that exhaust memory.",
|
|
1845
|
-
check(content, filePath) {
|
|
1846
|
-
if (!/(?:server|app|index|main)\.[jt]sx?$/.test(filePath)) return [];
|
|
1847
|
-
if (!/(?:express|hono|fastify|koa)/i.test(content)) return [];
|
|
1848
|
-
const matches = [];
|
|
1849
|
-
if (/express\.json\s*\(\s*\)/g.test(content)) {
|
|
1850
|
-
matches.push(...findMatches(
|
|
1851
|
-
content,
|
|
1852
|
-
/express\.json\s*\(\s*\)/g,
|
|
1853
|
-
missingRequestSizeLimit,
|
|
1854
|
-
filePath,
|
|
1855
|
-
() => "Set a body size limit: express.json({ limit: '1mb' }). Without this, attackers can send huge payloads to crash your server."
|
|
1856
|
-
));
|
|
1857
|
-
}
|
|
1858
|
-
if (/bodyParser\.json\s*\(\s*\)/g.test(content)) {
|
|
1859
|
-
matches.push(...findMatches(
|
|
1860
|
-
content,
|
|
1861
|
-
/bodyParser\.json\s*\(\s*\)/g,
|
|
1862
|
-
missingRequestSizeLimit,
|
|
1863
|
-
filePath,
|
|
1864
|
-
() => "Set a body size limit: bodyParser.json({ limit: '1mb' })."
|
|
1865
|
-
));
|
|
1866
|
-
}
|
|
1867
|
-
return matches;
|
|
1868
|
-
}
|
|
1869
|
-
};
|
|
1870
|
-
var hardcodedIPAllowlist = {
|
|
1871
|
-
id: "VC053",
|
|
1872
|
-
title: "Hardcoded IP or Host Allowlist",
|
|
1873
|
-
severity: "medium",
|
|
1874
|
-
category: "Configuration",
|
|
1875
|
-
description: "Hardcoded IP addresses or hostnames in allowlists are brittle and hard to update. They should be in environment variables or configuration files.",
|
|
1876
|
-
check(content, filePath) {
|
|
1877
|
-
if (filePath.includes("test") || filePath.includes("mock") || filePath.match(/\.(md|txt)$/)) return [];
|
|
1878
|
-
const matches = [];
|
|
1879
|
-
const patterns = [
|
|
1880
|
-
/(?:allowedIPs|allowed_ips|whitelist|allowlist|trustedHosts)\s*[:=]\s*\[\s*["'`]\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/gi,
|
|
1881
|
-
/(?:allowedIPs|allowed_ips|whitelist|allowlist|trustedHosts)\s*[:=]\s*\[\s*["'`][\w.-]+\.(?:com|net|org|io)/gi
|
|
1882
|
-
];
|
|
1883
|
-
for (const p of patterns) {
|
|
1884
|
-
matches.push(...findMatches(
|
|
1885
|
-
content,
|
|
1886
|
-
p,
|
|
1887
|
-
hardcodedIPAllowlist,
|
|
1888
|
-
filePath,
|
|
1889
|
-
() => "Move IP/host allowlists to environment variables or a config file: const allowed = process.env.ALLOWED_IPS?.split(',') || [];"
|
|
1890
|
-
));
|
|
1891
|
-
}
|
|
1892
|
-
return matches;
|
|
1129
|
+
function detectFramework(files) {
|
|
1130
|
+
const frameworks = /* @__PURE__ */ new Set();
|
|
1131
|
+
for (const { path, content } of files) {
|
|
1132
|
+
if (path.match(/next\.config/i) || /from\s+["']next/i.test(content)) frameworks.add("next.js");
|
|
1133
|
+
if (/from\s+["']react-native/i.test(content) || path.match(/react-native\.config/i)) frameworks.add("react-native");
|
|
1134
|
+
else if (/from\s+["']react/i.test(content) || /import\s+React/i.test(content)) frameworks.add("react");
|
|
1135
|
+
if (/from\s+["']express/i.test(content) || /require\s*\(\s*["']express/i.test(content)) frameworks.add("express");
|
|
1136
|
+
if (/from\s+["']hono/i.test(content)) frameworks.add("hono");
|
|
1137
|
+
if (/from\s+["']fastify/i.test(content)) frameworks.add("fastify");
|
|
1138
|
+
if (/from\s+["']electron/i.test(content) || path.match(/electron/i)) frameworks.add("electron");
|
|
1139
|
+
if (path.match(/settings\.py$/) || /from\s+django/i.test(content)) frameworks.add("django");
|
|
1140
|
+
if (/from\s+flask/i.test(content) || /Flask\s*\(/i.test(content)) frameworks.add("flask");
|
|
1141
|
+
if (/from\s+["']vue/i.test(content) || path.match(/vue\.config/i)) frameworks.add("vue");
|
|
1142
|
+
if (/from\s+["']svelte/i.test(content) || path.match(/svelte\.config/i)) frameworks.add("svelte");
|
|
1893
1143
|
}
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
description: "Storing tokens, passwords, or secrets in localStorage is insecure \u2014 it's accessible to any JavaScript on the page (XSS) and persists indefinitely. Use httpOnly cookies instead.",
|
|
1901
|
-
check(content, filePath) {
|
|
1902
|
-
if (!filePath.match(/\.(jsx?|tsx?|vue|svelte)$/)) return [];
|
|
1903
|
-
if (isTestFile(filePath)) return [];
|
|
1904
|
-
if (/mock|spec/i.test(filePath)) return [];
|
|
1905
|
-
if (!/localStorage\.setItem|localStorage\[/i.test(content)) return [];
|
|
1906
|
-
const matches = [];
|
|
1907
|
-
const patterns = [
|
|
1908
|
-
/localStorage\.setItem\s*\(\s*["'`](?:token|access_token|auth_token|jwt|session|refresh_token|api_key|password|secret)/gi,
|
|
1909
|
-
/localStorage\s*\[\s*["'`](?:token|access_token|auth_token|jwt|session|refresh_token|api_key|password|secret)/gi
|
|
1910
|
-
];
|
|
1911
|
-
for (const p of patterns) {
|
|
1912
|
-
matches.push(...findMatches(
|
|
1913
|
-
content,
|
|
1914
|
-
p,
|
|
1915
|
-
sensitiveLocalStorage,
|
|
1916
|
-
filePath,
|
|
1917
|
-
() => "Don't store tokens/secrets in localStorage \u2014 use httpOnly cookies instead. localStorage is accessible to any XSS attack. For session tokens, set them as httpOnly, secure, sameSite cookies."
|
|
1918
|
-
));
|
|
1919
|
-
}
|
|
1920
|
-
return matches;
|
|
1144
|
+
if (frameworks.size === 0) frameworks.add("unknown");
|
|
1145
|
+
return [...frameworks];
|
|
1146
|
+
}
|
|
1147
|
+
function calculateGrade(findings, totalFiles) {
|
|
1148
|
+
if (findings.length === 0) {
|
|
1149
|
+
return { grade: "A+", score: 100, summary: "No security issues detected. Excellent!" };
|
|
1921
1150
|
}
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
/(?:sourceMap|source-map|sourcemap)\s*[:=]\s*true/gi,
|
|
1938
|
-
exposedSourceMaps,
|
|
1939
|
-
filePath,
|
|
1940
|
-
() => "Disable source maps in production builds: sourceMap: process.env.NODE_ENV !== 'production'. Or use 'hidden-source-map' to generate maps without exposing them."
|
|
1941
|
-
));
|
|
1942
|
-
}
|
|
1943
|
-
}
|
|
1944
|
-
if (/productionSourceMap\s*:\s*true/i.test(content)) {
|
|
1945
|
-
matches.push(...findMatches(
|
|
1946
|
-
content,
|
|
1947
|
-
/productionSourceMap\s*:\s*true/gi,
|
|
1948
|
-
exposedSourceMaps,
|
|
1949
|
-
filePath,
|
|
1950
|
-
() => "Set productionSourceMap: false to avoid exposing source code in production."
|
|
1951
|
-
));
|
|
1151
|
+
let deductions = 0;
|
|
1152
|
+
for (const f of findings) {
|
|
1153
|
+
switch (f.severity) {
|
|
1154
|
+
case "critical":
|
|
1155
|
+
deductions += 15;
|
|
1156
|
+
break;
|
|
1157
|
+
case "high":
|
|
1158
|
+
deductions += 8;
|
|
1159
|
+
break;
|
|
1160
|
+
case "medium":
|
|
1161
|
+
deductions += 4;
|
|
1162
|
+
break;
|
|
1163
|
+
case "low":
|
|
1164
|
+
deductions += 1;
|
|
1165
|
+
break;
|
|
1952
1166
|
}
|
|
1953
|
-
return matches;
|
|
1954
1167
|
}
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
if (/(?:server|app|index|main)\.[jt]sx?$/.test(filePath)) {
|
|
1978
|
-
if (/(?:express|hono|fastify|koa)/i.test(content)) {
|
|
1979
|
-
if (!/X-Frame-Options|frame-ancestors|helmet/i.test(content)) {
|
|
1980
|
-
return findMatches(
|
|
1981
|
-
content,
|
|
1982
|
-
/(?:express|hono|fastify|koa)\s*\(/gi,
|
|
1983
|
-
clickjacking,
|
|
1984
|
-
filePath,
|
|
1985
|
-
() => "Add X-Frame-Options header: res.setHeader('X-Frame-Options', 'DENY'). Or use helmet: app.use(helmet()) which sets this and other security headers."
|
|
1986
|
-
);
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
1989
|
-
}
|
|
1990
|
-
return [];
|
|
1991
|
-
}
|
|
1992
|
-
};
|
|
1993
|
-
var overlyPermissiveIAM = {
|
|
1994
|
-
id: "VC057",
|
|
1995
|
-
title: "Overly Permissive IAM/Cloud Permissions",
|
|
1996
|
-
severity: "critical",
|
|
1997
|
-
category: "Authorization",
|
|
1998
|
-
description: "Wildcard (*) permissions in AWS IAM, GCP, or Terraform configs grant unrestricted access, violating the principle of least privilege.",
|
|
1999
|
-
check(content, filePath) {
|
|
2000
|
-
if (!filePath.match(/\.(tf|hcl|json|yaml|yml)$/) && !filePath.match(/(?:iam|policy|role|permission)/i)) return [];
|
|
2001
|
-
const matches = [];
|
|
2002
|
-
const patterns = [
|
|
2003
|
-
// AWS IAM wildcard
|
|
2004
|
-
/["'`]Action["'`]\s*:\s*["'`]\*["'`]/g,
|
|
2005
|
-
/["'`]Resource["'`]\s*:\s*["'`]\*["'`]/g,
|
|
2006
|
-
// Terraform aws_iam
|
|
2007
|
-
/actions\s*=\s*\[\s*["'`]\*["'`]\s*\]/g,
|
|
2008
|
-
/resources\s*=\s*\[\s*["'`]\*["'`]\s*\]/g,
|
|
2009
|
-
// GCP bindings
|
|
2010
|
-
/role\s*[:=]\s*["'`]roles\/(?:owner|editor)["'`]/g
|
|
2011
|
-
];
|
|
2012
|
-
for (const p of patterns) {
|
|
2013
|
-
matches.push(...findMatches(
|
|
2014
|
-
content,
|
|
2015
|
-
p,
|
|
2016
|
-
overlyPermissiveIAM,
|
|
2017
|
-
filePath,
|
|
2018
|
-
() => "Follow the principle of least privilege: replace wildcard (*) with specific actions and resources. Example: 'Action': 's3:GetObject' instead of '*'."
|
|
2019
|
-
));
|
|
2020
|
-
}
|
|
2021
|
-
return matches;
|
|
2022
|
-
}
|
|
2023
|
-
};
|
|
2024
|
-
var dockerRunAsRoot = {
|
|
2025
|
-
id: "VC058",
|
|
2026
|
-
title: "Docker Container Running as Root",
|
|
2027
|
-
severity: "high",
|
|
2028
|
-
category: "Configuration",
|
|
2029
|
-
description: "Containers running as root give attackers full system access if they escape the container. Always run as a non-root user.",
|
|
2030
|
-
check(content, filePath) {
|
|
2031
|
-
if (!filePath.match(/Dockerfile$/i)) return [];
|
|
2032
|
-
const hasUser = /^\s*USER\s+/m.test(content);
|
|
2033
|
-
if (hasUser) return [];
|
|
2034
|
-
return [{
|
|
2035
|
-
rule: "VC058",
|
|
2036
|
-
title: dockerRunAsRoot.title,
|
|
2037
|
-
severity: "high",
|
|
2038
|
-
category: "Configuration",
|
|
2039
|
-
file: filePath,
|
|
2040
|
-
line: 1,
|
|
2041
|
-
snippet: getSnippet(content, 1),
|
|
2042
|
-
fix: "Add a USER directive: RUN addgroup -S app && adduser -S app -G app\\nUSER app. Place it after installing dependencies but before COPY/CMD."
|
|
2043
|
-
}];
|
|
2044
|
-
}
|
|
2045
|
-
};
|
|
2046
|
-
var exposedDockerPorts = {
|
|
2047
|
-
id: "VC059",
|
|
2048
|
-
title: "Docker Compose Binding to All Interfaces",
|
|
2049
|
-
severity: "medium",
|
|
2050
|
-
category: "Configuration",
|
|
2051
|
-
description: "Binding ports to 0.0.0.0 (default) in Docker Compose exposes services to the entire network. Bind to 127.0.0.1 for local-only access.",
|
|
2052
|
-
check(content, filePath) {
|
|
2053
|
-
if (!filePath.match(/docker-compose|compose\.(yaml|yml)$/i)) return [];
|
|
2054
|
-
const matches = [];
|
|
2055
|
-
const portPattern = /ports:\s*\n(?:\s*-\s*["'`]?\d+:\d+["'`]?\s*\n?)+/g;
|
|
2056
|
-
if (portPattern.test(content) && !/127\.0\.0\.1:/i.test(content)) {
|
|
2057
|
-
matches.push(...findMatches(
|
|
2058
|
-
content,
|
|
2059
|
-
/^\s*-\s*["'`]?\d+:\d+["'`]?/gm,
|
|
2060
|
-
exposedDockerPorts,
|
|
2061
|
-
filePath,
|
|
2062
|
-
() => "Bind to localhost only: '127.0.0.1:3000:3000' instead of '3000:3000'. This prevents external network access to the service."
|
|
2063
|
-
));
|
|
2064
|
-
}
|
|
2065
|
-
return matches;
|
|
2066
|
-
}
|
|
2067
|
-
};
|
|
2068
|
-
var weakHashing = {
|
|
2069
|
-
id: "VC060",
|
|
2070
|
-
title: "Weak Hashing Algorithm for Passwords",
|
|
2071
|
-
severity: "critical",
|
|
2072
|
-
category: "Cryptography",
|
|
2073
|
-
description: "MD5 and SHA1/SHA256 are too fast for password hashing \u2014 they can be brute-forced at billions of attempts per second. Use bcrypt, scrypt, or argon2 instead.",
|
|
2074
|
-
check(content, filePath) {
|
|
2075
|
-
if (filePath.includes("test") || filePath.includes("mock")) return [];
|
|
2076
|
-
if (/bcrypt|scrypt|argon2/i.test(content)) return [];
|
|
2077
|
-
const matches = [];
|
|
2078
|
-
const patterns = [
|
|
2079
|
-
/(?:md5|sha1|sha256|sha512)\s*\([^)]*(?:password|passwd|pwd)/gi,
|
|
2080
|
-
/createHash\s*\(\s*["'`](?:md5|sha1|sha256)["'`]\).*(?:password|passwd|pwd)/gi,
|
|
2081
|
-
/(?:password|passwd|pwd).*createHash\s*\(\s*["'`](?:md5|sha1|sha256)["'`]\)/gi,
|
|
2082
|
-
/hashlib\.(?:md5|sha1|sha256)\s*\([^)]*(?:password|passwd|pwd)/gi,
|
|
2083
|
-
/Digest::(?:MD5|SHA1|SHA256).*(?:password|passwd|pwd)/gi
|
|
2084
|
-
];
|
|
2085
|
-
for (const p of patterns) {
|
|
2086
|
-
matches.push(...findMatches(
|
|
2087
|
-
content,
|
|
2088
|
-
p,
|
|
2089
|
-
weakHashing,
|
|
2090
|
-
filePath,
|
|
2091
|
-
() => "Use bcrypt, scrypt, or argon2 for password hashing \u2014 they're intentionally slow. Example: const hash = await bcrypt.hash(password, 12);"
|
|
2092
|
-
));
|
|
2093
|
-
}
|
|
2094
|
-
return matches;
|
|
2095
|
-
}
|
|
2096
|
-
};
|
|
2097
|
-
var disabledTLSVerification = {
|
|
2098
|
-
id: "VC061",
|
|
2099
|
-
title: "Disabled TLS Certificate Verification",
|
|
2100
|
-
severity: "critical",
|
|
2101
|
-
category: "Cryptography",
|
|
2102
|
-
description: "Disabling TLS certificate verification (NODE_TLS_REJECT_UNAUTHORIZED=0 or rejectUnauthorized:false) makes all HTTPS connections vulnerable to man-in-the-middle attacks.",
|
|
2103
|
-
check(content, filePath) {
|
|
2104
|
-
const matches = [];
|
|
2105
|
-
const patterns = [
|
|
2106
|
-
/NODE_TLS_REJECT_UNAUTHORIZED\s*[:=]\s*["'`]?0["'`]?/g,
|
|
2107
|
-
/rejectUnauthorized\s*:\s*false/g,
|
|
2108
|
-
/verify\s*[:=]\s*false.*(?:ssl|tls|cert|https)/gi,
|
|
2109
|
-
/PYTHONHTTPSVERIFY\s*[:=]\s*["'`]?0["'`]?/g,
|
|
2110
|
-
/ssl_verify\s*[:=]\s*false/gi,
|
|
2111
|
-
/InsecureSkipVerify\s*:\s*true/g
|
|
2112
|
-
];
|
|
2113
|
-
for (const p of patterns) {
|
|
2114
|
-
matches.push(...findMatches(
|
|
2115
|
-
content,
|
|
2116
|
-
p,
|
|
2117
|
-
disabledTLSVerification,
|
|
2118
|
-
filePath,
|
|
2119
|
-
() => "Never disable TLS certificate verification in production. Fix the root cause: install the correct CA certificate, or use NODE_EXTRA_CA_CERTS for custom CAs."
|
|
2120
|
-
));
|
|
2121
|
-
}
|
|
2122
|
-
return matches;
|
|
2123
|
-
}
|
|
2124
|
-
};
|
|
2125
|
-
var hardcodedEncryptionKey = {
|
|
2126
|
-
id: "VC062",
|
|
2127
|
-
title: "Hardcoded Encryption Key or IV",
|
|
2128
|
-
severity: "critical",
|
|
2129
|
-
category: "Cryptography",
|
|
2130
|
-
description: "Hardcoded encryption keys and initialization vectors (IVs) in source code can be extracted to decrypt all data. IVs must be random per encryption operation.",
|
|
2131
|
-
check(content, filePath) {
|
|
2132
|
-
if (filePath.endsWith(".example") || filePath.endsWith(".template")) return [];
|
|
2133
|
-
if (isTestFile(filePath)) return [];
|
|
2134
|
-
if (filePath.match(/\.(html|htm|xml|plist|svg|xhtml)$/)) return [];
|
|
2135
|
-
if (!/(?:cipher|encrypt|decrypt|crypto|aes|createCipher)/i.test(content)) return [];
|
|
2136
|
-
const matches = [];
|
|
2137
|
-
const patterns = [
|
|
2138
|
-
// Encryption key as string literal
|
|
2139
|
-
/(?:encryption_key|encryptionKey|cipher_key|cipherKey|aes_key|AES_KEY|ENCRYPTION_KEY)\s*[:=]\s*["'`][^"'`]{8,}["'`]/g,
|
|
2140
|
-
// createCipheriv with hardcoded key
|
|
2141
|
-
/createCipher(?:iv)?\s*\(\s*["'`][^"'`]+["'`]\s*,\s*["'`][^"'`]+["'`]/g,
|
|
2142
|
-
// Buffer.from with hardcoded key near cipher context
|
|
2143
|
-
/(?:key|iv|nonce)\s*[:=]\s*Buffer\.from\s*\(\s*["'`][^"'`]{8,}["'`]/gi,
|
|
2144
|
-
// Static IV (should be random) — only in crypto context
|
|
2145
|
-
/(?:^|[\s,({])(?:iv|nonce|initialVector)\s*[:=]\s*["'`][0-9a-fA-F]{16,}["'`]/gi
|
|
2146
|
-
];
|
|
2147
|
-
for (const p of patterns) {
|
|
2148
|
-
const rawMatches = findMatches(
|
|
2149
|
-
content,
|
|
2150
|
-
p,
|
|
2151
|
-
hardcodedEncryptionKey,
|
|
2152
|
-
filePath,
|
|
2153
|
-
() => "Move encryption keys to environment variables. Generate IVs randomly per operation: crypto.randomBytes(16). Never reuse IVs."
|
|
2154
|
-
);
|
|
2155
|
-
for (const rm3 of rawMatches) {
|
|
2156
|
-
const lineStart = content.lastIndexOf("\n", content.split("\n").slice(0, rm3.line - 1).join("\n").length) + 1;
|
|
2157
|
-
const lineEnd = content.indexOf("\n", lineStart + 1);
|
|
2158
|
-
const lineText = content.substring(lineStart, lineEnd === -1 ? content.length : lineEnd);
|
|
2159
|
-
if (/meta\s+http-equiv|Content-Security-Policy|X-Frame-Options|X-Content-Type/i.test(lineText)) continue;
|
|
2160
|
-
matches.push(rm3);
|
|
2161
|
-
}
|
|
2162
|
-
}
|
|
2163
|
-
return matches;
|
|
2164
|
-
}
|
|
2165
|
-
};
|
|
2166
|
-
var dangerousInnerHTML = {
|
|
2167
|
-
id: "VC063",
|
|
2168
|
-
title: "Unsanitized dangerouslySetInnerHTML",
|
|
2169
|
-
severity: "critical",
|
|
2170
|
-
category: "Injection",
|
|
2171
|
-
description: "Using dangerouslySetInnerHTML without sanitization (DOMPurify) enables XSS attacks. User-controlled content injected as raw HTML can execute arbitrary JavaScript.",
|
|
2172
|
-
check(content, filePath) {
|
|
2173
|
-
if (!filePath.match(/\.(jsx|tsx)$/)) return [];
|
|
2174
|
-
if (!/dangerouslySetInnerHTML/i.test(content)) return [];
|
|
2175
|
-
const hasSanitize = /DOMPurify|sanitize|purify|xss|sanitizeHtml|isomorphic-dompurify/i.test(content);
|
|
2176
|
-
if (hasSanitize) return [];
|
|
2177
|
-
return findMatches(
|
|
2178
|
-
content,
|
|
2179
|
-
/dangerouslySetInnerHTML\s*=\s*\{\s*\{/g,
|
|
2180
|
-
dangerousInnerHTML,
|
|
2181
|
-
filePath,
|
|
2182
|
-
() => "Sanitize HTML before using dangerouslySetInnerHTML: dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }}. Install: npm install dompurify"
|
|
2183
|
-
);
|
|
2184
|
-
}
|
|
2185
|
-
};
|
|
2186
|
-
var exposedServerActions = {
|
|
2187
|
-
id: "VC064",
|
|
2188
|
-
title: "Next.js Server Action Without Auth Check",
|
|
2189
|
-
severity: "high",
|
|
2190
|
-
category: "Authorization",
|
|
2191
|
-
description: "Next.js Server Actions ('use server') are publicly callable endpoints. Without authentication checks, anyone can invoke them directly.",
|
|
2192
|
-
check(content, filePath) {
|
|
2193
|
-
if (!filePath.match(/\.(jsx?|tsx?)$/)) return [];
|
|
2194
|
-
if (!/["']use server["']/i.test(content)) return [];
|
|
2195
|
-
const hasAuth = /getServerSession|auth\(\)|currentUser|getUser|requireAuth|session|clerk|getAuth/i.test(content);
|
|
2196
|
-
if (hasAuth) return [];
|
|
2197
|
-
if (/export\s+async\s+function/i.test(content)) {
|
|
2198
|
-
return findMatches(
|
|
2199
|
-
content,
|
|
2200
|
-
/export\s+async\s+function\s+\w+/g,
|
|
2201
|
-
exposedServerActions,
|
|
2202
|
-
filePath,
|
|
2203
|
-
() => "Add authentication to Server Actions: const session = await getServerSession(); if (!session) throw new Error('Unauthorized');"
|
|
2204
|
-
);
|
|
2205
|
-
}
|
|
2206
|
-
return [];
|
|
2207
|
-
}
|
|
2208
|
-
};
|
|
2209
|
-
var unprotectedAPIRoutes = {
|
|
2210
|
-
id: "VC065",
|
|
2211
|
-
title: "Unprotected Next.js API Route",
|
|
2212
|
-
severity: "high",
|
|
2213
|
-
category: "Authorization",
|
|
2214
|
-
description: "Next.js API routes under /api/ without authentication middleware can be called by anyone, exposing data or mutations.",
|
|
2215
|
-
check(content, filePath) {
|
|
2216
|
-
if (!filePath.match(/\/api\/.*\.(jsx?|tsx?)$/) && !filePath.match(/\/app\/api\/.*route\.(jsx?|tsx?)$/)) return [];
|
|
2217
|
-
if (/health|status|public|webhook/i.test(filePath)) return [];
|
|
2218
|
-
const hasAuth = /getServerSession|auth\(\)|currentUser|getUser|requireAuth|session|clerk|getAuth|verifyToken|authenticate|middleware/i.test(content);
|
|
2219
|
-
if (hasAuth) return [];
|
|
2220
|
-
const hasHandler = /export\s+(?:async\s+)?function\s+(?:GET|POST|PUT|DELETE|PATCH)|export\s+default/i.test(content);
|
|
2221
|
-
if (hasHandler) {
|
|
2222
|
-
return findMatches(
|
|
2223
|
-
content,
|
|
2224
|
-
/export\s+(?:async\s+)?function\s+(?:GET|POST|PUT|DELETE|PATCH)/g,
|
|
2225
|
-
unprotectedAPIRoutes,
|
|
2226
|
-
filePath,
|
|
2227
|
-
() => "Add authentication to API routes: const session = await getServerSession(authOptions); if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });"
|
|
2228
|
-
);
|
|
2229
|
-
}
|
|
2230
|
-
return [];
|
|
2231
|
-
}
|
|
2232
|
-
};
|
|
2233
|
-
var clientComponentSecret = {
|
|
2234
|
-
id: "VC066",
|
|
2235
|
-
title: "Secret Used in Client Component",
|
|
2236
|
-
severity: "critical",
|
|
2237
|
-
category: "Secrets",
|
|
2238
|
-
description: "Using server-side secrets (process.env without NEXT_PUBLIC_) in 'use client' components exposes them in the browser bundle.",
|
|
2239
|
-
check(content, filePath) {
|
|
2240
|
-
if (!filePath.match(/\.(jsx?|tsx?)$/)) return [];
|
|
2241
|
-
if (!/["']use client["']/i.test(content)) return [];
|
|
2242
|
-
const pattern = /process\.env\.(?!NEXT_PUBLIC_)[A-Z_]{3,}/g;
|
|
2243
|
-
if (pattern.test(content)) {
|
|
2244
|
-
return findMatches(
|
|
2245
|
-
content,
|
|
2246
|
-
/process\.env\.(?!NEXT_PUBLIC_)[A-Z_]{3,}/g,
|
|
2247
|
-
clientComponentSecret,
|
|
2248
|
-
filePath,
|
|
2249
|
-
() => "Server-side env vars are not available in client components and may leak in builds. Move this logic to a Server Component or API route."
|
|
2250
|
-
);
|
|
2251
|
-
}
|
|
2252
|
-
return [];
|
|
2253
|
-
}
|
|
2254
|
-
};
|
|
2255
|
-
var insecureDeepLink = {
|
|
2256
|
-
id: "VC067",
|
|
2257
|
-
title: "Insecure Deep Link Handling",
|
|
2258
|
-
severity: "high",
|
|
2259
|
-
category: "Injection",
|
|
2260
|
-
description: "Deep links that navigate or execute actions without validating the URL scheme, host, or parameters can be exploited for phishing or unauthorized actions.",
|
|
2261
|
-
check(content, filePath) {
|
|
2262
|
-
if (!/(?:Linking|DeepLinking|deep.?link|handleURL|openURL|url.?scheme)/i.test(content)) return [];
|
|
2263
|
-
const matches = [];
|
|
2264
|
-
const patterns = [
|
|
2265
|
-
/Linking\.addEventListener\s*\([^)]*(?:url|link)/gi,
|
|
2266
|
-
/Linking\.getInitialURL\s*\(\)/g,
|
|
2267
|
-
/handleOpenURL|handleDeepLink|onDeepLink/gi
|
|
2268
|
-
];
|
|
2269
|
-
const hasValidation = /allowedSchemes|allowedHosts|validateURL|isAllowedURL|whitelist|URL.*hostname.*includes/i.test(content);
|
|
2270
|
-
if (hasValidation) return [];
|
|
2271
|
-
for (const p of patterns) {
|
|
2272
|
-
matches.push(...findMatches(
|
|
2273
|
-
content,
|
|
2274
|
-
p,
|
|
2275
|
-
insecureDeepLink,
|
|
2276
|
-
filePath,
|
|
2277
|
-
() => "Validate deep link URLs: check the scheme and host against an allowlist before navigating or executing actions."
|
|
2278
|
-
));
|
|
2279
|
-
}
|
|
2280
|
-
return matches;
|
|
2281
|
-
}
|
|
2282
|
-
};
|
|
2283
|
-
var sensitiveAsyncStorage = {
|
|
2284
|
-
id: "VC068",
|
|
2285
|
-
title: "Sensitive Data in AsyncStorage",
|
|
2286
|
-
severity: "high",
|
|
2287
|
-
category: "Secrets",
|
|
2288
|
-
description: "React Native AsyncStorage is unencrypted. Storing tokens, passwords, or secrets there makes them readable by other apps or anyone with device access.",
|
|
2289
|
-
check(content, filePath) {
|
|
2290
|
-
if (!/AsyncStorage/i.test(content)) return [];
|
|
2291
|
-
const patterns = [
|
|
2292
|
-
/AsyncStorage\.setItem\s*\(\s*["'`](?:token|access_token|auth_token|jwt|session|refresh_token|api_key|password|secret|private_key)/gi
|
|
2293
|
-
];
|
|
2294
|
-
const matches = [];
|
|
2295
|
-
for (const p of patterns) {
|
|
2296
|
-
matches.push(...findMatches(
|
|
2297
|
-
content,
|
|
2298
|
-
p,
|
|
2299
|
-
sensitiveAsyncStorage,
|
|
2300
|
-
filePath,
|
|
2301
|
-
() => "Use react-native-keychain or expo-secure-store instead of AsyncStorage for sensitive data. These use the OS keychain (encrypted)."
|
|
2302
|
-
));
|
|
2303
|
-
}
|
|
2304
|
-
return matches;
|
|
2305
|
-
}
|
|
2306
|
-
};
|
|
2307
|
-
var missingCertPinning = {
|
|
2308
|
-
id: "VC069",
|
|
2309
|
-
title: "Missing Certificate Pinning in Mobile App",
|
|
2310
|
-
severity: "medium",
|
|
2311
|
-
category: "Cryptography",
|
|
2312
|
-
description: "Mobile apps without SSL certificate pinning are vulnerable to MITM attacks via compromised or rogue CAs. Pin your API server's certificate.",
|
|
2313
|
-
check(content, filePath) {
|
|
2314
|
-
if (!/(?:react.native|expo|android|ios|mobile)/i.test(filePath) && !/(?:React.*Native|expo)/i.test(content)) return [];
|
|
2315
|
-
if (!/(?:fetch|axios|http|api|request)/i.test(content)) return [];
|
|
2316
|
-
if (/axios\.create|new\s+(?:HttpClient|ApiClient)/i.test(content)) {
|
|
2317
|
-
const hasPinning = /pinning|certificate|cert|ssl|TrustKit|react-native-ssl-pinning|cert-pinner/i.test(content);
|
|
2318
|
-
if (!hasPinning) {
|
|
2319
|
-
return findMatches(
|
|
2320
|
-
content,
|
|
2321
|
-
/axios\.create|new\s+(?:HttpClient|ApiClient)/gi,
|
|
2322
|
-
missingCertPinning,
|
|
2323
|
-
filePath,
|
|
2324
|
-
() => "Add SSL certificate pinning: use react-native-ssl-pinning or TrustKit. This prevents MITM attacks via rogue certificates."
|
|
2325
|
-
);
|
|
2326
|
-
}
|
|
2327
|
-
}
|
|
2328
|
-
return [];
|
|
2329
|
-
}
|
|
2330
|
-
};
|
|
2331
|
-
var androidDebuggable = {
|
|
2332
|
-
id: "VC070",
|
|
2333
|
-
title: "Android App Debuggable in Production",
|
|
2334
|
-
severity: "high",
|
|
2335
|
-
category: "Configuration",
|
|
2336
|
-
description: "android:debuggable='true' in AndroidManifest.xml allows attackers to attach debuggers, inspect memory, and bypass security controls.",
|
|
2337
|
-
check(content, filePath) {
|
|
2338
|
-
if (!filePath.match(/AndroidManifest\.xml$/i)) return [];
|
|
2339
|
-
if (/android:debuggable\s*=\s*["']true["']/i.test(content)) {
|
|
2340
|
-
return findMatches(
|
|
2341
|
-
content,
|
|
2342
|
-
/android:debuggable\s*=\s*["']true["']/gi,
|
|
2343
|
-
androidDebuggable,
|
|
2344
|
-
filePath,
|
|
2345
|
-
() => "Remove android:debuggable='true' or set it to false. Debug builds should use build variants, not manifest flags."
|
|
2346
|
-
);
|
|
2347
|
-
}
|
|
2348
|
-
return [];
|
|
2349
|
-
}
|
|
2350
|
-
};
|
|
2351
|
-
var djangoDebug = {
|
|
2352
|
-
id: "VC071",
|
|
2353
|
-
title: "Django DEBUG Mode Enabled",
|
|
2354
|
-
severity: "critical",
|
|
2355
|
-
category: "Configuration",
|
|
2356
|
-
description: "Django with DEBUG=True exposes detailed error pages with source code, database queries, environment variables, and installed apps to anyone.",
|
|
2357
|
-
check(content, filePath) {
|
|
2358
|
-
if (!filePath.match(/settings\.py$/i) && !filePath.match(/config.*\.py$/i)) return [];
|
|
2359
|
-
if (/^\s*DEBUG\s*=\s*True\s*$/m.test(content)) {
|
|
2360
|
-
const hasEnvCheck = /os\.environ|env\(|config\(|getenv/i.test(content);
|
|
2361
|
-
if (!hasEnvCheck) {
|
|
2362
|
-
return findMatches(
|
|
2363
|
-
content,
|
|
2364
|
-
/^\s*DEBUG\s*=\s*True/gm,
|
|
2365
|
-
djangoDebug,
|
|
2366
|
-
filePath,
|
|
2367
|
-
() => "Use environment variable: DEBUG = os.environ.get('DEBUG', 'False') == 'True'. Never hardcode DEBUG=True."
|
|
2368
|
-
);
|
|
2369
|
-
}
|
|
2370
|
-
}
|
|
2371
|
-
return [];
|
|
2372
|
-
}
|
|
2373
|
-
};
|
|
2374
|
-
var flaskSecretKey = {
|
|
2375
|
-
id: "VC072",
|
|
2376
|
-
title: "Flask Secret Key Hardcoded",
|
|
2377
|
-
severity: "critical",
|
|
2378
|
-
category: "Secrets",
|
|
2379
|
-
description: "Hardcoded Flask secret_key allows attackers to forge sessions, CSRF tokens, and signed cookies. Must be a random value from environment variables.",
|
|
2380
|
-
check(content, filePath) {
|
|
2381
|
-
if (!filePath.match(/\.py$/)) return [];
|
|
2382
|
-
const patterns = [
|
|
2383
|
-
/(?:app\.)?secret_key\s*=\s*["'`][^"'`]{3,}["'`]/g,
|
|
2384
|
-
/(?:app\.config)\s*\[\s*["']SECRET_KEY["']\s*\]\s*=\s*["'`][^"'`]{3,}["'`]/g
|
|
2385
|
-
];
|
|
2386
|
-
const hasEnv = /os\.environ|env\(|config\(|getenv/i.test(content);
|
|
2387
|
-
if (hasEnv) return [];
|
|
2388
|
-
const matches = [];
|
|
2389
|
-
for (const p of patterns) {
|
|
2390
|
-
matches.push(...findMatches(
|
|
2391
|
-
content,
|
|
2392
|
-
p,
|
|
2393
|
-
flaskSecretKey,
|
|
2394
|
-
filePath,
|
|
2395
|
-
() => `Use environment variable: app.secret_key = os.environ['SECRET_KEY']. Generate with: python -c "import secrets; print(secrets.token_hex(32))"`
|
|
2396
|
-
));
|
|
2397
|
-
}
|
|
2398
|
-
return matches;
|
|
2399
|
-
}
|
|
2400
|
-
};
|
|
2401
|
-
var pickleDeserialization = {
|
|
2402
|
-
id: "VC073",
|
|
2403
|
-
title: "Unsafe Pickle Deserialization",
|
|
2404
|
-
severity: "critical",
|
|
2405
|
-
category: "Injection",
|
|
2406
|
-
description: "pickle.loads() on untrusted data allows arbitrary code execution. An attacker can craft a pickle payload that runs system commands on your server.",
|
|
2407
|
-
check(content, filePath) {
|
|
2408
|
-
if (!filePath.match(/\.py$/)) return [];
|
|
2409
|
-
const matches = [];
|
|
2410
|
-
const patterns = [
|
|
2411
|
-
/pickle\.loads?\s*\(/g,
|
|
2412
|
-
/cPickle\.loads?\s*\(/g,
|
|
2413
|
-
/shelve\.open\s*\(/g,
|
|
2414
|
-
/yaml\.load\s*\([^)]*(?!Loader\s*=\s*yaml\.SafeLoader)/g
|
|
2415
|
-
];
|
|
2416
|
-
const hasSafe = /restricted_loads|SafeUnpickler|safe_load|yaml\.safe_load/i.test(content);
|
|
2417
|
-
if (hasSafe) return [];
|
|
2418
|
-
for (const p of patterns) {
|
|
2419
|
-
matches.push(...findMatches(
|
|
2420
|
-
content,
|
|
2421
|
-
p,
|
|
2422
|
-
pickleDeserialization,
|
|
2423
|
-
filePath,
|
|
2424
|
-
() => "Never unpickle untrusted data \u2014 it allows arbitrary code execution. Use JSON for data exchange, or yaml.safe_load() for YAML."
|
|
2425
|
-
));
|
|
2426
|
-
}
|
|
2427
|
-
return matches;
|
|
2428
|
-
}
|
|
2429
|
-
};
|
|
2430
|
-
var missingCSRF = {
|
|
2431
|
-
id: "VC074",
|
|
2432
|
-
title: "CSRF Protection Disabled",
|
|
2433
|
-
severity: "high",
|
|
2434
|
-
category: "Authorization",
|
|
2435
|
-
description: "Using @csrf_exempt on state-changing views (POST/PUT/DELETE) allows attackers to forge requests from other sites, performing actions as authenticated users.",
|
|
2436
|
-
check(content, filePath) {
|
|
2437
|
-
if (!filePath.match(/\.py$/)) return [];
|
|
2438
|
-
const matches = [];
|
|
2439
|
-
if (/csrf_exempt/i.test(content)) {
|
|
2440
|
-
matches.push(...findMatches(
|
|
2441
|
-
content,
|
|
2442
|
-
/@csrf_exempt/g,
|
|
2443
|
-
missingCSRF,
|
|
2444
|
-
filePath,
|
|
2445
|
-
() => "Remove @csrf_exempt and use proper CSRF tokens. For APIs, use token-based auth (JWT) instead of session cookies."
|
|
2446
|
-
));
|
|
2447
|
-
}
|
|
2448
|
-
if (/MIDDLEWARE.*=.*\[/s.test(content) && !/CsrfViewMiddleware/i.test(content) && /django/i.test(content)) {
|
|
2449
|
-
matches.push(...findMatches(
|
|
2450
|
-
content,
|
|
2451
|
-
/MIDDLEWARE\s*=/g,
|
|
2452
|
-
missingCSRF,
|
|
2453
|
-
filePath,
|
|
2454
|
-
() => "Re-add 'django.middleware.csrf.CsrfViewMiddleware' to MIDDLEWARE. CSRF protection is essential for session-based auth."
|
|
2455
|
-
));
|
|
2456
|
-
}
|
|
2457
|
-
return matches;
|
|
2458
|
-
}
|
|
2459
|
-
};
|
|
2460
|
-
var githubActionsInjection = {
|
|
2461
|
-
id: "VC075",
|
|
2462
|
-
title: "GitHub Actions Script Injection",
|
|
2463
|
-
severity: "critical",
|
|
2464
|
-
category: "Injection",
|
|
2465
|
-
description: "Using ${{ github.event.* }} directly in 'run:' steps allows attackers to inject shell commands via PR titles, issue bodies, or branch names.",
|
|
2466
|
-
check(content, filePath) {
|
|
2467
|
-
if (!filePath.match(/\.github\/workflows\/.*\.(yml|yaml)$/i)) return [];
|
|
2468
|
-
const matches = [];
|
|
2469
|
-
const patterns = [
|
|
2470
|
-
/run:.*\$\{\{\s*github\.event\.(?:issue|pull_request|comment|review|head_commit)\.(?:title|body|message)/gi,
|
|
2471
|
-
/run:.*\$\{\{\s*github\.event\.(?:inputs|head_ref|base_ref)/gi
|
|
2472
|
-
];
|
|
2473
|
-
for (const p of patterns) {
|
|
2474
|
-
matches.push(...findMatches(
|
|
2475
|
-
content,
|
|
2476
|
-
p,
|
|
2477
|
-
githubActionsInjection,
|
|
2478
|
-
filePath,
|
|
2479
|
-
() => "Never use ${{ github.event.* }} directly in 'run:'. Pass it as an environment variable: env: TITLE: ${{ github.event.issue.title }} then use $TITLE in the script."
|
|
2480
|
-
));
|
|
2481
|
-
}
|
|
2482
|
-
return matches;
|
|
2483
|
-
}
|
|
2484
|
-
};
|
|
2485
|
-
var secretsInCI = {
|
|
2486
|
-
id: "VC076",
|
|
2487
|
-
title: "Hardcoded Secrets in CI/CD Config",
|
|
2488
|
-
severity: "critical",
|
|
2489
|
-
category: "Secrets",
|
|
2490
|
-
description: "Hardcoded tokens, passwords, or API keys in CI/CD workflow files are visible to anyone with repo access. Use encrypted secrets instead.",
|
|
2491
|
-
check(content, filePath) {
|
|
2492
|
-
if (!filePath.match(/\.github\/workflows\/|\.gitlab-ci|Jenkinsfile|\.circleci|bitbucket-pipelines/i)) return [];
|
|
2493
|
-
const matches = [];
|
|
2494
|
-
const patterns = [
|
|
2495
|
-
/(?:password|token|key|secret|api_key|apikey)\s*[:=]\s*["'`][A-Za-z0-9+/=_-]{20,}["'`]/gi,
|
|
2496
|
-
/(?:DOCKER_PASSWORD|NPM_TOKEN|AWS_SECRET_ACCESS_KEY|GH_TOKEN|GITHUB_TOKEN)\s*[:=]\s*["'`][^"'`$]+["'`]/gi
|
|
2497
|
-
];
|
|
2498
|
-
for (const p of patterns) {
|
|
2499
|
-
matches.push(...findMatches(
|
|
2500
|
-
content,
|
|
2501
|
-
p,
|
|
2502
|
-
secretsInCI,
|
|
2503
|
-
filePath,
|
|
2504
|
-
() => "Use repository secrets: ${{ secrets.MY_TOKEN }} (GitHub) or CI/CD variable settings. Never hardcode credentials in workflow files."
|
|
2505
|
-
));
|
|
2506
|
-
}
|
|
2507
|
-
return matches;
|
|
2508
|
-
}
|
|
2509
|
-
};
|
|
2510
|
-
var corsServerless = {
|
|
2511
|
-
id: "VC077",
|
|
2512
|
-
title: "CORS Wildcard in Serverless Config",
|
|
2513
|
-
severity: "high",
|
|
2514
|
-
category: "Configuration",
|
|
2515
|
-
description: "Setting Access-Control-Allow-Origin: * in serverless.yml, vercel.json, or similar configs allows any website to make authenticated requests to your API.",
|
|
2516
|
-
check(content, filePath) {
|
|
2517
|
-
if (!filePath.match(/serverless\.(yml|yaml)|vercel\.json|netlify\.toml|amplify\.yml/i)) return [];
|
|
2518
|
-
const matches = [];
|
|
2519
|
-
if (/(?:Access-Control-Allow-Origin|allowOrigin|cors).*['"]\*['"]/i.test(content)) {
|
|
2520
|
-
matches.push(...findMatches(
|
|
2521
|
-
content,
|
|
2522
|
-
/(?:Access-Control-Allow-Origin|allowOrigin|cors).*['"]\*['"]/gi,
|
|
2523
|
-
corsServerless,
|
|
2524
|
-
filePath,
|
|
2525
|
-
() => "Replace wildcard CORS with specific origins: allowOrigin: ['https://yourdomain.com']. Wildcard allows any site to call your API."
|
|
2526
|
-
));
|
|
2527
|
-
}
|
|
2528
|
-
return matches;
|
|
2529
|
-
}
|
|
2530
|
-
};
|
|
2531
|
-
var k8sPrivileged = {
|
|
2532
|
-
id: "VC078",
|
|
2533
|
-
title: "Kubernetes Privileged Container",
|
|
2534
|
-
severity: "critical",
|
|
2535
|
-
category: "Configuration",
|
|
2536
|
-
description: "Running containers with privileged: true or as root in Kubernetes gives full host access, making container escapes trivial.",
|
|
2537
|
-
check(content, filePath) {
|
|
2538
|
-
if (!filePath.match(/\.(yaml|yml)$/) || !/(?:kind|apiVersion|container)/i.test(content)) return [];
|
|
2539
|
-
const matches = [];
|
|
2540
|
-
const patterns = [
|
|
2541
|
-
/privileged\s*:\s*true/g,
|
|
2542
|
-
/runAsUser\s*:\s*0\b/g,
|
|
2543
|
-
/runAsNonRoot\s*:\s*false/g,
|
|
2544
|
-
/allowPrivilegeEscalation\s*:\s*true/g
|
|
2545
|
-
];
|
|
2546
|
-
for (const p of patterns) {
|
|
2547
|
-
matches.push(...findMatches(
|
|
2548
|
-
content,
|
|
2549
|
-
p,
|
|
2550
|
-
k8sPrivileged,
|
|
2551
|
-
filePath,
|
|
2552
|
-
() => "Set securityContext: { privileged: false, runAsNonRoot: true, allowPrivilegeEscalation: false }. Never run containers as root in production."
|
|
2553
|
-
));
|
|
2554
|
-
}
|
|
2555
|
-
return matches;
|
|
2556
|
-
}
|
|
2557
|
-
};
|
|
2558
|
-
function detectFramework(files) {
|
|
2559
|
-
const frameworks = /* @__PURE__ */ new Set();
|
|
2560
|
-
for (const { path, content } of files) {
|
|
2561
|
-
if (path.match(/next\.config/i) || /from\s+["']next/i.test(content)) frameworks.add("next.js");
|
|
2562
|
-
if (/from\s+["']react-native/i.test(content) || path.match(/react-native\.config/i)) frameworks.add("react-native");
|
|
2563
|
-
else if (/from\s+["']react/i.test(content) || /import\s+React/i.test(content)) frameworks.add("react");
|
|
2564
|
-
if (/from\s+["']express/i.test(content) || /require\s*\(\s*["']express/i.test(content)) frameworks.add("express");
|
|
2565
|
-
if (/from\s+["']hono/i.test(content)) frameworks.add("hono");
|
|
2566
|
-
if (/from\s+["']fastify/i.test(content)) frameworks.add("fastify");
|
|
2567
|
-
if (/from\s+["']electron/i.test(content) || path.match(/electron/i)) frameworks.add("electron");
|
|
2568
|
-
if (path.match(/settings\.py$/) || /from\s+django/i.test(content)) frameworks.add("django");
|
|
2569
|
-
if (/from\s+flask/i.test(content) || /Flask\s*\(/i.test(content)) frameworks.add("flask");
|
|
2570
|
-
if (/from\s+["']vue/i.test(content) || path.match(/vue\.config/i)) frameworks.add("vue");
|
|
2571
|
-
if (/from\s+["']svelte/i.test(content) || path.match(/svelte\.config/i)) frameworks.add("svelte");
|
|
2572
|
-
}
|
|
2573
|
-
if (frameworks.size === 0) frameworks.add("unknown");
|
|
2574
|
-
return [...frameworks];
|
|
2575
|
-
}
|
|
2576
|
-
function calculateGrade(findings, totalFiles) {
|
|
2577
|
-
if (findings.length === 0) {
|
|
2578
|
-
return { grade: "A+", score: 100, summary: "No security issues detected. Excellent!" };
|
|
2579
|
-
}
|
|
2580
|
-
let deductions = 0;
|
|
2581
|
-
for (const f of findings) {
|
|
2582
|
-
switch (f.severity) {
|
|
2583
|
-
case "critical":
|
|
2584
|
-
deductions += 15;
|
|
2585
|
-
break;
|
|
2586
|
-
case "high":
|
|
2587
|
-
deductions += 8;
|
|
2588
|
-
break;
|
|
2589
|
-
case "medium":
|
|
2590
|
-
deductions += 4;
|
|
2591
|
-
break;
|
|
2592
|
-
case "low":
|
|
2593
|
-
deductions += 1;
|
|
2594
|
-
break;
|
|
2595
|
-
}
|
|
2596
|
-
}
|
|
2597
|
-
const sizeBuffer = Math.min(Math.log2(Math.max(totalFiles, 1)) * 2, 15);
|
|
2598
|
-
const score = Math.max(0, Math.min(100, 100 - deductions + sizeBuffer));
|
|
2599
|
-
let grade;
|
|
2600
|
-
let summary;
|
|
2601
|
-
if (score >= 95) {
|
|
2602
|
-
grade = "A+";
|
|
2603
|
-
summary = "Excellent security posture with minimal issues.";
|
|
2604
|
-
} else if (score >= 85) {
|
|
2605
|
-
grade = "A";
|
|
2606
|
-
summary = "Strong security with a few minor concerns.";
|
|
2607
|
-
} else if (score >= 70) {
|
|
2608
|
-
grade = "B";
|
|
2609
|
-
summary = "Good security but some issues need attention.";
|
|
2610
|
-
} else if (score >= 55) {
|
|
2611
|
-
grade = "C";
|
|
2612
|
-
summary = "Fair security \u2014 several vulnerabilities should be fixed.";
|
|
2613
|
-
} else if (score >= 35) {
|
|
2614
|
-
grade = "D";
|
|
2615
|
-
summary = "Poor security \u2014 critical issues require immediate attention.";
|
|
2616
|
-
} else {
|
|
2617
|
-
grade = "F";
|
|
2618
|
-
summary = "Failing \u2014 serious vulnerabilities present. Fix critical issues immediately.";
|
|
2619
|
-
}
|
|
2620
|
-
return { grade, score: Math.round(score), summary };
|
|
2621
|
-
}
|
|
2622
|
-
var jwtAlgConfusion = {
|
|
2623
|
-
id: "VC079",
|
|
2624
|
-
title: "JWT Algorithm Confusion (alg:none)",
|
|
2625
|
-
severity: "critical",
|
|
2626
|
-
category: "Authentication",
|
|
2627
|
-
description: "Accepting 'none' as a JWT algorithm allows attackers to forge tokens by removing the signature entirely.",
|
|
2628
|
-
check(content, filePath) {
|
|
2629
|
-
if (!/jwt|jsonwebtoken|jose/i.test(content)) return [];
|
|
2630
|
-
const matches = [];
|
|
2631
|
-
const patterns = [
|
|
2632
|
-
/algorithms\s*:\s*\[.*["']none["']/gi,
|
|
2633
|
-
/algorithm\s*[:=]\s*["']none["']/gi
|
|
2634
|
-
];
|
|
2635
|
-
for (const p of patterns) {
|
|
2636
|
-
matches.push(...findMatches(
|
|
2637
|
-
content,
|
|
2638
|
-
p,
|
|
2639
|
-
jwtAlgConfusion,
|
|
2640
|
-
filePath,
|
|
2641
|
-
() => "Never allow algorithm 'none'. Explicitly specify: algorithms: ['RS256'] or algorithms: ['HS256']. Reject tokens with alg:none."
|
|
2642
|
-
));
|
|
2643
|
-
}
|
|
2644
|
-
if (/jwt\.verify\s*\([^)]*\)\s*(?!.*algorithms)/i.test(content) && !/algorithms/i.test(content)) {
|
|
2645
|
-
matches.push(...findMatches(
|
|
2646
|
-
content,
|
|
2647
|
-
/jwt\.verify\s*\(/g,
|
|
2648
|
-
jwtAlgConfusion,
|
|
2649
|
-
filePath,
|
|
2650
|
-
() => "Specify allowed algorithms in jwt.verify: jwt.verify(token, secret, { algorithms: ['HS256'] })."
|
|
2651
|
-
));
|
|
2652
|
-
}
|
|
2653
|
-
return matches;
|
|
2654
|
-
}
|
|
2655
|
-
};
|
|
2656
|
-
var regexDos = {
|
|
2657
|
-
id: "VC080",
|
|
2658
|
-
title: "Potential Regular Expression DoS (ReDoS)",
|
|
2659
|
-
severity: "high",
|
|
2660
|
-
category: "Availability",
|
|
2661
|
-
description: "Nested quantifiers like (a+)+ or (a*){2,} cause catastrophic backtracking, allowing attackers to freeze your server with crafted input.",
|
|
2662
|
-
check(content, filePath) {
|
|
2663
|
-
if (filePath.includes("test") || filePath.includes("mock")) return [];
|
|
2664
|
-
const matches = [];
|
|
2665
|
-
const patterns = [
|
|
2666
|
-
/new\s+RegExp\s*\(\s*["'`].*\([^)]*[+*]\)[+*{]/g,
|
|
2667
|
-
/\/.*\([^)]*[+*]\)[+*{].*\//g
|
|
2668
|
-
];
|
|
2669
|
-
for (const p of patterns) {
|
|
2670
|
-
matches.push(...findMatches(
|
|
2671
|
-
content,
|
|
2672
|
-
p,
|
|
2673
|
-
regexDos,
|
|
2674
|
-
filePath,
|
|
2675
|
-
() => "Avoid nested quantifiers in regex. Use atomic groups, possessive quantifiers, or the 're2' library for safe regex execution."
|
|
2676
|
-
));
|
|
2677
|
-
}
|
|
2678
|
-
return matches;
|
|
2679
|
-
}
|
|
2680
|
-
};
|
|
2681
|
-
var xxeVulnerability = {
|
|
2682
|
-
id: "VC081",
|
|
2683
|
-
title: "XML External Entity (XXE) Injection",
|
|
2684
|
-
severity: "critical",
|
|
2685
|
-
category: "Injection",
|
|
2686
|
-
description: "XML parsers that process external entities allow attackers to read files, perform SSRF, or cause DoS via billion-laughs attacks.",
|
|
2687
|
-
check(content, filePath) {
|
|
2688
|
-
if (!/xml|parseXML|DOMParser|SAXParser|etree|lxml/i.test(content)) return [];
|
|
2689
|
-
const matches = [];
|
|
2690
|
-
const patterns = [
|
|
2691
|
-
/\.parseXML\s*\(/g,
|
|
2692
|
-
/new\s+DOMParser\s*\(\)/g,
|
|
2693
|
-
/etree\.parse\s*\(/g,
|
|
2694
|
-
/lxml\.etree/g,
|
|
2695
|
-
/SAXParserFactory/g,
|
|
2696
|
-
/XMLReaderFactory/g
|
|
2697
|
-
];
|
|
2698
|
-
const hasProtection = /noent.*false|resolveExternals.*false|FEATURE_EXTERNAL.*false|defusedxml|disallow-doctype-decl/i.test(content);
|
|
2699
|
-
if (hasProtection) return [];
|
|
2700
|
-
for (const p of patterns) {
|
|
2701
|
-
matches.push(...findMatches(
|
|
2702
|
-
content,
|
|
2703
|
-
p,
|
|
2704
|
-
xxeVulnerability,
|
|
2705
|
-
filePath,
|
|
2706
|
-
() => "Disable external entities: set noent: false, or use defusedxml (Python). For Java: factory.setFeature('http://apache.org/xml/features/disallow-doctype-decl', true)."
|
|
2707
|
-
));
|
|
2708
|
-
}
|
|
2709
|
-
return matches;
|
|
2710
|
-
}
|
|
2711
|
-
};
|
|
2712
|
-
var ssti = {
|
|
2713
|
-
id: "VC082",
|
|
2714
|
-
title: "Server-Side Template Injection (SSTI)",
|
|
2715
|
-
severity: "critical",
|
|
2716
|
-
category: "Injection",
|
|
2717
|
-
description: "Rendering templates from user-controlled strings allows attackers to execute arbitrary code on the server.",
|
|
2718
|
-
check(content, filePath) {
|
|
2719
|
-
if (filePath.includes("test") || filePath.includes("mock")) return [];
|
|
2720
|
-
const matches = [];
|
|
2721
|
-
const patterns = [
|
|
2722
|
-
/render_template_string\s*\(\s*(?!["'`])/g,
|
|
2723
|
-
/Template\s*\(\s*(?:req\.|body\.|input|params|args|user)/gi,
|
|
2724
|
-
/engine\.render\s*\(\s*(?:req\.|body\.|input)/gi,
|
|
2725
|
-
/nunjucks\.renderString\s*\(\s*(?:req\.|body\.|input)/gi,
|
|
2726
|
-
/ejs\.render\s*\(\s*(?:req\.|body\.|input)/gi
|
|
2727
|
-
];
|
|
2728
|
-
for (const p of patterns) {
|
|
2729
|
-
matches.push(...findMatches(
|
|
2730
|
-
content,
|
|
2731
|
-
p,
|
|
2732
|
-
ssti,
|
|
2733
|
-
filePath,
|
|
2734
|
-
() => "Never render templates from user input. Use pre-defined templates and pass data as context variables: render_template('template.html', data=user_data)."
|
|
2735
|
-
));
|
|
2736
|
-
}
|
|
2737
|
-
return matches;
|
|
2738
|
-
}
|
|
2739
|
-
};
|
|
2740
|
-
var javaDeserialization = {
|
|
2741
|
-
id: "VC083",
|
|
2742
|
-
title: "Insecure Java Deserialization",
|
|
2743
|
-
severity: "critical",
|
|
2744
|
-
category: "Injection",
|
|
2745
|
-
description: "ObjectInputStream.readObject() on untrusted data allows arbitrary code execution via gadget chains in the classpath.",
|
|
2746
|
-
check(content, filePath) {
|
|
2747
|
-
if (!filePath.match(/\.java$|\.kt$/)) return [];
|
|
2748
|
-
const matches = [];
|
|
2749
|
-
const patterns = [
|
|
2750
|
-
/ObjectInputStream\s*\(/g,
|
|
2751
|
-
/\.readObject\s*\(\)/g,
|
|
2752
|
-
/XMLDecoder\s*\(/g,
|
|
2753
|
-
/XStream\s*\(\)/g
|
|
2754
|
-
];
|
|
2755
|
-
const hasSafe = /ValidatingObjectInputStream|ObjectInputFilter|SerialKiller|NotSerializableException/i.test(content);
|
|
2756
|
-
if (hasSafe) return [];
|
|
2757
|
-
for (const p of patterns) {
|
|
2758
|
-
matches.push(...findMatches(
|
|
2759
|
-
content,
|
|
2760
|
-
p,
|
|
2761
|
-
javaDeserialization,
|
|
2762
|
-
filePath,
|
|
2763
|
-
() => "Use ValidatingObjectInputStream with an allowlist of classes, or avoid Java serialization entirely. Use JSON instead."
|
|
2764
|
-
));
|
|
2765
|
-
}
|
|
2766
|
-
return matches;
|
|
2767
|
-
}
|
|
2768
|
-
};
|
|
2769
|
-
var missingSRI = {
|
|
2770
|
-
id: "VC084",
|
|
2771
|
-
title: "Missing Subresource Integrity (SRI)",
|
|
2772
|
-
severity: "medium",
|
|
2773
|
-
category: "Configuration",
|
|
2774
|
-
description: "External scripts and stylesheets loaded without integrity= attributes can be tampered with if the CDN is compromised.",
|
|
2775
|
-
check(content, filePath) {
|
|
2776
|
-
if (!filePath.match(/\.(html|htm|jsx|tsx|ejs|hbs)$/)) return [];
|
|
2777
|
-
const matches = [];
|
|
2778
|
-
const scriptPattern = /<script\s+[^>]*src\s*=\s*["']https?:\/\/[^"']+["'][^>]*>/gi;
|
|
2779
|
-
let m;
|
|
2780
|
-
const re = new RegExp(scriptPattern.source, scriptPattern.flags);
|
|
2781
|
-
while ((m = re.exec(content)) !== null) {
|
|
2782
|
-
if (!m[0].includes("integrity")) {
|
|
2783
|
-
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
2784
|
-
matches.push({
|
|
2785
|
-
rule: "VC084",
|
|
2786
|
-
title: missingSRI.title,
|
|
2787
|
-
severity: "medium",
|
|
2788
|
-
category: "Configuration",
|
|
2789
|
-
file: filePath,
|
|
2790
|
-
line: lineNum,
|
|
2791
|
-
snippet: getSnippet(content, lineNum),
|
|
2792
|
-
fix: 'Add integrity and crossorigin attributes: <script src="..." integrity="sha384-..." crossorigin="anonymous">'
|
|
2793
|
-
});
|
|
2794
|
-
}
|
|
2795
|
-
}
|
|
2796
|
-
return matches;
|
|
2797
|
-
}
|
|
2798
|
-
};
|
|
2799
|
-
var exposedAdminRoutes = {
|
|
2800
|
-
id: "VC085",
|
|
2801
|
-
title: "Exposed Admin or Debug Route",
|
|
2802
|
-
severity: "high",
|
|
2803
|
-
category: "Information Leakage",
|
|
2804
|
-
description: "Routes like /admin, /debug, /phpinfo, or /actuator without authentication expose sensitive controls and information to attackers.",
|
|
2805
|
-
check(content, filePath) {
|
|
2806
|
-
if (!/(?:\/api\/|routes?\/|server\.|app\.|index\.[jt]s)/i.test(filePath)) return [];
|
|
2807
|
-
const matches = [];
|
|
2808
|
-
const patterns = [
|
|
2809
|
-
/[.'"]\s*(?:get|use|all)\s*\(\s*["'`]\/(?:admin|debug|_debug|__debug__|phpinfo|actuator|graphiql|playground|swagger)["'`]/gi
|
|
2810
|
-
];
|
|
2811
|
-
const hasAuth = /auth|requireAuth|isAdmin|requireAdmin|authenticate|middleware.*admin/i.test(content);
|
|
2812
|
-
if (hasAuth) return [];
|
|
2813
|
-
for (const p of patterns) {
|
|
2814
|
-
matches.push(...findMatches(
|
|
2815
|
-
content,
|
|
2816
|
-
p,
|
|
2817
|
-
exposedAdminRoutes,
|
|
2818
|
-
filePath,
|
|
2819
|
-
() => "Protect admin/debug routes with authentication middleware. In production, disable debug endpoints entirely."
|
|
2820
|
-
));
|
|
2821
|
-
}
|
|
2822
|
-
return matches;
|
|
2823
|
-
}
|
|
2824
|
-
};
|
|
2825
|
-
var insecureWebSocket = {
|
|
2826
|
-
id: "VC086",
|
|
2827
|
-
title: "Insecure WebSocket Connection (ws://)",
|
|
2828
|
-
severity: "medium",
|
|
2829
|
-
category: "Configuration",
|
|
2830
|
-
description: "Using ws:// instead of wss:// transmits data in plaintext, vulnerable to eavesdropping and man-in-the-middle attacks.",
|
|
2831
|
-
check(content, filePath) {
|
|
2832
|
-
if (filePath.includes("test") || filePath.includes("mock")) return [];
|
|
2833
|
-
return findMatches(
|
|
2834
|
-
content,
|
|
2835
|
-
/new\s+WebSocket\s*\(\s*["'`]ws:\/\//g,
|
|
2836
|
-
insecureWebSocket,
|
|
2837
|
-
filePath,
|
|
2838
|
-
() => "Use wss:// (WebSocket Secure) instead of ws:// for encrypted connections: new WebSocket('wss://...')."
|
|
2839
|
-
);
|
|
2840
|
-
}
|
|
2841
|
-
};
|
|
2842
|
-
var missingHSTS = {
|
|
2843
|
-
id: "VC087",
|
|
2844
|
-
title: "Missing HTTP Strict Transport Security (HSTS)",
|
|
2845
|
-
severity: "medium",
|
|
2846
|
-
category: "Configuration",
|
|
2847
|
-
description: "Without HSTS headers, browsers allow downgrade attacks from HTTPS to HTTP, exposing traffic to interception.",
|
|
2848
|
-
check(content, filePath) {
|
|
2849
|
-
if (!/(?:server|app|index|main)\.[jt]sx?$/.test(filePath)) return [];
|
|
2850
|
-
if (!/(?:express|hono|fastify|koa)/i.test(content)) return [];
|
|
2851
|
-
if (/Strict-Transport-Security|hsts|helmet/i.test(content)) return [];
|
|
2852
|
-
return findMatches(
|
|
2853
|
-
content,
|
|
2854
|
-
/(?:express|hono|fastify|koa)\s*\(/gi,
|
|
2855
|
-
missingHSTS,
|
|
2856
|
-
filePath,
|
|
2857
|
-
() => "Add HSTS header: res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'). Or use helmet()."
|
|
2858
|
-
);
|
|
2859
|
-
}
|
|
2860
|
-
};
|
|
2861
|
-
var sensitiveURLParams = {
|
|
2862
|
-
id: "VC088",
|
|
2863
|
-
title: "Sensitive Data in URL Parameters",
|
|
2864
|
-
severity: "high",
|
|
2865
|
-
category: "Information Leakage",
|
|
2866
|
-
description: "Passing passwords, tokens, or API keys in URL query parameters exposes them in server logs, browser history, and referrer headers.",
|
|
2867
|
-
check(content, filePath) {
|
|
2868
|
-
if (filePath.includes("test") || filePath.includes("mock")) return [];
|
|
2869
|
-
const matches = [];
|
|
2870
|
-
const patterns = [
|
|
2871
|
-
/\?\s*(?:password|token|secret|api_key|apiKey|access_token|ssn|credit_card)\s*=/gi,
|
|
2872
|
-
/[&?](?:password|token|secret|api_key|apiKey|access_token)\s*=\s*\$\{/gi
|
|
2873
|
-
];
|
|
2874
|
-
for (const p of patterns) {
|
|
2875
|
-
matches.push(...findMatches(
|
|
2876
|
-
content,
|
|
2877
|
-
p,
|
|
2878
|
-
sensitiveURLParams,
|
|
2879
|
-
filePath,
|
|
2880
|
-
() => "Never pass sensitive data in URL parameters. Use request headers (Authorization: Bearer ...) or POST body instead."
|
|
2881
|
-
));
|
|
2882
|
-
}
|
|
2883
|
-
return matches;
|
|
2884
|
-
}
|
|
2885
|
-
};
|
|
2886
|
-
var missingContentDisposition = {
|
|
2887
|
-
id: "VC089",
|
|
2888
|
-
title: "File Download Missing Content-Disposition",
|
|
2889
|
-
severity: "medium",
|
|
2890
|
-
category: "Configuration",
|
|
2891
|
-
description: "File download endpoints without Content-Disposition headers may render files inline, leading to XSS if the file contains HTML/JS.",
|
|
2892
|
-
check(content, filePath) {
|
|
2893
|
-
if (!/(?:download|sendFile|send_file|pipe|createReadStream)/i.test(content)) return [];
|
|
2894
|
-
if (!/(?:\/api\/|routes?\/|controllers?\/|server\.|handler)/i.test(filePath)) return [];
|
|
2895
|
-
if (/Content-Disposition|attachment|download/i.test(content)) return [];
|
|
2896
|
-
return findMatches(
|
|
2897
|
-
content,
|
|
2898
|
-
/(?:sendFile|send_file|createReadStream|\.pipe)\s*\(/gi,
|
|
2899
|
-
missingContentDisposition,
|
|
2900
|
-
filePath,
|
|
2901
|
-
() => `Set Content-Disposition: attachment header on file downloads: res.setHeader('Content-Disposition', 'attachment; filename="file.pdf"').`
|
|
2902
|
-
);
|
|
2903
|
-
}
|
|
2904
|
-
};
|
|
2905
|
-
var hostHeaderRedirect = {
|
|
2906
|
-
id: "VC090",
|
|
2907
|
-
title: "Open Redirect via Host Header",
|
|
2908
|
-
severity: "high",
|
|
2909
|
-
category: "Injection",
|
|
2910
|
-
description: "Using req.headers.host to construct redirect URLs allows attackers to inject a malicious host header, redirecting users to phishing sites.",
|
|
2911
|
-
check(content, filePath) {
|
|
2912
|
-
if (filePath.includes("test") || filePath.includes("mock")) return [];
|
|
2913
|
-
const matches = [];
|
|
2914
|
-
if (/req\.headers\.host|req\.get\s*\(\s*["']host["']\)/i.test(content) && /redirect|location/i.test(content)) {
|
|
2915
|
-
matches.push(...findMatches(
|
|
2916
|
-
content,
|
|
2917
|
-
/req\.headers\.host|req\.get\s*\(\s*["']host["']\)/gi,
|
|
2918
|
-
hostHeaderRedirect,
|
|
2919
|
-
filePath,
|
|
2920
|
-
() => "Don't use req.headers.host for redirects \u2014 it's attacker-controlled. Use a hardcoded domain or environment variable."
|
|
2921
|
-
));
|
|
2922
|
-
}
|
|
2923
|
-
return matches;
|
|
2924
|
-
}
|
|
2925
|
-
};
|
|
2926
|
-
var raceCondition = {
|
|
2927
|
-
id: "VC091",
|
|
2928
|
-
title: "Potential Race Condition (TOCTOU)",
|
|
2929
|
-
severity: "high",
|
|
2930
|
-
category: "Authorization",
|
|
2931
|
-
description: "Check-then-act patterns (e.g., checking if a file exists then writing to it) are vulnerable to race conditions where state changes between the check and the action.",
|
|
2932
|
-
check(content, filePath) {
|
|
2933
|
-
if (filePath.includes("test") || filePath.includes("mock")) return [];
|
|
2934
|
-
const matches = [];
|
|
2935
|
-
const patterns = [
|
|
2936
|
-
/(?:existsSync|exists)\s*\([^)]+\)[\s\S]{0,50}(?:writeFileSync|writeFile|unlinkSync|unlink|renameSync)\s*\(/g,
|
|
2937
|
-
/os\.path\.exists\s*\([^)]+\)[\s\S]{0,50}open\s*\(/g,
|
|
2938
|
-
/File\.exists\?\s*\([^)]+\)[\s\S]{0,50}File\.(?:write|delete)/g
|
|
2939
|
-
];
|
|
2940
|
-
for (const p of patterns) {
|
|
2941
|
-
matches.push(...findMatches(
|
|
2942
|
-
content,
|
|
2943
|
-
p,
|
|
2944
|
-
raceCondition,
|
|
2945
|
-
filePath,
|
|
2946
|
-
() => "Use atomic operations instead of check-then-act. For files: use fs.open with 'wx' flag (exclusive create), or use file locks."
|
|
2947
|
-
));
|
|
2948
|
-
}
|
|
2949
|
-
return matches;
|
|
2950
|
-
}
|
|
2951
|
-
};
|
|
2952
|
-
var unsafeObjectAssign = {
|
|
2953
|
-
id: "VC092",
|
|
2954
|
-
title: "Unsafe Object Spread from User Input",
|
|
2955
|
-
severity: "medium",
|
|
2956
|
-
category: "Injection",
|
|
2957
|
-
description: "Spreading request body into a new object can copy __proto__, constructor, or other dangerous properties, enabling prototype pollution.",
|
|
2958
|
-
check(content, filePath) {
|
|
2959
|
-
if (filePath.includes("test") || filePath.includes("mock")) return [];
|
|
2960
|
-
const matches = [];
|
|
2961
|
-
const patterns = [
|
|
2962
|
-
/Object\.assign\s*\(\s*\{\s*\}\s*,\s*(?:req\.(?:body|query|params)|body|input)\s*\)/gi,
|
|
2963
|
-
/\{\s*\.\.\.(?:req\.(?:body|query|params)|body|input)\s*\}/gi
|
|
2964
|
-
];
|
|
2965
|
-
const hasSafe = /omit.*__proto__|sanitize|pick\(|lodash\.pick|stripProto/i.test(content);
|
|
2966
|
-
if (hasSafe) return [];
|
|
2967
|
-
for (const p of patterns) {
|
|
2968
|
-
matches.push(...findMatches(
|
|
2969
|
-
content,
|
|
2970
|
-
p,
|
|
2971
|
-
unsafeObjectAssign,
|
|
2972
|
-
filePath,
|
|
2973
|
-
() => "Explicitly pick allowed properties instead of spreading: const { name, email } = req.body; const safe = { name, email };"
|
|
2974
|
-
));
|
|
2975
|
-
}
|
|
2976
|
-
return matches;
|
|
2977
|
-
}
|
|
2978
|
-
};
|
|
2979
|
-
var unprotectedDownload = {
|
|
2980
|
-
id: "VC093",
|
|
2981
|
-
title: "File Download Without Path Validation",
|
|
2982
|
-
severity: "medium",
|
|
2983
|
-
category: "Authorization",
|
|
2984
|
-
description: "File download endpoints that accept user-controlled filenames without path validation allow directory traversal to read arbitrary files.",
|
|
2985
|
-
check(content, filePath) {
|
|
2986
|
-
if (!/(?:download|sendFile|send_file)/i.test(content)) return [];
|
|
2987
|
-
if (!/(?:\/api\/|routes?\/|controllers?\/|server\.|handler)/i.test(filePath)) return [];
|
|
2988
|
-
const matches = [];
|
|
2989
|
-
if (/(?:sendFile|download|send_file)\s*\([^)]*(?:req\.|params\.|query\.|body\.)/i.test(content)) {
|
|
2990
|
-
const hasValidation = /path\.resolve|path\.normalize|path\.join.*__dirname|realpath|includes\s*\(\s*["']\.\./i.test(content);
|
|
2991
|
-
if (!hasValidation) {
|
|
2992
|
-
matches.push(...findMatches(
|
|
2993
|
-
content,
|
|
2994
|
-
/(?:sendFile|download|send_file)\s*\(/gi,
|
|
2995
|
-
unprotectedDownload,
|
|
2996
|
-
filePath,
|
|
2997
|
-
() => "Validate file paths: const safePath = path.resolve(DOWNLOAD_DIR, filename); if (!safePath.startsWith(DOWNLOAD_DIR)) throw new Error('Invalid path');"
|
|
2998
|
-
));
|
|
2999
|
-
}
|
|
3000
|
-
}
|
|
3001
|
-
return matches;
|
|
3002
|
-
}
|
|
3003
|
-
};
|
|
3004
|
-
var commandInjection = {
|
|
3005
|
-
id: "VC094",
|
|
3006
|
-
title: "Potential Command Injection",
|
|
3007
|
-
severity: "critical",
|
|
3008
|
-
category: "Injection",
|
|
3009
|
-
description: "Passing user input to shell commands (exec, system, child_process) allows attackers to execute arbitrary system commands.",
|
|
3010
|
-
check(content, filePath) {
|
|
3011
|
-
if (filePath.includes("test") || filePath.includes("mock")) return [];
|
|
3012
|
-
const matches = [];
|
|
3013
|
-
const patterns = [
|
|
3014
|
-
// Node.js
|
|
3015
|
-
/(?:exec|execSync)\s*\(\s*(?:`[^`]*\$\{|["'][^"']*\+\s*(?:req\.|body\.|input|params|args|user))/gi,
|
|
3016
|
-
/child_process.*exec\s*\(\s*(?!["'`])/g,
|
|
3017
|
-
// Python
|
|
3018
|
-
/os\.system\s*\(\s*(?!["'`].*["'`]\s*\))/g,
|
|
3019
|
-
/subprocess\.(?:call|run|Popen)\s*\([^)]*shell\s*=\s*True/gi,
|
|
3020
|
-
// Ruby
|
|
3021
|
-
/system\s*\(\s*["'].*#\{/g
|
|
3022
|
-
];
|
|
3023
|
-
const hasSafe = /execFile|spawn|escapeshellarg|shlex\.quote|shellEscape/i.test(content);
|
|
3024
|
-
if (hasSafe) return [];
|
|
3025
|
-
for (const p of patterns) {
|
|
3026
|
-
matches.push(...findMatches(
|
|
3027
|
-
content,
|
|
3028
|
-
p,
|
|
3029
|
-
commandInjection,
|
|
3030
|
-
filePath,
|
|
3031
|
-
() => "Use execFile/spawn instead of exec (avoids shell). Never concatenate user input into shell commands. Use parameterized arguments."
|
|
3032
|
-
));
|
|
3033
|
-
}
|
|
3034
|
-
return matches;
|
|
3035
|
-
}
|
|
3036
|
-
};
|
|
3037
|
-
var corsLocalhost = {
|
|
3038
|
-
id: "VC095",
|
|
3039
|
-
title: "Hardcoded Localhost CORS Origin",
|
|
3040
|
-
severity: "medium",
|
|
3041
|
-
category: "Configuration",
|
|
3042
|
-
description: "Hardcoded localhost CORS origins in production code allow any local process to make authenticated requests to your API.",
|
|
3043
|
-
check(content, filePath) {
|
|
3044
|
-
if (filePath.includes("test") || filePath.includes("mock") || filePath.includes(".env")) return [];
|
|
3045
|
-
if (!/origin/i.test(content)) return [];
|
|
3046
|
-
return findMatches(
|
|
3047
|
-
content,
|
|
3048
|
-
/origin\s*[:=]\s*["'`]http:\/\/localhost/gi,
|
|
3049
|
-
corsLocalhost,
|
|
3050
|
-
filePath,
|
|
3051
|
-
() => "Use environment variables for CORS origins: origin: process.env.ALLOWED_ORIGIN. Remove localhost from production configs."
|
|
3052
|
-
);
|
|
3053
|
-
}
|
|
3054
|
-
};
|
|
3055
|
-
var insecureGRPC = {
|
|
3056
|
-
id: "VC096",
|
|
3057
|
-
title: "Unencrypted gRPC Channel",
|
|
3058
|
-
severity: "medium",
|
|
3059
|
-
category: "Configuration",
|
|
3060
|
-
description: "Using insecure gRPC channels transmits data including credentials in plaintext.",
|
|
3061
|
-
check(content, filePath) {
|
|
3062
|
-
if (!/grpc/i.test(content)) return [];
|
|
3063
|
-
return findMatches(
|
|
3064
|
-
content,
|
|
3065
|
-
/(?:insecure_channel|createInsecure|grpc\.Insecure)/gi,
|
|
3066
|
-
insecureGRPC,
|
|
3067
|
-
filePath,
|
|
3068
|
-
() => "Use encrypted gRPC channels: grpc.ssl_channel_credentials() or grpc.credentials.createSsl()."
|
|
3069
|
-
);
|
|
3070
|
-
}
|
|
3071
|
-
};
|
|
3072
|
-
var complianceMap = {
|
|
3073
|
-
VC001: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
3074
|
-
VC002: { owasp: "A05:2021", cwe: "CWE-200" },
|
|
3075
|
-
VC003: { owasp: "A01:2021", cwe: "CWE-862" },
|
|
3076
|
-
VC004: { owasp: "A01:2021", cwe: "CWE-284" },
|
|
3077
|
-
VC005: { owasp: "A08:2021", cwe: "CWE-345" },
|
|
3078
|
-
VC006: { owasp: "A03:2021", cwe: "CWE-89" },
|
|
3079
|
-
VC007: { owasp: "A03:2021", cwe: "CWE-79" },
|
|
3080
|
-
VC008: { owasp: "A04:2021", cwe: "CWE-770" },
|
|
3081
|
-
VC009: { owasp: "A05:2021", cwe: "CWE-942" },
|
|
3082
|
-
VC010: { owasp: "A01:2021", cwe: "CWE-602" },
|
|
3083
|
-
VC011: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
3084
|
-
VC012: { owasp: "A05:2021", cwe: "CWE-200" },
|
|
3085
|
-
VC013: { owasp: "A01:2021", cwe: "CWE-269" },
|
|
3086
|
-
VC014: { owasp: "A05:2021", cwe: "CWE-538" },
|
|
3087
|
-
VC015: { owasp: "A03:2021", cwe: "CWE-95" },
|
|
3088
|
-
VC016: { owasp: "A01:2021", cwe: "CWE-601" },
|
|
3089
|
-
VC017: { owasp: "A05:2021", cwe: "CWE-614" },
|
|
3090
|
-
VC018: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
3091
|
-
VC019: { owasp: "A05:2021", cwe: "CWE-693" },
|
|
3092
|
-
VC020: { owasp: "A05:2021", cwe: "CWE-1021" },
|
|
3093
|
-
VC021: { owasp: "A01:2021", cwe: "CWE-22" },
|
|
3094
|
-
VC022: { owasp: "A03:2021", cwe: "CWE-79" },
|
|
3095
|
-
VC023: { owasp: "A08:2021", cwe: "CWE-1321" },
|
|
3096
|
-
VC024: { owasp: "A04:2021", cwe: "CWE-770" },
|
|
3097
|
-
VC025: { owasp: "A03:2021", cwe: "CWE-22" },
|
|
3098
|
-
VC026: { owasp: "A05:2021", cwe: "CWE-693" },
|
|
3099
|
-
VC027: { owasp: "A05:2021", cwe: "CWE-693" },
|
|
3100
|
-
VC028: { owasp: "A07:2021", cwe: "CWE-20" },
|
|
3101
|
-
VC029: { owasp: "A08:2021", cwe: "CWE-20" },
|
|
3102
|
-
VC030: { owasp: "A08:2021", cwe: "CWE-502" },
|
|
3103
|
-
VC031: { owasp: "A02:2021", cwe: "CWE-321" },
|
|
3104
|
-
VC032: { owasp: "A05:2021", cwe: "CWE-319" },
|
|
3105
|
-
VC033: { owasp: "A05:2021", cwe: "CWE-215" },
|
|
3106
|
-
VC034: { owasp: "A02:2021", cwe: "CWE-338" },
|
|
3107
|
-
VC035: { owasp: "A01:2021", cwe: "CWE-601" },
|
|
3108
|
-
VC036: { owasp: "A04:2021", cwe: "CWE-755" },
|
|
3109
|
-
VC037: { owasp: "A09:2021", cwe: "CWE-209" },
|
|
3110
|
-
VC038: { owasp: "A04:2021", cwe: "CWE-434" },
|
|
3111
|
-
VC039: { owasp: "A06:2021", cwe: "CWE-1104" },
|
|
3112
|
-
VC040: { owasp: "A05:2021", cwe: "CWE-538" },
|
|
3113
|
-
VC041: { owasp: "A10:2021", cwe: "CWE-918" },
|
|
3114
|
-
VC042: { owasp: "A01:2021", cwe: "CWE-915" },
|
|
3115
|
-
VC043: { owasp: "A02:2021", cwe: "CWE-208" },
|
|
3116
|
-
VC044: { owasp: "A09:2021", cwe: "CWE-117" },
|
|
3117
|
-
VC045: { owasp: "A07:2021", cwe: "CWE-521" },
|
|
3118
|
-
VC046: { owasp: "A07:2021", cwe: "CWE-384" },
|
|
3119
|
-
VC047: { owasp: "A07:2021", cwe: "CWE-307" },
|
|
3120
|
-
VC048: { owasp: "A03:2021", cwe: "CWE-943" },
|
|
3121
|
-
VC049: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
3122
|
-
VC050: { owasp: "A02:2021", cwe: "CWE-319" },
|
|
3123
|
-
VC051: { owasp: "A05:2021", cwe: "CWE-200" },
|
|
3124
|
-
VC052: { owasp: "A04:2021", cwe: "CWE-770" },
|
|
3125
|
-
VC053: { owasp: "A05:2021", cwe: "CWE-798" },
|
|
3126
|
-
VC054: { owasp: "A07:2021", cwe: "CWE-922" },
|
|
3127
|
-
VC055: { owasp: "A05:2021", cwe: "CWE-540" },
|
|
3128
|
-
VC056: { owasp: "A05:2021", cwe: "CWE-1021" },
|
|
3129
|
-
VC057: { owasp: "A01:2021", cwe: "CWE-269" },
|
|
3130
|
-
VC058: { owasp: "A05:2021", cwe: "CWE-250" },
|
|
3131
|
-
VC059: { owasp: "A05:2021", cwe: "CWE-284" },
|
|
3132
|
-
VC060: { owasp: "A02:2021", cwe: "CWE-328" },
|
|
3133
|
-
VC061: { owasp: "A02:2021", cwe: "CWE-295" },
|
|
3134
|
-
VC062: { owasp: "A02:2021", cwe: "CWE-321" },
|
|
3135
|
-
VC063: { owasp: "A03:2021", cwe: "CWE-79" },
|
|
3136
|
-
VC064: { owasp: "A01:2021", cwe: "CWE-862" },
|
|
3137
|
-
VC065: { owasp: "A01:2021", cwe: "CWE-862" },
|
|
3138
|
-
VC066: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
3139
|
-
VC067: { owasp: "A01:2021", cwe: "CWE-601" },
|
|
3140
|
-
VC068: { owasp: "A07:2021", cwe: "CWE-922" },
|
|
3141
|
-
VC069: { owasp: "A02:2021", cwe: "CWE-295" },
|
|
3142
|
-
VC070: { owasp: "A05:2021", cwe: "CWE-489" },
|
|
3143
|
-
VC071: { owasp: "A05:2021", cwe: "CWE-215" },
|
|
3144
|
-
VC072: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
3145
|
-
VC073: { owasp: "A08:2021", cwe: "CWE-502" },
|
|
3146
|
-
VC074: { owasp: "A01:2021", cwe: "CWE-352" },
|
|
3147
|
-
VC075: { owasp: "A03:2021", cwe: "CWE-78" },
|
|
3148
|
-
VC076: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
3149
|
-
VC077: { owasp: "A05:2021", cwe: "CWE-942" },
|
|
3150
|
-
VC078: { owasp: "A05:2021", cwe: "CWE-250" },
|
|
3151
|
-
VC079: { owasp: "A02:2021", cwe: "CWE-327" },
|
|
3152
|
-
VC080: { owasp: "A04:2021", cwe: "CWE-1333" },
|
|
3153
|
-
VC081: { owasp: "A03:2021", cwe: "CWE-611" },
|
|
3154
|
-
VC082: { owasp: "A03:2021", cwe: "CWE-94" },
|
|
3155
|
-
VC083: { owasp: "A08:2021", cwe: "CWE-502" },
|
|
3156
|
-
VC084: { owasp: "A06:2021", cwe: "CWE-353" },
|
|
3157
|
-
VC085: { owasp: "A01:2021", cwe: "CWE-862" },
|
|
3158
|
-
VC086: { owasp: "A02:2021", cwe: "CWE-319" },
|
|
3159
|
-
VC087: { owasp: "A05:2021", cwe: "CWE-311" },
|
|
3160
|
-
VC088: { owasp: "A07:2021", cwe: "CWE-598" },
|
|
3161
|
-
VC089: { owasp: "A05:2021", cwe: "CWE-430" },
|
|
3162
|
-
VC090: { owasp: "A01:2021", cwe: "CWE-601" },
|
|
3163
|
-
VC091: { owasp: "A04:2021", cwe: "CWE-367" },
|
|
3164
|
-
VC092: { owasp: "A08:2021", cwe: "CWE-1321" },
|
|
3165
|
-
VC093: { owasp: "A01:2021", cwe: "CWE-22" },
|
|
3166
|
-
VC094: { owasp: "A03:2021", cwe: "CWE-78" },
|
|
3167
|
-
VC095: { owasp: "A05:2021", cwe: "CWE-942" },
|
|
3168
|
-
VC096: { owasp: "A02:2021", cwe: "CWE-319" },
|
|
3169
|
-
VC097: { owasp: "A09:2021", cwe: "CWE-532" },
|
|
3170
|
-
VC098: { owasp: "A04:2021", cwe: "CWE-400" },
|
|
3171
|
-
VC099: { owasp: "A04:2021", cwe: "CWE-401" },
|
|
3172
|
-
VC100: { owasp: "A04:2021", cwe: "CWE-400" },
|
|
3173
|
-
VC101: { owasp: "A04:2021", cwe: "CWE-400" },
|
|
3174
|
-
VC102: { owasp: "A04:2021", cwe: "CWE-400" },
|
|
3175
|
-
VC103: { owasp: "A04:2021", cwe: "CWE-710" },
|
|
3176
|
-
VC104: { owasp: "A04:2021", cwe: "CWE-390" },
|
|
3177
|
-
VC105: { owasp: "A04:2021", cwe: "CWE-710" },
|
|
3178
|
-
VC106: { owasp: "A04:2021", cwe: "CWE-710" },
|
|
3179
|
-
VC107: { owasp: "A02:2021", cwe: "CWE-311" },
|
|
3180
|
-
VC108: { owasp: "A01:2021", cwe: "CWE-284" },
|
|
3181
|
-
VC109: { owasp: "A01:2021", cwe: "CWE-284" },
|
|
3182
|
-
VC110: { owasp: "A09:2021", cwe: "CWE-778" },
|
|
3183
|
-
VC111: { owasp: "A05:2021", cwe: "CWE-16" },
|
|
3184
|
-
VC112: { owasp: "A06:2021", cwe: "CWE-1104" },
|
|
3185
|
-
VC113: { owasp: "A05:2021", cwe: "CWE-200" },
|
|
3186
|
-
VC114: { owasp: "A05:2021", cwe: "CWE-16" },
|
|
3187
|
-
VC115: { owasp: "A02:2021", cwe: "CWE-311" },
|
|
3188
|
-
VC116: { owasp: "A05:2021", cwe: "CWE-770" },
|
|
3189
|
-
VC117: { owasp: "A03:2021", cwe: "CWE-22" },
|
|
3190
|
-
VC118: { owasp: "A09:2021", cwe: "CWE-532" },
|
|
3191
|
-
VC119: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
3192
|
-
VC120: { owasp: "A07:2021", cwe: "CWE-352" },
|
|
3193
|
-
VC121: { owasp: "A06:2021", cwe: "CWE-1104" },
|
|
3194
|
-
VC122: { owasp: "A02:2021", cwe: "CWE-326" },
|
|
3195
|
-
VC123: { owasp: "A02:2021", cwe: "CWE-326" },
|
|
3196
|
-
VC124: { owasp: "A02:2021", cwe: "CWE-327" },
|
|
3197
|
-
VC125: { owasp: "A07:2021", cwe: "CWE-640" },
|
|
3198
|
-
VC126: { owasp: "A05:2021", cwe: "CWE-200" },
|
|
3199
|
-
VC127: { owasp: "A01:2021", cwe: "CWE-862" },
|
|
3200
|
-
VC128: { owasp: "A05:2021", cwe: "CWE-444" },
|
|
3201
|
-
VC129: { owasp: "A02:2021", cwe: "CWE-311" },
|
|
3202
|
-
VC130: { owasp: "A07:2021", cwe: "CWE-307" },
|
|
3203
|
-
VC131: { owasp: "A06:2021", cwe: "CWE-1104" }
|
|
3204
|
-
};
|
|
3205
|
-
var consoleLogProduction = {
|
|
3206
|
-
id: "VC097",
|
|
3207
|
-
title: "Console.log Left in Production Code",
|
|
3208
|
-
severity: "medium",
|
|
3209
|
-
category: "Performance",
|
|
3210
|
-
description: "console.log statements left in production code can leak sensitive data, slow down rendering, and clutter browser consoles.",
|
|
3211
|
-
check(content, filePath) {
|
|
3212
|
-
if (filePath.match(/test|spec|mock|__tests__|fixture|\.test\.|\.spec\./i)) return [];
|
|
3213
|
-
if (!/console\.log\s*\(/g.test(content)) return [];
|
|
3214
|
-
const lines = content.split("\n");
|
|
3215
|
-
const matches = [];
|
|
3216
|
-
for (let i = 0; i < lines.length; i++) {
|
|
3217
|
-
const line = lines[i].trim();
|
|
3218
|
-
if (/console\.log\s*\(/.test(line) && !line.startsWith("//") && !line.startsWith("*") && !/if\s*\(\s*(?:debug|process\.env)/i.test(lines[Math.max(0, i - 1)] + line)) {
|
|
3219
|
-
matches.push({ rule: "VC097", title: consoleLogProduction.title, severity: "medium", category: "Performance", file: filePath, line: i + 1, snippet: getSnippet(content, i + 1), fix: "Remove console.log or use a logger that can be disabled in production." });
|
|
3220
|
-
}
|
|
3221
|
-
}
|
|
3222
|
-
return matches.slice(0, 3);
|
|
3223
|
-
}
|
|
3224
|
-
};
|
|
3225
|
-
var syncFileOps = {
|
|
3226
|
-
id: "VC098",
|
|
3227
|
-
title: "Synchronous File Operations",
|
|
3228
|
-
severity: "medium",
|
|
3229
|
-
category: "Performance",
|
|
3230
|
-
description: "Synchronous file operations (readFileSync, writeFileSync) block the event loop, causing all other requests to wait.",
|
|
3231
|
-
check(content, filePath) {
|
|
3232
|
-
if (filePath.match(/test|spec|mock|__tests__|fixture|config|\.config\./i)) return [];
|
|
3233
|
-
return findMatches(
|
|
3234
|
-
content,
|
|
3235
|
-
/(?:readFileSync|writeFileSync|appendFileSync|mkdirSync|rmdirSync|statSync)\s*\(/g,
|
|
3236
|
-
syncFileOps,
|
|
3237
|
-
filePath,
|
|
3238
|
-
() => "Use async file operations (readFile, writeFile) to avoid blocking the event loop."
|
|
3239
|
-
);
|
|
3240
|
-
}
|
|
3241
|
-
};
|
|
3242
|
-
var eventListenerLeak = {
|
|
3243
|
-
id: "VC099",
|
|
3244
|
-
title: "Memory Leak: Event Listener Not Cleaned Up",
|
|
3245
|
-
severity: "high",
|
|
3246
|
-
category: "Performance",
|
|
3247
|
-
description: "Adding event listeners in React useEffect without a cleanup function causes memory leaks as listeners accumulate on re-renders.",
|
|
3248
|
-
check(content, filePath) {
|
|
3249
|
-
if (!filePath.match(/\.(jsx|tsx)$/)) return [];
|
|
3250
|
-
if (!/addEventListener/i.test(content)) return [];
|
|
3251
|
-
if (/removeEventListener/i.test(content)) return [];
|
|
3252
|
-
if (!/useEffect/i.test(content)) return [];
|
|
3253
|
-
return findMatches(
|
|
3254
|
-
content,
|
|
3255
|
-
/addEventListener\s*\(/g,
|
|
3256
|
-
eventListenerLeak,
|
|
3257
|
-
filePath,
|
|
3258
|
-
() => "Return a cleanup function from useEffect: useEffect(() => { window.addEventListener('resize', fn); return () => window.removeEventListener('resize', fn); }, []);"
|
|
3259
|
-
);
|
|
3260
|
-
}
|
|
3261
|
-
};
|
|
3262
|
-
var nPlusOneQuery = {
|
|
3263
|
-
id: "VC100",
|
|
3264
|
-
title: "N+1 Query Pattern Detected",
|
|
3265
|
-
severity: "medium",
|
|
3266
|
-
category: "Performance",
|
|
3267
|
-
description: "Database queries inside loops cause N+1 performance problems \u2014 one query per iteration instead of a single batch query.",
|
|
3268
|
-
check(content, filePath) {
|
|
3269
|
-
if (filePath.match(/test|spec|mock/i)) return [];
|
|
3270
|
-
const hasLoopWithQuery = /(?:for\s*\(|\.forEach\s*\(|\.map\s*\(|while\s*\()[^}]*(?:\.find\(|\.findOne\(|\.findById\(|\.query\(|\.execute\(|SELECT\s)/is.test(content);
|
|
3271
|
-
if (!hasLoopWithQuery) return [];
|
|
3272
|
-
return findMatches(
|
|
3273
|
-
content,
|
|
3274
|
-
/(?:for\s*\(|\.forEach\s*\(|\.map\s*\(|while\s*\()/g,
|
|
3275
|
-
nPlusOneQuery,
|
|
3276
|
-
filePath,
|
|
3277
|
-
() => "Fetch all data in a single query using WHERE IN, JOIN, or batch operations instead of querying per item in a loop."
|
|
3278
|
-
).slice(0, 2);
|
|
3279
|
-
}
|
|
3280
|
-
};
|
|
3281
|
-
var largeBundleImport = {
|
|
3282
|
-
id: "VC101",
|
|
3283
|
-
title: "Importing Entire Library (Large Bundle)",
|
|
3284
|
-
severity: "medium",
|
|
3285
|
-
category: "Performance",
|
|
3286
|
-
description: "Importing entire libraries like lodash or moment.js adds hundreds of KB to your bundle. Import only the functions you need.",
|
|
3287
|
-
check(content, filePath) {
|
|
3288
|
-
if (!filePath.match(/\.(jsx?|tsx?)$/)) return [];
|
|
3289
|
-
const matches = [];
|
|
3290
|
-
const patterns = [
|
|
3291
|
-
/import\s+_\s+from\s+['"]lodash['"]/g,
|
|
3292
|
-
/import\s+\*\s+as\s+_\s+from\s+['"]lodash['"]/g,
|
|
3293
|
-
/import\s+moment\s+from\s+['"]moment['"]/g,
|
|
3294
|
-
/const\s+_\s*=\s*require\s*\(\s*['"]lodash['"]\s*\)/g,
|
|
3295
|
-
/const\s+moment\s*=\s*require\s*\(\s*['"]moment['"]\s*\)/g
|
|
3296
|
-
];
|
|
3297
|
-
for (const p of patterns) {
|
|
3298
|
-
matches.push(...findMatches(
|
|
3299
|
-
content,
|
|
3300
|
-
p,
|
|
3301
|
-
largeBundleImport,
|
|
3302
|
-
filePath,
|
|
3303
|
-
() => "Import only what you need: import { debounce } from 'lodash/debounce'. Or switch to lighter alternatives like date-fns instead of moment."
|
|
3304
|
-
));
|
|
3305
|
-
}
|
|
3306
|
-
return matches;
|
|
3307
|
-
}
|
|
3308
|
-
};
|
|
3309
|
-
var blockingMainThread = {
|
|
3310
|
-
id: "VC102",
|
|
3311
|
-
title: "Blocking Main Thread with Heavy Computation",
|
|
3312
|
-
severity: "medium",
|
|
3313
|
-
category: "Performance",
|
|
3314
|
-
description: "Infinite loops or deeply nested iterations on the main thread freeze the UI and cause unresponsiveness.",
|
|
3315
|
-
check(content, filePath) {
|
|
3316
|
-
if (filePath.match(/worker|test|spec|mock/i)) return [];
|
|
3317
|
-
const matches = [];
|
|
3318
|
-
if (/while\s*\(\s*true\s*\)/g.test(content)) {
|
|
3319
|
-
matches.push(...findMatches(
|
|
3320
|
-
content,
|
|
3321
|
-
/while\s*\(\s*true\s*\)/g,
|
|
3322
|
-
blockingMainThread,
|
|
3323
|
-
filePath,
|
|
3324
|
-
() => "Avoid while(true) on the main thread. Use Web Workers for heavy computation or requestIdleCallback for non-urgent work."
|
|
3325
|
-
));
|
|
3326
|
-
}
|
|
3327
|
-
return matches;
|
|
3328
|
-
}
|
|
3329
|
-
};
|
|
3330
|
-
var todoLeftInCode = {
|
|
3331
|
-
id: "VC103",
|
|
3332
|
-
title: "TODO/FIXME Left in Code",
|
|
3333
|
-
severity: "low",
|
|
3334
|
-
category: "Code Quality",
|
|
3335
|
-
description: "TODO, FIXME, HACK, and XXX comments indicate unfinished work that should be resolved before shipping to production.",
|
|
3336
|
-
check(content, filePath) {
|
|
3337
|
-
if (filePath.match(/test|spec|mock|__tests__|fixture|node_modules/i)) return [];
|
|
3338
|
-
return findMatches(
|
|
3339
|
-
content,
|
|
3340
|
-
/\/\/\s*(?:TODO|FIXME|HACK|XXX)\b/gi,
|
|
3341
|
-
todoLeftInCode,
|
|
3342
|
-
filePath,
|
|
3343
|
-
() => "Resolve TODO/FIXME comments before shipping. If it's intentional tech debt, track it in your issue tracker instead."
|
|
3344
|
-
).slice(0, 5);
|
|
3345
|
-
}
|
|
3346
|
-
};
|
|
3347
|
-
var emptyCatchBlock = {
|
|
3348
|
-
id: "VC104",
|
|
3349
|
-
title: "Empty Catch Block",
|
|
3350
|
-
severity: "medium",
|
|
3351
|
-
category: "Code Quality",
|
|
3352
|
-
description: "Empty catch blocks silently swallow errors, making bugs impossible to diagnose. At minimum, log the error.",
|
|
3353
|
-
check(content, filePath) {
|
|
3354
|
-
if (filePath.match(/test|spec|mock/i)) return [];
|
|
3355
|
-
return findMatches(
|
|
3356
|
-
content,
|
|
3357
|
-
/catch\s*(?:\([^)]*\))?\s*\{\s*\}/g,
|
|
3358
|
-
emptyCatchBlock,
|
|
3359
|
-
filePath,
|
|
3360
|
-
() => "Handle errors in catch blocks: catch(err) { console.error('Operation failed:', err); } or re-throw if appropriate."
|
|
3361
|
-
);
|
|
3362
|
-
}
|
|
3363
|
-
};
|
|
3364
|
-
var callbackHell = {
|
|
3365
|
-
id: "VC105",
|
|
3366
|
-
title: "Deeply Nested Callbacks (Promise Chain)",
|
|
3367
|
-
severity: "medium",
|
|
3368
|
-
category: "Code Quality",
|
|
3369
|
-
description: "Long .then() chains or deeply nested callbacks are hard to read, debug, and maintain. Refactor to async/await.",
|
|
3370
|
-
check(content, filePath) {
|
|
3371
|
-
if (filePath.match(/test|spec|mock/i)) return [];
|
|
3372
|
-
return findMatches(
|
|
3373
|
-
content,
|
|
3374
|
-
/\.then\s*\([^)]*\)\s*\.then\s*\([^)]*\)\s*\.then/g,
|
|
3375
|
-
callbackHell,
|
|
3376
|
-
filePath,
|
|
3377
|
-
() => "Refactor .then() chains to async/await for cleaner, more readable code."
|
|
3378
|
-
);
|
|
3379
|
-
}
|
|
3380
|
-
};
|
|
3381
|
-
var magicNumbers = {
|
|
3382
|
-
id: "VC106",
|
|
3383
|
-
title: "Magic Numbers in Code",
|
|
3384
|
-
severity: "low",
|
|
3385
|
-
category: "Code Quality",
|
|
3386
|
-
description: "Unnamed numeric constants in conditions or calculations make code hard to understand. Extract them into named constants.",
|
|
3387
|
-
check(content, filePath) {
|
|
3388
|
-
if (filePath.match(/test|spec|mock|config|\.config\.|constant|enum|migration/i)) return [];
|
|
3389
|
-
if (!filePath.match(/\.(jsx?|tsx?)$/)) return [];
|
|
3390
|
-
const matches = [];
|
|
3391
|
-
const lines = content.split("\n");
|
|
3392
|
-
for (let i = 0; i < lines.length; i++) {
|
|
3393
|
-
const line = lines[i].trim();
|
|
3394
|
-
if (line.startsWith("//") || line.startsWith("*")) continue;
|
|
3395
|
-
if (/(?:===|!==|>=?|<=?)\s*\d{3,}/.test(line) || /(?:setTimeout|setInterval)\s*\([^,]+,\s*\d{4,}/.test(line)) {
|
|
3396
|
-
matches.push({ rule: "VC106", title: magicNumbers.title, severity: "low", category: "Code Quality", file: filePath, line: i + 1, snippet: getSnippet(content, i + 1), fix: "Extract magic numbers into named constants: const MAX_RETRIES = 3; const TIMEOUT_MS = 5000;" });
|
|
3397
|
-
}
|
|
3398
|
-
}
|
|
3399
|
-
return matches.slice(0, 3);
|
|
3400
|
-
}
|
|
3401
|
-
};
|
|
3402
|
-
var s3BucketNoEncryption = {
|
|
3403
|
-
id: "VC107",
|
|
3404
|
-
title: "S3 Bucket Without Encryption",
|
|
3405
|
-
severity: "high",
|
|
3406
|
-
category: "Infrastructure",
|
|
3407
|
-
description: "AWS S3 buckets without server-side encryption leave data at rest unprotected. Enable encryption to protect sensitive data.",
|
|
3408
|
-
check(content, filePath) {
|
|
3409
|
-
if (!filePath.match(/\.tf$/)) return [];
|
|
3410
|
-
if (!/resource\s+"aws_s3_bucket"/.test(content)) return [];
|
|
3411
|
-
const matches = [];
|
|
3412
|
-
const bucketPattern = /resource\s+"aws_s3_bucket"\s+"(\w+)"/g;
|
|
3413
|
-
let m;
|
|
3414
|
-
while ((m = bucketPattern.exec(content)) !== null) {
|
|
3415
|
-
if (isCommentLine(content, m.index)) continue;
|
|
3416
|
-
const blockEnd = Math.min(m.index + 2e3, content.length);
|
|
3417
|
-
const blockContent = content.substring(m.index, blockEnd);
|
|
3418
|
-
if (!/server_side_encryption/.test(blockContent)) {
|
|
3419
|
-
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3420
|
-
matches.push({
|
|
3421
|
-
rule: "VC107",
|
|
3422
|
-
title: s3BucketNoEncryption.title,
|
|
3423
|
-
severity: "high",
|
|
3424
|
-
category: "Infrastructure",
|
|
3425
|
-
file: filePath,
|
|
3426
|
-
line: lineNum,
|
|
3427
|
-
snippet: getSnippet(content, lineNum),
|
|
3428
|
-
fix: "Enable S3 bucket encryption: server_side_encryption_configuration { rule { apply_server_side_encryption_by_default { sse_algorithm = 'aws:kms' } } }"
|
|
3429
|
-
});
|
|
3430
|
-
}
|
|
3431
|
-
}
|
|
3432
|
-
return matches;
|
|
3433
|
-
}
|
|
3434
|
-
};
|
|
3435
|
-
var securityGroupAllInbound = {
|
|
3436
|
-
id: "VC108",
|
|
3437
|
-
title: "Security Group Allows All Inbound",
|
|
3438
|
-
severity: "critical",
|
|
3439
|
-
category: "Infrastructure",
|
|
3440
|
-
description: "Security groups allowing all inbound traffic (0.0.0.0/0 on all ports) expose resources to the entire internet.",
|
|
3441
|
-
check(content, filePath) {
|
|
3442
|
-
if (!filePath.match(/\.(tf|json|yaml|yml)$/)) return [];
|
|
3443
|
-
const matches = [];
|
|
3444
|
-
if (filePath.match(/\.tf$/)) {
|
|
3445
|
-
const ingressPattern = /ingress\s*\{[^}]*cidr_blocks\s*=\s*\[\s*"0\.0\.0\.0\/0"\s*\][^}]*\}/gs;
|
|
3446
|
-
let m;
|
|
3447
|
-
while ((m = ingressPattern.exec(content)) !== null) {
|
|
3448
|
-
if (isCommentLine(content, m.index)) continue;
|
|
3449
|
-
if (/from_port\s*=\s*0/.test(m[0]) || !/from_port/.test(m[0])) {
|
|
3450
|
-
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3451
|
-
matches.push({
|
|
3452
|
-
rule: "VC108",
|
|
3453
|
-
title: securityGroupAllInbound.title,
|
|
3454
|
-
severity: "critical",
|
|
3455
|
-
category: "Infrastructure",
|
|
3456
|
-
file: filePath,
|
|
3457
|
-
line: lineNum,
|
|
3458
|
-
snippet: getSnippet(content, lineNum),
|
|
3459
|
-
fix: "Restrict security group ingress to specific IP ranges and ports."
|
|
3460
|
-
});
|
|
3461
|
-
}
|
|
3462
|
-
}
|
|
3463
|
-
}
|
|
3464
|
-
if (filePath.match(/\.(json|yaml|yml)$/)) {
|
|
3465
|
-
const cfnPattern = /AWS::EC2::SecurityGroup/g;
|
|
3466
|
-
if (cfnPattern.test(content) && /CidrIp\s*:\s*["']?0\.0\.0\.0\/0/.test(content)) {
|
|
3467
|
-
matches.push(...findMatches(
|
|
3468
|
-
content,
|
|
3469
|
-
/CidrIp\s*:\s*["']?0\.0\.0\.0\/0/g,
|
|
3470
|
-
securityGroupAllInbound,
|
|
3471
|
-
filePath,
|
|
3472
|
-
() => "Restrict security group ingress to specific IP ranges and ports."
|
|
3473
|
-
));
|
|
3474
|
-
}
|
|
3475
|
-
}
|
|
3476
|
-
return matches;
|
|
3477
|
-
}
|
|
3478
|
-
};
|
|
3479
|
-
var rdsPubliclyAccessible = {
|
|
3480
|
-
id: "VC109",
|
|
3481
|
-
title: "RDS Instance Publicly Accessible",
|
|
3482
|
-
severity: "critical",
|
|
3483
|
-
category: "Infrastructure",
|
|
3484
|
-
description: "RDS instances with publicly_accessible = true are exposed to the internet, risking unauthorized database access.",
|
|
3485
|
-
check(content, filePath) {
|
|
3486
|
-
if (!filePath.match(/\.tf$/)) return [];
|
|
3487
|
-
if (!/resource\s+"aws_db_instance"/.test(content)) return [];
|
|
3488
|
-
return findMatches(
|
|
3489
|
-
content,
|
|
3490
|
-
/publicly_accessible\s*=\s*true/g,
|
|
3491
|
-
rdsPubliclyAccessible,
|
|
3492
|
-
filePath,
|
|
3493
|
-
() => "Set publicly_accessible = false. Access RDS through VPC private subnets."
|
|
3494
|
-
);
|
|
3495
|
-
}
|
|
3496
|
-
};
|
|
3497
|
-
var missingCloudTrail = {
|
|
3498
|
-
id: "VC110",
|
|
3499
|
-
title: "Missing CloudTrail Logging",
|
|
3500
|
-
severity: "high",
|
|
3501
|
-
category: "Infrastructure",
|
|
3502
|
-
description: "AWS environments without CloudTrail lack audit logging of API calls, making it difficult to detect unauthorized activity.",
|
|
3503
|
-
check(content, filePath) {
|
|
3504
|
-
if (!filePath.match(/\.tf$/)) return [];
|
|
3505
|
-
if (!/provider\s+"aws"/.test(content)) return [];
|
|
3506
|
-
if (/aws_cloudtrail/.test(content)) return [];
|
|
3507
|
-
const lineNum = content.substring(0, content.search(/provider\s+"aws"/)).split("\n").length;
|
|
3508
|
-
return [{
|
|
3509
|
-
rule: "VC110",
|
|
3510
|
-
title: missingCloudTrail.title,
|
|
3511
|
-
severity: "high",
|
|
3512
|
-
category: "Infrastructure",
|
|
3513
|
-
file: filePath,
|
|
3514
|
-
line: lineNum,
|
|
3515
|
-
snippet: getSnippet(content, lineNum),
|
|
3516
|
-
fix: "Enable CloudTrail for audit logging of all AWS API calls."
|
|
3517
|
-
}];
|
|
3518
|
-
}
|
|
3519
|
-
};
|
|
3520
|
-
var lambdaWithoutVPC = {
|
|
3521
|
-
id: "VC111",
|
|
3522
|
-
title: "Lambda Without VPC",
|
|
3523
|
-
severity: "medium",
|
|
3524
|
-
category: "Infrastructure",
|
|
3525
|
-
description: "Lambda functions not placed in a VPC lack network isolation and cannot access VPC-only resources securely.",
|
|
3526
|
-
check(content, filePath) {
|
|
3527
|
-
if (!filePath.match(/\.tf$/)) return [];
|
|
3528
|
-
if (!/resource\s+"aws_lambda_function"/.test(content)) return [];
|
|
3529
|
-
const matches = [];
|
|
3530
|
-
const lambdaPattern = /resource\s+"aws_lambda_function"\s+"(\w+)"/g;
|
|
3531
|
-
let m;
|
|
3532
|
-
while ((m = lambdaPattern.exec(content)) !== null) {
|
|
3533
|
-
if (isCommentLine(content, m.index)) continue;
|
|
3534
|
-
const blockEnd = Math.min(m.index + 2e3, content.length);
|
|
3535
|
-
const blockContent = content.substring(m.index, blockEnd);
|
|
3536
|
-
if (!/vpc_config\s*\{/.test(blockContent)) {
|
|
3537
|
-
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3538
|
-
matches.push({
|
|
3539
|
-
rule: "VC111",
|
|
3540
|
-
title: lambdaWithoutVPC.title,
|
|
3541
|
-
severity: "medium",
|
|
3542
|
-
category: "Infrastructure",
|
|
3543
|
-
file: filePath,
|
|
3544
|
-
line: lineNum,
|
|
3545
|
-
snippet: getSnippet(content, lineNum),
|
|
3546
|
-
fix: "Place Lambda functions in a VPC for network isolation."
|
|
3547
|
-
});
|
|
3548
|
-
}
|
|
3549
|
-
}
|
|
3550
|
-
return matches;
|
|
3551
|
-
}
|
|
3552
|
-
};
|
|
3553
|
-
var dockerLatestTag = {
|
|
3554
|
-
id: "VC112",
|
|
3555
|
-
title: "Docker Image Using Latest Tag",
|
|
3556
|
-
severity: "high",
|
|
3557
|
-
category: "Configuration",
|
|
3558
|
-
description: "Using :latest or no tag in Docker FROM directives leads to non-reproducible builds and potential security regressions.",
|
|
3559
|
-
check(content, filePath) {
|
|
3560
|
-
if (!filePath.match(/Dockerfile$/i)) return [];
|
|
3561
|
-
const matches = [];
|
|
3562
|
-
const fromPattern = /^FROM\s+(?!scratch)(\S+?)(?:\s+AS\s+\S+)?\s*$/gm;
|
|
3563
|
-
let m;
|
|
3564
|
-
while ((m = fromPattern.exec(content)) !== null) {
|
|
3565
|
-
const image = m[1];
|
|
3566
|
-
if (image.endsWith(":latest") || !image.includes(":") && !image.startsWith("$")) {
|
|
3567
|
-
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3568
|
-
matches.push({
|
|
3569
|
-
rule: "VC112",
|
|
3570
|
-
title: dockerLatestTag.title,
|
|
3571
|
-
severity: "high",
|
|
3572
|
-
category: "Configuration",
|
|
3573
|
-
file: filePath,
|
|
3574
|
-
line: lineNum,
|
|
3575
|
-
snippet: getSnippet(content, lineNum),
|
|
3576
|
-
fix: "Pin Docker image versions: FROM node:20-alpine instead of FROM node:latest"
|
|
3577
|
-
});
|
|
3578
|
-
}
|
|
3579
|
-
}
|
|
3580
|
-
return matches;
|
|
3581
|
-
}
|
|
3582
|
-
};
|
|
3583
|
-
var dockerCopySensitive = {
|
|
3584
|
-
id: "VC113",
|
|
3585
|
-
title: "Docker COPY With Sensitive Files",
|
|
3586
|
-
severity: "high",
|
|
3587
|
-
category: "Configuration",
|
|
3588
|
-
description: "Using COPY . . or ADD . . without a .dockerignore can leak .env files, .git history, and other sensitive data into the Docker image.",
|
|
3589
|
-
check(content, filePath) {
|
|
3590
|
-
if (!filePath.match(/Dockerfile$/i)) return [];
|
|
3591
|
-
if (!/(?:COPY|ADD)\s+\.\s+\./.test(content)) return [];
|
|
3592
|
-
return findMatches(
|
|
3593
|
-
content,
|
|
3594
|
-
/(?:COPY|ADD)\s+\.\s+\./g,
|
|
3595
|
-
dockerCopySensitive,
|
|
3596
|
-
filePath,
|
|
3597
|
-
() => "Use .dockerignore to exclude .env, .git, node_modules, and sensitive files from the Docker build context."
|
|
3598
|
-
);
|
|
3599
|
-
}
|
|
3600
|
-
};
|
|
3601
|
-
var dockerTooManyPorts = {
|
|
3602
|
-
id: "VC114",
|
|
3603
|
-
title: "Docker Exposing Too Many Ports",
|
|
3604
|
-
severity: "medium",
|
|
3605
|
-
category: "Configuration",
|
|
3606
|
-
description: "Exposing many ports or wide port ranges increases the attack surface of a container.",
|
|
3607
|
-
check(content, filePath) {
|
|
3608
|
-
if (!filePath.match(/Dockerfile$/i)) return [];
|
|
3609
|
-
const matches = [];
|
|
3610
|
-
const rangePattern = /^EXPOSE\s+\d+-\d+/gm;
|
|
3611
|
-
matches.push(...findMatches(
|
|
3612
|
-
content,
|
|
3613
|
-
rangePattern,
|
|
3614
|
-
dockerTooManyPorts,
|
|
3615
|
-
filePath,
|
|
3616
|
-
() => "Only expose necessary ports. Each EXPOSE should have a clear purpose."
|
|
3617
|
-
));
|
|
3618
|
-
const exposeLines = content.split("\n").filter((l) => /^\s*EXPOSE\s+/.test(l));
|
|
3619
|
-
if (exposeLines.length > 3) {
|
|
3620
|
-
const firstExpose = content.search(/^\s*EXPOSE\s+/m);
|
|
3621
|
-
if (firstExpose >= 0) {
|
|
3622
|
-
const lineNum = content.substring(0, firstExpose).split("\n").length;
|
|
3623
|
-
matches.push({
|
|
3624
|
-
rule: "VC114",
|
|
3625
|
-
title: dockerTooManyPorts.title,
|
|
3626
|
-
severity: "medium",
|
|
3627
|
-
category: "Configuration",
|
|
3628
|
-
file: filePath,
|
|
3629
|
-
line: lineNum,
|
|
3630
|
-
snippet: getSnippet(content, lineNum),
|
|
3631
|
-
fix: "Only expose necessary ports. Each EXPOSE should have a clear purpose."
|
|
3632
|
-
});
|
|
3633
|
-
}
|
|
3634
|
-
}
|
|
3635
|
-
return matches;
|
|
3636
|
-
}
|
|
3637
|
-
};
|
|
3638
|
-
var k8sSecretNotEncrypted = {
|
|
3639
|
-
id: "VC115",
|
|
3640
|
-
title: "Kubernetes Secret Not Encrypted",
|
|
3641
|
-
severity: "high",
|
|
3642
|
-
category: "Infrastructure",
|
|
3643
|
-
description: "Kubernetes Secrets stored as plain base64 in manifests are not encrypted and can be trivially decoded. Use sealed-secrets or external-secrets.",
|
|
3644
|
-
check(content, filePath) {
|
|
3645
|
-
if (!filePath.match(/\.(yaml|yml)$/)) return [];
|
|
3646
|
-
if (!/kind\s*:\s*Secret/i.test(content)) return [];
|
|
3647
|
-
if (/sealedsecrets\.bitnami\.com|external-secrets\.io/i.test(content)) return [];
|
|
3648
|
-
return findMatches(
|
|
3649
|
-
content,
|
|
3650
|
-
/kind\s*:\s*Secret/g,
|
|
3651
|
-
k8sSecretNotEncrypted,
|
|
3652
|
-
filePath,
|
|
3653
|
-
() => "Use sealed-secrets or external-secrets-operator. Never store plain secrets in manifests."
|
|
3654
|
-
);
|
|
3655
|
-
}
|
|
3656
|
-
};
|
|
3657
|
-
var k8sNoResourceLimits = {
|
|
3658
|
-
id: "VC116",
|
|
3659
|
-
title: "Kubernetes Pod Without Resource Limits",
|
|
3660
|
-
severity: "medium",
|
|
3661
|
-
category: "Infrastructure",
|
|
3662
|
-
description: "Pods or deployments without resource limits can consume excessive CPU/memory, causing cluster instability.",
|
|
3663
|
-
check(content, filePath) {
|
|
3664
|
-
if (!filePath.match(/\.(yaml|yml)$/)) return [];
|
|
3665
|
-
if (!/kind\s*:\s*(?:Pod|Deployment|StatefulSet|DaemonSet|Job|CronJob)/i.test(content)) return [];
|
|
3666
|
-
if (/resources\s*:\s*\n\s+limits\s*:/m.test(content)) return [];
|
|
3667
|
-
if (/resources\s*:.*limits/i.test(content)) return [];
|
|
3668
|
-
const kindMatch = content.match(/kind\s*:\s*(?:Pod|Deployment|StatefulSet|DaemonSet|Job|CronJob)/i);
|
|
3669
|
-
if (!kindMatch) return [];
|
|
3670
|
-
const lineNum = content.substring(0, kindMatch.index).split("\n").length;
|
|
3671
|
-
return [{
|
|
3672
|
-
rule: "VC116",
|
|
3673
|
-
title: k8sNoResourceLimits.title,
|
|
3674
|
-
severity: "medium",
|
|
3675
|
-
category: "Infrastructure",
|
|
3676
|
-
file: filePath,
|
|
3677
|
-
line: lineNum,
|
|
3678
|
-
snippet: getSnippet(content, lineNum),
|
|
3679
|
-
fix: "Set resource limits to prevent pods from consuming excessive CPU/memory."
|
|
3680
|
-
}];
|
|
3681
|
-
}
|
|
3682
|
-
};
|
|
3683
|
-
var pathTraversal = {
|
|
3684
|
-
id: "VC117",
|
|
3685
|
-
title: "Path Traversal Vulnerability",
|
|
3686
|
-
severity: "critical",
|
|
3687
|
-
category: "Injection",
|
|
3688
|
-
description: "User input is used to construct file paths without sanitization, allowing attackers to read/write arbitrary files (e.g., ../../etc/passwd).",
|
|
3689
|
-
check(content, filePath) {
|
|
3690
|
-
if (isTestFile(filePath)) return [];
|
|
3691
|
-
if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|php|java)$/)) return [];
|
|
3692
|
-
const findings = [];
|
|
3693
|
-
const patterns = [
|
|
3694
|
-
/(?:readFile|readFileSync|createReadStream|writeFile|writeFileSync|appendFile|unlink|unlinkSync|access|stat|statSync|existsSync)\s*\(\s*(?:req\.|request\.|ctx\.|params\.|query\.)/gi,
|
|
3695
|
-
/(?:path\.join|path\.resolve)\s*\([^)]*(?:req\.|request\.|ctx\.|params\.|query\.)[^)]*\)/gi,
|
|
3696
|
-
/(?:res\.sendFile|res\.download)\s*\([^)]*(?:req\.|request\.)/gi,
|
|
3697
|
-
/open\s*\(\s*(?:request\.|params\[|args\.)/gi
|
|
3698
|
-
];
|
|
3699
|
-
for (const pat of patterns) {
|
|
3700
|
-
let m;
|
|
3701
|
-
while ((m = pat.exec(content)) !== null) {
|
|
3702
|
-
if (isCommentLine(content, m.index)) continue;
|
|
3703
|
-
const surrounding = content.substring(Math.max(0, m.index - 200), m.index + 200);
|
|
3704
|
-
if (/path\.normalize|sanitize|\.replace\(\s*['"]\.\./i.test(surrounding)) continue;
|
|
3705
|
-
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3706
|
-
findings.push({
|
|
3707
|
-
rule: "VC117",
|
|
3708
|
-
title: pathTraversal.title,
|
|
3709
|
-
severity: "critical",
|
|
3710
|
-
category: "Injection",
|
|
3711
|
-
file: filePath,
|
|
3712
|
-
line: lineNum,
|
|
3713
|
-
snippet: getSnippet(content, lineNum),
|
|
3714
|
-
fix: "Sanitize file paths: use path.normalize(), reject paths containing '..', and validate against an allowed base directory."
|
|
3715
|
-
});
|
|
3716
|
-
}
|
|
3717
|
-
}
|
|
3718
|
-
return findings;
|
|
3719
|
-
}
|
|
3720
|
-
};
|
|
3721
|
-
var piiInLogs = {
|
|
3722
|
-
id: "VC118",
|
|
3723
|
-
title: "Personally Identifiable Information in Logs",
|
|
3724
|
-
severity: "high",
|
|
3725
|
-
category: "Information Leakage",
|
|
3726
|
-
description: "Logging statements that include email addresses, passwords, SSNs, credit card numbers, or other PII can leak sensitive data to log aggregation systems.",
|
|
3727
|
-
check(content, filePath) {
|
|
3728
|
-
if (isTestFile(filePath)) return [];
|
|
3729
|
-
if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];
|
|
3730
|
-
const findings = [];
|
|
3731
|
-
const logPattern = /(?:console\.(?:log|info|warn|error|debug)|logger\.(?:info|warn|error|debug|log)|log\.(?:info|warn|error|debug)|logging\.(?:info|warn|error|debug))\s*\([^)]*(?:password|passwd|secret|ssn|social.?security|credit.?card|cardNumber|cvv|token|bearer|authorization)\b/gi;
|
|
3732
|
-
let m;
|
|
3733
|
-
while ((m = logPattern.exec(content)) !== null) {
|
|
3734
|
-
if (isCommentLine(content, m.index)) continue;
|
|
3735
|
-
if (isInsideFixMessage(content, m.index)) continue;
|
|
3736
|
-
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3737
|
-
findings.push({
|
|
3738
|
-
rule: "VC118",
|
|
3739
|
-
title: piiInLogs.title,
|
|
3740
|
-
severity: "high",
|
|
3741
|
-
category: "Information Leakage",
|
|
3742
|
-
file: filePath,
|
|
3743
|
-
line: lineNum,
|
|
3744
|
-
snippet: getSnippet(content, lineNum),
|
|
3745
|
-
fix: "Never log sensitive data. Redact or mask PII before logging: log('user login', { email: maskEmail(email) })."
|
|
3746
|
-
});
|
|
3747
|
-
}
|
|
3748
|
-
return findings;
|
|
3749
|
-
}
|
|
3750
|
-
};
|
|
3751
|
-
var hardcodedOAuthSecret = {
|
|
3752
|
-
id: "VC119",
|
|
3753
|
-
title: "Hardcoded OAuth Client Secret",
|
|
3754
|
-
severity: "critical",
|
|
3755
|
-
category: "Secrets",
|
|
3756
|
-
description: "OAuth client secrets hardcoded in source code can be extracted and used to impersonate your application.",
|
|
3757
|
-
check(content, filePath) {
|
|
3758
|
-
if (isTestFile(filePath)) return [];
|
|
3759
|
-
if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php|env|json|yaml|yml)$/)) return [];
|
|
3760
|
-
if (/package-lock|yarn\.lock|pnpm-lock|composer\.lock/i.test(filePath)) return [];
|
|
3761
|
-
const findings = [];
|
|
3762
|
-
const patterns = [
|
|
3763
|
-
/client[_-]?secret\s*[:=]\s*["'][a-zA-Z0-9_\-]{20,}["']/gi,
|
|
3764
|
-
/GOOGLE_CLIENT_SECRET\s*[:=]\s*["'][^"']{10,}["']/gi,
|
|
3765
|
-
/GITHUB_CLIENT_SECRET\s*[:=]\s*["'][^"']{10,}["']/gi,
|
|
3766
|
-
/FACEBOOK_APP_SECRET\s*[:=]\s*["'][^"']{10,}["']/gi,
|
|
3767
|
-
/AUTH0_CLIENT_SECRET\s*[:=]\s*["'][^"']{10,}["']/gi
|
|
3768
|
-
];
|
|
3769
|
-
for (const pat of patterns) {
|
|
3770
|
-
let m;
|
|
3771
|
-
while ((m = pat.exec(content)) !== null) {
|
|
3772
|
-
if (isCommentLine(content, m.index)) continue;
|
|
3773
|
-
if (/process\.env|os\.environ|ENV\[|getenv|\$\{|<your|CHANGE_ME|xxx|placeholder/i.test(m[0])) continue;
|
|
3774
|
-
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3775
|
-
findings.push({
|
|
3776
|
-
rule: "VC119",
|
|
3777
|
-
title: hardcodedOAuthSecret.title,
|
|
3778
|
-
severity: "critical",
|
|
3779
|
-
category: "Secrets",
|
|
3780
|
-
file: filePath,
|
|
3781
|
-
line: lineNum,
|
|
3782
|
-
snippet: getSnippet(content, lineNum),
|
|
3783
|
-
fix: "Move OAuth client secrets to environment variables. Never commit secrets to source control."
|
|
3784
|
-
});
|
|
3785
|
-
}
|
|
3786
|
-
}
|
|
3787
|
-
return findings;
|
|
3788
|
-
}
|
|
3789
|
-
};
|
|
3790
|
-
var missingOAuthState = {
|
|
3791
|
-
id: "VC120",
|
|
3792
|
-
title: "OAuth Flow Missing State Parameter",
|
|
3793
|
-
severity: "high",
|
|
3794
|
-
category: "Authentication",
|
|
3795
|
-
description: "OAuth authorization requests without a state parameter are vulnerable to CSRF attacks, allowing attackers to link their account to a victim's session.",
|
|
3796
|
-
check(content, filePath) {
|
|
3797
|
-
if (isTestFile(filePath)) return [];
|
|
3798
|
-
if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];
|
|
3799
|
-
const findings = [];
|
|
3800
|
-
const oauthUrlPattern = /(?:authorize\?|\/oauth\/authorize|\/auth\?|authorization_endpoint)[^}]*(?:client_id|response_type)/gi;
|
|
3801
|
-
let m;
|
|
3802
|
-
while ((m = oauthUrlPattern.exec(content)) !== null) {
|
|
3803
|
-
if (isCommentLine(content, m.index)) continue;
|
|
3804
|
-
const surrounding = content.substring(m.index, Math.min(content.length, m.index + 500));
|
|
3805
|
-
if (/state\s*[=:]/i.test(surrounding)) continue;
|
|
3806
|
-
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3807
|
-
findings.push({
|
|
3808
|
-
rule: "VC120",
|
|
3809
|
-
title: missingOAuthState.title,
|
|
3810
|
-
severity: "high",
|
|
3811
|
-
category: "Authentication",
|
|
3812
|
-
file: filePath,
|
|
3813
|
-
line: lineNum,
|
|
3814
|
-
snippet: getSnippet(content, lineNum),
|
|
3815
|
-
fix: "Always include a cryptographically random 'state' parameter in OAuth authorization requests and validate it on callback."
|
|
3816
|
-
});
|
|
3817
|
-
}
|
|
3818
|
-
return findings;
|
|
3819
|
-
}
|
|
3820
|
-
};
|
|
3821
|
-
var unpinnedGitHubAction = {
|
|
3822
|
-
id: "VC121",
|
|
3823
|
-
title: "Unpinned GitHub Actions Version",
|
|
3824
|
-
severity: "high",
|
|
3825
|
-
category: "Supply Chain",
|
|
3826
|
-
description: "GitHub Actions using branch references (@main, @master) instead of commit SHAs can be compromised via supply-chain attacks.",
|
|
3827
|
-
check(content, filePath) {
|
|
3828
|
-
if (!filePath.match(/\.github\/workflows\/.*\.(yml|yaml)$/)) return [];
|
|
3829
|
-
const findings = [];
|
|
3830
|
-
const usesPattern = /uses\s*:\s*[\w\-]+\/[\w\-]+@(main|master|dev|develop|latest)\b/gi;
|
|
3831
|
-
let m;
|
|
3832
|
-
while ((m = usesPattern.exec(content)) !== null) {
|
|
3833
|
-
if (isCommentLine(content, m.index)) continue;
|
|
3834
|
-
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3835
|
-
findings.push({
|
|
3836
|
-
rule: "VC121",
|
|
3837
|
-
title: unpinnedGitHubAction.title,
|
|
3838
|
-
severity: "high",
|
|
3839
|
-
category: "Supply Chain",
|
|
3840
|
-
file: filePath,
|
|
3841
|
-
line: lineNum,
|
|
3842
|
-
snippet: getSnippet(content, lineNum),
|
|
3843
|
-
fix: "Pin GitHub Actions to a specific commit SHA instead of a branch name: uses: owner/action@<full-sha>"
|
|
3844
|
-
});
|
|
3845
|
-
}
|
|
3846
|
-
return findings;
|
|
3847
|
-
}
|
|
3848
|
-
};
|
|
3849
|
-
var deprecatedTLS = {
|
|
3850
|
-
id: "VC122",
|
|
3851
|
-
title: "Deprecated TLS Version Configured",
|
|
3852
|
-
severity: "high",
|
|
3853
|
-
category: "Cryptography",
|
|
3854
|
-
description: "TLS 1.0 and 1.1 are deprecated and have known vulnerabilities. Only TLS 1.2+ should be used.",
|
|
3855
|
-
check(content, filePath) {
|
|
3856
|
-
if (isTestFile(filePath)) return [];
|
|
3857
|
-
if (!filePath.match(/\.(js|ts|py|rb|go|java|yaml|yml|json|conf|cfg|xml)$/)) return [];
|
|
3858
|
-
const findings = [];
|
|
3859
|
-
const patterns = [
|
|
3860
|
-
/(?:TLSv1_METHOD|TLSv1\.0|TLSv1\.1|ssl\.PROTOCOL_TLSv1|SSLv3|SSLv2|TLS_1_0|TLS_1_1|minVersion\s*[:=]\s*["']TLSv1(?:\.1)?["'])/gi,
|
|
3861
|
-
/(?:tls\.DEFAULT_MIN_VERSION|secureProtocol)\s*[:=]\s*["'](?:TLSv1_method|TLSv1_1_method|SSLv3_method)["']/gi,
|
|
3862
|
-
/ssl_protocols\s+.*(?:TLSv1(?:\.1)?(?:\s|;))/gi
|
|
3863
|
-
];
|
|
3864
|
-
for (const pat of patterns) {
|
|
3865
|
-
let m;
|
|
3866
|
-
while ((m = pat.exec(content)) !== null) {
|
|
3867
|
-
if (isCommentLine(content, m.index)) continue;
|
|
3868
|
-
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3869
|
-
findings.push({
|
|
3870
|
-
rule: "VC122",
|
|
3871
|
-
title: deprecatedTLS.title,
|
|
3872
|
-
severity: "high",
|
|
3873
|
-
category: "Cryptography",
|
|
3874
|
-
file: filePath,
|
|
3875
|
-
line: lineNum,
|
|
3876
|
-
snippet: getSnippet(content, lineNum),
|
|
3877
|
-
fix: "Use TLS 1.2 or higher. Set minVersion to 'TLSv1.2' and remove support for TLS 1.0/1.1."
|
|
3878
|
-
});
|
|
3879
|
-
}
|
|
3880
|
-
}
|
|
3881
|
-
return findings;
|
|
3882
|
-
}
|
|
3883
|
-
};
|
|
3884
|
-
var weakRSAKeySize = {
|
|
3885
|
-
id: "VC123",
|
|
3886
|
-
title: "Weak RSA Key Size",
|
|
3887
|
-
severity: "high",
|
|
3888
|
-
category: "Cryptography",
|
|
3889
|
-
description: "RSA keys smaller than 2048 bits are considered insecure and can be factored with modern hardware.",
|
|
3890
|
-
check(content, filePath) {
|
|
3891
|
-
if (isTestFile(filePath)) return [];
|
|
3892
|
-
if (!filePath.match(/\.(js|ts|py|rb|go|java|php)$/)) return [];
|
|
3893
|
-
const findings = [];
|
|
3894
|
-
const patterns = [
|
|
3895
|
-
/generateKeyPair\s*\(\s*["']rsa["']\s*,\s*\{[^}]*modulusLength\s*:\s*(512|768|1024)\b/gi,
|
|
3896
|
-
/RSA\.generate\s*\(\s*(512|768|1024)\b/gi,
|
|
3897
|
-
/rsa\.GenerateKey\s*\([^,]*,\s*(512|768|1024)\b/gi,
|
|
3898
|
-
/KeyPairGenerator.*initialize\s*\(\s*(512|768|1024)\b/gi
|
|
3899
|
-
];
|
|
3900
|
-
for (const pat of patterns) {
|
|
3901
|
-
let m;
|
|
3902
|
-
while ((m = pat.exec(content)) !== null) {
|
|
3903
|
-
if (isCommentLine(content, m.index)) continue;
|
|
3904
|
-
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3905
|
-
findings.push({
|
|
3906
|
-
rule: "VC123",
|
|
3907
|
-
title: weakRSAKeySize.title,
|
|
3908
|
-
severity: "high",
|
|
3909
|
-
category: "Cryptography",
|
|
3910
|
-
file: filePath,
|
|
3911
|
-
line: lineNum,
|
|
3912
|
-
snippet: getSnippet(content, lineNum),
|
|
3913
|
-
fix: "Use RSA key sizes of at least 2048 bits. 4096 bits is recommended for long-term security."
|
|
3914
|
-
});
|
|
3915
|
-
}
|
|
3916
|
-
}
|
|
3917
|
-
return findings;
|
|
3918
|
-
}
|
|
3919
|
-
};
|
|
3920
|
-
var ecbModeEncryption = {
|
|
3921
|
-
id: "VC124",
|
|
3922
|
-
title: "Insecure ECB Mode Encryption",
|
|
3923
|
-
severity: "high",
|
|
3924
|
-
category: "Cryptography",
|
|
3925
|
-
description: "ECB (Electronic Codebook) mode encrypts identical plaintext blocks to identical ciphertext, leaking patterns in the data.",
|
|
3926
|
-
check(content, filePath) {
|
|
3927
|
-
if (isTestFile(filePath)) return [];
|
|
3928
|
-
if (!filePath.match(/\.(js|ts|py|rb|go|java|php)$/)) return [];
|
|
3929
|
-
const findings = [];
|
|
3930
|
-
const patterns = [
|
|
3931
|
-
/createCipher(?:iv)?\s*\(\s*["'](?:aes-(?:128|192|256)-ecb|des-ecb)["']/gi,
|
|
3932
|
-
/AES\.(?:new|MODE_ECB)|MODE_ECB/gi,
|
|
3933
|
-
/Cipher\.getInstance\s*\(\s*["']AES\/ECB/gi,
|
|
3934
|
-
/aes\.NewCipher|cipher\.NewECBEncrypter/gi
|
|
3935
|
-
];
|
|
3936
|
-
for (const pat of patterns) {
|
|
3937
|
-
let m;
|
|
3938
|
-
while ((m = pat.exec(content)) !== null) {
|
|
3939
|
-
if (isCommentLine(content, m.index)) continue;
|
|
3940
|
-
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3941
|
-
findings.push({
|
|
3942
|
-
rule: "VC124",
|
|
3943
|
-
title: ecbModeEncryption.title,
|
|
3944
|
-
severity: "high",
|
|
3945
|
-
category: "Cryptography",
|
|
3946
|
-
file: filePath,
|
|
3947
|
-
line: lineNum,
|
|
3948
|
-
snippet: getSnippet(content, lineNum),
|
|
3949
|
-
fix: "Use AES-GCM or AES-CBC with HMAC instead of ECB mode. GCM provides both encryption and authentication."
|
|
3950
|
-
});
|
|
3951
|
-
}
|
|
3952
|
-
}
|
|
3953
|
-
return findings;
|
|
3954
|
-
}
|
|
3955
|
-
};
|
|
3956
|
-
var insecurePasswordReset = {
|
|
3957
|
-
id: "VC125",
|
|
3958
|
-
title: "Insecure Password Reset Implementation",
|
|
3959
|
-
severity: "high",
|
|
3960
|
-
category: "Authentication",
|
|
3961
|
-
description: "Password reset using predictable tokens, no expiration, or user-enumeration leaks can be exploited to take over accounts.",
|
|
3962
|
-
check(content, filePath) {
|
|
3963
|
-
if (isTestFile(filePath)) return [];
|
|
3964
|
-
if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];
|
|
3965
|
-
if (!/(?:reset|forgot).*(?:password|passwd)/i.test(content)) return [];
|
|
3966
|
-
const findings = [];
|
|
3967
|
-
const weakTokens = /(?:reset|forgot).*(?:token|code)\s*[:=].*(?:Date\.now|Math\.random|uuid\(\)|nanoid\(\))/gi;
|
|
3968
|
-
let m;
|
|
3969
|
-
while ((m = weakTokens.exec(content)) !== null) {
|
|
3970
|
-
if (isCommentLine(content, m.index)) continue;
|
|
3971
|
-
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3972
|
-
findings.push({
|
|
3973
|
-
rule: "VC125",
|
|
3974
|
-
title: insecurePasswordReset.title,
|
|
3975
|
-
severity: "high",
|
|
3976
|
-
category: "Authentication",
|
|
3977
|
-
file: filePath,
|
|
3978
|
-
line: lineNum,
|
|
3979
|
-
snippet: getSnippet(content, lineNum),
|
|
3980
|
-
fix: "Use crypto.randomBytes(32).toString('hex') for reset tokens. Set expiration (15-60 minutes) and single-use enforcement."
|
|
3981
|
-
});
|
|
3982
|
-
}
|
|
3983
|
-
const enumeration = /(?:user|email|account)\s*(?:not\s*found|does\s*not\s*exist|invalid)/gi;
|
|
3984
|
-
while ((m = enumeration.exec(content)) !== null) {
|
|
3985
|
-
if (isCommentLine(content, m.index)) continue;
|
|
3986
|
-
const surrounding = content.substring(Math.max(0, m.index - 500), m.index);
|
|
3987
|
-
if (!/(?:reset|forgot).*password/i.test(surrounding)) continue;
|
|
3988
|
-
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3989
|
-
findings.push({
|
|
3990
|
-
rule: "VC125",
|
|
3991
|
-
title: insecurePasswordReset.title,
|
|
3992
|
-
severity: "high",
|
|
3993
|
-
category: "Authentication",
|
|
3994
|
-
file: filePath,
|
|
3995
|
-
line: lineNum,
|
|
3996
|
-
snippet: getSnippet(content, lineNum),
|
|
3997
|
-
fix: "Always return the same response regardless of whether the email exists. Say 'If an account exists, a reset link was sent.'"
|
|
3998
|
-
});
|
|
3999
|
-
}
|
|
4000
|
-
return findings;
|
|
4001
|
-
}
|
|
4002
|
-
};
|
|
4003
|
-
var terraformStateExposed = {
|
|
4004
|
-
id: "VC126",
|
|
4005
|
-
title: "Terraform State File Committed",
|
|
4006
|
-
severity: "critical",
|
|
4007
|
-
category: "Secrets",
|
|
4008
|
-
description: "Terraform state files contain sensitive infrastructure details, secrets, and access credentials in plaintext. They must never be committed to version control.",
|
|
4009
|
-
check(content, filePath) {
|
|
4010
|
-
const findings = [];
|
|
4011
|
-
if (/terraform\.tfstate(\.backup)?$/.test(filePath)) {
|
|
4012
|
-
findings.push({
|
|
4013
|
-
rule: "VC126",
|
|
4014
|
-
title: terraformStateExposed.title,
|
|
4015
|
-
severity: "critical",
|
|
4016
|
-
category: "Secrets",
|
|
4017
|
-
file: filePath,
|
|
4018
|
-
line: 1,
|
|
4019
|
-
snippet: getSnippet(content, 1),
|
|
4020
|
-
fix: "Add '*.tfstate' and '*.tfstate.backup' to .gitignore. Use remote state backends (S3, GCS, Terraform Cloud) instead."
|
|
4021
|
-
});
|
|
4022
|
-
}
|
|
4023
|
-
if (filePath.endsWith(".gitignore") && !content.includes("tfstate")) {
|
|
4024
|
-
if (/\.tf$/.test(filePath) || content.includes(".tf")) {
|
|
4025
|
-
}
|
|
4026
|
-
}
|
|
4027
|
-
return findings;
|
|
4028
|
-
}
|
|
4029
|
-
};
|
|
4030
|
-
var insecureHTTPMethods = {
|
|
4031
|
-
id: "VC127",
|
|
4032
|
-
title: "Dangerous HTTP Methods Without Auth",
|
|
4033
|
-
severity: "high",
|
|
4034
|
-
category: "Authorization",
|
|
4035
|
-
description: "DELETE, PUT, and PATCH endpoints without authentication checks allow unauthorized data modification or deletion.",
|
|
4036
|
-
check(content, filePath) {
|
|
4037
|
-
if (isTestFile(filePath)) return [];
|
|
4038
|
-
if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];
|
|
4039
|
-
const findings = [];
|
|
4040
|
-
const routePatterns = [
|
|
4041
|
-
/(?:app|router|server)\.(delete|put|patch)\s*\(\s*["']/gi
|
|
4042
|
-
];
|
|
4043
|
-
for (const pat of routePatterns) {
|
|
4044
|
-
let m;
|
|
4045
|
-
while ((m = pat.exec(content)) !== null) {
|
|
4046
|
-
if (isCommentLine(content, m.index)) continue;
|
|
4047
|
-
const handlerBlock = content.substring(m.index, Math.min(content.length, m.index + 500));
|
|
4048
|
-
if (/auth|authenticate|authorize|requireAuth|isAuthenticated|protect|guard|middleware|session|jwt|verify|clerk|getAuth/i.test(handlerBlock)) continue;
|
|
4049
|
-
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
4050
|
-
findings.push({
|
|
4051
|
-
rule: "VC127",
|
|
4052
|
-
title: insecureHTTPMethods.title,
|
|
4053
|
-
severity: "high",
|
|
4054
|
-
category: "Authorization",
|
|
4055
|
-
file: filePath,
|
|
4056
|
-
line: lineNum,
|
|
4057
|
-
snippet: getSnippet(content, lineNum),
|
|
4058
|
-
fix: "Add authentication middleware to DELETE, PUT, and PATCH routes. Verify the user has permission to modify the resource."
|
|
4059
|
-
});
|
|
4060
|
-
}
|
|
4061
|
-
}
|
|
4062
|
-
return findings;
|
|
4063
|
-
}
|
|
4064
|
-
};
|
|
4065
|
-
var httpRequestSmuggling = {
|
|
4066
|
-
id: "VC128",
|
|
4067
|
-
title: "HTTP Request Smuggling Risk",
|
|
4068
|
-
severity: "high",
|
|
4069
|
-
category: "Configuration",
|
|
4070
|
-
description: "Manually parsing Content-Length or Transfer-Encoding headers can lead to request smuggling when behind a reverse proxy.",
|
|
4071
|
-
check(content, filePath) {
|
|
4072
|
-
if (isTestFile(filePath)) return [];
|
|
4073
|
-
if (!filePath.match(/\.(js|ts|py|rb|go|java|php)$/)) return [];
|
|
4074
|
-
const findings = [];
|
|
4075
|
-
const patterns = [
|
|
4076
|
-
/(?:headers?\[?|getHeader\s*\(\s*)["'](?:content-length|transfer-encoding)["']\s*\]?\s*\)/gi,
|
|
4077
|
-
/parseInt\s*\(\s*(?:req|request)\.headers?\[?\s*["']content-length["']/gi
|
|
4078
|
-
];
|
|
4079
|
-
for (const pat of patterns) {
|
|
4080
|
-
let m;
|
|
4081
|
-
while ((m = pat.exec(content)) !== null) {
|
|
4082
|
-
if (isCommentLine(content, m.index)) continue;
|
|
4083
|
-
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
4084
|
-
findings.push({
|
|
4085
|
-
rule: "VC128",
|
|
4086
|
-
title: httpRequestSmuggling.title,
|
|
4087
|
-
severity: "high",
|
|
4088
|
-
category: "Configuration",
|
|
4089
|
-
file: filePath,
|
|
4090
|
-
line: lineNum,
|
|
4091
|
-
snippet: getSnippet(content, lineNum),
|
|
4092
|
-
fix: "Let your framework handle Content-Length and Transfer-Encoding headers. Do not manually parse or trust these values."
|
|
4093
|
-
});
|
|
4094
|
-
}
|
|
4095
|
-
}
|
|
4096
|
-
return findings;
|
|
1168
|
+
const sizeBuffer = Math.min(Math.log2(Math.max(totalFiles, 1)) * 2, 15);
|
|
1169
|
+
const score = Math.max(0, Math.min(100, 100 - deductions + sizeBuffer));
|
|
1170
|
+
let grade;
|
|
1171
|
+
let summary;
|
|
1172
|
+
if (score >= 95) {
|
|
1173
|
+
grade = "A+";
|
|
1174
|
+
summary = "Excellent security posture with minimal issues.";
|
|
1175
|
+
} else if (score >= 85) {
|
|
1176
|
+
grade = "A";
|
|
1177
|
+
summary = "Strong security with a few minor concerns.";
|
|
1178
|
+
} else if (score >= 70) {
|
|
1179
|
+
grade = "B";
|
|
1180
|
+
summary = "Good security but some issues need attention.";
|
|
1181
|
+
} else if (score >= 55) {
|
|
1182
|
+
grade = "C";
|
|
1183
|
+
summary = "Fair security \u2014 several vulnerabilities should be fixed.";
|
|
1184
|
+
} else if (score >= 35) {
|
|
1185
|
+
grade = "D";
|
|
1186
|
+
summary = "Poor security \u2014 critical issues require immediate attention.";
|
|
1187
|
+
} else {
|
|
1188
|
+
grade = "F";
|
|
1189
|
+
summary = "Failing \u2014 serious vulnerabilities present. Fix critical issues immediately.";
|
|
4097
1190
|
}
|
|
1191
|
+
return { grade, score: Math.round(score), summary };
|
|
1192
|
+
}
|
|
1193
|
+
var complianceMap = {
|
|
1194
|
+
VC001: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
1195
|
+
VC002: { owasp: "A05:2021", cwe: "CWE-200" },
|
|
1196
|
+
VC003: { owasp: "A01:2021", cwe: "CWE-862" },
|
|
1197
|
+
VC004: { owasp: "A01:2021", cwe: "CWE-284" },
|
|
1198
|
+
VC005: { owasp: "A08:2021", cwe: "CWE-345" },
|
|
1199
|
+
VC006: { owasp: "A03:2021", cwe: "CWE-89" },
|
|
1200
|
+
VC007: { owasp: "A03:2021", cwe: "CWE-79" },
|
|
1201
|
+
VC008: { owasp: "A04:2021", cwe: "CWE-770" },
|
|
1202
|
+
VC009: { owasp: "A05:2021", cwe: "CWE-942" },
|
|
1203
|
+
VC010: { owasp: "A01:2021", cwe: "CWE-602" },
|
|
1204
|
+
VC011: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
1205
|
+
VC012: { owasp: "A05:2021", cwe: "CWE-200" },
|
|
1206
|
+
VC013: { owasp: "A01:2021", cwe: "CWE-269" },
|
|
1207
|
+
VC014: { owasp: "A05:2021", cwe: "CWE-538" },
|
|
1208
|
+
VC015: { owasp: "A03:2021", cwe: "CWE-95" },
|
|
1209
|
+
VC016: { owasp: "A01:2021", cwe: "CWE-601" },
|
|
1210
|
+
VC017: { owasp: "A05:2021", cwe: "CWE-614" },
|
|
1211
|
+
VC018: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
1212
|
+
VC019: { owasp: "A05:2021", cwe: "CWE-693" },
|
|
1213
|
+
VC020: { owasp: "A05:2021", cwe: "CWE-1021" },
|
|
1214
|
+
VC021: { owasp: "A01:2021", cwe: "CWE-22" },
|
|
1215
|
+
VC022: { owasp: "A03:2021", cwe: "CWE-79" },
|
|
1216
|
+
VC023: { owasp: "A08:2021", cwe: "CWE-1321" },
|
|
1217
|
+
VC024: { owasp: "A04:2021", cwe: "CWE-770" },
|
|
1218
|
+
VC025: { owasp: "A03:2021", cwe: "CWE-22" },
|
|
1219
|
+
VC026: { owasp: "A05:2021", cwe: "CWE-693" },
|
|
1220
|
+
VC027: { owasp: "A05:2021", cwe: "CWE-693" },
|
|
1221
|
+
VC028: { owasp: "A07:2021", cwe: "CWE-20" },
|
|
1222
|
+
VC029: { owasp: "A08:2021", cwe: "CWE-20" },
|
|
1223
|
+
VC030: { owasp: "A08:2021", cwe: "CWE-502" },
|
|
1224
|
+
VC031: { owasp: "A02:2021", cwe: "CWE-321" },
|
|
1225
|
+
VC032: { owasp: "A05:2021", cwe: "CWE-319" },
|
|
1226
|
+
VC033: { owasp: "A05:2021", cwe: "CWE-215" },
|
|
1227
|
+
VC034: { owasp: "A02:2021", cwe: "CWE-338" },
|
|
1228
|
+
VC035: { owasp: "A01:2021", cwe: "CWE-601" },
|
|
1229
|
+
VC036: { owasp: "A04:2021", cwe: "CWE-755" },
|
|
1230
|
+
VC037: { owasp: "A09:2021", cwe: "CWE-209" },
|
|
1231
|
+
VC038: { owasp: "A04:2021", cwe: "CWE-434" },
|
|
1232
|
+
VC039: { owasp: "A06:2021", cwe: "CWE-1104" },
|
|
1233
|
+
VC040: { owasp: "A05:2021", cwe: "CWE-538" },
|
|
1234
|
+
VC041: { owasp: "A10:2021", cwe: "CWE-918" },
|
|
1235
|
+
VC042: { owasp: "A01:2021", cwe: "CWE-915" },
|
|
1236
|
+
VC043: { owasp: "A02:2021", cwe: "CWE-208" },
|
|
1237
|
+
VC044: { owasp: "A09:2021", cwe: "CWE-117" },
|
|
1238
|
+
VC045: { owasp: "A07:2021", cwe: "CWE-521" },
|
|
1239
|
+
VC046: { owasp: "A07:2021", cwe: "CWE-384" },
|
|
1240
|
+
VC047: { owasp: "A07:2021", cwe: "CWE-307" },
|
|
1241
|
+
VC048: { owasp: "A03:2021", cwe: "CWE-943" },
|
|
1242
|
+
VC049: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
1243
|
+
VC050: { owasp: "A02:2021", cwe: "CWE-319" },
|
|
1244
|
+
VC051: { owasp: "A05:2021", cwe: "CWE-200" },
|
|
1245
|
+
VC052: { owasp: "A04:2021", cwe: "CWE-770" },
|
|
1246
|
+
VC053: { owasp: "A05:2021", cwe: "CWE-798" },
|
|
1247
|
+
VC054: { owasp: "A07:2021", cwe: "CWE-922" },
|
|
1248
|
+
VC055: { owasp: "A05:2021", cwe: "CWE-540" },
|
|
1249
|
+
VC056: { owasp: "A05:2021", cwe: "CWE-1021" },
|
|
1250
|
+
VC057: { owasp: "A01:2021", cwe: "CWE-269" },
|
|
1251
|
+
VC058: { owasp: "A05:2021", cwe: "CWE-250" },
|
|
1252
|
+
VC059: { owasp: "A05:2021", cwe: "CWE-284" },
|
|
1253
|
+
VC060: { owasp: "A02:2021", cwe: "CWE-328" },
|
|
1254
|
+
VC061: { owasp: "A02:2021", cwe: "CWE-295" },
|
|
1255
|
+
VC062: { owasp: "A02:2021", cwe: "CWE-321" },
|
|
1256
|
+
VC063: { owasp: "A03:2021", cwe: "CWE-79" },
|
|
1257
|
+
VC064: { owasp: "A01:2021", cwe: "CWE-862" },
|
|
1258
|
+
VC065: { owasp: "A01:2021", cwe: "CWE-862" },
|
|
1259
|
+
VC066: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
1260
|
+
VC067: { owasp: "A01:2021", cwe: "CWE-601" },
|
|
1261
|
+
VC068: { owasp: "A07:2021", cwe: "CWE-922" },
|
|
1262
|
+
VC069: { owasp: "A02:2021", cwe: "CWE-295" },
|
|
1263
|
+
VC070: { owasp: "A05:2021", cwe: "CWE-489" },
|
|
1264
|
+
VC071: { owasp: "A05:2021", cwe: "CWE-215" },
|
|
1265
|
+
VC072: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
1266
|
+
VC073: { owasp: "A08:2021", cwe: "CWE-502" },
|
|
1267
|
+
VC074: { owasp: "A01:2021", cwe: "CWE-352" },
|
|
1268
|
+
VC075: { owasp: "A03:2021", cwe: "CWE-78" },
|
|
1269
|
+
VC076: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
1270
|
+
VC077: { owasp: "A05:2021", cwe: "CWE-942" },
|
|
1271
|
+
VC078: { owasp: "A05:2021", cwe: "CWE-250" },
|
|
1272
|
+
VC079: { owasp: "A02:2021", cwe: "CWE-327" },
|
|
1273
|
+
VC080: { owasp: "A04:2021", cwe: "CWE-1333" },
|
|
1274
|
+
VC081: { owasp: "A03:2021", cwe: "CWE-611" },
|
|
1275
|
+
VC082: { owasp: "A03:2021", cwe: "CWE-94" },
|
|
1276
|
+
VC083: { owasp: "A08:2021", cwe: "CWE-502" },
|
|
1277
|
+
VC084: { owasp: "A06:2021", cwe: "CWE-353" },
|
|
1278
|
+
VC085: { owasp: "A01:2021", cwe: "CWE-862" },
|
|
1279
|
+
VC086: { owasp: "A02:2021", cwe: "CWE-319" },
|
|
1280
|
+
VC087: { owasp: "A05:2021", cwe: "CWE-311" },
|
|
1281
|
+
VC088: { owasp: "A07:2021", cwe: "CWE-598" },
|
|
1282
|
+
VC089: { owasp: "A05:2021", cwe: "CWE-430" },
|
|
1283
|
+
VC090: { owasp: "A01:2021", cwe: "CWE-601" },
|
|
1284
|
+
VC091: { owasp: "A04:2021", cwe: "CWE-367" },
|
|
1285
|
+
VC092: { owasp: "A08:2021", cwe: "CWE-1321" },
|
|
1286
|
+
VC093: { owasp: "A01:2021", cwe: "CWE-22" },
|
|
1287
|
+
VC094: { owasp: "A03:2021", cwe: "CWE-78" },
|
|
1288
|
+
VC095: { owasp: "A05:2021", cwe: "CWE-942" },
|
|
1289
|
+
VC096: { owasp: "A02:2021", cwe: "CWE-319" },
|
|
1290
|
+
VC097: { owasp: "A09:2021", cwe: "CWE-532" },
|
|
1291
|
+
VC098: { owasp: "A04:2021", cwe: "CWE-400" },
|
|
1292
|
+
VC099: { owasp: "A04:2021", cwe: "CWE-401" },
|
|
1293
|
+
VC100: { owasp: "A04:2021", cwe: "CWE-400" },
|
|
1294
|
+
VC101: { owasp: "A04:2021", cwe: "CWE-400" },
|
|
1295
|
+
VC102: { owasp: "A04:2021", cwe: "CWE-400" },
|
|
1296
|
+
VC103: { owasp: "A04:2021", cwe: "CWE-710" },
|
|
1297
|
+
VC104: { owasp: "A04:2021", cwe: "CWE-390" },
|
|
1298
|
+
VC105: { owasp: "A04:2021", cwe: "CWE-710" },
|
|
1299
|
+
VC106: { owasp: "A04:2021", cwe: "CWE-710" },
|
|
1300
|
+
VC107: { owasp: "A02:2021", cwe: "CWE-311" },
|
|
1301
|
+
VC108: { owasp: "A01:2021", cwe: "CWE-284" },
|
|
1302
|
+
VC109: { owasp: "A01:2021", cwe: "CWE-284" },
|
|
1303
|
+
VC110: { owasp: "A09:2021", cwe: "CWE-778" },
|
|
1304
|
+
VC111: { owasp: "A05:2021", cwe: "CWE-16" },
|
|
1305
|
+
VC112: { owasp: "A06:2021", cwe: "CWE-1104" },
|
|
1306
|
+
VC113: { owasp: "A05:2021", cwe: "CWE-200" },
|
|
1307
|
+
VC114: { owasp: "A05:2021", cwe: "CWE-16" },
|
|
1308
|
+
VC115: { owasp: "A02:2021", cwe: "CWE-311" },
|
|
1309
|
+
VC116: { owasp: "A05:2021", cwe: "CWE-770" },
|
|
1310
|
+
VC117: { owasp: "A03:2021", cwe: "CWE-22" },
|
|
1311
|
+
VC118: { owasp: "A09:2021", cwe: "CWE-532" },
|
|
1312
|
+
VC119: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
1313
|
+
VC120: { owasp: "A07:2021", cwe: "CWE-352" },
|
|
1314
|
+
VC121: { owasp: "A06:2021", cwe: "CWE-1104" },
|
|
1315
|
+
VC122: { owasp: "A02:2021", cwe: "CWE-326" },
|
|
1316
|
+
VC123: { owasp: "A02:2021", cwe: "CWE-326" },
|
|
1317
|
+
VC124: { owasp: "A02:2021", cwe: "CWE-327" },
|
|
1318
|
+
VC125: { owasp: "A07:2021", cwe: "CWE-640" },
|
|
1319
|
+
VC126: { owasp: "A05:2021", cwe: "CWE-200" },
|
|
1320
|
+
VC127: { owasp: "A01:2021", cwe: "CWE-862" },
|
|
1321
|
+
VC128: { owasp: "A05:2021", cwe: "CWE-444" },
|
|
1322
|
+
VC129: { owasp: "A02:2021", cwe: "CWE-311" },
|
|
1323
|
+
VC130: { owasp: "A07:2021", cwe: "CWE-307" },
|
|
1324
|
+
VC131: { owasp: "A06:2021", cwe: "CWE-1104" }
|
|
4098
1325
|
};
|
|
4099
|
-
var
|
|
4100
|
-
id: "
|
|
4101
|
-
title: "
|
|
4102
|
-
severity: "
|
|
4103
|
-
category: "
|
|
4104
|
-
description: "
|
|
1326
|
+
var consoleLogProduction = {
|
|
1327
|
+
id: "VC097",
|
|
1328
|
+
title: "Console.log Left in Production Code",
|
|
1329
|
+
severity: "medium",
|
|
1330
|
+
category: "Performance",
|
|
1331
|
+
description: "console.log statements left in production code can leak sensitive data, slow down rendering, and clutter browser consoles.",
|
|
4105
1332
|
check(content, filePath) {
|
|
4106
|
-
if (
|
|
4107
|
-
if (
|
|
4108
|
-
const
|
|
4109
|
-
const
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
let m;
|
|
4115
|
-
while ((m = pat.exec(content)) !== null) {
|
|
4116
|
-
if (isCommentLine(content, m.index)) continue;
|
|
4117
|
-
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
4118
|
-
findings.push({
|
|
4119
|
-
rule: "VC129",
|
|
4120
|
-
title: unencryptedPII.title,
|
|
4121
|
-
severity: "high",
|
|
4122
|
-
category: "Information Leakage",
|
|
4123
|
-
file: filePath,
|
|
4124
|
-
line: lineNum,
|
|
4125
|
-
snippet: getSnippet(content, lineNum),
|
|
4126
|
-
fix: "Encrypt sensitive fields at the application level before storing. Use field-level encryption with a KMS for SSN, credit cards, and health data."
|
|
4127
|
-
});
|
|
1333
|
+
if (filePath.match(/test|spec|mock|__tests__|fixture|\.test\.|\.spec\./i)) return [];
|
|
1334
|
+
if (!/console\.log\s*\(/g.test(content)) return [];
|
|
1335
|
+
const lines = content.split("\n");
|
|
1336
|
+
const matches = [];
|
|
1337
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1338
|
+
const line = lines[i].trim();
|
|
1339
|
+
if (/console\.log\s*\(/.test(line) && !line.startsWith("//") && !line.startsWith("*") && !/if\s*\(\s*(?:debug|process\.env)/i.test(lines[Math.max(0, i - 1)] + line)) {
|
|
1340
|
+
matches.push({ rule: "VC097", title: consoleLogProduction.title, severity: "medium", category: "Performance", file: filePath, line: i + 1, snippet: getSnippet(content, i + 1), fix: "Remove console.log or use a logger that can be disabled in production." });
|
|
4128
1341
|
}
|
|
4129
1342
|
}
|
|
4130
|
-
return
|
|
1343
|
+
return matches.slice(0, 3);
|
|
4131
1344
|
}
|
|
4132
1345
|
};
|
|
4133
|
-
var
|
|
4134
|
-
id: "
|
|
4135
|
-
title: "
|
|
4136
|
-
severity: "
|
|
4137
|
-
category: "
|
|
4138
|
-
description: "
|
|
1346
|
+
var todoLeftInCode = {
|
|
1347
|
+
id: "VC103",
|
|
1348
|
+
title: "TODO/FIXME Left in Code",
|
|
1349
|
+
severity: "low",
|
|
1350
|
+
category: "Code Quality",
|
|
1351
|
+
description: "TODO, FIXME, HACK, and XXX comments indicate unfinished work that should be resolved before shipping to production.",
|
|
4139
1352
|
check(content, filePath) {
|
|
4140
|
-
if (
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
if (/rateLimit|rateLimiter|throttle|slowDown|express-rate-limit|rate_limit|RateLimiter|limiter/i.test(surrounding)) continue;
|
|
4149
|
-
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
4150
|
-
findings.push({
|
|
4151
|
-
rule: "VC130",
|
|
4152
|
-
title: missingAuthRateLimit.title,
|
|
4153
|
-
severity: "high",
|
|
4154
|
-
category: "Authentication",
|
|
4155
|
-
file: filePath,
|
|
4156
|
-
line: lineNum,
|
|
4157
|
-
snippet: getSnippet(content, lineNum),
|
|
4158
|
-
fix: "Add rate limiting to authentication endpoints. Limit to 5-10 attempts per minute per IP. Use express-rate-limit or similar."
|
|
4159
|
-
});
|
|
4160
|
-
}
|
|
4161
|
-
return findings;
|
|
1353
|
+
if (filePath.match(/test|spec|mock|__tests__|fixture|node_modules/i)) return [];
|
|
1354
|
+
return findMatches(
|
|
1355
|
+
content,
|
|
1356
|
+
/\/\/\s*(?:TODO|FIXME|HACK|XXX)\b/gi,
|
|
1357
|
+
todoLeftInCode,
|
|
1358
|
+
filePath,
|
|
1359
|
+
() => "Resolve TODO/FIXME comments before shipping. If it's intentional tech debt, track it in your issue tracker instead."
|
|
1360
|
+
).slice(0, 5);
|
|
4162
1361
|
}
|
|
4163
1362
|
};
|
|
4164
|
-
var
|
|
4165
|
-
id: "
|
|
4166
|
-
title: "
|
|
4167
|
-
severity: "
|
|
4168
|
-
category: "
|
|
4169
|
-
description: "
|
|
1363
|
+
var emptyCatchBlock = {
|
|
1364
|
+
id: "VC104",
|
|
1365
|
+
title: "Empty Catch Block",
|
|
1366
|
+
severity: "medium",
|
|
1367
|
+
category: "Code Quality",
|
|
1368
|
+
description: "Empty catch blocks silently swallow errors, making bugs impossible to diagnose. At minimum, log the error.",
|
|
4170
1369
|
check(content, filePath) {
|
|
4171
|
-
if (
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
[/"express"\s*:\s*"[\^~]?[0-3]\./g, "express < 4.x has multiple known vulnerabilities"],
|
|
4180
|
-
[/"axios"\s*:\s*"[\^~]?0\.[0-9]\./g, "axios < 0.21 has SSRF vulnerabilities"],
|
|
4181
|
-
[/"tar"\s*:\s*"[\^~]?[0-5]\./g, "tar < 6.x has path traversal vulnerabilities"],
|
|
4182
|
-
[/"glob-parent"\s*:\s*"[\^~]?[0-4]\./g, "glob-parent < 5.1.2 has ReDoS vulnerability"]
|
|
4183
|
-
];
|
|
4184
|
-
for (const [pat, message] of vulnerablePackages) {
|
|
4185
|
-
let m;
|
|
4186
|
-
while ((m = pat.exec(content)) !== null) {
|
|
4187
|
-
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
4188
|
-
findings.push({
|
|
4189
|
-
rule: "VC131",
|
|
4190
|
-
title: vulnerableDependencies.title,
|
|
4191
|
-
severity: "high",
|
|
4192
|
-
category: "Supply Chain",
|
|
4193
|
-
file: filePath,
|
|
4194
|
-
line: lineNum,
|
|
4195
|
-
snippet: getSnippet(content, lineNum),
|
|
4196
|
-
fix: `${message}. Update to the latest version and run 'npm audit' regularly.`
|
|
4197
|
-
});
|
|
4198
|
-
}
|
|
4199
|
-
}
|
|
4200
|
-
return findings;
|
|
1370
|
+
if (filePath.match(/test|spec|mock/i)) return [];
|
|
1371
|
+
return findMatches(
|
|
1372
|
+
content,
|
|
1373
|
+
/catch\s*(?:\([^)]*\))?\s*\{\s*\}/g,
|
|
1374
|
+
emptyCatchBlock,
|
|
1375
|
+
filePath,
|
|
1376
|
+
() => "Handle errors in catch blocks: catch(err) { console.error('Operation failed:', err); } or re-throw if appropriate."
|
|
1377
|
+
);
|
|
4201
1378
|
}
|
|
4202
1379
|
};
|
|
4203
1380
|
var freeRules = [
|
|
@@ -4262,160 +1439,12 @@ var freeRules = [
|
|
|
4262
1439
|
disabledTLSVerification
|
|
4263
1440
|
// VC061
|
|
4264
1441
|
];
|
|
4265
|
-
|
|
4266
|
-
hardcodedSecrets,
|
|
4267
|
-
exposedEnvFile,
|
|
4268
|
-
missingAuthMiddleware,
|
|
4269
|
-
supabaseNoRLS,
|
|
4270
|
-
stripeWebhookUnprotected,
|
|
4271
|
-
sqlInjection,
|
|
4272
|
-
xssVulnerability,
|
|
4273
|
-
noRateLimiting,
|
|
4274
|
-
corsWildcard,
|
|
4275
|
-
clientSideAuth,
|
|
4276
|
-
nextPublicSecret,
|
|
4277
|
-
firebaseClientConfig,
|
|
4278
|
-
supabaseAnonAdmin,
|
|
4279
|
-
envNotGitignored,
|
|
4280
|
-
evalUsage,
|
|
4281
|
-
unvalidatedRedirect,
|
|
4282
|
-
insecureCookies,
|
|
4283
|
-
exposedAuthSecret,
|
|
4284
|
-
insecureElectronWindow,
|
|
4285
|
-
missingCSP,
|
|
4286
|
-
ipcPathTraversal,
|
|
4287
|
-
unsanitizedHTMLExport,
|
|
4288
|
-
prototypePollution,
|
|
4289
|
-
missingFileSizeLimits,
|
|
4290
|
-
unsanitizedFilenames,
|
|
4291
|
-
electronNavigationUnrestricted,
|
|
4292
|
-
missingSecurityMeta,
|
|
4293
|
-
unvalidatedAPIParams,
|
|
4294
|
-
unvalidatedEventData,
|
|
4295
|
-
insecureDeserialization,
|
|
4296
|
-
hardcodedJWTSecret,
|
|
4297
|
-
missingHTTPS,
|
|
4298
|
-
exposedDebugMode,
|
|
4299
|
-
insecureRandomness,
|
|
4300
|
-
openRedirectParams,
|
|
4301
|
-
missingErrorBoundary,
|
|
4302
|
-
exposedStackTraces,
|
|
4303
|
-
insecureFileUpload,
|
|
4304
|
-
missingLockFile,
|
|
4305
|
-
exposedGitDir,
|
|
4306
|
-
ssrfVulnerability,
|
|
4307
|
-
massAssignment,
|
|
4308
|
-
timingAttack,
|
|
4309
|
-
logInjection,
|
|
4310
|
-
weakPasswordRequirements,
|
|
4311
|
-
sessionFixation,
|
|
4312
|
-
missingBruteForce,
|
|
4313
|
-
nosqlInjection,
|
|
4314
|
-
exposedDBCredentials,
|
|
4315
|
-
missingDBEncryption,
|
|
4316
|
-
graphqlIntrospection,
|
|
4317
|
-
missingRequestSizeLimit,
|
|
4318
|
-
hardcodedIPAllowlist,
|
|
4319
|
-
sensitiveLocalStorage,
|
|
4320
|
-
exposedSourceMaps,
|
|
4321
|
-
clickjacking,
|
|
4322
|
-
overlyPermissiveIAM,
|
|
4323
|
-
dockerRunAsRoot,
|
|
4324
|
-
exposedDockerPorts,
|
|
4325
|
-
weakHashing,
|
|
4326
|
-
disabledTLSVerification,
|
|
4327
|
-
hardcodedEncryptionKey,
|
|
4328
|
-
dangerousInnerHTML,
|
|
4329
|
-
exposedServerActions,
|
|
4330
|
-
unprotectedAPIRoutes,
|
|
4331
|
-
clientComponentSecret,
|
|
4332
|
-
insecureDeepLink,
|
|
4333
|
-
sensitiveAsyncStorage,
|
|
4334
|
-
missingCertPinning,
|
|
4335
|
-
androidDebuggable,
|
|
4336
|
-
djangoDebug,
|
|
4337
|
-
flaskSecretKey,
|
|
4338
|
-
pickleDeserialization,
|
|
4339
|
-
missingCSRF,
|
|
4340
|
-
githubActionsInjection,
|
|
4341
|
-
secretsInCI,
|
|
4342
|
-
corsServerless,
|
|
4343
|
-
k8sPrivileged,
|
|
4344
|
-
jwtAlgConfusion,
|
|
4345
|
-
regexDos,
|
|
4346
|
-
xxeVulnerability,
|
|
4347
|
-
ssti,
|
|
4348
|
-
javaDeserialization,
|
|
4349
|
-
missingSRI,
|
|
4350
|
-
exposedAdminRoutes,
|
|
4351
|
-
insecureWebSocket,
|
|
4352
|
-
missingHSTS,
|
|
4353
|
-
sensitiveURLParams,
|
|
4354
|
-
missingContentDisposition,
|
|
4355
|
-
hostHeaderRedirect,
|
|
4356
|
-
raceCondition,
|
|
4357
|
-
unsafeObjectAssign,
|
|
4358
|
-
unprotectedDownload,
|
|
4359
|
-
commandInjection,
|
|
4360
|
-
corsLocalhost,
|
|
4361
|
-
insecureGRPC,
|
|
4362
|
-
consoleLogProduction,
|
|
4363
|
-
syncFileOps,
|
|
4364
|
-
eventListenerLeak,
|
|
4365
|
-
nPlusOneQuery,
|
|
4366
|
-
largeBundleImport,
|
|
4367
|
-
blockingMainThread,
|
|
4368
|
-
todoLeftInCode,
|
|
4369
|
-
emptyCatchBlock,
|
|
4370
|
-
callbackHell,
|
|
4371
|
-
magicNumbers,
|
|
4372
|
-
s3BucketNoEncryption,
|
|
4373
|
-
securityGroupAllInbound,
|
|
4374
|
-
rdsPubliclyAccessible,
|
|
4375
|
-
missingCloudTrail,
|
|
4376
|
-
lambdaWithoutVPC,
|
|
4377
|
-
dockerLatestTag,
|
|
4378
|
-
dockerCopySensitive,
|
|
4379
|
-
dockerTooManyPorts,
|
|
4380
|
-
k8sSecretNotEncrypted,
|
|
4381
|
-
k8sNoResourceLimits,
|
|
4382
|
-
pathTraversal,
|
|
4383
|
-
// VC117
|
|
4384
|
-
piiInLogs,
|
|
4385
|
-
// VC118
|
|
4386
|
-
hardcodedOAuthSecret,
|
|
4387
|
-
// VC119
|
|
4388
|
-
missingOAuthState,
|
|
4389
|
-
// VC120
|
|
4390
|
-
unpinnedGitHubAction,
|
|
4391
|
-
// VC121
|
|
4392
|
-
deprecatedTLS,
|
|
4393
|
-
// VC122
|
|
4394
|
-
weakRSAKeySize,
|
|
4395
|
-
// VC123
|
|
4396
|
-
ecbModeEncryption,
|
|
4397
|
-
// VC124
|
|
4398
|
-
insecurePasswordReset,
|
|
4399
|
-
// VC125
|
|
4400
|
-
terraformStateExposed,
|
|
4401
|
-
// VC126
|
|
4402
|
-
insecureHTTPMethods,
|
|
4403
|
-
// VC127
|
|
4404
|
-
httpRequestSmuggling,
|
|
4405
|
-
// VC128
|
|
4406
|
-
unencryptedPII,
|
|
4407
|
-
// VC129
|
|
4408
|
-
missingAuthRateLimit,
|
|
4409
|
-
// VC130
|
|
4410
|
-
vulnerableDependencies
|
|
4411
|
-
// VC131
|
|
4412
|
-
];
|
|
4413
|
-
function runCustomRules(content, filePath, disabledRules = [], tier = "free") {
|
|
1442
|
+
function runCustomRules(content, filePath, disabledRules = [], tier = "free", extraRules = []) {
|
|
4414
1443
|
const findings = [];
|
|
4415
1444
|
if (/function runScan\(files\)|export function runCustomRules/.test(content) && /const (?:rules|allRules)\s*[:=]/.test(content) && /findMatches/.test(content)) {
|
|
4416
1445
|
return findings;
|
|
4417
1446
|
}
|
|
4418
|
-
const ruleset = tier === "pro" ?
|
|
1447
|
+
const ruleset = tier === "pro" && extraRules.length > 0 ? [...freeRules, ...extraRules] : freeRules;
|
|
4419
1448
|
for (const rule of ruleset) {
|
|
4420
1449
|
if (disabledRules.includes(rule.id)) continue;
|
|
4421
1450
|
const matches = rule.check(content, filePath);
|
|
@@ -6032,6 +3061,22 @@ async function scanCommand(directory, options) {
|
|
|
6032
3061
|
`));
|
|
6033
3062
|
}
|
|
6034
3063
|
}
|
|
3064
|
+
let proRulesExtra = [];
|
|
3065
|
+
if (tier === "pro") {
|
|
3066
|
+
const cached = loadCachedProRules();
|
|
3067
|
+
if (cached) {
|
|
3068
|
+
proRulesExtra = cached;
|
|
3069
|
+
} else {
|
|
3070
|
+
if (!isSilent) console.log(chalk2.gray(" Downloading Pro rules..."));
|
|
3071
|
+
const downloaded = await downloadProRulesBundle();
|
|
3072
|
+
if (downloaded) {
|
|
3073
|
+
proRulesExtra = loadCachedProRules() || [];
|
|
3074
|
+
}
|
|
3075
|
+
if (proRulesExtra.length === 0 && !isSilent) {
|
|
3076
|
+
console.log(chalk2.yellow(" Could not load Pro rules \u2014 scanning with free rules only"));
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
6035
3080
|
const spinner = ora({
|
|
6036
3081
|
text: "Scanning files...",
|
|
6037
3082
|
color: "cyan",
|
|
@@ -6071,7 +3116,7 @@ async function scanCommand(directory, options) {
|
|
|
6071
3116
|
fileContentsForAnalysis.push({ path: filePath, content });
|
|
6072
3117
|
const astCtx = buildASTContext(content, filePath);
|
|
6073
3118
|
if (astCtx.isScannerFile) continue;
|
|
6074
|
-
const findings = runCustomRules(content, filePath, config.disableRules, tier);
|
|
3119
|
+
const findings = runCustomRules(content, filePath, config.disableRules, tier, proRulesExtra);
|
|
6075
3120
|
for (const f of findings) {
|
|
6076
3121
|
if (astCtx.isTestFile) {
|
|
6077
3122
|
f.confidence = "low";
|
|
@@ -6311,6 +3356,7 @@ async function logoutCommand() {
|
|
|
6311
3356
|
return;
|
|
6312
3357
|
}
|
|
6313
3358
|
clearToken();
|
|
3359
|
+
clearProRulesCache();
|
|
6314
3360
|
console.log(chalk3.green("Logged out successfully."));
|
|
6315
3361
|
}
|
|
6316
3362
|
async function whoamiCommand() {
|
|
@@ -6448,7 +3494,7 @@ auth.command("login").description("Log in to your XploitScan account").action(lo
|
|
|
6448
3494
|
auth.command("logout").description("Log out of your XploitScan account").action(logoutCommand);
|
|
6449
3495
|
auth.command("whoami").description("Show current logged-in user").action(whoamiCommand);
|
|
6450
3496
|
program.command("upgrade").description("Upgrade to XploitScan Pro for unlimited scans").action(async () => {
|
|
6451
|
-
const { getStoredToken: getStoredToken2, getCheckoutUrl } = await import("./api-
|
|
3497
|
+
const { getStoredToken: getStoredToken2, getCheckoutUrl } = await import("./api-ZNWEMMEL.js");
|
|
6452
3498
|
const chalk4 = (await import("chalk")).default;
|
|
6453
3499
|
const token = getStoredToken2();
|
|
6454
3500
|
if (!token) {
|