oauthlint-rules 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/schema.d.ts.map +1 -1
  2. package/dist/schema.js +10 -4
  3. package/dist/schema.js.map +1 -1
  4. package/package.json +10 -3
  5. package/rules/cors/null-origin.yml +53 -0
  6. package/rules/cors/reflect-origin.yml +68 -0
  7. package/rules/flow/credentials-in-url.yml +37 -0
  8. package/rules/flow/secret-in-log.yml +95 -0
  9. package/rules/flow/timing-unsafe-compare.yml +62 -16
  10. package/rules/flow/weak-bcrypt-rounds.yml +41 -0
  11. package/rules/flow/weak-password-hash.yml +74 -0
  12. package/rules/go/cookie/insecure.yml +35 -0
  13. package/rules/go/cors/allow-all.yml +44 -0
  14. package/rules/go/crypto/bcrypt-low-cost.yml +37 -0
  15. package/rules/go/crypto/weak-cipher.yml +38 -0
  16. package/rules/go/crypto/weak-password-hash.yml +47 -0
  17. package/rules/go/flow/weak-rand.yml +61 -0
  18. package/rules/go/jwt/hardcoded-secret.yml +37 -0
  19. package/rules/go/jwt/none-algorithm.yml +38 -0
  20. package/rules/go/jwt/parse-unverified.yml +33 -0
  21. package/rules/go/jwt/unchecked-method.yml +72 -0
  22. package/rules/go/tls/insecure-skip-verify.yml +32 -0
  23. package/rules/go/tls/min-version.yml +36 -0
  24. package/rules/java/cookie/insecure.yml +31 -0
  25. package/rules/java/cors/allow-all.yml +47 -0
  26. package/rules/java/crypto/ecb-mode.yml +42 -0
  27. package/rules/java/crypto/insecure-random.yml +62 -0
  28. package/rules/java/crypto/weak-password-hash.yml +47 -0
  29. package/rules/java/jwt/hardcoded-secret.yml +43 -0
  30. package/rules/java/jwt/unsigned-jwt.yml +41 -0
  31. package/rules/java/session/fixation-disabled.yml +34 -0
  32. package/rules/java/tls/trust-all-certs.yml +48 -0
  33. package/rules/java/web/csrf-disabled.yml +35 -0
  34. package/rules/java/web/frame-options-disabled.yml +37 -0
  35. package/rules/java/web/permit-all.yml +36 -0
  36. package/rules/jwt/decode-without-verify.yml +56 -0
  37. package/rules/jwt/no-algorithms-allowlist.yml +38 -0
  38. package/rules/oauth/no-nonce.yml +55 -0
  39. package/rules/oauth/pkce-plain.yml +40 -0
  40. package/rules/py/cookie/insecure-flags.yml +41 -0
  41. package/rules/py/flow/csrf-exempt.yml +39 -0
  42. package/rules/py/flow/debug-enabled.yml +37 -0
  43. package/rules/py/flow/insecure-random-token.yml +51 -0
  44. package/rules/py/flow/requests-verify-disabled.yml +50 -0
  45. package/rules/py/flow/weak-password-hash.yml +64 -0
  46. package/rules/py/jwt/alg-none.yml +41 -0
  47. package/rules/py/jwt/hardcoded-secret.yml +40 -0
  48. package/rules/py/jwt/no-algorithms.yml +45 -0
  49. package/rules/py/jwt/no-verify.yml +37 -0
  50. package/rules/py/secret/django-hardcoded-key.yml +32 -0
  51. package/rules/py/secret/flask-hardcoded-key.yml +33 -0
  52. package/rules/rust/cookie/insecure.yml +35 -0
  53. package/rules/rust/cors/permissive.yml +38 -0
  54. package/rules/rust/crypto/bcrypt-low-cost.yml +40 -0
  55. package/rules/rust/crypto/weak-cipher.yml +41 -0
  56. package/rules/rust/crypto/weak-password-hash.yml +46 -0
  57. package/rules/rust/flow/timing-unsafe-compare.yml +77 -0
  58. package/rules/rust/jwt/disable-signature-validation.yml +31 -0
  59. package/rules/rust/jwt/hardcoded-secret.yml +41 -0
  60. package/rules/rust/jwt/no-aud-validation.yml +34 -0
  61. package/rules/rust/jwt/no-expiration-validation.yml +35 -0
  62. package/rules/rust/tls/accept-invalid-certs.yml +30 -0
  63. package/rules/rust/tls/accept-invalid-hostnames.yml +32 -0
  64. package/rules/secret/public-env-secret.yml +58 -0
  65. package/rules/session/hardcoded-secret.yml +42 -0
