oauthlint-rules 0.2.6 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/schema.d.ts +60 -0
  2. package/dist/schema.d.ts.map +1 -1
  3. package/dist/schema.js +12 -0
  4. package/dist/schema.js.map +1 -1
  5. package/package.json +1 -1
  6. package/rules/cookie/no-httponly.yml +5 -1
  7. package/rules/cookie/no-samesite.yml +5 -1
  8. package/rules/cookie/no-secure.yml +8 -5
  9. package/rules/cookie/samesite-none-insecure.yml +22 -0
  10. package/rules/flow/oauth-credential-in-log.yml +97 -0
  11. package/rules/flow/open-redirect.yml +11 -0
  12. package/rules/flow/ssrf.yml +19 -0
  13. package/rules/flow/timing-unsafe-compare.yml +19 -0
  14. package/rules/go/cookie/insecure.yml +15 -6
  15. package/rules/go/flow/oauth-credential-in-log.yml +90 -0
  16. package/rules/go/jwt/untrusted-verify-key.yml +68 -0
  17. package/rules/go/oauth/insecure-token-endpoint.yml +39 -0
  18. package/rules/go/oauth/ropc-grant.yml +64 -0
  19. package/rules/go/oauth/static-state.yml +39 -0
  20. package/rules/go/tls/insecure-skip-verify.yml +14 -3
  21. package/rules/go/tls/min-version.yml +12 -3
  22. package/rules/java/cors/credentialed-wildcard.yml +44 -0
  23. package/rules/java/jwt/none-algorithm.yml +37 -0
  24. package/rules/java/jwt/untrusted-verify-key.yml +54 -0
  25. package/rules/java/oauth/insecure-token-endpoint.yml +39 -0
  26. package/rules/java/oauth/ropc-grant.yml +45 -0
  27. package/rules/java/oauth/static-state.yml +38 -0
  28. package/rules/java/web/permit-all-actuator.yml +43 -0
  29. package/rules/jwt/ignore-expiration.yml +21 -11
  30. package/rules/jwt/untrusted-verify-key.yml +77 -0
  31. package/rules/oauth/insecure-token-endpoint.yml +41 -0
  32. package/rules/oauth/ropc-grant.yml +48 -0
  33. package/rules/oauth/static-state.yml +49 -0
  34. package/rules/py/flow/oauth-credential-in-log.yml +91 -0
  35. package/rules/py/jwt/untrusted-verify-key.yml +75 -0
  36. package/rules/py/oauth/insecure-token-endpoint.yml +40 -0
  37. package/rules/py/oauth/insecure-transport-env.yml +43 -0
  38. package/rules/py/oauth/ropc-grant.yml +58 -0
  39. package/rules/py/oauth/static-state.yml +45 -0
  40. package/rules/py/oauth/token-request-verify-disabled.yml +41 -0
  41. package/rules/rust/oauth/insecure-token-endpoint.yml +39 -0
  42. package/rules/rust/oauth/ropc-grant.yml +57 -0
  43. package/rules/rust/oauth/static-state.yml +58 -0
