kramscan 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +81 -54
- package/dist/cli.js +6 -0
- package/dist/commands/dev.d.ts +2 -0
- package/dist/commands/dev.js +236 -0
- package/dist/commands/gate.d.ts +2 -0
- package/dist/commands/gate.js +109 -0
- package/dist/commands/scan.js +1 -0
- package/dist/commands/scans.js +4 -0
- package/dist/core/config-schema.js +1 -1
- package/dist/core/config.js +3 -3
- package/dist/core/diff-engine.d.ts +12 -0
- package/dist/core/diff-engine.js +47 -0
- package/dist/core/scan-index.d.ts +1 -0
- package/dist/core/scanner.js +7 -1
- package/dist/core/server-probe.d.ts +20 -0
- package/dist/core/server-probe.js +109 -0
- package/dist/core/vulnerability-detector.d.ts +6 -0
- package/dist/core/vulnerability-detector.js +21 -0
- package/dist/plugins/index.d.ts +5 -0
- package/dist/plugins/index.js +11 -1
- package/dist/plugins/vulnerabilities/CORSAnalyzerPlugin.d.ts +10 -0
- package/dist/plugins/vulnerabilities/CORSAnalyzerPlugin.js +67 -0
- package/dist/plugins/vulnerabilities/CookieSecurityPlugin.d.ts +10 -0
- package/dist/plugins/vulnerabilities/CookieSecurityPlugin.js +91 -0
- package/dist/plugins/vulnerabilities/DebugEndpointPlugin.d.ts +15 -0
- package/dist/plugins/vulnerabilities/DebugEndpointPlugin.js +222 -0
- package/dist/plugins/vulnerabilities/DirectoryTraversalPlugin.d.ts +13 -0
- package/dist/plugins/vulnerabilities/DirectoryTraversalPlugin.js +110 -0
- package/dist/plugins/vulnerabilities/OpenRedirectPlugin.d.ts +10 -0
- package/dist/plugins/vulnerabilities/OpenRedirectPlugin.js +69 -0
- package/dist/reports/PdfGenerator.js +26 -1
- package/dist/utils/theme.d.ts +1 -0
- package/dist/utils/theme.js +7 -1
- package/package.json +6 -3
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.diffScanResults = diffScanResults;
|
|
4
|
+
/**
|
|
5
|
+
* Creates a unique fingerprint for a vulnerability to enable comparison.
|
|
6
|
+
*/
|
|
7
|
+
function vulnFingerprint(v) {
|
|
8
|
+
return `${v.type}:${v.severity}:${v.title}:${new URL(v.url).pathname}`;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Compares two scan results and produces a diff of new and resolved vulnerabilities.
|
|
12
|
+
*/
|
|
13
|
+
function diffScanResults(previous, current) {
|
|
14
|
+
const prevFingerprints = new Map();
|
|
15
|
+
const currFingerprints = new Map();
|
|
16
|
+
for (const v of previous.vulnerabilities) {
|
|
17
|
+
prevFingerprints.set(vulnFingerprint(v), v);
|
|
18
|
+
}
|
|
19
|
+
for (const v of current.vulnerabilities) {
|
|
20
|
+
currFingerprints.set(vulnFingerprint(v), v);
|
|
21
|
+
}
|
|
22
|
+
const newVulnerabilities = [];
|
|
23
|
+
const resolvedVulnerabilities = [];
|
|
24
|
+
let unchangedCount = 0;
|
|
25
|
+
// Find new vulnerabilities (in current but not in previous)
|
|
26
|
+
for (const [fp, vuln] of currFingerprints) {
|
|
27
|
+
if (!prevFingerprints.has(fp)) {
|
|
28
|
+
newVulnerabilities.push(vuln);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
unchangedCount++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Find resolved vulnerabilities (in previous but not in current)
|
|
35
|
+
for (const [fp, vuln] of prevFingerprints) {
|
|
36
|
+
if (!currFingerprints.has(fp)) {
|
|
37
|
+
resolvedVulnerabilities.push(vuln);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
newVulnerabilities,
|
|
42
|
+
resolvedVulnerabilities,
|
|
43
|
+
unchangedCount,
|
|
44
|
+
previousTotal: previous.vulnerabilities.length,
|
|
45
|
+
currentTotal: current.vulnerabilities.length,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -13,6 +13,7 @@ export interface ScanIndexEntry {
|
|
|
13
13
|
low: number;
|
|
14
14
|
info: number;
|
|
15
15
|
};
|
|
16
|
+
score?: number;
|
|
16
17
|
}
|
|
17
18
|
export declare function addScanToIndex(entry: Omit<ScanIndexEntry, "id">): Promise<ScanIndexEntry>;
|
|
18
19
|
export declare function listScans(limit?: number): Promise<ScanIndexEntry[]>;
|
package/dist/core/scanner.js
CHANGED
|
@@ -30,7 +30,7 @@ class Scanner extends events_1.EventEmitter {
|
|
|
30
30
|
maxLinksPerPage = 50;
|
|
31
31
|
includePatterns = [];
|
|
32
32
|
excludePatterns = [];
|
|
33
|
-
userAgent = "KramScan/0.
|
|
33
|
+
userAgent = "KramScan/0.2.0";
|
|
34
34
|
scanErrors = [];
|
|
35
35
|
pluginErrors = new Map();
|
|
36
36
|
usePlugins = true;
|
|
@@ -63,6 +63,11 @@ class Scanner extends events_1.EventEmitter {
|
|
|
63
63
|
plugins_1.pluginManager.register(new plugins_2.SecurityHeadersPlugin());
|
|
64
64
|
plugins_1.pluginManager.register(new plugins_2.SensitiveDataPlugin());
|
|
65
65
|
plugins_1.pluginManager.register(new plugins_2.CSRFPlugin());
|
|
66
|
+
plugins_1.pluginManager.register(new plugins_2.CORSAnalyzerPlugin());
|
|
67
|
+
plugins_1.pluginManager.register(new plugins_2.DebugEndpointPlugin());
|
|
68
|
+
plugins_1.pluginManager.register(new plugins_2.DirectoryTraversalPlugin());
|
|
69
|
+
plugins_1.pluginManager.register(new plugins_2.CookieSecurityPlugin());
|
|
70
|
+
plugins_1.pluginManager.register(new plugins_2.OpenRedirectPlugin());
|
|
66
71
|
}
|
|
67
72
|
// Type-safe event emitter methods
|
|
68
73
|
emit(event, data) {
|
|
@@ -189,6 +194,7 @@ class Scanner extends events_1.EventEmitter {
|
|
|
189
194
|
testedForms: this.testedForms,
|
|
190
195
|
requestsMade: this.requestsMade,
|
|
191
196
|
},
|
|
197
|
+
score: this.detector.calculateScore(),
|
|
192
198
|
};
|
|
193
199
|
this.emit("scan:complete", { result });
|
|
194
200
|
return result;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ProbeResult {
|
|
2
|
+
reachable: boolean;
|
|
3
|
+
statusCode?: number;
|
|
4
|
+
server?: string;
|
|
5
|
+
framework?: string;
|
|
6
|
+
responseTime: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Probes a localhost URL for server readiness.
|
|
10
|
+
* Polls with exponential backoff until the server responds or timeout is reached.
|
|
11
|
+
*/
|
|
12
|
+
export declare function probeServer(url: string, options?: {
|
|
13
|
+
timeout?: number;
|
|
14
|
+
interval?: number;
|
|
15
|
+
maxAttempts?: number;
|
|
16
|
+
}): Promise<ProbeResult>;
|
|
17
|
+
/**
|
|
18
|
+
* Checks if a URL points to localhost.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isLocalhost(url: string): boolean;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.probeServer = probeServer;
|
|
7
|
+
exports.isLocalhost = isLocalhost;
|
|
8
|
+
const http_1 = __importDefault(require("http"));
|
|
9
|
+
const https_1 = __importDefault(require("https"));
|
|
10
|
+
/**
|
|
11
|
+
* Probes a localhost URL for server readiness.
|
|
12
|
+
* Polls with exponential backoff until the server responds or timeout is reached.
|
|
13
|
+
*/
|
|
14
|
+
async function probeServer(url, options = {}) {
|
|
15
|
+
const { timeout = 30000, interval = 1000, maxAttempts = 20 } = options;
|
|
16
|
+
const startTime = Date.now();
|
|
17
|
+
let attempts = 0;
|
|
18
|
+
let lastError = null;
|
|
19
|
+
while (attempts < maxAttempts && Date.now() - startTime < timeout) {
|
|
20
|
+
attempts++;
|
|
21
|
+
try {
|
|
22
|
+
const result = await pingUrl(url);
|
|
23
|
+
if (result.reachable) {
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
lastError = `HTTP ${result.statusCode}`;
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
lastError = err.message;
|
|
30
|
+
}
|
|
31
|
+
// Exponential backoff with cap
|
|
32
|
+
const delay = Math.min(interval * Math.pow(1.5, attempts - 1), 5000);
|
|
33
|
+
await sleep(delay);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
reachable: false,
|
|
37
|
+
responseTime: Date.now() - startTime,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Single ping to a URL — returns immediately.
|
|
42
|
+
*/
|
|
43
|
+
function pingUrl(url) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const startTime = Date.now();
|
|
46
|
+
const parsedUrl = new URL(url);
|
|
47
|
+
const client = parsedUrl.protocol === "https:" ? https_1.default : http_1.default;
|
|
48
|
+
const req = client.get(url, {
|
|
49
|
+
timeout: 5000,
|
|
50
|
+
rejectUnauthorized: false, // Allow self-signed certs in dev
|
|
51
|
+
}, (res) => {
|
|
52
|
+
const responseTime = Date.now() - startTime;
|
|
53
|
+
const server = res.headers["server"] || undefined;
|
|
54
|
+
const poweredBy = res.headers["x-powered-by"] || "";
|
|
55
|
+
// Detect framework from headers
|
|
56
|
+
let framework;
|
|
57
|
+
if (poweredBy.includes("Express"))
|
|
58
|
+
framework = "Express.js";
|
|
59
|
+
else if (poweredBy.includes("Next.js"))
|
|
60
|
+
framework = "Next.js";
|
|
61
|
+
else if (poweredBy.includes("Nuxt"))
|
|
62
|
+
framework = "Nuxt.js";
|
|
63
|
+
else if (poweredBy.includes("PHP"))
|
|
64
|
+
framework = "PHP";
|
|
65
|
+
else if (poweredBy.includes("ASP.NET"))
|
|
66
|
+
framework = "ASP.NET";
|
|
67
|
+
else if (res.headers["x-django-request-id"])
|
|
68
|
+
framework = "Django";
|
|
69
|
+
else if (res.headers["x-request-id"] && server?.includes("nginx"))
|
|
70
|
+
framework = "Rails/nginx";
|
|
71
|
+
// Consume body to free socket
|
|
72
|
+
res.resume();
|
|
73
|
+
resolve({
|
|
74
|
+
reachable: (res.statusCode || 0) >= 100 && (res.statusCode || 0) < 600,
|
|
75
|
+
statusCode: res.statusCode,
|
|
76
|
+
server: server,
|
|
77
|
+
framework,
|
|
78
|
+
responseTime,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
req.on("error", (err) => {
|
|
82
|
+
reject(err);
|
|
83
|
+
});
|
|
84
|
+
req.on("timeout", () => {
|
|
85
|
+
req.destroy();
|
|
86
|
+
reject(new Error("Connection timed out"));
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function sleep(ms) {
|
|
91
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Checks if a URL points to localhost.
|
|
95
|
+
*/
|
|
96
|
+
function isLocalhost(url) {
|
|
97
|
+
try {
|
|
98
|
+
const parsed = new URL(url);
|
|
99
|
+
const host = parsed.hostname.toLowerCase();
|
|
100
|
+
return (host === "localhost" ||
|
|
101
|
+
host === "127.0.0.1" ||
|
|
102
|
+
host === "0.0.0.0" ||
|
|
103
|
+
host === "::1" ||
|
|
104
|
+
host.endsWith(".localhost"));
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -28,6 +28,7 @@ export interface ScanResult {
|
|
|
28
28
|
testedForms: number;
|
|
29
29
|
requestsMade: number;
|
|
30
30
|
};
|
|
31
|
+
score: number;
|
|
31
32
|
}
|
|
32
33
|
export declare class VulnerabilityDetector {
|
|
33
34
|
private vulnerabilities;
|
|
@@ -59,5 +60,10 @@ export declare class VulnerabilityDetector {
|
|
|
59
60
|
low: number;
|
|
60
61
|
info: number;
|
|
61
62
|
};
|
|
63
|
+
/**
|
|
64
|
+
* Calculates a security score from 0-100
|
|
65
|
+
* 100 is perfect, 0 is very poor
|
|
66
|
+
*/
|
|
67
|
+
calculateScore(): number;
|
|
62
68
|
clear(): void;
|
|
63
69
|
}
|
|
@@ -452,6 +452,27 @@ class VulnerabilityDetector {
|
|
|
452
452
|
}
|
|
453
453
|
return summary;
|
|
454
454
|
}
|
|
455
|
+
/**
|
|
456
|
+
* Calculates a security score from 0-100
|
|
457
|
+
* 100 is perfect, 0 is very poor
|
|
458
|
+
*/
|
|
459
|
+
calculateScore() {
|
|
460
|
+
if (this.vulnerabilities.length === 0)
|
|
461
|
+
return 100;
|
|
462
|
+
const weights = {
|
|
463
|
+
critical: 25,
|
|
464
|
+
high: 10,
|
|
465
|
+
medium: 5,
|
|
466
|
+
low: 2,
|
|
467
|
+
info: 0,
|
|
468
|
+
};
|
|
469
|
+
let totalDeduction = 0;
|
|
470
|
+
for (const vuln of this.vulnerabilities) {
|
|
471
|
+
totalDeduction += weights[vuln.severity] || 0;
|
|
472
|
+
}
|
|
473
|
+
const score = Math.max(0, 100 - totalDeduction);
|
|
474
|
+
return Math.round(score);
|
|
475
|
+
}
|
|
455
476
|
clear() {
|
|
456
477
|
this.vulnerabilities = [];
|
|
457
478
|
this.reportedHeaders.clear();
|
package/dist/plugins/index.d.ts
CHANGED
|
@@ -5,3 +5,8 @@ export { SQLInjectionPlugin } from "./vulnerabilities/SQLInjectionPlugin";
|
|
|
5
5
|
export { SecurityHeadersPlugin } from "./vulnerabilities/SecurityHeadersPlugin";
|
|
6
6
|
export { SensitiveDataPlugin } from "./vulnerabilities/SensitiveDataPlugin";
|
|
7
7
|
export { CSRFPlugin } from "./vulnerabilities/CSRFPlugin";
|
|
8
|
+
export { CORSAnalyzerPlugin } from "./vulnerabilities/CORSAnalyzerPlugin";
|
|
9
|
+
export { DebugEndpointPlugin } from "./vulnerabilities/DebugEndpointPlugin";
|
|
10
|
+
export { DirectoryTraversalPlugin } from "./vulnerabilities/DirectoryTraversalPlugin";
|
|
11
|
+
export { CookieSecurityPlugin } from "./vulnerabilities/CookieSecurityPlugin";
|
|
12
|
+
export { OpenRedirectPlugin } from "./vulnerabilities/OpenRedirectPlugin";
|
package/dist/plugins/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CSRFPlugin = exports.SensitiveDataPlugin = exports.SecurityHeadersPlugin = exports.SQLInjectionPlugin = exports.XSSPlugin = exports.pluginManager = exports.PluginManager = exports.BaseVulnerabilityPlugin = void 0;
|
|
3
|
+
exports.OpenRedirectPlugin = exports.CookieSecurityPlugin = exports.DirectoryTraversalPlugin = exports.DebugEndpointPlugin = exports.CORSAnalyzerPlugin = exports.CSRFPlugin = exports.SensitiveDataPlugin = exports.SecurityHeadersPlugin = exports.SQLInjectionPlugin = exports.XSSPlugin = exports.pluginManager = exports.PluginManager = exports.BaseVulnerabilityPlugin = void 0;
|
|
4
4
|
var types_1 = require("./types");
|
|
5
5
|
Object.defineProperty(exports, "BaseVulnerabilityPlugin", { enumerable: true, get: function () { return types_1.BaseVulnerabilityPlugin; } });
|
|
6
6
|
var PluginManager_1 = require("./PluginManager");
|
|
@@ -17,3 +17,13 @@ var SensitiveDataPlugin_1 = require("./vulnerabilities/SensitiveDataPlugin");
|
|
|
17
17
|
Object.defineProperty(exports, "SensitiveDataPlugin", { enumerable: true, get: function () { return SensitiveDataPlugin_1.SensitiveDataPlugin; } });
|
|
18
18
|
var CSRFPlugin_1 = require("./vulnerabilities/CSRFPlugin");
|
|
19
19
|
Object.defineProperty(exports, "CSRFPlugin", { enumerable: true, get: function () { return CSRFPlugin_1.CSRFPlugin; } });
|
|
20
|
+
var CORSAnalyzerPlugin_1 = require("./vulnerabilities/CORSAnalyzerPlugin");
|
|
21
|
+
Object.defineProperty(exports, "CORSAnalyzerPlugin", { enumerable: true, get: function () { return CORSAnalyzerPlugin_1.CORSAnalyzerPlugin; } });
|
|
22
|
+
var DebugEndpointPlugin_1 = require("./vulnerabilities/DebugEndpointPlugin");
|
|
23
|
+
Object.defineProperty(exports, "DebugEndpointPlugin", { enumerable: true, get: function () { return DebugEndpointPlugin_1.DebugEndpointPlugin; } });
|
|
24
|
+
var DirectoryTraversalPlugin_1 = require("./vulnerabilities/DirectoryTraversalPlugin");
|
|
25
|
+
Object.defineProperty(exports, "DirectoryTraversalPlugin", { enumerable: true, get: function () { return DirectoryTraversalPlugin_1.DirectoryTraversalPlugin; } });
|
|
26
|
+
var CookieSecurityPlugin_1 = require("./vulnerabilities/CookieSecurityPlugin");
|
|
27
|
+
Object.defineProperty(exports, "CookieSecurityPlugin", { enumerable: true, get: function () { return CookieSecurityPlugin_1.CookieSecurityPlugin; } });
|
|
28
|
+
var OpenRedirectPlugin_1 = require("./vulnerabilities/OpenRedirectPlugin");
|
|
29
|
+
Object.defineProperty(exports, "OpenRedirectPlugin", { enumerable: true, get: function () { return OpenRedirectPlugin_1.OpenRedirectPlugin; } });
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BaseVulnerabilityPlugin, PluginContext } from "../types";
|
|
2
|
+
import { Vulnerability } from "../../core/vulnerability-detector";
|
|
3
|
+
export declare class CORSAnalyzerPlugin extends BaseVulnerabilityPlugin {
|
|
4
|
+
readonly name = "CORS Analyzer";
|
|
5
|
+
readonly type: "header";
|
|
6
|
+
readonly description = "Detects overly permissive Cross-Origin Resource Sharing configurations";
|
|
7
|
+
private readonly reportedHosts;
|
|
8
|
+
analyzeHeaders(context: PluginContext, headers: Record<string, string>): Promise<Vulnerability[]>;
|
|
9
|
+
reset(): void;
|
|
10
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CORSAnalyzerPlugin = void 0;
|
|
4
|
+
const types_1 = require("../types");
|
|
5
|
+
class CORSAnalyzerPlugin extends types_1.BaseVulnerabilityPlugin {
|
|
6
|
+
name = "CORS Analyzer";
|
|
7
|
+
type = "header";
|
|
8
|
+
description = "Detects overly permissive Cross-Origin Resource Sharing configurations";
|
|
9
|
+
reportedHosts = new Set();
|
|
10
|
+
async analyzeHeaders(context, headers) {
|
|
11
|
+
const host = new URL(context.url).host;
|
|
12
|
+
// Only report once per host
|
|
13
|
+
if (this.reportedHosts.has(host)) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
const vulnerabilities = [];
|
|
17
|
+
const acao = headers["access-control-allow-origin"];
|
|
18
|
+
const acac = headers["access-control-allow-credentials"];
|
|
19
|
+
const acam = headers["access-control-allow-methods"];
|
|
20
|
+
const acah = headers["access-control-allow-headers"];
|
|
21
|
+
// Check for wildcard origin
|
|
22
|
+
if (acao === "*") {
|
|
23
|
+
vulnerabilities.push(this.createVulnerability("CORS: Wildcard Origin Allowed", `The server at ${host} allows requests from any origin (Access-Control-Allow-Origin: *). ` +
|
|
24
|
+
`This can expose sensitive data to malicious third-party websites.`, context.url, "medium", `Access-Control-Allow-Origin: *`, "Restrict Access-Control-Allow-Origin to trusted domains only. " +
|
|
25
|
+
"Use a whitelist of allowed origins instead of the wildcard *.", "CWE-942"));
|
|
26
|
+
}
|
|
27
|
+
// Check for wildcard with credentials (very dangerous)
|
|
28
|
+
if (acao === "*" && acac?.toLowerCase() === "true") {
|
|
29
|
+
vulnerabilities.push(this.createVulnerability("CORS: Wildcard Origin with Credentials", `The server at ${host} allows any origin AND sends credentials. ` +
|
|
30
|
+
`This is a critical misconfiguration that can lead to complete session hijacking.`, context.url, "critical", `Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true`, "Never combine wildcard origin with credentials. " +
|
|
31
|
+
"Validate the Origin header against a strict whitelist and reflect only trusted origins.", "CWE-942"));
|
|
32
|
+
}
|
|
33
|
+
// Check if origin is reflected without validation (test with a probe)
|
|
34
|
+
if (acao && acao !== "*" && acao !== "null") {
|
|
35
|
+
// If the ACAO reflects back what looks like an origin, it might be reflecting without validation
|
|
36
|
+
const isLikelyReflecting = acao.includes("://") && !acao.includes(host);
|
|
37
|
+
if (isLikelyReflecting && acac?.toLowerCase() === "true") {
|
|
38
|
+
vulnerabilities.push(this.createVulnerability("CORS: Origin Reflection with Credentials", `The server at ${host} appears to reflect the Origin header value while also allowing credentials. ` +
|
|
39
|
+
`An attacker can make authenticated requests from any origin.`, context.url, "high", `Access-Control-Allow-Origin: ${acao}, Access-Control-Allow-Credentials: true`, "Validate the Origin header against a strict whitelist of trusted domains. " +
|
|
40
|
+
"Never blindly reflect the Origin header.", "CWE-942"));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Check for null origin allowed (can be exploited via sandboxed iframes)
|
|
44
|
+
if (acao === "null") {
|
|
45
|
+
vulnerabilities.push(this.createVulnerability("CORS: Null Origin Allowed", `The server at ${host} accepts the 'null' origin. ` +
|
|
46
|
+
`Attackers can exploit this using sandboxed iframes or data URIs.`, context.url, "medium", `Access-Control-Allow-Origin: null`, "Do not allow the 'null' origin. Sandboxed iframes and redirects can send Origin: null.", "CWE-942"));
|
|
47
|
+
}
|
|
48
|
+
// Check for dangerous methods
|
|
49
|
+
if (acam) {
|
|
50
|
+
const dangerousMethods = ["PUT", "DELETE", "PATCH"];
|
|
51
|
+
const allowedMethods = acam.toUpperCase().split(",").map(m => m.trim());
|
|
52
|
+
const exposed = dangerousMethods.filter(m => allowedMethods.includes(m));
|
|
53
|
+
if (exposed.length > 0 && acao === "*") {
|
|
54
|
+
vulnerabilities.push(this.createVulnerability("CORS: Dangerous Methods with Wildcard Origin", `The server at ${host} allows ${exposed.join(", ")} methods from any origin. ` +
|
|
55
|
+
`This could allow unauthorized data modification from third-party sites.`, context.url, "high", `Access-Control-Allow-Methods: ${acam}; Access-Control-Allow-Origin: *`, "Restrict allowed methods to those actually needed, and never combine with wildcard origin.", "CWE-942"));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (vulnerabilities.length > 0) {
|
|
59
|
+
this.reportedHosts.add(host);
|
|
60
|
+
}
|
|
61
|
+
return vulnerabilities;
|
|
62
|
+
}
|
|
63
|
+
reset() {
|
|
64
|
+
this.reportedHosts.clear();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.CORSAnalyzerPlugin = CORSAnalyzerPlugin;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BaseVulnerabilityPlugin, PluginContext } from "../types";
|
|
2
|
+
import { Vulnerability } from "../../core/vulnerability-detector";
|
|
3
|
+
export declare class CookieSecurityPlugin extends BaseVulnerabilityPlugin {
|
|
4
|
+
readonly name = "Cookie Security Auditor";
|
|
5
|
+
readonly type: "header";
|
|
6
|
+
readonly description = "Audits cookies for missing security flags (HttpOnly, Secure, SameSite)";
|
|
7
|
+
private readonly reportedCookies;
|
|
8
|
+
analyzeHeaders(context: PluginContext, headers: Record<string, string>): Promise<Vulnerability[]>;
|
|
9
|
+
reset(): void;
|
|
10
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CookieSecurityPlugin = void 0;
|
|
4
|
+
const types_1 = require("../types");
|
|
5
|
+
class CookieSecurityPlugin extends types_1.BaseVulnerabilityPlugin {
|
|
6
|
+
name = "Cookie Security Auditor";
|
|
7
|
+
type = "header";
|
|
8
|
+
description = "Audits cookies for missing security flags (HttpOnly, Secure, SameSite)";
|
|
9
|
+
reportedCookies = new Set();
|
|
10
|
+
async analyzeHeaders(context, headers) {
|
|
11
|
+
const vulnerabilities = [];
|
|
12
|
+
const host = new URL(context.url).host;
|
|
13
|
+
// Collect all set-cookie headers
|
|
14
|
+
const setCookieHeader = headers["set-cookie"];
|
|
15
|
+
if (!setCookieHeader)
|
|
16
|
+
return [];
|
|
17
|
+
// set-cookie headers might be combined with newlines in some scenarios
|
|
18
|
+
const cookies = setCookieHeader.split(/,(?=[^;]*=)/g).map(c => c.trim());
|
|
19
|
+
for (const cookie of cookies) {
|
|
20
|
+
const cookieName = cookie.split("=")[0]?.trim();
|
|
21
|
+
if (!cookieName)
|
|
22
|
+
continue;
|
|
23
|
+
const cookieKey = `${host}:${cookieName}`;
|
|
24
|
+
if (this.reportedCookies.has(cookieKey))
|
|
25
|
+
continue;
|
|
26
|
+
const cookieLower = cookie.toLowerCase();
|
|
27
|
+
const issues = [];
|
|
28
|
+
// Check HttpOnly flag
|
|
29
|
+
if (!cookieLower.includes("httponly")) {
|
|
30
|
+
issues.push("Missing HttpOnly flag (vulnerable to XSS cookie theft)");
|
|
31
|
+
}
|
|
32
|
+
// Check Secure flag
|
|
33
|
+
if (!cookieLower.includes("secure")) {
|
|
34
|
+
issues.push("Missing Secure flag (cookie sent over HTTP)");
|
|
35
|
+
}
|
|
36
|
+
// Check SameSite attribute
|
|
37
|
+
if (!cookieLower.includes("samesite")) {
|
|
38
|
+
issues.push("Missing SameSite attribute (vulnerable to CSRF)");
|
|
39
|
+
}
|
|
40
|
+
else if (cookieLower.includes("samesite=none")) {
|
|
41
|
+
if (!cookieLower.includes("secure")) {
|
|
42
|
+
issues.push("SameSite=None without Secure flag (browser will reject)");
|
|
43
|
+
}
|
|
44
|
+
issues.push("SameSite=None allows cross-site requests (verify this is intentional)");
|
|
45
|
+
}
|
|
46
|
+
// Check for session-like cookies with missing flags
|
|
47
|
+
const isSessionCookie = /^(sess|session|sid|token|auth|jwt|csrf|xsrf|connect\.sid|phpsessid|jsessionid|asp\.net_sessionid)/i.test(cookieName);
|
|
48
|
+
if (issues.length > 0) {
|
|
49
|
+
const severity = isSessionCookie
|
|
50
|
+
? (issues.some(i => i.includes("HttpOnly")) ? "high" : "medium")
|
|
51
|
+
: "low";
|
|
52
|
+
this.reportedCookies.add(cookieKey);
|
|
53
|
+
vulnerabilities.push(this.createVulnerability(`Insecure Cookie: ${cookieName}`, `The cookie '${cookieName}' on ${host} has security issues:\n` +
|
|
54
|
+
issues.map(i => ` • ${i}`).join("\n"), context.url, severity, `Set-Cookie: ${cookie.substring(0, 200)}`, "Set all cookies with: HttpOnly (prevents JS access), " +
|
|
55
|
+
"Secure (HTTPS only), SameSite=Lax or Strict (CSRF protection). " +
|
|
56
|
+
"Example: Set-Cookie: session=abc; HttpOnly; Secure; SameSite=Lax; Path=/", "CWE-614"));
|
|
57
|
+
}
|
|
58
|
+
// Check for overly broad domain
|
|
59
|
+
const domainMatch = cookie.match(/domain=([^;]+)/i);
|
|
60
|
+
if (domainMatch) {
|
|
61
|
+
const domain = domainMatch[1].trim();
|
|
62
|
+
if (domain.startsWith(".") && domain.split(".").length <= 2) {
|
|
63
|
+
this.reportedCookies.add(cookieKey + ":domain");
|
|
64
|
+
vulnerabilities.push(this.createVulnerability(`Cookie Domain Too Broad: ${cookieName}`, `The cookie '${cookieName}' has domain set to '${domain}', which allows ` +
|
|
65
|
+
`any subdomain to read this cookie. This increases the attack surface.`, context.url, "low", `Domain=${domain}`, "Set the cookie domain to the most specific subdomain possible.", "CWE-1275"));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Check for missing expiry on session cookies (persistent session)
|
|
69
|
+
if (isSessionCookie && !cookieLower.includes("expires") && !cookieLower.includes("max-age")) {
|
|
70
|
+
// This is actually good practice (session cookie dies with browser close)
|
|
71
|
+
// But if there's _no_ expiry and it's NOT a session cookie, flag it
|
|
72
|
+
}
|
|
73
|
+
else if (isSessionCookie && cookieLower.includes("max-age")) {
|
|
74
|
+
const maxAgeMatch = cookie.match(/max-age=(\d+)/i);
|
|
75
|
+
if (maxAgeMatch) {
|
|
76
|
+
const maxAge = parseInt(maxAgeMatch[1], 10);
|
|
77
|
+
const thirtyDays = 30 * 24 * 60 * 60;
|
|
78
|
+
if (maxAge > thirtyDays) {
|
|
79
|
+
vulnerabilities.push(this.createVulnerability(`Long-Lived Session Cookie: ${cookieName}`, `The session cookie '${cookieName}' has a very long lifetime (${Math.round(maxAge / 86400)} days). ` +
|
|
80
|
+
`Long-lived session tokens increase the window for token theft.`, context.url, "low", `Max-Age=${maxAge} (${Math.round(maxAge / 86400)} days)`, "Set session cookie lifetime to the minimum needed (e.g., 24 hours for most apps).", "CWE-613"));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return vulnerabilities;
|
|
86
|
+
}
|
|
87
|
+
reset() {
|
|
88
|
+
this.reportedCookies.clear();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.CookieSecurityPlugin = CookieSecurityPlugin;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BaseVulnerabilityPlugin, PluginContext } from "../types";
|
|
2
|
+
import { Vulnerability } from "../../core/vulnerability-detector";
|
|
3
|
+
export declare class DebugEndpointPlugin extends BaseVulnerabilityPlugin {
|
|
4
|
+
readonly name = "Debug Endpoint Detector";
|
|
5
|
+
readonly type: "info";
|
|
6
|
+
readonly description = "Probes for common debug, admin, and development endpoints left exposed";
|
|
7
|
+
private readonly reportedPaths;
|
|
8
|
+
/**
|
|
9
|
+
* Common debug/dev endpoints that developers forget to disable before deployment.
|
|
10
|
+
* Each entry has a path, a human-readable name, and expected severity.
|
|
11
|
+
*/
|
|
12
|
+
private readonly debugEndpoints;
|
|
13
|
+
analyzeContent(context: PluginContext, content: string): Promise<Vulnerability[]>;
|
|
14
|
+
reset(): void;
|
|
15
|
+
}
|