oauthlint-rules 0.2.6 → 0.3.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 (35) hide show
  1. package/package.json +1 -1
  2. package/rules/cookie/no-httponly.yml +5 -1
  3. package/rules/cookie/no-samesite.yml +5 -1
  4. package/rules/cookie/no-secure.yml +8 -5
  5. package/rules/flow/oauth-credential-in-log.yml +97 -0
  6. package/rules/go/cookie/insecure.yml +15 -6
  7. package/rules/go/flow/oauth-credential-in-log.yml +90 -0
  8. package/rules/go/jwt/untrusted-verify-key.yml +68 -0
  9. package/rules/go/oauth/insecure-token-endpoint.yml +39 -0
  10. package/rules/go/oauth/ropc-grant.yml +49 -0
  11. package/rules/go/oauth/static-state.yml +39 -0
  12. package/rules/go/tls/insecure-skip-verify.yml +14 -3
  13. package/rules/go/tls/min-version.yml +12 -3
  14. package/rules/java/cors/credentialed-wildcard.yml +44 -0
  15. package/rules/java/jwt/none-algorithm.yml +37 -0
  16. package/rules/java/jwt/untrusted-verify-key.yml +54 -0
  17. package/rules/java/oauth/insecure-token-endpoint.yml +39 -0
  18. package/rules/java/oauth/ropc-grant.yml +45 -0
  19. package/rules/java/oauth/static-state.yml +38 -0
  20. package/rules/java/web/permit-all-actuator.yml +43 -0
  21. package/rules/jwt/ignore-expiration.yml +21 -11
  22. package/rules/jwt/untrusted-verify-key.yml +77 -0
  23. package/rules/oauth/insecure-token-endpoint.yml +41 -0
  24. package/rules/oauth/ropc-grant.yml +43 -0
  25. package/rules/oauth/static-state.yml +49 -0
  26. package/rules/py/flow/oauth-credential-in-log.yml +91 -0
  27. package/rules/py/jwt/untrusted-verify-key.yml +75 -0
  28. package/rules/py/oauth/insecure-token-endpoint.yml +40 -0
  29. package/rules/py/oauth/insecure-transport-env.yml +43 -0
  30. package/rules/py/oauth/ropc-grant.yml +50 -0
  31. package/rules/py/oauth/static-state.yml +45 -0
  32. package/rules/py/oauth/token-request-verify-disabled.yml +41 -0
  33. package/rules/rust/oauth/insecure-token-endpoint.yml +39 -0
  34. package/rules/rust/oauth/ropc-grant.yml +40 -0
  35. package/rules/rust/oauth/static-state.yml +38 -0
