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.
- package/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +35 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +72 -0
- package/dist/loader.js.map +1 -0
- package/dist/manifest.d.ts +10 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +10 -0
- package/dist/manifest.js.map +1 -0
- package/dist/schema.d.ts +425 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +71 -0
- package/dist/schema.js.map +1 -0
- package/package.json +61 -0
- package/rules/cookie/long-lived.yml +42 -0
- package/rules/cookie/no-httponly.yml +54 -0
- package/rules/cookie/no-samesite.yml +56 -0
- package/rules/cookie/no-secure.yml +58 -0
- package/rules/cors/wildcard-with-credentials.yml +56 -0
- package/rules/flow/insecure-random.yml +56 -0
- package/rules/flow/no-rate-limit.yml +39 -0
- package/rules/flow/password-min-length.yml +44 -0
- package/rules/flow/password-plaintext.yml +82 -0
- package/rules/flow/timing-unsafe-compare.yml +103 -0
- package/rules/jwt/alg-none.yml +46 -0
- package/rules/jwt/algorithm-confusion.yml +67 -0
- package/rules/jwt/in-url.yml +33 -0
- package/rules/jwt/localstorage.yml +39 -0
- package/rules/jwt/no-audience.yml +49 -0
- package/rules/jwt/no-expiration.yml +45 -0
- package/rules/jwt/no-issuer.yml +40 -0
- package/rules/jwt/weak-secret.yml +56 -0
- package/rules/oauth/broad-scope.yml +39 -0
- package/rules/oauth/hardcoded-secret.yml +41 -0
- package/rules/oauth/implicit-flow.yml +36 -0
- package/rules/oauth/long-token-lifetime.yml +57 -0
- package/rules/oauth/no-pkce.yml +50 -0
- package/rules/oauth/no-state-validation.yml +42 -0
- package/rules/oauth/no-state.yml +41 -0
- package/rules/oauth/open-redirect-callback.yml +47 -0
- package/rules/oauth/wildcard-redirect.yml +45 -0
- package/rules/secret/provider-key.yml +44 -0
- package/rules/session/id-in-url.yml +30 -0
- 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/
|