oauthlint-rules 0.1.1 → 0.2.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.
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +10 -4
- package/dist/schema.js.map +1 -1
- package/package.json +9 -2
- package/rules/cors/null-origin.yml +53 -0
- package/rules/cors/reflect-origin.yml +78 -0
- package/rules/flow/credentials-in-url.yml +37 -0
- package/rules/flow/secret-in-log.yml +95 -0
- package/rules/flow/timing-unsafe-compare.yml +62 -16
- package/rules/flow/weak-bcrypt-rounds.yml +41 -0
- package/rules/flow/weak-password-hash.yml +74 -0
- package/rules/go/cookie/insecure.yml +35 -0
- package/rules/go/cors/allow-all.yml +44 -0
- package/rules/go/crypto/bcrypt-low-cost.yml +37 -0
- package/rules/go/crypto/weak-cipher.yml +38 -0
- package/rules/go/crypto/weak-password-hash.yml +47 -0
- package/rules/go/flow/weak-rand.yml +61 -0
- package/rules/go/jwt/hardcoded-secret.yml +37 -0
- package/rules/go/jwt/none-algorithm.yml +38 -0
- package/rules/go/jwt/parse-unverified.yml +33 -0
- package/rules/go/jwt/unchecked-method.yml +72 -0
- package/rules/go/tls/insecure-skip-verify.yml +32 -0
- package/rules/go/tls/min-version.yml +36 -0
- package/rules/java/cookie/insecure.yml +31 -0
- package/rules/java/cors/allow-all.yml +47 -0
- package/rules/java/crypto/ecb-mode.yml +42 -0
- package/rules/java/crypto/insecure-random.yml +62 -0
- package/rules/java/crypto/weak-password-hash.yml +47 -0
- package/rules/java/jwt/hardcoded-secret.yml +43 -0
- package/rules/java/jwt/unsigned-jwt.yml +41 -0
- package/rules/java/session/fixation-disabled.yml +34 -0
- package/rules/java/tls/trust-all-certs.yml +48 -0
- package/rules/java/web/csrf-disabled.yml +35 -0
- package/rules/java/web/frame-options-disabled.yml +37 -0
- package/rules/java/web/permit-all.yml +36 -0
- package/rules/jwt/decode-without-verify.yml +56 -0
- package/rules/jwt/localstorage.yml +13 -1
- package/rules/jwt/no-algorithms-allowlist.yml +38 -0
- package/rules/oauth/no-nonce.yml +55 -0
- package/rules/oauth/no-state-validation.yml +28 -6
- package/rules/oauth/pkce-plain.yml +40 -0
- package/rules/py/cookie/insecure-flags.yml +41 -0
- package/rules/py/flow/csrf-exempt.yml +39 -0
- package/rules/py/flow/debug-enabled.yml +37 -0
- package/rules/py/flow/insecure-random-token.yml +51 -0
- package/rules/py/flow/requests-verify-disabled.yml +50 -0
- package/rules/py/flow/weak-password-hash.yml +64 -0
- package/rules/py/jwt/alg-none.yml +41 -0
- package/rules/py/jwt/hardcoded-secret.yml +40 -0
- package/rules/py/jwt/no-algorithms.yml +45 -0
- package/rules/py/jwt/no-verify.yml +37 -0
- package/rules/py/secret/django-hardcoded-key.yml +32 -0
- package/rules/py/secret/flask-hardcoded-key.yml +33 -0
- package/rules/rust/cookie/insecure.yml +35 -0
- package/rules/rust/cors/permissive.yml +38 -0
- package/rules/rust/crypto/bcrypt-low-cost.yml +40 -0
- package/rules/rust/crypto/weak-cipher.yml +41 -0
- package/rules/rust/crypto/weak-password-hash.yml +46 -0
- package/rules/rust/flow/timing-unsafe-compare.yml +77 -0
- package/rules/rust/jwt/disable-signature-validation.yml +31 -0
- package/rules/rust/jwt/hardcoded-secret.yml +41 -0
- package/rules/rust/jwt/no-aud-validation.yml +34 -0
- package/rules/rust/jwt/no-expiration-validation.yml +35 -0
- package/rules/rust/tls/accept-invalid-certs.yml +30 -0
- package/rules/rust/tls/accept-invalid-hostnames.yml +32 -0
- package/rules/secret/public-env-secret.yml +58 -0
- 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
|
|
@@ -21,10 +21,22 @@ rules:
|
|
|
21
21
|
# NOT match the bare words `auth`/`access`/`refresh`/`session` because they
|
|
22
22
|
# appear as substrings of innocuous keys (author_filter, auto_refresh,
|
|
23
23
|
# access_count, sidebar_session) and produced false positives.
|
|
24
|
+
# A write to web storage (`setItem(k, v)` or `store[k] = v`) is flagged when
|
|
25
|
+
# EITHER the key is a token-named string literal OR the value is a token-named
|
|
26
|
+
# identifier. The latter catches `localStorage.setItem(TOKEN_KEY, token)`,
|
|
27
|
+
# where the key is a variable — the most common real-world shape. We match
|
|
28
|
+
# only the strong words `token`/`jwt`/`bearer`/`credential`/`api_key` (never
|
|
29
|
+
# bare `auth`/`access`/`refresh`/`session`, which appear in innocuous keys
|
|
30
|
+
# like `author_filter`, `auto_refresh`, `access_count`).
|
|
24
31
|
pattern-either:
|
|
32
|
+
# `setItem(key, value)` where a strong token word appears in EITHER the key
|
|
33
|
+
# or the value. Matching the whole call as one range (rather than separate
|
|
34
|
+
# key/value patterns) catches `setItem(TOKEN_KEY, token)` — the common shape
|
|
35
|
+
# where the key is a variable — without double-counting literal-key lines.
|
|
25
36
|
- patterns:
|
|
26
37
|
- pattern-regex: |-
|
|
27
|
-
(?:window\.|globalThis\.|self\.)?(?:localStorage|sessionStorage)\.setItem\(
|
|
38
|
+
(?:window\.|globalThis\.|self\.)?(?:localStorage|sessionStorage)\.setItem\([^)]*?(?i:token|jwt|bearer|credential|api[_-]?key)
|
|
39
|
+
# Bracket assignment with a token-named string-literal key.
|
|
28
40
|
- patterns:
|
|
29
41
|
- pattern-regex: |-
|
|
30
42
|
(?:window\.|globalThis\.|self\.)?(?:localStorage|sessionStorage)\[\s*['"][^'"]*(?i:token|jwt|bearer|credential|api[_-]?key)[^'"]*['"]\s*\]\s*=
|
|
@@ -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
|
|
@@ -13,24 +13,46 @@ rules:
|
|
|
13
13
|
Compare the received `state` to the value you stored before
|
|
14
14
|
redirecting (session, signed cookie, or Redis). Reject the callback
|
|
15
15
|
if it's missing or doesn't match.
|
|
16
|
-
# Suppress when the received `state` is
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
16
|
+
# Suppress when the received `state` is validated. Two shapes count as
|
|
17
|
+
# validation:
|
|
18
|
+
# 1. The read appears directly inside an `if` condition, e.g.
|
|
19
|
+
# `if (req.query.state !== stored)`. The deep-expression operator
|
|
20
|
+
# `<... state ...>` makes this order-agnostic and helper-aware, so
|
|
21
|
+
# `if (stored === req.query.state)` and `if (!verifyState(req.query.state))`
|
|
22
|
+
# are all safe (the previous exact-shape patterns only matched one
|
|
23
|
+
# operand order → false positives).
|
|
24
|
+
# 2. The read is captured into a variable that is later compared, e.g.
|
|
25
|
+
# `const state = url.searchParams.get("state"); ...; if (state !== stored)`.
|
|
26
|
+
# This is the most common real-world shape (the callback reads state
|
|
27
|
+
# once, then validates the local) and the inline-`if` suppressors alone
|
|
28
|
+
# miss it. The sequence patterns below match the read assigned to `$S`
|
|
29
|
+
# followed (after any statements) by an `if` that references `$S`.
|
|
22
30
|
pattern-either:
|
|
23
31
|
- patterns:
|
|
24
32
|
- pattern-either:
|
|
25
33
|
- pattern: '$REQ.query.state'
|
|
26
34
|
- pattern: '$REQ.body.state'
|
|
27
35
|
- pattern: '$URL.searchParams.get("state")'
|
|
36
|
+
# (1) read used directly inside an `if` condition
|
|
28
37
|
- pattern-not-inside: |
|
|
29
38
|
if (<... $REQ.query.state ...>) { ... }
|
|
30
39
|
- pattern-not-inside: |
|
|
31
40
|
if (<... $REQ.body.state ...>) { ... }
|
|
32
41
|
- pattern-not-inside: |
|
|
33
42
|
if (<... $URL.searchParams.get("state") ...>) { ... }
|
|
43
|
+
# (2) read captured into a local that is later validated in an `if`
|
|
44
|
+
- pattern-not-inside: |
|
|
45
|
+
$S = $REQ.query.state;
|
|
46
|
+
...
|
|
47
|
+
if (<... $S ...>) { ... }
|
|
48
|
+
- pattern-not-inside: |
|
|
49
|
+
$S = $REQ.body.state;
|
|
50
|
+
...
|
|
51
|
+
if (<... $S ...>) { ... }
|
|
52
|
+
- pattern-not-inside: |
|
|
53
|
+
$S = $URL.searchParams.get("state");
|
|
54
|
+
...
|
|
55
|
+
if (<... $S ...>) { ... }
|
|
34
56
|
metadata:
|
|
35
57
|
oauthlint-rule-id: AUTH-OAUTH-007
|
|
36
58
|
oauthlint-doc-url: https://oauthlint.dev/rules/oauth-no-state-validation
|
|
@@ -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
|