@@ -0,0 +1,54 @@
1
+ rules:
2
+ - id: auth.java.jwt.untrusted-verify-key
3
+ languages:
4
+ - java
5
+ severity: ERROR
6
+ message: |
7
+ Untrusted request input flows into the JWT verification key. When the
8
+ attacker controls the key, they sign their own forged token and supply the
9
+ matching key, so every token "verifies" — a complete authentication bypass
10
+ (CWE-347, Improper Verification of Cryptographic Signature).
11
+
12
+ The verification key must be fixed server-side. Resolve it from trusted
13
+ configuration, a keystore, or a vetted key set keyed by a validated `kid`
14
+ — never from `request.getParameter(...)` / `request.getHeader(...)` or a
15
+ `@RequestParam` / `@RequestHeader` value.
16
+ # Taint mode with the sink FOCUSED on the key argument — never the token. The
17
+ # token is supposed to come from the request, so focusing on it would
18
+ # false-positive on every correct call; focusing on the key fires only when
19
+ # the attacker controls verification itself. Sources are the servlet request
20
+ # accessors that return raw client input. A key fetched from a keystore
21
+ # (`KeyStore.getKey(...)`) clears the taint.
22
+ mode: taint
23
+ pattern-sources:
24
+ - pattern: $REQ.getParameter(...)
25
+ - pattern: $REQ.getParameterValues(...)
26
+ - pattern: $REQ.getHeader(...)
27
+ - pattern: $REQ.getHeaders(...)
28
+ pattern-sanitizers:
29
+ - pattern: $KS.getKey(...)
30
+ pattern-sinks:
31
+ # jjwt: the signing/verification key passed to the parser.
32
+ - patterns:
33
+ - pattern-either:
34
+ - pattern: $P.setSigningKey($SINK)
35
+ - pattern: $P.verifyWith($SINK)
36
+ - focus-metavariable: $SINK
37
+ # nimbus-jose-jwt: the HMAC secret handed to the verifier.
38
+ - patterns:
39
+ - pattern: new MACVerifier($SINK)
40
+ - focus-metavariable: $SINK
41
+ metadata:
42
+ oauthlint-rule-id: AUTH-JAVA-JWT-003
43
+ oauthlint-doc-url: https://oauthlint.dev/rules/java-jwt-untrusted-verify-key
44
+ category: security
45
+ cwe: CWE-347
46
+ owasp: API2:2023
47
+ llm-prevalence: LOW
48
+ technology:
49
+ - jjwt
50
+ - nimbus-jose-jwt
51
+ references:
52
+ - https://cwe.mitre.org/data/definitions/347.html
53
+ - https://datatracker.ietf.org/doc/html/rfc7518#section-3.1
54
+ - https://owasp.org/www-project-api-security/
@@ -0,0 +1,39 @@
1
+ rules:
2
+ - id: auth.java.oauth.insecure-token-endpoint
3
+ languages:
4
+ - java
5
+ severity: ERROR
6
+ message: |
7
+ An OAuth/OIDC endpoint is being contacted over cleartext `http://`.
8
+ Authorization codes, `client_secret`, access/refresh tokens, and the
9
+ `code_verifier` then travel unencrypted — a network attacker can read or
10
+ rewrite them and take over the flow (CWE-319).
11
+
12
+ RFC 6749 §3.1 / §10.9 require TLS for the authorization and token
13
+ endpoints. Use `https://` for every authorize, token, and userinfo URL.
14
+ `http://localhost` and loopback addresses are fine for local development
15
+ and are not flagged.
16
+ # A string literal that targets an OAuth/OIDC endpoint over http://. Required
17
+ # OAuth markers keep this precise: a generic http URL is NOT flagged, only
18
+ # one carrying an authorize/token request or an /oauth path. `https://`
19
+ # cannot match (the scheme is literal), and localhost / loopback dev hosts
20
+ # are subtracted.
21
+ patterns:
22
+ - pattern-regex: |-
23
+ "http://[^"\s]+(?:response_type=|client_id=|client_secret=|grant_type=|code_challenge=|/oauth2?/|/connect/token|/o/oauth2|/authorize|/oauth/token)[^"]*"
24
+ - pattern-not-regex: |-
25
+ http://(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])
26
+ metadata:
27
+ oauthlint-rule-id: AUTH-JAVA-OAUTH-002
28
+ oauthlint-doc-url: https://oauthlint.dev/rules/java-oauth-insecure-token-endpoint
29
+ category: security
30
+ cwe: CWE-319
31
+ owasp: A02:2021
32
+ llm-prevalence: MEDIUM
33
+ technology:
34
+ - oauth2
35
+ - oidc
36
+ references:
37
+ - https://datatracker.ietf.org/doc/html/rfc6749#section-3.1
38
+ - https://datatracker.ietf.org/doc/html/rfc6749#section-10.9
39
+ - https://cwe.mitre.org/data/definitions/319.html
@@ -0,0 +1,45 @@
1
+ rules:
2
+ - id: auth.java.oauth.ropc-grant
3
+ languages:
4
+ - java
5
+ severity: ERROR
6
+ message: |
7
+ OAuth token request uses the Resource Owner Password Credentials grant
8
+ (`grant_type=password`). The application collects the user's password and
9
+ replays it to the authorization server — exactly what OAuth was designed
10
+ to avoid. It cannot support federation, MFA, or step-up auth, and any
11
+ compromise of your service exposes raw user passwords (CWE-522).
12
+
13
+ The OAuth 2.0 Security BCP (RFC 9700 §2.4) forbids ROPC and OAuth 2.1
14
+ removes it entirely. Use the authorization-code flow with PKCE
15
+ (`grant_type=authorization_code`) for user login, or `client_credentials`
16
+ for machine-to-machine.
17
+ # Match `grant_type=password` only where it is a token-request body, so an
18
+ # unrelated `grant_type` variable is never flagged.
19
+ # - URL-encoded form string body: "grant_type=password&username=…". The
20
+ # value is bounded so `grant_type=password_reset` is NOT matched.
21
+ # - Form builders that pair the key and value as two string-literal args:
22
+ # OkHttp `FormBody.Builder().add("grant_type", "password")`, Spring
23
+ # `BodyInserters.fromFormData("grant_type", "password")` /
24
+ # `MultiValueMap.add(...)`, Apache `new BasicNameValuePair(...)`.
25
+ pattern-either:
26
+ - pattern-regex: |-
27
+ [?&"']grant_type=password(?:["'&\s]|$)
28
+ - pattern-regex: |-
29
+ ["']grant_type["']\s*,\s*["']password["']
30
+ metadata:
31
+ oauthlint-rule-id: AUTH-JAVA-OAUTH-001
32
+ oauthlint-doc-url: https://oauthlint.dev/rules/java-oauth-ropc-grant
33
+ category: security
34
+ cwe: CWE-522
35
+ owasp: API2:2023
36
+ llm-prevalence: MEDIUM
37
+ technology:
38
+ - oauth2
39
+ - spring-web
40
+ - okhttp
41
+ - apache-httpclient
42
+ references:
43
+ - https://datatracker.ietf.org/doc/html/rfc9700#section-2.4
44
+ - https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1#section-2.4
45
+ - https://cwe.mitre.org/data/definitions/522.html
@@ -0,0 +1,38 @@
1
+ rules:
2
+ - id: auth.java.oauth.static-state
3
+ languages:
4
+ - java
5
+ severity: WARNING
6
+ message: |
7
+ OAuth authorization request sends a hardcoded, constant `state` value. A
8
+ static `state` provides ZERO CSRF protection — the whole point is an
9
+ unguessable, per-request value that you store and then compare on the
10
+ callback. A literal that ships in your source is known to everyone and
11
+ identical on every request, so an attacker can forge a matching callback
12
+ (CWE-330).
13
+
14
+ Generate `state` fresh per request from a CSPRNG (`new SecureRandom()` /
15
+ `Base64.getUrlEncoder().encodeToString(randomBytes)`), persist it in the
16
+ session, and verify it when the provider redirects back.
17
+ # An inline authorize URL string literal that carries BOTH a `response_type`
18
+ # (so we know it is an authorize request, not some unrelated `state` field)
19
+ # AND a constant `state=` value. A per-request value is built by
20
+ # concatenation — `"…&state=" + state` — whose literal ends right after
21
+ # `state=`, so the value-character class below cannot match it.
22
+ patterns:
23
+ - pattern-regex: |-
24
+ "https?://[^"\s]+\?[^"]*response_type=[^"]*"
25
+ - pattern-regex: |-
26
+ "https?://[^"\s]+\?[^"]*state=[A-Za-z0-9._~%-]+[^"]*"
27
+ metadata:
28
+ oauthlint-rule-id: AUTH-JAVA-OAUTH-003
29
+ oauthlint-doc-url: https://oauthlint.dev/rules/java-oauth-static-state
30
+ category: security
31
+ cwe: CWE-330
32
+ owasp: API1:2023
33
+ llm-prevalence: MEDIUM
34
+ technology:
35
+ - oauth2
36
+ references:
37
+ - https://datatracker.ietf.org/doc/html/rfc6749#section-10.12
38
+ - https://cwe.mitre.org/data/definitions/330.html
@@ -0,0 +1,43 @@
1
+ rules:
2
+ - id: auth.java.web.permit-all-actuator
3
+ languages:
4
+ - java
5
+ severity: WARNING
6
+ message: |
7
+ Spring Security grants `permitAll()` to a sensitive management path. Spring
8
+ Boot Actuator and similar diagnostic endpoints expose health, environment,
9
+ configuration, thread dumps, and heap dumps — opening them to anonymous
10
+ access leaks secrets and internal state and can enable remote code
11
+ execution (CWE-862, broken access control). This is a common AI-generated
12
+ mistake: the management path is opened to "fix" a probe or scrape and the
13
+ intended authentication is never added.
14
+
15
+ Require authentication for management endpoints — e.g.
16
+ `requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole("ADMIN")` — and
17
+ expose only `/actuator/health` (and `/info`) publicly if you must.
18
+ # Flag permitAll() on a matcher whose path literal targets a management /
19
+ # diagnostics surface (actuator, jolokia, heap/thread dumps, internal).
20
+ # Ordinary application matchers (e.g. "/public/**", "/login") are not
21
+ # flagged. Covers the Spring Security 6 `requestMatchers` and the legacy
22
+ # `antMatchers` / `mvcMatchers`.
23
+ patterns:
24
+ - pattern-either:
25
+ - pattern: $X.requestMatchers($P).permitAll()
26
+ - pattern: $X.antMatchers($P).permitAll()
27
+ - pattern: $X.mvcMatchers($P).permitAll()
28
+ - metavariable-regex:
29
+ metavariable: $P
30
+ regex: (?s).*/(?:actuator|jolokia|heapdump|threaddump|internal)\b.*
31
+ metadata:
32
+ oauthlint-rule-id: AUTH-JAVA-WEB-004
33
+ oauthlint-doc-url: https://oauthlint.dev/rules/java-web-permit-all-actuator
34
+ category: security
35
+ cwe: CWE-862
36
+ owasp: A01:2021
37
+ llm-prevalence: MEDIUM
38
+ technology:
39
+ - spring-security
40
+ - spring-boot-actuator
41
+ references:
42
+ - https://docs.spring.io/spring-boot/reference/actuator/endpoints.html
43
+ - https://cwe.mitre.org/data/definitions/862.html
@@ -18,17 +18,27 @@ rules:
18
18
  # pass `ignoreExpiration: true`; `verify(...)` without that option, and
19
19
  # `ignoreExpiration: false`, are never matched. We also support the
20
20
  # destructured import `import { verify } from 'jsonwebtoken'`, scoped to that
21
- # import so an unrelated `verify()` is safe. The options object is matched
22
- # with `{ ..., ignoreExpiration: true, ... }` so the flag is caught
23
- # regardless of which other options (algorithms, maxAge, audience, ...)
24
- # appear alongside it.
25
- pattern-either:
26
- - pattern: 'jwt.verify($T, $K, { ..., ignoreExpiration: true, ... })'
27
- - patterns:
28
- - pattern-inside: |
29
- import { ..., verify, ... } from 'jsonwebtoken'
30
- ...
31
- - pattern: 'verify($T, $K, { ..., ignoreExpiration: true, ... })'
21
+ # import so an unrelated `verify()` is safe. The verify call is matched with
22
+ # `pattern-inside` (any options object), and the matched node is narrowed to
23
+ # the `ignoreExpiration: true` property itself this keeps detection
24
+ # identical (the flag is still caught regardless of which other options
25
+ # appear alongside it) while letting the autofix below rewrite just that
26
+ # property and leave every sibling option untouched.
27
+ patterns:
28
+ - pattern-either:
29
+ - pattern-inside: 'jwt.verify($T, $K, {...})'
30
+ - patterns:
31
+ - pattern-inside: |
32
+ import { ..., verify, ... } from 'jsonwebtoken'
33
+ ...
34
+ - pattern-inside: 'verify($T, $K, {...})'
35
+ - pattern: 'ignoreExpiration: true'
36
+ # Safe, deterministic autofix: flip the disabled check back on. `false` is the
37
+ # secure value — it is the library default (expiry enforced) and the exact
38
+ # value the rule treats as compliant — so the rewrite fully resolves the
39
+ # finding. The match is narrowed to the property, so `ignoreExpiration: true`
40
+ # becomes `ignoreExpiration: false` with the rest of the options object intact.
41
+ fix: 'ignoreExpiration: false'
32
42
  metadata:
33
43
  oauthlint-rule-id: AUTH-JWT-011
34
44
  oauthlint-doc-url: https://oauthlint.dev/rules/jwt-ignore-expiration
@@ -0,0 +1,77 @@
1
+ rules:
2
+ - id: auth.jwt.untrusted-verify-key
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: ERROR
7
+ message: |
8
+ Untrusted request input flows into the verification key or the
9
+ `algorithms` allowlist of `jwt.verify(...)`. When the attacker controls
10
+ the key, they sign their own forged token and supply the matching key,
11
+ so every token "verifies" — a complete authentication bypass. When the
12
+ attacker controls `algorithms`, they can downgrade verification (e.g. to
13
+ `HS256` against a public key, or to `none` on older libraries) and defeat
14
+ the signature check (CWE-347, Improper Verification of Cryptographic
15
+ Signature).
16
+
17
+ The verification key and the accepted algorithms must be fixed
18
+ server-side. Pin `algorithms` to a constant allowlist
19
+ (`{ algorithms: ['RS256'] }`) and resolve the key from trusted
20
+ configuration or a vetted key set keyed by a validated `kid` — never from
21
+ `req.query` / `req.body` / `req.params` / `req.headers`.
22
+ # Taint mode with the sink FOCUSED on the key / algorithms argument — never
23
+ # the token. The token is supposed to come from the request, so focusing on
24
+ # it would false-positive on every correct call; focusing on the key and the
25
+ # `algorithms` value fires only when the attacker controls verification
26
+ # itself. Routing a candidate algorithm/key through an allow-list /
27
+ # validation helper clears the taint. Distinct from
28
+ # auth.jwt.algorithm-confusion (HS* + a PEM public key) and
29
+ # auth.jwt.no-algorithms-allowlist (a MISSING allowlist): this is about a
30
+ # request-CONTROLLED key or algorithm.
31
+ mode: taint
32
+ pattern-sources:
33
+ - pattern: $REQ.query
34
+ - pattern: $REQ.params
35
+ - pattern: $REQ.body
36
+ - pattern: $REQ.headers
37
+ - pattern: $REQ.query.$X
38
+ - pattern: $REQ.params.$X
39
+ - pattern: $REQ.body.$X
40
+ - pattern: $REQ.headers.$X
41
+ - pattern: $REQ.query[$K]
42
+ - pattern: $REQ.params[$K]
43
+ - pattern: $REQ.body[$K]
44
+ - pattern: $REQ.headers[$K]
45
+ pattern-sanitizers:
46
+ # A candidate algorithm or key vetted against an allow-list / validator is
47
+ # no longer attacker-controlled when it reaches verify().
48
+ - pattern: isAllowedAlgorithm(...)
49
+ - pattern: validateAlgorithm(...)
50
+ - pattern: isAllowedKey(...)
51
+ - pattern: $ALLOW.includes(...)
52
+ - pattern: $ALLOW.has(...)
53
+ - pattern: $ALLOW.indexOf(...)
54
+ pattern-sinks:
55
+ # Request input used as the verification key (2- and 3-argument forms).
56
+ - patterns:
57
+ - pattern-either:
58
+ - pattern: jwt.verify($T, $SINK)
59
+ - pattern: jwt.verify($T, $SINK, $OPTS)
60
+ - focus-metavariable: $SINK
61
+ # Request input used as the accepted `algorithms`.
62
+ - patterns:
63
+ - pattern: 'jwt.verify($T, $KEY, {..., algorithms: $SINK, ...})'
64
+ - focus-metavariable: $SINK
65
+ metadata:
66
+ oauthlint-rule-id: AUTH-JWT-012
67
+ oauthlint-doc-url: https://oauthlint.dev/rules/jwt-untrusted-verify-key
68
+ category: security
69
+ cwe: CWE-347
70
+ owasp: API2:2023
71
+ llm-prevalence: LOW
72
+ technology:
73
+ - jsonwebtoken
74
+ references:
75
+ - https://cwe.mitre.org/data/definitions/347.html
76
+ - https://datatracker.ietf.org/doc/html/rfc7518#section-3.1
77
+ - https://owasp.org/www-project-api-security/
@@ -0,0 +1,41 @@
1
+ rules:
2
+ - id: auth.oauth.insecure-token-endpoint
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: ERROR
7
+ message: |
8
+ An OAuth/OIDC endpoint is being contacted over cleartext `http://`.
9
+ Authorization codes, `client_secret`, access/refresh tokens, and the
10
+ `code_verifier` then travel unencrypted — a network attacker can read
11
+ or rewrite them and take over the flow.
12
+
13
+ RFC 6749 §3.1 / §10.9 require TLS for the authorization and token
14
+ endpoints. Use `https://` for every authorize, token, and userinfo
15
+ URL. `http://localhost` is fine for local development and is not
16
+ flagged.
17
+ pattern-either:
18
+ # A string literal that targets an OAuth/OIDC endpoint over http://.
19
+ # Required OAuth markers keep this precise: a generic http URL is NOT
20
+ # flagged, only one carrying an authorize/token request or an /oauth path.
21
+ # `https://` cannot match (the scheme is `http://` literally), and the
22
+ # localhost / loopback dev hosts are subtracted below.
23
+ - patterns:
24
+ - pattern-regex: |-
25
+ ['"]http://[^'"\s]+(?:response_type=|client_id=|client_secret=|grant_type=|code_challenge=|/oauth2?/|/connect/token|/o/oauth2|/authorize|/oauth/token)[^'"]*['"]
26
+ - pattern-not-regex: |-
27
+ http://(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])
28
+ metadata:
29
+ oauthlint-rule-id: AUTH-OAUTH-015
30
+ oauthlint-doc-url: https://oauthlint.dev/rules/oauth-insecure-token-endpoint
31
+ category: security
32
+ cwe: CWE-319
33
+ owasp: A02:2021
34
+ llm-prevalence: MEDIUM
35
+ technology:
36
+ - oauth2
37
+ - oidc
38
+ references:
39
+ - https://datatracker.ietf.org/doc/html/rfc6749#section-3.1
40
+ - https://datatracker.ietf.org/doc/html/rfc6749#section-10.9
41
+ - https://cwe.mitre.org/data/definitions/319.html
@@ -0,0 +1,43 @@
1
+ rules:
2
+ - id: auth.oauth.ropc-grant
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: ERROR
7
+ message: |
8
+ OAuth token request uses the Resource Owner Password Credentials
9
+ grant (`grant_type=password`). The app collects the user's password
10
+ and replays it to the authorization server — exactly what OAuth was
11
+ designed to avoid. It cannot support federation, MFA, or step-up
12
+ auth, and any compromise of your service exposes raw user passwords.
13
+
14
+ The OAuth 2.0 Security BCP (RFC 9700 §2.4) forbids ROPC and OAuth 2.1
15
+ removes it entirely. Use the authorization-code flow with PKCE
16
+ (`grant_type=authorization_code`) for user login, or
17
+ `client_credentials` for machine-to-machine.
18
+ pattern-either:
19
+ # Object literal / assignment: { grant_type: 'password' } or
20
+ # grant_type = 'password'. The `=` alt also covers `grant_type = "password"`.
21
+ - pattern-regex: |-
22
+ grant_type\s*[:=]\s*['"]password['"]
23
+ # URL-encoded request body: "grant_type=password&username=…" (single,
24
+ # double, or template-literal string). The value is bounded so
25
+ # `grant_type=password_reset` and friends are not flagged.
26
+ - pattern-regex: |-
27
+ [?&'"`]grant_type=password(?:[&'"`\s]|$)
28
+ # URLSearchParams / FormData builders: append/set('grant_type','password').
29
+ - pattern-regex: |-
30
+ ['"]grant_type['"]\s*,\s*['"]password['"]
31
+ metadata:
32
+ oauthlint-rule-id: AUTH-OAUTH-014
33
+ oauthlint-doc-url: https://oauthlint.dev/rules/oauth-ropc-grant
34
+ category: security
35
+ cwe: CWE-522
36
+ owasp: API2:2023
37
+ llm-prevalence: MEDIUM
38
+ technology:
39
+ - oauth2
40
+ references:
41
+ - https://datatracker.ietf.org/doc/html/rfc9700#section-2.4
42
+ - https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1#section-2.4
43
+ - https://cwe.mitre.org/data/definitions/522.html
@@ -0,0 +1,49 @@
1
+ rules:
2
+ - id: auth.oauth.static-state
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: WARNING
7
+ message: |
8
+ OAuth authorization request sends a hardcoded, constant `state`
9
+ value. A static `state` provides ZERO CSRF protection — the whole
10
+ point is an unguessable, per-request value that you store and then
11
+ compare on the callback. A literal that ships in your source is known
12
+ to everyone and identical on every request, so an attacker can forge
13
+ a matching callback.
14
+
15
+ Generate `state` fresh per request from a CSPRNG
16
+ (`crypto.randomBytes(32).toString('hex')` /
17
+ `crypto.getRandomValues`), persist it in the session/cookie, and
18
+ verify it when the provider redirects back.
19
+ pattern-either:
20
+ # Programmatic authorize build: a string-literal `state` alongside a
21
+ # `response_type` in the same URLSearchParams (so we know it is an
22
+ # authorize request, not some unrelated `state` field). A per-request
23
+ # value is a variable/shorthand, which is NOT a quoted literal and so
24
+ # does not match.
25
+ - patterns:
26
+ - pattern: 'new URLSearchParams({..., response_type: $R, ...})'
27
+ - pattern-either:
28
+ - pattern: 'new URLSearchParams({..., state: "$S", ...})'
29
+ - pattern: "new URLSearchParams({..., state: '$S', ...})"
30
+ # Inline authorize URL string literal carrying both response_type and a
31
+ # constant state value. A dynamic state would be a template literal
32
+ # (backticks) with `${...}`, which this single/double-quoted form excludes.
33
+ - patterns:
34
+ - pattern-regex: |-
35
+ ['"]https?://[^'"\s]+\?[^'"]*response_type=[^'"]*['"]
36
+ - pattern-regex: |-
37
+ ['"]https?://[^'"\s]+\?[^'"]*state=[A-Za-z0-9._~%-]+[^'"]*['"]
38
+ metadata:
39
+ oauthlint-rule-id: AUTH-OAUTH-016
40
+ oauthlint-doc-url: https://oauthlint.dev/rules/oauth-static-state
41
+ category: security
42
+ cwe: CWE-330
43
+ owasp: API1:2023
44
+ llm-prevalence: MEDIUM
45
+ technology:
46
+ - oauth2
47
+ references:
48
+ - https://datatracker.ietf.org/doc/html/rfc6749#section-10.12
49
+ - https://cwe.mitre.org/data/definitions/330.html
@@ -0,0 +1,91 @@
1
+ rules:
2
+ - id: auth.py.flow.oauth-credential-in-log
3
+ languages:
4
+ - python
5
+ severity: ERROR
6
+ message: |
7
+ An OAuth/OIDC credential taken from the request — an authorization
8
+ `code`, an `access_token` / `refresh_token` / `id_token`, a bearer
9
+ `token`, a `client_secret`, or the raw `Authorization` header — flows
10
+ into a logging call (`print`, `logging.*`, or a `logger.*`). Logs are
11
+ written to files, shipped to aggregators (Datadog, Splunk, CloudWatch)
12
+ and read by people and systems that should never see live credentials. A
13
+ leaked authorization code or token can be replayed to impersonate the
14
+ user or complete the OAuth exchange (CWE-532).
15
+
16
+ Never log the raw credential. Redact or mask it before logging, log a
17
+ non-sensitive identifier instead (a user id, a key id), or drop the field
18
+ entirely.
19
+ # Taint mode so indirection is caught — `at = request.args.get("access_token");
20
+ # logger.info(at)` flags, not just the inline form. The source list is
21
+ # narrowed to OAuth/OIDC credential field names (and the Authorization
22
+ # header) via a metavariable-regex on the accessor key, so logging a benign
23
+ # field such as `request.args.get("page")` does not fire. Routing the value
24
+ # through a redaction/masking helper clears the taint.
25
+ mode: taint
26
+ pattern-sources:
27
+ # Credential-named request fields: request.args.get("code"),
28
+ # request.form["access_token"], flask.request.json.get("id_token"), …
29
+ - patterns:
30
+ - pattern-either:
31
+ - pattern: request.args.get($K)
32
+ - pattern: request.form.get($K)
33
+ - pattern: request.values.get($K)
34
+ - pattern: request.json.get($K)
35
+ - pattern: request.args[$K]
36
+ - pattern: request.form[$K]
37
+ - pattern: request.values[$K]
38
+ - pattern: request.json[$K]
39
+ - pattern: flask.request.args.get($K)
40
+ - pattern: flask.request.form.get($K)
41
+ - pattern: flask.request.json.get($K)
42
+ - metavariable-regex:
43
+ metavariable: $K
44
+ regex: (?i)^["'](?:code|access[_-]?token|accesstoken|refresh[_-]?token|refreshtoken|id[_-]?token|idtoken|token|client[_-]?secret|clientsecret)["']$
45
+ # Raw Authorization header.
46
+ - patterns:
47
+ - pattern-either:
48
+ - pattern: request.headers.get($K)
49
+ - pattern: request.headers[$K]
50
+ - pattern: flask.request.headers.get($K)
51
+ - metavariable-regex:
52
+ metavariable: $K
53
+ regex: (?i)^["']authorization["']$
54
+ pattern-sanitizers:
55
+ # Redaction / masking helpers — the value that reaches the log is no
56
+ # longer the live credential.
57
+ - pattern: redact(...)
58
+ - pattern: mask(...)
59
+ - pattern: mask_token(...)
60
+ pattern-sinks:
61
+ - patterns:
62
+ - pattern-either:
63
+ - pattern: print(...)
64
+ - pattern: logging.info(...)
65
+ - pattern: logging.debug(...)
66
+ - pattern: logging.warning(...)
67
+ - pattern: logging.error(...)
68
+ - pattern: logging.exception(...)
69
+ - pattern: logging.critical(...)
70
+ # logger.<level>(...) — a log-named receiver with a log-level method.
71
+ - patterns:
72
+ - pattern: $LOG.$LEVEL(...)
73
+ - metavariable-regex:
74
+ metavariable: $LOG
75
+ regex: (?i)^.*log(?:ger)?$
76
+ - metavariable-regex:
77
+ metavariable: $LEVEL
78
+ regex: ^(?:info|debug|warning|warn|error|exception|critical)$
79
+ metadata:
80
+ oauthlint-rule-id: AUTH-PY-FLOW-009
81
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-flow-oauth-credential-in-log
82
+ category: security
83
+ cwe: CWE-532
84
+ owasp: API8:2023
85
+ llm-prevalence: MEDIUM
86
+ technology:
87
+ - flask
88
+ references:
89
+ - https://cwe.mitre.org/data/definitions/532.html
90
+ - https://datatracker.ietf.org/doc/html/rfc6749#section-10.3
91
+ - https://owasp.org/API-Security/editions/2023/en/0xa8-security-misconfiguration/
@@ -0,0 +1,75 @@
1
+ rules:
2
+ - id: auth.py.jwt.untrusted-verify-key
3
+ languages:
4
+ - python
5
+ severity: ERROR
6
+ message: |
7
+ Untrusted request input flows into the verification key or the
8
+ `algorithms` allowlist of `jwt.decode(...)` (PyJWT / python-jose). When
9
+ the attacker controls the key, they sign their own forged token and
10
+ supply the matching key, so every token "verifies" — a complete
11
+ authentication bypass. When the attacker controls `algorithms`, they can
12
+ downgrade verification (e.g. to `HS256` against a public key, or to
13
+ `none` on older libraries) and defeat the signature check (CWE-347,
14
+ Improper Verification of Cryptographic Signature).
15
+
16
+ The verification key and the accepted algorithms must be fixed
17
+ server-side. Pin `algorithms` to a constant allowlist
18
+ (`algorithms=["RS256"]`) and resolve the key from trusted configuration or
19
+ a vetted key set keyed by a validated `kid` — never from `request.args`,
20
+ `request.form`, `request.json`, or `request.headers`.
21
+ # Taint mode with the sink FOCUSED on the key / algorithms argument — never
22
+ # the token. The token is supposed to come from the request, so focusing on
23
+ # it would false-positive on every correct call; focusing on the key and the
24
+ # `algorithms` value fires only when the attacker controls verification
25
+ # itself. Routing a candidate algorithm/key through an allow-list /
26
+ # validation helper clears the taint. Distinct from
27
+ # auth.py.jwt.algorithm-confusion (HS* + an asymmetric family) and
28
+ # auth.py.jwt.no-algorithms (a MISSING allowlist): this is about a
29
+ # request-CONTROLLED key or algorithm.
30
+ mode: taint
31
+ pattern-sources:
32
+ - pattern: request.args.get(...)
33
+ - pattern: request.args.getlist(...)
34
+ - pattern: request.args[...]
35
+ - pattern: request.form.get(...)
36
+ - pattern: request.form[...]
37
+ - pattern: request.values.get(...)
38
+ - pattern: request.json.get(...)
39
+ - pattern: request.headers.get(...)
40
+ - pattern: request.headers[...]
41
+ - pattern: flask.request.args.get(...)
42
+ - pattern: flask.request.headers.get(...)
43
+ pattern-sanitizers:
44
+ # A candidate algorithm or key vetted against an allow-list / validator is
45
+ # no longer attacker-controlled when it reaches decode().
46
+ - pattern: is_allowed_algorithm(...)
47
+ - pattern: validate_algorithm(...)
48
+ - pattern: is_allowed_key(...)
49
+ - pattern: load_trusted_key(...)
50
+ pattern-sinks:
51
+ # Request input used as the verification key (positional and keyword forms).
52
+ - patterns:
53
+ - pattern-either:
54
+ - pattern: jwt.decode($T, $SINK)
55
+ - pattern: jwt.decode($T, $SINK, ...)
56
+ - pattern: jwt.decode($T, key=$SINK, ...)
57
+ - focus-metavariable: $SINK
58
+ # Request input used as the accepted `algorithms`.
59
+ - patterns:
60
+ - pattern: jwt.decode(..., algorithms=$SINK, ...)
61
+ - focus-metavariable: $SINK
62
+ metadata:
63
+ oauthlint-rule-id: AUTH-PY-JWT-007
64
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-jwt-untrusted-verify-key
65
+ category: security
66
+ cwe: CWE-347
67
+ owasp: API2:2023
68
+ llm-prevalence: LOW
69
+ technology:
70
+ - pyjwt
71
+ - python-jose
72
+ references:
73
+ - https://cwe.mitre.org/data/definitions/347.html
74
+ - https://datatracker.ietf.org/doc/html/rfc7518#section-3.1
75
+ - https://owasp.org/www-project-api-security/