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,39 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.jwt.localstorage
|
|
3
|
+
languages:
|
|
4
|
+
- javascript
|
|
5
|
+
- typescript
|
|
6
|
+
severity: WARNING
|
|
7
|
+
message: |
|
|
8
|
+
A JWT (or other auth token) is being written to `localStorage`. Any
|
|
9
|
+
XSS that lands on the page can exfiltrate the token via
|
|
10
|
+
`localStorage.getItem(...)` — and unlike `HttpOnly` cookies, there
|
|
11
|
+
is no browser-side mitigation.
|
|
12
|
+
|
|
13
|
+
Store auth tokens in an `HttpOnly; Secure; SameSite=Strict` cookie
|
|
14
|
+
set by the server, or in memory only. `sessionStorage` is no safer
|
|
15
|
+
against XSS — it's the same attacker capability.
|
|
16
|
+
|
|
17
|
+
OWASP ASVS V3.4: tokens must not be stored where untrusted scripts
|
|
18
|
+
can read them.
|
|
19
|
+
# The key must contain a strong token word (`token` catches accessToken,
|
|
20
|
+
# refresh_token, authToken, idToken, sessionToken, …). We deliberately do
|
|
21
|
+
# NOT match the bare words `auth`/`access`/`refresh`/`session` because they
|
|
22
|
+
# appear as substrings of innocuous keys (author_filter, auto_refresh,
|
|
23
|
+
# access_count, sidebar_session) and produced false positives.
|
|
24
|
+
pattern-either:
|
|
25
|
+
- patterns:
|
|
26
|
+
- pattern-regex: |-
|
|
27
|
+
(?:window\.|globalThis\.|self\.)?(?:localStorage|sessionStorage)\.setItem\(\s*['"][^'"]*(?i:token|jwt|bearer|credential|api[_-]?key)[^'"]*['"]
|
|
28
|
+
- patterns:
|
|
29
|
+
- pattern-regex: |-
|
|
30
|
+
(?:window\.|globalThis\.|self\.)?(?:localStorage|sessionStorage)\[\s*['"][^'"]*(?i:token|jwt|bearer|credential|api[_-]?key)[^'"]*['"]\s*\]\s*=
|
|
31
|
+
metadata:
|
|
32
|
+
oauthlint-rule-id: AUTH-JWT-005
|
|
33
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/jwt-localstorage
|
|
34
|
+
category: security
|
|
35
|
+
cwe: CWE-922
|
|
36
|
+
owasp: API8:2023
|
|
37
|
+
llm-prevalence: HIGH
|
|
38
|
+
references:
|
|
39
|
+
- https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.jwt.no-audience
|
|
3
|
+
languages:
|
|
4
|
+
- javascript
|
|
5
|
+
- typescript
|
|
6
|
+
severity: WARNING
|
|
7
|
+
message: |
|
|
8
|
+
JWT is being verified without checking the `aud` (audience) claim.
|
|
9
|
+
A token issued for one of your services (e.g. an internal worker)
|
|
10
|
+
can then be replayed against another service that trusts the same
|
|
11
|
+
key, leading to confused-deputy attacks.
|
|
12
|
+
|
|
13
|
+
Pass `{ audience: 'your-api' }` to `jwt.verify` (or validate the
|
|
14
|
+
`aud` claim manually) on every verification path.
|
|
15
|
+
|
|
16
|
+
RFC 7519 §4.1.3 defines the `aud` claim explicitly for this use case.
|
|
17
|
+
# Scoped to the `jsonwebtoken` library by matching the common alias
|
|
18
|
+
# `jwt` (the literal identifier 99% of users pick). Generic `X.verify(...)`
|
|
19
|
+
# calls on unrelated libs (jose internals, custom signers) no longer
|
|
20
|
+
# fire — this was the dominant FP class in real-world validation.
|
|
21
|
+
pattern-either:
|
|
22
|
+
- patterns:
|
|
23
|
+
- pattern: 'jwt.verify($TOKEN, $SECRET, $OPTS)'
|
|
24
|
+
- metavariable-pattern:
|
|
25
|
+
metavariable: $OPTS
|
|
26
|
+
patterns:
|
|
27
|
+
- pattern-not: '{..., audience: ..., ...}'
|
|
28
|
+
- pattern: '{...}'
|
|
29
|
+
# Callback form: jwt.verify(token, secret, options, cb) without audience.
|
|
30
|
+
- patterns:
|
|
31
|
+
- pattern: 'jwt.verify($TOKEN, $SECRET, $OPTS, $CB)'
|
|
32
|
+
- metavariable-pattern:
|
|
33
|
+
metavariable: $OPTS
|
|
34
|
+
patterns:
|
|
35
|
+
- pattern-not: '{..., audience: ..., ...}'
|
|
36
|
+
- pattern: '{...}'
|
|
37
|
+
- patterns:
|
|
38
|
+
- pattern: 'jwt.verify($TOKEN, $SECRET)'
|
|
39
|
+
metadata:
|
|
40
|
+
oauthlint-rule-id: AUTH-JWT-004
|
|
41
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/jwt-no-audience
|
|
42
|
+
category: security
|
|
43
|
+
cwe: CWE-345
|
|
44
|
+
owasp: API2:2023
|
|
45
|
+
llm-prevalence: MEDIUM
|
|
46
|
+
technology:
|
|
47
|
+
- jsonwebtoken
|
|
48
|
+
references:
|
|
49
|
+
- https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.jwt.no-expiration
|
|
3
|
+
languages:
|
|
4
|
+
- javascript
|
|
5
|
+
- typescript
|
|
6
|
+
severity: WARNING
|
|
7
|
+
message: |
|
|
8
|
+
JWT is signed without any `expiresIn` / `exp` claim, OR a token is
|
|
9
|
+
verified without an `maxAge` check. A stolen token therefore remains
|
|
10
|
+
valid forever.
|
|
11
|
+
|
|
12
|
+
Always set a reasonable expiration on access tokens (5-60 minutes is
|
|
13
|
+
typical) and verify it with `{ maxAge: '15m' }` or by validating the
|
|
14
|
+
`exp` claim explicitly.
|
|
15
|
+
# Scoped to the `jsonwebtoken` library by matching the common alias
|
|
16
|
+
# `jwt`. Generic `X.sign(...)` calls on unrelated libs (jose
|
|
17
|
+
# internals, custom signers, ECDSA primitives) no longer fire —
|
|
18
|
+
# this was the dominant FP class in real-world validation.
|
|
19
|
+
pattern-either:
|
|
20
|
+
- patterns:
|
|
21
|
+
- pattern: 'jwt.sign($PAYLOAD, $SECRET)'
|
|
22
|
+
- pattern-not: 'jwt.sign({..., exp: ..., ...}, $SECRET)'
|
|
23
|
+
- pattern-not: 'jwt.sign({..., expiresIn: ..., ...}, $SECRET)'
|
|
24
|
+
- patterns:
|
|
25
|
+
- pattern: 'jwt.sign($PAYLOAD, $SECRET, $OPTS)'
|
|
26
|
+
# exp can live in the payload too, not only the options object.
|
|
27
|
+
- pattern-not: 'jwt.sign({..., exp: ..., ...}, $SECRET, $OPTS)'
|
|
28
|
+
- pattern-not: 'jwt.sign({..., expiresIn: ..., ...}, $SECRET, $OPTS)'
|
|
29
|
+
- metavariable-pattern:
|
|
30
|
+
metavariable: $OPTS
|
|
31
|
+
patterns:
|
|
32
|
+
- pattern-not: '{..., expiresIn: ..., ...}'
|
|
33
|
+
- pattern-not: '{..., exp: ..., ...}'
|
|
34
|
+
- pattern: '{...}'
|
|
35
|
+
metadata:
|
|
36
|
+
oauthlint-rule-id: AUTH-JWT-003
|
|
37
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/jwt-no-expiration
|
|
38
|
+
category: security
|
|
39
|
+
cwe: CWE-613
|
|
40
|
+
owasp: API2:2023
|
|
41
|
+
llm-prevalence: HIGH
|
|
42
|
+
technology:
|
|
43
|
+
- jsonwebtoken
|
|
44
|
+
references:
|
|
45
|
+
- https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.jwt.no-issuer
|
|
3
|
+
languages:
|
|
4
|
+
- javascript
|
|
5
|
+
- typescript
|
|
6
|
+
severity: INFO
|
|
7
|
+
message: |
|
|
8
|
+
JWT is being verified without checking the `iss` (issuer) claim. If
|
|
9
|
+
your verification key is shared across multiple authorization
|
|
10
|
+
servers (or even tenants on a single IdP), this lets a token signed
|
|
11
|
+
by one issuer be accepted by code that was meant to trust another.
|
|
12
|
+
|
|
13
|
+
Pass `{ issuer: 'https://your-idp.example.com' }` to `jwt.verify` so
|
|
14
|
+
that the trust chain is explicit. RFC 7519 §4.1.1 defines the
|
|
15
|
+
`iss` claim for exactly this purpose.
|
|
16
|
+
# Scoped to `jsonwebtoken` (alias `jwt`) for the same reason as
|
|
17
|
+
# auth.jwt.no-audience: generic $X.verify() patterns produced too
|
|
18
|
+
# many FPs on jose/custom signer code.
|
|
19
|
+
pattern-either:
|
|
20
|
+
- patterns:
|
|
21
|
+
- pattern: 'jwt.verify($TOKEN, $SECRET, $OPTS)'
|
|
22
|
+
- metavariable-pattern:
|
|
23
|
+
metavariable: $OPTS
|
|
24
|
+
patterns:
|
|
25
|
+
- pattern-not: '{..., issuer: ..., ...}'
|
|
26
|
+
- pattern: '{...}'
|
|
27
|
+
# 2-arg verify has no options at all → no issuer check (parity with
|
|
28
|
+
# auth.jwt.no-audience).
|
|
29
|
+
- pattern: 'jwt.verify($TOKEN, $SECRET)'
|
|
30
|
+
metadata:
|
|
31
|
+
oauthlint-rule-id: AUTH-JWT-006
|
|
32
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/jwt-no-issuer
|
|
33
|
+
category: security
|
|
34
|
+
cwe: CWE-345
|
|
35
|
+
owasp: API2:2023
|
|
36
|
+
llm-prevalence: LOW
|
|
37
|
+
technology:
|
|
38
|
+
- jsonwebtoken
|
|
39
|
+
references:
|
|
40
|
+
- https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.jwt.weak-secret
|
|
3
|
+
languages:
|
|
4
|
+
- javascript
|
|
5
|
+
- typescript
|
|
6
|
+
severity: ERROR
|
|
7
|
+
message: |
|
|
8
|
+
JWT signing or verification uses a hard-coded secret. Anyone who reads
|
|
9
|
+
the source (including via leaked GitHub commits) can forge valid tokens.
|
|
10
|
+
|
|
11
|
+
Move the secret to an environment variable or secret manager, and ensure
|
|
12
|
+
the value is at least 256 bits (32 ASCII characters) when using HMAC-based
|
|
13
|
+
algorithms (HS256/HS384/HS512).
|
|
14
|
+
|
|
15
|
+
Real-world incident: 24,008 unique secrets were found in MCP config files
|
|
16
|
+
and 3.2% of Claude Code commits leaked secrets in GitGuardian's 2026 report.
|
|
17
|
+
# Scoped to the `jsonwebtoken` alias `jwt` to avoid matching jose's
|
|
18
|
+
# low-level `sig.sign(key, options)` and similar generic patterns
|
|
19
|
+
# that were the FP class in real-world validation.
|
|
20
|
+
pattern-either:
|
|
21
|
+
- patterns:
|
|
22
|
+
- pattern-either:
|
|
23
|
+
- pattern: 'jwt.sign($PAYLOAD, $SECRET)'
|
|
24
|
+
- pattern: 'jwt.sign($PAYLOAD, $SECRET, ...)'
|
|
25
|
+
- pattern: 'jwt.verify($TOKEN, $SECRET)'
|
|
26
|
+
- pattern: 'jwt.verify($TOKEN, $SECRET, ...)'
|
|
27
|
+
- metavariable-regex:
|
|
28
|
+
metavariable: $SECRET
|
|
29
|
+
regex: ^(["'])(?:[a-zA-Z0-9!._-]{1,20}|secret|password|changeme|jwt[-_]?secret|my[-_]?secret)\1$
|
|
30
|
+
# Destructured import: `import { sign, verify } from 'jsonwebtoken'`.
|
|
31
|
+
# Scoped to the import so bare sign()/verify() from other libraries are
|
|
32
|
+
# not matched.
|
|
33
|
+
- patterns:
|
|
34
|
+
- pattern-inside: |
|
|
35
|
+
import { ... } from 'jsonwebtoken'
|
|
36
|
+
...
|
|
37
|
+
- pattern-either:
|
|
38
|
+
- pattern: 'sign($PAYLOAD, $SECRET)'
|
|
39
|
+
- pattern: 'sign($PAYLOAD, $SECRET, ...)'
|
|
40
|
+
- pattern: 'verify($TOKEN, $SECRET)'
|
|
41
|
+
- pattern: 'verify($TOKEN, $SECRET, ...)'
|
|
42
|
+
- metavariable-regex:
|
|
43
|
+
metavariable: $SECRET
|
|
44
|
+
regex: ^(["'])(?:[a-zA-Z0-9!._-]{1,20}|secret|password|changeme|jwt[-_]?secret|my[-_]?secret)\1$
|
|
45
|
+
metadata:
|
|
46
|
+
oauthlint-rule-id: AUTH-JWT-002
|
|
47
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/jwt-weak-secret
|
|
48
|
+
category: security
|
|
49
|
+
cwe: CWE-798
|
|
50
|
+
owasp: API2:2023
|
|
51
|
+
llm-prevalence: HIGH
|
|
52
|
+
technology:
|
|
53
|
+
- jsonwebtoken
|
|
54
|
+
references:
|
|
55
|
+
- https://datatracker.ietf.org/doc/html/rfc7518#section-3.2
|
|
56
|
+
- https://cwe.mitre.org/data/definitions/798.html
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.oauth.broad-scope
|
|
3
|
+
languages:
|
|
4
|
+
- javascript
|
|
5
|
+
- typescript
|
|
6
|
+
severity: INFO
|
|
7
|
+
message: |
|
|
8
|
+
OAuth scope request includes an over-broad scope such as `admin`,
|
|
9
|
+
`full_access`, `*`, or `repo` (entire GitHub access). LLMs default
|
|
10
|
+
to the widest scope that "works", but every extra scope expands
|
|
11
|
+
the blast radius if the access token is leaked or replayed.
|
|
12
|
+
|
|
13
|
+
Request the narrowest scope that satisfies your feature. Examples:
|
|
14
|
+
use `repo:status` instead of `repo`, `gmail.send` instead of
|
|
15
|
+
`https://mail.google.com/`, and scope down to `read:user` when you
|
|
16
|
+
only need a profile.
|
|
17
|
+
# An over-broad scope must be a STANDALONE token (bounded by the quote, a
|
|
18
|
+
# space, or a comma — OAuth scopes are space-delimited). Structured
|
|
19
|
+
# sub-scopes such as `admin:read`, `repo:status`, `public_repo`,
|
|
20
|
+
# `channels:admin`, `calendar.all` are narrower and intentionally NOT
|
|
21
|
+
# flagged (they previously matched via `\b` and produced false positives).
|
|
22
|
+
pattern-either:
|
|
23
|
+
- pattern-regex: |-
|
|
24
|
+
scope\s*[:=]\s*(['"])(?:[^'"]*[ ,])?(?:admin|full_access|all|root|superuser|repo)(?:[ ,][^'"]*)?\1
|
|
25
|
+
- pattern-regex: |-
|
|
26
|
+
scope\s*[:=]\s*['"][^'"]*\*[^'"]*['"]
|
|
27
|
+
- pattern-regex: |-
|
|
28
|
+
scope\s*[:=]\s*['"][^'"]*https://mail\.google\.com/[^'"]*['"]
|
|
29
|
+
- pattern-regex: |-
|
|
30
|
+
scopes?\s*[:=]\s*\[[^\]]*(['"])(?:[^'"]*[ ,])?(?:admin|full_access|all|root|superuser|repo)(?:[ ,][^'"]*)?\1[^\]]*\]
|
|
31
|
+
metadata:
|
|
32
|
+
oauthlint-rule-id: AUTH-OAUTH-006
|
|
33
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/oauth-broad-scope
|
|
34
|
+
category: security
|
|
35
|
+
cwe: CWE-272
|
|
36
|
+
owasp: API1:2023
|
|
37
|
+
llm-prevalence: HIGH
|
|
38
|
+
references:
|
|
39
|
+
- https://datatracker.ietf.org/doc/html/rfc6749#section-3.3
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.oauth.hardcoded-secret
|
|
3
|
+
languages:
|
|
4
|
+
- javascript
|
|
5
|
+
- typescript
|
|
6
|
+
severity: ERROR
|
|
7
|
+
message: |
|
|
8
|
+
An OAuth `client_secret` (or similarly sensitive credential) is being
|
|
9
|
+
assigned a hard-coded string literal. The moment this lands in git, it
|
|
10
|
+
is one search away from compromise.
|
|
11
|
+
|
|
12
|
+
Replace the literal with `process.env.OAUTH_CLIENT_SECRET` (or your
|
|
13
|
+
secret manager equivalent) and add the variable to `.env.example` with
|
|
14
|
+
a placeholder so contributors know it is required.
|
|
15
|
+
|
|
16
|
+
GitGuardian 2026 found 28.6M public secrets on GitHub, with Claude Code
|
|
17
|
+
commits leaking at 2x baseline. This is the most common AI-coding leak.
|
|
18
|
+
# Key matching is case-insensitive (client_secret / clientSecret /
|
|
19
|
+
# CLIENT_SECRET). The value must be a quoted ≥8-char literal, so `process.env`
|
|
20
|
+
# references never match. Env-var template strings, <placeholders>, and
|
|
21
|
+
# obvious doc/test placeholders are excluded to avoid false positives.
|
|
22
|
+
pattern-either:
|
|
23
|
+
- patterns:
|
|
24
|
+
- pattern-regex: |-
|
|
25
|
+
(?i)client[_-]?secret\s*[:=]\s*['"][^'"\s]{8,}['"]
|
|
26
|
+
- pattern-not-regex: |-
|
|
27
|
+
(?i)client[_-]?secret\s*[:=]\s*['"]\$\{?[A-Za-z_]+\}?['"]
|
|
28
|
+
- pattern-not-regex: |-
|
|
29
|
+
(?i)client[_-]?secret\s*[:=]\s*['"]<[^'"]*>['"]
|
|
30
|
+
- pattern-not-regex: |-
|
|
31
|
+
(?i)client[_-]?secret\s*[:=]\s*['"](?:your[-_]|my[-_]|example|placeholder|xxx+|todo|fixme|test|dummy|fake|sample|changeme|redacted)
|
|
32
|
+
metadata:
|
|
33
|
+
oauthlint-rule-id: AUTH-OAUTH-003
|
|
34
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/oauth-hardcoded-secret
|
|
35
|
+
category: security
|
|
36
|
+
cwe: CWE-798
|
|
37
|
+
owasp: API8:2023
|
|
38
|
+
llm-prevalence: HIGH
|
|
39
|
+
references:
|
|
40
|
+
- https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1
|
|
41
|
+
- https://blog.gitguardian.com/the-state-of-secrets-sprawl-2026/
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.oauth.implicit-flow
|
|
3
|
+
languages:
|
|
4
|
+
- javascript
|
|
5
|
+
- typescript
|
|
6
|
+
severity: ERROR
|
|
7
|
+
message: |
|
|
8
|
+
OAuth implicit flow (`response_type=token` or `response_type=id_token
|
|
9
|
+
token`) is deprecated by OAuth 2.0 Security BCP (RFC 9700) and the
|
|
10
|
+
OAuth 2.1 working draft. The access token leaks into the URL
|
|
11
|
+
fragment, browser history, and referrer headers, and there is no
|
|
12
|
+
refresh-token mechanism.
|
|
13
|
+
|
|
14
|
+
Migrate to authorization code + PKCE (`response_type=code` with a
|
|
15
|
+
`code_challenge`). All modern OAuth providers (Google, Microsoft,
|
|
16
|
+
Auth0, Okta, Keycloak, WSO2) support this for SPAs and native apps.
|
|
17
|
+
pattern-either:
|
|
18
|
+
- pattern-regex: |-
|
|
19
|
+
response_type\s*[:=]\s*['"]token['"]
|
|
20
|
+
- pattern-regex: |-
|
|
21
|
+
response_type\s*[:=]\s*['"]id_token\s+token['"]
|
|
22
|
+
- pattern-regex: |-
|
|
23
|
+
response_type\s*[:=]\s*['"]token\s+id_token['"]
|
|
24
|
+
- pattern-regex: |-
|
|
25
|
+
[?&]response_type=token(?:&|['"]|#|$)
|
|
26
|
+
- pattern-regex: |-
|
|
27
|
+
[?&]response_type=(?:id_token(?:%20|\+)token|token(?:%20|\+)id_token)(?:&|['"]|#|$)
|
|
28
|
+
metadata:
|
|
29
|
+
oauthlint-rule-id: AUTH-OAUTH-005
|
|
30
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/oauth-implicit-flow
|
|
31
|
+
category: security
|
|
32
|
+
cwe: CWE-1004
|
|
33
|
+
owasp: API1:2023
|
|
34
|
+
llm-prevalence: MEDIUM
|
|
35
|
+
references:
|
|
36
|
+
- https://datatracker.ietf.org/doc/html/rfc9700
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.oauth.long-token-lifetime
|
|
3
|
+
languages:
|
|
4
|
+
- javascript
|
|
5
|
+
- typescript
|
|
6
|
+
severity: WARNING
|
|
7
|
+
message: |
|
|
8
|
+
An OAuth `expires_in` (or comparable token-lifetime field) is being
|
|
9
|
+
set to a literal value greater than 86_400 seconds — i.e. longer
|
|
10
|
+
than 24 hours. Long-lived access tokens make every token theft
|
|
11
|
+
catastrophic because they remain valid for days or weeks; the
|
|
12
|
+
industry standard is 15-60 minutes for access tokens, paired with
|
|
13
|
+
a refresh-token rotation flow for longer sessions.
|
|
14
|
+
|
|
15
|
+
Don't issue access tokens longer than a day. Use refresh tokens
|
|
16
|
+
with proper rotation (RFC 6749 §6, RFC 9700 §4.14) for "stay
|
|
17
|
+
logged in" semantics.
|
|
18
|
+
pattern-either:
|
|
19
|
+
# Numeric literal as an object property.
|
|
20
|
+
- patterns:
|
|
21
|
+
- pattern-either:
|
|
22
|
+
- pattern: '{ ..., expires_in: $TTL, ... }'
|
|
23
|
+
- pattern: '{ ..., expiresIn: $TTL, ... }'
|
|
24
|
+
- pattern: '{ ..., access_token_ttl: $TTL, ... }'
|
|
25
|
+
- pattern: '{ ..., accessTokenTtl: $TTL, ... }'
|
|
26
|
+
- pattern: '{ ..., tokenLifetime: $TTL, ... }'
|
|
27
|
+
- metavariable-comparison:
|
|
28
|
+
metavariable: $TTL
|
|
29
|
+
comparison: $TTL > 86400
|
|
30
|
+
# Numeric literal via member assignment (config.expires_in = N).
|
|
31
|
+
- patterns:
|
|
32
|
+
- pattern-either:
|
|
33
|
+
- pattern: '$X.expires_in = $TTL'
|
|
34
|
+
- pattern: '$X.expiresIn = $TTL'
|
|
35
|
+
- pattern: '$X.access_token_ttl = $TTL'
|
|
36
|
+
- pattern: '$X.accessTokenTtl = $TTL'
|
|
37
|
+
- pattern: '$X.tokenLifetime = $TTL'
|
|
38
|
+
- metavariable-comparison:
|
|
39
|
+
metavariable: $TTL
|
|
40
|
+
comparison: $TTL > 86400
|
|
41
|
+
# jsonwebtoken / `ms`-style string durations longer than a day: >=2 days,
|
|
42
|
+
# or any weeks / months / years. ('1d', '15m', '12h' are <= 24h and are
|
|
43
|
+
# intentionally not matched.)
|
|
44
|
+
- pattern-regex: |-
|
|
45
|
+
(?:expiresIn|expires_in)\s*[:=]\s*['"](?:[2-9]|[1-9][0-9]+)\s*(?:d|days?)['"]
|
|
46
|
+
- pattern-regex: |-
|
|
47
|
+
(?:expiresIn|expires_in)\s*[:=]\s*['"][1-9][0-9]*\s*(?:w|y|weeks?|years?|months?)['"]
|
|
48
|
+
metadata:
|
|
49
|
+
oauthlint-rule-id: AUTH-OAUTH-009
|
|
50
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/oauth-long-token-lifetime
|
|
51
|
+
category: security
|
|
52
|
+
cwe: CWE-613
|
|
53
|
+
owasp: API2:2023
|
|
54
|
+
llm-prevalence: MEDIUM
|
|
55
|
+
references:
|
|
56
|
+
- https://datatracker.ietf.org/doc/html/rfc9700#section-4.14
|
|
57
|
+
- https://datatracker.ietf.org/doc/html/rfc6749#section-6
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.oauth.no-pkce
|
|
3
|
+
languages:
|
|
4
|
+
- javascript
|
|
5
|
+
- typescript
|
|
6
|
+
severity: WARNING
|
|
7
|
+
message: |
|
|
8
|
+
OAuth 2.0 authorization request looks like a public client (SPA,
|
|
9
|
+
mobile, or native app) but does not include a `code_challenge`
|
|
10
|
+
parameter. Without PKCE, the authorization code can be intercepted
|
|
11
|
+
and exchanged by an attacker.
|
|
12
|
+
|
|
13
|
+
RFC 8252 §6 mandates PKCE for native/SPA clients. RFC 9700 (OAuth 2.0
|
|
14
|
+
Security BCP) recommends PKCE for ALL clients, including confidential
|
|
15
|
+
ones, as defence in depth.
|
|
16
|
+
|
|
17
|
+
Generate a `code_verifier` (43-128 char), derive `code_challenge =
|
|
18
|
+
BASE64URL-NoPad(SHA256(code_verifier))`, send it with
|
|
19
|
+
`code_challenge_method=S256` on the authorize call, and POST the
|
|
20
|
+
`code_verifier` on the token call.
|
|
21
|
+
pattern-either:
|
|
22
|
+
# Inline authorize URL — any provider's authorize endpoint carrying an
|
|
23
|
+
# authorization-code request (client_id + response_type=code) with no
|
|
24
|
+
# code_challenge. The path is not hardcoded (Google uses /o/oauth2/v2/auth,
|
|
25
|
+
# Auth0/Okta /authorize, GitHub /oauth/authorize, …).
|
|
26
|
+
- patterns:
|
|
27
|
+
- pattern-regex: |-
|
|
28
|
+
['"]https?://[^'"\s]+\?[^'"]*response_type=code[^'"]*['"]
|
|
29
|
+
- pattern-regex: |-
|
|
30
|
+
client_id=
|
|
31
|
+
- pattern-not-regex: |-
|
|
32
|
+
code_challenge
|
|
33
|
+
# Programmatic URLSearchParams build (AST object match — robust to key
|
|
34
|
+
# order and object length, unlike a char-window regex).
|
|
35
|
+
- patterns:
|
|
36
|
+
- pattern: 'new URLSearchParams({..., client_id: $C, ...})'
|
|
37
|
+
- pattern-either:
|
|
38
|
+
- pattern: "new URLSearchParams({..., response_type: 'code', ...})"
|
|
39
|
+
- pattern: 'new URLSearchParams({..., response_type: "code", ...})'
|
|
40
|
+
- pattern-not: 'new URLSearchParams({..., code_challenge: $X, ...})'
|
|
41
|
+
metadata:
|
|
42
|
+
oauthlint-rule-id: AUTH-OAUTH-004
|
|
43
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/oauth-no-pkce
|
|
44
|
+
category: security
|
|
45
|
+
cwe: CWE-345
|
|
46
|
+
owasp: API1:2023
|
|
47
|
+
llm-prevalence: HIGH
|
|
48
|
+
references:
|
|
49
|
+
- https://datatracker.ietf.org/doc/html/rfc7636
|
|
50
|
+
- https://datatracker.ietf.org/doc/html/rfc8252#section-6
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.oauth.no-state-validation
|
|
3
|
+
languages:
|
|
4
|
+
- javascript
|
|
5
|
+
- typescript
|
|
6
|
+
severity: WARNING
|
|
7
|
+
message: |
|
|
8
|
+
OAuth callback handler reads `state` from the request but never
|
|
9
|
+
compares it to a stored value. Sending `state` on the authorize
|
|
10
|
+
call is half of the CSRF mitigation; verifying it on the callback
|
|
11
|
+
is the other half.
|
|
12
|
+
|
|
13
|
+
Compare the received `state` to the value you stored before
|
|
14
|
+
redirecting (session, signed cookie, or Redis). Reject the callback
|
|
15
|
+
if it's missing or doesn't match.
|
|
16
|
+
# Suppress when the received `state` is referenced inside ANY `if`
|
|
17
|
+
# condition — that's the validation. The deep-expression operator
|
|
18
|
+
# `<... state ...>` makes this order-agnostic and helper-aware, so
|
|
19
|
+
# `if (req.query.state !== stored)`, `if (stored === req.query.state)`, and
|
|
20
|
+
# `if (!verifyState(req.query.state))` are all treated as safe (the previous
|
|
21
|
+
# exact-shape patterns only matched one operand order → false positives).
|
|
22
|
+
pattern-either:
|
|
23
|
+
- patterns:
|
|
24
|
+
- pattern-either:
|
|
25
|
+
- pattern: '$REQ.query.state'
|
|
26
|
+
- pattern: '$REQ.body.state'
|
|
27
|
+
- pattern: '$URL.searchParams.get("state")'
|
|
28
|
+
- pattern-not-inside: |
|
|
29
|
+
if (<... $REQ.query.state ...>) { ... }
|
|
30
|
+
- pattern-not-inside: |
|
|
31
|
+
if (<... $REQ.body.state ...>) { ... }
|
|
32
|
+
- pattern-not-inside: |
|
|
33
|
+
if (<... $URL.searchParams.get("state") ...>) { ... }
|
|
34
|
+
metadata:
|
|
35
|
+
oauthlint-rule-id: AUTH-OAUTH-007
|
|
36
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/oauth-no-state-validation
|
|
37
|
+
category: security
|
|
38
|
+
cwe: CWE-352
|
|
39
|
+
owasp: API1:2023
|
|
40
|
+
llm-prevalence: HIGH
|
|
41
|
+
references:
|
|
42
|
+
- https://datatracker.ietf.org/doc/html/rfc6749#section-10.12
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.oauth.no-state
|
|
3
|
+
languages:
|
|
4
|
+
- javascript
|
|
5
|
+
- typescript
|
|
6
|
+
severity: ERROR
|
|
7
|
+
message: |
|
|
8
|
+
OAuth 2.0 authorization request is being built WITHOUT a `state`
|
|
9
|
+
parameter. This opens you to CSRF attacks during the OAuth dance —
|
|
10
|
+
an attacker can trick the victim into logging in as the attacker.
|
|
11
|
+
|
|
12
|
+
Always generate a cryptographically random `state`, store it in a
|
|
13
|
+
session/cookie, and validate it on the callback. PKCE alone is not a
|
|
14
|
+
substitute for `state` when handling browser sessions.
|
|
15
|
+
pattern-either:
|
|
16
|
+
# Inline authorize URL carrying both client_id and response_type but no
|
|
17
|
+
# state. The path is not hardcoded (Google uses /o/oauth2/v2/auth, etc.).
|
|
18
|
+
- patterns:
|
|
19
|
+
- pattern-regex: |-
|
|
20
|
+
['"]https?://[^'"\s]+\?[^'"]*client_id=[^'"]*['"]
|
|
21
|
+
- pattern-regex: |-
|
|
22
|
+
response_type=
|
|
23
|
+
- pattern-not-regex: |-
|
|
24
|
+
state=
|
|
25
|
+
# Programmatic URLSearchParams build (AST object match — robust to key
|
|
26
|
+
# order and length). Fires when client_id + response_type are present but
|
|
27
|
+
# state is not (covers both `state: x` and the `state` shorthand).
|
|
28
|
+
- patterns:
|
|
29
|
+
- pattern: 'new URLSearchParams({..., client_id: $C, ...})'
|
|
30
|
+
- pattern: 'new URLSearchParams({..., response_type: $R, ...})'
|
|
31
|
+
- pattern-not: 'new URLSearchParams({..., state: $S, ...})'
|
|
32
|
+
- pattern-not: 'new URLSearchParams({..., state, ...})'
|
|
33
|
+
metadata:
|
|
34
|
+
oauthlint-rule-id: AUTH-OAUTH-001
|
|
35
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/oauth-no-state
|
|
36
|
+
category: security
|
|
37
|
+
cwe: CWE-352
|
|
38
|
+
owasp: API1:2023
|
|
39
|
+
llm-prevalence: HIGH
|
|
40
|
+
references:
|
|
41
|
+
- https://datatracker.ietf.org/doc/html/rfc6749#section-10.12
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.oauth.open-redirect-callback
|
|
3
|
+
languages:
|
|
4
|
+
- javascript
|
|
5
|
+
- typescript
|
|
6
|
+
severity: ERROR
|
|
7
|
+
message: |
|
|
8
|
+
The OAuth callback handler redirects to a URL taken straight from
|
|
9
|
+
the request without validating it. An attacker can craft a phishing
|
|
10
|
+
link to your real callback that forwards the victim to a malicious
|
|
11
|
+
site under your domain's trust.
|
|
12
|
+
|
|
13
|
+
Maintain an explicit allow-list of post-login redirect destinations
|
|
14
|
+
(route names or full URLs you control). Never forward to an
|
|
15
|
+
arbitrary `req.query.redirect_to`, `req.query.next`, or
|
|
16
|
+
`req.query.return_url` value.
|
|
17
|
+
# Taint mode so indirection (const next = req.query.next; res.redirect(next))
|
|
18
|
+
# and defaults/casts are caught, not just the direct form. Validating the
|
|
19
|
+
# value against an allow-list (Set.has / Array.includes) clears the taint.
|
|
20
|
+
mode: taint
|
|
21
|
+
pattern-sources:
|
|
22
|
+
- pattern: '$REQ.query.$A'
|
|
23
|
+
- pattern: '$REQ.body.$A'
|
|
24
|
+
- pattern: '$REQ.params.$A'
|
|
25
|
+
- pattern: '$REQ.query[$K]'
|
|
26
|
+
- pattern: '$REQ.body[$K]'
|
|
27
|
+
pattern-sanitizers:
|
|
28
|
+
- pattern: '$SET.has(...)'
|
|
29
|
+
- pattern: '$ARR.includes(...)'
|
|
30
|
+
- pattern: '$ARR.indexOf(...)'
|
|
31
|
+
pattern-sinks:
|
|
32
|
+
- patterns:
|
|
33
|
+
- pattern-either:
|
|
34
|
+
- pattern: '$RES.redirect($SINK)'
|
|
35
|
+
- pattern: '$RES.redirect($CODE, $SINK)'
|
|
36
|
+
- focus-metavariable: $SINK
|
|
37
|
+
metadata:
|
|
38
|
+
oauthlint-rule-id: AUTH-OAUTH-008
|
|
39
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/oauth-open-redirect-callback
|
|
40
|
+
category: security
|
|
41
|
+
cwe: CWE-601
|
|
42
|
+
owasp: API1:2023
|
|
43
|
+
llm-prevalence: HIGH
|
|
44
|
+
technology:
|
|
45
|
+
- express
|
|
46
|
+
references:
|
|
47
|
+
- https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.oauth.wildcard-redirect
|
|
3
|
+
languages:
|
|
4
|
+
- javascript
|
|
5
|
+
- typescript
|
|
6
|
+
severity: ERROR
|
|
7
|
+
message: |
|
|
8
|
+
OAuth `redirect_uri` allow-list contains a wildcard, an `http://` URL,
|
|
9
|
+
or `localhost`. Wildcards (and HTTP) let an attacker register their own
|
|
10
|
+
callback URL and harvest authorization codes; `localhost` whitelisting
|
|
11
|
+
is acceptable for dev tooling but disastrous in production.
|
|
12
|
+
|
|
13
|
+
Pin redirect URIs to exact, HTTPS URLs of subdomains you control. RFC 6749
|
|
14
|
+
§10.6 explicitly requires "exact match" or restricted matching.
|
|
15
|
+
pattern-either:
|
|
16
|
+
# Array allow-list containing a wildcard.
|
|
17
|
+
- patterns:
|
|
18
|
+
- pattern-regex: |-
|
|
19
|
+
redirect_uris?\s*[:=]\s*\[[^\]]*['"][^'"]*\*[^'"]*['"]
|
|
20
|
+
# Array allow-list containing an http:// URL (loopback dev URLs allowed).
|
|
21
|
+
- patterns:
|
|
22
|
+
- pattern-regex: |-
|
|
23
|
+
redirect_uris?\s*[:=]\s*\[[^\]]*['"]http://[^'"]*['"]
|
|
24
|
+
- pattern-not-regex: |-
|
|
25
|
+
http://(?:localhost|127\.0\.0\.1|\[::1\])
|
|
26
|
+
# Scalar redirect_uri with a wildcard.
|
|
27
|
+
- patterns:
|
|
28
|
+
- pattern-regex: |-
|
|
29
|
+
redirect_uri\s*[:=]\s*['"][^'"]*\*[^'"]*['"]
|
|
30
|
+
# Scalar redirect_uri with an http:// URL (loopback dev URLs allowed).
|
|
31
|
+
- patterns:
|
|
32
|
+
- pattern-regex: |-
|
|
33
|
+
redirect_uri\s*[:=]\s*['"]http://[^'"]*['"]
|
|
34
|
+
- pattern-not-regex: |-
|
|
35
|
+
http://(?:localhost|127\.0\.0\.1|\[::1\])
|
|
36
|
+
metadata:
|
|
37
|
+
oauthlint-rule-id: AUTH-OAUTH-002
|
|
38
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/oauth-wildcard-redirect
|
|
39
|
+
category: security
|
|
40
|
+
cwe: CWE-601
|
|
41
|
+
owasp: API1:2023
|
|
42
|
+
llm-prevalence: MEDIUM
|
|
43
|
+
references:
|
|
44
|
+
- https://datatracker.ietf.org/doc/html/rfc6749#section-10.6
|
|
45
|
+
- https://datatracker.ietf.org/doc/html/rfc8252#section-7.3
|