@lateos/npm-scan 0.18.1 → 0.18.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/.dockerignore +20 -20
- package/.husky/pre-commit +1 -1
- package/CHANGELOG.md +233 -199
- package/LICENSING.md +19 -19
- package/README.de.md +708 -708
- package/README.fr.md +707 -707
- package/README.ja.md +704 -704
- package/README.md +826 -826
- package/README.zh.md +708 -708
- package/SECURITY.md +72 -72
- package/backend/cra.js +68 -68
- package/backend/db/schema.sql +32 -32
- package/backend/db.js +88 -88
- package/backend/detectors/atk-001-lifecycle.js +17 -17
- package/backend/detectors/atk-002-obfusc.js +261 -261
- package/backend/detectors/atk-003-creds.js +13 -13
- package/backend/detectors/atk-004-persist.js +13 -13
- package/backend/detectors/atk-005-exfil.js +13 -13
- package/backend/detectors/atk-006-depconf.js +14 -14
- package/backend/detectors/atk-007-typosquat.js +34 -34
- package/backend/detectors/atk-008-tarball-tamper.js +91 -91
- package/backend/detectors/atk-009-dormant-trigger.js +62 -62
- package/backend/detectors/atk-010-sandbox-evasion.js +50 -50
- package/backend/detectors/atk-011-transitive-prop.js +76 -76
- package/backend/detectors/cve-2026-48710-badhost/codePattern.js +99 -99
- package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -105
- package/backend/detectors/cve-2026-48710-badhost/index.js +15 -15
- package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -305
- package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -189
- package/backend/detectors/hf-impersonation/index.js +396 -396
- package/backend/detectors/hf-impersonation/jaro-winkler.js +44 -44
- package/backend/detectors/hf-impersonation/known-orgs.js +5 -5
- package/backend/detectors/hf-impersonation/simhash.js +46 -46
- package/backend/detectors/index.js +81 -75
- package/backend/detectors/megalodon/d1-workflow-scan.js +147 -147
- package/backend/detectors/megalodon/d2-credential-harvest.js +61 -61
- package/backend/detectors/megalodon/d3-publish-velocity.js +67 -67
- package/backend/detectors/megalodon/d4-publisher-drift.js +124 -124
- package/backend/detectors/megalodon/d5-bot-commit-identity.js +3 -3
- package/backend/detectors/megalodon/d6-date-anachronism.js +3 -3
- package/backend/detectors/megalodon/index.js +80 -80
- package/backend/detectors/megalodon/types.js +9 -9
- package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +42 -42
- package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +116 -116
- package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +72 -72
- package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +45 -45
- package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +95 -95
- package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +38 -38
- package/backend/detectors/mini-shai-hulud/index.js +118 -118
- package/backend/detectors/mini-shai-hulud/iocs.json +79 -79
- package/backend/detectors/tier1-cloud-imds.js +124 -0
- package/backend/detectors/tier1-infostealer.js +36 -0
- package/backend/detectors/tier1-multistage-postinstall.js +81 -0
- package/backend/detectors/tier1-version-confusion.js +107 -0
- package/backend/fetch.js +175 -175
- package/backend/index.js +4 -4
- package/backend/license.js +89 -89
- package/backend/lockfile.js +379 -379
- package/backend/pdf.js +245 -245
- package/backend/policy.js +193 -193
- package/backend/report.js +254 -254
- package/backend/sbom.js +66 -66
- package/backend/siem/cef.js +32 -32
- package/backend/siem/ecs.js +40 -40
- package/backend/siem/index.js +18 -18
- package/backend/siem/qradar.js +56 -56
- package/backend/siem/sentinel.js +27 -27
- package/backend/vsix-scan/detectors/activation-event-risk.js +116 -116
- package/backend/vsix-scan/detectors/burst-publish.js +52 -52
- package/backend/vsix-scan/detectors/exfil-pattern.js +88 -88
- package/backend/vsix-scan/detectors/known-ioc.js +105 -105
- package/backend/vsix-scan/detectors/orphan-commit-fetch.js +69 -69
- package/backend/vsix-scan/detectors/publisher-anomaly.js +70 -70
- package/backend/vsix-scan/index.js +183 -183
- package/backend/vsix-scan/marketplace-client.js +145 -145
- package/backend/vsix-scan/vsix-iocs.json +31 -31
- package/cli/cli.js +458 -458
- package/deploy/helm/npm-scan/Chart.yaml +21 -21
- package/deploy/helm/npm-scan/templates/_helpers.tpl +8 -8
- package/deploy/helm/npm-scan/templates/api.yaml +93 -93
- package/deploy/helm/npm-scan/templates/ingress.yaml +27 -27
- package/deploy/helm/npm-scan/templates/postgresql.yaml +66 -66
- package/deploy/helm/npm-scan/templates/secrets.yaml +18 -18
- package/deploy/helm/npm-scan/templates/worker.yaml +31 -31
- package/deploy/helm/npm-scan/values.byoc.yaml +74 -74
- package/deploy/helm/npm-scan/values.yaml +102 -102
- package/package.json +57 -57
- package/scripts/download-corpus.js +30 -30
- package/scripts/gen-mal-corpus.js +34 -34
- package/test/fixtures/lockfiles/npm-lock.json +68 -68
- package/test/fixtures/lockfiles/pnpm-lock.yaml +117 -117
- package/test/fixtures/lockfiles/yarn.lock +103 -103
- package/test/fixtures/mock-data.js +69 -69
|
@@ -1,99 +1,99 @@
|
|
|
1
|
-
import { codePatternAuthFinding, codePatternInfoFinding } from './findings.js';
|
|
2
|
-
|
|
3
|
-
const AUTH_CONTEXT_PATHS = [
|
|
4
|
-
'middleware',
|
|
5
|
-
'auth',
|
|
6
|
-
'security',
|
|
7
|
-
'router',
|
|
8
|
-
'depends',
|
|
9
|
-
'guard',
|
|
10
|
-
'permission',
|
|
11
|
-
];
|
|
12
|
-
|
|
13
|
-
const URL_PATH_PATTERN = /request\.url\.path|req\.url\.path|self\.request\.url\.path/g;
|
|
14
|
-
const SCOPE_PATH_PATTERN = /request\.scope\s*\[\s*["']path["']\s*\]|request\.scope\.get\s*\(\s*["']path["']\s*\)/g;
|
|
15
|
-
|
|
16
|
-
function hasAuthContext(filePath) {
|
|
17
|
-
const lower = filePath.toLowerCase();
|
|
18
|
-
return AUTH_CONTEXT_PATHS.some(ctx => lower.includes(ctx));
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function findFunctionBoundaries(lines) {
|
|
22
|
-
const functions = [];
|
|
23
|
-
let currentFn = null;
|
|
24
|
-
let fnBodyStart = -1;
|
|
25
|
-
let indent = 0;
|
|
26
|
-
|
|
27
|
-
for (let i = 0; i < lines.length; i++) {
|
|
28
|
-
const line = lines[i];
|
|
29
|
-
const defMatch = line.match(/^(def\s+\w+|async\s+def\s+\w+)/);
|
|
30
|
-
if (defMatch) {
|
|
31
|
-
if (currentFn) {
|
|
32
|
-
functions.push({ name: currentFn, startLine: fnBodyStart, endLine: i - 1 });
|
|
33
|
-
}
|
|
34
|
-
currentFn = defMatch[1];
|
|
35
|
-
fnBodyStart = i;
|
|
36
|
-
indent = line.length - line.trimStart().length;
|
|
37
|
-
} else if (currentFn && line.trim() && line.length - line.trimStart().length <= indent && !line.trim().startsWith('#') && !line.trim().startsWith('@')) {
|
|
38
|
-
functions.push({ name: currentFn, startLine: fnBodyStart, endLine: i - 1 });
|
|
39
|
-
currentFn = null;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
if (currentFn) {
|
|
43
|
-
functions.push({ name: currentFn, startLine: fnBodyStart, endLine: lines.length - 1 });
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return functions;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function hasScopePathInFunction(lines, fnStart, fnEnd) {
|
|
50
|
-
for (let i = fnStart; i <= fnEnd && i < lines.length; i++) {
|
|
51
|
-
if (SCOPE_PATH_PATTERN.test(lines[i])) return true;
|
|
52
|
-
}
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function scanCodePatterns(allFiles) {
|
|
57
|
-
const findings = [];
|
|
58
|
-
|
|
59
|
-
for (const file of (allFiles || [])) {
|
|
60
|
-
const content = typeof file.content === 'string' ? file.content : '';
|
|
61
|
-
if (!content) continue;
|
|
62
|
-
const path = file.path || '';
|
|
63
|
-
if (!path.endsWith('.py')) continue;
|
|
64
|
-
|
|
65
|
-
const lines = content.split('\n');
|
|
66
|
-
const isAuthContext = hasAuthContext(path);
|
|
67
|
-
const functions = findFunctionBoundaries(lines);
|
|
68
|
-
const suppressedLines = new Set();
|
|
69
|
-
|
|
70
|
-
for (const fn of functions) {
|
|
71
|
-
if (hasScopePathInFunction(lines, fn.startLine, fn.endLine)) {
|
|
72
|
-
for (let i = fn.startLine; i <= fn.endLine && i < lines.length; i++) {
|
|
73
|
-
if (URL_PATH_PATTERN.test(lines[i])) {
|
|
74
|
-
suppressedLines.add(i + 1);
|
|
75
|
-
}
|
|
76
|
-
URL_PATH_PATTERN.lastIndex = 0;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (isAuthContext) {
|
|
82
|
-
let m;
|
|
83
|
-
while ((m = URL_PATH_PATTERN.exec(content)) !== null) {
|
|
84
|
-
const lineNumber = content.slice(0, m.index).split('\n').length;
|
|
85
|
-
if (suppressedLines.has(lineNumber)) continue;
|
|
86
|
-
findings.push(codePatternAuthFinding(path, lineNumber));
|
|
87
|
-
}
|
|
88
|
-
} else {
|
|
89
|
-
let m;
|
|
90
|
-
while ((m = URL_PATH_PATTERN.exec(content)) !== null) {
|
|
91
|
-
const lineNumber = content.slice(0, m.index).split('\n').length;
|
|
92
|
-
if (suppressedLines.has(lineNumber)) continue;
|
|
93
|
-
findings.push(codePatternInfoFinding(path, lineNumber));
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return findings;
|
|
99
|
-
}
|
|
1
|
+
import { codePatternAuthFinding, codePatternInfoFinding } from './findings.js';
|
|
2
|
+
|
|
3
|
+
const AUTH_CONTEXT_PATHS = [
|
|
4
|
+
'middleware',
|
|
5
|
+
'auth',
|
|
6
|
+
'security',
|
|
7
|
+
'router',
|
|
8
|
+
'depends',
|
|
9
|
+
'guard',
|
|
10
|
+
'permission',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const URL_PATH_PATTERN = /request\.url\.path|req\.url\.path|self\.request\.url\.path/g;
|
|
14
|
+
const SCOPE_PATH_PATTERN = /request\.scope\s*\[\s*["']path["']\s*\]|request\.scope\.get\s*\(\s*["']path["']\s*\)/g;
|
|
15
|
+
|
|
16
|
+
function hasAuthContext(filePath) {
|
|
17
|
+
const lower = filePath.toLowerCase();
|
|
18
|
+
return AUTH_CONTEXT_PATHS.some(ctx => lower.includes(ctx));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function findFunctionBoundaries(lines) {
|
|
22
|
+
const functions = [];
|
|
23
|
+
let currentFn = null;
|
|
24
|
+
let fnBodyStart = -1;
|
|
25
|
+
let indent = 0;
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < lines.length; i++) {
|
|
28
|
+
const line = lines[i];
|
|
29
|
+
const defMatch = line.match(/^(def\s+\w+|async\s+def\s+\w+)/);
|
|
30
|
+
if (defMatch) {
|
|
31
|
+
if (currentFn) {
|
|
32
|
+
functions.push({ name: currentFn, startLine: fnBodyStart, endLine: i - 1 });
|
|
33
|
+
}
|
|
34
|
+
currentFn = defMatch[1];
|
|
35
|
+
fnBodyStart = i;
|
|
36
|
+
indent = line.length - line.trimStart().length;
|
|
37
|
+
} else if (currentFn && line.trim() && line.length - line.trimStart().length <= indent && !line.trim().startsWith('#') && !line.trim().startsWith('@')) {
|
|
38
|
+
functions.push({ name: currentFn, startLine: fnBodyStart, endLine: i - 1 });
|
|
39
|
+
currentFn = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (currentFn) {
|
|
43
|
+
functions.push({ name: currentFn, startLine: fnBodyStart, endLine: lines.length - 1 });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return functions;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function hasScopePathInFunction(lines, fnStart, fnEnd) {
|
|
50
|
+
for (let i = fnStart; i <= fnEnd && i < lines.length; i++) {
|
|
51
|
+
if (SCOPE_PATH_PATTERN.test(lines[i])) return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function scanCodePatterns(allFiles) {
|
|
57
|
+
const findings = [];
|
|
58
|
+
|
|
59
|
+
for (const file of (allFiles || [])) {
|
|
60
|
+
const content = typeof file.content === 'string' ? file.content : '';
|
|
61
|
+
if (!content) continue;
|
|
62
|
+
const path = file.path || '';
|
|
63
|
+
if (!path.endsWith('.py')) continue;
|
|
64
|
+
|
|
65
|
+
const lines = content.split('\n');
|
|
66
|
+
const isAuthContext = hasAuthContext(path);
|
|
67
|
+
const functions = findFunctionBoundaries(lines);
|
|
68
|
+
const suppressedLines = new Set();
|
|
69
|
+
|
|
70
|
+
for (const fn of functions) {
|
|
71
|
+
if (hasScopePathInFunction(lines, fn.startLine, fn.endLine)) {
|
|
72
|
+
for (let i = fn.startLine; i <= fn.endLine && i < lines.length; i++) {
|
|
73
|
+
if (URL_PATH_PATTERN.test(lines[i])) {
|
|
74
|
+
suppressedLines.add(i + 1);
|
|
75
|
+
}
|
|
76
|
+
URL_PATH_PATTERN.lastIndex = 0;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (isAuthContext) {
|
|
82
|
+
let m;
|
|
83
|
+
while ((m = URL_PATH_PATTERN.exec(content)) !== null) {
|
|
84
|
+
const lineNumber = content.slice(0, m.index).split('\n').length;
|
|
85
|
+
if (suppressedLines.has(lineNumber)) continue;
|
|
86
|
+
findings.push(codePatternAuthFinding(path, lineNumber));
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
let m;
|
|
90
|
+
while ((m = URL_PATH_PATTERN.exec(content)) !== null) {
|
|
91
|
+
const lineNumber = content.slice(0, m.index).split('\n').length;
|
|
92
|
+
if (suppressedLines.has(lineNumber)) continue;
|
|
93
|
+
findings.push(codePatternInfoFinding(path, lineNumber));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return findings;
|
|
99
|
+
}
|
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
const CVE = 'CVE-2026-48710';
|
|
2
|
-
const NICKNAME = 'BadHost';
|
|
3
|
-
const CVSS = 7.0;
|
|
4
|
-
const REFERENCES = [
|
|
5
|
-
'https://ostif.org/disclosing-the-badhost-vulnerability-in-starlette/',
|
|
6
|
-
'https://github.com/Kludex/starlette/security/advisories/GHSA-86qp-5c8j-p5mr',
|
|
7
|
-
'https://badhost.org/',
|
|
8
|
-
'https://osv.dev/vulnerability/PYSEC-2026-161',
|
|
9
|
-
];
|
|
10
|
-
|
|
11
|
-
const MITIGATION_NOTE = 'Partial mitigation: Cloudflare and AWS ALB reject malformed Host headers for properly proxied deployments. Direct uvicorn/hypercorn/daphne/granian exposure with no reverse proxy in front is highest risk.';
|
|
12
|
-
|
|
13
|
-
const DEPENDENCY_REMEDIATION = 'Upgrade starlette to >= 1.0.1. If starlette is inherited transitively through fastapi, vllm, litellm, or an MCP server package, upgrade the top-level package to a version that pins starlette >= 1.0.1. Verify with: pip show starlette.';
|
|
14
|
-
|
|
15
|
-
const CODE_REMEDIATION = 'Replace request.url.path with request.scope["path"] for all security-sensitive decisions (auth checks, path allowlists, rate limiting gates). The reconstructed request.url is influenced by the unvalidated Host header in Starlette < 1.0.1.';
|
|
16
|
-
|
|
17
|
-
function makeFinding(overrides = {}) {
|
|
18
|
-
return {
|
|
19
|
-
id: CVE,
|
|
20
|
-
severity: 'high',
|
|
21
|
-
title: `${NICKNAME} — ${overrides.source || 'unknown'}`,
|
|
22
|
-
description: '',
|
|
23
|
-
remediation: '',
|
|
24
|
-
evidence: JSON.stringify({
|
|
25
|
-
cve: CVE,
|
|
26
|
-
nickname: NICKNAME,
|
|
27
|
-
cvss: CVSS,
|
|
28
|
-
references: REFERENCES,
|
|
29
|
-
...overrides,
|
|
30
|
-
}),
|
|
31
|
-
...overrides,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function directDependencyFinding(version, specifier) {
|
|
36
|
-
return makeFinding({
|
|
37
|
-
severity: 'high',
|
|
38
|
-
confidence: 'HIGH',
|
|
39
|
-
source: 'direct-dependency',
|
|
40
|
-
title: `${NICKNAME}: Starlette ${version} vulnerable`,
|
|
41
|
-
description: `Starlette ${version} (${specifier}) is vulnerable to CVE-2026-48710 (BadHost) — authentication bypass via Host header injection. Upgrade to starlette >= 1.0.1.`,
|
|
42
|
-
remediation: `${DEPENDENCY_REMEDIATION} ${MITIGATION_NOTE}`,
|
|
43
|
-
file: null,
|
|
44
|
-
line: null,
|
|
45
|
-
via: null,
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function directDependencyUnpinnedFinding() {
|
|
50
|
-
return makeFinding({
|
|
51
|
-
severity: 'high',
|
|
52
|
-
confidence: 'HIGH',
|
|
53
|
-
source: 'direct-dependency-unpinned',
|
|
54
|
-
title: `${NICKNAME}: Starlette unpinned`,
|
|
55
|
-
description: 'Starlette is declared with no version constraint — assume vulnerable to CVE-2026-48710 (BadHost) until pinned to >= 1.0.1.',
|
|
56
|
-
remediation: `${DEPENDENCY_REMEDIATION} ${MITIGATION_NOTE}`,
|
|
57
|
-
file: null,
|
|
58
|
-
line: null,
|
|
59
|
-
via: null,
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function transitiveDependencyFinding(packageName, tier) {
|
|
64
|
-
const confidence = tier === 1 ? 'HIGH' : 'MEDIUM';
|
|
65
|
-
const tierLabel = tier === 1 ? 'Tier 1' : 'Tier 2';
|
|
66
|
-
return makeFinding({
|
|
67
|
-
severity: 'high',
|
|
68
|
-
confidence,
|
|
69
|
-
source: 'transitive-dependency',
|
|
70
|
-
title: `${NICKNAME}: Transitive via ${packageName}`,
|
|
71
|
-
description: `Starlette not directly pinned; inherited through ${packageName} (${tierLabel}). ${packageName} depends on Starlette — if its version constraint allows Starlette < 1.0.1, your deployment is vulnerable.`,
|
|
72
|
-
remediation: `${DEPENDENCY_REMEDIATION} ${MITIGATION_NOTE}`,
|
|
73
|
-
file: null,
|
|
74
|
-
line: null,
|
|
75
|
-
via: packageName,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function codePatternAuthFinding(filePath, lineNumber) {
|
|
80
|
-
return makeFinding({
|
|
81
|
-
severity: 'medium',
|
|
82
|
-
confidence: 'MEDIUM',
|
|
83
|
-
source: 'code-pattern',
|
|
84
|
-
title: `${NICKNAME}: Dangerous path extraction in auth/middleware`,
|
|
85
|
-
description: `request.url.path used in auth/middleware context at ${filePath}:${lineNumber} — use request.scope['path'] instead. The reconstructed request.url is influenced by the unvalidated Host header in Starlette < 1.0.1.`,
|
|
86
|
-
remediation: `${CODE_REMEDIATION} ${MITIGATION_NOTE}`,
|
|
87
|
-
file: filePath,
|
|
88
|
-
line: lineNumber,
|
|
89
|
-
via: null,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function codePatternInfoFinding(filePath, lineNumber) {
|
|
94
|
-
return makeFinding({
|
|
95
|
-
severity: 'info',
|
|
96
|
-
confidence: 'LOW',
|
|
97
|
-
source: 'code-pattern',
|
|
98
|
-
title: `${NICKNAME}: request.url.path usage detected`,
|
|
99
|
-
description: `request.url.path used at ${filePath}:${lineNumber} — may be influenced by unvalidated Host header in Starlette < 1.0.1. Verify request.scope['path'] is used for security decisions.`,
|
|
100
|
-
remediation: `${CODE_REMEDIATION} ${MITIGATION_NOTE}`,
|
|
101
|
-
file: filePath,
|
|
102
|
-
line: lineNumber,
|
|
103
|
-
via: null,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
1
|
+
const CVE = 'CVE-2026-48710';
|
|
2
|
+
const NICKNAME = 'BadHost';
|
|
3
|
+
const CVSS = 7.0;
|
|
4
|
+
const REFERENCES = [
|
|
5
|
+
'https://ostif.org/disclosing-the-badhost-vulnerability-in-starlette/',
|
|
6
|
+
'https://github.com/Kludex/starlette/security/advisories/GHSA-86qp-5c8j-p5mr',
|
|
7
|
+
'https://badhost.org/',
|
|
8
|
+
'https://osv.dev/vulnerability/PYSEC-2026-161',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const MITIGATION_NOTE = 'Partial mitigation: Cloudflare and AWS ALB reject malformed Host headers for properly proxied deployments. Direct uvicorn/hypercorn/daphne/granian exposure with no reverse proxy in front is highest risk.';
|
|
12
|
+
|
|
13
|
+
const DEPENDENCY_REMEDIATION = 'Upgrade starlette to >= 1.0.1. If starlette is inherited transitively through fastapi, vllm, litellm, or an MCP server package, upgrade the top-level package to a version that pins starlette >= 1.0.1. Verify with: pip show starlette.';
|
|
14
|
+
|
|
15
|
+
const CODE_REMEDIATION = 'Replace request.url.path with request.scope["path"] for all security-sensitive decisions (auth checks, path allowlists, rate limiting gates). The reconstructed request.url is influenced by the unvalidated Host header in Starlette < 1.0.1.';
|
|
16
|
+
|
|
17
|
+
function makeFinding(overrides = {}) {
|
|
18
|
+
return {
|
|
19
|
+
id: CVE,
|
|
20
|
+
severity: 'high',
|
|
21
|
+
title: `${NICKNAME} — ${overrides.source || 'unknown'}`,
|
|
22
|
+
description: '',
|
|
23
|
+
remediation: '',
|
|
24
|
+
evidence: JSON.stringify({
|
|
25
|
+
cve: CVE,
|
|
26
|
+
nickname: NICKNAME,
|
|
27
|
+
cvss: CVSS,
|
|
28
|
+
references: REFERENCES,
|
|
29
|
+
...overrides,
|
|
30
|
+
}),
|
|
31
|
+
...overrides,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function directDependencyFinding(version, specifier) {
|
|
36
|
+
return makeFinding({
|
|
37
|
+
severity: 'high',
|
|
38
|
+
confidence: 'HIGH',
|
|
39
|
+
source: 'direct-dependency',
|
|
40
|
+
title: `${NICKNAME}: Starlette ${version} vulnerable`,
|
|
41
|
+
description: `Starlette ${version} (${specifier}) is vulnerable to CVE-2026-48710 (BadHost) — authentication bypass via Host header injection. Upgrade to starlette >= 1.0.1.`,
|
|
42
|
+
remediation: `${DEPENDENCY_REMEDIATION} ${MITIGATION_NOTE}`,
|
|
43
|
+
file: null,
|
|
44
|
+
line: null,
|
|
45
|
+
via: null,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function directDependencyUnpinnedFinding() {
|
|
50
|
+
return makeFinding({
|
|
51
|
+
severity: 'high',
|
|
52
|
+
confidence: 'HIGH',
|
|
53
|
+
source: 'direct-dependency-unpinned',
|
|
54
|
+
title: `${NICKNAME}: Starlette unpinned`,
|
|
55
|
+
description: 'Starlette is declared with no version constraint — assume vulnerable to CVE-2026-48710 (BadHost) until pinned to >= 1.0.1.',
|
|
56
|
+
remediation: `${DEPENDENCY_REMEDIATION} ${MITIGATION_NOTE}`,
|
|
57
|
+
file: null,
|
|
58
|
+
line: null,
|
|
59
|
+
via: null,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function transitiveDependencyFinding(packageName, tier) {
|
|
64
|
+
const confidence = tier === 1 ? 'HIGH' : 'MEDIUM';
|
|
65
|
+
const tierLabel = tier === 1 ? 'Tier 1' : 'Tier 2';
|
|
66
|
+
return makeFinding({
|
|
67
|
+
severity: 'high',
|
|
68
|
+
confidence,
|
|
69
|
+
source: 'transitive-dependency',
|
|
70
|
+
title: `${NICKNAME}: Transitive via ${packageName}`,
|
|
71
|
+
description: `Starlette not directly pinned; inherited through ${packageName} (${tierLabel}). ${packageName} depends on Starlette — if its version constraint allows Starlette < 1.0.1, your deployment is vulnerable.`,
|
|
72
|
+
remediation: `${DEPENDENCY_REMEDIATION} ${MITIGATION_NOTE}`,
|
|
73
|
+
file: null,
|
|
74
|
+
line: null,
|
|
75
|
+
via: packageName,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function codePatternAuthFinding(filePath, lineNumber) {
|
|
80
|
+
return makeFinding({
|
|
81
|
+
severity: 'medium',
|
|
82
|
+
confidence: 'MEDIUM',
|
|
83
|
+
source: 'code-pattern',
|
|
84
|
+
title: `${NICKNAME}: Dangerous path extraction in auth/middleware`,
|
|
85
|
+
description: `request.url.path used in auth/middleware context at ${filePath}:${lineNumber} — use request.scope['path'] instead. The reconstructed request.url is influenced by the unvalidated Host header in Starlette < 1.0.1.`,
|
|
86
|
+
remediation: `${CODE_REMEDIATION} ${MITIGATION_NOTE}`,
|
|
87
|
+
file: filePath,
|
|
88
|
+
line: lineNumber,
|
|
89
|
+
via: null,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function codePatternInfoFinding(filePath, lineNumber) {
|
|
94
|
+
return makeFinding({
|
|
95
|
+
severity: 'info',
|
|
96
|
+
confidence: 'LOW',
|
|
97
|
+
source: 'code-pattern',
|
|
98
|
+
title: `${NICKNAME}: request.url.path usage detected`,
|
|
99
|
+
description: `request.url.path used at ${filePath}:${lineNumber} — may be influenced by unvalidated Host header in Starlette < 1.0.1. Verify request.scope['path'] is used for security decisions.`,
|
|
100
|
+
remediation: `${CODE_REMEDIATION} ${MITIGATION_NOTE}`,
|
|
101
|
+
file: filePath,
|
|
102
|
+
line: lineNumber,
|
|
103
|
+
via: null,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { scanFiles } from './manifest.js';
|
|
2
|
-
import { scanTransitive } from './transitive.js';
|
|
3
|
-
import { scanCodePatterns } from './codePattern.js';
|
|
4
|
-
|
|
5
|
-
export async function scan(pkgJson, files = [], registryMeta = null, allFiles = null) {
|
|
6
|
-
const targetFiles = allFiles || files;
|
|
7
|
-
|
|
8
|
-
const manifestFindings = scanFiles(targetFiles);
|
|
9
|
-
const transitiveFindings = scanTransitive(targetFiles);
|
|
10
|
-
const codeFindings = scanCodePatterns(targetFiles);
|
|
11
|
-
|
|
12
|
-
const allFindings = [...manifestFindings, ...transitiveFindings, ...codeFindings];
|
|
13
|
-
|
|
14
|
-
return allFindings;
|
|
15
|
-
}
|
|
1
|
+
import { scanFiles } from './manifest.js';
|
|
2
|
+
import { scanTransitive } from './transitive.js';
|
|
3
|
+
import { scanCodePatterns } from './codePattern.js';
|
|
4
|
+
|
|
5
|
+
export async function scan(pkgJson, files = [], registryMeta = null, allFiles = null) {
|
|
6
|
+
const targetFiles = allFiles || files;
|
|
7
|
+
|
|
8
|
+
const manifestFindings = scanFiles(targetFiles);
|
|
9
|
+
const transitiveFindings = scanTransitive(targetFiles);
|
|
10
|
+
const codeFindings = scanCodePatterns(targetFiles);
|
|
11
|
+
|
|
12
|
+
const allFindings = [...manifestFindings, ...transitiveFindings, ...codeFindings];
|
|
13
|
+
|
|
14
|
+
return allFindings;
|
|
15
|
+
}
|