oauthlint-rules 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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -0
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +3 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/loader.d.ts +35 -0
  8. package/dist/loader.d.ts.map +1 -0
  9. package/dist/loader.js +72 -0
  10. package/dist/loader.js.map +1 -0
  11. package/dist/manifest.d.ts +10 -0
  12. package/dist/manifest.d.ts.map +1 -0
  13. package/dist/manifest.js +10 -0
  14. package/dist/manifest.js.map +1 -0
  15. package/dist/schema.d.ts +425 -0
  16. package/dist/schema.d.ts.map +1 -0
  17. package/dist/schema.js +71 -0
  18. package/dist/schema.js.map +1 -0
  19. package/package.json +61 -0
  20. package/rules/cookie/long-lived.yml +42 -0
  21. package/rules/cookie/no-httponly.yml +54 -0
  22. package/rules/cookie/no-samesite.yml +56 -0
  23. package/rules/cookie/no-secure.yml +58 -0
  24. package/rules/cors/wildcard-with-credentials.yml +56 -0
  25. package/rules/flow/insecure-random.yml +56 -0
  26. package/rules/flow/no-rate-limit.yml +39 -0
  27. package/rules/flow/password-min-length.yml +44 -0
  28. package/rules/flow/password-plaintext.yml +82 -0
  29. package/rules/flow/timing-unsafe-compare.yml +103 -0
  30. package/rules/jwt/alg-none.yml +46 -0
  31. package/rules/jwt/algorithm-confusion.yml +67 -0
  32. package/rules/jwt/in-url.yml +33 -0
  33. package/rules/jwt/localstorage.yml +39 -0
  34. package/rules/jwt/no-audience.yml +49 -0
  35. package/rules/jwt/no-expiration.yml +45 -0
  36. package/rules/jwt/no-issuer.yml +40 -0
  37. package/rules/jwt/weak-secret.yml +56 -0
  38. package/rules/oauth/broad-scope.yml +39 -0
  39. package/rules/oauth/hardcoded-secret.yml +41 -0
  40. package/rules/oauth/implicit-flow.yml +36 -0
  41. package/rules/oauth/long-token-lifetime.yml +57 -0
  42. package/rules/oauth/no-pkce.yml +50 -0
  43. package/rules/oauth/no-state-validation.yml +42 -0
  44. package/rules/oauth/no-state.yml +41 -0
  45. package/rules/oauth/open-redirect-callback.yml +47 -0
  46. package/rules/oauth/wildcard-redirect.yml +45 -0
  47. package/rules/secret/provider-key.yml +44 -0
  48. package/rules/session/id-in-url.yml +30 -0
  49. package/rules/session/no-regeneration.yml +53 -0