@@ -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,48 @@
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 entry: `{ grant_type: 'password' }` (bare or quoted key).
20
+ # The colon anchors it to a property, not a bare variable assignment.
21
+ - pattern-regex: |-
22
+ grant_type\s*:\s*['"]password['"]
23
+ # Property write that builds a request body: `body.grant_type = 'password'`.
24
+ # The leading `.` requires a member assignment, so a library's own bare
25
+ # local `grant_type = 'password'` (an internal constant) is NOT flagged.
26
+ - pattern-regex: |-
27
+ \.grant_type\s*=\s*['"]password['"]
28
+ # URL-encoded request body: "grant_type=password&username=…" (single,
29
+ # double, or template-literal string). The value is bounded so
30
+ # `grant_type=password_reset` and friends are not flagged.
31
+ - pattern-regex: |-
32
+ [?&'"`]grant_type=password(?:[&'"`\s]|$)
33
+ # URLSearchParams / FormData builders: append/set('grant_type','password').
34
+ - pattern-regex: |-
35
+ ['"]grant_type['"]\s*,\s*['"]password['"]
36
+ metadata:
37
+ oauthlint-rule-id: AUTH-OAUTH-014
38
+ oauthlint-doc-url: https://oauthlint.dev/rules/oauth-ropc-grant
39
+ category: security
40
+ cwe: CWE-522
41
+ owasp: API2:2023
42
+ llm-prevalence: MEDIUM
43
+ technology:
44
+ - oauth2
45
+ references:
46
+ - https://datatracker.ietf.org/doc/html/rfc9700#section-2.4
47
+ - https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1#section-2.4
48
+ - 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/
@@ -0,0 +1,40 @@
1
+ rules:
2
+ - id: auth.py.oauth.insecure-token-endpoint
3
+ languages:
4
+ - python
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
10
+ or rewrite them and take over the flow.
11
+
12
+ RFC 6749 §3.1 / §10.9 require TLS for the authorization and token
13
+ endpoints. Use `https://` for every authorize, token, userinfo, and
14
+ `.well-known` discovery URL. `http://localhost` and loopback addresses
15
+ are fine for local development and are not flagged.
16
+ # A string literal (including an f-string) that targets an OAuth/OIDC
17
+ # endpoint over http://. Required OAuth markers keep this precise: a generic
18
+ # http URL is NOT flagged, only one carrying an authorize/token request or an
19
+ # /oauth, /connect/token, or /.well-known path. `https://` cannot match (the
20
+ # scheme is the literal `http://`), and the localhost / loopback dev hosts
21
+ # are subtracted below.
22
+ patterns:
23
+ - pattern-regex: |-
24
+ ['"]http://[^'"\s]+(?:response_type=|client_id=|client_secret=|grant_type=|code_challenge=|/oauth2?/|/connect/token|/o/oauth2|/authorize|/oauth/token|/\.well-known/)[^'"]*['"]
25
+ - pattern-not-regex: |-
26
+ http://(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])
27
+ metadata:
28
+ oauthlint-rule-id: AUTH-PY-OAUTH-002
29
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-oauth-insecure-token-endpoint
30
+ category: security
31
+ cwe: CWE-319
32
+ owasp: A02:2021
33
+ llm-prevalence: MEDIUM
34
+ technology:
35
+ - oauth2
36
+ - oidc
37
+ references:
38
+ - https://datatracker.ietf.org/doc/html/rfc6749#section-3.1
39
+ - https://datatracker.ietf.org/doc/html/rfc6749#section-10.9
40
+ - https://cwe.mitre.org/data/definitions/319.html
@@ -0,0 +1,43 @@
1
+ rules:
2
+ - id: auth.py.oauth.insecure-transport-env
3
+ languages:
4
+ - python
5
+ severity: ERROR
6
+ message: |
7
+ `OAUTHLIB_INSECURE_TRANSPORT` is being set, which disables oauthlib's
8
+ HTTPS requirement for OAuth flows (`requests-oauthlib`, Authlib's
9
+ requests integration, Django OAuth Toolkit). oauthlib raises
10
+ `InsecureTransportError` to stop you exchanging codes and tokens over
11
+ cleartext; setting this variable silences that guard, so authorization
12
+ codes, `client_secret`, and access/refresh tokens travel over plain
13
+ `http://` where a network attacker can read or rewrite them (CWE-319).
14
+
15
+ Remove this assignment and serve every OAuth endpoint over `https://`.
16
+ For local development use a loopback HTTPS listener or a tunnel rather
17
+ than disabling transport security in code that can ship to production.
18
+ # Matches only an ASSIGNMENT (or setdefault/putenv) of the
19
+ # OAUTHLIB_INSECURE_TRANSPORT key. Merely reading it
20
+ # (`os.environ.get("OAUTHLIB_INSECURE_TRANSPORT")`) is not flagged, and other
21
+ # environment keys are untouched. Semgrep normalises quote style, so the
22
+ # single- and double-quoted spellings are both covered.
23
+ pattern-either:
24
+ - pattern: os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = ...
25
+ - pattern: os.environ.setdefault("OAUTHLIB_INSECURE_TRANSPORT", ...)
26
+ - pattern: os.putenv("OAUTHLIB_INSECURE_TRANSPORT", ...)
27
+ - pattern: environ["OAUTHLIB_INSECURE_TRANSPORT"] = ...
28
+ - pattern: environ.setdefault("OAUTHLIB_INSECURE_TRANSPORT", ...)
29
+ metadata:
30
+ oauthlint-rule-id: AUTH-PY-OAUTH-004
31
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-oauth-insecure-transport-env
32
+ category: security
33
+ cwe: CWE-319
34
+ owasp: A02:2021
35
+ llm-prevalence: HIGH
36
+ technology:
37
+ - oauthlib
38
+ - requests-oauthlib
39
+ - authlib
40
+ references:
41
+ - https://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html
42
+ - https://datatracker.ietf.org/doc/html/rfc6749#section-3.1
43
+ - https://cwe.mitre.org/data/definitions/319.html
@@ -0,0 +1,58 @@
1
+ rules:
2
+ - id: auth.py.oauth.ropc-grant
3
+ languages:
4
+ - python
5
+ severity: ERROR
6
+ message: |
7
+ OAuth token request uses the Resource Owner Password Credentials
8
+ grant (`grant_type=password`). The app collects the user's password
9
+ and replays it to the authorization server — exactly what OAuth was
10
+ designed to avoid. It cannot support federation, MFA, or step-up
11
+ auth, and any compromise of your service exposes raw user passwords.
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
16
+ `client_credentials` for machine-to-machine. In Python this covers a
17
+ `requests`/`httpx`/`urllib` body, an OAuth client call, or a
18
+ URL-encoded body string.
19
+ # Regex-based so it is library-agnostic across requests, httpx, urllib and
20
+ # OAuth clients. The `password` value is matched as an exact quoted literal
21
+ # (dict / kwarg form) or bounded token (URL-encoded form), so
22
+ # `grant_type=password_reset` and `grant_type=client_credentials` never
23
+ # fire, and a dynamic `grant_type=grant` variable is not a literal and is
24
+ # not flagged. Each form anchors `grant_type` to a request-parameter
25
+ # position (dict key, call keyword, query string, or pair) so a library's
26
+ # own bare local assignment — `grant_type = "password"`, as in Authlib's
27
+ # `_guess_grant_type` — is NOT treated as an application sending the grant.
28
+ pattern-either:
29
+ # Dict / mapping entry: `{"grant_type": "password"}`. The key is a quoted
30
+ # literal followed by a colon, which a bare variable assignment is not.
31
+ - pattern-regex: |-
32
+ ['"]grant_type['"]\s*:\s*['"]password['"]
33
+ # Call keyword argument: `fetch_token(..., grant_type="password")`. The
34
+ # leading `(` or `,` anchors it to an argument list, so a top-level
35
+ # statement `grant_type = "password"` (library internal) does not match.
36
+ - pattern-regex: |-
37
+ [(,]\s*grant_type\s*=\s*['"]password['"]
38
+ # URL-encoded request body: "grant_type=password&username=…". The value is
39
+ # bounded so `grant_type=password_reset` is not flagged.
40
+ - pattern-regex: |-
41
+ [?&'"]grant_type=password(?:[&'"\s]|$)
42
+ # Tuple/list pair form: ("grant_type", "password").
43
+ - pattern-regex: |-
44
+ ['"]grant_type['"]\s*,\s*['"]password['"]
45
+ metadata:
46
+ oauthlint-rule-id: AUTH-PY-OAUTH-001
47
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-oauth-ropc-grant
48
+ category: security
49
+ cwe: CWE-522
50
+ owasp: API2:2023
51
+ llm-prevalence: MEDIUM
52
+ technology:
53
+ - oauth2
54
+ - requests
55
+ references:
56
+ - https://datatracker.ietf.org/doc/html/rfc9700#section-2.4
57
+ - https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1#section-2.4
58
+ - https://cwe.mitre.org/data/definitions/522.html
@@ -0,0 +1,45 @@
1
+ rules:
2
+ - id: auth.py.oauth.static-state
3
+ languages:
4
+ - python
5
+ severity: WARNING
6
+ message: |
7
+ OAuth authorization request sends a hardcoded, constant `state`
8
+ value. A static `state` provides ZERO CSRF protection — the whole
9
+ point is an unguessable, per-request value that you store and then
10
+ compare on the callback. A literal that ships in your source is known
11
+ to everyone and identical on every request, so an attacker can forge
12
+ a matching callback.
13
+
14
+ Generate `state` fresh per request from a CSPRNG
15
+ (`secrets.token_urlsafe(32)`), persist it in the session, and verify
16
+ it when the provider redirects back.
17
+ pattern-either:
18
+ # Authorize parameter dict carrying a `response_type` (so we know it is an
19
+ # authorize request, not some unrelated `state` field) AND a string-LITERAL
20
+ # `state`. A per-request value is a variable or a function call, which is
21
+ # not a quoted literal and so does not match.
22
+ - patterns:
23
+ - pattern: '{..., "response_type": ..., ...}'
24
+ - pattern-either:
25
+ - pattern: '{..., "state": "$S", ...}'
26
+ # Inline authorize URL string literal carrying both response_type and a
27
+ # constant state value. A dynamic state would be an f-string with `{state}`,
28
+ # whose `{` is excluded from the value character class below.
29
+ - patterns:
30
+ - pattern-regex: |-
31
+ ['"]https?://[^'"\s]+\?[^'"]*response_type=[^'"]*['"]
32
+ - pattern-regex: |-
33
+ ['"]https?://[^'"\s]+\?[^'"]*state=[A-Za-z0-9._~%-]+[^'"]*['"]
34
+ metadata:
35
+ oauthlint-rule-id: AUTH-PY-OAUTH-003
36
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-oauth-static-state
37
+ category: security
38
+ cwe: CWE-330
39
+ owasp: API1:2023
40
+ llm-prevalence: MEDIUM
41
+ technology:
42
+ - oauth2
43
+ references:
44
+ - https://datatracker.ietf.org/doc/html/rfc6749#section-10.12
45
+ - https://cwe.mitre.org/data/definitions/330.html
@@ -0,0 +1,41 @@
1
+ rules:
2
+ - id: auth.py.oauth.token-request-verify-disabled
3
+ languages:
4
+ - python
5
+ severity: ERROR
6
+ message: |
7
+ An OAuth client fetches or refreshes a token with TLS certificate
8
+ verification disabled (`verify=False`). The token request carries the
9
+ `client_secret`, the authorization `code`, and the issued access/refresh
10
+ tokens; with verification off, an attacker who can intercept the
11
+ connection presents any certificate and reads or tampers with them — a
12
+ classic man-in-the-middle on the most sensitive call in the flow
13
+ (CWE-295).
14
+
15
+ Never pass `verify=False` to `fetch_token` / `refresh_token` /
16
+ `fetch_access_token` (Authlib, requests-oauthlib). Leave verification on
17
+ (the default) so the system CA bundle is used, or point `verify` at a CA
18
+ bundle path for a private CA.
19
+ # Scoped to OAuth client token methods (Authlib OAuth1/2 sessions,
20
+ # requests-oauthlib) and the literal `verify=False`. `verify=True` and
21
+ # `verify="/path/ca.pem"` are not flagged. This is distinct from
22
+ # auth.py.flow.requests-verify-disabled, which covers the `requests` HTTP
23
+ # verbs (`get`/`post`/…); here the sink is the OAuth token-exchange call.
24
+ pattern-either:
25
+ - pattern: $C.fetch_token(..., verify=False, ...)
26
+ - pattern: $C.refresh_token(..., verify=False, ...)
27
+ - pattern: $C.fetch_access_token(..., verify=False, ...)
28
+ metadata:
29
+ oauthlint-rule-id: AUTH-PY-OAUTH-005
30
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-oauth-token-request-verify-disabled
31
+ category: security
32
+ cwe: CWE-295
33
+ owasp: A02:2021
34
+ llm-prevalence: MEDIUM
35
+ technology:
36
+ - authlib
37
+ - requests-oauthlib
38
+ references:
39
+ - https://docs.authlib.org/en/latest/client/oauth2.html
40
+ - https://requests.readthedocs.io/en/latest/user/advanced/#ssl-cert-verification
41
+ - https://cwe.mitre.org/data/definitions/295.html
@@ -0,0 +1,39 @@
1
+ rules:
2
+ - id: auth.rust.oauth.insecure-token-endpoint
3
+ languages:
4
+ - rust
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
10
+ or rewrite them and take over the flow.
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
+ (including the `oauth2` crate's `AuthUrl` / `TokenUrl`).
15
+ `http://localhost` is fine for local development and is not flagged.
16
+ # A string literal that targets an OAuth/OIDC endpoint over http://.
17
+ # Required OAuth markers keep this precise: a generic http URL is NOT
18
+ # flagged, only one carrying an authorize/token request or an /oauth path.
19
+ # `https://` cannot match, and the localhost / loopback dev hosts are
20
+ # subtracted below.
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-RUST-OAUTH-002
28
+ oauthlint-doc-url: https://oauthlint.dev/rules/rust-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,57 @@
1
+ rules:
2
+ - id: auth.rust.oauth.ropc-grant
3
+ languages:
4
+ - rust
5
+ severity: ERROR
6
+ message: |
7
+ OAuth token request uses the Resource Owner Password Credentials
8
+ grant (`grant_type=password`). The app collects the user's password
9
+ and replays it to the authorization server — exactly what OAuth was
10
+ designed to avoid. It cannot support federation, MFA, or step-up
11
+ auth, and any compromise of your service exposes raw user passwords.
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
16
+ `client_credentials` for machine-to-machine. With the `oauth2` crate,
17
+ use `authorize_url` / `exchange_code` instead of `exchange_password`.
18
+ pattern-either:
19
+ # oauth2 crate ROPC helper — its only purpose is the password grant.
20
+ - pattern: $C.exchange_password(...)
21
+ # reqwest / hand-built form pair: ("grant_type", "password"). The value
22
+ # is bounded so `password_reset` and friends are not matched.
23
+ - pattern-regex: |-
24
+ "grant_type"\s*,\s*"password"
25
+ # URL-encoded request body string: "grant_type=password&username=…".
26
+ - pattern-regex: |-
27
+ [?&"]grant_type=password(?:[&"\s\\]|$)
28
+ # We flag an application sending ROPC, not the example/test code or vendored
29
+ # client libraries that legitimately exercise the grant. Excluding these
30
+ # trees keeps the signal on first-party application code. (`tests.rs` is the
31
+ # Rust convention for a sibling test module file, e.g. oauth2-rs'
32
+ # `src/token/tests.rs`.)
33
+ paths:
34
+ exclude:
35
+ - "**/test/**"
36
+ - "**/tests.rs"
37
+ - "**/*_test.*"
38
+ - "**/*.test.*"
39
+ - "**/example/**"
40
+ - "**/examples/**"
41
+ - "**/mock*/**"
42
+ - "**/vendor/**"
43
+ - "**/node_modules/**"
44
+ - "**/target/**"
45
+ metadata:
46
+ oauthlint-rule-id: AUTH-RUST-OAUTH-001
47
+ oauthlint-doc-url: https://oauthlint.dev/rules/rust-oauth-ropc-grant
48
+ category: security
49
+ cwe: CWE-522
50
+ owasp: API2:2023
51
+ llm-prevalence: MEDIUM
52
+ technology:
53
+ - oauth2
54
+ references:
55
+ - https://datatracker.ietf.org/doc/html/rfc9700#section-2.4
56
+ - https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1#section-2.4
57
+ - https://cwe.mitre.org/data/definitions/522.html