oauthlint-rules 0.4.0 → 0.5.1

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 (43) hide show
  1. package/package.json +1 -1
  2. package/rules/cookie/long-lived.yml +2 -2
  3. package/rules/express/cookie-insecure.yml +44 -0
  4. package/rules/flow/insecure-random.yml +5 -5
  5. package/rules/flow/oauth-credential-in-log.yml +9 -8
  6. package/rules/flow/secret-in-log.yml +7 -6
  7. package/rules/go/flow/oauth-credential-in-log.yml +9 -8
  8. package/rules/go/flow/weak-rand.yml +3 -3
  9. package/rules/go/jwt/skip-claims-validation.yml +6 -6
  10. package/rules/go/jwt/unchecked-method.yml +6 -6
  11. package/rules/go/tls/insecure-skip-verify.yml +6 -6
  12. package/rules/java/crypto/noop-password-encoder.yml +38 -0
  13. package/rules/java/web/security-ignoring-all.yml +38 -0
  14. package/rules/java/web/wildcard-permit-all.yml +37 -0
  15. package/rules/jwt/ignore-expiration.yml +4 -4
  16. package/rules/nextauth/hardcoded-secret.yml +62 -0
  17. package/rules/oauth/implicit-flow.yml +3 -3
  18. package/rules/oauth/long-token-lifetime.yml +3 -3
  19. package/rules/oauth/no-pkce.yml +4 -4
  20. package/rules/oauth/token-in-localstorage.yml +6 -7
  21. package/rules/passport/jwt-ignore-expiration.yml +58 -0
  22. package/rules/py/cors/allow-all.yml +8 -7
  23. package/rules/py/cors/fastapi-wildcard-credentials.yml +49 -0
  24. package/rules/py/crypto/passlib-weak-scheme.yml +40 -0
  25. package/rules/py/django/cors-allow-all.yml +34 -0
  26. package/rules/py/drf/default-authentication-empty.yml +32 -0
  27. package/rules/py/drf/default-permission-allowany.yml +35 -0
  28. package/rules/py/drf/view-authentication-disabled.yml +34 -0
  29. package/rules/py/flask/session-cookie-insecure.yml +42 -0
  30. package/rules/py/flow/insecure-random-token.yml +6 -5
  31. package/rules/py/flow/oauth-credential-in-log.yml +9 -8
  32. package/rules/py/flow/requests-verify-disabled.yml +29 -17
  33. package/rules/py/jwt/algorithm-confusion.yml +9 -8
  34. package/rules/py/jwt/verify-claims-disabled.yml +45 -0
  35. package/rules/py/oauth/insecure-transport-env.yml +3 -3
  36. package/rules/py/oauth/token-request-verify-disabled.yml +14 -4
  37. package/rules/rust/crypto/weak-password-hash.yml +6 -5
  38. package/rules/rust/jwt/algorithm-confusion.yml +8 -7
  39. package/rules/rust/jwt/hardcoded-secret.yml +5 -4
  40. package/rules/rust/jwt/no-issuer-validation.yml +7 -7
  41. package/rules/session/hardcoded-secret.yml +3 -3
  42. package/rules/session/no-regeneration.yml +6 -5
  43. package/rules/tls/reject-unauthorized.yml +8 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oauthlint-rules",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Semgrep rules catching the OAuth/OIDC/JWT anti-patterns that AI coding tools systematically produce.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -6,8 +6,8 @@ rules:
6
6
  severity: INFO
7
7
  message: |
8
8
  An auth-looking cookie is being set with a `maxAge` greater than
9
- 30 days (the threshold is 30 × 24 × 60 × 60 × 1000 = 2_592_000_000
10
- milliseconds). Long-lived session cookies expand the blast radius
9
+ 30 days. The threshold is 30 × 24 × 60 × 60 × 1000 = 2_592_000_000
10
+ milliseconds. Long-lived session cookies expand the blast radius
11
11
  of any single token theft and bypass server-side revocation if
12
12
  the application doesn't validate freshness on every request.
13
13
 
