oauthlint-rules 0.2.5 → 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 (40) 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/flow/secret-in-response.yml +70 -0
  7. package/rules/go/cookie/insecure.yml +15 -6
  8. package/rules/go/flow/oauth-credential-in-log.yml +90 -0
  9. package/rules/go/flow/secret-in-response.yml +69 -0
  10. package/rules/go/jwt/untrusted-verify-key.yml +68 -0
  11. package/rules/go/oauth/insecure-token-endpoint.yml +39 -0
  12. package/rules/go/oauth/ropc-grant.yml +49 -0
  13. package/rules/go/oauth/static-state.yml +39 -0
  14. package/rules/go/tls/insecure-skip-verify.yml +14 -3
  15. package/rules/go/tls/min-version.yml +12 -3
  16. package/rules/java/cors/credentialed-wildcard.yml +44 -0
  17. package/rules/java/jwt/none-algorithm.yml +37 -0
  18. package/rules/java/jwt/untrusted-verify-key.yml +54 -0
  19. package/rules/java/oauth/insecure-token-endpoint.yml +39 -0
  20. package/rules/java/oauth/ropc-grant.yml +45 -0
  21. package/rules/java/oauth/static-state.yml +38 -0
  22. package/rules/java/web/permit-all-actuator.yml +43 -0
  23. package/rules/jwt/ignore-expiration.yml +21 -11
  24. package/rules/jwt/untrusted-verify-key.yml +77 -0
  25. package/rules/oauth/insecure-token-endpoint.yml +41 -0
  26. package/rules/oauth/ropc-grant.yml +43 -0
  27. package/rules/oauth/static-state.yml +49 -0
  28. package/rules/py/flow/oauth-credential-in-log.yml +91 -0
  29. package/rules/py/flow/secret-in-response.yml +88 -0
  30. package/rules/py/jwt/algorithm-confusion.yml +48 -0
  31. package/rules/py/jwt/untrusted-verify-key.yml +75 -0
  32. package/rules/py/oauth/insecure-token-endpoint.yml +40 -0
  33. package/rules/py/oauth/insecure-transport-env.yml +43 -0
  34. package/rules/py/oauth/ropc-grant.yml +50 -0
  35. package/rules/py/oauth/static-state.yml +45 -0
  36. package/rules/py/oauth/token-request-verify-disabled.yml +41 -0
  37. package/rules/rust/jwt/algorithm-confusion.yml +50 -0
  38. package/rules/rust/oauth/insecure-token-endpoint.yml +39 -0
  39. package/rules/rust/oauth/ropc-grant.yml +40 -0
  40. package/rules/rust/oauth/static-state.yml +38 -0
