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.
Files changed (90) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +35 -0
  3. package/README.md +160 -0
  4. package/demo/baseline/.github/workflows/ci.yml +18 -0
  5. package/demo/baseline/Dockerfile +12 -0
  6. package/demo/baseline/app/__init__.py +0 -0
  7. package/demo/baseline/app/server.py +37 -0
  8. package/demo/baseline/package-lock.json +21 -0
  9. package/demo/baseline/package.json +9 -0
  10. package/demo/baseline/requirements.txt +1 -0
  11. package/demo/baseline/scripts/deploy.sh +13 -0
  12. package/demo/expected.json +126 -0
  13. package/demo/planted/.github/workflows/ci.yml +20 -0
  14. package/demo/planted/Dockerfile +11 -0
  15. package/demo/planted/app/config.py +3 -0
  16. package/demo/planted/app/search.py +17 -0
  17. package/demo/planted/app/server.py +40 -0
  18. package/demo/planted/package-lock.json +21 -0
  19. package/demo/planted/package.json +9 -0
  20. package/demo/planted/scripts/deploy.sh +13 -0
  21. package/dist/bin.js +42546 -0
  22. package/docs/agents.md +121 -0
  23. package/docs/cli.md +161 -0
  24. package/docs/config.md +165 -0
  25. package/docs/custom-scanners.md +164 -0
  26. package/docs/faq.md +54 -0
  27. package/docs/github-action.md +73 -0
  28. package/docs/index.md +29 -0
  29. package/docs/llms.txt +13 -0
  30. package/docs/quickstart.md +82 -0
  31. package/docs/scanners.md +148 -0
  32. package/docs/security.md +74 -0
  33. package/docs/telemetry.md +9 -0
  34. package/lenses/a11y-icon-only-button-no-aria-label.md +51 -0
  35. package/lenses/array-iteration-missing-key-prop.md +31 -0
  36. package/lenses/async-await-in-loop-n-plus-one.md +42 -0
  37. package/lenses/async-click-double-fire-race.md +59 -0
  38. package/lenses/async-floating-promise.md +40 -0
  39. package/lenses/async-promise-all-swallows-errors.md +42 -0
  40. package/lenses/async-unhandled-rejection-in-handler.md +42 -0
  41. package/lenses/auth-missing-on-state-change-route.md +60 -0
  42. package/lenses/auth-role-from-user-input.md +49 -0
  43. package/lenses/auth-timing-attack-password-compare.md +59 -0
  44. package/lenses/cookie-missing-secure-httponly.md +46 -0
  45. package/lenses/cors-wildcard-with-credentials.md +47 -0
  46. package/lenses/crypto-jwt-verify-without-algo-allowlist.md +41 -0
  47. package/lenses/crypto-math-random-for-tokens.md +53 -0
  48. package/lenses/crypto-md5-sha1-for-secrets.md +60 -0
  49. package/lenses/env-vars-read-at-module-top.md +42 -0
  50. package/lenses/eval-on-user-input.md +51 -0
  51. package/lenses/fetch-without-timeout.md +41 -0
  52. package/lenses/id-enumeration-sequential.md +49 -0
  53. package/lenses/interactive-state-decoupled-from-output.md +67 -0
  54. package/lenses/json-parse-no-try-catch.md +41 -0
  55. package/lenses/missing-rate-limit-on-auth.md +49 -0
  56. package/lenses/oauth-scope-wider-than-use.md +79 -0
  57. package/lenses/object-spread-clobber.md +44 -0
  58. package/lenses/open-redirect-from-untrusted-host.md +55 -0
  59. package/lenses/orm-drizzle-on-conflict-clobber.md +47 -0
  60. package/lenses/path-traversal-in-fs-access.md +59 -0
  61. package/lenses/pii-in-url-or-log.md +48 -0
  62. package/lenses/race-check-then-act.md +49 -0
  63. package/lenses/react-dangerously-set-inner-html.md +32 -0
  64. package/lenses/react-fetch-in-effect-without-abort.md +31 -0
  65. package/lenses/react-stale-closure-in-callback.md +28 -0
  66. package/lenses/react-state-set-in-render.md +33 -0
  67. package/lenses/react-use-effect-missing-cleanup.md +29 -0
  68. package/lenses/react-use-effect-missing-deps.md +30 -0
  69. package/lenses/regexp-from-user-input.md +43 -0
  70. package/lenses/return-shape-contract-break.md +71 -0
  71. package/lenses/secrets-logged-in-error-path.md +60 -0
  72. package/lenses/sql-migration-references-later-object.md +47 -0
  73. package/lenses/sql-string-concatenation.md +63 -0
  74. package/lenses/ssrf-server-side-fetch.md +69 -0
  75. package/lenses/supabase-comment-on-function-unqualified.md +37 -0
  76. package/lenses/supabase-function-default-public-execute.md +50 -0
  77. package/lenses/supabase-security-definer-no-search-path.md +34 -0
  78. package/lenses/supabase-single-500-on-no-match.md +65 -0
  79. package/lenses/upsert-state-column.md +57 -0
  80. package/lenses/url-not-encoded-for-user-id.md +78 -0
  81. package/lenses/use-state-default-not-functional.md +30 -0
  82. package/package.json +57 -0
  83. package/skills/openqodex/SKILL.md +141 -0
  84. package/templates/README.md +67 -0
  85. package/templates/claude-code/settings-hook.json +16 -0
  86. package/templates/cline/openqodex.md +9 -0
  87. package/templates/codex/AGENTS-section.md +8 -0
  88. package/templates/codex/hooks.json +16 -0
  89. package/templates/cursor/openqodex.mdc +15 -0
  90. 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)