@@ -0,0 +1,44 @@
1
+ rules:
2
+ - id: auth.express.cookie-insecure
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: WARNING
7
+ message: |
8
+ An Express session cookie is explicitly configured as insecure. Setting
9
+ `secure: false` lets the browser send the session cookie over plain HTTP,
10
+ and `httpOnly: false` exposes it to `document.cookie` (any XSS reads it).
11
+
12
+ Either drop the flag (the framework default may still need hardening) or
13
+ set `secure: true` and `httpOnly: true` for production. If you need
14
+ insecure cookies in dev, gate the value on `NODE_ENV !== 'production'`
15
+ rather than hard-coding `false`.
16
+ # Targets the session-middleware config object only, never `res.cookie(...)`
17
+ # (that shape is covered by the auth.cookie.* rules). We match ONLY an
18
+ # explicit `secure: false` / `httpOnly: false`; a MISSING key is left alone
19
+ # because express-session already defaults `secure` to false and dev setups
20
+ # legitimately omit it, so a missing key is undecidable prod-vs-dev and would
21
+ # be false-positive prone.
22
+ pattern-either:
23
+ # express-session: the flags live on a nested `cookie:` object.
24
+ - pattern: 'session({..., cookie: {..., secure: false, ...}, ...})'
25
+ - pattern: 'session({..., cookie: {..., httpOnly: false, ...}, ...})'
26
+ - pattern: 'expressSession({..., cookie: {..., secure: false, ...}, ...})'
27
+ - pattern: 'expressSession({..., cookie: {..., httpOnly: false, ...}, ...})'
28
+ # cookie-session: the flags live directly on the options object.
29
+ - pattern: 'cookieSession({..., secure: false, ...})'
30
+ - pattern: 'cookieSession({..., httpOnly: false, ...})'
31
+ metadata:
32
+ oauthlint-rule-id: AUTH-EXPRESS-001
33
+ oauthlint-doc-url: https://oauthlint.dev/rules/express-cookie-insecure
34
+ category: security
35
+ cwe: CWE-614
36
+ owasp: A05:2021
37
+ llm-prevalence: MEDIUM
38
+ technology:
39
+ - express-session
40
+ - cookie-session
41
+ references:
42
+ - https://github.com/expressjs/session#cookiesecure
43
+ - https://github.com/expressjs/cookie-session#options
44
+ - https://cwe.mitre.org/data/definitions/614.html
@@ -5,11 +5,11 @@ rules:
5
5
  - typescript
6
6
  severity: ERROR
7
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
8
+ `Math.random()` produces a value whose name marks it as
9
+ security-sensitive. The value is assigned to (or computed for) a token,
10
+ a CSRF value, an OAuth `state`, a session id, a nonce, or a
11
+ verification code. `Math.random()` is NOT cryptographically secure and
12
+ is predictable enough for an attacker with enough samples to recover
13
13
  the seed.
14
14
 
15
15
  Use `crypto.randomBytes(N)` (Node) or `crypto.getRandomValues()`
@@ -5,14 +5,15 @@ rules:
5
5
  - typescript
6
6
  severity: ERROR
7
7
  message: |
8
- An OAuth/OIDC credential taken from the request an authorization
9
- `code`, an `access_token` / `refresh_token` / `id_token`, a bearer
10
- `token`, a `client_secret`, or the raw `Authorization` header flows
11
- into a logging call (`console.*` or `logger.*`). Logs are written to
12
- files, shipped to aggregators (Datadog, Splunk, CloudWatch) and read by
13
- people and systems that should never see live credentials. A leaked
14
- authorization code or token can be replayed to impersonate the user or
15
- complete the OAuth exchange (CWE-532).
8
+ An OAuth/OIDC credential from the request flows into a logging call.
9
+ The tainted value is an authorization `code`, an `access_token` /
10
+ `refresh_token` / `id_token`, a bearer `token`, a `client_secret`, or
11
+ the raw `Authorization` header, and the sink is a `console.*` or
12
+ `logger.*` call. Logs are written to files, shipped to aggregators
13
+ (Datadog, Splunk, CloudWatch) and read by people and systems that should
14
+ never see live credentials. A leaked authorization code or token can be
15
+ replayed to impersonate the user or complete the OAuth exchange
16
+ (CWE-532).
16
17
 
17
18
  Never log the raw credential. Redact or mask it before logging
18
19
  (`token.slice(0, 4) + '…'`), log a non-sensitive identifier instead
@@ -5,12 +5,13 @@ rules:
5
5
  - typescript
6
6
  severity: WARNING
7
7
  message: |
