hullproof 0.0.0-stage → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +222 -0
- package/NOTICE +32 -0
- package/README.md +313 -2
- package/checks/db.firebase-rules-not-open.json +24 -0
- package/checks/db.rls-every-table.json +25 -0
- package/checks/log.no-credential-in-logs.json +30 -0
- package/checks/secrets.no-env-file-committed.json +31 -0
- package/checks/secrets.no-secret-in-public-env.json +37 -0
- package/checks/storage.firebase-rules-not-open.json +22 -0
- package/checks/supply.no-untrusted-event-in-run.json +23 -0
- package/checks/web.cors-no-reflected-origin.json +31 -0
- package/detectors/ai.json +25 -0
- package/detectors/auth.json +27 -0
- package/detectors/ci.json +19 -0
- package/detectors/containers.json +8 -0
- package/detectors/data_stores.json +38 -0
- package/detectors/frameworks.json +94 -0
- package/detectors/hosting.json +44 -0
- package/detectors/iac.json +43 -0
- package/detectors/languages.json +48 -0
- package/detectors/mobile.json +22 -0
- package/detectors/package_managers.json +39 -0
- package/detectors/payments.json +19 -0
- package/detectors/runtimes.json +17 -0
- package/dist/checks/code/admin-role.js +46 -0
- package/dist/checks/code/owner-filter.js +91 -0
- package/dist/checks/code/shared.js +84 -0
- package/dist/checks/code/webhook-verify.js +77 -0
- package/dist/checks/context.js +64 -0
- package/dist/checks/map/config-checks.js +74 -0
- package/dist/checks/map/entry-checks.js +97 -0
- package/dist/checks/map/shared.js +44 -0
- package/dist/checks/ops.js +307 -0
- package/dist/checks/pro-map.js +1 -0
- package/dist/checks/run.js +92 -0
- package/dist/checks/types.js +1 -0
- package/dist/ci/comment.js +44 -0
- package/dist/ci/sarif-check.js +157 -0
- package/dist/ci/sarif.js +61 -0
- package/dist/cli.js +358 -0
- package/dist/commands/ci.js +115 -0
- package/dist/commands/git.js +58 -0
- package/dist/commands/hooks.js +30 -0
- package/dist/commands/hunt-pro.js +1 -0
- package/dist/commands/hunt.js +86 -0
- package/dist/commands/map.js +33 -0
- package/dist/commands/mcp.js +17 -0
- package/dist/commands/pack.js +48 -0
- package/dist/commands/prove-pro.js +1 -0
- package/dist/commands/prove.js +173 -0
- package/dist/commands/report-gate.js +92 -0
- package/dist/commands/scan.js +158 -0
- package/dist/confirm/agreement.js +18 -0
- package/dist/confirm/chain.js +84 -0
- package/dist/confirm/confirm.js +112 -0
- package/dist/confirm/honesty.js +75 -0
- package/dist/confirm/lead.js +92 -0
- package/dist/confirm/merge.js +88 -0
- package/dist/confirm/query.js +145 -0
- package/dist/confirm/rule.js +86 -0
- package/dist/confirm/store.js +42 -0
- package/dist/confirm/summary.js +6 -0
- package/dist/confirm/synthesis.js +28 -0
- package/dist/discover/detectors.js +50 -0
- package/dist/discover/discover.js +270 -0
- package/dist/discover/extract.js +282 -0
- package/dist/discover/git.js +40 -0
- package/dist/discover/kit-files.js +47 -0
- package/dist/discover/walk.js +110 -0
- package/dist/env.js +57 -0
- package/dist/gate/gate.js +173 -0
- package/dist/hooks/install.js +153 -0
- package/dist/hooks/run.js +112 -0
- package/dist/map/analyze.js +201 -0
- package/dist/map/build.js +253 -0
- package/dist/map/candidates.js +49 -0
- package/dist/map/cblocks.js +77 -0
- package/dist/map/config.js +291 -0
- package/dist/map/dotnet.js +242 -0
- package/dist/map/entry.js +78 -0
- package/dist/map/expo.js +190 -0
- package/dist/map/go.js +292 -0
- package/dist/map/js-express.js +143 -0
- package/dist/map/js-misc.js +295 -0
- package/dist/map/js-next.js +292 -0
- package/dist/map/laravel.js +389 -0
- package/dist/map/lex-more.js +305 -0
- package/dist/map/lex.js +425 -0
- package/dist/map/light-index.js +191 -0
- package/dist/map/light-profiles.js +324 -0
- package/dist/map/light.js +574 -0
- package/dist/map/local-vocab.js +27 -0
- package/dist/map/mobile.js +326 -0
- package/dist/map/py-django.js +281 -0
- package/dist/map/py.js +391 -0
- package/dist/map/rails.js +391 -0
- package/dist/map/repo.js +178 -0
- package/dist/map/rust.js +312 -0
- package/dist/map/spring.js +207 -0
- package/dist/map/taint.js +96 -0
- package/dist/map/touches.js +355 -0
- package/dist/map/vocab.js +88 -0
- package/dist/mapping/controls.js +34 -0
- package/dist/mapping/dependency-policy.js +44 -0
- package/dist/mapping/map.js +44 -0
- package/dist/mapping/maps/gitleaks.json +12 -0
- package/dist/mapping/maps/osv-scanner.json +7 -0
- package/dist/mapping/maps/trivy.json +13 -0
- package/dist/mcp/confine.js +59 -0
- package/dist/mcp/protocol.js +140 -0
- package/dist/mcp/schema.js +43 -0
- package/dist/mcp/server.js +37 -0
- package/dist/mcp/tools.js +238 -0
- package/dist/packs/coverage.js +79 -0
- package/dist/packs/manifest.js +36 -0
- package/dist/packs/validate.js +271 -0
- package/dist/prove/container.js +276 -0
- package/dist/prove/exec-main.js +20 -0
- package/dist/prove/executor.js +149 -0
- package/dist/prove/local.js +131 -0
- package/dist/prove/probes.js +65 -0
- package/dist/prove/recipe.js +278 -0
- package/dist/prove/run.js +136 -0
- package/dist/prove/runner.js +1 -0
- package/dist/prove/safety.js +126 -0
- package/dist/prove/spec.js +357 -0
- package/dist/prove/store.js +65 -0
- package/dist/report/build.js +297 -0
- package/dist/report/entry-points.js +75 -0
- package/dist/report/hunt-section.js +71 -0
- package/dist/report/kit.js +41 -0
- package/dist/report/load.js +35 -0
- package/dist/report/messages.js +273 -0
- package/dist/report/prove-section.js +65 -0
- package/dist/report/report.js +156 -0
- package/dist/report/surface.js +21 -0
- package/dist/report/test-code.js +51 -0
- package/dist/report/tiers.js +44 -0
- package/dist/scanners/common.js +162 -0
- package/dist/scanners/gitleaks.js +90 -0
- package/dist/scanners/index.js +113 -0
- package/dist/scanners/osv-scanner.js +77 -0
- package/dist/scanners/run-targets.js +34 -0
- package/dist/scanners/secret-noise.js +89 -0
- package/dist/scanners/semgrep.js +142 -0
- package/dist/scanners/trivy.js +65 -0
- package/dist/scanners/types.js +1 -0
- package/dist/types/check.js +1 -0
- package/dist/types/codemap.js +1 -0
- package/dist/types/findings.js +5 -0
- package/dist/types/hunt.js +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/prove.js +1 -0
- package/dist/types/results.js +2 -0
- package/dist/types/scan.js +1 -0
- package/dist/types/stack.js +5 -0
- package/dist/util.js +71 -0
- package/dist/version.js +2 -0
- package/package.json +46 -4
- package/packs/android/fixture-result.json +43 -0
- package/packs/android/pack.json +56 -0
- package/packs/dotnet/fixture-result.json +62 -0
- package/packs/dotnet/pack.json +88 -0
- package/packs/expo/answer-key.json +51 -0
- package/packs/expo/fixture-result.json +62 -0
- package/packs/expo/pack.json +79 -0
- package/packs/firebase/answer-key.json +27 -0
- package/packs/firebase/fixture-result.json +43 -0
- package/packs/firebase/pack.json +58 -0
- package/packs/go/answer-key.json +101 -0
- package/packs/go/fixture-result.json +64 -0
- package/packs/go/pack.json +99 -0
- package/packs/ios/fixture-result.json +43 -0
- package/packs/ios/pack.json +55 -0
- package/packs/laravel/answer-key.json +84 -0
- package/packs/laravel/fixture-result.json +60 -0
- package/packs/laravel/pack.json +91 -0
- package/packs/node-ts/answer-key.json +220 -0
- package/packs/node-ts/fixture-result.json +114 -0
- package/packs/node-ts/pack.json +134 -0
- package/packs/python/answer-key.json +195 -0
- package/packs/python/fixture-result.json +119 -0
- package/packs/python/pack.json +121 -0
- package/packs/rails/answer-key.json +84 -0
- package/packs/rails/fixture-result.json +60 -0
- package/packs/rails/pack.json +78 -0
- package/packs/rust/fixture-result.json +43 -0
- package/packs/rust/pack.json +69 -0
- package/packs/spring/fixture-result.json +63 -0
- package/packs/spring/pack.json +103 -0
- package/packs/supabase/answer-key.json +52 -0
- package/packs/supabase/fixture-result.json +58 -0
- package/packs/supabase/pack.json +60 -0
- package/rules/gitleaks.toml +256 -0
- package/rules/semgrep/api-route-no-auth.yaml +31 -0
- package/rules/semgrep/cors-reflect-origin.yaml +48 -0
- package/rules/semgrep/eval-or-new-function.yaml +185 -0
- package/rules/semgrep/log-session-token.yaml +48 -0
- package/rules/semgrep/math-random-tokens.yaml +24 -0
- package/rules/semgrep/next-public-secret-name.yaml +32 -0
- package/rules/semgrep/postgrest-filter-injection.yaml +78 -0
- package/rules/semgrep/raw-sql-concat.yaml +42 -0
- package/rules/semgrep/read-by-id-no-owner.yaml +49 -0
- package/rules/semgrep/server-action-no-auth.yaml +26 -0
- package/rules/semgrep/sql-variants.yaml +99 -0
- package/rules/semgrep/ssrf-unguarded-url.yaml +137 -0
- package/rules/semgrep/subprocess-injection.yaml +163 -0
- package/rules/semgrep/taint-go.yaml +333 -0
- package/rules/semgrep/taint-java.yaml +317 -0
- package/rules/semgrep/taint-javascript.yaml +412 -0
- package/rules/semgrep/taint-php.yaml +361 -0
- package/rules/semgrep/taint-python.yaml +414 -0
- package/rules/semgrep/tool-endpoint-admin-client.yaml +86 -0
- package/rules/semgrep/user-metadata-authz.yaml +22 -0
- package/rules/semgrep/verify-result-unused.yaml +62 -0
- package/rules/semgrep/webhook-parse-before-verify.yaml +29 -0
- package/rules/test-artifact-ids.json +4 -0
- package/rules/tiers.json +427 -0
- package/schemas/check-results.schema.json +55 -0
- package/schemas/check.schema.json +41 -0
- package/schemas/codemap.schema.json +216 -0
- package/schemas/findings.schema.json +1048 -0
- package/schemas/hunt-output.schema.json +46 -0
- package/schemas/hunt-result.schema.json +58 -0
- package/schemas/results.schema.json +17 -0
- package/schemas/scan-hits.schema.json +32 -0
- package/schemas/scan-meta.schema.json +25 -0
- package/schemas/stack.schema.json +93 -0
- package/src/checks/README.md +32 -0
- package/src/checks/code/admin-role.ts +49 -0
- package/src/checks/code/owner-filter.ts +95 -0
- package/src/checks/code/shared.ts +66 -0
- package/src/checks/code/webhook-verify.ts +75 -0
- package/src/checks/context.ts +72 -0
- package/src/checks/map/config-checks.ts +79 -0
- package/src/checks/map/entry-checks.ts +116 -0
- package/src/checks/map/shared.ts +63 -0
- package/src/checks/ops.ts +280 -0
- package/src/checks/pro-map.ts +4 -0
- package/src/checks/run.ts +80 -0
- package/src/checks/types.ts +12 -0
- package/src/ci/comment.ts +52 -0
- package/src/ci/sarif-check.ts +100 -0
- package/src/ci/sarif.ts +72 -0
- package/src/cli.ts +273 -0
- package/src/commands/ci.ts +104 -0
- package/src/commands/git.ts +59 -0
- package/src/commands/hooks.ts +27 -0
- package/src/commands/hunt-pro.ts +4 -0
- package/src/commands/hunt.ts +68 -0
- package/src/commands/map.ts +27 -0
- package/src/commands/mcp.ts +18 -0
- package/src/commands/pack.ts +34 -0
- package/src/commands/prove-pro.ts +4 -0
- package/src/commands/prove.ts +126 -0
- package/src/commands/report-gate.ts +75 -0
- package/src/commands/scan.ts +156 -0
- package/src/confirm/agreement.ts +23 -0
- package/src/confirm/chain.ts +67 -0
- package/src/confirm/confirm.ts +107 -0
- package/src/confirm/honesty.ts +65 -0
- package/src/confirm/lead.ts +76 -0
- package/src/confirm/merge.ts +89 -0
- package/src/confirm/query.ts +126 -0
- package/src/confirm/rule.ts +76 -0
- package/src/confirm/store.ts +42 -0
- package/src/confirm/summary.ts +9 -0
- package/src/confirm/synthesis.ts +33 -0
- package/src/discover/detectors.ts +71 -0
- package/src/discover/discover.ts +253 -0
- package/src/discover/extract.ts +235 -0
- package/src/discover/git.ts +31 -0
- package/src/discover/kit-files.ts +37 -0
- package/src/discover/walk.ts +88 -0
- package/src/env.ts +51 -0
- package/src/gate/gate.ts +169 -0
- package/src/hooks/install.ts +142 -0
- package/src/hooks/run.ts +121 -0
- package/src/map/analyze.ts +219 -0
- package/src/map/build.ts +206 -0
- package/src/map/candidates.ts +47 -0
- package/src/map/cblocks.ts +55 -0
- package/src/map/config.ts +207 -0
- package/src/map/dotnet.ts +205 -0
- package/src/map/entry.ts +80 -0
- package/src/map/expo.ts +171 -0
- package/src/map/go.ts +271 -0
- package/src/map/js-express.ts +135 -0
- package/src/map/js-misc.ts +221 -0
- package/src/map/js-next.ts +282 -0
- package/src/map/laravel.ts +327 -0
- package/src/map/lex-more.ts +152 -0
- package/src/map/lex.ts +274 -0
- package/src/map/light-index.ts +134 -0
- package/src/map/light-profiles.ts +363 -0
- package/src/map/light.ts +484 -0
- package/src/map/local-vocab.ts +30 -0
- package/src/map/mobile.ts +263 -0
- package/src/map/py-django.ts +239 -0
- package/src/map/py.ts +329 -0
- package/src/map/rails.ts +291 -0
- package/src/map/repo.ts +150 -0
- package/src/map/rust.ts +256 -0
- package/src/map/spring.ts +185 -0
- package/src/map/taint.ts +86 -0
- package/src/map/touches.ts +296 -0
- package/src/map/vocab.ts +106 -0
- package/src/mapping/controls.ts +47 -0
- package/src/mapping/dependency-policy.ts +55 -0
- package/src/mapping/map.ts +59 -0
- package/src/mapping/maps/gitleaks.json +12 -0
- package/src/mapping/maps/osv-scanner.json +7 -0
- package/src/mapping/maps/trivy.json +13 -0
- package/src/mcp/confine.ts +44 -0
- package/src/mcp/protocol.ts +144 -0
- package/src/mcp/schema.ts +35 -0
- package/src/mcp/server.ts +32 -0
- package/src/mcp/tools.ts +235 -0
- package/src/packs/coverage.ts +98 -0
- package/src/packs/manifest.ts +97 -0
- package/src/packs/validate.ts +204 -0
- package/src/prove/container.ts +237 -0
- package/src/prove/exec-main.ts +16 -0
- package/src/prove/executor.ts +130 -0
- package/src/prove/local.ts +100 -0
- package/src/prove/probes.ts +68 -0
- package/src/prove/recipe.ts +215 -0
- package/src/prove/run.ts +124 -0
- package/src/prove/runner.ts +28 -0
- package/src/prove/safety.ts +98 -0
- package/src/prove/spec.ts +256 -0
- package/src/prove/store.ts +48 -0
- package/src/report/build.ts +304 -0
- package/src/report/entry-points.ts +78 -0
- package/src/report/hunt-section.ts +67 -0
- package/src/report/kit.ts +59 -0
- package/src/report/load.ts +49 -0
- package/src/report/messages.ts +270 -0
- package/src/report/prove-section.ts +61 -0
- package/src/report/report.ts +160 -0
- package/src/report/surface.ts +32 -0
- package/src/report/test-code.ts +56 -0
- package/src/report/tiers.ts +50 -0
- package/src/scanners/common.ts +149 -0
- package/src/scanners/gitleaks.ts +89 -0
- package/src/scanners/index.ts +103 -0
- package/src/scanners/osv-scanner.ts +90 -0
- package/src/scanners/run-targets.ts +35 -0
- package/src/scanners/secret-noise.ts +80 -0
- package/src/scanners/semgrep.ts +139 -0
- package/src/scanners/trivy.ts +66 -0
- package/src/scanners/types.ts +98 -0
- package/src/types/check.ts +52 -0
- package/src/types/codemap.ts +142 -0
- package/src/types/findings.ts +107 -0
- package/src/types/hunt.ts +97 -0
- package/src/types/index.ts +10 -0
- package/src/types/prove.ts +83 -0
- package/src/types/results.ts +15 -0
- package/src/types/scan.ts +63 -0
- package/src/types/stack.ts +61 -0
- package/src/util.ts +59 -0
- package/src/version.ts +2 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 Rare Phronesis Limited
|
|
3
|
+
# Written for the Hullproof Engine from the CWE definitions named in each rule. Not copied or derived from any Semgrep
|
|
4
|
+
# registry rule. Fixtures: test/fixtures/rules/taint-python.py, run by test/rules.test.ts with semgrep --test.
|
|
5
|
+
#
|
|
6
|
+
# Sources are request data in Flask, Django (and Django REST framework) and FastAPI (and Starlette): the request object's
|
|
7
|
+
# fields and the parameters of a function under a route decorator. Taint flows through string building, assignment and
|
|
8
|
+
# calls by default. Each rule names its sanitizers; a project guard is recognised when its name says what it checks.
|
|
9
|
+
rules:
|
|
10
|
+
- id: hullproof-taint-py-sql-injection
|
|
11
|
+
languages: [python]
|
|
12
|
+
mode: taint
|
|
13
|
+
severity: ERROR
|
|
14
|
+
message: >-
|
|
15
|
+
Request data reaches the text of a SQL query (CWE-89). Pass the value as a bound parameter, for example
|
|
16
|
+
cursor.execute("SELECT * FROM t WHERE id = %s", (value,)), and keep the query text constant.
|
|
17
|
+
metadata:
|
|
18
|
+
cwe: "CWE-89: Improper Neutralization of Special Elements used in an SQL Command"
|
|
19
|
+
sec_ids: [SEC-API-017]
|
|
20
|
+
pattern-sources:
|
|
21
|
+
- pattern: flask.request
|
|
22
|
+
- patterns:
|
|
23
|
+
- pattern-either:
|
|
24
|
+
- pattern: $REQ.GET
|
|
25
|
+
- pattern: $REQ.POST
|
|
26
|
+
- pattern: $REQ.COOKIES
|
|
27
|
+
- pattern: $REQ.META
|
|
28
|
+
- pattern: $REQ.FILES
|
|
29
|
+
- pattern: $REQ.body
|
|
30
|
+
- pattern: $REQ.headers
|
|
31
|
+
- pattern: $REQ.cookies
|
|
32
|
+
- pattern: $REQ.data
|
|
33
|
+
- pattern: $REQ.query_params
|
|
34
|
+
- pattern: $REQ.path_params
|
|
35
|
+
- pattern: $REQ.json()
|
|
36
|
+
- pattern: $REQ.body()
|
|
37
|
+
- pattern: $REQ.form()
|
|
38
|
+
- metavariable-regex:
|
|
39
|
+
metavariable: $REQ
|
|
40
|
+
regex: ^(request|req|self\.request)$
|
|
41
|
+
- patterns:
|
|
42
|
+
- pattern-either:
|
|
43
|
+
- pattern-inside: |
|
|
44
|
+
@$APP.$VERB(...)
|
|
45
|
+
def $F(..., $P, ...):
|
|
46
|
+
...
|
|
47
|
+
- pattern-inside: |
|
|
48
|
+
@$APP.$VERB(...)
|
|
49
|
+
async def $F(..., $P, ...):
|
|
50
|
+
...
|
|
51
|
+
- metavariable-regex:
|
|
52
|
+
metavariable: $VERB
|
|
53
|
+
regex: ^(route|get|post|put|patch|delete|api_route|websocket)$
|
|
54
|
+
- focus-metavariable: $P
|
|
55
|
+
pattern-sanitizers:
|
|
56
|
+
- pattern-either:
|
|
57
|
+
- pattern: int(...)
|
|
58
|
+
- pattern: float(...)
|
|
59
|
+
- pattern: bool(...)
|
|
60
|
+
- pattern: uuid.UUID(...)
|
|
61
|
+
pattern-sinks:
|
|
62
|
+
- patterns:
|
|
63
|
+
- pattern-either:
|
|
64
|
+
- pattern: $C.execute($Q, ...)
|
|
65
|
+
- pattern: $C.executemany($Q, ...)
|
|
66
|
+
- pattern: $C.executescript($Q, ...)
|
|
67
|
+
- pattern: $C.exec_driver_sql($Q, ...)
|
|
68
|
+
- pattern: $M.objects.raw($Q, ...)
|
|
69
|
+
- pattern: $M.objects.extra(..., where=$Q, ...)
|
|
70
|
+
- pattern: django.db.models.expressions.RawSQL($Q, ...)
|
|
71
|
+
- pattern: sqlalchemy.text($Q, ...)
|
|
72
|
+
- pattern: pandas.read_sql($Q, ...)
|
|
73
|
+
- pattern: pandas.read_sql_query($Q, ...)
|
|
74
|
+
- focus-metavariable: $Q
|
|
75
|
+
|
|
76
|
+
- id: hullproof-taint-py-xss
|
|
77
|
+
languages: [python]
|
|
78
|
+
mode: taint
|
|
79
|
+
severity: ERROR
|
|
80
|
+
message: >-
|
|
81
|
+
Request data is written into an HTML response without encoding (CWE-79). Render it through an autoescaping template
|
|
82
|
+
(render_template, Django templates) or encode it with html.escape or markupsafe.escape before it is placed in HTML.
|
|
83
|
+
metadata:
|
|
84
|
+
cwe: "CWE-79: Improper Neutralization of Input During Web Page Generation"
|
|
85
|
+
sec_ids: [SEC-WEB-031]
|
|
86
|
+
pattern-sources:
|
|
87
|
+
- pattern: flask.request
|
|
88
|
+
- patterns:
|
|
89
|
+
- pattern-either:
|
|
90
|
+
- pattern: $REQ.GET
|
|
91
|
+
- pattern: $REQ.POST
|
|
92
|
+
- pattern: $REQ.COOKIES
|
|
93
|
+
- pattern: $REQ.META
|
|
94
|
+
- pattern: $REQ.FILES
|
|
95
|
+
- pattern: $REQ.body
|
|
96
|
+
- pattern: $REQ.headers
|
|
97
|
+
- pattern: $REQ.cookies
|
|
98
|
+
- pattern: $REQ.data
|
|
99
|
+
- pattern: $REQ.query_params
|
|
100
|
+
- pattern: $REQ.path_params
|
|
101
|
+
- pattern: $REQ.json()
|
|
102
|
+
- pattern: $REQ.body()
|
|
103
|
+
- pattern: $REQ.form()
|
|
104
|
+
- metavariable-regex:
|
|
105
|
+
metavariable: $REQ
|
|
106
|
+
regex: ^(request|req|self\.request)$
|
|
107
|
+
- patterns:
|
|
108
|
+
- pattern-either:
|
|
109
|
+
- pattern-inside: |
|
|
110
|
+
@$APP.$VERB(...)
|
|
111
|
+
def $F(..., $P, ...):
|
|
112
|
+
...
|
|
113
|
+
- pattern-inside: |
|
|
114
|
+
@$APP.$VERB(...)
|
|
115
|
+
async def $F(..., $P, ...):
|
|
116
|
+
...
|
|
117
|
+
- metavariable-regex:
|
|
118
|
+
metavariable: $VERB
|
|
119
|
+
regex: ^(route|get|post|put|patch|delete|api_route|websocket)$
|
|
120
|
+
- focus-metavariable: $P
|
|
121
|
+
pattern-sanitizers:
|
|
122
|
+
- pattern-either:
|
|
123
|
+
- pattern: html.escape(...)
|
|
124
|
+
- pattern: markupsafe.escape(...)
|
|
125
|
+
- pattern: flask.escape(...)
|
|
126
|
+
- pattern: django.utils.html.escape(...)
|
|
127
|
+
- pattern: django.utils.html.conditional_escape(...)
|
|
128
|
+
- pattern: bleach.clean(...)
|
|
129
|
+
- pattern: flask.jsonify(...)
|
|
130
|
+
- pattern: flask.render_template(...)
|
|
131
|
+
- pattern: django.shortcuts.render(...)
|
|
132
|
+
- pattern: flask.redirect(...)
|
|
133
|
+
- pattern: flask.url_for(...)
|
|
134
|
+
- pattern: flask.send_file(...)
|
|
135
|
+
- pattern: flask.send_from_directory(...)
|
|
136
|
+
- pattern: urllib.parse.quote(...)
|
|
137
|
+
- pattern: int(...)
|
|
138
|
+
- pattern: float(...)
|
|
139
|
+
- pattern: bool(...)
|
|
140
|
+
- pattern: len(...)
|
|
141
|
+
pattern-sinks:
|
|
142
|
+
# A Flask view's return value becomes a text/html response when it is a string. Dict and list returns become JSON.
|
|
143
|
+
- patterns:
|
|
144
|
+
- pattern-inside: |
|
|
145
|
+
@$APP.route(...)
|
|
146
|
+
def $F(...):
|
|
147
|
+
...
|
|
148
|
+
- pattern-either:
|
|
149
|
+
- pattern: return $X
|
|
150
|
+
- pattern: return $X, ...
|
|
151
|
+
- pattern-not: return {...}
|
|
152
|
+
- pattern-not: return [...]
|
|
153
|
+
- pattern-not: return {...}, ...
|
|
154
|
+
- focus-metavariable: $X
|
|
155
|
+
- patterns:
|
|
156
|
+
- pattern-either:
|
|
157
|
+
- pattern: flask.make_response($X, ...)
|
|
158
|
+
- pattern: flask.Response($X, ...)
|
|
159
|
+
- pattern: flask.render_template_string($X, ...)
|
|
160
|
+
- pattern: markupsafe.Markup($X)
|
|
161
|
+
- pattern: django.http.HttpResponse($X, ...)
|
|
162
|
+
- pattern: django.utils.safestring.mark_safe($X)
|
|
163
|
+
- pattern: django.utils.html.format_html($X, ...)
|
|
164
|
+
- pattern: fastapi.responses.HTMLResponse($X, ...)
|
|
165
|
+
- pattern: starlette.responses.HTMLResponse($X, ...)
|
|
166
|
+
- pattern-not: flask.Response($X, ..., mimetype="application/json", ...)
|
|
167
|
+
- pattern-not: django.http.HttpResponse($X, ..., content_type="application/json", ...)
|
|
168
|
+
- focus-metavariable: $X
|
|
169
|
+
|
|
170
|
+
- id: hullproof-taint-py-command-injection
|
|
171
|
+
languages: [python]
|
|
172
|
+
mode: taint
|
|
173
|
+
severity: ERROR
|
|
174
|
+
message: >-
|
|
175
|
+
Request data reaches a shell command or chooses the program that runs (CWE-78). Run a fixed program with an
|
|
176
|
+
argument list and no shell, map the request value to a fixed set of options, or quote it with shlex.quote when a
|
|
177
|
+
shell cannot be avoided.
|
|
178
|
+
metadata:
|
|
179
|
+
cwe: "CWE-78: Improper Neutralization of Special Elements used in an OS Command"
|
|
180
|
+
sec_ids: [SEC-API-020]
|
|
181
|
+
pattern-sources:
|
|
182
|
+
- pattern: flask.request
|
|
183
|
+
- patterns:
|
|
184
|
+
- pattern-either:
|
|
185
|
+
- pattern: $REQ.GET
|
|
186
|
+
- pattern: $REQ.POST
|
|
187
|
+
- pattern: $REQ.COOKIES
|
|
188
|
+
- pattern: $REQ.META
|
|
189
|
+
- pattern: $REQ.FILES
|
|
190
|
+
- pattern: $REQ.body
|
|
191
|
+
- pattern: $REQ.headers
|
|
192
|
+
- pattern: $REQ.cookies
|
|
193
|
+
- pattern: $REQ.data
|
|
194
|
+
- pattern: $REQ.query_params
|
|
195
|
+
- pattern: $REQ.path_params
|
|
196
|
+
- pattern: $REQ.json()
|
|
197
|
+
- pattern: $REQ.body()
|
|
198
|
+
- pattern: $REQ.form()
|
|
199
|
+
- metavariable-regex:
|
|
200
|
+
metavariable: $REQ
|
|
201
|
+
regex: ^(request|req|self\.request)$
|
|
202
|
+
- patterns:
|
|
203
|
+
- pattern-either:
|
|
204
|
+
- pattern-inside: |
|
|
205
|
+
@$APP.$VERB(...)
|
|
206
|
+
def $F(..., $P, ...):
|
|
207
|
+
...
|
|
208
|
+
- pattern-inside: |
|
|
209
|
+
@$APP.$VERB(...)
|
|
210
|
+
async def $F(..., $P, ...):
|
|
211
|
+
...
|
|
212
|
+
- metavariable-regex:
|
|
213
|
+
metavariable: $VERB
|
|
214
|
+
regex: ^(route|get|post|put|patch|delete|api_route|websocket)$
|
|
215
|
+
- focus-metavariable: $P
|
|
216
|
+
pattern-sanitizers:
|
|
217
|
+
- pattern-either:
|
|
218
|
+
- pattern: shlex.quote(...)
|
|
219
|
+
- pattern: pipes.quote(...)
|
|
220
|
+
- pattern: int(...)
|
|
221
|
+
- pattern: float(...)
|
|
222
|
+
pattern-sinks:
|
|
223
|
+
- patterns:
|
|
224
|
+
- pattern-either:
|
|
225
|
+
- pattern: os.system($X)
|
|
226
|
+
- pattern: os.popen($X, ...)
|
|
227
|
+
- pattern: subprocess.getoutput($X)
|
|
228
|
+
- pattern: subprocess.getstatusoutput($X)
|
|
229
|
+
- pattern: asyncio.create_subprocess_shell($X, ...)
|
|
230
|
+
- pattern: subprocess.$F($X, ..., shell=True, ...)
|
|
231
|
+
- pattern: subprocess.$F([$X, ...], ...)
|
|
232
|
+
- pattern: subprocess.$F([$SH, "-c", $X, ...], ...)
|
|
233
|
+
- patterns:
|
|
234
|
+
- pattern: os.$EXEC($X, ...)
|
|
235
|
+
- metavariable-regex:
|
|
236
|
+
metavariable: $EXEC
|
|
237
|
+
regex: ^(execl|execle|execlp|execv|execve|execvp|execvpe|spawnl|spawnlp|spawnv|spawnvp)$
|
|
238
|
+
- focus-metavariable: $X
|
|
239
|
+
- patterns:
|
|
240
|
+
- pattern: subprocess.$F($X, ...)
|
|
241
|
+
- pattern-not: subprocess.$F([...], ...)
|
|
242
|
+
- metavariable-regex:
|
|
243
|
+
metavariable: $F
|
|
244
|
+
regex: ^(run|call|check_call|check_output|Popen)$
|
|
245
|
+
- focus-metavariable: $X
|
|
246
|
+
|
|
247
|
+
- id: hullproof-taint-py-path-traversal
|
|
248
|
+
languages: [python]
|
|
249
|
+
mode: taint
|
|
250
|
+
severity: WARNING
|
|
251
|
+
message: >-
|
|
252
|
+
Request data becomes part of a file path the server opens, writes, lists or deletes (CWE-22). Reduce the name with
|
|
253
|
+
os.path.basename or werkzeug secure_filename, or resolve the full path and check that it stays inside a fixed folder
|
|
254
|
+
before using it.
|
|
255
|
+
metadata:
|
|
256
|
+
cwe: "CWE-22: Improper Limitation of a Pathname to a Restricted Directory"
|
|
257
|
+
sec_ids: []
|
|
258
|
+
pattern-sources:
|
|
259
|
+
- pattern: flask.request
|
|
260
|
+
- patterns:
|
|
261
|
+
- pattern-either:
|
|
262
|
+
- pattern: $REQ.GET
|
|
263
|
+
- pattern: $REQ.POST
|
|
264
|
+
- pattern: $REQ.COOKIES
|
|
265
|
+
- pattern: $REQ.META
|
|
266
|
+
- pattern: $REQ.FILES
|
|
267
|
+
- pattern: $REQ.body
|
|
268
|
+
- pattern: $REQ.headers
|
|
269
|
+
- pattern: $REQ.cookies
|
|
270
|
+
- pattern: $REQ.data
|
|
271
|
+
- pattern: $REQ.query_params
|
|
272
|
+
- pattern: $REQ.path_params
|
|
273
|
+
- pattern: $REQ.json()
|
|
274
|
+
- pattern: $REQ.body()
|
|
275
|
+
- pattern: $REQ.form()
|
|
276
|
+
- metavariable-regex:
|
|
277
|
+
metavariable: $REQ
|
|
278
|
+
regex: ^(request|req|self\.request)$
|
|
279
|
+
- patterns:
|
|
280
|
+
- pattern-either:
|
|
281
|
+
- pattern-inside: |
|
|
282
|
+
@$APP.$VERB(...)
|
|
283
|
+
def $F(..., $P, ...):
|
|
284
|
+
...
|
|
285
|
+
- pattern-inside: |
|
|
286
|
+
@$APP.$VERB(...)
|
|
287
|
+
async def $F(..., $P, ...):
|
|
288
|
+
...
|
|
289
|
+
- metavariable-regex:
|
|
290
|
+
metavariable: $VERB
|
|
291
|
+
regex: ^(route|get|post|put|patch|delete|api_route|websocket)$
|
|
292
|
+
- focus-metavariable: $P
|
|
293
|
+
pattern-sanitizers:
|
|
294
|
+
- pattern-either:
|
|
295
|
+
- pattern: os.path.basename(...)
|
|
296
|
+
- pattern: werkzeug.utils.secure_filename(...)
|
|
297
|
+
- pattern: werkzeug.security.safe_join(...)
|
|
298
|
+
- pattern: werkzeug.utils.safe_join(...)
|
|
299
|
+
- pattern: flask.safe_join(...)
|
|
300
|
+
- pattern: int(...)
|
|
301
|
+
- pattern: uuid.UUID(...)
|
|
302
|
+
- by-side-effect: true
|
|
303
|
+
patterns:
|
|
304
|
+
- pattern: $GUARD($X, ...)
|
|
305
|
+
- metavariable-regex:
|
|
306
|
+
metavariable: $GUARD
|
|
307
|
+
regex: (?i).*(safe|allow|valid|guard|check|verify|within|inside|contain).*
|
|
308
|
+
- focus-metavariable: $X
|
|
309
|
+
pattern-sinks:
|
|
310
|
+
- patterns:
|
|
311
|
+
- pattern-either:
|
|
312
|
+
- pattern: open($X, ...)
|
|
313
|
+
- pattern: io.open($X, ...)
|
|
314
|
+
- pattern: codecs.open($X, ...)
|
|
315
|
+
- pattern: pathlib.Path($X, ...)
|
|
316
|
+
- pattern: pathlib.Path($BASE, $X, ...)
|
|
317
|
+
- pattern: flask.send_file($X, ...)
|
|
318
|
+
- patterns:
|
|
319
|
+
- pattern: os.$OSF($X, ...)
|
|
320
|
+
- metavariable-regex:
|
|
321
|
+
metavariable: $OSF
|
|
322
|
+
regex: ^(remove|unlink|rmdir|removedirs|mkdir|makedirs|listdir|scandir|stat|chmod|chown|rename|replace|walk|open|truncate)$
|
|
323
|
+
- patterns:
|
|
324
|
+
- pattern-either:
|
|
325
|
+
- pattern: shutil.$SHF($X, ...)
|
|
326
|
+
- pattern: shutil.$SHF($SRC, $X, ...)
|
|
327
|
+
- metavariable-regex:
|
|
328
|
+
metavariable: $SHF
|
|
329
|
+
regex: ^(copy|copyfile|copy2|copytree|move|rmtree)$
|
|
330
|
+
- focus-metavariable: $X
|
|
331
|
+
|
|
332
|
+
- id: hullproof-taint-py-ssrf
|
|
333
|
+
languages: [python]
|
|
334
|
+
mode: taint
|
|
335
|
+
severity: ERROR
|
|
336
|
+
message: >-
|
|
337
|
+
Request data chooses the address of an outbound request the server makes (CWE-918). Allow only http and https,
|
|
338
|
+
resolve the host, refuse private, loopback, link local and metadata addresses, connect to the checked address and
|
|
339
|
+
do not follow redirects unchecked. A value that only fills a path or query under a fixed host is not matched.
|
|
340
|
+
metadata:
|
|
341
|
+
cwe: "CWE-918: Server-Side Request Forgery (SSRF)"
|
|
342
|
+
sec_ids: [SEC-API-035]
|
|
343
|
+
pattern-sources:
|
|
344
|
+
- pattern: flask.request
|
|
345
|
+
- patterns:
|
|
346
|
+
- pattern-either:
|
|
347
|
+
- pattern: $REQ.GET
|
|
348
|
+
- pattern: $REQ.POST
|
|
349
|
+
- pattern: $REQ.COOKIES
|
|
350
|
+
- pattern: $REQ.META
|
|
351
|
+
- pattern: $REQ.FILES
|
|
352
|
+
- pattern: $REQ.body
|
|
353
|
+
- pattern: $REQ.headers
|
|
354
|
+
- pattern: $REQ.cookies
|
|
355
|
+
- pattern: $REQ.data
|
|
356
|
+
- pattern: $REQ.query_params
|
|
357
|
+
- pattern: $REQ.path_params
|
|
358
|
+
- pattern: $REQ.json()
|
|
359
|
+
- pattern: $REQ.body()
|
|
360
|
+
- pattern: $REQ.form()
|
|
361
|
+
- metavariable-regex:
|
|
362
|
+
metavariable: $REQ
|
|
363
|
+
regex: ^(request|req|self\.request)$
|
|
364
|
+
- patterns:
|
|
365
|
+
- pattern-either:
|
|
366
|
+
- pattern-inside: |
|
|
367
|
+
@$APP.$VERB(...)
|
|
368
|
+
def $F(..., $P, ...):
|
|
369
|
+
...
|
|
370
|
+
- pattern-inside: |
|
|
371
|
+
@$APP.$VERB(...)
|
|
372
|
+
async def $F(..., $P, ...):
|
|
373
|
+
...
|
|
374
|
+
- metavariable-regex:
|
|
375
|
+
metavariable: $VERB
|
|
376
|
+
regex: ^(route|get|post|put|patch|delete|api_route|websocket)$
|
|
377
|
+
- focus-metavariable: $P
|
|
378
|
+
pattern-sanitizers:
|
|
379
|
+
- pattern-either:
|
|
380
|
+
- pattern: int(...)
|
|
381
|
+
- pattern: urllib.parse.quote(...)
|
|
382
|
+
- pattern: urllib.parse.urlencode(...)
|
|
383
|
+
# A fixed scheme and host before the request value: the value can only change the path or the query.
|
|
384
|
+
- patterns:
|
|
385
|
+
- pattern: '"$URL" + $Y'
|
|
386
|
+
- metavariable-regex:
|
|
387
|
+
metavariable: $URL
|
|
388
|
+
regex: ^https?://[^/]+/
|
|
389
|
+
- pattern-regex: f["']https?://[^/"'{]+/[^"']*["']
|
|
390
|
+
- by-side-effect: true
|
|
391
|
+
patterns:
|
|
392
|
+
- pattern: $GUARD($X, ...)
|
|
393
|
+
- metavariable-regex:
|
|
394
|
+
metavariable: $GUARD
|
|
395
|
+
regex: (?i).*(safe|allow|valid|guard|check|verify).*
|
|
396
|
+
- focus-metavariable: $X
|
|
397
|
+
pattern-sinks:
|
|
398
|
+
- patterns:
|
|
399
|
+
- pattern-either:
|
|
400
|
+
- patterns:
|
|
401
|
+
- pattern-either:
|
|
402
|
+
- pattern: requests.$M($X, ...)
|
|
403
|
+
- pattern: httpx.$M($X, ...)
|
|
404
|
+
- metavariable-regex:
|
|
405
|
+
metavariable: $M
|
|
406
|
+
regex: ^(get|post|put|patch|delete|head|options)$
|
|
407
|
+
- pattern: requests.request($METHOD, $X, ...)
|
|
408
|
+
- pattern: httpx.request($METHOD, $X, ...)
|
|
409
|
+
- pattern: urllib.request.urlopen($X, ...)
|
|
410
|
+
- pattern: urllib.request.Request($X, ...)
|
|
411
|
+
- pattern: urllib3.PoolManager(...).request($METHOD, $X, ...)
|
|
412
|
+
- pattern: http.client.HTTPConnection($X, ...)
|
|
413
|
+
- pattern: http.client.HTTPSConnection($X, ...)
|
|
414
|
+
- focus-metavariable: $X
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 Rare Phronesis Limited
|
|
3
|
+
# Copied from the Hullproof kit (kit/tools/hullproof/rules/tool-endpoint-admin-client.yaml).
|
|
4
|
+
#
|
|
5
|
+
rules:
|
|
6
|
+
- id: hullproof-tool-handler-admin-client-no-scope
|
|
7
|
+
languages: [typescript, javascript]
|
|
8
|
+
severity: WARNING
|
|
9
|
+
message: >-
|
|
10
|
+
A tool handler (an MCP server tool or a model tool) uses an admin or service role client and
|
|
11
|
+
takes no argument that could carry the caller (the handler has at most one parameter, its input).
|
|
12
|
+
Anyone who can call the tool, or any prompt that makes the model call it, acts with the service
|
|
13
|
+
role's powers. Use a client that carries the caller's own token, or check the caller's scope and
|
|
14
|
+
ownership of every id before using the admin client. Flagged for review.
|
|
15
|
+
# The admin client names below are heuristics. Edit them to match the project. A handler with two or
|
|
16
|
+
# more parameters is not flagged, because the second one (the MCP extra argument, or a context object)
|
|
17
|
+
# is where the caller identity arrives. Read those handlers by hand.
|
|
18
|
+
patterns:
|
|
19
|
+
- pattern-inside:
|
|
20
|
+
pattern-either:
|
|
21
|
+
- pattern: $S.tool(..., async () => { ... }, ...)
|
|
22
|
+
- pattern: $S.tool(..., async ($A) => { ... }, ...)
|
|
23
|
+
- pattern: $S.registerTool(..., async () => { ... }, ...)
|
|
24
|
+
- pattern: $S.registerTool(..., async ($A) => { ... }, ...)
|
|
25
|
+
- pattern: '$S.addTool({..., execute: async () => { ... }, ...})'
|
|
26
|
+
- pattern: '$S.addTool({..., execute: async ($A) => { ... }, ...})'
|
|
27
|
+
- pattern: '$T({..., execute: async () => { ... }, ...})'
|
|
28
|
+
- pattern: '$T({..., execute: async ($A) => { ... }, ...})'
|
|
29
|
+
- pattern: '$T({..., func: async () => { ... }, ...})'
|
|
30
|
+
- pattern: '$T({..., func: async ($A) => { ... }, ...})'
|
|
31
|
+
- pattern: $S.setRequestHandler(CallToolRequestSchema, async () => { ... })
|
|
32
|
+
- pattern: $S.setRequestHandler(CallToolRequestSchema, async ($A) => { ... })
|
|
33
|
+
# A registration made inside a function that takes a parameter (a per request or per user factory)
|
|
34
|
+
# is not flagged: the caller is captured by the closure. Read those by hand.
|
|
35
|
+
- pattern-not-inside: |
|
|
36
|
+
function $F($P, ...) { ... $S.tool(...); ... }
|
|
37
|
+
- pattern-not-inside: |
|
|
38
|
+
async function $F($P, ...) { ... $S.tool(...); ... }
|
|
39
|
+
- pattern-not-inside: |
|
|
40
|
+
($P, ...) => { ... $S.tool(...); ... }
|
|
41
|
+
- pattern-not-inside: |
|
|
42
|
+
async ($P, ...) => { ... $S.tool(...); ... }
|
|
43
|
+
- pattern-not-inside: |
|
|
44
|
+
function $F($P, ...) { ... $S.registerTool(...); ... }
|
|
45
|
+
- pattern-not-inside: |
|
|
46
|
+
async function $F($P, ...) { ... $S.registerTool(...); ... }
|
|
47
|
+
- pattern-not-inside: |
|
|
48
|
+
($P, ...) => { ... $S.registerTool(...); ... }
|
|
49
|
+
- pattern-not-inside: |
|
|
50
|
+
async ($P, ...) => { ... $S.registerTool(...); ... }
|
|
51
|
+
- pattern-not-inside: |
|
|
52
|
+
function $F($P, ...) { ... $S.addTool(...); ... }
|
|
53
|
+
- pattern-not-inside: |
|
|
54
|
+
async function $F($P, ...) { ... $S.addTool(...); ... }
|
|
55
|
+
- pattern-not-inside: |
|
|
56
|
+
($P, ...) => { ... $S.addTool(...); ... }
|
|
57
|
+
- pattern-not-inside: |
|
|
58
|
+
async ($P, ...) => { ... $S.addTool(...); ... }
|
|
59
|
+
- pattern-not-inside: |
|
|
60
|
+
function $F($P, ...) { ... $S.setRequestHandler(...); ... }
|
|
61
|
+
- pattern-not-inside: |
|
|
62
|
+
async function $F($P, ...) { ... $S.setRequestHandler(...); ... }
|
|
63
|
+
- pattern-not-inside: |
|
|
64
|
+
($P, ...) => { ... $S.setRequestHandler(...); ... }
|
|
65
|
+
- pattern-not-inside: |
|
|
66
|
+
async ($P, ...) => { ... $S.setRequestHandler(...); ... }
|
|
67
|
+
- pattern-either:
|
|
68
|
+
- patterns:
|
|
69
|
+
- pattern: $ADM(...)
|
|
70
|
+
- metavariable-regex:
|
|
71
|
+
metavariable: $ADM
|
|
72
|
+
regex: (?i)^(create|get|make|build|init)?(supabase)?(admin|service|servicerole|privileged)(supabase)?(client|db|database)?$
|
|
73
|
+
- pattern: process.env.SUPABASE_SERVICE_ROLE_KEY
|
|
74
|
+
- pattern: process.env.SERVICE_ROLE_KEY
|
|
75
|
+
- pattern: supabaseAdmin
|
|
76
|
+
- pattern: $X.auth.admin
|
|
77
|
+
- pattern: adminClient
|
|
78
|
+
- pattern: serviceClient
|
|
79
|
+
metadata:
|
|
80
|
+
sec_ids: [SEC-AI-020, SEC-AI-021, SEC-AGENT-008, SEC-SECRETS-003]
|
|
81
|
+
severity_hint: CRITICAL
|
|
82
|
+
confidence: LOW
|
|
83
|
+
note: >-
|
|
84
|
+
Pattern based lead for review. The finding is on the admin client use inside the handler. A caller
|
|
85
|
+
check made before the handler runs (in middleware) is not seen and shows up here, so confirm where
|
|
86
|
+
the caller is authenticated.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 Rare Phronesis Limited
|
|
3
|
+
# Copied from the Hullproof kit (kit/tools/hullproof/rules/user-metadata-authz.yaml).
|
|
4
|
+
#
|
|
5
|
+
rules:
|
|
6
|
+
- id: hullproof-user-metadata-authz
|
|
7
|
+
languages: [typescript, javascript]
|
|
8
|
+
severity: ERROR
|
|
9
|
+
message: >-
|
|
10
|
+
user_metadata is editable by the signed in user. Do not read it in an authorization decision.
|
|
11
|
+
Use app_metadata or a server owned table.
|
|
12
|
+
pattern-either:
|
|
13
|
+
- pattern: if (<... $U.user_metadata ...>) $S
|
|
14
|
+
- pattern: $U.user_metadata.$F === $V
|
|
15
|
+
- pattern: $U.user_metadata.$F == $V
|
|
16
|
+
- pattern: $U.user_metadata.$F !== $V
|
|
17
|
+
- pattern: $U.user_metadata.$F != $V
|
|
18
|
+
- pattern: '$C ? <... $U.user_metadata ...> : $E'
|
|
19
|
+
metadata:
|
|
20
|
+
sec_ids: [SEC-DB-006, SEC-AUTHZ-005]
|
|
21
|
+
severity_hint: CRITICAL
|
|
22
|
+
confidence: MEDIUM
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 Rare Phronesis Limited
|
|
3
|
+
# Copied from the Hullproof kit (kit/tools/hullproof/rules/verify-result-unused.yaml).
|
|
4
|
+
#
|
|
5
|
+
rules:
|
|
6
|
+
- id: hullproof-verify-result-unused
|
|
7
|
+
languages: [typescript, javascript]
|
|
8
|
+
severity: ERROR
|
|
9
|
+
message: >-
|
|
10
|
+
The result of a signature or token check is computed and then not used to reject the request. The check runs and the
|
|
11
|
+
handler goes on regardless, so a forged request is accepted. Test the result and return an error, or throw, when it is false.
|
|
12
|
+
patterns:
|
|
13
|
+
- pattern-either:
|
|
14
|
+
- patterns:
|
|
15
|
+
- pattern-either:
|
|
16
|
+
- pattern: const $OK = $V(...);
|
|
17
|
+
- pattern: const $OK = await $V(...);
|
|
18
|
+
- pattern: const $OK = $O.$V(...);
|
|
19
|
+
- pattern: const $OK = await $O.$V(...);
|
|
20
|
+
- pattern: let $OK = $V(...);
|
|
21
|
+
- pattern: let $OK = $O.$V(...);
|
|
22
|
+
- pattern-not-inside: |
|
|
23
|
+
{
|
|
24
|
+
...
|
|
25
|
+
const $OK = ...;
|
|
26
|
+
...
|
|
27
|
+
<... $OK ...>
|
|
28
|
+
}
|
|
29
|
+
- pattern-not-inside: |
|
|
30
|
+
{
|
|
31
|
+
...
|
|
32
|
+
let $OK = ...;
|
|
33
|
+
...
|
|
34
|
+
<... $OK ...>
|
|
35
|
+
}
|
|
36
|
+
- patterns:
|
|
37
|
+
- pattern-either:
|
|
38
|
+
- pattern: $V(...);
|
|
39
|
+
- pattern: await $V(...);
|
|
40
|
+
- pattern: $O.$V(...);
|
|
41
|
+
- pattern: await $O.$V(...);
|
|
42
|
+
- pattern-not-inside: const $X = ...;
|
|
43
|
+
- pattern-not-inside: let $X = ...;
|
|
44
|
+
- pattern-not-inside: $X = ...;
|
|
45
|
+
- pattern-not-inside: return ...;
|
|
46
|
+
- pattern-not-inside: if (...) ...
|
|
47
|
+
- pattern-not-inside: throw ...;
|
|
48
|
+
- pattern-not-inside: $A && $B
|
|
49
|
+
- pattern-not-inside: $A || $B
|
|
50
|
+
- pattern-not-inside: '!$A'
|
|
51
|
+
- pattern-not-inside: '$A ? $B : $C'
|
|
52
|
+
- metavariable-regex:
|
|
53
|
+
metavariable: $V
|
|
54
|
+
regex: ^(verifySignature|verifyHmac|verifyWebhookSignature|isValidSignature|isSignatureValid|validateSignature|checkSignature|compareSignatures|timingSafeEqual|safeCompare|verifyTokenSignature)$
|
|
55
|
+
metadata:
|
|
56
|
+
sec_ids: [SEC-API-101, SEC-AUTH-002]
|
|
57
|
+
severity_hint: CRITICAL
|
|
58
|
+
confidence: MEDIUM
|
|
59
|
+
note: >-
|
|
60
|
+
Finds a boolean returning check whose result is thrown away, or stored in a variable that nothing reads afterwards. The
|
|
61
|
+
function names are a convention guess, extend the list with the project's own verify helpers. A check that throws on
|
|
62
|
+
failure, such as a library constructEvent call, is not in the list.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 Rare Phronesis Limited
|
|
3
|
+
# Copied from the Hullproof kit (kit/tools/hullproof/rules/webhook-parse-before-verify.yaml).
|
|
4
|
+
#
|
|
5
|
+
rules:
|
|
6
|
+
- id: hullproof-webhook-parse-before-verify
|
|
7
|
+
languages: [typescript, javascript]
|
|
8
|
+
severity: ERROR
|
|
9
|
+
message: >-
|
|
10
|
+
The request body is parsed as JSON before the webhook signature is verified. Read the raw
|
|
11
|
+
text first, verify the signature over those exact bytes, then parse.
|
|
12
|
+
patterns:
|
|
13
|
+
- pattern-either:
|
|
14
|
+
- pattern: |
|
|
15
|
+
$B = await $REQ.json();
|
|
16
|
+
...
|
|
17
|
+
$OBJ.$M(...);
|
|
18
|
+
- pattern: |
|
|
19
|
+
$B = await $REQ.formData();
|
|
20
|
+
...
|
|
21
|
+
$OBJ.$M(...);
|
|
22
|
+
- metavariable-regex:
|
|
23
|
+
metavariable: $M
|
|
24
|
+
regex: ^(constructEvent|constructEventAsync|verify|verifySignature|verifyWebhook|verifyWebhookSignature|validateEvent|unwrap|isValidSignature)$
|
|
25
|
+
metadata:
|
|
26
|
+
sec_ids: [SEC-API-101]
|
|
27
|
+
severity_hint: HIGH
|
|
28
|
+
confidence: MEDIUM
|
|
29
|
+
note: Heuristic. Matches a JSON body read followed by a verify style call in the same block.
|