@@ -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,88 @@
1
+ rules:
2
+ - id: auth.py.flow.secret-in-response
3
+ languages:
4
+ - python
5
+ severity: ERROR
6
+ message: |
7
+ A server-side secret read from the environment flows into an HTTP
8
+ response sent back to the client — leaking it (CWE-200). Anything you
9
+ return from a Flask view (via `jsonify(...)`, `make_response(...)`,
10
+ `Response(...)`) is visible to every caller, so an env value whose name
11
+ looks like a credential (`API_KEY`, `CLIENT_SECRET`, `*_TOKEN`,
12
+ `*_PASSWORD`, a private key, an access key, ...) must never reach it.
13
+
14
+ Never send a server secret to the client. Return only the data the
15
+ caller needs; keep credentials server-side. If a value genuinely must be
16
+ surfaced, redact or mask it first (`redact(...)` / `mask_secret(...)`),
17
+ and prefer exposing only public configuration (names prefixed
18
+ `PUBLIC_` / `NEXT_PUBLIC_` / `VITE_`) to clients.
19
+ # REVERSE-direction taint: the SOURCE is the secret (an os.environ / getenv
20
+ # value whose name looks like a credential), the SINK is the HTTP response.
21
+ # Taint mode so indirection (secret = os.getenv('CLIENT_SECRET');
22
+ # return jsonify(secret=secret)) is caught, not just the inline form.
23
+ # `metavariable-regex` constrains the env var NAME to credential-looking
24
+ # words while excluding public-by-convention prefixes, and passing the value
25
+ # through a redaction/mask helper clears the taint. The sink focuses the
26
+ # response value argument. `by-side-effect` is unsupported on semgrep 1.157,
27
+ # so the source is constrained inline rather than via a propagator.
28
+ mode: taint
29
+ pattern-sources:
30
+ - patterns:
31
+ - pattern-either:
32
+ - pattern: os.environ[$KEY]
33
+ - pattern: os.environ.get($KEY)
34
+ - pattern: 'os.environ.get($KEY, ...)'
35
+ - pattern: os.getenv($KEY)
36
+ - pattern: 'os.getenv($KEY, ...)'
37
+ # Qualified `from os import environ, getenv` forms.
38
+ - pattern: environ[$KEY]
39
+ - pattern: environ.get($KEY)
40
+ - pattern: 'environ.get($KEY, ...)'
41
+ - pattern: getenv($KEY)
42
+ - pattern: 'getenv($KEY, ...)'
43
+ # $KEY must look like a credential AND must not be a public-by-
44
+ # convention name. A single regex: a negative lookahead drops the
45
+ # PUBLIC_/NEXT_PUBLIC_/VITE_ prefixes (so NEXT_PUBLIC_API_KEY is NOT a
46
+ # source), then a credential keyword must appear. The optional leading
47
+ # quote tolerates semgrep binding the string literal with or without
48
+ # its quotes; `(?i)` makes the whole match case-insensitive.
49
+ - metavariable-regex:
50
+ metavariable: $KEY
51
+ regex: '(?i)^[''"]?(?!(?:NEXT_PUBLIC_|VITE_|PUBLIC_))\w*(?:secret|password|passwd|token|api[_-]?key|private[_-]?key|client[_-]?secret|credential|access[_-]?key)'
52
+ pattern-sanitizers:
53
+ # Redaction / masking helpers: a secret passed through one of these is no
54
+ # longer sensitive, so returning the result is safe.
55
+ - pattern: redact(...)
56
+ - pattern: mask(...)
57
+ - pattern: mask_secret(...)
58
+ pattern-sinks:
59
+ - patterns:
60
+ - pattern-either:
61
+ # Flask response constructors — the reliable sinks. Each covers a
62
+ # positional value and a keyword value (jsonify(secret=...)), and
63
+ # taint reaching a value nested inside (e.g. a dict) still fires.
64
+ - pattern: 'jsonify(..., $X, ...)'
65
+ - pattern: 'jsonify(..., $K=$X, ...)'
66
+ - pattern: 'flask.jsonify(..., $X, ...)'
67
+ - pattern: 'flask.jsonify(..., $K=$X, ...)'
68
+ - pattern: 'make_response(..., $X, ...)'
69
+ - pattern: 'make_response(..., $K=$X, ...)'
70
+ - pattern: 'flask.make_response(..., $X, ...)'
71
+ - pattern: 'flask.make_response(..., $K=$X, ...)'
72
+ - pattern: 'Response(..., $X, ...)'
73
+ - pattern: 'Response(..., $K=$X, ...)'
74
+ - pattern: 'flask.Response(..., $X, ...)'
75
+ - pattern: 'flask.Response(..., $K=$X, ...)'
76
+ - focus-metavariable: $X
77
+ metadata:
78
+ oauthlint-rule-id: AUTH-PY-FLOW-008
79
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-flow-secret-in-response
80
+ category: security
81
+ cwe: CWE-200
82
+ owasp: API3:2023
83
+ llm-prevalence: HIGH
84
+ technology:
85
+ - flask
86
+ references:
87
+ - https://cwe.mitre.org/data/definitions/200.html
88
+ - https://owasp.org/API-Security/editions/2023/en/0xa3-broken-object-property-level-authorization/
@@ -0,0 +1,48 @@
1
+ rules:
2
+ - id: auth.py.jwt.algorithm-confusion
3
+ languages:
4
+ - python
5
+ severity: ERROR
6
+ message: |
7
+ A JWT is decoded with an `algorithms` allowlist that mixes a symmetric
8
+ HMAC algorithm (HS256/HS384/HS512) with an asymmetric one (RS*/ES*/PS*).
9
+ This enables the "algorithm confusion" attack: an attacker who knows your
10
+ RSA/EC PUBLIC key can sign a forged token with HMAC, using that public key
11
+ string as the shared secret. Because HS* is also accepted, PyJWT verifies
12
+ the forgery with the public key as the HMAC secret and treats it as valid
13
+ — a complete authentication bypass.
14
+
15
+ Allow only ONE algorithm family — the one you actually use. If you issue
16
+ RS256 tokens, pin `jwt.decode(token, public_key, algorithms=["RS256"])`
17
+ and never also accept an HS* algorithm with the same verification key.
18
+
19
+ CWE-327: Use of a Broken or Risky Cryptographic Algorithm.
20
+ # Distinct from auth.py.jwt.no-algorithms (which flags a MISSING `algorithms`
21
+ # allowlist): here the allowlist is PRESENT but dangerously MIXES a symmetric
22
+ # (HS*) and an asymmetric (RS*/ES*/PS*) family in the same `jwt.decode` call.
23
+ #
24
+ # A single metavariable-regex over the bound list literal `$ALGS` requires
25
+ # BOTH families to appear (order-independent lookaheads), so single-family
26
+ # lists like ["RS256"] or ["HS256"] never match. `(?s)` lets `.` span a
27
+ # multi-line list. The `import jwt` guard pins this to PyJWT (joserfc and
28
+ # other libs expose a different `jwt.decode`).
29
+ patterns:
30
+ - pattern: jwt.decode(..., algorithms=$ALGS, ...)
31
+ - metavariable-regex:
32
+ metavariable: $ALGS
33
+ regex: (?s)(?=.*['"]HS(256|384|512)['"])(?=.*['"](RS|ES|PS)(256|384|512)['"])
34
+ - pattern-inside: |
35
+ import jwt
36
+ ...
37
+ metadata:
38
+ oauthlint-rule-id: AUTH-PY-JWT-006
39
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-jwt-algorithm-confusion
40
+ category: security
41
+ cwe: CWE-327
42
+ owasp: API2:2023
43
+ llm-prevalence: MEDIUM
44
+ technology:
45
+ - PyJWT
46
+ references:
47
+ - https://cwe.mitre.org/data/definitions/327.html
48
+ - https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
@@ -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,50 @@
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.
25
+ pattern-either:
26
+ # Dict entry `"grant_type": "password"` or keyword `grant_type="password"`.
27
+ # The optional quote after the key covers the dict form's closing `"`.
28
+ - pattern-regex: |-
29
+ grant_type['"]?\s*[:=]\s*['"]password['"]
30
+ # URL-encoded request body: "grant_type=password&username=…". The value is
31
+ # bounded so `grant_type=password_reset` is not flagged.
32
+ - pattern-regex: |-
33
+ [?&'"]grant_type=password(?:[&'"\s]|$)
34
+ # Tuple/list pair form: ("grant_type", "password").
35
+ - pattern-regex: |-
36
+ ['"]grant_type['"]\s*,\s*['"]password['"]
37
+ metadata:
38
+ oauthlint-rule-id: AUTH-PY-OAUTH-001
39
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-oauth-ropc-grant
40
+ category: security
41
+ cwe: CWE-522
42
+ owasp: API2:2023
43
+ llm-prevalence: MEDIUM
44
+ technology:
45
+ - oauth2
46
+ - requests
47
+ references:
48
+ - https://datatracker.ietf.org/doc/html/rfc9700#section-2.4
49
+ - https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1#section-2.4
50
+ - 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,50 @@
1
+ rules:
2
+ - id: auth.rust.jwt.algorithm-confusion
3
+ languages:
4
+ - rust
5
+ severity: ERROR
6
+ message: |
7
+ A `jsonwebtoken` `Validation` accepts a list of algorithms that MIXES an
8
+ HMAC family (`Algorithm::HS256`/`HS384`/`HS512`) with an asymmetric family
9
+ (`Algorithm::RS*`/`ES*`/`PS*`). This is the "algorithm confusion" attack:
10
+ when both families are accepted, an attacker takes your RSA/EC PUBLIC key
11
+ (which is not secret) and signs a forged token with HS*, using the public
12
+ key bytes as the HMAC shared secret. `decode` then verifies that forged
13
+ token as valid, letting the attacker mint arbitrary identities and claims.
14
+
15
+ Pin `validation.algorithms` to a SINGLE family you actually use, e.g.
16
+ `validation.algorithms = vec![Algorithm::RS256];` when your issuer signs
17
+ with RSA, or `vec![Algorithm::HS256]` for a genuinely symmetric secret.
18
+ Never accept an HMAC algorithm alongside an asymmetric one.
19
+
20
+ CWE-327: use of a broken or risky cryptographic algorithm/configuration.
21
+ # Algorithm confusion = the accepted-algorithm list mixes an HMAC variant
22
+ # (HS*) with an asymmetric variant (RS*/ES*/PS*). We bind the algorithm list
23
+ # expression to $VEC — set either via the public `algorithms` field or via a
24
+ # `set_algorithms(...)` call — and require, with two ANDed metavariable-regex
25
+ # constraints, that its source text contains BOTH an `Algorithm::HS*` AND an
26
+ # `Algorithm::RS*|ES*|PS*` variant. Order-independent (no ellipsis-in-macro
27
+ # fragility) and inherently low-FP: a single-family list such as
28
+ # `vec![Algorithm::RS256]` fails the HS* constraint and is never flagged.
29
+ patterns:
30
+ - pattern-either:
31
+ - pattern: $V.algorithms = $VEC;
32
+ - pattern: $V.set_algorithms($VEC)
33
+ - metavariable-regex:
34
+ metavariable: $VEC
35
+ regex: '(?s).*Algorithm::HS(256|384|512)'
36
+ - metavariable-regex:
37
+ metavariable: $VEC
38
+ regex: '(?s).*Algorithm::(RS|ES|PS)(256|384|512)'
39
+ metadata:
40
+ oauthlint-rule-id: AUTH-RUST-JWT-006
41
+ oauthlint-doc-url: https://oauthlint.dev/rules/rust-jwt-algorithm-confusion
42
+ category: security
43
+ cwe: CWE-327
44
+ owasp: API2:2023
45
+ llm-prevalence: MEDIUM
46
+ technology:
47
+ - jsonwebtoken
48
+ references:
49
+ - https://cwe.mitre.org/data/definitions/327.html
50
+ - https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
@@ -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,40 @@
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
+ metadata:
29
+ oauthlint-rule-id: AUTH-RUST-OAUTH-001
30
+ oauthlint-doc-url: https://oauthlint.dev/rules/rust-oauth-ropc-grant
31
+ category: security
32
+ cwe: CWE-522
33
+ owasp: API2:2023
34
+ llm-prevalence: MEDIUM
35
+ technology:
36
+ - oauth2
37
+ references:
38
+ - https://datatracker.ietf.org/doc/html/rfc9700#section-2.4
39
+ - https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1#section-2.4
40
+ - https://cwe.mitre.org/data/definitions/522.html