xploitscan 1.0.8 → 1.0.10
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 +3 -3
- package/dist/index.js +96 -37
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
**Security scanner for AI-generated code.** Find vulnerabilities before attackers do.
|
|
7
7
|
|
|
8
|
-
Built for developers shipping code via Cursor, Lovable, Bolt, Replit, and Claude Code.
|
|
8
|
+
Built for developers shipping code via Cursor, Lovable, Bolt, Replit, and Claude Code. 151 security rules. Plain-English results. Copy-paste fixes.
|
|
9
9
|
|
|
10
10
|
## Quick Start
|
|
11
11
|
|
|
@@ -17,7 +17,7 @@ No install, no config, no account required. Your code stays 100% local.
|
|
|
17
17
|
|
|
18
18
|
## What It Catches
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
151 rules across 15+ categories:
|
|
21
21
|
|
|
22
22
|
| Category | Examples | Rules |
|
|
23
23
|
|----------|---------|-------|
|
|
@@ -130,7 +130,7 @@ Scan via the web at [xploitscan.com](https://xploitscan.com):
|
|
|
130
130
|
- SOC2/ISO27001 compliance mapping
|
|
131
131
|
- Slack and Discord webhook notifications
|
|
132
132
|
|
|
133
|
-
**Free**: 5 scans/day, 30 core rules. **Pro** ($29/mo): unlimited scans, all
|
|
133
|
+
**Free**: 5 scans/day, 30 core rules. **Pro** ($29/mo): unlimited scans, all 151 rules, and all dashboard features. **Team** ($99/mo): everything in Pro plus 5 seats, shared scan history, RBAC, and portfolio reports. Annual plans save 20%.
|
|
134
134
|
|
|
135
135
|
## Supported Languages
|
|
136
136
|
|
package/dist/index.js
CHANGED
|
@@ -398,7 +398,12 @@ var missingAuthMiddleware = {
|
|
|
398
398
|
// Next.js API routes
|
|
399
399
|
/export\s+(?:async\s+)?function\s+(?:GET|POST|PUT|PATCH|DELETE)\s*\(/gi
|
|
400
400
|
];
|
|
401
|
-
if (
|
|
401
|
+
if (isTestFile(filePath)) return [];
|
|
402
|
+
if (filePath.match(/\.(md|txt|rst|html|css|json|yaml|yml)$/)) return [];
|
|
403
|
+
const isAuthRoute = /\/auth\/|\/login|\/signup|\/register|\/logout|\/password\/|\/forgot|\/reset/i.test(filePath);
|
|
404
|
+
if (isAuthRoute) return [];
|
|
405
|
+
const isWebhookRoute = /\/webhook/i.test(filePath);
|
|
406
|
+
if (isWebhookRoute) return [];
|
|
402
407
|
const authPatterns = [
|
|
403
408
|
/auth/i,
|
|
404
409
|
/session/i,
|
|
@@ -409,6 +414,8 @@ var missingAuthMiddleware = {
|
|
|
409
414
|
/currentUser/i,
|
|
410
415
|
/isAuthenticated/i,
|
|
411
416
|
/requireAuth/i,
|
|
417
|
+
/requireUser/i,
|
|
418
|
+
/requireUserForApi/i,
|
|
412
419
|
/clerk/i,
|
|
413
420
|
/supabase\.auth/i,
|
|
414
421
|
/getServerSession/i,
|
|
@@ -420,7 +427,10 @@ var missingAuthMiddleware = {
|
|
|
420
427
|
/withAuth/i,
|
|
421
428
|
/passport/i,
|
|
422
429
|
/firebase\.auth/i,
|
|
423
|
-
/cognito/i
|
|
430
|
+
/cognito/i,
|
|
431
|
+
/verifyCronSecret/i,
|
|
432
|
+
/verifySecret/i,
|
|
433
|
+
/checkApiKey/i
|
|
424
434
|
];
|
|
425
435
|
const hasAuth = authPatterns.some((p) => p.test(content));
|
|
426
436
|
if (hasAuth) return [];
|
|
@@ -482,18 +492,33 @@ var stripeWebhookUnprotected = {
|
|
|
482
492
|
category: "Payment Security",
|
|
483
493
|
description: "Stripe webhook endpoints without signature verification allow attackers to fake payment events.",
|
|
484
494
|
check(content, filePath) {
|
|
485
|
-
if (
|
|
486
|
-
|
|
487
|
-
if (
|
|
488
|
-
const
|
|
489
|
-
if (
|
|
490
|
-
return
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
()
|
|
496
|
-
|
|
495
|
+
if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];
|
|
496
|
+
if (isTestFile(filePath)) return [];
|
|
497
|
+
if (!/stripe/i.test(content)) return [];
|
|
498
|
+
const hasStripeWebhookHandler = /(?:stripe.*webhook|webhook.*stripe)/i.test(content) || /(?:checkout\.session\.completed|invoice\.paid|payment_intent|customer\.subscription)/i.test(content);
|
|
499
|
+
if (!hasStripeWebhookHandler) return [];
|
|
500
|
+
if (/constructEvent|verifyHeader|stripe[_-]?signature|webhook[_-]?secret|STRIPE_WEBHOOK_SECRET/i.test(content)) return [];
|
|
501
|
+
const handlerPatterns = [
|
|
502
|
+
// POST handler that processes Stripe events
|
|
503
|
+
/export\s+(?:async\s+)?function\s+POST\s*\(/g,
|
|
504
|
+
// Express-style Stripe webhook route
|
|
505
|
+
/\.(post|all)\s*\(\s*["'`][^"'`]*(?:stripe|webhook)[^"'`]*["'`]/gi,
|
|
506
|
+
// Event type checking without prior verification
|
|
507
|
+
/(?:event\.type|req\.body\.type)\s*===?\s*["'`](?:checkout|invoice|payment|customer)\./g
|
|
508
|
+
];
|
|
509
|
+
const matches = [];
|
|
510
|
+
for (const pattern of handlerPatterns) {
|
|
511
|
+
matches.push(
|
|
512
|
+
...findMatches(
|
|
513
|
+
content,
|
|
514
|
+
pattern,
|
|
515
|
+
stripeWebhookUnprotected,
|
|
516
|
+
filePath,
|
|
517
|
+
() => "Verify the Stripe webhook signature using stripe.webhooks.constructEvent(body, sig, webhookSecret) to prevent forged payment events."
|
|
518
|
+
)
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
return matches;
|
|
497
522
|
}
|
|
498
523
|
};
|
|
499
524
|
var sqlInjection = {
|
|
@@ -552,15 +577,19 @@ var xssVulnerability = {
|
|
|
552
577
|
];
|
|
553
578
|
const matches = [];
|
|
554
579
|
for (const pattern of patterns) {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
() => "Sanitize user input before rendering as HTML. Use a library like DOMPurify: DOMPurify.sanitize(userInput)"
|
|
562
|
-
)
|
|
580
|
+
const raw = findMatches(
|
|
581
|
+
content,
|
|
582
|
+
pattern,
|
|
583
|
+
xssVulnerability,
|
|
584
|
+
filePath,
|
|
585
|
+
() => "Sanitize user input before rendering as HTML. Use a library like DOMPurify: DOMPurify.sanitize(userInput)"
|
|
563
586
|
);
|
|
587
|
+
for (const m of raw) {
|
|
588
|
+
const lineText = content.split("\n")[m.line - 1] || "";
|
|
589
|
+
if (/\.innerHTML\s*=\s*['"]/.test(lineText) && !/\$\{/.test(lineText)) continue;
|
|
590
|
+
if (/\.innerHTML\s*=\s*['"][^'"]*['"]\s*$/.test(lineText)) continue;
|
|
591
|
+
matches.push(m);
|
|
592
|
+
}
|
|
564
593
|
}
|
|
565
594
|
return matches;
|
|
566
595
|
}
|
|
@@ -663,13 +692,21 @@ var nextPublicSecret = {
|
|
|
663
692
|
];
|
|
664
693
|
const matches = [];
|
|
665
694
|
for (const p of patterns) {
|
|
666
|
-
|
|
695
|
+
const raw = findMatches(
|
|
667
696
|
content,
|
|
668
697
|
p,
|
|
669
698
|
nextPublicSecret,
|
|
670
699
|
filePath,
|
|
671
700
|
() => "Remove the NEXT_PUBLIC_ prefix. Only use NEXT_PUBLIC_ for values safe to expose in the browser."
|
|
672
|
-
)
|
|
701
|
+
);
|
|
702
|
+
for (const m of raw) {
|
|
703
|
+
const lineText = content.split("\n")[m.line - 1] || "";
|
|
704
|
+
if (/PUBLISHABLE|ANON_KEY|PUBLIC_KEY/i.test(lineText)) continue;
|
|
705
|
+
if (/CLERK_PUBLISHABLE/i.test(lineText)) continue;
|
|
706
|
+
if (/STRIPE_PUBLISHABLE/i.test(lineText)) continue;
|
|
707
|
+
if (/=\s*["']?\s*$|=\s*["']?pk_(?:test|live)_["']?\s*$/.test(lineText)) continue;
|
|
708
|
+
matches.push(m);
|
|
709
|
+
}
|
|
673
710
|
}
|
|
674
711
|
return matches;
|
|
675
712
|
}
|
|
@@ -741,6 +778,8 @@ var unvalidatedRedirect = {
|
|
|
741
778
|
category: "Injection",
|
|
742
779
|
description: "Redirecting users to URLs from untrusted input enables phishing attacks.",
|
|
743
780
|
check(content, filePath) {
|
|
781
|
+
if (isTestFile(filePath)) return [];
|
|
782
|
+
if (/isAllowedRedirect|validateRedirect|isSafeRedirect|allowedDomains|trustedDomains|whitelist.*url|allowlist.*url/i.test(content)) return [];
|
|
744
783
|
const patterns = [
|
|
745
784
|
/window\.location\s*=\s*(?!["'`]https?:\/\/)/g,
|
|
746
785
|
/window\.location\.href\s*=\s*(?!["'`]https?:\/\/)/g,
|
|
@@ -1119,16 +1158,31 @@ var dangerousInnerHTML = {
|
|
|
1119
1158
|
description: "Using dangerouslySetInnerHTML without sanitization (DOMPurify) enables XSS attacks. User-controlled content injected as raw HTML can execute arbitrary JavaScript.",
|
|
1120
1159
|
check(content, filePath) {
|
|
1121
1160
|
if (!filePath.match(/\.(jsx|tsx)$/)) return [];
|
|
1161
|
+
if (isTestFile(filePath)) return [];
|
|
1122
1162
|
if (!/dangerouslySetInnerHTML/i.test(content)) return [];
|
|
1123
1163
|
const hasSanitize = /DOMPurify|sanitize|purify|xss|sanitizeHtml|isomorphic-dompurify/i.test(content);
|
|
1124
1164
|
if (hasSanitize) return [];
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1165
|
+
const findings = [];
|
|
1166
|
+
const re = /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:\s*([^}]+)\}/g;
|
|
1167
|
+
let m;
|
|
1168
|
+
while ((m = re.exec(content)) !== null) {
|
|
1169
|
+
if (isCommentLine(content, m.index)) continue;
|
|
1170
|
+
const value = m[1].trim();
|
|
1171
|
+
if (/^[A-Z][A-Z0-9_]+$/.test(value)) continue;
|
|
1172
|
+
if (/^["'`][^$]*["'`]$/.test(value)) continue;
|
|
1173
|
+
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
1174
|
+
findings.push({
|
|
1175
|
+
rule: "VC063",
|
|
1176
|
+
title: dangerousInnerHTML.title,
|
|
1177
|
+
severity: "critical",
|
|
1178
|
+
category: "Injection",
|
|
1179
|
+
file: filePath,
|
|
1180
|
+
line: lineNum,
|
|
1181
|
+
snippet: getSnippet(content, lineNum),
|
|
1182
|
+
fix: "Sanitize HTML before using dangerouslySetInnerHTML: dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }}. Install: npm install dompurify"
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
return findings;
|
|
1132
1186
|
}
|
|
1133
1187
|
};
|
|
1134
1188
|
function detectFramework(files) {
|
|
@@ -1354,21 +1408,24 @@ var complianceMap = {
|
|
|
1354
1408
|
var consoleLogProduction = {
|
|
1355
1409
|
id: "VC097",
|
|
1356
1410
|
title: "Console.log Left in Production Code",
|
|
1357
|
-
severity: "
|
|
1411
|
+
severity: "low",
|
|
1358
1412
|
category: "Performance",
|
|
1359
1413
|
description: "console.log statements left in production code can leak sensitive data, slow down rendering, and clutter browser consoles.",
|
|
1360
1414
|
check(content, filePath) {
|
|
1361
|
-
if (filePath
|
|
1415
|
+
if (isTestFile(filePath)) return [];
|
|
1416
|
+
if (/(?:migrate|seed|script|cli|setup|dev)\./i.test(filePath)) return [];
|
|
1362
1417
|
if (!/console\.log\s*\(/g.test(content)) return [];
|
|
1363
1418
|
const lines = content.split("\n");
|
|
1419
|
+
const logCount = lines.filter((l) => /console\.log\s*\(/.test(l.trim()) && !l.trim().startsWith("//")).length;
|
|
1420
|
+
if (logCount > 5) return [];
|
|
1364
1421
|
const matches = [];
|
|
1365
1422
|
for (let i = 0; i < lines.length; i++) {
|
|
1366
1423
|
const line = lines[i].trim();
|
|
1367
1424
|
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)) {
|
|
1368
|
-
matches.push({ rule: "VC097", title: consoleLogProduction.title, severity: "
|
|
1425
|
+
matches.push({ rule: "VC097", title: consoleLogProduction.title, severity: "low", category: "Performance", file: filePath, line: i + 1, snippet: getSnippet(content, i + 1), fix: "Remove console.log or use a structured logger that can be disabled in production." });
|
|
1369
1426
|
}
|
|
1370
1427
|
}
|
|
1371
|
-
return matches.slice(0,
|
|
1428
|
+
return matches.slice(0, 1);
|
|
1372
1429
|
}
|
|
1373
1430
|
};
|
|
1374
1431
|
var todoLeftInCode = {
|
|
@@ -2496,6 +2553,8 @@ function scanEntropy(files) {
|
|
|
2496
2553
|
for (const { path: filePath, content } of files) {
|
|
2497
2554
|
if (SKIP_FILES.test(filePath)) continue;
|
|
2498
2555
|
if (SKIP_FILENAMES.test(filePath)) continue;
|
|
2556
|
+
const basename = filePath.split("/").pop() || "";
|
|
2557
|
+
if (SKIP_FILENAMES.test(basename)) continue;
|
|
2499
2558
|
if (filePath.includes("node_modules")) continue;
|
|
2500
2559
|
if (filePath.includes(".min.")) continue;
|
|
2501
2560
|
if (/(?:\.test\.|\.spec\.|__tests__|__mocks__|fixtures?\/)/i.test(filePath)) continue;
|
|
@@ -3470,9 +3529,9 @@ async function scanCommand(directory, options) {
|
|
|
3470
3529
|
if (tier === "free" && !isSilent) {
|
|
3471
3530
|
console.log("");
|
|
3472
3531
|
if (userPlan === "anonymous") {
|
|
3473
|
-
console.log(chalk2.gray(" Scanned with 30 free rules.") + chalk2.cyan(" Log in to unlock all
|
|
3532
|
+
console.log(chalk2.gray(" Scanned with 30 free rules.") + chalk2.cyan(" Log in to unlock all 151 rules \u2192") + chalk2.bold(" xploitscan auth login"));
|
|
3474
3533
|
} else {
|
|
3475
|
-
console.log(chalk2.gray(" Scanned with 30 rules.") + chalk2.cyan(" Upgrade to Pro for all
|
|
3534
|
+
console.log(chalk2.gray(" Scanned with 30 rules.") + chalk2.cyan(" Upgrade to Pro for all 151 rules \u2192") + chalk2.bold(" xploitscan upgrade"));
|
|
3476
3535
|
}
|
|
3477
3536
|
console.log("");
|
|
3478
3537
|
}
|