8
- A secret-shaped value (`password`, `token`, `secret`, `apiKey`,
9
- `accessToken`, `refreshToken`, `privateKey`, `clientSecret`, …) is
10
- being passed to a logging call (`console.*` or `logger.*`). Logs are
11
- routinely written to files, shipped to aggregators (Datadog, Splunk,
12
- CloudWatch) and read by people who should never see the raw secret —
13
- this is a textbook credential leak.
8
+ A secret-shaped value is passed to a logging call. The logged
9
+ identifier is named like a credential (`password`, `token`, `secret`,
10
+ `apiKey`, `accessToken`, `refreshToken`, `privateKey`, `clientSecret`,
11
+ …) and the sink is a `console.*` or `logger.*` call. Logs are routinely
12
+ written to files, shipped to aggregators (Datadog, Splunk, CloudWatch)
13
+ and read by people who should never see the raw secret. This is a
14
+ textbook credential leak.
14
15
 
15
16
  Never log secrets. Redact or mask them before logging
16
17
  (`token.slice(0, 4) + '…'`), log a non-sensitive identifier instead
@@ -4,14 +4,15 @@ rules:
4
4
  - go
5
5
  severity: ERROR
6
6
  message: |
7
- An OAuth/OIDC credential taken from the HTTP request an authorization
8
- `code`, an `access_token` / `refresh_token` / `id_token`, a bearer
9
- `token`, a `client_secret`, or the raw `Authorization` header flows
10
- into a logging call (`log.*`, `slog.*`, `fmt.Print*`, or a `logger.*`
11
- method). Logs are written to files, shipped to aggregators (Datadog,
12
- Splunk, CloudWatch) and read by people and systems that should never see
13
- live credentials. A leaked authorization code or token can be replayed
14
- to impersonate the user or complete the OAuth exchange (CWE-532).
7
+ An OAuth/OIDC credential from the HTTP request flows into a logging call.
8
+ The tainted value is an authorization `code`, an `access_token` /
9
+ `refresh_token` / `id_token`, a bearer `token`, a `client_secret`, or
10
+ the raw `Authorization` header, and the sink is a `log.*`, `slog.*`,
11
+ `fmt.Print*`, or `logger.*` call. Logs are written to files, shipped to
12
+ aggregators (Datadog, Splunk, CloudWatch) and read by people and systems
13
+ that should never see live credentials. A leaked authorization code or
14
+ token can be replayed to impersonate the user or complete the OAuth
15
+ exchange (CWE-532).
15
16
 
16
17
  Never log the raw credential. Redact or mask it before logging, log a
17
18
  non-sensitive identifier instead (a user id, a key id), or drop the
@@ -4,9 +4,9 @@ rules:
4
4
  - go
5
5
  severity: ERROR
6
6
  message: |
7
- A security-sensitive value its name indicates a token, secret, key,
8
- password, nonce, OTP, or salt is being generated with the `math/rand`
9
- package. `math/rand` is a deterministic PRNG: its output is predictable
7
+ A security-sensitive value is being generated with the `math/rand`
8
+ package. Its name indicates a token, secret, key, password, nonce, OTP,
9
+ or salt. `math/rand` is a deterministic PRNG: its output is predictable
10
10
  and an attacker who observes enough values can recover the seed and
11
11
  forecast every future token. For OAuth/OIDC this means forgeable
12
12
  `state` values, guessable authorization codes, and predictable refresh
@@ -4,12 +4,12 @@ rules:
4
4
  - go
5
5
  severity: WARNING
6
6
  message: |
7
- A JWT parser is configured with `jwt.WithoutClaimsValidation()`, which
8
- turns OFF the standard registered-claims validation golang-jwt performs by
9
- default — the `exp` (expiry), `nbf` (not-before) and `iat` (issued-at)
10
- checks. With validation disabled an expired or not-yet-valid token still
11
- parses successfully, so a stolen or long-expired token is accepted as if
12
- it were current (CWE-613).
7
+ A JWT parser turns off registered-claims validation with
8
+ `jwt.WithoutClaimsValidation()`. That option disables the `exp`
9
+ (expiry), `nbf` (not-before) and `iat` (issued-at) checks golang-jwt
10
+ performs by default. With validation disabled an expired or
11
+ not-yet-valid token still parses successfully, so a stolen or
12
+ long-expired token is accepted as if it were current (CWE-613).
13
13
 
14
14
  Remove `jwt.WithoutClaimsValidation()` and let golang-jwt validate the
15
15
  time-based claims. If a specific claim must be relaxed, scope it narrowly
@@ -4,12 +4,12 @@ rules:
4
4
  - go
5
5
  severity: ERROR
6
6
  message: |
