@clear-capabilities/agentic-security-scanner 0.74.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/CHANGELOG.md +1580 -0
- package/bin/.agentic-security/findings.json +1577 -0
- package/bin/.agentic-security/last-scan.json +1577 -0
- package/bin/.agentic-security/last-scan.json.sig +1 -0
- package/bin/.agentic-security/scan-history.json +465 -0
- package/bin/.agentic-security/streak.json +25 -0
- package/bin/agentic-security-audit.js +198 -0
- package/bin/agentic-security-consistency.js +80 -0
- package/bin/agentic-security-diff.js +136 -0
- package/bin/agentic-security-lsp.js +12 -0
- package/bin/agentic-security-mcp.js +40 -0
- package/bin/agentic-security-rule.js +153 -0
- package/bin/agentic-security.js +1683 -0
- package/dist/117.index.js +207 -0
- package/dist/178.index.js +250 -0
- package/dist/218.index.js +793 -0
- package/dist/227.index.js +192 -0
- package/dist/301.index.js +167 -0
- package/dist/384.index.js +18 -0
- package/dist/476.index.js +126 -0
- package/dist/513.index.js +373 -0
- package/dist/520.index.js +13 -0
- package/dist/601.index.js +1038 -0
- package/dist/634.index.js +1892 -0
- package/dist/637.index.js +216 -0
- package/dist/660.index.js +131 -0
- package/dist/675.index.js +451 -0
- package/dist/826.index.js +188 -0
- package/dist/830.index.js +133 -0
- package/dist/agentic-security.mjs +272 -0
- package/dist/agentic-security.mjs.sha256 +1 -0
- package/dist/calibration-seed.json +27 -0
- package/package.json +77 -0
- package/src/.agentic-security/findings.json +80844 -0
- package/src/.agentic-security/last-scan.json +80844 -0
- package/src/.agentic-security/last-scan.json.sig +1 -0
- package/src/.agentic-security/scan-history.json +8408 -0
- package/src/.agentic-security/streak.json +26 -0
- package/src/badge.js +188 -0
- package/src/compare.js +203 -0
- package/src/dataflow/.agentic-security/findings.json +3487 -0
- package/src/dataflow/.agentic-security/last-scan.json +3487 -0
- package/src/dataflow/.agentic-security/last-scan.json.sig +1 -0
- package/src/dataflow/.agentic-security/scan-history.json +735 -0
- package/src/dataflow/.agentic-security/streak.json +24 -0
- package/src/dataflow/CLAUDE.md +38 -0
- package/src/dataflow/access-paths.js +172 -0
- package/src/dataflow/async-sequencing.js +177 -0
- package/src/dataflow/backward.js +201 -0
- package/src/dataflow/catalog-expanded.js +485 -0
- package/src/dataflow/catalog.js +659 -0
- package/src/dataflow/cross-repo.js +219 -0
- package/src/dataflow/engine.js +588 -0
- package/src/dataflow/exception-flow.js +116 -0
- package/src/dataflow/exploit-prover.js +187 -0
- package/src/dataflow/higher-order.js +221 -0
- package/src/dataflow/ifds.js +347 -0
- package/src/dataflow/implicit-flow.js +129 -0
- package/src/dataflow/incremental.js +229 -0
- package/src/dataflow/index.js +181 -0
- package/src/dataflow/numeric-domain.js +192 -0
- package/src/dataflow/path-feasibility.js +114 -0
- package/src/dataflow/points-to.js +337 -0
- package/src/dataflow/polyglot.js +190 -0
- package/src/dataflow/proven-clean.js +159 -0
- package/src/dataflow/receiver-context.js +76 -0
- package/src/dataflow/sanitizer-proof.js +154 -0
- package/src/dataflow/soft-taint.js +140 -0
- package/src/dataflow/string-domain.js +234 -0
- package/src/dataflow/stub-aware-filter.js +100 -0
- package/src/dataflow/summaries.js +132 -0
- package/src/dataflow/symbolic-exec.js +238 -0
- package/src/dataflow/tabulation.js +135 -0
- package/src/engine.js +7763 -0
- package/src/history-scan.js +229 -0
- package/src/index.js +3 -0
- package/src/integrations/.agentic-security/findings.json +1504 -0
- package/src/integrations/.agentic-security/last-scan.json +1504 -0
- package/src/integrations/.agentic-security/scan-history.json +40 -0
- package/src/integrations/.agentic-security/streak.json +21 -0
- package/src/integrations/index.js +321 -0
- package/src/integrations/tickets.js +200 -0
- package/src/ir/.agentic-security/findings.json +3036 -0
- package/src/ir/.agentic-security/last-scan.json +3036 -0
- package/src/ir/.agentic-security/last-scan.json.sig +1 -0
- package/src/ir/.agentic-security/scan-history.json +364 -0
- package/src/ir/.agentic-security/streak.json +23 -0
- package/src/ir/CLAUDE.md +172 -0
- package/src/ir/callgraph.js +73 -0
- package/src/ir/class-hierarchy.js +195 -0
- package/src/ir/index.js +152 -0
- package/src/ir/parser-cs.js +260 -0
- package/src/ir/parser-java.js +286 -0
- package/src/ir/parser-js.js +413 -0
- package/src/ir/parser-kt.js +258 -0
- package/src/ir/parser-py-cst.js +136 -0
- package/src/ir/parser-py.helper.py +501 -0
- package/src/ir/parser-py.js +312 -0
- package/src/ir/ssa.js +315 -0
- package/src/ir/type-stubs.js +288 -0
- package/src/leaderboard.js +152 -0
- package/src/llm-validator/.agentic-security/findings.json +1891 -0
- package/src/llm-validator/.agentic-security/last-scan.json +1891 -0
- package/src/llm-validator/.agentic-security/last-scan.json.sig +1 -0
- package/src/llm-validator/.agentic-security/scan-history.json +168 -0
- package/src/llm-validator/.agentic-security/streak.json +20 -0
- package/src/llm-validator/consistency.js +141 -0
- package/src/llm-validator/index.js +437 -0
- package/src/lsp/.agentic-security/findings.json +28 -0
- package/src/lsp/.agentic-security/last-scan.json +28 -0
- package/src/lsp/.agentic-security/scan-history.json +79 -0
- package/src/lsp/.agentic-security/streak.json +22 -0
- package/src/lsp/server.js +275 -0
- package/src/mcp/.agentic-security/findings.json +8358 -0
- package/src/mcp/.agentic-security/last-scan.json +8358 -0
- package/src/mcp/.agentic-security/last-scan.json.sig +1 -0
- package/src/mcp/.agentic-security/scan-history.json +1125 -0
- package/src/mcp/.agentic-security/streak.json +22 -0
- package/src/mcp/CLAUDE.md +54 -0
- package/src/mcp/audit.js +136 -0
- package/src/mcp/redact.js +75 -0
- package/src/mcp/server.js +158 -0
- package/src/mcp/stdio.js +83 -0
- package/src/mcp/tools.js +940 -0
- package/src/mcp/validate.js +49 -0
- package/src/personality.js +164 -0
- package/src/poc-video.js +239 -0
- package/src/posture/.agentic-security/findings.json +51239 -0
- package/src/posture/.agentic-security/last-scan.json +51239 -0
- package/src/posture/.agentic-security/last-scan.json.sig +1 -0
- package/src/posture/.agentic-security/scan-history.json +5557 -0
- package/src/posture/.agentic-security/streak.json +24 -0
- package/src/posture/CLAUDE.md +42 -0
- package/src/posture/adversarial-self-test.js +114 -0
- package/src/posture/adversary-agent.js +204 -0
- package/src/posture/agents-memory.js +135 -0
- package/src/posture/ai-code-fingerprint.js +171 -0
- package/src/posture/aibom.js +284 -0
- package/src/posture/api-inventory.js +96 -0
- package/src/posture/attack-playbooks.js +305 -0
- package/src/posture/auditor-agent.js +115 -0
- package/src/posture/auth-posture-import.js +135 -0
- package/src/posture/baseline-compare.js +114 -0
- package/src/posture/blast-radius.js +836 -0
- package/src/posture/bounty-prediction.js +141 -0
- package/src/posture/business-logic.js +239 -0
- package/src/posture/calibration-drift.js +93 -0
- package/src/posture/calibration-seed.json +27 -0
- package/src/posture/calibration.js +204 -0
- package/src/posture/clustering.js +75 -0
- package/src/posture/concurrency-checker.js +265 -0
- package/src/posture/confidence.js +65 -0
- package/src/posture/container-runtime.js +149 -0
- package/src/posture/counterfactual.js +109 -0
- package/src/posture/cross-lang-graphql.js +165 -0
- package/src/posture/cross-lang-grpc.js +166 -0
- package/src/posture/cross-lang-meta.js +101 -0
- package/src/posture/cross-lang-openapi.js +187 -0
- package/src/posture/cross-lang-orm.js +153 -0
- package/src/posture/cross-lang-queues.js +210 -0
- package/src/posture/crown-jewels.js +110 -0
- package/src/posture/custom-rules.js +361 -0
- package/src/posture/cve-alert-daemon.js +433 -0
- package/src/posture/cve-lookup.js +129 -0
- package/src/posture/dead-code.js +430 -0
- package/src/posture/defender-agent.js +158 -0
- package/src/posture/deploy-platform.js +204 -0
- package/src/posture/detector-fuzz.js +61 -0
- package/src/posture/deterministic.js +99 -0
- package/src/posture/drift.js +165 -0
- package/src/posture/epss.js +156 -0
- package/src/posture/exploitability-probability.js +212 -0
- package/src/posture/exploitability.js +121 -0
- package/src/posture/feature-flags.js +110 -0
- package/src/posture/finding-defaults.js +132 -0
- package/src/posture/fix-history.js +411 -0
- package/src/posture/fix-plan.js +121 -0
- package/src/posture/fix-verify-loop.js +157 -0
- package/src/posture/fix-verify.js +130 -0
- package/src/posture/flow-narration.js +105 -0
- package/src/posture/grader-calibration.js +156 -0
- package/src/posture/harness-discovery.js +113 -0
- package/src/posture/holdout-eval.js +144 -0
- package/src/posture/iac-reachability.js +163 -0
- package/src/posture/iam-policy.js +128 -0
- package/src/posture/integrity.js +97 -0
- package/src/posture/learning.js +166 -0
- package/src/posture/license-policy.js +109 -0
- package/src/posture/llm-redteam-prompts.js +418 -0
- package/src/posture/llm-redteam.js +303 -0
- package/src/posture/material-change.js +163 -0
- package/src/posture/mitigation-composite.js +55 -0
- package/src/posture/mttr.js +91 -0
- package/src/posture/network-policy-import.js +126 -0
- package/src/posture/path-predicates.js +99 -0
- package/src/posture/persona-prioritization.js +153 -0
- package/src/posture/poc-cwe-map.js +51 -0
- package/src/posture/poc-generator.js +500 -0
- package/src/posture/policy-gate.js +174 -0
- package/src/posture/pre-incident-archaeology.js +110 -0
- package/src/posture/profile.js +93 -0
- package/src/posture/reachability-filter.js +42 -0
- package/src/posture/regression-test-gen.js +200 -0
- package/src/posture/reverse-blast-radius.js +110 -0
- package/src/posture/router.js +109 -0
- package/src/posture/rule-overrides.js +198 -0
- package/src/posture/rule-pack-signing.js +209 -0
- package/src/posture/rule-packs.js +143 -0
- package/src/posture/rule-synthesis.js +108 -0
- package/src/posture/ruleset-version.js +71 -0
- package/src/posture/sbom.js +129 -0
- package/src/posture/schema-aware-bridge.js +207 -0
- package/src/posture/security-trend.js +87 -0
- package/src/posture/semantic-clone.js +114 -0
- package/src/posture/specification-mining.js +170 -0
- package/src/posture/stable-id.js +75 -0
- package/src/posture/stack-playbook.js +229 -0
- package/src/posture/streak.js +249 -0
- package/src/posture/suppressions.js +135 -0
- package/src/posture/telemetry-ingest.js +112 -0
- package/src/posture/threat-model.js +145 -0
- package/src/posture/three-agent-pipeline.js +74 -0
- package/src/posture/triage.js +146 -0
- package/src/posture/trust-boundary-diagram.js +115 -0
- package/src/posture/type-narrowing.js +129 -0
- package/src/posture/validator-metrics.js +179 -0
- package/src/posture/verifier-ephemeral.js +118 -0
- package/src/posture/verifier-target.js +147 -0
- package/src/posture/verifier.js +257 -0
- package/src/posture/version.js +75 -0
- package/src/posture/waf-ingest.js +200 -0
- package/src/posture/why-fired.js +141 -0
- package/src/pr-comment.js +172 -0
- package/src/pr-delta.js +198 -0
- package/src/report/.agentic-security/findings.json +79 -0
- package/src/report/.agentic-security/last-scan.json +79 -0
- package/src/report/.agentic-security/last-scan.json.sig +1 -0
- package/src/report/.agentic-security/scan-history.json +332 -0
- package/src/report/.agentic-security/streak.json +23 -0
- package/src/report/index.js +1136 -0
- package/src/report/mascot.js +42 -0
- package/src/runScan.js +141 -0
- package/src/sast/.agentic-security/findings.json +5051 -0
- package/src/sast/.agentic-security/last-scan.json +5051 -0
- package/src/sast/.agentic-security/last-scan.json.sig +1 -0
- package/src/sast/.agentic-security/scan-history.json +788 -0
- package/src/sast/.agentic-security/streak.json +23 -0
- package/src/sast/CLAUDE.md +39 -0
- package/src/sast/_comment-strip.js +46 -0
- package/src/sast/agent-tool-escalation.js +131 -0
- package/src/sast/auth-provider.js +171 -0
- package/src/sast/authz.js +236 -0
- package/src/sast/bench-shape/.agentic-security/findings.json +28 -0
- package/src/sast/bench-shape/.agentic-security/last-scan.json +28 -0
- package/src/sast/bench-shape/.agentic-security/scan-history.json +24 -0
- package/src/sast/bench-shape/.agentic-security/streak.json +22 -0
- package/src/sast/bench-shape/index.js +62 -0
- package/src/sast/claude-hook-injection.js +199 -0
- package/src/sast/claude-md-prompt-injection.js +170 -0
- package/src/sast/claude-settings.js +165 -0
- package/src/sast/client-side.js +149 -0
- package/src/sast/cpp-bench-extras.js +122 -0
- package/src/sast/cpp-dataflow.js +430 -0
- package/src/sast/cpp.js +248 -0
- package/src/sast/csharp.js +152 -0
- package/src/sast/csrf.js +82 -0
- package/src/sast/dart-flutter.js +173 -0
- package/src/sast/db-rls.js +147 -0
- package/src/sast/db-taint.js +215 -0
- package/src/sast/defi-deep.js +242 -0
- package/src/sast/deserialization-gadgets.js +113 -0
- package/src/sast/django-hardening.js +230 -0
- package/src/sast/env-hygiene.js +125 -0
- package/src/sast/fastapi-hardening.js +145 -0
- package/src/sast/go-extended.js +84 -0
- package/src/sast/host-header.js +106 -0
- package/src/sast/index.js +17 -0
- package/src/sast/java-ast-folding.js +561 -0
- package/src/sast/java-bench-extras.js +708 -0
- package/src/sast/java-collection-passthrough.js +178 -0
- package/src/sast/java-constant-fold.js +244 -0
- package/src/sast/java-deserialization.js +125 -0
- package/src/sast/jndi.js +104 -0
- package/src/sast/juliet-shape.js +324 -0
- package/src/sast/jwt-exp.js +104 -0
- package/src/sast/kotlin.js +82 -0
- package/src/sast/laravel-hardening.js +198 -0
- package/src/sast/ldap-injection.js +100 -0
- package/src/sast/llm-owasp.js +465 -0
- package/src/sast/llm-stored-prompt.js +103 -0
- package/src/sast/llm-trading-agent.js +161 -0
- package/src/sast/llm.js +308 -0
- package/src/sast/logic.js +140 -0
- package/src/sast/mass-assignment.js +101 -0
- package/src/sast/mcp-audit.js +242 -0
- package/src/sast/mobile-manifest.js +195 -0
- package/src/sast/model-load.js +164 -0
- package/src/sast/mutation-xss.js +87 -0
- package/src/sast/nosql-injection.js +82 -0
- package/src/sast/open-redirect.js +119 -0
- package/src/sast/php.js +91 -0
- package/src/sast/pipeline.js +122 -0
- package/src/sast/primary-cwe-java.js +155 -0
- package/src/sast/prompt-firewall.js +151 -0
- package/src/sast/prompt-template.js +157 -0
- package/src/sast/prototype-pollution.js +112 -0
- package/src/sast/python-sinks.js +195 -0
- package/src/sast/quarkus-hardening.js +102 -0
- package/src/sast/rag-poisoning.js +118 -0
- package/src/sast/rate-limit.js +128 -0
- package/src/sast/response-splitting.js +138 -0
- package/src/sast/ruby.js +108 -0
- package/src/sast/rust.js +105 -0
- package/src/sast/solidity.js +167 -0
- package/src/sast/springboot-hardening.js +186 -0
- package/src/sast/ssrf-cloud-metadata.js +80 -0
- package/src/sast/ssti.js +116 -0
- package/src/sast/swift.js +162 -0
- package/src/sast/toctou.js +95 -0
- package/src/sast/webhook.js +101 -0
- package/src/sast/xpath-injection.js +51 -0
- package/src/sast/xxe.js +140 -0
- package/src/sast/zip-slip.js +200 -0
- package/src/sca/base-images.json +45 -0
- package/src/sca/container.js +107 -0
- package/src/sca/dep-confusion.js +134 -0
- package/src/sca/index.js +6 -0
- package/src/sca/popular-packages.json +41 -0
- package/src/sca/sarif-ingest.js +187 -0
- package/src/sca/vuln-function-hints.json +89 -0
- package/src/secrets/index.js +4 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Environment hygiene checker.
|
|
2
|
+
//
|
|
3
|
+
// Vibecoders leak secrets through env files: .env committed to git,
|
|
4
|
+
// NEXT_PUBLIC_ vars that expose private values, .env.example with real
|
|
5
|
+
// credentials, and hardcoded fallback values that look real.
|
|
6
|
+
//
|
|
7
|
+
// Findings:
|
|
8
|
+
// ENV_NEXT_PUBLIC_SECRET — NEXT_PUBLIC_ variable whose name implies it's secret
|
|
9
|
+
// ENV_EXAMPLE_REAL_VALUE — .env.example / .env.sample with a real-looking value
|
|
10
|
+
// ENV_HARDCODED_FALLBACK — process.env.X || "looks-real" fallback in source
|
|
11
|
+
// ENV_MISSING_GITIGNORE — .env / .env.local present but not in .gitignore
|
|
12
|
+
// ENV_DOTENV_IN_SOURCE — .env file content loaded via require/import in prod code
|
|
13
|
+
|
|
14
|
+
const _ENV_FILE_RE = /^\.env(?:\.(?:local|development|production|test|staging))?$/i;
|
|
15
|
+
const _NONPROD_RE = /(?:^|\/)(?:tests?|__tests__|spec|fixtures?|examples?|node_modules)\//i;
|
|
16
|
+
|
|
17
|
+
// NEXT_PUBLIC_ variables whose name implies they are secrets
|
|
18
|
+
const NEXT_PUBLIC_SENSITIVE_RE = /NEXT_PUBLIC_\w*(?:SECRET|KEY|TOKEN|PASSWORD|PASS|CREDENTIAL|API_KEY|PRIVATE|SIGNING|WEBHOOK|SALT|SEED)\w*/gi;
|
|
19
|
+
|
|
20
|
+
// .env.example / .env.sample with non-placeholder values
|
|
21
|
+
// A "real" value is: not empty, not "xxx...", not "your_...", not "<...>", not "changeme", not "placeholder"
|
|
22
|
+
const ENV_EXAMPLE_RE = /^\.env\.(?:example|sample|template)$/i;
|
|
23
|
+
const PLACEHOLDER_RE = /^(?:|xxx+|your[_-]|<[^>]+>|changeme|change[_-]?me|placeholder|todo|fixme|replace|dummy|fake|test|example|sample|n\/a|none|null|undefined|\$\{[^}]+\})$/i;
|
|
24
|
+
const REAL_VALUE_RE = /^[a-zA-Z0-9+/=_\-]{8,}$/; // looks like an actual token/key
|
|
25
|
+
|
|
26
|
+
// process.env.X || "fallback" where fallback looks real (not empty string, not "localhost", not "3000")
|
|
27
|
+
const HARDCODED_FALLBACK_RE = /process\.env\.(\w+)\s*\|\|\s*['"`]([^'"`]{4,})['"`]/g;
|
|
28
|
+
const BENIGN_FALLBACK_RE = /^(?:localhost|127\.0\.0\.1|0\.0\.0\.0|3000|8080|8000|production|development|test|info|debug|warn|error|true|false|\/|\.|\s*)$/i;
|
|
29
|
+
|
|
30
|
+
// require('dotenv') / import dotenv in non-config files
|
|
31
|
+
const DOTENV_IMPORT_RE = /(?:from|require)\s*\(?\s*['"`]dotenv['"`]/;
|
|
32
|
+
const CONFIG_FILE_RE = /(?:^|\/)(?:config|env|\.env|setup|bootstrap|init|app)\.[cm]?[jt]s$/i;
|
|
33
|
+
|
|
34
|
+
function _lineNumber(content, index) {
|
|
35
|
+
return content.slice(0, index).split('\n').length;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function scanEnvHygiene(file, content) {
|
|
39
|
+
if (_NONPROD_RE.test(file)) return [];
|
|
40
|
+
const findings = [];
|
|
41
|
+
const basename = file.split('/').pop();
|
|
42
|
+
const isEnvExample = ENV_EXAMPLE_RE.test(basename);
|
|
43
|
+
const isEnvFile = _ENV_FILE_RE.test(basename);
|
|
44
|
+
const isJsTs = /\.(?:js|jsx|ts|tsx|mjs|cjs)$/i.test(file);
|
|
45
|
+
|
|
46
|
+
// --- NEXT_PUBLIC_ with sensitive name ---
|
|
47
|
+
if (isEnvFile || isEnvExample || isJsTs) {
|
|
48
|
+
const matches = [...content.matchAll(new RegExp(NEXT_PUBLIC_SENSITIVE_RE.source, 'gi'))];
|
|
49
|
+
for (const m of matches) {
|
|
50
|
+
const lineNum = _lineNumber(content, m.index);
|
|
51
|
+
findings.push({
|
|
52
|
+
id: `env-hygiene:ENV_NEXT_PUBLIC_SECRET:${file}:${lineNum}`,
|
|
53
|
+
title: `NEXT_PUBLIC_ variable exposes a sensitive value client-side: ${m[0]}`,
|
|
54
|
+
severity: 'critical',
|
|
55
|
+
file, line: lineNum,
|
|
56
|
+
description: `${m[0]} is prefixed with NEXT_PUBLIC_, which means Next.js will bundle its value into the client-side JavaScript. Any visitor can read it from the page source. Variables named SECRET, KEY, TOKEN, or similar should never be public.`,
|
|
57
|
+
remediation: 'Remove the NEXT_PUBLIC_ prefix. Access this value only in Server Components, API Routes, or Server Actions where it stays on the server.',
|
|
58
|
+
cwe: 'CWE-522',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// --- .env.example with real values ---
|
|
64
|
+
if (isEnvExample) {
|
|
65
|
+
const lines = content.split('\n');
|
|
66
|
+
for (let i = 0; i < lines.length; i++) {
|
|
67
|
+
const line = lines[i].trim();
|
|
68
|
+
if (!line || line.startsWith('#')) continue;
|
|
69
|
+
const eqIdx = line.indexOf('=');
|
|
70
|
+
if (eqIdx === -1) continue;
|
|
71
|
+
const value = line.slice(eqIdx + 1).trim().replace(/^['"`]|['"`]$/g, '');
|
|
72
|
+
if (value && !PLACEHOLDER_RE.test(value) && REAL_VALUE_RE.test(value)) {
|
|
73
|
+
findings.push({
|
|
74
|
+
id: `env-hygiene:ENV_EXAMPLE_REAL_VALUE:${file}:${i + 1}`,
|
|
75
|
+
title: '.env.example contains a real-looking credential value',
|
|
76
|
+
severity: 'high',
|
|
77
|
+
file, line: i + 1,
|
|
78
|
+
description: `Line ${i + 1} in ${basename} has a value that looks like a real secret rather than a placeholder. If this is an actual credential, it is now permanently in git history.`,
|
|
79
|
+
remediation: 'Replace real values with descriptive placeholders: `API_KEY=your_api_key_here` or `DATABASE_URL=postgres://user:pass@host/db`. Rotate the leaked credential immediately.',
|
|
80
|
+
cwe: 'CWE-798',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --- process.env.X || "real-fallback" in source ---
|
|
87
|
+
if (isJsTs) {
|
|
88
|
+
let m;
|
|
89
|
+
const re = new RegExp(HARDCODED_FALLBACK_RE.source, 'g');
|
|
90
|
+
while ((m = re.exec(content)) !== null) {
|
|
91
|
+
const [, varName, fallback] = m;
|
|
92
|
+
if (BENIGN_FALLBACK_RE.test(fallback)) continue;
|
|
93
|
+
// Skip if the variable name is obviously benign
|
|
94
|
+
if (/^(?:NODE_ENV|PORT|HOST|LOG_LEVEL|TIMEOUT|DEBUG|NEXT_PUBLIC_APP_URL|APP_URL|BASE_URL|PUBLIC_URL)\b/.test(varName)) continue;
|
|
95
|
+
const lineNum = _lineNumber(content, m.index);
|
|
96
|
+
findings.push({
|
|
97
|
+
id: `env-hygiene:ENV_HARDCODED_FALLBACK:${file}:${lineNum}`,
|
|
98
|
+
title: `Hardcoded fallback for ${varName} looks like a real credential`,
|
|
99
|
+
severity: 'high',
|
|
100
|
+
file, line: lineNum,
|
|
101
|
+
description: `process.env.${varName} falls back to "${fallback.slice(0, 20)}${fallback.length > 20 ? '...' : ''}" when the env var is unset. If this value is a real secret, it is committed to source and will be used silently when the env var is misconfigured in production.`,
|
|
102
|
+
remediation: `Remove the fallback: throw an error at startup if ${varName} is unset. Use a config validation library like zod or @t3-oss/env-nextjs to enforce required env vars.`,
|
|
103
|
+
cwe: 'CWE-798',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- dotenv imported outside of config/entry files ---
|
|
109
|
+
if (isJsTs && !CONFIG_FILE_RE.test(file) && DOTENV_IMPORT_RE.test(content)) {
|
|
110
|
+
const lineNum = _lineNumber(content, content.search(DOTENV_IMPORT_RE));
|
|
111
|
+
findings.push({
|
|
112
|
+
id: `env-hygiene:ENV_DOTENV_IN_SOURCE:${file}:${lineNum}`,
|
|
113
|
+
title: 'dotenv loaded in non-entry file',
|
|
114
|
+
severity: 'low',
|
|
115
|
+
file, line: lineNum,
|
|
116
|
+
description: 'dotenv.config() in a non-entry module can silently load .env files in production, overriding real environment variables set by your platform. It also makes testing harder.',
|
|
117
|
+
remediation: 'Call dotenv.config() only once, in your application entry point (server.js, index.js). Better yet, use platform-native env management (Vercel env vars, Railway variables).',
|
|
118
|
+
cwe: 'CWE-665',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return findings;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export { scanEnvHygiene };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// FastAPI framework hardening.
|
|
2
|
+
//
|
|
3
|
+
// Coverage:
|
|
4
|
+
// 1. app.run(host="0.0.0.0", debug=True) or uvicorn run with debug=True
|
|
5
|
+
// 2. CORSMiddleware(allow_origins=["*"]) with allow_credentials=True
|
|
6
|
+
// 3. @app.<verb>("...") without Depends(security_dep) on mutating endpoints
|
|
7
|
+
// 4. JWT decode without signature verification (jwt.decode(token, options={"verify_signature": False}))
|
|
8
|
+
// 5. Pydantic Settings with literal API key default
|
|
9
|
+
// 6. HTTPBearer used without auto_error checks
|
|
10
|
+
// 7. app.add_middleware(TrustedHostMiddleware, allowed_hosts=["*"])
|
|
11
|
+
|
|
12
|
+
const _PY_RE = /\.py$/i;
|
|
13
|
+
|
|
14
|
+
function _line(raw, idx) {
|
|
15
|
+
return raw.slice(0, idx).split('\n').length;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function _isFastApi(raw) {
|
|
19
|
+
return /\bfrom\s+fastapi\b/.test(raw) || /\bimport\s+fastapi\b/.test(raw);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function scanFastapiHardening(file, raw) {
|
|
23
|
+
if (!file || !raw || typeof raw !== 'string') return [];
|
|
24
|
+
if (!_PY_RE.test(file)) return [];
|
|
25
|
+
if (!_isFastApi(raw)) return [];
|
|
26
|
+
if (raw.length > 200_000) return [];
|
|
27
|
+
|
|
28
|
+
const findings = [];
|
|
29
|
+
|
|
30
|
+
// 1. debug=True on app run
|
|
31
|
+
for (const m of raw.matchAll(/\b(?:app\.run|uvicorn\.run)\s*\([^)]*\bdebug\s*=\s*True\b/g)) {
|
|
32
|
+
findings.push({
|
|
33
|
+
id: `fastapi:debug-true:${file}:${_line(raw, m.index)}`,
|
|
34
|
+
file, line: _line(raw, m.index),
|
|
35
|
+
vuln: 'FastAPI / uvicorn run with debug=True',
|
|
36
|
+
severity: 'high',
|
|
37
|
+
family: 'fastapi-debug-enabled',
|
|
38
|
+
cwe: 'CWE-489',
|
|
39
|
+
confidence: 0.95,
|
|
40
|
+
description: 'debug=True enables hot-reload and detailed tracebacks on errors. In a deployed environment this leaks stack traces with line-level file paths and the request body.',
|
|
41
|
+
remediation: 'Remove debug=True from production entry points. Use `uvicorn app:app --reload` only on local dev.',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2. Wildcard CORS with credentials — matches both direct instantiation
|
|
46
|
+
// `CORSMiddleware(...)` and `add_middleware(CORSMiddleware, ...)` shapes.
|
|
47
|
+
for (const m of raw.matchAll(/CORSMiddleware[^)]*\)/g)) {
|
|
48
|
+
const block = m[0];
|
|
49
|
+
const hasWildcard = /allow_origins\s*=\s*\[\s*['"]\*['"]\s*\]/.test(block);
|
|
50
|
+
const hasCreds = /allow_credentials\s*=\s*True\b/.test(block);
|
|
51
|
+
if (hasWildcard && hasCreds) {
|
|
52
|
+
findings.push({
|
|
53
|
+
id: `fastapi:cors-wildcard-credentials:${file}:${_line(raw, m.index)}`,
|
|
54
|
+
file, line: _line(raw, m.index),
|
|
55
|
+
vuln: 'FastAPI CORS allow_origins=["*"] combined with allow_credentials=True',
|
|
56
|
+
severity: 'critical',
|
|
57
|
+
family: 'fastapi-cors-wildcard',
|
|
58
|
+
cwe: 'CWE-942',
|
|
59
|
+
confidence: 0.95,
|
|
60
|
+
description: 'Browsers block this combination — but FastAPI still emits Access-Control-Allow-Origin: * which most reverse-proxies and SDKs interpret as "any origin can read credentialed responses." Some browsers no longer enforce; libraries and middleboxes do not.',
|
|
61
|
+
remediation: 'Use an explicit allow-list: allow_origins=["https://app.example.com"]. Never combine wildcard with credentials.',
|
|
62
|
+
});
|
|
63
|
+
} else if (hasWildcard) {
|
|
64
|
+
findings.push({
|
|
65
|
+
id: `fastapi:cors-wildcard:${file}:${_line(raw, m.index)}`,
|
|
66
|
+
file, line: _line(raw, m.index),
|
|
67
|
+
vuln: 'FastAPI CORS allow_origins=["*"]',
|
|
68
|
+
severity: 'medium',
|
|
69
|
+
family: 'fastapi-cors-wildcard',
|
|
70
|
+
cwe: 'CWE-942',
|
|
71
|
+
confidence: 0.85,
|
|
72
|
+
description: 'Wildcard CORS broadens the API\'s exposure to scraping and CSRF-like abuse.',
|
|
73
|
+
remediation: 'Use an explicit allow-list: allow_origins=["https://app.example.com"].',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 3. Mutating endpoint without Depends() injecting security
|
|
79
|
+
const mutatingRouteRe = /@\s*(?:app|router)\.(?:post|put|patch|delete)\s*\(\s*['"][^'"]+['"][^)]*\)\s*(?:async\s+)?def\s+(\w+)\s*\(([^)]*)\)/g;
|
|
80
|
+
for (const m of raw.matchAll(mutatingRouteRe)) {
|
|
81
|
+
const params = m[2];
|
|
82
|
+
// Look for Security(...) or Depends(...) pointing at an auth dep, or a current_user param.
|
|
83
|
+
if (/\b(?:Security\s*\(|Depends\s*\(\s*(?:get_current_user|require_auth|verify_jwt|require_admin|oauth2_scheme))/.test(params)) continue;
|
|
84
|
+
if (/\b(?:current_user|user\s*:\s*User|token\s*:\s*str\s*=\s*Depends)/.test(params)) continue;
|
|
85
|
+
findings.push({
|
|
86
|
+
id: `fastapi:no-auth-dep:${file}:${_line(raw, m.index)}:${m[1]}`,
|
|
87
|
+
file, line: _line(raw, m.index),
|
|
88
|
+
vuln: `FastAPI mutating endpoint ${m[1]}() has no Security() / Depends() auth dependency`,
|
|
89
|
+
severity: 'high',
|
|
90
|
+
family: 'fastapi-missing-auth',
|
|
91
|
+
cwe: 'CWE-862',
|
|
92
|
+
confidence: 0.7,
|
|
93
|
+
description: 'A POST/PUT/PATCH/DELETE handler is declared without a Security(...) or Depends(get_current_user) parameter. Unless a global middleware enforces auth (rare), this endpoint is callable anonymously.',
|
|
94
|
+
remediation: 'Add: current_user: User = Depends(get_current_user) — or Security(oauth2_scheme, scopes=["admin"]) — as a parameter to the route handler.',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 4. JWT decode without signature verification
|
|
99
|
+
for (const m of raw.matchAll(/\bjwt\.decode\s*\([^)]*verify_signature\s*['"]?\s*:?\s*False/g)) {
|
|
100
|
+
findings.push({
|
|
101
|
+
id: `fastapi:jwt-no-verify:${file}:${_line(raw, m.index)}`,
|
|
102
|
+
file, line: _line(raw, m.index),
|
|
103
|
+
vuln: 'jwt.decode called with verify_signature=False',
|
|
104
|
+
severity: 'critical',
|
|
105
|
+
family: 'fastapi-jwt-no-verify',
|
|
106
|
+
cwe: 'CWE-347',
|
|
107
|
+
confidence: 0.95,
|
|
108
|
+
description: 'jwt.decode with verify_signature=False reads claims without verifying the token. An attacker can forge any claims (including admin roles) and the token will be accepted.',
|
|
109
|
+
remediation: 'Remove verify_signature=False. Always verify against the public key / shared secret.',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 5. Pydantic Settings with literal API-key-shaped default
|
|
114
|
+
for (const m of raw.matchAll(/\b(?:[Aa]pi_?[Kk]ey|secret_key|jwt_secret)\s*:\s*str\s*=\s*['"]([A-Za-z0-9!@#$%^&*_+\-=]{12,})['"]/g)) {
|
|
115
|
+
if (/your[-_]?key|change[-_]?me|placeholder|example|TODO/i.test(m[1])) continue;
|
|
116
|
+
findings.push({
|
|
117
|
+
id: `fastapi:hardcoded-default-secret:${file}:${_line(raw, m.index)}`,
|
|
118
|
+
file, line: _line(raw, m.index),
|
|
119
|
+
vuln: 'Pydantic Settings (or similar) has a hardcoded default secret',
|
|
120
|
+
severity: 'critical',
|
|
121
|
+
family: 'fastapi-hardcoded-credential',
|
|
122
|
+
cwe: 'CWE-798',
|
|
123
|
+
confidence: 0.85,
|
|
124
|
+
description: 'A real secret value is the literal default for a config field. Anyone who reads the source has the key.',
|
|
125
|
+
remediation: 'Set the field with no default (forcing env-var provision) or default to None and raise if missing at startup.',
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 6. TrustedHostMiddleware with wildcard
|
|
130
|
+
for (const m of raw.matchAll(/TrustedHostMiddleware\s*[^)]*allowed_hosts\s*=\s*\[\s*['"]\*['"]\s*\]/g)) {
|
|
131
|
+
findings.push({
|
|
132
|
+
id: `fastapi:trusted-host-wildcard:${file}:${_line(raw, m.index)}`,
|
|
133
|
+
file, line: _line(raw, m.index),
|
|
134
|
+
vuln: 'TrustedHostMiddleware allowed_hosts=["*"]',
|
|
135
|
+
severity: 'medium',
|
|
136
|
+
family: 'fastapi-trusted-host-wildcard',
|
|
137
|
+
cwe: 'CWE-20',
|
|
138
|
+
confidence: 0.9,
|
|
139
|
+
description: 'Disables Host header validation. Combined with downstream URL generation (password-reset emails, OAuth callbacks), attackers can route victims through their own hostnames.',
|
|
140
|
+
remediation: 'Provide an explicit allow-list of your real hostnames.',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return findings;
|
|
145
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Go SAST — extensions to the existing Go coverage in engine.js.
|
|
2
|
+
//
|
|
3
|
+
// Adds:
|
|
4
|
+
// - text/template misuse imported instead of html/template for HTML
|
|
5
|
+
// - exec.Command shell-form exec.Command("sh", "-c", <var>) vs argv-form
|
|
6
|
+
// - http.Client custom-transport http.Get(<var>) or http.NewRequest with
|
|
7
|
+
// user-controlled URL and no allowlist
|
|
8
|
+
//
|
|
9
|
+
// These patterns are narrow and complement the existing regex-based Go rules
|
|
10
|
+
// (GORM raw SQL, source patterns for net/http, Echo, Chi, Gin).
|
|
11
|
+
|
|
12
|
+
import { blankComments } from './_comment-strip.js';
|
|
13
|
+
|
|
14
|
+
const FINDINGS = [
|
|
15
|
+
{
|
|
16
|
+
id: 'go-text-template-html', severity: 'high', cwe: 'CWE-79', family: 'xss',
|
|
17
|
+
// import "text/template" AND ResponseWriter.Write in the same file
|
|
18
|
+
re: /\b"text\/template"/g,
|
|
19
|
+
vuln: 'XSS — text/template package used in an HTTP handler (no auto-escaping)',
|
|
20
|
+
remediation: 'For HTML output, import `"html/template"` instead. text/template does not escape HTML — any user value placed in the template renders as raw HTML. The two packages have nearly identical APIs; the switch is usually one import line.',
|
|
21
|
+
// Only fire when the file looks like an HTTP handler.
|
|
22
|
+
requiresContext: /\bhttp\.(?:ResponseWriter|HandleFunc|Handler)\b|\bw\s*\.\s*Write\b|\bgin\.|\becho\.|\bchi\./,
|
|
23
|
+
fileSafe: /\b"html\/template"/,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'go-exec-shell-form', severity: 'critical', cwe: 'CWE-78', family: 'command-injection',
|
|
27
|
+
// exec.Command("sh", "-c", varExpr) — the explicit shell-invocation shape
|
|
28
|
+
re: /\bexec\.Command\s*\(\s*"(?:sh|bash|zsh|\/bin\/sh|\/bin\/bash)"\s*,\s*"-c"\s*,\s*(?!"[^"]*"\s*\))/g,
|
|
29
|
+
vuln: 'Command Injection — exec.Command shell-form with dynamic argument',
|
|
30
|
+
remediation: 'Replace the shell form with the argv form: `exec.Command("ls", "-l", userDir)`. The shell form (`sh -c "<cmd>"`) interprets `;`, `&&`, `|`, `$()` etc. in user input.',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'go-http-user-url', severity: 'high', cwe: 'CWE-918', family: 'ssrf',
|
|
34
|
+
// http.Get(varExpr) or http.NewRequest(method, varExpr, body) where the URL is a variable.
|
|
35
|
+
// Already covered for fetch/axios in the engine, but the Go-specific shape
|
|
36
|
+
// gets missed because the engine's pattern doesn't include http.Get/Post.
|
|
37
|
+
re: /\bhttp\.(?:Get|Post|Head|PostForm)\s*\(\s*(?!"[^"]*"\s*\))[a-zA-Z_]\w*/g,
|
|
38
|
+
vuln: 'SSRF — http.Get/Post with variable URL',
|
|
39
|
+
remediation: 'Allowlist the destination host before any net/http call. Use `net/url.Parse(target)` then check `parsed.Host` against an explicit allowlist. Reject RFC1918 (10/8, 172.16/12, 192.168/16) and the cloud metadata addresses (169.254.169.254, fd00:ec2::254).',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'go-newrequest-user-url', severity: 'high', cwe: 'CWE-918', family: 'ssrf',
|
|
43
|
+
re: /\bhttp\.NewRequest(?:WithContext)?\s*\(\s*[^,]+,\s*(?!"[^"]*"\s*[,)])[a-zA-Z_]\w*/g,
|
|
44
|
+
vuln: 'SSRF — http.NewRequest with variable URL',
|
|
45
|
+
remediation: 'Validate the URL before building the request. The standard pattern: `u, err := url.Parse(target); if err != nil || !allowlist[u.Host] { return forbidden }`. Reject schemes other than http/https up front.',
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
function lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
|
|
50
|
+
|
|
51
|
+
export function scanGoExtended(fp, raw) {
|
|
52
|
+
if (!/\.go$/i.test(fp)) return [];
|
|
53
|
+
if (!raw || raw.length > 500_000) return [];
|
|
54
|
+
const code = blankComments(raw);
|
|
55
|
+
const out = [];
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
for (const rule of FINDINGS) {
|
|
58
|
+
if (rule.fileSafe && rule.fileSafe.test(code)) continue;
|
|
59
|
+
if (rule.requiresContext && !rule.requiresContext.test(code)) continue;
|
|
60
|
+
const re = new RegExp(rule.re.source, rule.re.flags);
|
|
61
|
+
let m;
|
|
62
|
+
while ((m = re.exec(code))) {
|
|
63
|
+
const line = lineOf(raw, m.index);
|
|
64
|
+
const id = `${rule.id}:${fp}:${line}`;
|
|
65
|
+
if (seen.has(id)) continue;
|
|
66
|
+
seen.add(id);
|
|
67
|
+
out.push({
|
|
68
|
+
id, file: fp, line,
|
|
69
|
+
vuln: rule.vuln,
|
|
70
|
+
severity: rule.severity,
|
|
71
|
+
cwe: rule.cwe,
|
|
72
|
+
stride: rule.family === 'xss' ? 'Tampering'
|
|
73
|
+
: rule.family === 'command-injection' ? 'Elevation of Privilege'
|
|
74
|
+
: rule.family === 'ssrf' ? 'Spoofing'
|
|
75
|
+
: 'Tampering',
|
|
76
|
+
snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
|
|
77
|
+
remediation: rule.remediation,
|
|
78
|
+
confidence: 0.85,
|
|
79
|
+
parser: 'GO',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Host header attack detection.
|
|
2
|
+
//
|
|
3
|
+
// req.headers.host / req.host is attacker-controlled (HTTP/1.1 lets clients
|
|
4
|
+
// set whatever Host they want; even with allowlisted hostnames, reverse
|
|
5
|
+
// proxies often forward whatever they receive). When used to build:
|
|
6
|
+
// - password-reset / email-verification URLs → Account-takeover via
|
|
7
|
+
// poisoned reset link
|
|
8
|
+
// - server-side redirects (Location header) → Open redirect / phishing
|
|
9
|
+
// - cache keys → Cache poisoning
|
|
10
|
+
// the attacker controls the URL embedded in security-sensitive emails or in
|
|
11
|
+
// downstream caches.
|
|
12
|
+
//
|
|
13
|
+
// Patterns:
|
|
14
|
+
// `https://${req.headers.host}/reset?token=...`
|
|
15
|
+
// `'http://' + req.headers.host + '/verify?token=...'`
|
|
16
|
+
// res.redirect(req.headers.host + ...)
|
|
17
|
+
//
|
|
18
|
+
// Safe shapes (file-level suppression of a finding):
|
|
19
|
+
// - An ALLOWED_HOSTS/TRUSTED_HOSTS array compared against the header
|
|
20
|
+
|
|
21
|
+
const HOST_SOURCE_RE = /\b(?:req|request|ctx)\s*\.\s*(?:headers\s*(?:\.|\[\s*['"])\s*host\s*['"]?\s*\]?|host(?![A-Za-z_]))/g;
|
|
22
|
+
const HOST_X_FORWARDED_RE = /\b(?:req|request|ctx)\s*\.\s*(?:headers\s*(?:\.|\[\s*['"])\s*x-forwarded-host\s*['"]?\s*\]?)/g;
|
|
23
|
+
|
|
24
|
+
// Reset / verify URL pattern: hostSource concatenated/interpolated with reset/verify path.
|
|
25
|
+
const RESET_URL_TEMPLATE_RE = /`[^`]*\$\{[^}]*\b(?:req|request|ctx)\s*\.\s*(?:headers\s*(?:\.|\[\s*['"])\s*(?:host|x-forwarded-host)\s*['"]?\s*\]?|host(?![A-Za-z_]))[^}]*\}[^`]*(?:reset|verify|confirm|invite|onboard|password|token|auth|magic)[^`]*`/gi;
|
|
26
|
+
const RESET_URL_CONCAT_RE = /['"][^'"]*(?:https?:\/\/|^\/|^)['"][^;]{0,80}?(?:req|request|ctx)\s*\.\s*(?:headers\.host|host|headers\[['"]host['"]\])[^;]{0,200}?(?:reset|verify|confirm|invite|password|token|auth|magic)/gi;
|
|
27
|
+
|
|
28
|
+
// Direct redirect using host header
|
|
29
|
+
const HOST_IN_REDIRECT_RE = /\b(?:res|response)\s*\.\s*(?:redirect|location)\s*\([^)]*\b(?:req|request|ctx)\s*\.\s*(?:headers\.host|host|headers\[['"]host['"]\])/g;
|
|
30
|
+
|
|
31
|
+
const TRUSTED_HOSTS_RE = /\b(?:ALLOWED_HOSTS|TRUSTED_HOSTS|allowedHosts|trustedHosts|hostAllowlist|hostnameAllowlist|validHosts)\b/;
|
|
32
|
+
|
|
33
|
+
import { blankComments } from './_comment-strip.js';
|
|
34
|
+
|
|
35
|
+
function _lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
|
|
36
|
+
|
|
37
|
+
export function scanHostHeader(fp, raw) {
|
|
38
|
+
if (!/\.(?:js|jsx|ts|tsx|mjs|cjs|py)$/i.test(fp)) return [];
|
|
39
|
+
if (!raw || raw.length > 500_000) return [];
|
|
40
|
+
// Blank out comments while preserving character indices.
|
|
41
|
+
const code = /\.py$/i.test(fp) ? blankComments(raw, 'py') : blankComments(raw);
|
|
42
|
+
const findings = [];
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
const push = (f) => { if (!seen.has(f.id)) { seen.add(f.id); findings.push(f); } };
|
|
45
|
+
const hasAllowlist = TRUSTED_HOSTS_RE.test(code);
|
|
46
|
+
|
|
47
|
+
// 1. Template literal with host + reset/verify keyword
|
|
48
|
+
const tre = new RegExp(RESET_URL_TEMPLATE_RE.source, RESET_URL_TEMPLATE_RE.flags);
|
|
49
|
+
let m;
|
|
50
|
+
while ((m = tre.exec(code))) {
|
|
51
|
+
if (hasAllowlist) continue;
|
|
52
|
+
const line = _lineOf(raw, m.index);
|
|
53
|
+
push({
|
|
54
|
+
id: `host-header:${fp}:${line}:reset-tpl`,
|
|
55
|
+
file: fp, line,
|
|
56
|
+
vuln: 'Host Header Attack: password-reset / verify URL built from req.headers.host',
|
|
57
|
+
severity: 'high',
|
|
58
|
+
cwe: 'CWE-20',
|
|
59
|
+
stride: 'Spoofing',
|
|
60
|
+
snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
|
|
61
|
+
remediation: 'An attacker can set the Host header to a domain they control; if your reset email then embeds that host, the recipient clicks a link pointing at attacker-controlled infrastructure that captures the token. Use a server-side constant (process.env.PUBLIC_HOST) for any URL that lands in an email, or validate req.headers.host against an explicit allowlist of canonical hostnames.',
|
|
62
|
+
confidence: 0.85,
|
|
63
|
+
parser: 'HOST-HEADER',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 2. String concatenation form (looser shape)
|
|
68
|
+
const cre = new RegExp(RESET_URL_CONCAT_RE.source, RESET_URL_CONCAT_RE.flags);
|
|
69
|
+
while ((m = cre.exec(code))) {
|
|
70
|
+
if (hasAllowlist) continue;
|
|
71
|
+
const line = _lineOf(raw, m.index);
|
|
72
|
+
push({
|
|
73
|
+
id: `host-header:${fp}:${line}:reset-concat`,
|
|
74
|
+
file: fp, line,
|
|
75
|
+
vuln: 'Host Header Attack: reset/verify URL concatenates req.headers.host',
|
|
76
|
+
severity: 'high',
|
|
77
|
+
cwe: 'CWE-20',
|
|
78
|
+
stride: 'Spoofing',
|
|
79
|
+
snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
|
|
80
|
+
remediation: 'Replace req.headers.host with process.env.PUBLIC_HOST (or equivalent server-side constant). For multi-tenant apps that legitimately need request-derived hosts, validate against a per-tenant allowlist.',
|
|
81
|
+
confidence: 0.7,
|
|
82
|
+
parser: 'HOST-HEADER',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 3. Redirect using host header
|
|
87
|
+
const rre = new RegExp(HOST_IN_REDIRECT_RE.source, HOST_IN_REDIRECT_RE.flags);
|
|
88
|
+
while ((m = rre.exec(code))) {
|
|
89
|
+
if (hasAllowlist) continue;
|
|
90
|
+
const line = _lineOf(raw, m.index);
|
|
91
|
+
push({
|
|
92
|
+
id: `host-header:${fp}:${line}:redirect`,
|
|
93
|
+
file: fp, line,
|
|
94
|
+
vuln: 'Host Header Attack: redirect target uses req.headers.host',
|
|
95
|
+
severity: 'high',
|
|
96
|
+
cwe: 'CWE-601',
|
|
97
|
+
stride: 'Spoofing',
|
|
98
|
+
snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
|
|
99
|
+
remediation: 'A server-side redirect to req.headers.host lets the attacker pick the destination. Use a fixed canonical URL or an explicit per-tenant allowlist.',
|
|
100
|
+
confidence: 0.85,
|
|
101
|
+
parser: 'HOST-HEADER',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return findings;
|
|
106
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// SAST submodule view of the engine — file-level static analysis.
|
|
2
|
+
export {
|
|
3
|
+
performAnalysis, performASTAnalysis, performRegexAnalysis,
|
|
4
|
+
scanRoutes, scanLogicVulns, scanStructuralVulns, scanExtraStructural,
|
|
5
|
+
scanReDoS, scanCiphers, scanGraphQL,
|
|
6
|
+
buildImportGraph, crossFileTaint, buildStoredTaintRegistry, crossStoredTaint,
|
|
7
|
+
crossSessionTaint, buildCallGraph, annotateReachability, detectGuardsForFinding,
|
|
8
|
+
inferSanitizers, applyLearnedSanitizers, applySanitizerEffectiveness,
|
|
9
|
+
crossFindingChain, classifyOrphans, classifyField, classifyEndpoint,
|
|
10
|
+
scoreTriage, dedupeFindingsWithEvidence,
|
|
11
|
+
SOURCE_PATTERNS, SINK_PATTERNS, SANITIZER_PATTERNS, ROUTE_PATTERNS,
|
|
12
|
+
LOGIC_PATTERNS, STRUCTURAL_VULN_PATTERNS, EXTRA_STRUCTURAL_PATTERNS,
|
|
13
|
+
CHAIN_RULES, GRAPHQL_VULN_PATTERNS, GUARD_PATTERNS,
|
|
14
|
+
SANITIZER_EFFECTIVENESS, SEVERITY_SCORE,
|
|
15
|
+
CIPHER_REST_PATTERNS, CIPHER_TRANSIT_PATTERNS,
|
|
16
|
+
DATA_CLASSES,
|
|
17
|
+
} from '../engine.js';
|