@@ -0,0 +1,47 @@
1
+ rules:
2
+ - id: auth.java.crypto.weak-password-hash
3
+ languages:
4
+ - java
5
+ severity: ERROR
6
+ message: |
7
+ A password is being hashed with a fast, general-purpose digest from the
8
+ JCA `MessageDigest` (MD5, SHA-1, SHA-256, SHA-512). These algorithms are
9
+ designed to be fast, which makes offline brute-force and rainbow-table
10
+ attacks cheap — they are NOT suitable for storing passwords (CWE-916).
11
+
12
+ Use a dedicated, slow password-hashing function with a per-password salt
13
+ and a tunable work factor: BCrypt (`BCryptPasswordEncoder`), Argon2
14
+ (`Argon2PasswordEncoder`), or PBKDF2 (`Pbkdf2PasswordEncoder` /
15
+ `SecretKeyFactory` with `PBKDF2WithHmacSHA256`). These resist brute-force
16
+ by design.
17
+ # Anchored to the *password* character of the input: the digested/updated
18
+ # argument must be named like a password (metavariable-regex on $PW), and a
19
+ # weak MessageDigest must be instantiated in the same enclosing method
20
+ # (pattern-inside). This avoids flagging `MessageDigest.getInstance("SHA-256")`
21
+ # used for file checksums or non-password fingerprints, and does not touch
22
+ # real password hashers (BCrypt/Argon2/PBKDF2).
23
+ patterns:
24
+ - pattern-either:
25
+ - pattern: $MD.digest($PW.getBytes(...))
26
+ - pattern: $MD.digest($PW)
27
+ - pattern: $MD.update($PW.getBytes(...))
28
+ - pattern: $MD.update($PW)
29
+ - metavariable-regex:
30
+ metavariable: $PW
31
+ regex: (?i).*(password|passwd|pwd).*
32
+ - pattern-inside: |
33
+ MessageDigest $MD = MessageDigest.getInstance("=~/(?i)^(MD5|SHA-?1|SHA-?256|SHA-?512)$/");
34
+ ...
35
+ metadata:
36
+ oauthlint-rule-id: AUTH-JAVA-CRYPTO-001
37
+ oauthlint-doc-url: https://oauthlint.dev/rules/java-crypto-weak-password-hash
38
+ category: security
39
+ cwe: CWE-916
40
+ owasp: A02:2021
41
+ llm-prevalence: HIGH
42
+ technology:
43
+ - java.security
44
+ references:
45
+ - https://cwe.mitre.org/data/definitions/916.html
46
+ - https://docs.spring.io/spring-security/reference/features/authentication/password-storage.html
47
+ - https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
@@ -0,0 +1,43 @@
1
+ rules:
2
+ - id: auth.java.jwt.hardcoded-secret
3
+ languages:
4
+ - java
5
+ severity: ERROR
6
+ message: |
7
+ A JWT signing key is hard-coded as a string literal (CWE-798). Anyone with
8
+ access to the source, the compiled artifact, or the version-control history
9
+ can read the secret and forge valid tokens — changing the subject, roles,
10
+ or expiry at will, because the signature will still verify. This is a common
11
+ AI-generated mistake: a placeholder secret is pasted inline to "make signing
12
+ work" and is never moved out of the code.
13
+
14
+ Load the signing key from outside the source: an environment variable
15
+ (`System.getenv("JWT_SECRET")`), a configuration property, or a dedicated
16
+ secret manager (Vault, AWS Secrets Manager, etc.). With jjwt, build the key
17
+ from those bytes via `Keys.hmacShaKeyFor(secret.getBytes(...))`. Never commit
18
+ the key, and rotate any secret that has already been checked in.
19
+ # jjwt (io.jsonwebtoken): flag only STRING LITERALS in a signing-key position.
20
+ # - Keys.hmacShaKeyFor("literal".getBytes(...)) (current API)
21
+ # - signWith(SignatureAlgorithm.HS*, "literal") (legacy API)
22
+ # - setSigningKey("literal" [.getBytes(...)]) (parser side)
23
+ # A variable, System.getenv(...), or a loaded Key/SecretKey is NOT matched
24
+ # because the argument is constrained to a `"..."` literal.
25
+ pattern-either:
26
+ - pattern: io.jsonwebtoken.security.Keys.hmacShaKeyFor("...".getBytes(...))
27
+ - pattern: Keys.hmacShaKeyFor("...".getBytes(...))
28
+ - pattern: $B.signWith($ALG, "...")
29
+ - pattern: $P.setSigningKey("...")
30
+ - pattern: $P.setSigningKey("...".getBytes(...))
31
+ metadata:
32
+ oauthlint-rule-id: AUTH-JAVA-JWT-002
33
+ oauthlint-doc-url: https://oauthlint.dev/rules/java-jwt-hardcoded-secret
34
+ category: security
35
+ cwe: CWE-798
36
+ owasp: API2:2023
37
+ llm-prevalence: HIGH
38
+ technology:
39
+ - jjwt
40
+ references:
41
+ - https://github.com/jwtk/jjwt#signing-key
42
+ - https://cwe.mitre.org/data/definitions/798.html
43
+ - https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html
@@ -0,0 +1,41 @@
1
+ rules:
2
+ - id: auth.java.jwt.unsigned-jwt
3
+ languages:
4
+ - java
5
+ severity: ERROR
6
+ message: |
7
+ This JWT is created or parsed without a signature, so its contents are
8
+ neither authenticated nor tamper-proof (CWE-347). An unsecured ("alg=none")
9
+ token can be forged by anyone — changing the claims (e.g. the subject or
10
+ roles) costs nothing because there is no signature to verify. This is a
11
+ common AI-generated mistake: the "plaintext"/"unsecured" JWT API is reached
12
+ for during prototyping and never swapped for a signed token.
13
+
14
+ Sign tokens and verify their signatures. With nimbus-jose-jwt use
15
+ `SignedJWT` (and verify with a `JWSVerifier`); with jjwt build tokens via
16
+ `Jwts.builder()...signWith(key)` and parse them with `parseSignedClaims`
17
+ (or the legacy `parseClaimsJws`) instead of `parseClaimsJwt` /
18
+ `parsePlaintextJwt`.
19
+ # nimbus-jose-jwt: PlainJWT is the unsecured (alg=none) variant.
20
+ # jjwt (io.jsonwebtoken): parseClaimsJwt / parsePlaintextJwt parse an
21
+ # UNSIGNED JWT; the signed counterparts are parseSignedClaims /
22
+ # parseClaimsJws and are intentionally not matched.
23
+ pattern-either:
24
+ - pattern: new com.nimbusds.jwt.PlainJWT(...)
25
+ - pattern: new PlainJWT(...)
26
+ - pattern: $P.parseClaimsJwt(...)
27
+ - pattern: $P.parsePlaintextJwt(...)
28
+ metadata:
29
+ oauthlint-rule-id: AUTH-JAVA-JWT-001
30
+ oauthlint-doc-url: https://oauthlint.dev/rules/java-jwt-unsigned-jwt
31
+ category: security
32
+ cwe: CWE-347
33
+ owasp: API2:2023
34
+ llm-prevalence: MEDIUM
35
+ technology:
36
+ - jjwt
37
+ - nimbus-jose-jwt
38
+ references:
39
+ - https://connect2id.com/products/nimbus-jose-jwt/examples/unsecured-jwt
40
+ - https://github.com/jwtk/jjwt#reading-a-jwt
41
+ - https://cwe.mitre.org/data/definitions/347.html
@@ -0,0 +1,34 @@
1
+ rules:
2
+ - id: auth.java.session.fixation-disabled
3
+ languages:
4
+ - java
5
+ severity: ERROR
6
+ message: |
7
+ Spring Security session fixation protection is disabled via
8
+ `sessionFixation().none()`. With `none()`, the session ID is NOT
9
+ regenerated when a user authenticates, so an attacker who fixes the
10
+ victim's session ID before login (e.g. by planting a cookie) keeps a
11
+ valid session and hijacks the authenticated account (CWE-384).
12
+
13
+ Leave the default (`changeSessionId`) in place, or use `migrateSession()`
14
+ to copy the existing session attributes into a new session ID. Never use
15
+ `none()`.
16
+ # Covers the legacy fluent form
17
+ # `sessionManagement().sessionFixation().none()` and the Spring Security 6
18
+ # lambda DSL `sessionManagement(s -> s.sessionFixation(f -> f.none()))`.
19
+ # Targets only `none()` — never `changeSessionId()` or `migrateSession()`.
20
+ pattern-either:
21
+ - pattern: $S.sessionFixation().none()
22
+ - pattern: $S.sessionFixation($F -> $F.none())
23
+ metadata:
24
+ oauthlint-rule-id: AUTH-JAVA-SESSION-001
25
+ oauthlint-doc-url: https://oauthlint.dev/rules/java-session-fixation-disabled
26
+ category: security
27
+ cwe: CWE-384
28
+ owasp: A07:2021
29
+ llm-prevalence: MEDIUM
30
+ technology:
31
+ - spring-security
32
+ references:
33
+ - https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html
34
+ - https://cwe.mitre.org/data/definitions/384.html
@@ -0,0 +1,48 @@
1
+ rules:
2
+ - id: auth.java.tls.trust-all-certs
3
+ languages:
4
+ - java
5
+ severity: ERROR
6
+ message: |
7
+ TLS hostname verification is disabled. A permissive `HostnameVerifier`
8
+ that returns `true` for every host — or Apache HttpClient's
9
+ `NoopHostnameVerifier` — accepts any certificate regardless of the name
10
+ it was issued for, so a man-in-the-middle can present a certificate for a
11
+ different domain and the connection succeeds (CWE-295). This is a common
12
+ AI-generated mistake — the verifier is stubbed out to "fix" a handshake
13
+ or certificate error during development and shipped to production.
14
+
15
+ Never accept all hosts. Leave the default hostname verification in place
16
+ (do not call `setHostnameVerifier`/`setDefaultHostnameVerifier` with a
17
+ permissive verifier), or fix the trust chain by supplying a proper
18
+ truststore so the certificate validates normally.
19
+ # Targets only the permissive verifier: a lambda returning a constant true,
20
+ # an anonymous HostnameVerifier whose verify(...) returns true, and Apache
21
+ # HttpClient's NoopHostnameVerifier. A HostnameVerifier with real
22
+ # (non-trivial) logic and the default verifier are not matched.
23
+ pattern-either:
24
+ # Lambda that always returns true: (hostname, session) -> true
25
+ - pattern: ($HOST, $SESSION) -> true
26
+ # Anonymous HostnameVerifier whose verify(...) just returns true.
27
+ - pattern: |
28
+ new HostnameVerifier() {
29
+ public boolean verify(String $H, SSLSession $S) { return true; }
30
+ }
31
+ # Apache HttpClient NoopHostnameVerifier (constructor or INSTANCE).
32
+ - pattern: new org.apache.http.conn.ssl.NoopHostnameVerifier()
33
+ - pattern: new NoopHostnameVerifier()
34
+ - pattern: org.apache.http.conn.ssl.NoopHostnameVerifier.INSTANCE
35
+ - pattern: NoopHostnameVerifier.INSTANCE
36
+ metadata:
37
+ oauthlint-rule-id: AUTH-JAVA-TLS-001
38
+ oauthlint-doc-url: https://oauthlint.dev/rules/java-tls-trust-all-certs
39
+ category: security
40
+ cwe: CWE-295
41
+ owasp: A02:2021
42
+ llm-prevalence: HIGH
43
+ technology:
44
+ - javax.net.ssl
45
+ - apache-httpclient
46
+ references:
47
+ - https://cwe.mitre.org/data/definitions/295.html
48
+ - https://docs.oracle.com/javase/8/docs/api/javax/net/ssl/HostnameVerifier.html
@@ -0,0 +1,35 @@
1
+ rules:
2
+ - id: auth.java.web.csrf-disabled
3
+ languages:
4
+ - java
5
+ severity: ERROR
6
+ message: |
7
+ Spring Security CSRF protection is disabled. With CSRF off, an attacker
8
+ can forge state-changing requests that a logged-in victim's browser
9
+ submits with their session cookie (CWE-352). This is one of the most
10
+ common AI-generated Spring mistakes — `csrf().disable()` is pasted in to
11
+ "make the API work" and never removed.
12
+
13
+ Keep CSRF protection enabled. For a stateless API authenticated with
14
+ bearer tokens (not cookies), scope it instead — e.g. ignore specific
15
+ paths or use a `CookieCsrfTokenRepository` — rather than disabling it
16
+ globally.
17
+ # Covers the legacy fluent form `http.csrf().disable()`, the Spring
18
+ # Security 6 lambda DSL `http.csrf(c -> c.disable())`, and the method
19
+ # reference `http.csrf(AbstractHttpConfigurer::disable)`.
20
+ pattern-either:
21
+ - pattern: $H.csrf().disable()
22
+ - pattern: $H.csrf($C -> $C.disable())
23
+ - pattern: $H.csrf(AbstractHttpConfigurer::disable)
24
+ metadata:
25
+ oauthlint-rule-id: AUTH-JAVA-WEB-001
26
+ oauthlint-doc-url: https://oauthlint.dev/rules/java-web-csrf-disabled
27
+ category: security
28
+ cwe: CWE-352
29
+ owasp: A01:2021
30
+ llm-prevalence: HIGH
31
+ technology:
32
+ - spring-security
33
+ references:
34
+ - https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html
35
+ - https://cwe.mitre.org/data/definitions/352.html
@@ -0,0 +1,37 @@
1
+ rules:
2
+ - id: auth.java.web.frame-options-disabled
3
+ languages:
4
+ - java
5
+ severity: WARNING
6
+ message: |
7
+ Spring Security's `X-Frame-Options` header is disabled. This header is
8
+ the browser's clickjacking defense — with it off, an attacker can embed
9
+ the application in a hidden `<iframe>` on a malicious page and trick a
10
+ logged-in victim into clicking UI elements they cannot see (CWE-1021).
11
+
12
+ Keep `X-Frame-Options` set to DENY or SAMEORIGIN — e.g.
13
+ `frameOptions(f -> f.sameOrigin())` or `frameOptions(f -> f.deny())`. If
14
+ you need to allow framing from a specific set of origins, use a Content
15
+ Security Policy `frame-ancestors` directive instead of disabling the
16
+ protection outright.
17
+ # Covers the legacy fluent form `http.headers().frameOptions().disable()`,
18
+ # the Spring Security 6 lambda DSL
19
+ # `http.headers(h -> h.frameOptions(f -> f.disable()))`, and the method
20
+ # reference `frameOptions(FrameOptionsConfig::disable)`. Only the `disable`
21
+ # terminal is flagged; `sameOrigin()` and `deny()` are left alone.
22
+ pattern-either:
23
+ - pattern: $H.frameOptions().disable()
24
+ - pattern: $H.frameOptions($F -> $F.disable())
25
+ - pattern: $H.frameOptions(FrameOptionsConfig::disable)
26
+ metadata:
27
+ oauthlint-rule-id: AUTH-JAVA-WEB-003
28
+ oauthlint-doc-url: https://oauthlint.dev/rules/java-web-frame-options-disabled
29
+ category: security
30
+ cwe: CWE-1021
31
+ owasp: A05:2021
32
+ llm-prevalence: MEDIUM
33
+ technology:
34
+ - spring-security
35
+ references:
36
+ - https://docs.spring.io/spring-security/reference/servlet/exploits/headers.html
37
+ - https://cwe.mitre.org/data/definitions/1021.html
@@ -0,0 +1,36 @@
1
+ rules:
2
+ - id: auth.java.web.permit-all
3
+ languages:
4
+ - java
5
+ severity: ERROR
6
+ message: |
7
+ Spring Security authorizes every request without authentication via
8
+ `anyRequest().permitAll()`. Because `anyRequest()` is the catch-all
9
+ matcher, this makes the entire application publicly accessible — any
10
+ endpoint, including state-changing and sensitive ones, can be reached
11
+ without a logged-in user (CWE-862, broken access control). This is a
12
+ common AI-generated Spring mistake: the default-allow line is pasted in
13
+ to "make it work" and the intended access rules are never added.
14
+
15
+ Require authentication by default with `anyRequest().authenticated()`,
16
+ and open only the specific public routes explicitly, e.g.
17
+ `requestMatchers("/public/**").permitAll()`. Granting `permitAll()` on a
18
+ specific matcher is fine; granting it on `anyRequest()` is not.
19
+ # Targets the catch-all `anyRequest().permitAll()` in both the legacy
20
+ # fluent chain and the Spring Security 6 lambda DSL. `$X` lets the chain be
21
+ # rooted on `http`, `auth`, an `authorizeHttpRequests(...)` registry, or
22
+ # any earlier matcher in the chain.
23
+ pattern-either:
24
+ - pattern: $X.anyRequest().permitAll()
25
+ metadata:
26
+ oauthlint-rule-id: AUTH-JAVA-WEB-002
27
+ oauthlint-doc-url: https://oauthlint.dev/rules/java-web-permit-all
28
+ category: security
29
+ cwe: CWE-862
30
+ owasp: A01:2021
31
+ llm-prevalence: HIGH
32
+ technology:
33
+ - spring-security
34
+ references:
35
+ - https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-http-requests.html
36
+ - https://cwe.mitre.org/data/definitions/862.html
@@ -0,0 +1,56 @@
1
+ rules:
2
+ - id: auth.jwt.decode-without-verify
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: WARNING
7
+ message: |
8
+ `jwt.decode()` from `jsonwebtoken` only parses the token — it does NOT
9
+ verify the signature. Trusting any claim returned by `decode()` (e.g.
10
+ `sub`, `role`, `scope`) lets an attacker forge a token and bypass
11
+ authentication entirely.
12
+
13
+ Use `jwt.verify(token, secret, { algorithms: ['RS256'] })`, which checks
14
+ the signature before returning the payload. Only use `decode()` for
15
+ non-security-sensitive inspection (e.g. reading `kid` before verifying).
16
+ # Scoped to the `jsonwebtoken` library via the common alias `jwt`, mirroring
17
+ # no-expiration.yml / alg-none.yml. `jwt.verify(...)` is never matched, and
18
+ # other libraries (`jose.decodeJwt`, a bare `decodeJwt(...)`) are out of
19
+ # scope. We also support the destructured import `import { decode } from
20
+ # 'jsonwebtoken'`, scoped to that import so an unrelated `decode()` is safe.
21
+ #
22
+ # FP guard: jsonwebtoken's `decode(token[, options])` takes the token STRING
23
+ # as its first argument and is synchronous. Other libraries that expose a
24
+ # `jwt.decode` — notably next-auth's own jose-backed helper — are called as
25
+ # `await jwt.decode({ token, salt, ... })` (awaited, single options object).
26
+ # We exclude both shapes so we don't flag those (real-world FP on next-auth).
27
+ pattern-either:
28
+ - patterns:
29
+ - pattern-either:
30
+ - pattern: 'jwt.decode($X)'
31
+ - pattern: 'jwt.decode($X, $OPTS)'
32
+ - pattern-not: 'jwt.decode({...})'
33
+ - pattern-not: 'jwt.decode({...}, $OPTS)'
34
+ - pattern-not: 'await jwt.decode(...)'
35
+ - patterns:
36
+ - pattern-inside: |
37
+ import { ..., decode, ... } from 'jsonwebtoken'
38
+ ...
39
+ - pattern-either:
40
+ - pattern: 'decode($X)'
41
+ - pattern: 'decode($X, $OPTS)'
42
+ - pattern-not: 'decode({...})'
43
+ - pattern-not: 'decode({...}, $OPTS)'
44
+ - pattern-not: 'await decode(...)'
45
+ metadata:
46
+ oauthlint-rule-id: AUTH-JWT-009
47
+ oauthlint-doc-url: https://oauthlint.dev/rules/jwt-decode-without-verify
48
+ category: security
49
+ cwe: CWE-347
50
+ owasp: API2:2023
51
+ llm-prevalence: HIGH
52
+ technology:
53
+ - jsonwebtoken
54
+ references:
55
+ - https://github.com/auth0/node-jsonwebtoken#jwtdecodetoken--options
56
+ - https://cwe.mitre.org/data/definitions/347.html
@@ -0,0 +1,38 @@
1
+ rules:
2
+ - id: auth.jwt.no-algorithms-allowlist
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: WARNING
7
+ message: |
8
+ `jwt.verify(...)` is called without an explicit `algorithms` allowlist.
9
+ Without pinning the accepted algorithms, a token signed with an
10
+ unexpected algorithm — or even `alg: none` on older versions — can be
11
+ accepted, opening the door to algorithm-confusion attacks.
12
+
13
+ Always pass the algorithm(s) you actually expect, e.g.
14
+ `{ algorithms: ['RS256'] }`.
15
+ # Scoped to the `jsonwebtoken` library by matching the common alias
16
+ # `jwt`. Only `jwt.verify(...)` is flagged — `jwt.sign(...)`,
17
+ # `jwt.decode(...)` and `jose.jwtVerify(...)` are intentionally ignored.
18
+ pattern-either:
19
+ - pattern: 'jwt.verify($T, $SECRET)'
20
+ - patterns:
21
+ - pattern: 'jwt.verify($T, $SECRET, $OPTS)'
22
+ - metavariable-pattern:
23
+ metavariable: $OPTS
24
+ patterns:
25
+ - pattern-not: '{..., algorithms: ..., ...}'
26
+ - pattern: '{...}'
27
+ metadata:
28
+ oauthlint-rule-id: AUTH-JWT-010
29
+ oauthlint-doc-url: https://oauthlint.dev/rules/jwt-no-algorithms-allowlist
30
+ category: security
31
+ cwe: CWE-347
32
+ owasp: API2:2023
33
+ llm-prevalence: HIGH
34
+ technology:
35
+ - jsonwebtoken
36
+ references:
37
+ - https://datatracker.ietf.org/doc/html/rfc7518#section-3.1
38
+ - https://owasp.org/www-project-api-security/
@@ -0,0 +1,55 @@
1
+ rules:
2
+ - id: auth.oauth.no-nonce
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: WARNING
7
+ message: |
8
+ OIDC authorization request (scope contains `openid`) is being built
9
+ WITHOUT a `nonce` parameter. The nonce binds the id_token to this
10
+ specific authorization request — without it, an attacker can replay or
11
+ substitute a stolen/forged id_token (OIDC Core §3.1.2.1).
12
+
13
+ Generate a cryptographically random `nonce`, store it in the
14
+ session/cookie, send it on the authorize call, and verify the `nonce`
15
+ claim in the returned id_token. It is REQUIRED for the implicit and
16
+ hybrid flows and RECOMMENDED for the authorization-code flow.
17
+ pattern-either:
18
+ # Inline authorize URL carrying an OIDC scope (scope=openid…) but no
19
+ # nonce. The endpoint path is not hardcoded (Google /o/oauth2/v2/auth,
20
+ # Auth0/Okta /authorize, …). Only OIDC requests carry `openid` in scope.
21
+ - patterns:
22
+ - pattern-regex: |-
23
+ ['"]https?://[^'"\s]+\?[^'"]*scope=[^'"&]*openid[^'"]*['"]
24
+ - pattern-regex: |-
25
+ response_type=
26
+ # Suppress when the SAME URL string already carries a nonce. A bare
27
+ # `nonce=` not-regex would be tested against the narrow
28
+ # `response_type=` span and never subtract, so match the whole URL.
29
+ - pattern-not-regex: |-
30
+ ['"]https?://[^'"\s]+\?[^'"]*nonce=[^'"]*['"]
31
+ # Programmatic URLSearchParams build (AST object match — robust to key
32
+ # order and object length). Fires when response_type is present and the
33
+ # scope value contains `openid`, but nonce is not (covers both
34
+ # `nonce: x` and the `nonce` shorthand). The scope test is an AST
35
+ # metavariable-regex so it composes with the `nonce` pattern-not on the
36
+ # same node (a free-floating pattern-regex would not).
37
+ - patterns:
38
+ - pattern: 'new URLSearchParams({..., response_type: $R, ...})'
39
+ - pattern: 'new URLSearchParams({..., scope: $S, ...})'
40
+ - metavariable-regex:
41
+ metavariable: $S
42
+ regex: .*openid.*
43
+ - pattern-not: 'new URLSearchParams({..., nonce: $N, ...})'
44
+ - pattern-not: 'new URLSearchParams({..., nonce, ...})'
45
+ metadata:
46
+ oauthlint-rule-id: AUTH-OAUTH-010
47
+ oauthlint-doc-url: https://oauthlint.dev/rules/oauth-no-nonce
48
+ category: security
49
+ cwe: CWE-294
50
+ owasp: API2:2023
51
+ llm-prevalence: MEDIUM
52
+ technology:
53
+ - oidc
54
+ references:
55
+ - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
@@ -0,0 +1,40 @@
1
+ rules:
2
+ - id: auth.oauth.pkce-plain
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: WARNING
7
+ message: |
8
+ PKCE is configured with `code_challenge_method=plain`. The `plain`
9
+ method sends the `code_verifier` itself as the `code_challenge`, so an
10
+ attacker who intercepts the authorization request learns the verifier
11
+ and PKCE provides no protection against authorization-code interception.
12
+
13
+ Use `code_challenge_method=S256`: derive `code_challenge =
14
+ BASE64URL-NoPad(SHA256(code_verifier))` (RFC 7636 §4.2). S256 is
15
+ mandatory for clients that can compute SHA-256.
16
+ pattern-either:
17
+ # Query-string form: inline authorize URL, template fragment, or any
18
+ # string carrying the plain method. Matched literally so S256 never hits.
19
+ - pattern-regex: |-
20
+ code_challenge_method=plain\b
21
+ # Object literal: { code_challenge_method: 'plain' } (AST match — robust
22
+ # to key order and object length).
23
+ - pattern: "{..., code_challenge_method: 'plain', ...}"
24
+ - pattern: '{..., code_challenge_method: "plain", ...}'
25
+ # URLSearchParams.set / .append with the plain method.
26
+ - pattern: $P.set('code_challenge_method', 'plain')
27
+ - pattern: $P.set("code_challenge_method", "plain")
28
+ - pattern: $P.append('code_challenge_method', 'plain')
29
+ - pattern: $P.append("code_challenge_method", "plain")
30
+ metadata:
31
+ oauthlint-rule-id: AUTH-OAUTH-011
32
+ oauthlint-doc-url: https://oauthlint.dev/rules/oauth-pkce-plain
33
+ category: security
34
+ cwe: CWE-757
35
+ owasp: API2:2023
36
+ llm-prevalence: MEDIUM
37
+ technology:
38
+ - oauth
39
+ references:
40
+ - https://datatracker.ietf.org/doc/html/rfc7636#section-4.2
@@ -0,0 +1,41 @@
1
+ rules:
2
+ - id: auth.py.cookie.insecure-flags
3
+ languages:
4
+ - python
5
+ severity: ERROR
6
+ message: |
7
+ A session/auth cookie is issued with a security attribute explicitly
8
+ disabled. `secure=False` lets the cookie travel over plain HTTP (it can be
9
+ sniffed on the wire), and `httponly=False` exposes it to JavaScript so an
10
+ XSS payload can read and exfiltrate it — either way the session token is
11
+ at risk of theft (CWE-614, OWASP A05:2021). The same applies to Django's
12
+ `SESSION_COOKIE_SECURE = False`, `CSRF_COOKIE_SECURE = False`, and
13
+ `SESSION_COOKIE_HTTPONLY = False` settings.
14
+
15
+ Always set Secure + HttpOnly (and ideally `SameSite`) on auth cookies,
16
+ e.g. `response.set_cookie("session", token, secure=True, httponly=True,
17
+ samesite="Lax")` or, in Django settings, `SESSION_COOKIE_SECURE = True`
18
+ and `SESSION_COOKIE_HTTPONLY = True`.
19
+ # Matches ONLY the literal `False` on these specific kwargs/settings keys.
20
+ # `secure=True`, `SESSION_COOKIE_SECURE = True`, and the absence of the
21
+ # kwarg are NOT flagged. `$RESP` matches any response variable name.
22
+ pattern-either:
23
+ - pattern: $RESP.set_cookie(..., secure=False, ...)
24
+ - pattern: $RESP.set_cookie(..., httponly=False, ...)
25
+ - pattern: SESSION_COOKIE_SECURE = False
26
+ - pattern: CSRF_COOKIE_SECURE = False
27
+ - pattern: SESSION_COOKIE_HTTPONLY = False
28
+ metadata:
29
+ oauthlint-rule-id: AUTH-PY-COOKIE-001
30
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-cookie-insecure-flags
31
+ category: security
32
+ cwe: CWE-614
33
+ owasp: A05:2021
34
+ llm-prevalence: HIGH
35
+ technology:
36
+ - flask
37
+ - django
38
+ references:
39
+ - https://docs.djangoproject.com/en/stable/ref/settings/#session-cookie-secure
40
+ - https://flask.palletsprojects.com/en/stable/api/#flask.Response.set_cookie
41
+ - https://cwe.mitre.org/data/definitions/614.html
@@ -0,0 +1,39 @@
1
+ rules:
2
+ - id: auth.py.flow.csrf-exempt
3
+ languages:
4
+ - python
5
+ severity: WARNING
6
+ message: |
7
+ A Django view disables CSRF protection. The `@csrf_exempt` decorator
8
+ (from `django.views.decorators.csrf`), or `@method_decorator(csrf_exempt,
9
+ ...)` on a class-based view, turns off Django's CSRF middleware check for
10
+ that endpoint. An attacker can then forge cross-site requests that the
11
+ victim's browser submits with their session cookie — a CSRF vulnerability.
12
+
13
+ Do not exempt views from CSRF. Keep the default protection and submit the
14
+ CSRF token from your client. For machine-to-machine endpoints such as
15
+ webhooks, validate a signed request signature (e.g. an HMAC header)
16
+ instead of disabling CSRF wholesale.
17
+ # Scoped to the `@csrf_exempt` decorator and `@method_decorator(csrf_exempt, ...)`.
18
+ # A bare `import csrf_exempt` or an unrelated decorator is not matched.
19
+ pattern-either:
20
+ - patterns:
21
+ - pattern: |
22
+ @csrf_exempt
23
+ def $VIEW(...): ...
24
+ - patterns:
25
+ - pattern: |
26
+ @method_decorator(csrf_exempt, ...)
27
+ class $VIEW(...): ...
28
+ metadata:
29
+ oauthlint-rule-id: AUTH-PY-FLOW-005
30
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-flow-csrf-exempt
31
+ category: security
32
+ cwe: CWE-352
33
+ owasp: A01:2021
34
+ llm-prevalence: HIGH
35
+ technology:
36
+ - django
37
+ references:
38
+ - https://docs.djangoproject.com/en/stable/ref/csrf/
39
+ - https://cwe.mitre.org/data/definitions/352.html
@@ -0,0 +1,37 @@
1
+ rules:
2
+ - id: auth.py.flow.debug-enabled
3
+ languages:
4
+ - python
5
+ severity: WARNING
6
+ message: |
7
+ Debug mode is hard-coded to `True`. In production this leaks the
8
+ `SECRET_KEY`, environment variables, and full tracebacks, and Flask's
9
+ Werkzeug debugger additionally exposes an interactive console that
10
+ allows remote code execution.
11
+
12
+ Never enable debug mode in production. Drive it from an environment
13
+ variable that defaults to off, e.g.
14
+ `debug=os.environ.get("FLASK_DEBUG") == "1"` (Flask) or
15
+ `DEBUG = os.environ.get("DJANGO_DEBUG") == "1"` (Django).
16
+ # Matches only the literal `True`. `debug=False`, `DEBUG = False`,
17
+ # `DEBUG = os.environ.get(...)`, and `config(...)` are NOT matched.
18
+ # `$APP` is a metavariable for any application object name.
19
+ pattern-either:
20
+ - pattern: $APP.run(..., debug=True, ...)
21
+ - pattern: $APP.config["DEBUG"] = True
22
+ - pattern: $APP.debug = True
23
+ - pattern: DEBUG = True
24
+ metadata:
25
+ oauthlint-rule-id: AUTH-PY-FLOW-003
26
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-flow-debug-enabled
27
+ category: security
28
+ cwe: CWE-489
29
+ owasp: A05:2021
30
+ llm-prevalence: HIGH
31
+ technology:
32
+ - flask
33
+ - django
34
+ references:
35
+ - https://flask.palletsprojects.com/en/stable/config/#DEBUG
36
+ - https://docs.djangoproject.com/en/stable/ref/settings/#debug
37
+ - https://cwe.mitre.org/data/definitions/489.html