7
- A `Keyfunc` passed to `jwt.Parse`/`jwt.ParseWithClaims` returns the
8
- verification key WITHOUT first checking `token.Method` (the signing
9
- algorithm). This enables an algorithm-confusion attack: if the server
10
- verifies RS256 tokens with an RSA public key, an attacker can forge a
11
- token signed with HS256 using that public key as the HMAC secret, and the
12
- library will accept it — a complete authentication bypass (CWE-347).
7
+ A JWT `Keyfunc` returns the verification key without checking `token.Method`, enabling algorithm confusion.
8
+ It is passed to `jwt.Parse`/`jwt.ParseWithClaims` and hands back the key
9
+ without first asserting the signing algorithm. If the server verifies
10
+ RS256 tokens with an RSA public key, an attacker can forge an HS256 token
11
+ using that public key as the HMAC secret, and the library will accept it:
12
+ a complete authentication bypass (CWE-347).
13
13
 
14
14
  Always assert the signing method inside the keyfunc before returning the
15
15
  key, e.g.:
@@ -4,12 +4,12 @@ rules:
4
4
  - go
5
5
  severity: ERROR
6
6
  message: |
7
- A `tls.Config` is created with `InsecureSkipVerify: true`, which disables
8
- verification of the server's certificate chain and host name. Any
9
- attacker who can intercept the connection can present any certificate
10
- and read or tamper with the traffic a classic man-in-the-middle hole.
11
- For OAuth/OIDC this leaks authorization codes, access tokens, and client
12
- secrets in transit.
7
+ A `tls.Config` sets `InsecureSkipVerify: true`, disabling TLS certificate verification.
8
+ This turns off verification of the server's certificate chain and host
9
+ name, so any attacker who can intercept the connection can present any
10
+ certificate and read or tamper with the traffic: a classic
11
+ man-in-the-middle hole. For OAuth/OIDC this leaks authorization codes,
12
+ access tokens, and client secrets in transit.
13
13
 
14
14
  Never set `InsecureSkipVerify: true`. Leave verification on (the default).
15
15
  To trust a private CA in development, set `RootCAs` to a `*x509.CertPool`
