@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,101 @@
|
|
|
1
|
+
import { blankComments } from './_comment-strip.js';
|
|
2
|
+
// Mass assignment / over-posting detector.
|
|
3
|
+
//
|
|
4
|
+
// The classic shape: developer wires the entire request body into an ORM
|
|
5
|
+
// create/update call, allowing the client to set fields the route handler
|
|
6
|
+
// never intended (is_admin, role, balance, etc.).
|
|
7
|
+
//
|
|
8
|
+
// Languages covered:
|
|
9
|
+
// - JS/TS Express + Mongoose/Sequelize/Prisma/TypeORM
|
|
10
|
+
// - Ruby on Rails ActiveRecord
|
|
11
|
+
// - Python Django ORM / Flask-SQLAlchemy
|
|
12
|
+
// - Java Spring Data
|
|
13
|
+
// - Go GORM
|
|
14
|
+
//
|
|
15
|
+
// Heuristic per-language: spread/splat of a request-body shape directly into
|
|
16
|
+
// a constructor, create(), update(), .save(), .set(), or Object.assign on a
|
|
17
|
+
// model. Whitelisting (allowed-fields pick) is the canonical fix.
|
|
18
|
+
|
|
19
|
+
const ALLOW_LIST_HINTS = /(?:pick|allowedFields|permit|allowed_params|whitelist|strong_params|FILTER_FIELDS|select_for|only:)/;
|
|
20
|
+
|
|
21
|
+
const JS_PATTERNS = [
|
|
22
|
+
// Object.assign(user, req.body)
|
|
23
|
+
/Object\.assign\s*\(\s*(\w+)\s*,\s*(req|request)\s*\.\s*(body|params|query)\b/g,
|
|
24
|
+
// new User({ ...req.body }) or User.create({ ...req.body })
|
|
25
|
+
/(?:new\s+([A-Z]\w+)\s*\(\s*\{[^}]*\.\.\.\s*(?:req|request)\s*\.\s*(?:body|params|query))/g,
|
|
26
|
+
// User.create(req.body) · Model.update(req.body, …)
|
|
27
|
+
/\b([A-Z]\w+)\s*\.\s*(?:create|update|build|save)\s*\(\s*(?:req|request)\s*\.\s*(?:body|params|query)\s*[,)]/g,
|
|
28
|
+
// prisma.user.update({ data: req.body })
|
|
29
|
+
/\.\s*(?:create|update|upsert)\s*\(\s*\{[^}]*data\s*:\s*(?:req|request)\s*\.\s*(?:body|params|query)/g,
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const PY_PATTERNS = [
|
|
33
|
+
// Model.objects.create(**request.POST) / **request.data / **request.json
|
|
34
|
+
/\b([A-Z]\w+)\s*(?:\.objects)?\s*\.\s*(?:create|update|filter\([^)]*\)\.update)\s*\(\s*\*\*\s*request\s*\.\s*(?:POST|data|json|form)/g,
|
|
35
|
+
// serializer.save(**request.data)
|
|
36
|
+
/\.save\s*\(\s*\*\*\s*request\s*\.\s*(?:data|json|POST|form)/g,
|
|
37
|
+
// setattr(obj, k, v) loop over request.data without allow-list
|
|
38
|
+
/for\s+\w+\s*,\s*\w+\s+in\s+request\s*\.\s*(?:data|json|POST|form)\s*\.\s*items\s*\([^)]*\)\s*:\s*\n\s*setattr/g,
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const RB_PATTERNS = [
|
|
42
|
+
// User.create(params) · user.update(params)
|
|
43
|
+
/\b([A-Z]\w+)\s*\.\s*(?:create|update|new)\s*\(\s*params\s*[,)]/g,
|
|
44
|
+
// user.assign_attributes(params) · user.attributes = params
|
|
45
|
+
/\.(?:assign_attributes|attributes\s*=)\s*\(?\s*params\s*[,)]?/g,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const GO_PATTERNS = [
|
|
49
|
+
// db.Model(&user).Updates(input) where input is fully user-controlled
|
|
50
|
+
/\bdb\s*\.\s*(?:Model|Updates|Create)\s*\([^)]*&?\s*(\w+)\s*\)/g,
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
function lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
|
|
54
|
+
|
|
55
|
+
function pickPatterns(fp) {
|
|
56
|
+
if (/\.(?:js|jsx|ts|tsx|mjs|cjs)$/i.test(fp)) return { lang: 'js', patterns: JS_PATTERNS };
|
|
57
|
+
if (/\.py$/i.test(fp)) return { lang: 'py', patterns: PY_PATTERNS };
|
|
58
|
+
if (/\.rb$/i.test(fp)) return { lang: 'rb', patterns: RB_PATTERNS };
|
|
59
|
+
if (/\.go$/i.test(fp)) return { lang: 'go', patterns: GO_PATTERNS };
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function scanMassAssignment(fp, raw) {
|
|
64
|
+
if (!raw || raw.length > 500_000) return [];
|
|
65
|
+
const sel = pickPatterns(fp);
|
|
66
|
+
if (!sel) return [];
|
|
67
|
+
const findings = [];
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
const code = blankComments(raw, sel.lang === 'py' ? 'py' : undefined);
|
|
70
|
+
// Skip files that look like they use an allow-list — strong signal of safety.
|
|
71
|
+
if (ALLOW_LIST_HINTS.test(code.slice(0, 4000))) {
|
|
72
|
+
// not a hard skip: still scan but downgrade
|
|
73
|
+
}
|
|
74
|
+
for (const re of sel.patterns) {
|
|
75
|
+
const r = new RegExp(re.source, re.flags);
|
|
76
|
+
let m;
|
|
77
|
+
while ((m = r.exec(code))) {
|
|
78
|
+
const line = lineOf(raw, m.index);
|
|
79
|
+
const id = `mass-assignment:${fp}:${line}`;
|
|
80
|
+
if (seen.has(id)) continue;
|
|
81
|
+
seen.add(id);
|
|
82
|
+
// Look ±5 lines for an allow-list signal — if present, downgrade.
|
|
83
|
+
const lines = raw.split('\n');
|
|
84
|
+
const window = lines.slice(Math.max(0, line - 6), line + 5).join(' ');
|
|
85
|
+
const hasAllowList = ALLOW_LIST_HINTS.test(window);
|
|
86
|
+
findings.push({
|
|
87
|
+
id,
|
|
88
|
+
file: fp, line,
|
|
89
|
+
vuln: 'Mass Assignment: Unfiltered request body into model write',
|
|
90
|
+
severity: hasAllowList ? 'low' : 'high',
|
|
91
|
+
cwe: 'CWE-915',
|
|
92
|
+
stride: 'Tampering',
|
|
93
|
+
snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
|
|
94
|
+
remediation: 'Explicitly allow-list the fields a client may set, instead of spreading the whole request body into the model write. Express: `pick(req.body, ["name", "email"])` (lodash) before `.create()`. Rails: `params.require(:user).permit(:name, :email)`. Django: a `ModelForm` / `Serializer` with explicit fields. Mass-assigning the whole body lets a client elevate privileges by adding `is_admin: true` to the JSON.',
|
|
95
|
+
parser: 'MASS-ASSIGN',
|
|
96
|
+
confidence: hasAllowList ? 0.40 : 0.85,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return findings;
|
|
101
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// MCP / Agent tool security audit.
|
|
2
|
+
//
|
|
3
|
+
// Scans MCP server config files (claude_desktop_config.json, *.mcp.json, mcp.json,
|
|
4
|
+
// .claude/mcp.json) and tool-definition source for the canonical agent-host
|
|
5
|
+
// risks:
|
|
6
|
+
// - Untrusted server install vector (curl|sh, http://, unpinned npx)
|
|
7
|
+
// - Over-scoped filesystem grant (root, $HOME, /, *)
|
|
8
|
+
// - Hardcoded credential in env (sk-…, github_pat_…, AKIA…, ghp_…)
|
|
9
|
+
// - Prompt injection in description (instruction overrides in tool descriptions)
|
|
10
|
+
// - Dangerous tool capability (shell/exec/eval/sql exposed to model)
|
|
11
|
+
// - Unrestricted network passthrough (proxy tools without allow-list)
|
|
12
|
+
//
|
|
13
|
+
// F1 strategy:
|
|
14
|
+
// Recall — broad config-file matching across config name variants.
|
|
15
|
+
// Precision — fire only when one of the patterns above is concretely present
|
|
16
|
+
// in a JSON config or in a tool definition with description+name.
|
|
17
|
+
|
|
18
|
+
const _MCP_FILE_RE = /(?:^|[\\/])(?:claude_desktop_config\.json|\.?mcp\.json|[^/\\]+\.mcp\.json|mcp_servers\.json)$/i;
|
|
19
|
+
const _NONPROD_RE = /(?:^|[\\/])(?:tests?|examples?|fixtures?|node_modules|docs?)[\\/]/i;
|
|
20
|
+
|
|
21
|
+
// Hardcoded credential shapes commonly leaked in MCP env: blocks
|
|
22
|
+
const _HARDCODED_CRED_RE = [
|
|
23
|
+
/\b(?:sk-[A-Za-z0-9]{20,})\b/, // OpenAI / Anthropic
|
|
24
|
+
/\b(?:sk-ant-[A-Za-z0-9_-]{20,})\b/, // Anthropic
|
|
25
|
+
/\b(?:ghp_[A-Za-z0-9]{36})\b/, // GitHub PAT
|
|
26
|
+
/\b(?:github_pat_[A-Za-z0-9_]{20,})\b/, // GitHub fine-grained PAT
|
|
27
|
+
/\b(?:gho_[A-Za-z0-9]{36})\b/, // GitHub OAuth
|
|
28
|
+
/\bAKIA[0-9A-Z]{16}\b/, // AWS access key id
|
|
29
|
+
/\b(?:xox[abprs]-[A-Za-z0-9-]{10,})\b/, // Slack
|
|
30
|
+
/\b(?:gsk_[A-Za-z0-9]{30,})\b/, // Groq
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// Description fields that try to inject instructions into the agent
|
|
34
|
+
const _PROMPT_INJECTION_RE = [
|
|
35
|
+
/\b(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|prior|above|preceding)\s+(?:instructions|directives|prompts|rules)/i,
|
|
36
|
+
/\b(?:you\s+are\s+now|new\s+system\s+prompt|act\s+as|pretend\s+to\s+be)\b/i,
|
|
37
|
+
/\b(?:before\s+(?:running|invoking|using)\s+this\s+tool[^.]*?(?:read|exfiltrate|send|leak|copy|reveal|exec))/i,
|
|
38
|
+
/<\s*\|?\s*(?:system|im_start|im_end|assistant|user)\s*\|?\s*>/i,
|
|
39
|
+
/\b(?:print|reveal|output|show|expose|reveal)\s+(?:your|the)?\s*(?:system\s+prompt|instructions|api\s+key|credentials|secrets?)\b/i,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// Tool/server names that imply dangerous capabilities
|
|
43
|
+
const _DANGEROUS_CAPABILITY_NAMES = /\b(?:shell|bash|exec|run_command|run_shell|execute_shell|eval|eval_python|sandbox_exec|run_code|sudo|root|kubectl|docker_exec|admin|drop_table|raw_query|ssh|fetch_url_unrestricted)\b/i;
|
|
44
|
+
|
|
45
|
+
// Filesystem args / env paths that grant excessive scope
|
|
46
|
+
const _FS_OVERSCOPE_RE = [
|
|
47
|
+
/^(?:\/|~\/?|\$HOME\/?|\$\{?HOME\}?\/?)$/,
|
|
48
|
+
/^(?:\/|~|\$HOME|\$\{HOME\})\s*\*$/,
|
|
49
|
+
/^(?:\/|~|\$HOME|\$\{HOME\})\/\*\*$/,
|
|
50
|
+
];
|
|
51
|
+
const _FS_LIKE_PATH_RE = /^(?:[A-Za-z]:|\/|~\/?|\$HOME|\$\{HOME\}|\.\.?\/)/;
|
|
52
|
+
|
|
53
|
+
// Untrusted install / command vectors
|
|
54
|
+
const _UNTRUSTED_INSTALL_RE = [
|
|
55
|
+
/\bcurl\s+[^|]*\|\s*(?:sh|bash|zsh)\b/,
|
|
56
|
+
/\bwget\s+[^|]*\|\s*(?:sh|bash|zsh)\b/,
|
|
57
|
+
/^http:\/\//i,
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
// Floating npx pins / unpinned versions in command:/args:
|
|
61
|
+
const _FLOATING_PIN_RE = /@(?:latest|next|main|master|beta|canary)\b/;
|
|
62
|
+
|
|
63
|
+
function _stringsFromValue(v, out=[]) {
|
|
64
|
+
if (v === null || v === undefined) return out;
|
|
65
|
+
if (typeof v === 'string') { out.push(v); return out; }
|
|
66
|
+
if (typeof v === 'number' || typeof v === 'boolean') { out.push(String(v)); return out; }
|
|
67
|
+
if (Array.isArray(v)) { for (const x of v) _stringsFromValue(x, out); return out; }
|
|
68
|
+
if (typeof v === 'object') { for (const k of Object.keys(v)) _stringsFromValue(v[k], out); return out; }
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function _findLineOf(raw, needle) {
|
|
73
|
+
if (!raw || !needle) return 1;
|
|
74
|
+
const idx = raw.indexOf(needle);
|
|
75
|
+
if (idx === -1) return 1;
|
|
76
|
+
return raw.substring(0, idx).split('\n').length;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function _findKeyLine(raw, key) {
|
|
80
|
+
if (!raw || !key) return 1;
|
|
81
|
+
const re = new RegExp('"' + key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '"\\s*:');
|
|
82
|
+
const m = raw.match(re);
|
|
83
|
+
if (!m) return 1;
|
|
84
|
+
return raw.substring(0, m.index).split('\n').length;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function _isMcpConfigFile(fp) {
|
|
88
|
+
const norm = fp.replace(/\\/g, '/');
|
|
89
|
+
if (_NONPROD_RE.test(norm)) return false;
|
|
90
|
+
return _MCP_FILE_RE.test(norm);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Public: scan a single file. Mirrors scanLLM/scanPipeline shape so the engine
|
|
94
|
+
// can call it inline for each file.
|
|
95
|
+
export function scanMCP(fp, raw) {
|
|
96
|
+
if (!_isMcpConfigFile(fp)) return [];
|
|
97
|
+
if (!raw || raw.length > 200_000) return [];
|
|
98
|
+
let parsed;
|
|
99
|
+
try { parsed = JSON.parse(raw); } catch { return []; }
|
|
100
|
+
if (!parsed || typeof parsed !== 'object') return [];
|
|
101
|
+
|
|
102
|
+
const findings = [];
|
|
103
|
+
const seen = new Set();
|
|
104
|
+
const push = (f) => {
|
|
105
|
+
const key = `${f.file}:${f.line}:${f.vuln}`;
|
|
106
|
+
if (seen.has(key)) return;
|
|
107
|
+
seen.add(key);
|
|
108
|
+
findings.push(f);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Both `mcpServers` (Claude desktop config) and top-level server maps
|
|
112
|
+
const servers = (parsed.mcpServers && typeof parsed.mcpServers === 'object')
|
|
113
|
+
? parsed.mcpServers
|
|
114
|
+
: (parsed.servers && typeof parsed.servers === 'object')
|
|
115
|
+
? parsed.servers
|
|
116
|
+
: parsed;
|
|
117
|
+
|
|
118
|
+
for (const [name, srv] of Object.entries(servers || {})) {
|
|
119
|
+
if (!srv || typeof srv !== 'object') continue;
|
|
120
|
+
const line = _findKeyLine(raw, name);
|
|
121
|
+
|
|
122
|
+
// 1. Untrusted install vector in command/args
|
|
123
|
+
const cmd = typeof srv.command === 'string' ? srv.command : '';
|
|
124
|
+
const args = Array.isArray(srv.args) ? srv.args.filter(a => typeof a === 'string') : [];
|
|
125
|
+
const fullCmd = [cmd, ...args].join(' ');
|
|
126
|
+
if (fullCmd && _UNTRUSTED_INSTALL_RE.some(re => re.test(fullCmd))) {
|
|
127
|
+
push({
|
|
128
|
+
id: `mcp-audit:${fp}:${line}:untrusted-install`,
|
|
129
|
+
kind: 'mcp', severity: 'critical',
|
|
130
|
+
vuln: 'MCP: untrusted install vector (curl|sh / http://) in server command',
|
|
131
|
+
cwe: 'CWE-494', stride: 'Tampering',
|
|
132
|
+
file: fp, line, snippet: `${name}: ${fullCmd}`.slice(0, 200),
|
|
133
|
+
fix: `MCP server "${name}" runs through an untrusted bootstrap (curl|sh or http://). Pin to a published, signed package and use https. Example: replace \`curl http://… | sh\` with an explicit \`npx -y package@<sha>\` that you have audited.`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 2. Floating tag pin in npx/uvx invocations
|
|
138
|
+
if (args.some(a => _FLOATING_PIN_RE.test(a))) {
|
|
139
|
+
const offending = args.find(a => _FLOATING_PIN_RE.test(a));
|
|
140
|
+
push({
|
|
141
|
+
id: `mcp-audit:${fp}:${line}:floating-pin`,
|
|
142
|
+
kind: 'mcp', severity: 'high',
|
|
143
|
+
vuln: 'MCP: server pinned to a floating tag (@latest/@main)',
|
|
144
|
+
cwe: 'CWE-1357', stride: 'Tampering',
|
|
145
|
+
file: fp, line, snippet: `${name}: ${offending}`,
|
|
146
|
+
fix: `Floating tag means the publisher (or an attacker who compromises them) can ship new code into your agent any time. Pin to a specific semver: \`pkg@1.2.3\` — or, even better, a published SHA.`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 3. Hardcoded credentials in env or args
|
|
151
|
+
const envObj = (srv.env && typeof srv.env === 'object') ? srv.env : {};
|
|
152
|
+
const allEnvStrings = _stringsFromValue(envObj);
|
|
153
|
+
const allArgStrings = _stringsFromValue(args);
|
|
154
|
+
const allHaystacks = [...allEnvStrings, ...allArgStrings];
|
|
155
|
+
for (const s of allHaystacks) {
|
|
156
|
+
if (_HARDCODED_CRED_RE.some(re => re.test(s))) {
|
|
157
|
+
push({
|
|
158
|
+
id: `mcp-audit:${fp}:${line}:hardcoded-cred`,
|
|
159
|
+
kind: 'mcp', severity: 'critical',
|
|
160
|
+
vuln: 'MCP: hardcoded credential in server env/args',
|
|
161
|
+
cwe: 'CWE-798', stride: 'Information Disclosure',
|
|
162
|
+
file: fp, line, snippet: `${name}: <credential redacted>`,
|
|
163
|
+
fix: `MCP server "${name}" carries a hardcoded API key. Move it to a secret store and reference via \`env: { API_KEY: "\${{ secrets.API_KEY }}" }\` or read from the user's keychain at startup.`,
|
|
164
|
+
});
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 4. Filesystem over-scope. Common when @modelcontextprotocol/server-filesystem
|
|
170
|
+
// is invoked with a root/home arg.
|
|
171
|
+
const isFsServer = /filesystem|files?|fs/i.test(name) || /server-filesystem/.test(fullCmd);
|
|
172
|
+
if (isFsServer || args.some(a => _FS_LIKE_PATH_RE.test(a))) {
|
|
173
|
+
const overscoped = args.find(a =>
|
|
174
|
+
typeof a === 'string' && _FS_OVERSCOPE_RE.some(re => re.test(a.trim()))
|
|
175
|
+
);
|
|
176
|
+
if (overscoped) {
|
|
177
|
+
push({
|
|
178
|
+
id: `mcp-audit:${fp}:${line}:fs-overscope`,
|
|
179
|
+
kind: 'mcp', severity: 'high',
|
|
180
|
+
vuln: 'MCP: filesystem server granted root or $HOME scope',
|
|
181
|
+
cwe: 'CWE-732', stride: 'Elevation of Privilege',
|
|
182
|
+
file: fp, line, snippet: `${name}: ${overscoped}`,
|
|
183
|
+
fix: `Filesystem MCP server "${name}" can read every file in ${overscoped}. Scope to the specific project directory the agent needs, e.g. \`/Users/me/code/this-project\`. Never grant \`/\`, \`$HOME\`, or \`~\`.`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 5. Dangerous capability name (shell, exec, eval, etc.) exposed unscoped
|
|
189
|
+
if (_DANGEROUS_CAPABILITY_NAMES.test(name) || _DANGEROUS_CAPABILITY_NAMES.test(fullCmd)) {
|
|
190
|
+
push({
|
|
191
|
+
id: `mcp-audit:${fp}:${line}:dangerous-capability`,
|
|
192
|
+
kind: 'mcp', severity: 'high',
|
|
193
|
+
vuln: 'MCP: server exposes a dangerous capability (shell/exec/eval) to the model',
|
|
194
|
+
cwe: 'CWE-77', stride: 'Elevation of Privilege',
|
|
195
|
+
file: fp, line, snippet: `${name}`,
|
|
196
|
+
fix: `Server "${name}" lets the model run arbitrary commands. If you keep it, restrict the working directory and the allowed binary list, and require user confirmation per call (most clients support an \`approval: ask\` flag).`,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 6. Description-field prompt injection
|
|
201
|
+
const desc = typeof srv.description === 'string' ? srv.description : '';
|
|
202
|
+
if (desc && _PROMPT_INJECTION_RE.some(re => re.test(desc))) {
|
|
203
|
+
const dline = _findLineOf(raw, desc.slice(0, 40));
|
|
204
|
+
push({
|
|
205
|
+
id: `mcp-audit:${fp}:${dline}:prompt-injection-description`,
|
|
206
|
+
kind: 'mcp', severity: 'critical',
|
|
207
|
+
vuln: 'MCP: prompt-injection text inside server description',
|
|
208
|
+
cwe: 'CWE-1336', stride: 'Spoofing',
|
|
209
|
+
file: fp, line: dline, snippet: desc.slice(0, 200),
|
|
210
|
+
fix: `The description for "${name}" contains an instruction that overrides agent behavior. Treat MCP server metadata as untrusted input — the agent reads it. Strip the override and only describe what the server actually does.`,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Also: prompt injection inside any tool definitions embedded in this config
|
|
215
|
+
const toolsArr = Array.isArray(srv.tools) ? srv.tools : [];
|
|
216
|
+
for (const t of toolsArr) {
|
|
217
|
+
if (!t || typeof t !== 'object') continue;
|
|
218
|
+
const tDesc = typeof t.description === 'string' ? t.description : '';
|
|
219
|
+
if (tDesc && _PROMPT_INJECTION_RE.some(re => re.test(tDesc))) {
|
|
220
|
+
const tline = _findLineOf(raw, tDesc.slice(0, 40));
|
|
221
|
+
push({
|
|
222
|
+
id: `mcp-audit:${fp}:${tline}:prompt-injection-tool-desc`,
|
|
223
|
+
kind: 'mcp', severity: 'critical',
|
|
224
|
+
vuln: 'MCP: prompt-injection text inside tool description',
|
|
225
|
+
cwe: 'CWE-1336', stride: 'Spoofing',
|
|
226
|
+
file: fp, line: tline, snippet: tDesc.slice(0, 200),
|
|
227
|
+
fix: `Tool "${t.name || '(unnamed)'}" carries an instruction inside its description. The agent reads tool descriptions as part of its system context. Remove the injection.`,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return findings;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Public for tests + the engine
|
|
237
|
+
export const _internal = {
|
|
238
|
+
_isMcpConfigFile,
|
|
239
|
+
_HARDCODED_CRED_RE,
|
|
240
|
+
_PROMPT_INJECTION_RE,
|
|
241
|
+
_DANGEROUS_CAPABILITY_NAMES,
|
|
242
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// Mobile manifest audit — AndroidManifest.xml + Info.plist + module.json5.
|
|
2
|
+
//
|
|
3
|
+
// Cross-language: catches mobile security misconfig regardless of whether
|
|
4
|
+
// the codebase is Java / Kotlin / Swift / Dart / ArkTS.
|
|
5
|
+
//
|
|
6
|
+
// Coverage:
|
|
7
|
+
// AndroidManifest.xml
|
|
8
|
+
// - android:exported="true" on non-launcher activities
|
|
9
|
+
// - <application android:debuggable="true">
|
|
10
|
+
// - <application android:allowBackup="true"> with sensitive data hints
|
|
11
|
+
// - <application android:usesCleartextTraffic="true">
|
|
12
|
+
// - dangerous permissions without rationale
|
|
13
|
+
// Info.plist
|
|
14
|
+
// - NSAllowsArbitraryLoads = true (ATS bypass)
|
|
15
|
+
// - Missing NS*UsageDescription for declared permissions
|
|
16
|
+
// module.json5 (HarmonyOS)
|
|
17
|
+
// - Permission without usedScene / reason
|
|
18
|
+
|
|
19
|
+
const _ANDROID_MANIFEST_RE = /(?:^|[\\/])AndroidManifest\.xml$/i;
|
|
20
|
+
const _INFO_PLIST_RE = /(?:^|[\\/])Info\.plist$/i;
|
|
21
|
+
const _MODULE_JSON5_RE = /(?:^|[\\/])module\.json5$/i;
|
|
22
|
+
|
|
23
|
+
function _line(raw, idx) {
|
|
24
|
+
return raw.slice(0, idx).split('\n').length;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function scanMobileManifest(file, raw) {
|
|
28
|
+
if (!file || !raw || typeof raw !== 'string') return [];
|
|
29
|
+
if (raw.length > 200_000) return [];
|
|
30
|
+
|
|
31
|
+
if (_ANDROID_MANIFEST_RE.test(file)) return _scanAndroidManifest(file, raw);
|
|
32
|
+
if (_INFO_PLIST_RE.test(file)) return _scanInfoPlist(file, raw);
|
|
33
|
+
if (_MODULE_JSON5_RE.test(file)) return _scanModuleJson5(file, raw);
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function _scanAndroidManifest(file, raw) {
|
|
38
|
+
const findings = [];
|
|
39
|
+
|
|
40
|
+
// android:debuggable="true"
|
|
41
|
+
for (const m of raw.matchAll(/android:debuggable\s*=\s*["']true["']/g)) {
|
|
42
|
+
findings.push({
|
|
43
|
+
id: `mobile-android:debuggable-true:${file}:${_line(raw, m.index)}`,
|
|
44
|
+
file, line: _line(raw, m.index),
|
|
45
|
+
vuln: 'AndroidManifest <application android:debuggable="true">',
|
|
46
|
+
severity: 'critical',
|
|
47
|
+
family: 'mobile-android-debuggable',
|
|
48
|
+
cwe: 'CWE-489',
|
|
49
|
+
confidence: 0.95,
|
|
50
|
+
description: 'A debuggable APK shipped to production allows any user with adb to attach jdb, inspect process memory, and arbitrarily call methods. JADX + Frida pair this into a one-step bypass for any client-side check.',
|
|
51
|
+
remediation: 'Remove android:debuggable from <application>, or set false in release builds. Most build systems strip this from release variants automatically — verify your release manifest.',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// usesCleartextTraffic="true"
|
|
56
|
+
for (const m of raw.matchAll(/android:usesCleartextTraffic\s*=\s*["']true["']/g)) {
|
|
57
|
+
findings.push({
|
|
58
|
+
id: `mobile-android:cleartext-traffic:${file}:${_line(raw, m.index)}`,
|
|
59
|
+
file, line: _line(raw, m.index),
|
|
60
|
+
vuln: 'AndroidManifest android:usesCleartextTraffic="true"',
|
|
61
|
+
severity: 'high',
|
|
62
|
+
family: 'mobile-android-cleartext',
|
|
63
|
+
cwe: 'CWE-319',
|
|
64
|
+
confidence: 0.95,
|
|
65
|
+
description: 'Allows the app to make plaintext HTTP requests. Network-level attackers on the same Wi-Fi can intercept and tamper with traffic.',
|
|
66
|
+
remediation: 'Set usesCleartextTraffic="false" and rely on TLS. If specific dev/internal hosts genuinely need HTTP, scope them via network_security_config.xml domain-config — never the whole app.',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// android:exported="true" on non-launcher activities (no LAUNCHER intent filter)
|
|
71
|
+
// Walk each <activity ... android:exported="true" ...> ... </activity>
|
|
72
|
+
const activityRe = /<activity\b([^>]*)>([\s\S]*?)<\/activity>|<activity\b([^/]*)\/>/g;
|
|
73
|
+
let am;
|
|
74
|
+
while ((am = activityRe.exec(raw))) {
|
|
75
|
+
const head = am[1] || am[3] || '';
|
|
76
|
+
const body = am[2] || '';
|
|
77
|
+
if (!/android:exported\s*=\s*["']true["']/.test(head)) continue;
|
|
78
|
+
// Skip the launcher activity (which MUST be exported).
|
|
79
|
+
if (/android\.intent\.category\.LAUNCHER/.test(body) ||
|
|
80
|
+
/android\.intent\.category\.LAUNCHER/.test(head)) continue;
|
|
81
|
+
findings.push({
|
|
82
|
+
id: `mobile-android:exported-true:${file}:${_line(raw, am.index)}`,
|
|
83
|
+
file, line: _line(raw, am.index),
|
|
84
|
+
vuln: 'AndroidManifest <activity android:exported="true"> on a non-launcher activity',
|
|
85
|
+
severity: 'high',
|
|
86
|
+
family: 'mobile-android-exported',
|
|
87
|
+
cwe: 'CWE-926',
|
|
88
|
+
confidence: 0.85,
|
|
89
|
+
description: 'Any other app on the device can start this Activity via Intent. Combined with a vulnerable intent extras handler, this is a one-app RCE.',
|
|
90
|
+
remediation: 'Set android:exported="false" unless another app genuinely needs to start this Activity. If it does, verify intent extras and require android:permission to gate access.',
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// allowBackup="true" with no fullBackupContent restrictions
|
|
95
|
+
if (/android:allowBackup\s*=\s*["']true["']/.test(raw) && !/android:fullBackupContent\s*=/.test(raw)) {
|
|
96
|
+
const m = /android:allowBackup\s*=\s*["']true["']/.exec(raw);
|
|
97
|
+
findings.push({
|
|
98
|
+
id: `mobile-android:allow-backup:${file}:${_line(raw, m.index)}`,
|
|
99
|
+
file, line: _line(raw, m.index),
|
|
100
|
+
vuln: 'AndroidManifest android:allowBackup="true" without fullBackupContent restriction',
|
|
101
|
+
severity: 'medium',
|
|
102
|
+
family: 'mobile-android-backup',
|
|
103
|
+
cwe: 'CWE-552',
|
|
104
|
+
confidence: 0.75,
|
|
105
|
+
description: 'On debug-enabled devices, adb can pull the app\'s entire data dir (preferences, databases, files) via adb backup. Without a fullBackupContent restriction, sensitive data is included.',
|
|
106
|
+
remediation: 'Set allowBackup="false", or provide fullBackupContent="@xml/backup_rules" with an explicit include/exclude list.',
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return findings;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function _scanInfoPlist(file, raw) {
|
|
114
|
+
const findings = [];
|
|
115
|
+
|
|
116
|
+
// NSAllowsArbitraryLoads = true
|
|
117
|
+
const re = /<key>\s*NSAllowsArbitraryLoads\s*<\/key>\s*<true\s*\/>/i;
|
|
118
|
+
if (re.test(raw)) {
|
|
119
|
+
const m = re.exec(raw);
|
|
120
|
+
findings.push({
|
|
121
|
+
id: `mobile-ios:ats-disabled:${file}:${_line(raw, m.index)}`,
|
|
122
|
+
file, line: _line(raw, m.index),
|
|
123
|
+
vuln: 'Info.plist NSAllowsArbitraryLoads = true — App Transport Security disabled',
|
|
124
|
+
severity: 'high',
|
|
125
|
+
family: 'mobile-ios-ats-disabled',
|
|
126
|
+
cwe: 'CWE-319',
|
|
127
|
+
confidence: 0.95,
|
|
128
|
+
description: 'ATS-disabled apps make plaintext HTTP calls to arbitrary hosts. Network-level attackers can intercept tokens / PII / session cookies trivially.',
|
|
129
|
+
remediation: 'Remove NSAllowsArbitraryLoads or set to false. If a specific domain genuinely needs HTTP (legacy backend), scope via NSExceptionDomains with NSExceptionAllowsInsecureHTTPLoads on that single host.',
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Permissions declared without usage description — known iOS keys.
|
|
134
|
+
const IOS_PERMS = [
|
|
135
|
+
['NSCameraUsageDescription', 'camera'],
|
|
136
|
+
['NSMicrophoneUsageDescription', 'microphone'],
|
|
137
|
+
['NSLocationWhenInUseUsageDescription', 'location-when-in-use'],
|
|
138
|
+
['NSLocationAlwaysAndWhenInUseUsageDescription', 'location-always'],
|
|
139
|
+
['NSPhotoLibraryUsageDescription', 'photo library'],
|
|
140
|
+
['NSContactsUsageDescription', 'contacts'],
|
|
141
|
+
['NSCalendarsUsageDescription', 'calendar'],
|
|
142
|
+
['NSBluetoothAlwaysUsageDescription', 'bluetooth'],
|
|
143
|
+
['NSAppleMusicUsageDescription', 'media library'],
|
|
144
|
+
['NSMotionUsageDescription', 'motion'],
|
|
145
|
+
['NSFaceIDUsageDescription', 'face id'],
|
|
146
|
+
];
|
|
147
|
+
for (const [key, label] of IOS_PERMS) {
|
|
148
|
+
// Check: <key>KEY</key> present AND followed by empty <string></string> (within 200 chars)
|
|
149
|
+
const re2 = new RegExp(`<key>\\s*${key}\\s*</key>\\s*<string>\\s*</string>`, 'i');
|
|
150
|
+
if (re2.test(raw)) {
|
|
151
|
+
const m = re2.exec(raw);
|
|
152
|
+
findings.push({
|
|
153
|
+
id: `mobile-ios:empty-usage-desc:${file}:${_line(raw, m.index)}:${label}`,
|
|
154
|
+
file, line: _line(raw, m.index),
|
|
155
|
+
vuln: `Info.plist ${key} declared but description is empty`,
|
|
156
|
+
severity: 'low',
|
|
157
|
+
family: 'mobile-ios-empty-permission-rationale',
|
|
158
|
+
cwe: 'CWE-1059',
|
|
159
|
+
confidence: 0.9,
|
|
160
|
+
description: `iOS displays the usage description to the user when the app first requests ${label} access. An empty string leads to App Store rejection and a worse user-trust signal.`,
|
|
161
|
+
remediation: `Provide a clear, specific reason: <string>Camera access lets you scan QR codes for fast pairing.</string>`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return findings;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function _scanModuleJson5(file, raw) {
|
|
170
|
+
const findings = [];
|
|
171
|
+
// HarmonyOS: declared permission without usedScene or reason.
|
|
172
|
+
// Cheap regex check on the JSON5.
|
|
173
|
+
const permRe = /"requestPermissions"\s*:\s*\[([\s\S]*?)\]/;
|
|
174
|
+
const m = permRe.exec(raw);
|
|
175
|
+
if (!m) return findings;
|
|
176
|
+
const body = m[1];
|
|
177
|
+
// For each block { "name": "...", ... } in the array, check usedScene / reason.
|
|
178
|
+
for (const pm of body.matchAll(/\{[^{}]*"name"\s*:\s*"([^"]+)"[^{}]*\}/g)) {
|
|
179
|
+
const block = pm[0];
|
|
180
|
+
const permName = pm[1];
|
|
181
|
+
if (/"usedScene"\s*:/.test(block) && /"reason"\s*:/.test(block)) continue;
|
|
182
|
+
findings.push({
|
|
183
|
+
id: `mobile-harmony:missing-permission-rationale:${file}:${_line(raw, pm.index + m.index)}:${permName}`,
|
|
184
|
+
file, line: _line(raw, pm.index + m.index),
|
|
185
|
+
vuln: `HarmonyOS module.json5 permission "${permName}" missing usedScene or reason`,
|
|
186
|
+
severity: 'low',
|
|
187
|
+
family: 'mobile-harmony-permission-rationale',
|
|
188
|
+
cwe: 'CWE-862',
|
|
189
|
+
confidence: 0.85,
|
|
190
|
+
description: 'HarmonyOS requires every requested permission to declare usedScene and a user-facing reason. Missing the rationale leads to runtime denial.',
|
|
191
|
+
remediation: 'Add "usedScene": { "abilities": ["EntryAbility"], "when": "always" } and "reason": "$string:permission_reason".',
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return findings;
|
|
195
|
+
}
|