@@ -0,0 +1,56 @@
1
+ rules:
2
+ - id: auth.cookie.no-samesite
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: INFO
7
+ message: |
8
+ A session/auth cookie is being set WITHOUT the `SameSite`
9
+ attribute. Modern browsers default to `Lax`, but explicit is
10
+ better — and APIs that legitimately need cross-site usage should
11
+ consciously opt into `None` (with `Secure`), not silently inherit
12
+ whatever the browser does today.
13
+
14
+ For most auth flows, `SameSite=Strict` is the right answer; for
15
+ OAuth callbacks, `SameSite=Lax` is required.
16
+ pattern-either:
17
+ - patterns:
18
+ - pattern: $RES.cookie($NAME, $VAL, $OPTS)
19
+ - metavariable-regex:
20
+ metavariable: $NAME
21
+ regex: ^(['"])(?:[a-zA-Z0-9_-]*(?:session|sid|sess|auth|token|jwt|refresh|access)[a-zA-Z0-9_-]*)\1$
22
+ - metavariable-pattern:
23
+ metavariable: $OPTS
24
+ patterns:
25
+ - pattern-not: '{..., sameSite: ..., ...}'
26
+ - pattern-not: '{..., "sameSite": ..., ...}'
27
+ - pattern: '{...}'
28
+ # SameSite=None without Secure: None *requires* Secure, so this cookie is
29
+ # rejected by modern browsers and signals a real misconfiguration.
30
+ - patterns:
31
+ - pattern: $RES.cookie($NAME, $VAL, $OPTS)
32
+ - metavariable-regex:
33
+ metavariable: $NAME
34
+ regex: ^(['"])(?:[a-zA-Z0-9_-]*(?:session|sid|sess|auth|token|jwt|refresh|access)[a-zA-Z0-9_-]*)\1$
35
+ - metavariable-pattern:
36
+ metavariable: $OPTS
37
+ patterns:
38
+ - pattern-either:
39
+ - pattern: "{..., sameSite: 'none', ...}"
40
+ - pattern: '{..., sameSite: "none", ...}'
41
+ - pattern: "{..., sameSite: 'None', ...}"
42
+ - pattern: '{..., sameSite: "None", ...}'
43
+ - pattern-not: '{..., secure: true, ...}'
44
+ fix: "$RES.cookie($NAME, $VAL, { ...$OPTS, sameSite: 'strict' })"
45
+ metadata:
46
+ oauthlint-rule-id: AUTH-COOKIE-003
47
+ oauthlint-doc-url: https://oauthlint.dev/rules/cookie-no-samesite
48
+ category: security
49
+ cwe: CWE-1275
50
+ owasp: API1:2023
51
+ llm-prevalence: MEDIUM
52
+ technology:
53
+ - express
54
+ - fastify
55
+ references:
56
+ - https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-13#section-4.1.2.7
@@ -0,0 +1,58 @@
1
+ rules:
2
+ - id: auth.cookie.no-secure
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: WARNING
7
+ message: |
8
+ A cookie that looks like a session or auth cookie is being set WITHOUT
9
+ the `Secure` flag. The browser will happily send it over plain HTTP,
10
+ which means a network attacker (open Wi-Fi, malicious proxy, downgrade
11
+ attack) can capture it.
12
+
13
+ Add `{ secure: true }` to the cookie options. If you absolutely need
14
+ to set Secure-less cookies in dev, gate it on `NODE_ENV !== 'production'`.
15
+ pattern-either:
16
+ - patterns:
17
+ - pattern: $RES.cookie($NAME, $VAL, $OPTS)
18
+ - metavariable-regex:
19
+ metavariable: $NAME
20
+ regex: ^(['"])(?:[a-zA-Z0-9_-]*(?:session|sid|sess|auth|token|jwt|refresh|access)[a-zA-Z0-9_-]*)\1$
21
+ - metavariable-pattern:
22
+ metavariable: $OPTS
23
+ patterns:
24
+ - pattern-not: '{..., secure: true, ...}'
25
+ - pattern-not: '{..., secure: $X, ...}'
26
+ - pattern: '{...}'
27
+ # Secure explicitly disabled — the exact bug, must fire (the `secure: $X`
28
+ # suppression above would otherwise treat `secure: false` as compliant).
29
+ - patterns:
30
+ - pattern: $RES.cookie($NAME, $VAL, $OPTS)
31
+ - metavariable-regex:
32
+ metavariable: $NAME
33
+ regex: ^(['"])(?:[a-zA-Z0-9_-]*(?:session|sid|sess|auth|token|jwt|refresh|access)[a-zA-Z0-9_-]*)\1$
34
+ - metavariable-pattern:
35
+ metavariable: $OPTS
36
+ pattern: '{..., secure: false, ...}'
37
+ - patterns:
38
+ - pattern: $RES.cookie($NAME, $VAL)
39
+ - metavariable-regex:
40
+ metavariable: $NAME
41
+ regex: ^(['"])(?:[a-zA-Z0-9_-]*(?:session|sid|sess|auth|token|jwt|refresh|access)[a-zA-Z0-9_-]*)\1$
42
+ # Auto-fix applies to the first (3-arg) pattern: spread the existing
43
+ # options object and inject secure:true. Verbose but always-correct;
44
+ # users can run prettier afterwards. The 2-arg branch is left alone
45
+ # because rewriting it requires inserting a brand-new argument.
46
+ fix: '$RES.cookie($NAME, $VAL, { ...$OPTS, secure: true })'
47
+ metadata:
48
+ oauthlint-rule-id: AUTH-COOKIE-001
49
+ oauthlint-doc-url: https://oauthlint.dev/rules/cookie-no-secure
50
+ category: security
51
+ cwe: CWE-614
52
+ owasp: API8:2023
53
+ llm-prevalence: HIGH
54
+ technology:
55
+ - express
56
+ - fastify
57
+ references:
58
+ - https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.2.5
@@ -0,0 +1,56 @@
1
+ rules:
2
+ - id: auth.cors.wildcard-with-credentials
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: ERROR
7
+ message: |
8
+ CORS is configured with `Access-Control-Allow-Origin: *` and
9
+ `Access-Control-Allow-Credentials: true` at the same time. The
10
+ spec forbids this combination — browsers will block it — but
11
+ developers regularly try to "fix" the resulting error by switching
12
+ to `origin: true` (which echoes the requesting origin back),
13
+ effectively turning the policy into "allow credentials from
14
+ ANYWHERE". That's a CSRF-on-steroids primitive.
15
+
16
+ Decide which one you actually need:
17
+ - Public API, no cookies/auth headers needed cross-site →
18
+ `origin: '*'`, `credentials: false` (default).
19
+ - Authenticated API for a known frontend → enumerate the exact
20
+ origins, `credentials: true`.
21
+
22
+ Never combine wildcard origins with credentials enabled.
23
+ pattern-either:
24
+ # express cors() middleware — a wildcard ("*"), origin-echoing (true),
25
+ # request-reflected, or always-allow callback origin combined with
26
+ # credentials: true.
27
+ - patterns:
28
+ - pattern-either:
29
+ - pattern: 'cors({ ..., origin: "*", ..., credentials: true, ... })'
30
+ - pattern: 'cors({ ..., credentials: true, ..., origin: "*", ... })'
31
+ - pattern: 'cors({ ..., origin: true, ..., credentials: true, ... })'
32
+ - pattern: 'cors({ ..., credentials: true, ..., origin: true, ... })'
33
+ - pattern: 'cors({ ..., origin: $REQ.headers.origin, ..., credentials: true, ... })'
34
+ - pattern: 'cors({ ..., credentials: true, ..., origin: $REQ.headers.origin, ... })'
35
+ - pattern: 'cors({ ..., origin: ($O, $CB) => $CB(null, true), ..., credentials: true, ... })'
36
+ - pattern: 'cors({ ..., credentials: true, ..., origin: ($O, $CB) => $CB(null, true), ... })'
37
+ # Manual header writes: Access-Control-Allow-Origin: * together with
38
+ # Access-Control-Allow-Credentials: true (the two setHeader calls are
39
+ # always adjacent, so a bounded span regex catches both orders).
40
+ - pattern-regex: |-
41
+ Access-Control-Allow-Origin['"]\s*,\s*['"]\*['"][\s\S]{0,200}Access-Control-Allow-Credentials['"]\s*,\s*['"]true['"]
42
+ - pattern-regex: |-
43
+ Access-Control-Allow-Credentials['"]\s*,\s*['"]true['"][\s\S]{0,200}Access-Control-Allow-Origin['"]\s*,\s*['"]\*['"]
44
+ metadata:
45
+ oauthlint-rule-id: AUTH-CORS-001
46
+ oauthlint-doc-url: https://oauthlint.dev/rules/cors-wildcard-with-credentials
47
+ category: security
48
+ cwe: CWE-942
49
+ owasp: API8:2023
50
+ llm-prevalence: HIGH
51
+ technology:
52
+ - express
53
+ - cors
54
+ references:
55
+ - https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSNotSupportingCredentials
56
+ - https://portswigger.net/web-security/cors
@@ -0,0 +1,56 @@
1
+ rules:
2
+ - id: auth.flow.insecure-random
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: ERROR
7
+ message: |
8
+ `Math.random()` is being assigned to (or used to compute) a value
9
+ whose name indicates it is security-sensitive — a token, a CSRF
10
+ value, an OAuth `state`, a session id, a nonce, or a verification
11
+ code. `Math.random()` is NOT cryptographically secure and is
12
+ predictable enough for an attacker with enough samples to recover
13
+ the seed.
14
+
15
+ Use `crypto.randomBytes(N)` (Node) or `crypto.getRandomValues()`
16
+ (browser) and base64url-encode the result.
17
+
18
+ OWASP ASVS V2.5: all security-sensitive random values must come
19
+ from a CSPRNG.
20
+ # `<... Math.random() ...>` deep-matches Math.random() anywhere in the
21
+ # initializer, covering the common token idioms:
22
+ # Math.random().toString(36).slice(2) / .substring(2) / Math.floor(Math.random()*1e9)
23
+ # The name regex is case-insensitive (catches resetToken, csrfToken, …) and
24
+ # uses specific security words only — bare `code`/`sid` were removed because
25
+ # they matched barcode/zipcode/aside and produced false positives.
26
+ pattern-either:
27
+ - patterns:
28
+ - pattern: 'const $NAME = <... Math.random() ...>'
29
+ - metavariable-regex:
30
+ metavariable: $NAME
31
+ regex: (?i)^(?:[a-z][a-z0-9_]*)?(?:token|secret|csrf|nonce|sessionid|otp|verificationcode|verifycode|resetcode|apikey|password|salt)[a-z0-9_]*$
32
+ - patterns:
33
+ - pattern: 'let $NAME = <... Math.random() ...>'
34
+ - metavariable-regex:
35
+ metavariable: $NAME
36
+ regex: (?i)^(?:[a-z][a-z0-9_]*)?(?:token|secret|csrf|nonce|sessionid|otp|verificationcode|verifycode|resetcode|apikey|password|salt)[a-z0-9_]*$
37
+ - patterns:
38
+ - pattern: 'var $NAME = <... Math.random() ...>'
39
+ - metavariable-regex:
40
+ metavariable: $NAME
41
+ regex: (?i)^(?:[a-z][a-z0-9_]*)?(?:token|secret|csrf|nonce|sessionid|otp|verificationcode|verifycode|resetcode|apikey|password|salt)[a-z0-9_]*$
42
+ - patterns:
43
+ - pattern: '$OBJ.$NAME = <... Math.random() ...>'
44
+ - metavariable-regex:
45
+ metavariable: $NAME
46
+ regex: (?i)^(?:[a-z][a-z0-9_]*)?(?:token|secret|csrf|nonce|sessionid|otp|verificationcode|verifycode|resetcode|apikey|password|salt)[a-z0-9_]*$
47
+ metadata:
48
+ oauthlint-rule-id: AUTH-FLOW-003
49
+ oauthlint-doc-url: https://oauthlint.dev/rules/flow-insecure-random
50
+ category: security
51
+ cwe: CWE-338
52
+ owasp: API2:2023
53
+ llm-prevalence: HIGH
54
+ references:
55
+ - https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html
56
+ - https://cwe.mitre.org/data/definitions/338.html
@@ -0,0 +1,39 @@
1
+ rules:
2
+ - id: auth.flow.no-rate-limit
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: INFO
7
+ message: |
8
+ A `/login`, `/signin`, `/auth`, or `/reset-password` POST handler is
9
+ registered without any rate-limit middleware in scope. Without a
10
+ rate limit, credential-stuffing and brute-force attacks are
11
+ essentially free for the attacker.
12
+
13
+ Add `express-rate-limit`, `@fastify/rate-limit`, or a gateway-level
14
+ WAF rule. Per-IP + per-account is the typical pairing.
15
+ # 2-argument route registration (path, handler) on any router-like object
16
+ # (app / router / fastify / …) whose path looks like an auth endpoint. The
17
+ # 2-arg form means no middleware is in the chain; adding rate-limit
18
+ # middleware uses the 3+-arg form (or fastify's options object) and so is
19
+ # not flagged. Path matching is a regex, so `/api/login`, `/v1/auth`,
20
+ # `/auth/signin` etc. are all covered.
21
+ # NOTE: app-level rate limiting (`app.use(rateLimit())`) is invisible to a
22
+ # purely syntactic rule — hence INFO severity.
23
+ patterns:
24
+ - pattern: '$APP.post($PATH, $HANDLER)'
25
+ - metavariable-regex:
26
+ metavariable: $PATH
27
+ regex: ^['"][^'"]*\/(?:login|signin|sign-in|signup|sign-up|authenticate|auth|reset-password|forgot-password)(?![a-z-])[^'"]*['"]$
28
+ metadata:
29
+ oauthlint-rule-id: AUTH-FLOW-002
30
+ oauthlint-doc-url: https://oauthlint.dev/rules/flow-no-rate-limit
31
+ category: security
32
+ cwe: CWE-307
33
+ owasp: API4:2023
34
+ llm-prevalence: HIGH
35
+ technology:
36
+ - express
37
+ - fastify
38
+ references:
39
+ - https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
@@ -0,0 +1,44 @@
1
+ rules:
2
+ - id: auth.flow.password-min-length
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: WARNING
7
+ message: |
8
+ A password validation schema is enforcing a minimum length of less
9
+ than 8 characters. NIST SP 800-63B recommends ≥ 8 characters for
10
+ user-chosen passwords (with NO mandatory complexity rules — length
11
+ is the dominant strength factor). OWASP ASVS V2.1.1 requires ≥ 12
12
+ for high-assurance applications.
13
+
14
+ Common LLM-generated mistake: `password: z.string().min(6)` because
15
+ "6 looks reasonable" — it isn't. Bump the floor to 8 minimum, 12
16
+ preferred.
17
+ pattern-either:
18
+ - patterns:
19
+ # `$Z.string()` covers z./Joi./yup.; the 2-arg form catches the
20
+ # common `.min(6, 'too short')` custom-message style.
21
+ - pattern-either:
22
+ - pattern: '$Z.string().min($N)'
23
+ - pattern: '$Z.string().min($N, ...)'
24
+ - metavariable-comparison:
25
+ metavariable: $N
26
+ comparison: $N < 8
27
+ - pattern-inside: |
28
+ { ..., password: ..., ... }
29
+ - patterns:
30
+ - pattern-regex: \b(?:password|pwd)\s*\.\s*length\s*[<≤]=?\s*[1-7]\b
31
+ metadata:
32
+ oauthlint-rule-id: AUTH-FLOW-005
33
+ oauthlint-doc-url: https://oauthlint.dev/rules/flow-password-min-length
34
+ category: security
35
+ cwe: CWE-521
36
+ owasp: API2:2023
37
+ llm-prevalence: MEDIUM
38
+ technology:
39
+ - zod
40
+ - joi
41
+ - yup
42
+ references:
43
+ - https://pages.nist.gov/800-63-3/sp800-63b.html
44
+ - https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
@@ -0,0 +1,82 @@
1
+ rules:
2
+ - id: auth.flow.password-plaintext
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: ERROR
7
+ message: |
8
+ A user-supplied password is being persisted WITHOUT being hashed first.
9
+ Storing plaintext passwords means any database read (backup, SQL injection,
10
+ misconfigured backup, contractor with read-only access) leaks every
11
+ credential in one shot.
12
+
13
+ Hash with argon2id (recommended), bcrypt, or scrypt before persisting.
14
+ Never use plain SHA-256, MD5, or any unsalted hash for passwords.
15
+
16
+ OWASP ASVS V2.4 mandates an adaptive, salted hash. Every modern stack
17
+ ships one — there is no reason to roll your own.
18
+ pattern-either:
19
+ # Flat persistence object on a user-ish model/repo:
20
+ # db.users.create({password}), User.create({password}), userRepo.save({password}),
21
+ # new User({password}), prisma.user.insertOne({password}), …
22
+ - patterns:
23
+ - pattern-either:
24
+ - pattern: '$DB.$MODEL.create({..., password: $PWD, ...})'
25
+ - pattern: '$DB.$MODEL.insert({..., password: $PWD, ...})'
26
+ - pattern: '$DB.$MODEL.insertOne({..., password: $PWD, ...})'
27
+ - pattern: '$DB.$MODEL.save({..., password: $PWD, ...})'
28
+ - pattern: '$MODEL.create({..., password: $PWD, ...})'
29
+ - pattern: '$MODEL.insert({..., password: $PWD, ...})'
30
+ - pattern: '$MODEL.save({..., password: $PWD, ...})'
31
+ - pattern: 'new $MODEL({..., password: $PWD, ...})'
32
+ - metavariable-regex:
33
+ metavariable: $MODEL
34
+ regex: '(?i)user'
35
+ - metavariable-pattern:
36
+ metavariable: $PWD
37
+ patterns:
38
+ - pattern-not: '$X.hash(...)'
39
+ - pattern-not: 'hash($X, ...)'
40
+ - pattern-not: 'bcrypt.$Y(...)'
41
+ - pattern-not: 'argon2.$Y(...)'
42
+ - pattern-not: 'scrypt.$Y(...)'
43
+ - pattern-not: 'crypto.scrypt(...)'
44
+ - pattern-not: 'await $X.hash(...)'
45
+ - pattern-not: 'await hash($X, ...)'
46
+ # Don't flag a value whose name says it is already hashed
47
+ # (hashedPassword, passwordHash, …) — the common pre-hash idiom.
48
+ - metavariable-regex:
49
+ metavariable: $PWD
50
+ regex: '(?i)^(?!.*hash).*$'
51
+ # Prisma-style nested data wrapper: prisma.user.create({ data: { password } }).
52
+ - patterns:
53
+ - pattern-either:
54
+ - pattern: '$DB.$MODEL.create({..., data: {..., password: $PWD, ...}, ...})'
55
+ - pattern: '$DB.$MODEL.update({..., data: {..., password: $PWD, ...}, ...})'
56
+ - metavariable-regex:
57
+ metavariable: $MODEL
58
+ regex: '(?i)user'
59
+ - metavariable-pattern:
60
+ metavariable: $PWD
61
+ patterns:
62
+ - pattern-not: '$X.hash(...)'
63
+ - pattern-not: 'hash($X, ...)'
64
+ - pattern-not: 'bcrypt.$Y(...)'
65
+ - pattern-not: 'argon2.$Y(...)'
66
+ - pattern-not: 'scrypt.$Y(...)'
67
+ - pattern-not: 'crypto.scrypt(...)'
68
+ - pattern-not: 'await $X.hash(...)'
69
+ - pattern-not: 'await hash($X, ...)'
70
+ - metavariable-regex:
71
+ metavariable: $PWD
72
+ regex: '(?i)^(?!.*hash).*$'
73
+ metadata:
74
+ oauthlint-rule-id: AUTH-FLOW-001
75
+ oauthlint-doc-url: https://oauthlint.dev/rules/password-plaintext
76
+ category: security
77
+ cwe: CWE-256
78
+ owasp: API2:2023
79
+ llm-prevalence: MEDIUM
80
+ references:
81
+ - https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
82
+ - https://cwe.mitre.org/data/definitions/256.html
@@ -0,0 +1,103 @@
1
+ rules:
2
+ - id: auth.flow.timing-unsafe-compare
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: WARNING
7
+ message: |
8
+ A secret-shaped value (`password`, `token`, `secret`, `apiKey`,
9
+ `csrf`, `hmac`) is being compared with `===` / `!==` /
10
+ `string1 == string2`. JavaScript's equality operators short-circuit
11
+ on the first differing byte, which leaks the matching prefix
12
+ length over the wire — a classic timing-attack vector.
13
+
14
+ Use `crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b))` (both
15
+ buffers must be the same length, so hash first if needed). For
16
+ password verification, use `argon2.verify`, `bcrypt.compare`, or
17
+ `scrypt` — they handle constant-time comparison for you.
18
+ # We want to catch `secretVar === userInput` but NOT the dozens of
19
+ # legitimate non-secret comparisons that just happen to involve a
20
+ # variable whose name ends with one of our secret-looking suffixes:
21
+ # typeof token === 'string' (type check)
22
+ # signature !== '' (presence check)
23
+ # parts.length === 3 (shape check)
24
+ # token.length === expectedLength (length check)
25
+ # The pattern-not clauses below carve those out — they were the
26
+ # entirety of the FP class we saw on jose / next-auth in validation.
27
+ pattern-either:
28
+ - patterns:
29
+ - pattern-either:
30
+ - pattern: '$A === $B'
31
+ - pattern: '$A !== $B'
32
+ - pattern: '$A == $B'
33
+ - pattern: '$A != $B'
34
+ - metavariable-regex:
35
+ metavariable: $A
36
+ regex: (?i)^.*(?:password|secret|token|apikey|api_key|csrf|hmac|signature)$
37
+ - pattern-not: 'typeof $X === $Y'
38
+ - pattern-not: 'typeof $X !== $Y'
39
+ - pattern-not: 'typeof $X == $Y'
40
+ - pattern-not: 'typeof $X != $Y'
41
+ - pattern-not: '$X === ""'
42
+ - pattern-not: "$X === ''"
43
+ - pattern-not: '$X !== ""'
44
+ - pattern-not: "$X !== ''"
45
+ - pattern-not: '$X == ""'
46
+ - pattern-not: "$X == ''"
47
+ - pattern-not: '$X != ""'
48
+ - pattern-not: "$X != ''"
49
+ - pattern-not: '$X.length === $N'
50
+ - pattern-not: '$X.length !== $N'
51
+ - pattern-not: '$X.length == $N'
52
+ - pattern-not: '$X.length != $N'
53
+ - pattern-not: '$X === null'
54
+ - pattern-not: '$X !== null'
55
+ - pattern-not: '$X == null'
56
+ - pattern-not: '$X != null'
57
+ - pattern-not: '$X === undefined'
58
+ - pattern-not: '$X !== undefined'
59
+ - pattern-not: '$X == undefined'
60
+ - pattern-not: '$X != undefined'
61
+ - patterns:
62
+ - pattern-either:
63
+ - pattern: '$A === $B'
64
+ - pattern: '$A !== $B'
65
+ - pattern: '$A == $B'
66
+ - pattern: '$A != $B'
67
+ - metavariable-regex:
68
+ metavariable: $B
69
+ regex: (?i)^.*(?:password|secret|token|apikey|api_key|csrf|hmac|signature)$
70
+ - pattern-not: 'typeof $X === $Y'
71
+ - pattern-not: 'typeof $X !== $Y'
72
+ - pattern-not: 'typeof $X == $Y'
73
+ - pattern-not: 'typeof $X != $Y'
74
+ - pattern-not: '$X === ""'
75
+ - pattern-not: "$X === ''"
76
+ - pattern-not: '$X !== ""'
77
+ - pattern-not: "$X !== ''"
78
+ - pattern-not: '$X == ""'
79
+ - pattern-not: "$X == ''"
80
+ - pattern-not: '$X != ""'
81
+ - pattern-not: "$X != ''"
82
+ - pattern-not: '$X.length === $N'
83
+ - pattern-not: '$X.length !== $N'
84
+ - pattern-not: '$X.length == $N'
85
+ - pattern-not: '$X.length != $N'
86
+ - pattern-not: '$X === null'
87
+ - pattern-not: '$X !== null'
88
+ - pattern-not: '$X == null'
89
+ - pattern-not: '$X != null'
90
+ - pattern-not: '$X === undefined'
91
+ - pattern-not: '$X !== undefined'
92
+ - pattern-not: '$X == undefined'
93
+ - pattern-not: '$X != undefined'
94
+ metadata:
95
+ oauthlint-rule-id: AUTH-FLOW-004
96
+ oauthlint-doc-url: https://oauthlint.dev/rules/flow-timing-unsafe-compare
97
+ category: security
98
+ cwe: CWE-208
99
+ owasp: API2:2023
100
+ llm-prevalence: MEDIUM
101
+ references:
102
+ - https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
103
+ - https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b
@@ -0,0 +1,46 @@
1
+ rules:
2
+ - id: auth.jwt.alg-none
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: ERROR
7
+ message: |
8
+ JWTs are being verified with the `none` algorithm in the allowed list.
9
+ An attacker can forge any token by simply setting `alg: none` in the header
10
+ and supplying no signature, because the verification routine will accept it.
11
+
12
+ Restrict `algorithms` to the ones you actually use, e.g. ["RS256"] or ["ES256"].
13
+ Never include "none" or "None" in production code paths.
14
+
15
+ RFC 7518 §3.6 explicitly warns: "Implementations SHOULD NOT support the
16
+ 'none' algorithm in deployed systems."
17
+ # Scoped to `jsonwebtoken` (alias `jwt`) — generic $X.verify
18
+ # produced FPs on jose internals and re-exported test helpers.
19
+ pattern-either:
20
+ - pattern: 'jwt.verify($TOKEN, $SECRET, { ..., algorithms: [..., "none", ...], ... })'
21
+ - pattern: 'jwt.verify($TOKEN, $SECRET, { ..., algorithms: [..., "None", ...], ... })'
22
+ - pattern: 'jwt.verify($TOKEN, $SECRET, { ..., algorithms: [..., "NONE", ...], ... })'
23
+ - pattern: 'jwt.decode($TOKEN, { complete: true, algorithms: [..., "none", ...] })'
24
+ # Destructured import: `import { verify } from 'jsonwebtoken'`. Scoped to
25
+ # the import so a bare verify() from another library is not matched.
26
+ - patterns:
27
+ - pattern-inside: |
28
+ import { ..., verify, ... } from 'jsonwebtoken'
29
+ ...
30
+ - pattern-either:
31
+ - pattern: 'verify($TOKEN, $SECRET, { ..., algorithms: [..., "none", ...], ... })'
32
+ - pattern: 'verify($TOKEN, $SECRET, { ..., algorithms: [..., "None", ...], ... })'
33
+ - pattern: 'verify($TOKEN, $SECRET, { ..., algorithms: [..., "NONE", ...], ... })'
34
+ metadata:
35
+ oauthlint-rule-id: AUTH-JWT-001
36
+ oauthlint-doc-url: https://oauthlint.dev/rules/jwt-alg-none
37
+ category: security
38
+ cwe: CWE-327
39
+ owasp: API2:2023
40
+ llm-prevalence: HIGH
41
+ technology:
42
+ - jsonwebtoken
43
+ - jose
44
+ references:
45
+ - https://datatracker.ietf.org/doc/html/rfc7518#section-3.6
46
+ - https://owasp.org/www-project-api-security/
@@ -0,0 +1,67 @@
1
+ rules:
2
+ - id: auth.jwt.algorithm-confusion
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: ERROR
7
+ message: |
8
+ A JWT is being verified with HS256 (a symmetric algorithm) but the
9
+ key looks like an RSA / EC public key in PEM format. This is the
10
+ "algorithm confusion" attack: an attacker can sign forged tokens
11
+ with the public key and your code will happily verify them with
12
+ HMAC-SHA256 treating the PEM string as the shared secret.
13
+
14
+ Always pin the algorithm to the asymmetric one you actually use
15
+ (e.g. `algorithms: ['RS256']`) and pass the public key only when
16
+ verifying asymmetric tokens.
17
+
18
+ RFC 7518 §3.1 — the "alg" header must be matched to the key type.
19
+ # Algorithm confusion = verifying with an HMAC algorithm (HS*) while the key
20
+ # is an asymmetric PUBLIC key — the attacker signs with HS* using the public
21
+ # key as the shared secret. We match either an inline PEM literal OR a key
22
+ # bound to a public-key-named variable (the common real-world shape), in
23
+ # both cases combined with an HS* algorithm.
24
+ pattern-either:
25
+ - patterns:
26
+ - pattern: 'jwt.verify($TOKEN, $KEY, $OPTS)'
27
+ - metavariable-regex:
28
+ metavariable: $KEY
29
+ regex: '-----BEGIN [A-Z ]*PUBLIC KEY-----'
30
+ - metavariable-pattern:
31
+ metavariable: $OPTS
32
+ patterns:
33
+ - pattern-either:
34
+ - pattern: '{..., algorithms: ["HS256", ...], ...}'
35
+ - pattern: '{..., algorithms: ["HS384", ...], ...}'
36
+ - pattern: '{..., algorithms: ["HS512", ...], ...}'
37
+ - patterns:
38
+ - pattern: 'jwt.verify($TOKEN, $KEY)'
39
+ - metavariable-regex:
40
+ metavariable: $KEY
41
+ regex: '-----BEGIN [A-Z ]*PUBLIC KEY-----'
42
+ # Key is a public-key-named variable (RSA_PUBLIC, publicKey, pubKey, …)
43
+ # verified with an HS* algorithm.
44
+ - patterns:
45
+ - pattern: 'jwt.verify($TOKEN, $KEY, $OPTS)'
46
+ - metavariable-regex:
47
+ metavariable: $KEY
48
+ regex: '(?i).*pub(lic)?'
49
+ - metavariable-pattern:
50
+ metavariable: $OPTS
51
+ patterns:
52
+ - pattern-either:
53
+ - pattern: '{..., algorithms: ["HS256", ...], ...}'
54
+ - pattern: '{..., algorithms: ["HS384", ...], ...}'
55
+ - pattern: '{..., algorithms: ["HS512", ...], ...}'
56
+ metadata:
57
+ oauthlint-rule-id: AUTH-JWT-007
58
+ oauthlint-doc-url: https://oauthlint.dev/rules/jwt-algorithm-confusion
59
+ category: security
60
+ cwe: CWE-327
61
+ owasp: API2:2023
62
+ llm-prevalence: MEDIUM
63
+ technology:
64
+ - jsonwebtoken
65
+ references:
66
+ - https://datatracker.ietf.org/doc/html/rfc7518#section-3.1
67
+ - https://owasp.org/www-project-api-security/
@@ -0,0 +1,33 @@
1
+ rules:
2
+ - id: auth.jwt.in-url
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: ERROR
7
+ message: |
8
+ A JWT-looking value (header `eyJ…`) appears in a URL query string
9
+ or fragment. URLs leak into server logs, browser history, the
10
+ `Referer` header (sent to every third-party CDN, ad pixel, and
11
+ analytics script on the destination page), and even copy-paste
12
+ operations. A JWT in a URL is a leaked JWT.
13
+
14
+ Send JWTs in the `Authorization: Bearer …` header, or in a
15
+ `Secure; HttpOnly; SameSite` cookie. Never in the URL.
16
+
17
+ OWASP ASVS V3.2.3 explicitly forbids credentials in URL parameters.
18
+ pattern-either:
19
+ # JWT in a query parameter or fragment.
20
+ - pattern-regex: '[?&][^=]+=eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+'
21
+ - pattern-regex: "#[^=]+=eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+"
22
+ # JWT embedded in a URL path segment (e.g. /verify/eyJ...). The eyJ.x.y
23
+ # shape is JWT-specific, so this stays low-FP.
24
+ - pattern-regex: '/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+'
25
+ metadata:
26
+ oauthlint-rule-id: AUTH-JWT-008
27
+ oauthlint-doc-url: https://oauthlint.dev/rules/jwt-in-url
28
+ category: security
29
+ cwe: CWE-598
30
+ owasp: API1:2023
31
+ llm-prevalence: MEDIUM
32
+ references:
33
+ - https://owasp.org/www-project-application-security-verification-standard/