@@ -0,0 +1,38 @@
1
+ rules:
2
+ - id: auth.java.crypto.noop-password-encoder
3
+ languages:
4
+ - java
5
+ severity: ERROR
6
+ message: |
7
+ Spring stores passwords with no hashing.
8
+
9
+ `NoOpPasswordEncoder` keeps passwords in plaintext and
10
+ `withDefaultPasswordEncoder()` is a builder helper that Spring explicitly
11
+ marks for non-production use only. Either way the stored credential is not
12
+ hashed, so anyone who reads the database or a backup recovers every
13
+ password directly (CWE-256). This is a common AI-generated shortcut: the
14
+ no-op encoder is pasted in to "get login working" and never replaced.
15
+
16
+ Hash passwords with a dedicated, slow, salted algorithm. Use
17
+ `new BCryptPasswordEncoder()`, `Argon2PasswordEncoder`, or
18
+ `Pbkdf2PasswordEncoder` instead. A `DelegatingPasswordEncoder` built via
19
+ `PasswordEncoderFactories.createDelegatingPasswordEncoder()` is the
20
+ recommended default.
21
+ # Two literal sinks: the singleton no-op encoder and the deprecated
22
+ # User.withDefaultPasswordEncoder() builder helper. Both are tight enough
23
+ # that a match is always a real plaintext-password configuration.
24
+ pattern-either:
25
+ - pattern: NoOpPasswordEncoder.getInstance()
26
+ - pattern: $U.withDefaultPasswordEncoder()
27
+ metadata:
28
+ oauthlint-rule-id: AUTH-JAVA-CRYPTO-005
29
+ oauthlint-doc-url: https://oauthlint.dev/rules/java-crypto-noop-password-encoder
30
+ category: security
31
+ cwe: CWE-256
32
+ owasp: A02:2021
33
+ llm-prevalence: HIGH
34
+ technology:
35
+ - spring-security
36
+ references:
37
+ - https://docs.spring.io/spring-security/reference/features/authentication/password-storage.html
38
+ - https://cwe.mitre.org/data/definitions/256.html
@@ -0,0 +1,38 @@
1
+ rules:
2
+ - id: auth.java.web.security-ignoring-all
3
+ languages:
4
+ - java
5
+ severity: ERROR
6
+ message: |
7
+ Spring excludes all paths from the security filter chain.
8
+
9
+ `WebSecurity.ignoring()` removes the matched paths from the Spring
10
+ Security filter chain entirely, so they get no authentication,
11
+ authorization, CSRF, or header protection at all. Passing the `/**`
12
+ wildcard excludes every request, leaving the whole application unprotected
13
+ (CWE-862). This is a common AI-generated shortcut to silence security
14
+ errors during development that then ships to production.
15
+
16
+ Never `ignoring()` a broad wildcard. Limit it to genuinely static,
17
+ non-sensitive assets, e.g.
18
+ `web.ignoring().requestMatchers("/css/**", "/js/**")`, or better, handle
19
+ authorization inside the filter chain with `permitAll()` on scoped paths
20
+ so the security headers still apply.
21
+ # Literal `/**` passed to ignoring() across the Security 6 (requestMatchers)
22
+ # and legacy (antMatchers) APIs. A scoped asset path like "/css/**" does not
23
+ # match the literal "/**", so static-resource excludes do not fire.
24
+ pattern-either:
25
+ - pattern: $WEB.ignoring().requestMatchers("/**")
26
+ - pattern: $WEB.ignoring().antMatchers("/**")
27
+ metadata:
28
+ oauthlint-rule-id: AUTH-JAVA-WEB-006
29
+ oauthlint-doc-url: https://oauthlint.dev/rules/java-web-security-ignoring-all
30
+ category: security
31
+ cwe: CWE-862
32
+ owasp: A01:2021
33
+ llm-prevalence: MEDIUM
34
+ technology:
35
+ - spring-security
36
+ references:
37
+ - https://docs.spring.io/spring-security/reference/servlet/configuration/java.html
38
+ - https://cwe.mitre.org/data/definitions/862.html
@@ -0,0 +1,37 @@
1
+ rules:
2
+ - id: auth.java.web.wildcard-permit-all
3
+ languages:
4
+ - java
5
+ severity: ERROR
6
+ message: |
7
+ Spring permits every request via a catch-all matcher.
8
+
9
+ The `/**` matcher matches every path, so granting it `permitAll()` makes
10
+ the whole application reachable without authentication, including
11
+ state-changing and sensitive endpoints (CWE-862, broken access control).
12
+ This is a common AI-generated Spring mistake: a wide-open matcher is
13
+ pasted in to "make it work" and the intended access rules are never added.
14
+
15
+ Open only the specific public routes explicitly, e.g.
16
+ `requestMatchers("/public/**").permitAll()`, and require authentication by
17
+ default with `anyRequest().authenticated()`. Granting `permitAll()` on a
18
+ scoped path is fine; granting it on `/**` is not.
19
+ # Literal `/**` wildcard granted permitAll across the three matcher APIs
20
+ # (requestMatchers in Security 6, antMatchers/mvcMatchers in the legacy
21
+ # chain). Scoped paths like "/public/**" do not match the literal "/**".
22
+ pattern-either:
23
+ - pattern: $X.requestMatchers("/**").permitAll()
24
+ - pattern: $X.antMatchers("/**").permitAll()
25
+ - pattern: $X.mvcMatchers("/**").permitAll()
26
+ metadata:
27
+ oauthlint-rule-id: AUTH-JAVA-WEB-005
28
+ oauthlint-doc-url: https://oauthlint.dev/rules/java-web-wildcard-permit-all
29
+ category: security
30
+ cwe: CWE-862
31
+ owasp: A01:2021
32
+ llm-prevalence: HIGH
33
+ technology:
34
+ - spring-security
35
+ references:
36
+ - https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-http-requests.html
37
+ - https://cwe.mitre.org/data/definitions/862.html
@@ -5,10 +5,10 @@ rules:
5
5
  - typescript
6
6
  severity: WARNING
7
7
  message: |
8
- `jwt.verify(token, key, { ignoreExpiration: true })` from `jsonwebtoken`
9
- disables the `exp` (expiry) claim check, so an expired token is accepted
10
- as valid forever. A stolen or long-old token then never stops working,
11
- defeating the whole point of short-lived access tokens.
8
+ `ignoreExpiration: true` in a `jsonwebtoken` `verify()` call disables the
9
+ `exp` claim check. An expired token is then accepted as valid forever, so
10
+ a stolen or long-old token never stops working, defeating the whole point
11
+ of short-lived access tokens.
12
12
 
13
13
  Remove `ignoreExpiration: true` so the `exp` claim is enforced, and set a
