openqodex 0.1.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 +202 -0
- package/NOTICE +35 -0
- package/README.md +160 -0
- package/demo/baseline/.github/workflows/ci.yml +18 -0
- package/demo/baseline/Dockerfile +12 -0
- package/demo/baseline/app/__init__.py +0 -0
- package/demo/baseline/app/server.py +37 -0
- package/demo/baseline/package-lock.json +21 -0
- package/demo/baseline/package.json +9 -0
- package/demo/baseline/requirements.txt +1 -0
- package/demo/baseline/scripts/deploy.sh +13 -0
- package/demo/expected.json +126 -0
- package/demo/planted/.github/workflows/ci.yml +20 -0
- package/demo/planted/Dockerfile +11 -0
- package/demo/planted/app/config.py +3 -0
- package/demo/planted/app/search.py +17 -0
- package/demo/planted/app/server.py +40 -0
- package/demo/planted/package-lock.json +21 -0
- package/demo/planted/package.json +9 -0
- package/demo/planted/scripts/deploy.sh +13 -0
- package/dist/bin.js +42546 -0
- package/docs/agents.md +121 -0
- package/docs/cli.md +161 -0
- package/docs/config.md +165 -0
- package/docs/custom-scanners.md +164 -0
- package/docs/faq.md +54 -0
- package/docs/github-action.md +73 -0
- package/docs/index.md +29 -0
- package/docs/llms.txt +13 -0
- package/docs/quickstart.md +82 -0
- package/docs/scanners.md +148 -0
- package/docs/security.md +74 -0
- package/docs/telemetry.md +9 -0
- package/lenses/a11y-icon-only-button-no-aria-label.md +51 -0
- package/lenses/array-iteration-missing-key-prop.md +31 -0
- package/lenses/async-await-in-loop-n-plus-one.md +42 -0
- package/lenses/async-click-double-fire-race.md +59 -0
- package/lenses/async-floating-promise.md +40 -0
- package/lenses/async-promise-all-swallows-errors.md +42 -0
- package/lenses/async-unhandled-rejection-in-handler.md +42 -0
- package/lenses/auth-missing-on-state-change-route.md +60 -0
- package/lenses/auth-role-from-user-input.md +49 -0
- package/lenses/auth-timing-attack-password-compare.md +59 -0
- package/lenses/cookie-missing-secure-httponly.md +46 -0
- package/lenses/cors-wildcard-with-credentials.md +47 -0
- package/lenses/crypto-jwt-verify-without-algo-allowlist.md +41 -0
- package/lenses/crypto-math-random-for-tokens.md +53 -0
- package/lenses/crypto-md5-sha1-for-secrets.md +60 -0
- package/lenses/env-vars-read-at-module-top.md +42 -0
- package/lenses/eval-on-user-input.md +51 -0
- package/lenses/fetch-without-timeout.md +41 -0
- package/lenses/id-enumeration-sequential.md +49 -0
- package/lenses/interactive-state-decoupled-from-output.md +67 -0
- package/lenses/json-parse-no-try-catch.md +41 -0
- package/lenses/missing-rate-limit-on-auth.md +49 -0
- package/lenses/oauth-scope-wider-than-use.md +79 -0
- package/lenses/object-spread-clobber.md +44 -0
- package/lenses/open-redirect-from-untrusted-host.md +55 -0
- package/lenses/orm-drizzle-on-conflict-clobber.md +47 -0
- package/lenses/path-traversal-in-fs-access.md +59 -0
- package/lenses/pii-in-url-or-log.md +48 -0
- package/lenses/race-check-then-act.md +49 -0
- package/lenses/react-dangerously-set-inner-html.md +32 -0
- package/lenses/react-fetch-in-effect-without-abort.md +31 -0
- package/lenses/react-stale-closure-in-callback.md +28 -0
- package/lenses/react-state-set-in-render.md +33 -0
- package/lenses/react-use-effect-missing-cleanup.md +29 -0
- package/lenses/react-use-effect-missing-deps.md +30 -0
- package/lenses/regexp-from-user-input.md +43 -0
- package/lenses/return-shape-contract-break.md +71 -0
- package/lenses/secrets-logged-in-error-path.md +60 -0
- package/lenses/sql-migration-references-later-object.md +47 -0
- package/lenses/sql-string-concatenation.md +63 -0
- package/lenses/ssrf-server-side-fetch.md +69 -0
- package/lenses/supabase-comment-on-function-unqualified.md +37 -0
- package/lenses/supabase-function-default-public-execute.md +50 -0
- package/lenses/supabase-security-definer-no-search-path.md +34 -0
- package/lenses/supabase-single-500-on-no-match.md +65 -0
- package/lenses/upsert-state-column.md +57 -0
- package/lenses/url-not-encoded-for-user-id.md +78 -0
- package/lenses/use-state-default-not-functional.md +30 -0
- package/package.json +57 -0
- package/skills/openqodex/SKILL.md +141 -0
- package/templates/README.md +67 -0
- package/templates/claude-code/settings-hook.json +16 -0
- package/templates/cline/openqodex.md +9 -0
- package/templates/codex/AGENTS-section.md +8 -0
- package/templates/codex/hooks.json +16 -0
- package/templates/cursor/openqodex.mdc +15 -0
- package/toolchain.json +345 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: async-promise-all-swallows-errors
|
|
3
|
+
description: Promise.all over fire-and-forget side effects loses partial failures
|
|
4
|
+
triggers:
|
|
5
|
+
files:
|
|
6
|
+
- "*.ts"
|
|
7
|
+
- "*.tsx"
|
|
8
|
+
- "*.js"
|
|
9
|
+
- "*.jsx"
|
|
10
|
+
- "*.mjs"
|
|
11
|
+
- "*.cjs"
|
|
12
|
+
- "**/*.ts"
|
|
13
|
+
- "**/*.tsx"
|
|
14
|
+
- "**/*.js"
|
|
15
|
+
- "**/*.jsx"
|
|
16
|
+
- "**/*.mjs"
|
|
17
|
+
- "**/*.cjs"
|
|
18
|
+
hunk_regex: "Promise\\.all\\s*\\("
|
|
19
|
+
confidence_floor: 0.7
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
`Promise.all([...])` rejects on the FIRST rejection and silently
|
|
23
|
+
discards the results of every later promise, even ones that
|
|
24
|
+
succeeded after rejection. When the array members are independent
|
|
25
|
+
side-effects (insert N rows, fire N webhooks, send N emails), the
|
|
26
|
+
caller only sees one error but several side-effects may have run
|
|
27
|
+
and several may have silently not run. Recovery / retry becomes
|
|
28
|
+
impossible because the caller can't tell which ones happened.
|
|
29
|
+
|
|
30
|
+
Flag a `Promise.all([...])` where each item is an INDEPENDENT
|
|
31
|
+
side-effect (call to db/insert, http POST, message publish, file
|
|
32
|
+
write) and the caller doesn't:
|
|
33
|
+
- use `Promise.allSettled` to inspect each result, OR
|
|
34
|
+
- wrap each item in `.catch` to translate rejections into per-item
|
|
35
|
+
status
|
|
36
|
+
|
|
37
|
+
Suppress when:
|
|
38
|
+
- the items are reads (the all-or-nothing semantic matches "I need
|
|
39
|
+
every value to proceed")
|
|
40
|
+
- the wrapper IS `Promise.allSettled`
|
|
41
|
+
- the items are wrapped individually with `.catch` so the all sees
|
|
42
|
+
only fulfilled promises
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: async-unhandled-rejection-in-handler
|
|
3
|
+
description: Express / Hono / Fastify handler is async but has no try/catch and no errorHandler middleware
|
|
4
|
+
triggers:
|
|
5
|
+
files:
|
|
6
|
+
- "*.ts"
|
|
7
|
+
- "*.tsx"
|
|
8
|
+
- "*.js"
|
|
9
|
+
- "*.jsx"
|
|
10
|
+
- "*.mjs"
|
|
11
|
+
- "*.cjs"
|
|
12
|
+
- "**/*.ts"
|
|
13
|
+
- "**/*.tsx"
|
|
14
|
+
- "**/*.js"
|
|
15
|
+
- "**/*.jsx"
|
|
16
|
+
- "**/*.mjs"
|
|
17
|
+
- "**/*.cjs"
|
|
18
|
+
hunk_regex: "(app|router|server|fastify)\\.(get|post|put|patch|delete|on)\\s*\\([\\s\\S]{0,200}?\\basync\\b"
|
|
19
|
+
confidence_floor: 0.7
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
An HTTP route handler is declared `async` but its body contains an
|
|
23
|
+
`await` that can throw, no `try/catch`, and the framework in use is
|
|
24
|
+
NOT auto-promoting promise rejections to error middleware. Express 4
|
|
25
|
+
silently hangs the request; Express 5 + Hono + Fastify auto-handle,
|
|
26
|
+
but a misconfigured app or an older Express still drops these on the
|
|
27
|
+
floor: the client sees a request that times out at the LB while
|
|
28
|
+
the server logs a `UnhandledPromiseRejectionWarning` somewhere.
|
|
29
|
+
|
|
30
|
+
Flag when:
|
|
31
|
+
- handler is `async (req, res) => { ... await ... }`
|
|
32
|
+
- framework is Express 4 (check imports) OR the project has no
|
|
33
|
+
registered error-handling middleware visible
|
|
34
|
+
|
|
35
|
+
Suppress when:
|
|
36
|
+
- the handler is wrapped in an `asyncHandler` / `expressAsyncHandler`
|
|
37
|
+
/ `tryCatch` helper that translates rejection → next(err)
|
|
38
|
+
- framework is Hono / Fastify / Express 5 with the default error
|
|
39
|
+
surface (rejections become 500s automatically)
|
|
40
|
+
- the body has a top-level `try/catch` that ends in `res.status(...)`
|
|
41
|
+
- the handler is `next` style with the rejection threaded into
|
|
42
|
+
`.catch(next)`
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: auth-missing-on-state-change-route
|
|
3
|
+
description: POST / PUT / PATCH / DELETE route registered without an auth middleware in sight
|
|
4
|
+
triggers:
|
|
5
|
+
files:
|
|
6
|
+
- "*.ts"
|
|
7
|
+
- "*.tsx"
|
|
8
|
+
- "*.js"
|
|
9
|
+
- "*.jsx"
|
|
10
|
+
- "*.mjs"
|
|
11
|
+
- "*.cjs"
|
|
12
|
+
- "*.py"
|
|
13
|
+
- "*.go"
|
|
14
|
+
- "*.rb"
|
|
15
|
+
- "*.java"
|
|
16
|
+
- "*.kt"
|
|
17
|
+
- "**/*.ts"
|
|
18
|
+
- "**/*.tsx"
|
|
19
|
+
- "**/*.js"
|
|
20
|
+
- "**/*.jsx"
|
|
21
|
+
- "**/*.mjs"
|
|
22
|
+
- "**/*.cjs"
|
|
23
|
+
- "**/*.py"
|
|
24
|
+
- "**/*.go"
|
|
25
|
+
- "**/*.rb"
|
|
26
|
+
- "**/*.java"
|
|
27
|
+
- "**/*.kt"
|
|
28
|
+
hunk_regex: "\\.(post|put|patch|delete)\\s*\\(|@(Post|Put|Patch|Delete)\\b|@(post|put|patch|delete)\\b|@app\\.route\\b|@(Post|Put|Patch|Delete)Mapping\\b|@RequestMapping\\b"
|
|
29
|
+
security: true
|
|
30
|
+
confidence_floor: 0.75
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
A new route handler for a state-changing verb (POST / PUT / PATCH /
|
|
34
|
+
DELETE) is registered with no auth middleware chained on the route,
|
|
35
|
+
no `@auth` / `requireAuth` decorator, and no `req.user` /
|
|
36
|
+
`session.user` guard inside the body. Anyone with the URL can mutate
|
|
37
|
+
state. Particularly dangerous when the route is added under an
|
|
38
|
+
existing prefix where the dev assumed auth was inherited but the
|
|
39
|
+
prefix isn't actually protected.
|
|
40
|
+
|
|
41
|
+
Flag when the diff adds a state-changing route AND the body doesn't
|
|
42
|
+
reference an authenticated principal, AND no auth middleware appears
|
|
43
|
+
on the same route registration.
|
|
44
|
+
|
|
45
|
+
Suppress when:
|
|
46
|
+
- the route is explicitly public (signup, login, password-reset
|
|
47
|
+
request, public webhook with HMAC validation)
|
|
48
|
+
- a middleware chain on the router/app applies auth globally (look
|
|
49
|
+
for `app.use(authMiddleware)` / `router.use(...)` earlier in the
|
|
50
|
+
file)
|
|
51
|
+
- the framework auto-applies auth via a base controller / module
|
|
52
|
+
decorator (read the surrounding file before raising; the auth
|
|
53
|
+
may live one level up)
|
|
54
|
+
- the route reads/validates a signed token (webhook with HMAC,
|
|
55
|
+
short-lived signed URL)
|
|
56
|
+
|
|
57
|
+
In Java and Kotlin the same shape is `@PostMapping` / `@PutMapping` /
|
|
58
|
+
`@PatchMapping` / `@DeleteMapping` (or `@RequestMapping` with a method)
|
|
59
|
+
on a controller method with no `@PreAuthorize`, no `@Secured`, and no
|
|
60
|
+
rule in the security configuration covering the path.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: auth-role-from-user-input
|
|
3
|
+
description: Role / permission / isAdmin value taken from request body or query
|
|
4
|
+
triggers:
|
|
5
|
+
files:
|
|
6
|
+
- "*.ts"
|
|
7
|
+
- "*.tsx"
|
|
8
|
+
- "*.js"
|
|
9
|
+
- "*.jsx"
|
|
10
|
+
- "*.mjs"
|
|
11
|
+
- "*.cjs"
|
|
12
|
+
- "*.py"
|
|
13
|
+
- "*.go"
|
|
14
|
+
- "*.rb"
|
|
15
|
+
- "**/*.ts"
|
|
16
|
+
- "**/*.tsx"
|
|
17
|
+
- "**/*.js"
|
|
18
|
+
- "**/*.jsx"
|
|
19
|
+
- "**/*.mjs"
|
|
20
|
+
- "**/*.cjs"
|
|
21
|
+
- "**/*.py"
|
|
22
|
+
- "**/*.go"
|
|
23
|
+
- "**/*.rb"
|
|
24
|
+
hunk_regex: "(role|permission|isAdmin|is_admin|tier|plan|scopes?)\\b"
|
|
25
|
+
security: true
|
|
26
|
+
confidence_floor: 0.75
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
A role / permission / `isAdmin` / tier / plan value is being read
|
|
30
|
+
from request input (`req.body`, `req.query`, form data, JSON
|
|
31
|
+
payload) and used for an authorization check OR written to a
|
|
32
|
+
persistent record. This is the classic privilege-escalation bug:
|
|
33
|
+
the attacker passes `{"role": "admin"}` in the POST and the system
|
|
34
|
+
trusts it.
|
|
35
|
+
|
|
36
|
+
Flag when:
|
|
37
|
+
- a string/bool from request input is assigned to a `role` /
|
|
38
|
+
`permission` / `isAdmin` column on an `INSERT` / `UPDATE`
|
|
39
|
+
- a check like `if (req.body.role === 'admin')` gates an action
|
|
40
|
+
- a JWT / session is created with a `role` claim copied from input
|
|
41
|
+
|
|
42
|
+
Suppress when:
|
|
43
|
+
- the role assignment is explicitly gated by an admin-only
|
|
44
|
+
authorization check earlier in the function (look for
|
|
45
|
+
`requireRole('admin')` / `ctx.user.isAdmin`)
|
|
46
|
+
- the input goes through an allow-list AND the user is already an
|
|
47
|
+
admin (e.g. an admin promoting another user)
|
|
48
|
+
- the value is being compared to derive a STRING used for routing /
|
|
49
|
+
filtering (not for an authorization decision)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: auth-timing-attack-password-compare
|
|
3
|
+
description: Password / token / secret compared with == / === instead of a constant-time check
|
|
4
|
+
triggers:
|
|
5
|
+
files:
|
|
6
|
+
- "*.ts"
|
|
7
|
+
- "*.tsx"
|
|
8
|
+
- "*.js"
|
|
9
|
+
- "*.jsx"
|
|
10
|
+
- "*.mjs"
|
|
11
|
+
- "*.cjs"
|
|
12
|
+
- "*.py"
|
|
13
|
+
- "*.go"
|
|
14
|
+
- "*.rb"
|
|
15
|
+
- "*.java"
|
|
16
|
+
- "*.kt"
|
|
17
|
+
- "*.cs"
|
|
18
|
+
- "*.php"
|
|
19
|
+
- "**/*.ts"
|
|
20
|
+
- "**/*.tsx"
|
|
21
|
+
- "**/*.js"
|
|
22
|
+
- "**/*.jsx"
|
|
23
|
+
- "**/*.mjs"
|
|
24
|
+
- "**/*.cjs"
|
|
25
|
+
- "**/*.py"
|
|
26
|
+
- "**/*.go"
|
|
27
|
+
- "**/*.rb"
|
|
28
|
+
- "**/*.java"
|
|
29
|
+
- "**/*.kt"
|
|
30
|
+
- "**/*.cs"
|
|
31
|
+
- "**/*.php"
|
|
32
|
+
hunk_regex: "(password|secret|token|hmac|signature|apiKey|api_key)\\b\\s*[!=]==?"
|
|
33
|
+
security: true
|
|
34
|
+
confidence_floor: 0.7
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
A password / token / signature / HMAC / API key is being compared
|
|
38
|
+
with `==`, `===`, `!=`, `!==`, `.equals()`, or `strcmp`, all of
|
|
39
|
+
which short-circuit on the first byte mismatch and leak timing
|
|
40
|
+
information. An attacker can recover the secret byte-by-byte by
|
|
41
|
+
measuring response latency, especially against repeatedly-callable
|
|
42
|
+
endpoints.
|
|
43
|
+
|
|
44
|
+
Flag when:
|
|
45
|
+
- the comparison's left or right operand is named or sourced from
|
|
46
|
+
the request as a `password|secret|token|hmac|signature|apiKey`
|
|
47
|
+
- the operator is a default `==` / `===` / `!==` / `equals`
|
|
48
|
+
|
|
49
|
+
Suppress when:
|
|
50
|
+
- the call is `crypto.timingSafeEqual` /
|
|
51
|
+
`hmac.compare_digest` (Python) /
|
|
52
|
+
`subtle.ConstantTimeCompare` (Go) /
|
|
53
|
+
`MessageDigest.isEqual` (Java)
|
|
54
|
+
- the comparison is on the OUTPUT of a verified KDF (bcrypt /
|
|
55
|
+
scrypt / argon2 `compare`); the KDF wrapper is already
|
|
56
|
+
constant-time and that's the recommended API
|
|
57
|
+
- the comparison is against a literal "empty"/"missing" sentinel
|
|
58
|
+
(`token === undefined`); that's an existence check, not a value
|
|
59
|
+
match
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cookie-missing-secure-httponly
|
|
3
|
+
description: Auth / session cookie set without HttpOnly + Secure + SameSite
|
|
4
|
+
triggers:
|
|
5
|
+
files:
|
|
6
|
+
- "*.ts"
|
|
7
|
+
- "*.tsx"
|
|
8
|
+
- "*.js"
|
|
9
|
+
- "*.jsx"
|
|
10
|
+
- "*.mjs"
|
|
11
|
+
- "*.cjs"
|
|
12
|
+
- "*.py"
|
|
13
|
+
- "*.go"
|
|
14
|
+
- "*.rb"
|
|
15
|
+
- "**/*.ts"
|
|
16
|
+
- "**/*.tsx"
|
|
17
|
+
- "**/*.js"
|
|
18
|
+
- "**/*.jsx"
|
|
19
|
+
- "**/*.mjs"
|
|
20
|
+
- "**/*.cjs"
|
|
21
|
+
- "**/*.py"
|
|
22
|
+
- "**/*.go"
|
|
23
|
+
- "**/*.rb"
|
|
24
|
+
hunk_regex: "Set-Cookie\\b|res\\.cookie\\b|c\\.cookie\\b|reply\\.setCookie\\b|response\\.setHeader\\(.{0,40}cookie"
|
|
25
|
+
security: true
|
|
26
|
+
confidence_floor: 0.7
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
A `Set-Cookie` for a session / auth / refresh / CSRF cookie is
|
|
30
|
+
emitted without `HttpOnly` (JS-readable, stealable via XSS),
|
|
31
|
+
without `Secure` (sent over plain HTTP), or without a `SameSite`
|
|
32
|
+
attribute (allows CSRF via cross-site form submits in older
|
|
33
|
+
browsers / strict-mode-off contexts).
|
|
34
|
+
|
|
35
|
+
Flag when a cookie name containing `session|sid|auth|token|refresh`
|
|
36
|
+
is set with options that omit `httpOnly: true`, `secure: true`, or
|
|
37
|
+
`sameSite: 'strict'|'lax'`.
|
|
38
|
+
|
|
39
|
+
Suppress when:
|
|
40
|
+
- the cookie is intentionally JS-readable (e.g. a CSRF token cookie
|
|
41
|
+
paired with a header-based defense pattern), but check that
|
|
42
|
+
`secure + sameSite` are still set
|
|
43
|
+
- the environment is dev/test and the env-gated config explicitly
|
|
44
|
+
disables `secure` for local HTTP only
|
|
45
|
+
- a wrapper / framework default applies these attributes globally
|
|
46
|
+
(look for the wrapper's defaults before raising)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cors-wildcard-with-credentials
|
|
3
|
+
description: "CORS allows '*' origin while also setting credentials: true"
|
|
4
|
+
triggers:
|
|
5
|
+
files:
|
|
6
|
+
- "*.ts"
|
|
7
|
+
- "*.tsx"
|
|
8
|
+
- "*.js"
|
|
9
|
+
- "*.jsx"
|
|
10
|
+
- "*.mjs"
|
|
11
|
+
- "*.cjs"
|
|
12
|
+
- "*.py"
|
|
13
|
+
- "*.go"
|
|
14
|
+
- "*.rb"
|
|
15
|
+
- "**/*.ts"
|
|
16
|
+
- "**/*.tsx"
|
|
17
|
+
- "**/*.js"
|
|
18
|
+
- "**/*.jsx"
|
|
19
|
+
- "**/*.mjs"
|
|
20
|
+
- "**/*.cjs"
|
|
21
|
+
- "**/*.py"
|
|
22
|
+
- "**/*.go"
|
|
23
|
+
- "**/*.rb"
|
|
24
|
+
hunk_regex: "(Access-Control-Allow-Origin|cors\\s*\\(|allowOrigins|allow_origin)"
|
|
25
|
+
security: true
|
|
26
|
+
confidence_floor: 0.75
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
CORS is configured with `Access-Control-Allow-Origin: *` (or a
|
|
30
|
+
reflective `origin: true` that echoes the request's `Origin`)
|
|
31
|
+
together with `Access-Control-Allow-Credentials: true`. Browsers
|
|
32
|
+
reject wildcard + credentials together, but reflective Origin +
|
|
33
|
+
credentials lets any origin run authenticated requests against the
|
|
34
|
+
API: CSRF and cookie theft become trivial.
|
|
35
|
+
|
|
36
|
+
Flag when:
|
|
37
|
+
- `origin: '*'` is set with `credentials: true`
|
|
38
|
+
- `origin: true` (reflective) is set with `credentials: true` AND
|
|
39
|
+
there's no allow-list checked first
|
|
40
|
+
|
|
41
|
+
Suppress when:
|
|
42
|
+
- the origin is a concrete allow-list (`['https://app.example.com', ...]`)
|
|
43
|
+
or matched against one before being echoed
|
|
44
|
+
- `credentials: false` (the wildcard is safe without cookies)
|
|
45
|
+
- the API is genuinely public read-only and never reads / accepts
|
|
46
|
+
cookies / Authorization headers (defense in depth: still
|
|
47
|
+
recommend a concrete allow-list)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: crypto-jwt-verify-without-algo-allowlist
|
|
3
|
+
description: jwt.verify called without an explicit algorithms allowlist
|
|
4
|
+
triggers:
|
|
5
|
+
files:
|
|
6
|
+
- "*.ts"
|
|
7
|
+
- "*.tsx"
|
|
8
|
+
- "*.js"
|
|
9
|
+
- "*.jsx"
|
|
10
|
+
- "*.mjs"
|
|
11
|
+
- "*.cjs"
|
|
12
|
+
- "**/*.ts"
|
|
13
|
+
- "**/*.tsx"
|
|
14
|
+
- "**/*.js"
|
|
15
|
+
- "**/*.jsx"
|
|
16
|
+
- "**/*.mjs"
|
|
17
|
+
- "**/*.cjs"
|
|
18
|
+
hunk_regex: "jwt(\\.|.{0,40})verify\\s*\\(|jsonwebtoken|jose"
|
|
19
|
+
security: true
|
|
20
|
+
confidence_floor: 0.75
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
A JWT is verified without passing an explicit `algorithms` option.
|
|
24
|
+
The `jsonwebtoken` library historically accepted whatever the token
|
|
25
|
+
header declared, which lets an attacker switch to `alg: "none"`
|
|
26
|
+
(no signature) or downgrade an RSA-signed token to an HMAC verify
|
|
27
|
+
where the public key gets reused as the HMAC secret. Recent
|
|
28
|
+
versions tightened defaults but the attack returns the moment the
|
|
29
|
+
project pins an older version or uses an alternate library with
|
|
30
|
+
permissive defaults.
|
|
31
|
+
|
|
32
|
+
Flag when `jwt.verify(token, secret)` is called without `{
|
|
33
|
+
algorithms: ['HS256'|'RS256'|...] }` in the third argument.
|
|
34
|
+
|
|
35
|
+
Suppress when:
|
|
36
|
+
- `algorithms: [...]` is passed (with a concrete allow-list, not
|
|
37
|
+
e.g. `['none', ...]`)
|
|
38
|
+
- the library is `jose` and the call form already enforces alg via
|
|
39
|
+
the key object's `alg` field
|
|
40
|
+
- the verify is inside a wrapper that lints algorithms internally
|
|
41
|
+
(look for the wrapper definition before raising)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: crypto-math-random-for-tokens
|
|
3
|
+
description: Math.random() used to generate tokens, IDs, or secrets
|
|
4
|
+
triggers:
|
|
5
|
+
files:
|
|
6
|
+
- "*.ts"
|
|
7
|
+
- "*.tsx"
|
|
8
|
+
- "*.js"
|
|
9
|
+
- "*.jsx"
|
|
10
|
+
- "*.mjs"
|
|
11
|
+
- "*.cjs"
|
|
12
|
+
- "*.java"
|
|
13
|
+
- "*.kt"
|
|
14
|
+
- "**/*.ts"
|
|
15
|
+
- "**/*.tsx"
|
|
16
|
+
- "**/*.js"
|
|
17
|
+
- "**/*.jsx"
|
|
18
|
+
- "**/*.mjs"
|
|
19
|
+
- "**/*.cjs"
|
|
20
|
+
- "**/*.java"
|
|
21
|
+
- "**/*.kt"
|
|
22
|
+
hunk_regex: "Math\\.random\\s*\\(|\\bRandom\\s*\\("
|
|
23
|
+
security: true
|
|
24
|
+
confidence_floor: 0.75
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
`Math.random()` is a non-cryptographic PRNG. Its state is small,
|
|
28
|
+
its output is predictable from observed values, and modern V8 has
|
|
29
|
+
been shown to allow practical state-recovery attacks. Any token /
|
|
30
|
+
ID / nonce / one-time code generated with `Math.random()` and
|
|
31
|
+
used for a security purpose can be guessed.
|
|
32
|
+
|
|
33
|
+
Flag when `Math.random()` is used to build:
|
|
34
|
+
- session / password-reset / verify-email / magic-link tokens
|
|
35
|
+
- API keys, invite codes, OTP codes
|
|
36
|
+
- CSRF tokens or nonces
|
|
37
|
+
- request IDs that gate access (e.g. order-pickup codes)
|
|
38
|
+
|
|
39
|
+
Suppress when used for:
|
|
40
|
+
- jitter / backoff timing
|
|
41
|
+
- A/B test bucketing
|
|
42
|
+
- animation / UI randomness
|
|
43
|
+
- non-security IDs (telemetry trace fragments where collision is
|
|
44
|
+
the only concern)
|
|
45
|
+
- in tests, where determinism (`seedrandom`) or non-crypto values
|
|
46
|
+
are the point
|
|
47
|
+
|
|
48
|
+
Recommend `crypto.randomUUID()` / `crypto.randomBytes(n)` /
|
|
49
|
+
`crypto.getRandomValues(...)` per platform.
|
|
50
|
+
|
|
51
|
+
In Java and Kotlin the same bug is `new Random()`, `java.util.Random()`
|
|
52
|
+
or Kotlin's `Random()` where `SecureRandom` is required. `SecureRandom`
|
|
53
|
+
itself is the fix, not the bug.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: crypto-md5-sha1-for-secrets
|
|
3
|
+
description: MD5 / SHA-1 used for passwords, tokens, or signatures
|
|
4
|
+
triggers:
|
|
5
|
+
files:
|
|
6
|
+
- "*.ts"
|
|
7
|
+
- "*.tsx"
|
|
8
|
+
- "*.js"
|
|
9
|
+
- "*.jsx"
|
|
10
|
+
- "*.mjs"
|
|
11
|
+
- "*.cjs"
|
|
12
|
+
- "*.py"
|
|
13
|
+
- "*.go"
|
|
14
|
+
- "*.rb"
|
|
15
|
+
- "*.java"
|
|
16
|
+
- "*.kt"
|
|
17
|
+
- "*.cs"
|
|
18
|
+
- "*.php"
|
|
19
|
+
- "**/*.ts"
|
|
20
|
+
- "**/*.tsx"
|
|
21
|
+
- "**/*.js"
|
|
22
|
+
- "**/*.jsx"
|
|
23
|
+
- "**/*.mjs"
|
|
24
|
+
- "**/*.cjs"
|
|
25
|
+
- "**/*.py"
|
|
26
|
+
- "**/*.go"
|
|
27
|
+
- "**/*.rb"
|
|
28
|
+
- "**/*.java"
|
|
29
|
+
- "**/*.kt"
|
|
30
|
+
- "**/*.cs"
|
|
31
|
+
- "**/*.php"
|
|
32
|
+
hunk_regex: "\\b(md5|sha1|sha-?1)\\b|MessageDigest\\.getInstance\\s*\\(\\s*[\"'](MD5|SHA-?1)[\"']"
|
|
33
|
+
security: true
|
|
34
|
+
confidence_floor: 0.7
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
MD5 and SHA-1 are broken hashes for any security-bearing use:
|
|
38
|
+
collision attacks are practical on both, and pre-image attacks
|
|
39
|
+
on MD5 are within reach. Using them to hash passwords, derive
|
|
40
|
+
keys, or sign / verify integrity is a real bug, not a stylistic
|
|
41
|
+
preference.
|
|
42
|
+
|
|
43
|
+
Flag when MD5 / SHA-1 is used to:
|
|
44
|
+
- hash a password / passphrase (use bcrypt / scrypt / argon2)
|
|
45
|
+
- derive a key from a secret (use HKDF / PBKDF2)
|
|
46
|
+
- sign or verify a token / payload (use HMAC-SHA256 or better)
|
|
47
|
+
|
|
48
|
+
Suppress when:
|
|
49
|
+
- used for a NON-security purpose: ETag generation, cache key,
|
|
50
|
+
content addressing, deterministic identifier (the hash's
|
|
51
|
+
cryptographic weakness doesn't matter for cache invalidation)
|
|
52
|
+
- used to verify an integrity tag returned by an external system
|
|
53
|
+
that itself uses MD5 / SHA-1 (S3 ETag, legacy webhook contract)
|
|
54
|
+
- the call is to a `crypto.createHmac('sha1', ...)` and the spec
|
|
55
|
+
the code implements (e.g. AWS Signature V1, older OAuth flows)
|
|
56
|
+
mandates SHA-1
|
|
57
|
+
|
|
58
|
+
In Java and Kotlin this is `MessageDigest.getInstance("MD5")` or
|
|
59
|
+
`getInstance("SHA-1")`. The fix is BCrypt or Argon2 for passwords and
|
|
60
|
+
SHA-256 or better for signatures.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: env-vars-read-at-module-top
|
|
3
|
+
description: process.env read at module-evaluate time, undefined before loadEnvFile
|
|
4
|
+
triggers:
|
|
5
|
+
files:
|
|
6
|
+
- "*.ts"
|
|
7
|
+
- "*.tsx"
|
|
8
|
+
- "*.js"
|
|
9
|
+
- "*.jsx"
|
|
10
|
+
- "*.mjs"
|
|
11
|
+
- "*.cjs"
|
|
12
|
+
- "**/*.ts"
|
|
13
|
+
- "**/*.tsx"
|
|
14
|
+
- "**/*.js"
|
|
15
|
+
- "**/*.jsx"
|
|
16
|
+
- "**/*.mjs"
|
|
17
|
+
- "**/*.cjs"
|
|
18
|
+
hunk_regex: "process\\.env\\."
|
|
19
|
+
confidence_floor: 0.75
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
A module reads `process.env.X` at module-evaluate time (top-level
|
|
23
|
+
`const X = process.env.X` or in a default-export object). ESM
|
|
24
|
+
evaluates all imports BEFORE the entry's body, so any env var set
|
|
25
|
+
by `dotenv` / `process.loadEnvFile()` / a manual setup at the
|
|
26
|
+
entry point is undefined when this module loads.
|
|
27
|
+
|
|
28
|
+
Symptom: the value is `undefined` in dev despite the variable
|
|
29
|
+
existing in `.env`; client init silently uses defaults; integration
|
|
30
|
+
breaks in a way that's hard to root-cause from a stack trace.
|
|
31
|
+
|
|
32
|
+
Flag when a module's top-level body (NOT inside a function) reads
|
|
33
|
+
`process.env.X` AND the resulting value is exported / used to
|
|
34
|
+
initialize a client / SDK / config object.
|
|
35
|
+
|
|
36
|
+
Suppress when:
|
|
37
|
+
- the read is inside a function the entry point calls AFTER env
|
|
38
|
+
loading (lazy init pattern)
|
|
39
|
+
- the module documents that it must be imported AFTER env setup
|
|
40
|
+
with a comment explaining why
|
|
41
|
+
- the read is for a build-time constant baked in via the bundler
|
|
42
|
+
(Vite's `import.meta.env`, esbuild's `--define`)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: eval-on-user-input
|
|
3
|
+
description: eval / new Function / vm.runInThisContext over untrusted input, RCE
|
|
4
|
+
triggers:
|
|
5
|
+
files:
|
|
6
|
+
- "*.ts"
|
|
7
|
+
- "*.tsx"
|
|
8
|
+
- "*.js"
|
|
9
|
+
- "*.jsx"
|
|
10
|
+
- "*.mjs"
|
|
11
|
+
- "*.cjs"
|
|
12
|
+
- "*.java"
|
|
13
|
+
- "*.kt"
|
|
14
|
+
- "**/*.ts"
|
|
15
|
+
- "**/*.tsx"
|
|
16
|
+
- "**/*.js"
|
|
17
|
+
- "**/*.jsx"
|
|
18
|
+
- "**/*.mjs"
|
|
19
|
+
- "**/*.cjs"
|
|
20
|
+
- "**/*.java"
|
|
21
|
+
- "**/*.kt"
|
|
22
|
+
hunk_regex: "\\beval\\s*\\(|new\\s+Function\\s*\\(|vm\\.(runInThisContext|runInNewContext|runInContext)|child_process\\.(exec|execSync)|Runtime\\.getRuntime\\s*\\(\\s*\\)\\.exec|ProcessBuilder\\s*\\(|ScriptEngine\\b"
|
|
23
|
+
security: true
|
|
24
|
+
confidence_floor: 0.85
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
`eval(input)`, `new Function(input)`, or `vm.runInThisContext(input)`
|
|
28
|
+
called with a value derived from user input is server-side remote
|
|
29
|
+
code execution. `child_process.exec(input)` (the unsanitized
|
|
30
|
+
shell-string form) is the same thing for OS commands. There is no
|
|
31
|
+
"weak" version of this finding; if the input traces back to user
|
|
32
|
+
control, it is a critical bug.
|
|
33
|
+
|
|
34
|
+
Flag with high confidence when any of these calls receive a value
|
|
35
|
+
sourced from request body / query / path / form / env / file
|
|
36
|
+
contents.
|
|
37
|
+
|
|
38
|
+
Suppress when:
|
|
39
|
+
- the argument is a build-time constant
|
|
40
|
+
- the argument is derived from a strictly-typed structured input
|
|
41
|
+
(e.g. an integer parsed from `req.params.id`) embedded into a
|
|
42
|
+
fixed template, but recommend the safer alternative anyway
|
|
43
|
+
- `child_process.execFile` / `spawn` with an array of args is used
|
|
44
|
+
instead (no shell parsing)
|
|
45
|
+
- the input is run through a parser → AST → restricted-evaluator
|
|
46
|
+
(e.g. a sandboxed expression evaluator like `expr-eval`) rather
|
|
47
|
+
than the JS runtime
|
|
48
|
+
|
|
49
|
+
In Java and Kotlin the equivalents are `Runtime.getRuntime().exec(...)`,
|
|
50
|
+
`ProcessBuilder`, and the `ScriptEngine` family evaluating a script
|
|
51
|
+
string that came from a request.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fetch-without-timeout
|
|
3
|
+
description: Outbound HTTP request without timeout / abort signal, request thread hang
|
|
4
|
+
triggers:
|
|
5
|
+
files:
|
|
6
|
+
- "*.ts"
|
|
7
|
+
- "*.tsx"
|
|
8
|
+
- "*.js"
|
|
9
|
+
- "*.jsx"
|
|
10
|
+
- "*.mjs"
|
|
11
|
+
- "*.cjs"
|
|
12
|
+
- "**/*.ts"
|
|
13
|
+
- "**/*.tsx"
|
|
14
|
+
- "**/*.js"
|
|
15
|
+
- "**/*.jsx"
|
|
16
|
+
- "**/*.mjs"
|
|
17
|
+
- "**/*.cjs"
|
|
18
|
+
hunk_regex: "\\bfetch\\s*\\(|axios\\.(get|post|put|patch|delete|request)|got\\(|undici|ky\\(|node-fetch"
|
|
19
|
+
security: true
|
|
20
|
+
confidence_floor: 0.7
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
A server-side outbound HTTP call (fetch / axios / got / ky / undici)
|
|
24
|
+
is made without a timeout or `AbortSignal`. The upstream can take
|
|
25
|
+
seconds to minutes to time out at the network layer; meanwhile the
|
|
26
|
+
request handler is blocked, downstream callers stack up, and one
|
|
27
|
+
slow third party can drag the whole API into cascading latency.
|
|
28
|
+
|
|
29
|
+
Flag when:
|
|
30
|
+
- the call is `fetch(url)` with no `signal:` option
|
|
31
|
+
- `axios.get(...)` / `axios.request(...)` with no `timeout:` option
|
|
32
|
+
- the call is to a third-party host (not the same service / not
|
|
33
|
+
localhost)
|
|
34
|
+
|
|
35
|
+
Suppress when:
|
|
36
|
+
- `signal: AbortSignal.timeout(N)` is passed
|
|
37
|
+
- `timeout: N` is set
|
|
38
|
+
- the project-wide HTTP client wrapper applies a default timeout
|
|
39
|
+
(look for the wrapper before raising)
|
|
40
|
+
- the call is to a local sidecar / same-pod service where timeouts
|
|
41
|
+
are explicitly managed at the platform layer (rare in app code)
|