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,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,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
@@ -0,0 +1,38 @@
1
+ rules:
2
+ - id: auth.rust.oauth.static-state
3
+ languages:
4
+ - rust
5
+ severity: WARNING
6
+ message: |
7
+ OAuth authorization request is built with a hardcoded, constant `state`
8
+ value (`CsrfToken::new("literal")`). A static `state` provides ZERO CSRF
9
+ protection — the whole point is an unguessable, per-request value that
10
+ you store and then compare on the callback. A literal that ships in your
11
+ source is known to everyone and identical on every request, so an
12
+ attacker can forge a matching callback.
13
+
14
+ Use `CsrfToken::new_random` (the `oauth2` crate's CSPRNG-backed
15
+ generator), pass it to `authorize_url`, persist it in the session, and
16
+ verify it when the provider redirects back.
17
+ # `CsrfToken::new` with a STRING LITERAL is a constant state baked into the
18
+ # source. The four conversion forms below match only when the literal is the
19
+ # direct argument, so `CsrfToken::new(generate())` or a variable is never
20
+ # flagged. `CsrfToken::new_random()` is a different method and is not
21
+ # matched.
22
+ pattern-either:
23
+ - pattern: CsrfToken::new("...".to_string())
24
+ - pattern: CsrfToken::new("...".to_owned())
25
+ - pattern: CsrfToken::new("...".into())
26
+ - pattern: CsrfToken::new(String::from("..."))
27
+ metadata:
28
+ oauthlint-rule-id: AUTH-RUST-OAUTH-003
29
+ oauthlint-doc-url: https://oauthlint.dev/rules/rust-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