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.
- package/package.json +1 -1
- package/rules/cookie/long-lived.yml +2 -2
- package/rules/express/cookie-insecure.yml +44 -0
- package/rules/flow/insecure-random.yml +5 -5
- package/rules/flow/oauth-credential-in-log.yml +9 -8
- package/rules/flow/secret-in-log.yml +7 -6
- package/rules/go/flow/oauth-credential-in-log.yml +9 -8
- package/rules/go/flow/weak-rand.yml +3 -3
- package/rules/go/jwt/skip-claims-validation.yml +6 -6
- package/rules/go/jwt/unchecked-method.yml +6 -6
- package/rules/go/tls/insecure-skip-verify.yml +6 -6
- package/rules/java/crypto/noop-password-encoder.yml +38 -0
- package/rules/java/web/security-ignoring-all.yml +38 -0
- package/rules/java/web/wildcard-permit-all.yml +37 -0
- package/rules/jwt/ignore-expiration.yml +4 -4
- package/rules/nextauth/hardcoded-secret.yml +62 -0
- package/rules/oauth/implicit-flow.yml +3 -3
- package/rules/oauth/long-token-lifetime.yml +3 -3
- package/rules/oauth/no-pkce.yml +4 -4
- package/rules/oauth/token-in-localstorage.yml +6 -7
- package/rules/passport/jwt-ignore-expiration.yml +58 -0
- package/rules/py/cors/allow-all.yml +8 -7
- package/rules/py/cors/fastapi-wildcard-credentials.yml +49 -0
- package/rules/py/crypto/passlib-weak-scheme.yml +40 -0
- package/rules/py/django/cors-allow-all.yml +34 -0
- package/rules/py/drf/default-authentication-empty.yml +32 -0
- package/rules/py/drf/default-permission-allowany.yml +35 -0
- package/rules/py/drf/view-authentication-disabled.yml +34 -0
- package/rules/py/flask/session-cookie-insecure.yml +42 -0
- package/rules/py/flow/insecure-random-token.yml +6 -5
- package/rules/py/flow/oauth-credential-in-log.yml +9 -8
- package/rules/py/flow/requests-verify-disabled.yml +29 -17
- package/rules/py/jwt/algorithm-confusion.yml +9 -8
- package/rules/py/jwt/verify-claims-disabled.yml +45 -0
- package/rules/py/oauth/insecure-transport-env.yml +3 -3
- package/rules/py/oauth/token-request-verify-disabled.yml +14 -4
- package/rules/rust/crypto/weak-password-hash.yml +6 -5
- package/rules/rust/jwt/algorithm-confusion.yml +8 -7
- package/rules/rust/jwt/hardcoded-secret.yml +5 -4
- package/rules/rust/jwt/no-issuer-validation.yml +7 -7
- package/rules/session/hardcoded-secret.yml +3 -3
- package/rules/session/no-regeneration.yml +6 -5
- package/rules/tls/reject-unauthorized.yml +8 -7
package/package.json
CHANGED
|
@@ -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
|
|
10
|
-
milliseconds
|
|
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()`
|
|
9
|
-
|
|
10
|
-
value, an OAuth `state`, a session id, a nonce, or a
|
|
11
|
-
code. `Math.random()` is NOT cryptographically secure and
|
|
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
|
|
9
|
-
`code`, an `access_token` /
|
|
10
|
-
`
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
authorization code or token can be
|
|
15
|
-
complete the OAuth exchange
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
8
|
-
`code`, an `access_token` /
|
|
9
|
-
`
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
Splunk, CloudWatch) and read by people and systems
|
|
13
|
-
live credentials. A leaked authorization code or
|
|
14
|
-
to impersonate the user or complete the OAuth
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
parses successfully, so a stolen or
|
|
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`
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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`
|
|
8
|
-
verification of the server's certificate chain and host
|
|
9
|
-
attacker who can intercept the connection can present any
|
|
10
|
-
and read or tamper with the traffic
|
|
11
|
-
For OAuth/OIDC this leaks authorization codes,
|
|
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
|
-
`
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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 (
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
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.
|
package/rules/oauth/no-pkce.yml
CHANGED
|
@@ -5,10 +5,10 @@ rules:
|
|
|
5
5
|
- typescript
|
|
6
6
|
severity: WARNING
|
|
7
7
|
message: |
|
|
8
|
-
OAuth
|
|
9
|
-
mobile, or native app)
|
|
10
|
-
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
12
|
-
wildcard in place while keeping credentials on, which exposes
|
|
13
|
-
cross-origin access to ANY website (CWE-942). For OAuth/OIDC
|
|
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
|