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,31 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 Rare Phronesis Limited
|
|
3
|
+
# Copied from the Hullproof kit (kit/tools/hullproof/rules/api-route-no-auth.yaml).
|
|
4
|
+
#
|
|
5
|
+
rules:
|
|
6
|
+
- id: hullproof-api-route-no-auth
|
|
7
|
+
languages: [typescript, javascript]
|
|
8
|
+
severity: ERROR
|
|
9
|
+
message: >-
|
|
10
|
+
An API route handler (GET, POST, PUT, PATCH or DELETE) reads from a database or data client and nothing in the handler
|
|
11
|
+
reads a session, user, token, signature or secret. If the route is not meant to be public, any caller gets the data. Read
|
|
12
|
+
the session first and return 401 before any query, or confirm that the route is public on purpose.
|
|
13
|
+
patterns:
|
|
14
|
+
- pattern-either:
|
|
15
|
+
- pattern: export async function $M(...) { ... $DB.$READ(...); ... }
|
|
16
|
+
- pattern: export const $M = async (...) => { ... $DB.$READ(...); ... }
|
|
17
|
+
- metavariable-regex:
|
|
18
|
+
metavariable: $M
|
|
19
|
+
regex: ^(GET|POST|PUT|PATCH|DELETE)$
|
|
20
|
+
- metavariable-regex:
|
|
21
|
+
metavariable: $READ
|
|
22
|
+
regex: ^(select|findMany|findUnique|findFirst|findUniqueOrThrow|findFirstOrThrow|query|rpc|execute|single|maybeSingle|aggregate|download|list)$
|
|
23
|
+
- pattern-not-regex: (?i)(auth|getUser|getSession|getClaims|session|requireUser|currentUser|verify|signature|constructEvent|jwt|bearer|api_?key|cookies\s*\(|token|clerk|guard|permission|secret)
|
|
24
|
+
metadata:
|
|
25
|
+
sec_ids: [SEC-API-001, SEC-AUTHZ-002]
|
|
26
|
+
severity_hint: HIGH
|
|
27
|
+
confidence: LOW
|
|
28
|
+
note: >-
|
|
29
|
+
The handler text is searched for any of the words above, so one mention anywhere, even in a comment, counts as an auth
|
|
30
|
+
read. A deliberately public route is reported. Route handlers that call a shared wrapper with a name outside the list are
|
|
31
|
+
reported, add the wrapper name to the regex.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 Rare Phronesis Limited
|
|
3
|
+
# Copied from the Hullproof kit (kit/tools/hullproof/rules/cors-reflect-origin.yaml).
|
|
4
|
+
#
|
|
5
|
+
rules:
|
|
6
|
+
- id: hullproof-cors-reflect-origin
|
|
7
|
+
mode: taint
|
|
8
|
+
languages: [typescript, javascript]
|
|
9
|
+
severity: ERROR
|
|
10
|
+
message: >-
|
|
11
|
+
The request Origin header is copied into Access-Control-Allow-Origin. Compare it to an exact
|
|
12
|
+
allowlist and return the matched value, never the raw header.
|
|
13
|
+
pattern-sources:
|
|
14
|
+
- pattern: $REQ.headers.origin
|
|
15
|
+
- pattern: $REQ.headers["origin"]
|
|
16
|
+
- pattern: $REQ.headers.get("origin")
|
|
17
|
+
- pattern: $REQ.header("origin")
|
|
18
|
+
pattern-sinks:
|
|
19
|
+
- patterns:
|
|
20
|
+
- pattern-either:
|
|
21
|
+
- pattern: $RES.setHeader("Access-Control-Allow-Origin", $SINK)
|
|
22
|
+
- pattern: $RES.set("Access-Control-Allow-Origin", $SINK)
|
|
23
|
+
- pattern: $RES.header("Access-Control-Allow-Origin", $SINK)
|
|
24
|
+
- pattern: $RES.append("Access-Control-Allow-Origin", $SINK)
|
|
25
|
+
- pattern: $H.set("Access-Control-Allow-Origin", $SINK)
|
|
26
|
+
- pattern: '{..., "Access-Control-Allow-Origin": $SINK, ...}'
|
|
27
|
+
- focus-metavariable: $SINK
|
|
28
|
+
- pattern-not-inside: |
|
|
29
|
+
if (<... $L.includes(...) ...>) { ... }
|
|
30
|
+
- pattern-not-inside: |
|
|
31
|
+
if (<... $L.has(...) ...>) { ... }
|
|
32
|
+
metadata:
|
|
33
|
+
sec_ids: [SEC-WEB-022]
|
|
34
|
+
severity_hint: HIGH
|
|
35
|
+
confidence: MEDIUM
|
|
36
|
+
- id: hullproof-cors-origin-true
|
|
37
|
+
languages: [typescript, javascript]
|
|
38
|
+
severity: ERROR
|
|
39
|
+
message: >-
|
|
40
|
+
CORS is configured to accept any origin. Use an exact allowlist.
|
|
41
|
+
pattern-either:
|
|
42
|
+
- pattern: 'cors({..., origin: true, ...})'
|
|
43
|
+
- pattern: 'cors({..., origin: "*", credentials: true, ...})'
|
|
44
|
+
- pattern: 'cors({..., origin: (...) => true, ...})'
|
|
45
|
+
metadata:
|
|
46
|
+
sec_ids: [SEC-WEB-022]
|
|
47
|
+
severity_hint: HIGH
|
|
48
|
+
confidence: HIGH
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 Rare Phronesis Limited
|
|
3
|
+
# Copied from the Hullproof kit (kit/tools/hullproof/rules/eval-or-new-function.yaml).
|
|
4
|
+
#
|
|
5
|
+
rules:
|
|
6
|
+
- id: hullproof-eval-or-new-function
|
|
7
|
+
languages: [typescript, javascript]
|
|
8
|
+
severity: ERROR
|
|
9
|
+
message: >-
|
|
10
|
+
Dynamic code evaluation. Remove it, or prove the input is a constant. Never evaluate request
|
|
11
|
+
data or model output.
|
|
12
|
+
patterns:
|
|
13
|
+
- pattern-either:
|
|
14
|
+
- pattern: eval(...)
|
|
15
|
+
- pattern: (0, eval)(...)
|
|
16
|
+
- pattern: (0, $G.eval)(...)
|
|
17
|
+
- pattern: $G.eval(...)
|
|
18
|
+
- pattern: new Function(...)
|
|
19
|
+
- pattern: Function(...)
|
|
20
|
+
- pattern: $G.Function(...)
|
|
21
|
+
- pattern: new $G.Function(...)
|
|
22
|
+
- pattern: Function.prototype.constructor(...)
|
|
23
|
+
- pattern: Reflect.construct(Function, ...)
|
|
24
|
+
- pattern: Reflect.apply(eval, ...)
|
|
25
|
+
- pattern: Reflect.apply(Function, ...)
|
|
26
|
+
- pattern: vm.runInNewContext(...)
|
|
27
|
+
- pattern: vm.runInThisContext(...)
|
|
28
|
+
- pattern: vm.runInContext(...)
|
|
29
|
+
- pattern: vm.compileFunction(...)
|
|
30
|
+
- pattern: new vm.Script(...)
|
|
31
|
+
- pattern: new Script(...)
|
|
32
|
+
- pattern: new vm.SourceTextModule(...)
|
|
33
|
+
- pattern: $VM.runInNewContext(...)
|
|
34
|
+
- pattern: $VM.runInThisContext(...)
|
|
35
|
+
- pattern: $VM.runInContext(...)
|
|
36
|
+
- pattern: $VM.compileFunction(...)
|
|
37
|
+
- pattern: new $VM.Script(...)
|
|
38
|
+
- pattern: setTimeout("...", ...)
|
|
39
|
+
- pattern: setInterval("...", ...)
|
|
40
|
+
- pattern: setTimeout($S + $T, ...)
|
|
41
|
+
- pattern: setInterval($S + $T, ...)
|
|
42
|
+
- pattern: setTimeout(`...${$X}...`, ...)
|
|
43
|
+
- pattern: setInterval(`...${$X}...`, ...)
|
|
44
|
+
- pattern: setTimeout($S as any, ...)
|
|
45
|
+
- pattern: setInterval($S as any, ...)
|
|
46
|
+
- pattern: setTimeout($S as string, ...)
|
|
47
|
+
- pattern: setInterval($S as string, ...)
|
|
48
|
+
- pattern: setTimeout(String(...), ...)
|
|
49
|
+
- pattern: setTimeout($S.toString(), ...)
|
|
50
|
+
- pattern-not: eval("...")
|
|
51
|
+
- pattern-not: new Function(..., "...")
|
|
52
|
+
- pattern-not: Function("...")
|
|
53
|
+
- pattern-not: $G.eval("...")
|
|
54
|
+
- pattern-not: new vm.Script("...")
|
|
55
|
+
- pattern-not: new Script("...")
|
|
56
|
+
- pattern-not: new $VM.Script("...")
|
|
57
|
+
- pattern-not: vm.compileFunction("...")
|
|
58
|
+
- pattern-not: $VM.compileFunction("...")
|
|
59
|
+
- pattern-not: vm.runInContext("...", ...)
|
|
60
|
+
- pattern-not: vm.runInNewContext("...", ...)
|
|
61
|
+
- pattern-not: vm.runInThisContext("...")
|
|
62
|
+
metadata:
|
|
63
|
+
sec_ids: [SEC-API-021, SEC-WEB-031]
|
|
64
|
+
severity_hint: HIGH
|
|
65
|
+
confidence: HIGH
|
|
66
|
+
note: >-
|
|
67
|
+
Covers direct and indirect eval, the Function constructor (also through Reflect and a
|
|
68
|
+
global object), vm run and compile calls, and string timers. The global receiver match
|
|
69
|
+
($G.eval) is broad and also fires on any object with an eval method.
|
|
70
|
+
- id: hullproof-eval-through-alias
|
|
71
|
+
languages: [typescript, javascript]
|
|
72
|
+
severity: ERROR
|
|
73
|
+
message: >-
|
|
74
|
+
eval or the Function constructor is copied to another name and then called. This hides a
|
|
75
|
+
dynamic code call from name based review. Remove it.
|
|
76
|
+
patterns:
|
|
77
|
+
- pattern-either:
|
|
78
|
+
- pattern: |
|
|
79
|
+
$F = eval;
|
|
80
|
+
...
|
|
81
|
+
$F($X, ...);
|
|
82
|
+
- pattern: |
|
|
83
|
+
$F = Function;
|
|
84
|
+
...
|
|
85
|
+
$F($X, ...);
|
|
86
|
+
- pattern: |
|
|
87
|
+
$F = Function;
|
|
88
|
+
...
|
|
89
|
+
new $F($X, ...);
|
|
90
|
+
- pattern: |
|
|
91
|
+
$F = $G.eval;
|
|
92
|
+
...
|
|
93
|
+
$F($X, ...);
|
|
94
|
+
- pattern: $A.constructor.constructor($X)
|
|
95
|
+
- pattern: (() => {}).constructor($X)
|
|
96
|
+
- pattern: (function () {}).constructor($X)
|
|
97
|
+
- pattern: (async () => {}).constructor($X)
|
|
98
|
+
- pattern-not: $A.constructor.constructor("...")
|
|
99
|
+
metadata:
|
|
100
|
+
sec_ids: [SEC-API-021]
|
|
101
|
+
severity_hint: HIGH
|
|
102
|
+
confidence: MEDIUM
|
|
103
|
+
note: Same block alias only. An alias passed through a function parameter or an import is not followed.
|
|
104
|
+
- id: hullproof-expression-library-eval
|
|
105
|
+
languages: [typescript, javascript]
|
|
106
|
+
severity: ERROR
|
|
107
|
+
message: >-
|
|
108
|
+
A library that interprets text as code or as a template is called with a non constant value.
|
|
109
|
+
Expression and template engines run attacker text with the process's powers. Compile only
|
|
110
|
+
reviewed constants, or evaluate with a sandbox that has no access to globals.
|
|
111
|
+
patterns:
|
|
112
|
+
- pattern-either:
|
|
113
|
+
- patterns:
|
|
114
|
+
- pattern-either:
|
|
115
|
+
- pattern: $M.evaluate($X, ...)
|
|
116
|
+
- pattern: $M.parse($X).evaluate(...)
|
|
117
|
+
- pattern: $M.compile($X)
|
|
118
|
+
- pattern: $M.template($X, ...)
|
|
119
|
+
- pattern: $M.render($X, ...)
|
|
120
|
+
- pattern: $M.renderString($X, ...)
|
|
121
|
+
- pattern: $M.renderToString($X, ...)
|
|
122
|
+
- pattern: $M.run($X, ...)
|
|
123
|
+
- metavariable-regex:
|
|
124
|
+
metavariable: $M
|
|
125
|
+
regex: ^(math|mathjs|parser|expr|jexl|jsonata|_|lodash|ejs|pug|jade|Handlebars|handlebars|hbs|nunjucks|env|Mustache|mustache|doT|dot|liquid|engine|vm2|sandbox|VM)$
|
|
126
|
+
- pattern: jsonata($X)
|
|
127
|
+
- pattern: new Function($X)
|
|
128
|
+
- pattern: new VM(...).run($X)
|
|
129
|
+
- pattern: new NodeVM(...).run($X)
|
|
130
|
+
- pattern: new Parser().evaluate($X)
|
|
131
|
+
- pattern: new Parser().parse($X)
|
|
132
|
+
- pattern: new Jexl().eval($X)
|
|
133
|
+
- patterns:
|
|
134
|
+
- pattern-inside: |
|
|
135
|
+
import {..., evaluate, ...} from "mathjs";
|
|
136
|
+
...
|
|
137
|
+
- pattern: evaluate($X, ...)
|
|
138
|
+
- patterns:
|
|
139
|
+
- pattern-inside: |
|
|
140
|
+
import {..., compile, ...} from "handlebars";
|
|
141
|
+
...
|
|
142
|
+
- pattern: compile($X, ...)
|
|
143
|
+
- metavariable-pattern:
|
|
144
|
+
metavariable: $X
|
|
145
|
+
patterns:
|
|
146
|
+
- pattern-not: '"..."'
|
|
147
|
+
- pattern-not: "`...`"
|
|
148
|
+
metadata:
|
|
149
|
+
sec_ids: [SEC-API-021]
|
|
150
|
+
severity_hint: HIGH
|
|
151
|
+
confidence: MEDIUM
|
|
152
|
+
note: >-
|
|
153
|
+
Server template and expression engines (SEC-API-021).
|
|
154
|
+
Receiver names are matched by a regex over common import names. Rename the alias or edit the regex.
|
|
155
|
+
- id: hullproof-dynamic-module-path
|
|
156
|
+
languages: [typescript, javascript]
|
|
157
|
+
severity: WARNING
|
|
158
|
+
message: >-
|
|
159
|
+
A module is loaded from a computed path. If any part comes from a request, a database row or a
|
|
160
|
+
file, the caller chooses which code runs. Load from a fixed map of allowed names instead.
|
|
161
|
+
patterns:
|
|
162
|
+
- pattern-either:
|
|
163
|
+
- pattern: require($X)
|
|
164
|
+
- pattern: import($X)
|
|
165
|
+
- pattern: createRequire(...)($X)
|
|
166
|
+
- pattern: module.require($X)
|
|
167
|
+
- metavariable-pattern:
|
|
168
|
+
metavariable: $X
|
|
169
|
+
patterns:
|
|
170
|
+
- pattern-not: '"..."'
|
|
171
|
+
- pattern-not: "`...`"
|
|
172
|
+
- pattern-not: path.join(__dirname, "...")
|
|
173
|
+
- pattern-not: path.resolve(__dirname, "...")
|
|
174
|
+
- pattern-not: |
|
|
175
|
+
`./...${$Y}...`
|
|
176
|
+
- pattern-not: |
|
|
177
|
+
`../...${$Y}...`
|
|
178
|
+
- pattern-not: require.resolve(...)
|
|
179
|
+
metadata:
|
|
180
|
+
sec_ids: [SEC-API-021]
|
|
181
|
+
severity_hint: MEDIUM
|
|
182
|
+
confidence: LOW
|
|
183
|
+
note: >-
|
|
184
|
+
Relative template paths with a fixed directory prefix are skipped because a bundler limits
|
|
185
|
+
them to that folder. Confirm the variable part cannot contain dot dot segments.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 Rare Phronesis Limited
|
|
3
|
+
# Copied from the Hullproof kit (kit/tools/hullproof/rules/log-session-token.yaml).
|
|
4
|
+
#
|
|
5
|
+
rules:
|
|
6
|
+
- id: hullproof-log-session-token
|
|
7
|
+
languages: [typescript, javascript]
|
|
8
|
+
severity: WARNING
|
|
9
|
+
message: >-
|
|
10
|
+
A log call receives a value whose name says it is a session token, access token, authorization header, cookie, password or
|
|
11
|
+
secret. Logs are copied to vendors and read by more people than the database. Log a user id or a request id instead,
|
|
12
|
+
and never the credential.
|
|
13
|
+
patterns:
|
|
14
|
+
- pattern-either:
|
|
15
|
+
- patterns:
|
|
16
|
+
- pattern-either:
|
|
17
|
+
- pattern: $L.$M(<... $ARG ...>)
|
|
18
|
+
- pattern: $L.$M(..., <... $ARG ...>, ...)
|
|
19
|
+
- metavariable-regex:
|
|
20
|
+
metavariable: $ARG
|
|
21
|
+
regex: (?i)^(?:[\w$]+\??\.)*[\w$]*(?:accesstoken|refreshtoken|idtoken|sessiontoken|session_token|access_token|refresh_token|id_token|authorization|bearer|jwt|cookie|password|passwd|secret|apikey|api_key)[\w$]*$
|
|
22
|
+
- patterns:
|
|
23
|
+
- pattern-either:
|
|
24
|
+
- pattern: $L.$M($ARG)
|
|
25
|
+
- pattern: $L.$M(..., $ARG, ...)
|
|
26
|
+
- metavariable-regex:
|
|
27
|
+
metavariable: $ARG
|
|
28
|
+
regex: (?i)^(?:[\w$]+\??\.)*(?:token|session|headers)$
|
|
29
|
+
- patterns:
|
|
30
|
+
- pattern-either:
|
|
31
|
+
- pattern: $L.$M(<... $H.get($K) ...>)
|
|
32
|
+
- pattern: $L.$M(..., <... $H.get($K) ...>, ...)
|
|
33
|
+
- metavariable-regex:
|
|
34
|
+
metavariable: $K
|
|
35
|
+
regex: (?i)^["'`](authorization|cookie|x-api-key)["'`]$
|
|
36
|
+
- metavariable-regex:
|
|
37
|
+
metavariable: $L
|
|
38
|
+
regex: ^(console|logger|log|Logger|winston|pino|bunyan|Sentry|this\.logger)$
|
|
39
|
+
- metavariable-regex:
|
|
40
|
+
metavariable: $M
|
|
41
|
+
regex: ^(log|info|warn|error|debug|trace|fatal|captureMessage|addBreadcrumb)$
|
|
42
|
+
metadata:
|
|
43
|
+
sec_ids: [SEC-LOG-001]
|
|
44
|
+
severity_hint: HIGH
|
|
45
|
+
confidence: MEDIUM
|
|
46
|
+
note: >-
|
|
47
|
+
Matches names only. A string literal is not a match. Logging a whole session object or the whole headers object is
|
|
48
|
+
reported because both hold the token. A variable with a neutral name that holds a token is not found.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 Rare Phronesis Limited
|
|
3
|
+
# Copied from the Hullproof kit (kit/tools/hullproof/rules/math-random-tokens.yaml).
|
|
4
|
+
#
|
|
5
|
+
rules:
|
|
6
|
+
- id: hullproof-math-random-token
|
|
7
|
+
languages: [typescript, javascript]
|
|
8
|
+
severity: ERROR
|
|
9
|
+
message: >-
|
|
10
|
+
Math.random is not a secure random source. Use crypto.randomUUID or crypto.getRandomValues
|
|
11
|
+
for tokens, codes, secrets and identifiers that must be unguessable.
|
|
12
|
+
patterns:
|
|
13
|
+
- pattern-either:
|
|
14
|
+
- pattern: $N = <... Math.random() ...>
|
|
15
|
+
- pattern: function $N(...) { ... Math.random(); ... }
|
|
16
|
+
- pattern: function $N(...) { ... return <... Math.random() ...>; }
|
|
17
|
+
- pattern: '{..., $N: <... Math.random() ...>, ...}'
|
|
18
|
+
- metavariable-regex:
|
|
19
|
+
metavariable: $N
|
|
20
|
+
regex: (?i)^(.*(token|secret|invite|reset|nonce|otp|password|apikey|api_key|session|share|code|salt).*|id|.*_id|[a-z]*Id)$
|
|
21
|
+
metadata:
|
|
22
|
+
sec_ids: [SEC-DATA-001, SEC-AUTH-001]
|
|
23
|
+
severity_hint: HIGH
|
|
24
|
+
confidence: MEDIUM
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 Rare Phronesis Limited
|
|
3
|
+
# Copied from the Hullproof kit (kit/tools/hullproof/rules/next-public-secret-name.yaml).
|
|
4
|
+
#
|
|
5
|
+
rules:
|
|
6
|
+
- id: hullproof-next-public-secret-name
|
|
7
|
+
languages: [typescript, javascript]
|
|
8
|
+
severity: ERROR
|
|
9
|
+
message: >-
|
|
10
|
+
A variable with the NEXT_PUBLIC_ prefix is named like a service role key or a secret. Next.js copies every NEXT_PUBLIC_
|
|
11
|
+
variable into the browser bundle, so the value is public to everyone who loads the site. Rename it without the prefix,
|
|
12
|
+
read it only in server code, and rotate the key if it was ever deployed with the prefix.
|
|
13
|
+
patterns:
|
|
14
|
+
- pattern-either:
|
|
15
|
+
- pattern: process.env.$V
|
|
16
|
+
- pattern: process.env["$V"]
|
|
17
|
+
- pattern: process.env['$V']
|
|
18
|
+
- pattern: import.meta.env.$V
|
|
19
|
+
- pattern: '{..., $V: $X, ...}'
|
|
20
|
+
- pattern: '{..., "$V": $X, ...}'
|
|
21
|
+
- metavariable-regex:
|
|
22
|
+
metavariable: $V
|
|
23
|
+
regex: (?i)^NEXT_PUBLIC_.*(SERVICE_?ROLE|SERVICE_?KEY|SECRET|PRIVATE|ADMIN_?KEY|WEBHOOK_?SIGNING)
|
|
24
|
+
- focus-metavariable: $V
|
|
25
|
+
metadata:
|
|
26
|
+
sec_ids: [SEC-SECRETS-001, SEC-SECRETS-003]
|
|
27
|
+
severity_hint: CRITICAL
|
|
28
|
+
confidence: MEDIUM
|
|
29
|
+
note: >-
|
|
30
|
+
Matches the variable name only, never the value. It also fires on a safe use, for example a name that holds a public
|
|
31
|
+
identifier such as NEXT_PUBLIC_SECRET_SANTA_URL. Read the line. Env files are not scanned by this rule, see the
|
|
32
|
+
gitleaks configuration and the SEC-SECRETS-001 recipe for them.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 Rare Phronesis Limited
|
|
3
|
+
# Copied from the Hullproof kit (kit/tools/hullproof/rules/postgrest-filter-injection.yaml).
|
|
4
|
+
#
|
|
5
|
+
rules:
|
|
6
|
+
- id: hullproof-postgrest-filter-string
|
|
7
|
+
languages: [typescript, javascript]
|
|
8
|
+
severity: ERROR
|
|
9
|
+
message: >-
|
|
10
|
+
A PostgREST filter string, column or embed list is built from a value that is not a constant.
|
|
11
|
+
Filter strings are parsed as a small language, so a comma, a dot or a parenthesis in the value
|
|
12
|
+
adds conditions (for example an extra or branch that matches every row). Use the builder
|
|
13
|
+
methods with one column and one value each (eq, ilike, in), and take column names and sort
|
|
14
|
+
keys from a fixed allowlist.
|
|
15
|
+
patterns:
|
|
16
|
+
- pattern-either:
|
|
17
|
+
- pattern: $Q.or($F, ...)
|
|
18
|
+
- pattern: $Q.and($F, ...)
|
|
19
|
+
- pattern: $Q.filter($F, $OP, $V)
|
|
20
|
+
- pattern: $Q.not($F, $OP, $V)
|
|
21
|
+
- pattern: $Q.order($F, ...)
|
|
22
|
+
- pattern: $Q.eq($F, $V)
|
|
23
|
+
- pattern: $Q.neq($F, $V)
|
|
24
|
+
- pattern: $Q.ilike($F, $V)
|
|
25
|
+
- pattern: $Q.like($F, $V)
|
|
26
|
+
- pattern: $Q.in($F, $V)
|
|
27
|
+
- pattern: $Q.contains($F, $V)
|
|
28
|
+
- pattern: $Q.textSearch($F, $V, ...)
|
|
29
|
+
- pattern: $Q.from(...).select($F, ...)
|
|
30
|
+
- metavariable-pattern:
|
|
31
|
+
metavariable: $F
|
|
32
|
+
patterns:
|
|
33
|
+
- pattern: $Z
|
|
34
|
+
- pattern-not: '"..."'
|
|
35
|
+
- pattern-not: "`...`"
|
|
36
|
+
- pattern-not: '[...]'
|
|
37
|
+
- pattern-not: '{...}'
|
|
38
|
+
- metavariable-regex:
|
|
39
|
+
metavariable: $F
|
|
40
|
+
regex: (?s)^(?![A-Z][A-Z0-9_]*$)(?!\s*[\[{])(?!(?:z|zod|v|yup|Joi|t|S|Type|type)\.)
|
|
41
|
+
metadata:
|
|
42
|
+
sec_ids: [SEC-API-018, SEC-API-017]
|
|
43
|
+
severity_hint: HIGH
|
|
44
|
+
confidence: MEDIUM
|
|
45
|
+
note: >-
|
|
46
|
+
Fires on any non constant column name, filter string or sort key passed to a query builder
|
|
47
|
+
method. Value arguments of eq, ilike and in are bound by the client and are not flagged
|
|
48
|
+
unless the first argument is also dynamic. A constant written in upper case is skipped, and so is an argument that starts with a schema library name (z, zod, yup, Joi), because zod and similar libraries have or and and methods too.
|
|
49
|
+
Mongoose and similar libraries also have or and and methods: array and object arguments are skipped.
|
|
50
|
+
- id: hullproof-postgrest-interpolated-filter
|
|
51
|
+
languages: [typescript, javascript]
|
|
52
|
+
severity: ERROR
|
|
53
|
+
message: >-
|
|
54
|
+
A value is interpolated into a PostgREST filter string. A comma, dot or parenthesis in the value
|
|
55
|
+
changes the filter. Build the condition with one builder call per column and bind each value.
|
|
56
|
+
patterns:
|
|
57
|
+
- pattern-either:
|
|
58
|
+
- pattern: $Q.or(`...${$V}...`, ...)
|
|
59
|
+
- pattern: $Q.and(`...${$V}...`, ...)
|
|
60
|
+
- pattern: $Q.or("..." + $V, ...)
|
|
61
|
+
- pattern: $Q.and("..." + $V, ...)
|
|
62
|
+
- pattern: $Q.or($V + "...", ...)
|
|
63
|
+
- pattern: $Q.not($C, $O, `...${$V}...`)
|
|
64
|
+
- pattern: $Q.filter($C, $O, `...${$V}...`)
|
|
65
|
+
- pattern: $Q.from(...).select(`...${$V}...`, ...)
|
|
66
|
+
- pattern: $Q.select(`...${$V}...`, ...)
|
|
67
|
+
- metavariable-regex:
|
|
68
|
+
metavariable: $Q
|
|
69
|
+
regex: (?s).*(from|supabase|client|admin|db|query|builder|rest|table)\b.*
|
|
70
|
+
metadata:
|
|
71
|
+
sec_ids: [SEC-API-018, SEC-API-017]
|
|
72
|
+
severity_hint: CRITICAL
|
|
73
|
+
confidence: HIGH
|
|
74
|
+
note: >-
|
|
75
|
+
Template or concatenated string in or, and, filter, not or a select list on a Supabase or
|
|
76
|
+
PostgREST builder. The receiver name must mention from, supabase, client, admin, db, query,
|
|
77
|
+
builder, rest or table, which keeps unrelated or methods out. Single column calls such as
|
|
78
|
+
ilike with an interpolated pattern are bound by the client and are not matched.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 Rare Phronesis Limited
|
|
3
|
+
# Copied from the Hullproof kit (kit/tools/hullproof/rules/raw-sql-concat.yaml).
|
|
4
|
+
#
|
|
5
|
+
rules:
|
|
6
|
+
- id: hullproof-raw-sql-concat
|
|
7
|
+
languages: [typescript, javascript]
|
|
8
|
+
severity: ERROR
|
|
9
|
+
message: >-
|
|
10
|
+
A query sink receives a string built from an interpolated or concatenated value. Use
|
|
11
|
+
parameters or a tagged template that binds values.
|
|
12
|
+
patterns:
|
|
13
|
+
- pattern-either:
|
|
14
|
+
- pattern: $X.$M(`...${$Y}...`, ...)
|
|
15
|
+
- pattern: $X.$M("..." + $Y, ...)
|
|
16
|
+
- pattern: $X.$M($Y + "...", ...)
|
|
17
|
+
- pattern: $X.$M("...".concat(...), ...)
|
|
18
|
+
- pattern: $X.$M([...].join(...), ...)
|
|
19
|
+
- pattern: $X.$M(`...${$Y}...`.concat(...), ...)
|
|
20
|
+
- metavariable-regex:
|
|
21
|
+
metavariable: $M
|
|
22
|
+
regex: ^(query|execute|raw|unsafe|\$queryRawUnsafe|\$executeRawUnsafe|queryRawUnsafe|executeRawUnsafe|whereRaw|andWhereRaw|orWhereRaw|havingRaw|orderByRaw|groupByRaw|joinRaw|selectRaw|fromRaw|rawQuery|literal)$
|
|
23
|
+
metadata:
|
|
24
|
+
sec_ids: [SEC-API-017]
|
|
25
|
+
severity_hint: CRITICAL
|
|
26
|
+
confidence: HIGH
|
|
27
|
+
note: >-
|
|
28
|
+
Unambiguous query method names. Tagged template calls bind values and are not matched. The
|
|
29
|
+
queryRaw and executeRaw plain string misuse is in sql-variants.yaml.
|
|
30
|
+
- id: hullproof-sql-string-built-in-variable
|
|
31
|
+
languages: [generic]
|
|
32
|
+
severity: WARNING
|
|
33
|
+
message: >-
|
|
34
|
+
A template literal that looks like SQL contains an interpolated value. If it reaches a query
|
|
35
|
+
call, bind the value as a parameter instead.
|
|
36
|
+
paths:
|
|
37
|
+
include: ["*.ts", "*.tsx", "*.js", "*.jsx", "*.mjs"]
|
|
38
|
+
pattern-regex: '(?is)(?<![\w$])`[^`]*\b(?:select\s[^`]*\sfrom|insert\s+into|update\s+\w+\s+set|delete\s+from)\b[^`]*\$\{'
|
|
39
|
+
metadata:
|
|
40
|
+
sec_ids: [SEC-API-017]
|
|
41
|
+
severity_hint: HIGH
|
|
42
|
+
confidence: LOW
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 Rare Phronesis Limited
|
|
3
|
+
# Copied from the Hullproof kit (kit/tools/hullproof/rules/read-by-id-no-owner.yaml).
|
|
4
|
+
#
|
|
5
|
+
rules:
|
|
6
|
+
- id: hullproof-read-by-id-no-owner
|
|
7
|
+
languages: [typescript, javascript]
|
|
8
|
+
severity: ERROR
|
|
9
|
+
message: >-
|
|
10
|
+
A record is read by its id alone. The query holds no filter on the owner, user, organisation or tenant, so any caller who
|
|
11
|
+
knows or guesses another id gets that record. Add the owner filter to the same query (or rely on a row level security policy
|
|
12
|
+
that you have tested) and return 404 for a record that is not the caller's.
|
|
13
|
+
patterns:
|
|
14
|
+
- pattern-either:
|
|
15
|
+
- patterns:
|
|
16
|
+
- pattern-either:
|
|
17
|
+
- pattern: $C.from(...).select(...).eq("id", $X)
|
|
18
|
+
- pattern: $C.from(...).select().eq("id", $X)
|
|
19
|
+
- pattern-not-inside:
|
|
20
|
+
patterns:
|
|
21
|
+
- pattern: <... $Q.eq($K, ...) ...>
|
|
22
|
+
- metavariable-regex:
|
|
23
|
+
metavariable: $K
|
|
24
|
+
regex: (?i)^["'`].*(user|owner|org|tenant|team|account|created_?by|author|member|workspace|profile|customer).*["'`]$
|
|
25
|
+
- patterns:
|
|
26
|
+
- pattern-either:
|
|
27
|
+
- pattern: '$P.$T.$FIND({ where: { id: $X } })'
|
|
28
|
+
- pattern: '$P.$T.$FIND({ where: { id: $X }, ... })'
|
|
29
|
+
- pattern-not:
|
|
30
|
+
patterns:
|
|
31
|
+
- pattern: '$P.$T.$FIND({ where: { ..., $K: $V, ... } })'
|
|
32
|
+
- metavariable-regex:
|
|
33
|
+
metavariable: $K
|
|
34
|
+
regex: (?i)^["'`]?.*(user|owner|org|tenant|team|account|created_?by|author|member|workspace|profile|customer).*["'`]?$
|
|
35
|
+
- metavariable-regex:
|
|
36
|
+
metavariable: $FIND
|
|
37
|
+
regex: ^(findUnique|findUniqueOrThrow|findFirst|findFirstOrThrow)$
|
|
38
|
+
- pattern: $DB.select(...).from($T).where(eq($T2.id, $X))
|
|
39
|
+
- metavariable-regex:
|
|
40
|
+
metavariable: $X
|
|
41
|
+
regex: (?i)^(?!.*(user|session|auth|owner|me\b)).*$
|
|
42
|
+
metadata:
|
|
43
|
+
sec_ids: [SEC-AUTHZ-003]
|
|
44
|
+
severity_hint: HIGH
|
|
45
|
+
confidence: LOW
|
|
46
|
+
note: >-
|
|
47
|
+
Covers a Supabase client chain, a Prisma find call and a Drizzle select with one id condition. It cannot see a row level
|
|
48
|
+
security policy, a wrapper that adds the owner filter, or an owner check made on the next line. A value whose text names
|
|
49
|
+
user, session, auth or owner is treated as the caller's own id and skipped. Read the query and the policy before you rate it.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 Rare Phronesis Limited
|
|
3
|
+
# Copied from the Hullproof kit (kit/tools/hullproof/rules/server-action-no-auth.yaml).
|
|
4
|
+
#
|
|
5
|
+
rules:
|
|
6
|
+
- id: hullproof-server-action-no-auth
|
|
7
|
+
languages: [typescript, javascript]
|
|
8
|
+
severity: ERROR
|
|
9
|
+
message: >-
|
|
10
|
+
Exported function in a file with a top level "use server" directive has no call to a
|
|
11
|
+
session, auth or permission helper anywhere in its body. Every export is a public endpoint. Call the shared auth helper first.
|
|
12
|
+
patterns:
|
|
13
|
+
- pattern-inside: |
|
|
14
|
+
"use server";
|
|
15
|
+
...
|
|
16
|
+
- pattern-either:
|
|
17
|
+
- pattern: export async function $F(...) { ... }
|
|
18
|
+
- pattern: export const $F = async (...) => { ... }
|
|
19
|
+
- pattern-not-regex: (?i)\b(?:[\w$]*(?:getUser|getSession|getClaims|requireUser|currentUser|session|assertAdmin|requireAdmin|authorize|authenticate|requireRole|checkPermission|validateRequest|requireAuth|withAuth|checkAuth|assertAuth|verifyAuth)[\w$]*|auth)\s*\(
|
|
20
|
+
metadata:
|
|
21
|
+
sec_ids: [SEC-AUTHZ-002, SEC-API-001]
|
|
22
|
+
severity_hint: HIGH
|
|
23
|
+
confidence: MEDIUM
|
|
24
|
+
note: >-
|
|
25
|
+
The whole function text is searched, so a session read inside an if, try or nested block counts. A helper name in a comment also
|
|
26
|
+
counts. The helper name regex is a convention guess. Adjust it to the project's auth helper.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 Rare Phronesis Limited
|
|
3
|
+
# Copied from the Hullproof kit (kit/tools/hullproof/rules/sql-variants.yaml).
|
|
4
|
+
#
|
|
5
|
+
rules:
|
|
6
|
+
- id: hullproof-sql-raw-non-literal
|
|
7
|
+
languages: [typescript, javascript]
|
|
8
|
+
severity: ERROR
|
|
9
|
+
message: >-
|
|
10
|
+
A raw SQL helper receives a value that is not a constant string. Raw helpers send the text
|
|
11
|
+
to the database as code. Bind values as parameters, and take identifiers from a fixed
|
|
12
|
+
allowlist.
|
|
13
|
+
patterns:
|
|
14
|
+
- pattern-either:
|
|
15
|
+
- pattern: sql.raw($Y)
|
|
16
|
+
- pattern: sql.unsafe($Y, ...)
|
|
17
|
+
- pattern: Prisma.raw($Y)
|
|
18
|
+
- patterns:
|
|
19
|
+
- pattern-either:
|
|
20
|
+
- pattern: $DB.raw($Y, ...)
|
|
21
|
+
- pattern: $DB.unsafe($Y, ...)
|
|
22
|
+
- pattern: $DB.literal($Y)
|
|
23
|
+
- metavariable-regex:
|
|
24
|
+
metavariable: $DB
|
|
25
|
+
regex: (?i)^(?:.*\.)?(?:\w*knex\w*|\w*sequelize\w*|\w*db|db\w*|trx|tx|pool|database|conn|connection|kx|sql|pg|client|query)$
|
|
26
|
+
- patterns:
|
|
27
|
+
- pattern: $DB.$M($Y, ...)
|
|
28
|
+
- metavariable-regex:
|
|
29
|
+
metavariable: $M
|
|
30
|
+
regex: ^(whereRaw|andWhereRaw|orWhereRaw|havingRaw|orderByRaw|groupByRaw|joinRaw|selectRaw|fromRaw|\$queryRawUnsafe|\$executeRawUnsafe|queryRawUnsafe|executeRawUnsafe)$
|
|
31
|
+
- metavariable-pattern:
|
|
32
|
+
metavariable: $Y
|
|
33
|
+
patterns:
|
|
34
|
+
- pattern: $Z
|
|
35
|
+
- pattern-not: '"..."'
|
|
36
|
+
- pattern-not: "`...`"
|
|
37
|
+
metadata:
|
|
38
|
+
sec_ids: [SEC-API-017]
|
|
39
|
+
severity_hint: HIGH
|
|
40
|
+
confidence: MEDIUM
|
|
41
|
+
note: Fires on any non constant argument, including a variable that holds a reviewed constant. Read the variable.
|
|
42
|
+
- id: hullproof-sql-tagged-template-misuse
|
|
43
|
+
languages: [typescript, javascript]
|
|
44
|
+
severity: ERROR
|
|
45
|
+
message: >-
|
|
46
|
+
A function that is built for tagged templates is called with an ordinary string. The
|
|
47
|
+
interpolated value is pasted into the SQL text and is not bound. Write sql`...${value}...`
|
|
48
|
+
without the parentheses so the library binds it, or call the unsafe variant on purpose with
|
|
49
|
+
an allowlisted value.
|
|
50
|
+
patterns:
|
|
51
|
+
- pattern-either:
|
|
52
|
+
- pattern: sql(`...${$Y}...`)
|
|
53
|
+
- pattern: sql("..." + $Y)
|
|
54
|
+
- pattern: $DB.sql(`...${$Y}...`)
|
|
55
|
+
- pattern: $DB.$queryRaw(`...${$Y}...`)
|
|
56
|
+
- pattern: $DB.$executeRaw(`...${$Y}...`)
|
|
57
|
+
- pattern: Prisma.sql(`...${$Y}...`)
|
|
58
|
+
- pattern: $T`...${sql.raw($Y)}...`
|
|
59
|
+
- pattern: $T`...${Prisma.raw($Y)}...`
|
|
60
|
+
- pattern: $T`...${$DB.unsafe($Y)}...`
|
|
61
|
+
metadata:
|
|
62
|
+
sec_ids: [SEC-API-017]
|
|
63
|
+
severity_hint: CRITICAL
|
|
64
|
+
confidence: MEDIUM
|
|
65
|
+
note: Covers postgres.js, drizzle and Prisma call shapes. The raw helper inside a tagged template is also flagged.
|
|
66
|
+
- id: hullproof-sql-built-string-reaches-query
|
|
67
|
+
languages: [typescript, javascript]
|
|
68
|
+
mode: taint
|
|
69
|
+
severity: ERROR
|
|
70
|
+
message: >-
|
|
71
|
+
A string that starts like a SQL statement and holds a concatenated or interpolated value
|
|
72
|
+
reaches a query call. Bind the value as a parameter.
|
|
73
|
+
pattern-sources:
|
|
74
|
+
- pattern-regex: '(?is)(?<![\w$])`\s*(?:select|insert\s+into|update|delete\s+from|with\s+\w+\s+as|merge\s+into|create|alter|drop|truncate|call|exec|execute)\b[^`]*\$\{[^`]*`'
|
|
75
|
+
- patterns:
|
|
76
|
+
- pattern-either:
|
|
77
|
+
- pattern: '$L + $Y'
|
|
78
|
+
- pattern: '[$L, ...].join(...)'
|
|
79
|
+
- pattern: '$L.concat(...)'
|
|
80
|
+
- metavariable-regex:
|
|
81
|
+
metavariable: $L
|
|
82
|
+
regex: '(?is)^["''`]\s*(?:select|insert|update|delete|with|merge|create|alter|drop|truncate|call|exec|execute)\b'
|
|
83
|
+
pattern-sinks:
|
|
84
|
+
- patterns:
|
|
85
|
+
- pattern-either:
|
|
86
|
+
- pattern: $X.$M($Q, ...)
|
|
87
|
+
- pattern: $M($Q, ...)
|
|
88
|
+
- focus-metavariable: $Q
|
|
89
|
+
- metavariable-regex:
|
|
90
|
+
metavariable: $M
|
|
91
|
+
regex: ^(query|execute|exec|run|get|all|prepare|any|one|many|none|oneOrNone|manyOrNone|result|raw|unsafe|each|map|task|tx|\$queryRawUnsafe|\$executeRawUnsafe|queryRawUnsafe|executeRawUnsafe|whereRaw|havingRaw|orderByRaw|selectRaw|joinRaw|runSql|runQuery|rawQuery|literal)$
|
|
92
|
+
metadata:
|
|
93
|
+
sec_ids: [SEC-API-017]
|
|
94
|
+
severity_hint: CRITICAL
|
|
95
|
+
confidence: MEDIUM
|
|
96
|
+
note: >-
|
|
97
|
+
Follows a built string through a variable inside one function or file. The source must
|
|
98
|
+
start with a SQL keyword, which keeps get and run style method names from firing on
|
|
99
|
+
unrelated strings.
|