14
14
  sane `expiresIn` when signing (`jwt.sign(payload, key, { expiresIn: '15m'
@@ -0,0 +1,62 @@
1
+ rules:
2
+ - id: auth.nextauth.hardcoded-secret
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: ERROR
7
+ message: |
8
+ The NextAuth/Auth.js `secret` is set to a hard-coded string literal.
9
+
10
+ This value signs and encrypts every session JWT and CSRF token. Committed
11
+ to git it is one search away from compromise, letting an attacker forge
12
+ sessions for any user. Read it from the environment instead:
13
+ `secret: process.env.AUTH_SECRET` (or `NEXTAUTH_SECRET`) and add the
14
+ variable to `.env.example` with a placeholder.
15
+ # Anchored to a NextAuth/Auth.js config so a bare `secret:` in unrelated
16
+ # code never fires: the property must sit inside a `NextAuth(...)` /
17
+ # `Auth(...)` call, an `authOptions`/`authConfig` object, or an object typed
18
+ # as `NextAuthOptions` / `NextAuthConfig` / `AuthOptions`. The value is an
19
+ # AST string literal (`"..."`), so `process.env.*` reads are structurally
20
+ # excluded; the regex allow-list drops `${ENV}` templates, `<placeholders>`,
21
+ # and obvious doc/test stubs. `paths.exclude` keeps the rule off test and
22
+ # example trees (where dev secrets like `"secret"` are intentional), while
23
+ # still firing on its own fixtures under rules/tests/fixtures/.
24
+ patterns:
25
+ - pattern: 'secret: "..."'
26
+ - pattern-not-regex: |-
27
+ (?i)secret\s*:\s*['"]\$\{?[A-Za-z_]+\}?['"]
28
+ - pattern-not-regex: |-
29
+ (?i)secret\s*:\s*['"]<[^'"]*>['"]
30
+ - pattern-not-regex: |-
31
+ (?i)secret\s*:\s*['"](?:your[-_]|my[-_]|example|placeholder|xxx+|todo|fixme|test|dummy|fake|sample|changeme|change[-_]?me|redacted|replace)
32
+ - pattern-either:
33
+ - pattern-inside: 'NextAuth({...})'
34
+ - pattern-inside: 'NextAuth($A, {...})'
35
+ - pattern-inside: 'NextAuth($A, $B, {...})'
36
+ - pattern-inside: 'Auth($A, {...})'
37
+ - pattern-inside: 'authOptions = {...}'
38
+ - pattern-inside: 'authConfig = {...}'
39
+ - pattern-inside: '$X: NextAuthOptions = {...}'
40
+ - pattern-inside: '$X: NextAuthConfig = {...}'
41
+ - pattern-inside: '$X: AuthOptions = {...}'
42
+ paths:
43
+ exclude:
44
+ - "**/test/**"
45
+ - "**/__tests__/**"
46
+ - "**/*.test.*"
47
+ - "**/*.spec.*"
48
+ - "**/example/**"
49
+ - "**/examples/**"
50
+ - "**/demo/**"
51
+ metadata:
52
+ oauthlint-rule-id: AUTH-NEXTAUTH-001
53
+ oauthlint-doc-url: https://oauthlint.dev/rules/nextauth-hardcoded-secret
54
+ category: security
55
+ cwe: CWE-798
56
+ owasp: API8:2023
57
+ llm-prevalence: HIGH
58
+ technology:
59
+ - next-auth
60
+ references:
61
+ - https://authjs.dev/getting-started/deployment#auth_secret
62
+ - https://cwe.mitre.org/data/definitions/798.html
@@ -5,9 +5,9 @@ rules:
5
5
  - typescript
6
6
  severity: ERROR
7
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
8
+ OAuth implicit flow is deprecated by the OAuth 2.0 Security BCP (RFC 9700)
9
+ and the OAuth 2.1 draft. It is triggered by `response_type=token` or
10
+ `response_type=id_token token`. The access token leaks into the URL
11
11
  fragment, browser history, and referrer headers, and there is no
12
12
  refresh-token mechanism.
13
13
 
@@ -5,9 +5,9 @@ rules:
5
5
  - typescript
6
6
  severity: WARNING
7
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
8
+ An OAuth token-lifetime field is set to a literal value longer than
9
+ 24 hours. The value of `expires_in` (or a comparable field) exceeds
10
+ 86_400 seconds. Long-lived access tokens make every token theft
11
11
  catastrophic because they remain valid for days or weeks; the
12
12
  industry standard is 15-60 minutes for access tokens, paired with
13
13
  a refresh-token rotation flow for longer sessions.
@@ -5,10 +5,10 @@ rules:
5
5
  - typescript
6
6
  severity: WARNING
7
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.
8
+ OAuth authorization request from a public client omits the PKCE `code_challenge` parameter.
9
+ The request looks like a public client (SPA, mobile, or native app) yet
10
+ carries no `code_challenge`. Without PKCE, the authorization code can be
11
+ intercepted and exchanged by an attacker.
12
12
 
13
13
  RFC 8252 §6 mandates PKCE for native/SPA clients. RFC 9700 (OAuth 2.0
14
14
  Security BCP) recommends PKCE for ALL clients, including confidential
@@ -5,17 +5,16 @@ rules:
5
5
  - typescript
6
6
  severity: WARNING
7
7
  message: |
8
- An OAuth/OIDC token is being written to `localStorage` /
9
- `sessionStorage` under a token-named key (`access_token`,
10
- `refresh_token`, `id_token`, …). Web storage is readable by any
11
- script on the origin, so any XSS — including a compromised
12
- third-party dependency can exfiltrate the token via
13
- `getItem(...)`. There is no browser-side mitigation, unlike
8
+ An OAuth/OIDC token is written to `localStorage` / `sessionStorage`, readable by any script on the origin.
9
+ The key is token-named (`access_token`, `refresh_token`, `id_token`, …),
10
+ and web storage is exposed to every script on the page, so any XSS,
11
+ including a compromised third-party dependency, can exfiltrate the token
12
+ via `getItem(...)`. There is no browser-side mitigation, unlike
14
13
  `HttpOnly` cookies.
15
14
 
16
15
  Keep access/refresh/id tokens in memory only, or have the server
17
16
  issue them in a `Secure; HttpOnly; SameSite=Strict` cookie.
18
- `sessionStorage` is no safer than `localStorage` against XSS it
17
+ `sessionStorage` is no safer than `localStorage` against XSS: it
19
18
  grants the same attacker capability.
20
19
 
21
20
  See CWE-922: Insecure Storage of Sensitive Information.
@@ -0,0 +1,58 @@
1
+ rules:
2
+ - id: auth.passport.jwt-ignore-expiration
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: WARNING
7
+ message: |
8
+ A `passport-jwt` strategy is configured with `ignoreExpiration: true`.
9
+
10
+ This disables the `exp` claim check, so the strategy authenticates expired
11
+ tokens forever. A leaked or long-old JWT then never stops working,
12
+ defeating short-lived access tokens. Remove `ignoreExpiration: true` (the
13
+ default is `false`, which enforces `exp`) and issue tokens with a short
14
+ lifetime. See CWE-613 (Insufficient Session Expiration).
15
+ # Scoped to `passport-jwt` so an unrelated `ignoreExpiration: true` (in some
16
+ # other library's options) never fires. The file must import the
17
+ # `passport-jwt` Strategy (named, aliased, default, or via `require`), and
18
+ # the option must sit inside a `new ...Strategy({...}, ...)` construction.
19
+ # `new $S({...}, ...)` matches passport-jwt's `(opts, verify)` constructor
20
+ # for any binding name, so the common `new JwtStrategy({ ... }, verify)`
21
+ # alias spelling is covered without relying on Semgrep alias resolution. The
22
+ # match is narrowed to the `ignoreExpiration: true` property itself, so it
23
+ # is caught regardless of which sibling options appear. Non-overlapping with
24
+ # auth.jwt.ignore-expiration, which is scoped to `jsonwebtoken`'s
25
+ # `jwt.verify(...)`.
26
+ patterns:
27
+ - pattern-either:
28
+ - pattern-inside: |
29
+ import { ..., Strategy, ... } from 'passport-jwt'
30
+ ...
31
+ - pattern-inside: |
32
+ import Strategy from 'passport-jwt'
33
+ ...
34
+ - pattern-inside: |
35
+ $S = require('passport-jwt')
36
+ ...
37
+ - pattern-inside: |
38
+ $S = require('passport-jwt').Strategy
39
+ ...
40
+ - pattern-inside: |
41
+ { ..., Strategy, ... } = require('passport-jwt')
42
+ ...
43
+ - pattern-either:
44
+ - pattern-inside: 'new $S({...}, ...)'
45
+ - pattern-inside: 'new $S.Strategy({...}, ...)'
46
+ - pattern: 'ignoreExpiration: true'
47
+ metadata:
48
+ oauthlint-rule-id: AUTH-PASSPORT-001
49
+ oauthlint-doc-url: https://oauthlint.dev/rules/passport-jwt-ignore-expiration
50
+ category: security
51
+ cwe: CWE-613
52
+ owasp: API2:2023
53
+ llm-prevalence: MEDIUM
54
+ technology:
55
+ - passport-jwt
56
+ references:
57
+ - https://www.passportjs.org/packages/passport-jwt/
58
+ - https://cwe.mitre.org/data/definitions/613.html
@@ -4,14 +4,15 @@ rules:
4
4
  - python
5
5
  severity: ERROR
6
6
  message: |
7
- Flask-CORS is configured with `supports_credentials=True` together with a
8
- wildcard origin (`origins="*"`, `origins=["*"]`, or since Flask-CORS
9
- defaults to `*` no `origins` argument at all). The CORS spec forbids the
7
+ Flask-CORS allows any origin while credentials are enabled.
8
+ This configuration pairs `supports_credentials=True` with a wildcard
9
+ origin (`origins="*"`, `origins=["*"]`, or no `origins` argument at all,
10
+ since Flask-CORS defaults to `*`). The CORS spec forbids the
10
11
  `Access-Control-Allow-Origin: *` + `Access-Control-Allow-Credentials: true`
11
- combination, so browsers will block it; the dangerous "fix" is to leave the
12
- wildcard in place while keeping credentials on, which exposes credentialed
13
- cross-origin access to ANY website (CWE-942). For OAuth/OIDC this leaks
14
- cookies, session tokens and CSRF protections cross-origin.
12
+ combination, so browsers will block it; the dangerous "fix" is to leave
13
+ the wildcard in place while keeping credentials on, which exposes
14
+ credentialed cross-origin access to ANY website (CWE-942). For OAuth/OIDC
15
+ this leaks cookies, session tokens and CSRF protections cross-origin.
15
16
 
16
17
  Credentialed requests must use an explicit allow-list of trusted origins,
17
18
  e.g. `CORS(app, origins=["https://app.example.com"], supports_credentials=True)`.
@@ -0,0 +1,49 @@
1
+ rules:
2
+ - id: auth.py.cors.fastapi-wildcard-credentials
3
+ languages:
4
+ - python
5
+ severity: ERROR
6
+ message: |
7
+ FastAPI CORS allows any origin with credentials.
8
+
9
+ Starlette's `CORSMiddleware` is configured with a wildcard origin
10
+ (`allow_origins=["*"]` or `allow_origin_regex=".*"`) together with
11
+ `allow_credentials=True`. The CORS spec forbids
12
+ `Access-Control-Allow-Origin: *` alongside
13
+ `Access-Control-Allow-Credentials: true`, so Starlette silently reflects
14
+ the caller's `Origin` instead, turning the wildcard into "allow every
15
+ site" for credentialed requests. Any website can then read authenticated
16
+ responses, leaking cookies, session and OAuth tokens cross-origin
17
+ (CWE-942).
18
+
19
+ Credentialed CORS needs an explicit allow-list of trusted origins, e.g.
20
+ `allow_origins=["https://app.example.com"], allow_credentials=True`. If
21
+ the endpoint is genuinely public, drop credentials:
22
+ `allow_origins=["*"], allow_credentials=False`.
23
+ # Require the co-occurrence of a wildcard origin AND credentials on the SAME
24
+ # CORSMiddleware configuration. The first arm scopes to the `CORSMiddleware`
25
+ # symbol (either `app.add_middleware(CORSMiddleware, ...)` or a direct
26
+ # `CORSMiddleware(...)`); the AND-ed arms then demand both
27
+ # `allow_credentials=True` and a wildcard origin, so argument order does not
28
+ # matter and an explicit allow-list (or credentials disabled) never fires.
29
+ patterns:
30
+ - pattern-either:
31
+ - pattern: $APP.add_middleware(CORSMiddleware, ...)
32
+ - pattern: CORSMiddleware(...)
33
+ - pattern: $F(..., allow_credentials=True, ...)
34
+ - pattern-either:
35
+ - pattern: $F(..., allow_origins=["*"], ...)
36
+ - pattern: $F(..., allow_origin_regex=".*", ...)
37
+ metadata:
38
+ oauthlint-rule-id: AUTH-PY-CORS-002
39
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-cors-fastapi-wildcard-credentials
40
+ category: security
41
+ cwe: CWE-942
42
+ owasp: API8:2023
43
+ llm-prevalence: HIGH
44
+ technology:
45
+ - fastapi
46
+ references:
47
+ - https://www.starlette.io/middleware/#corsmiddleware
48
+ - https://fastapi.tiangolo.com/tutorial/cors/
49
+ - https://cwe.mitre.org/data/definitions/942.html