oauthlint-rules 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/package.json +1 -1
  2. package/rules/cookie/long-lived.yml +2 -2
  3. package/rules/express/cookie-insecure.yml +44 -0
  4. package/rules/flow/insecure-random.yml +5 -5
  5. package/rules/flow/oauth-credential-in-log.yml +9 -8
  6. package/rules/flow/secret-in-log.yml +7 -6
  7. package/rules/go/flow/oauth-credential-in-log.yml +9 -8
  8. package/rules/go/flow/weak-rand.yml +3 -3
  9. package/rules/go/jwt/skip-claims-validation.yml +6 -6
  10. package/rules/go/jwt/unchecked-method.yml +6 -6
  11. package/rules/go/tls/insecure-skip-verify.yml +6 -6
  12. package/rules/java/crypto/noop-password-encoder.yml +38 -0
  13. package/rules/java/web/security-ignoring-all.yml +38 -0
  14. package/rules/java/web/wildcard-permit-all.yml +37 -0
  15. package/rules/jwt/ignore-expiration.yml +4 -4
  16. package/rules/nextauth/hardcoded-secret.yml +62 -0
  17. package/rules/oauth/implicit-flow.yml +3 -3
  18. package/rules/oauth/long-token-lifetime.yml +3 -3
  19. package/rules/oauth/no-pkce.yml +4 -4
  20. package/rules/oauth/token-in-localstorage.yml +6 -7
  21. package/rules/passport/jwt-ignore-expiration.yml +58 -0
  22. package/rules/py/cors/allow-all.yml +8 -7
  23. package/rules/py/cors/fastapi-wildcard-credentials.yml +49 -0
  24. package/rules/py/crypto/passlib-weak-scheme.yml +40 -0
  25. package/rules/py/django/cors-allow-all.yml +34 -0
  26. package/rules/py/drf/default-authentication-empty.yml +32 -0
  27. package/rules/py/drf/default-permission-allowany.yml +35 -0
  28. package/rules/py/drf/view-authentication-disabled.yml +34 -0
  29. package/rules/py/flask/session-cookie-insecure.yml +42 -0
  30. package/rules/py/flow/insecure-random-token.yml +6 -5
  31. package/rules/py/flow/oauth-credential-in-log.yml +9 -8
  32. package/rules/py/flow/requests-verify-disabled.yml +29 -17
  33. package/rules/py/jwt/algorithm-confusion.yml +9 -8
  34. package/rules/py/jwt/verify-claims-disabled.yml +45 -0
  35. package/rules/py/oauth/insecure-transport-env.yml +3 -3
  36. package/rules/py/oauth/token-request-verify-disabled.yml +14 -4
  37. package/rules/rust/crypto/weak-password-hash.yml +6 -5
  38. package/rules/rust/jwt/algorithm-confusion.yml +8 -7
  39. package/rules/rust/jwt/hardcoded-secret.yml +5 -4
  40. package/rules/rust/jwt/no-issuer-validation.yml +7 -7
  41. package/rules/session/hardcoded-secret.yml +3 -3
  42. package/rules/session/no-regeneration.yml +6 -5
  43. package/rules/tls/reject-unauthorized.yml +8 -7
@@ -0,0 +1,40 @@
1
+ rules:
2
+ - id: auth.py.crypto.passlib-weak-scheme
3
+ languages:
4
+ - python
5
+ severity: ERROR
6
+ message: |
7
+ passlib configured with a weak or plaintext password scheme.
8
+
9
+ A `CryptContext` lists a password hashing scheme that is broken for
10
+ storing credentials: unsalted or fast digests (`hex_md5`, `hex_sha1`,
11
+ `hex_sha256`, `md5_crypt`, `ldap_md5`), legacy `des_crypt`, or outright
12
+ `plaintext` / `ldap_plaintext`. These are trivially brute-forced or
13
+ reversed, so any leaked hash exposes the underlying password (CWE-916).
14
+
15
+ Use a slow, salted, memory-hard scheme as the default, e.g.
16
+ `CryptContext(schemes=["argon2"])` or
17
+ `CryptContext(schemes=["bcrypt"])`. Keep a weak scheme only as a
18
+ `deprecated` verifier during migration, never as an active hashing scheme.
19
+ # Match the passlib `CryptContext(schemes=[...])` config where any element of
20
+ # the schemes list is in a CLOSED list of genuinely broken schemes. The
21
+ # `"$SCHEME"` string-literal metavariable captures each list element's text,
22
+ # and the anchored metavariable-regex matches ONLY the weak names, so modern
23
+ # schemes (bcrypt, argon2, scrypt, pbkdf2_sha256, ...) never fire.
24
+ patterns:
25
+ - pattern: CryptContext(..., schemes=[..., "$SCHEME", ...], ...)
26
+ - metavariable-regex:
27
+ metavariable: $SCHEME
28
+ regex: ^(md5_crypt|des_crypt|plaintext|hex_md5|hex_sha1|hex_sha256|ldap_md5|ldap_plaintext)$
29
+ metadata:
30
+ oauthlint-rule-id: AUTH-PY-CRYPTO-001
31
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-crypto-passlib-weak-scheme
32
+ category: security
33
+ cwe: CWE-916
34
+ owasp: A02:2021
35
+ llm-prevalence: MEDIUM
36
+ technology:
37
+ - passlib
38
+ references:
39
+ - https://passlib.readthedocs.io/en/stable/lib/passlib.context.html
40
+ - https://cwe.mitre.org/data/definitions/916.html
@@ -0,0 +1,34 @@
1
+ rules:
2
+ - id: auth.py.django.cors-allow-all
3
+ languages:
4
+ - python
5
+ severity: WARNING
6
+ message: |
7
+ django-cors-headers is configured to allow every origin, disabling
8
+ cross-origin access control.
9
+
10
+ `CORS_ALLOW_ALL_ORIGINS = True` (or the legacy `CORS_ORIGIN_ALLOW_ALL =
11
+ True`) reflects any site's `Origin`, so ANY website can make cross-origin
12
+ requests to your API; combined with credentialed sessions this leaks
13
+ cookies, tokens and CSRF protections cross-origin (CWE-942, OWASP
14
+ A05:2021). Set it to `False` and list trusted origins explicitly, e.g.
15
+ `CORS_ALLOWED_ORIGINS = ["https://app.example.com"]`.
16
+ # Matches ONLY the literal `True` on the django-cors-headers settings keys.
17
+ # `= False` and an explicit `CORS_ALLOWED_ORIGINS = [...]` allow-list are
18
+ # never flagged. Distinct from `auth.py.cors.allow-all`, which targets the
19
+ # Flask-CORS `CORS(...)` / `@cross_origin(...)` call forms.
20
+ pattern-either:
21
+ - pattern: CORS_ALLOW_ALL_ORIGINS = True
22
+ - pattern: CORS_ORIGIN_ALLOW_ALL = True
23
+ metadata:
24
+ oauthlint-rule-id: AUTH-PY-DJANGO-001
25
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-django-cors-allow-all
26
+ category: security
27
+ cwe: CWE-942
28
+ owasp: A05:2021
29
+ llm-prevalence: MEDIUM
30
+ technology:
31
+ - django-cors-headers
32
+ references:
33
+ - https://github.com/adamchainz/django-cors-headers#cors_allow_all_origins-bool
34
+ - https://cwe.mitre.org/data/definitions/942.html
@@ -0,0 +1,32 @@
1
+ rules:
2
+ - id: auth.py.drf.default-authentication-empty
3
+ languages:
4
+ - python
5
+ severity: ERROR
6
+ message: |
7
+ DRF disables authentication globally with an empty
8
+ `DEFAULT_AUTHENTICATION_CLASSES` list.
9
+
10
+ An empty list means no authentication scheme runs, so `request.user` is
11
+ never populated from a credential and every view falls back to anonymous
12
+ access (CWE-306, OWASP A01:2021). Permission checks that rely on
13
+ `request.user` being authenticated then have nothing to enforce against.
14
+
15
+ Populate the list with the schemes you use, for example
16
+ `rest_framework.authentication.SessionAuthentication` and
17
+ `rest_framework.authentication.TokenAuthentication`.
18
+ # Scoped to the `REST_FRAMEWORK` settings dict; matches ONLY the empty list.
19
+ # A populated list such as `[SessionAuthentication]` is not matched.
20
+ pattern: 'REST_FRAMEWORK = {..., "DEFAULT_AUTHENTICATION_CLASSES": [], ...}'
21
+ metadata:
22
+ oauthlint-rule-id: AUTH-PY-DRF-002
23
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-drf-default-authentication-empty
24
+ category: security
25
+ cwe: CWE-306
26
+ owasp: A01:2021
27
+ llm-prevalence: MEDIUM
28
+ technology:
29
+ - djangorestframework
30
+ references:
31
+ - https://www.django-rest-framework.org/api-guide/authentication/#setting-the-authentication-scheme
32
+ - https://cwe.mitre.org/data/definitions/306.html
@@ -0,0 +1,35 @@
1
+ rules:
2
+ - id: auth.py.drf.default-permission-allowany
3
+ languages:
4
+ - python
5
+ severity: ERROR
6
+ message: |
7
+ DRF makes every endpoint public because `DEFAULT_PERMISSION_CLASSES` is set
8
+ to `AllowAny`.
9
+
10
+ With `AllowAny` as the project-wide default, every view that does not
11
+ override `permission_classes` skips authorization entirely, so any
12
+ unauthenticated caller can reach it (CWE-862, OWASP A01:2021). This is easy
13
+ to ship by accident because it silently exposes future endpoints too.
14
+
15
+ Set the global default to a real permission such as
16
+ `rest_framework.permissions.IsAuthenticated` and opt specific views out to
17
+ public only when you mean to.
18
+ # Scoped to the `REST_FRAMEWORK` settings dict. Matches `AllowAny` both as the
19
+ # imported symbol and as the `'rest_framework.permissions.AllowAny'` string.
20
+ # `IsAuthenticated` (or any other class), or the key being absent, is not matched.
21
+ pattern-either:
22
+ - pattern: 'REST_FRAMEWORK = {..., "DEFAULT_PERMISSION_CLASSES": [..., AllowAny, ...], ...}'
23
+ - pattern: 'REST_FRAMEWORK = {..., "DEFAULT_PERMISSION_CLASSES": [..., "rest_framework.permissions.AllowAny", ...], ...}'
24
+ metadata:
25
+ oauthlint-rule-id: AUTH-PY-DRF-001
26
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-drf-default-permission-allowany
27
+ category: security
28
+ cwe: CWE-862
29
+ owasp: A01:2021
30
+ llm-prevalence: HIGH
31
+ technology:
32
+ - djangorestframework
33
+ references:
34
+ - https://www.django-rest-framework.org/api-guide/permissions/#setting-the-permission-policy
35
+ - https://cwe.mitre.org/data/definitions/862.html
@@ -0,0 +1,34 @@
1
+ rules:
2
+ - id: auth.py.drf.view-authentication-disabled
3
+ languages:
4
+ - python
5
+ severity: ERROR
6
+ message: |
7
+ A DRF view disables authentication with an empty `authentication_classes`
8
+ list.
9
+
10
+ Setting `authentication_classes = []` on a view (or the
11
+ `@authentication_classes([])` decorator on a function view) turns off every
12
+ authentication scheme for that endpoint, so `request.user` is always
13
+ anonymous and any permission tied to an authenticated user cannot hold
14
+ (CWE-306, OWASP A01:2021).
15
+
16
+ List the schemes the view should accept, for example
17
+ `authentication_classes = [TokenAuthentication]`, instead of emptying it.
18
+ # Matches ONLY an explicitly emptied `authentication_classes`, as a class
19
+ # attribute or via the decorator. A populated list is not matched.
20
+ pattern-either:
21
+ - pattern: authentication_classes = []
22
+ - pattern: "@authentication_classes([])"
23
+ metadata:
24
+ oauthlint-rule-id: AUTH-PY-DRF-003
25
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-drf-view-authentication-disabled
26
+ category: security
27
+ cwe: CWE-306
28
+ owasp: A01:2021
29
+ llm-prevalence: MEDIUM
30
+ technology:
31
+ - djangorestframework
32
+ references:
33
+ - https://www.django-rest-framework.org/api-guide/views/#api_view
34
+ - https://cwe.mitre.org/data/definitions/306.html
@@ -0,0 +1,42 @@
1
+ rules:
2
+ - id: auth.py.flask.session-cookie-insecure
3
+ languages:
4
+ - python
5
+ severity: ERROR
6
+ message: |
7
+ A Flask cookie security flag is disabled through `app.config`, weakening
8
+ session and remember-me cookie protection.
9
+
10
+ `SESSION_COOKIE_SECURE = False` lets the session cookie travel over plain
11
+ HTTP where it can be sniffed on the wire, and `SESSION_COOKIE_HTTPONLY =
12
+ False` exposes it to JavaScript so an XSS payload can read and exfiltrate
13
+ it; the `REMEMBER_COOKIE_*` flags do the same for Flask-Login's long-lived
14
+ remember-me token (CWE-614, OWASP A05:2021). Keep these `True` (or drive
15
+ them from an environment check), e.g. `app.config["SESSION_COOKIE_SECURE"]
16
+ = True` and `app.config["SESSION_COOKIE_HTTPONLY"] = True`.
17
+ # Matches ONLY the literal `False` set via `app.config[...] = False` (the
18
+ # subscript form) or `app.config.update(...=False)` (the keyword form) — the
19
+ # `app.config` variants that the bare module-level assignment rule
20
+ # `auth.py.cookie.insecure-flags` does NOT see. `= True` and env-driven
21
+ # values are never flagged. `$APP` matches any Flask app variable name.
22
+ pattern-either:
23
+ - pattern: $APP.config['SESSION_COOKIE_SECURE'] = False
24
+ - pattern: $APP.config['SESSION_COOKIE_HTTPONLY'] = False
25
+ - pattern: $APP.config['REMEMBER_COOKIE_SECURE'] = False
26
+ - pattern: $APP.config['REMEMBER_COOKIE_HTTPONLY'] = False
27
+ - pattern: $APP.config.update(..., SESSION_COOKIE_SECURE=False, ...)
28
+ - pattern: $APP.config.update(..., REMEMBER_COOKIE_SECURE=False, ...)
29
+ metadata:
30
+ oauthlint-rule-id: AUTH-PY-FLASK-001
31
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-flask-session-cookie-insecure
32
+ category: security
33
+ cwe: CWE-614
34
+ owasp: A05:2021
35
+ llm-prevalence: MEDIUM
36
+ technology:
37
+ - flask
38
+ - flask-login
39
+ references:
40
+ - https://flask.palletsprojects.com/en/stable/config/#SESSION_COOKIE_SECURE
41
+ - https://flask-login.readthedocs.io/en/latest/#cookie-settings
42
+ - https://cwe.mitre.org/data/definitions/614.html
@@ -4,11 +4,12 @@ rules:
4
4
  - python
5
5
  severity: ERROR
6
6
  message: |
7
- A security-sensitive value (token, secret, password, OTP, nonce, API key,
8
- reset/verification code) is being generated with the `random` module.
9
- `random` is a pseudo-random number generator seeded from predictable state
10
- and is NOT cryptographically secure its output can be predicted or
11
- reproduced by an attacker, defeating the secret entirely.
7
+ A security-sensitive value is being generated with the `random` module.
8
+ The value is a token, secret, password, OTP, nonce, API key, or
9
+ reset/verification code. `random` is a pseudo-random number generator
10
+ seeded from predictable state and is NOT cryptographically secure: its
11
+ output can be predicted or reproduced by an attacker, defeating the
12
+ secret entirely.
12
13
 
13
14
  Use the `secrets` module or `os.urandom` instead:
14
15
  `secrets.token_urlsafe(32)`, `secrets.token_hex(16)`,
@@ -4,14 +4,15 @@ rules:
4
4
  - python
5
5
  severity: ERROR
6
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).
7
+ An OAuth/OIDC credential from the request flows into a logging call.
8
+ The tainted value is an authorization `code`, an `access_token` /
9
+ `refresh_token` / `id_token`, a bearer `token`, a `client_secret`, or
10
+ the raw `Authorization` header, and the sink is `print`, `logging.*`,
11
+ or a `logger.*` call. Logs are written to files, shipped to aggregators
12
+ (Datadog, Splunk, CloudWatch) and read by people and systems that should
13
+ never see live credentials. A leaked authorization code or token can be
14
+ replayed to impersonate the user or complete the OAuth exchange
15
+ (CWE-532).
15
16
 
16
17
  Never log the raw credential. Redact or mask it before logging, log a
17
18
  non-sensitive identifier instead (a user id, a key id), or drop the field
@@ -19,23 +19,35 @@ rules:
19
19
  # Scoped to `requests.<method>(...)`, `requests.request(...)` and Session
20
20
  # objects (`$SESSION.<method>(...)`). Fires only on the literal
21
21
  # `verify=False`; `verify=True` and `verify="/path/ca.pem"` are not flagged.
22
- pattern-either:
23
- - pattern: requests.get(..., verify=False, ...)
24
- - pattern: requests.post(..., verify=False, ...)
25
- - pattern: requests.put(..., verify=False, ...)
26
- - pattern: requests.delete(..., verify=False, ...)
27
- - pattern: requests.patch(..., verify=False, ...)
28
- - pattern: requests.head(..., verify=False, ...)
29
- - pattern: requests.options(..., verify=False, ...)
30
- - pattern: requests.request(..., verify=False, ...)
31
- - pattern: $SESSION.get(..., verify=False, ...)
32
- - pattern: $SESSION.post(..., verify=False, ...)
33
- - pattern: $SESSION.put(..., verify=False, ...)
34
- - pattern: $SESSION.delete(..., verify=False, ...)
35
- - pattern: $SESSION.patch(..., verify=False, ...)
36
- - pattern: $SESSION.head(..., verify=False, ...)
37
- - pattern: $SESSION.options(..., verify=False, ...)
38
- - pattern: $SESSION.request(..., verify=False, ...)
22
+ patterns:
23
+ - pattern-either:
24
+ - pattern: requests.get(..., verify=$V, ...)
25
+ - pattern: requests.post(..., verify=$V, ...)
26
+ - pattern: requests.put(..., verify=$V, ...)
27
+ - pattern: requests.delete(..., verify=$V, ...)
28
+ - pattern: requests.patch(..., verify=$V, ...)
29
+ - pattern: requests.head(..., verify=$V, ...)
30
+ - pattern: requests.options(..., verify=$V, ...)
31
+ - pattern: requests.request(..., verify=$V, ...)
32
+ - pattern: $SESSION.get(..., verify=$V, ...)
33
+ - pattern: $SESSION.post(..., verify=$V, ...)
34
+ - pattern: $SESSION.put(..., verify=$V, ...)
35
+ - pattern: $SESSION.delete(..., verify=$V, ...)
36
+ - pattern: $SESSION.patch(..., verify=$V, ...)
37
+ - pattern: $SESSION.head(..., verify=$V, ...)
38
+ - pattern: $SESSION.options(..., verify=$V, ...)
39
+ - pattern: $SESSION.request(..., verify=$V, ...)
40
+ # Fire only on the literal `False` (unchanged detection), and focus the
41
+ # match/fix on that value token so the autofix flips just it, leaving the
42
+ # rest of the call intact.
43
+ - metavariable-regex:
44
+ metavariable: $V
45
+ regex: ^False$
46
+ - focus-metavariable: $V
47
+ # Safe, deterministic autofix: `True` is the library default (verification
48
+ # on) and the exact value the rule treats as compliant, so it fully resolves
49
+ # the finding. Only the value token is rewritten (`verify=False` -> `verify=True`).
50
+ fix: "True"
39
51
  metadata:
40
52
  oauthlint-rule-id: AUTH-PY-FLOW-002
41
53
  oauthlint-doc-url: https://oauthlint.dev/rules/py-flow-requests-verify-disabled
@@ -4,15 +4,16 @@ rules:
4
4
  - python
5
5
  severity: ERROR
6
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.
7
+ A JWT is decoded with an `algorithms` allowlist that mixes an HMAC algorithm with an asymmetric one.
8
+ The list combines a symmetric HMAC algorithm (HS256/HS384/HS512) with an
9
+ asymmetric one (RS*/ES*/PS*). This enables the "algorithm confusion"
10
+ attack: an attacker who knows your RSA/EC PUBLIC key can sign a forged
11
+ token with HMAC, using that public key string as the shared secret.
12
+ Because HS* is also accepted, PyJWT verifies the forgery with the public
13
+ key as the HMAC secret and treats it as valid: a complete authentication
14
+ bypass.
14
15
 
15
- Allow only ONE algorithm family the one you actually use. If you issue
16
+ Allow only ONE algorithm family, the one you actually use. If you issue
16
17
  RS256 tokens, pin `jwt.decode(token, public_key, algorithms=["RS256"])`
17
18
  and never also accept an HS* algorithm with the same verification key.
18
19
 
@@ -0,0 +1,45 @@
1
+ rules:
2
+ - id: auth.py.jwt.verify-claims-disabled
3
+ languages:
4
+ - python
5
+ severity: WARNING
6
+ message: |
7
+ PyJWT decode disables audience or issuer checks.
8
+
9
+ `jwt.decode(...)` is called with an `options` dict that turns off a claim
10
+ check: `"verify_aud": False`, `"verify_iss": False`, or
11
+ `"verify_nbf": False`. Skipping these lets a token minted for a different
12
+ audience or issuer (for example one from another tenant or a lower-trust
13
+ service) be accepted here, defeating the boundary those claims are meant
14
+ to enforce (CWE-347).
15
+
16
+ Remove the disabling option and validate the claim, e.g.
17
+ `jwt.decode(token, key, algorithms=["RS256"], audience="api",
18
+ issuer="https://issuer.example.com")`. PyJWT only checks `aud`/`iss` when
19
+ you pass the expected value, so supply it rather than disabling the check.
20
+ # Scoped to PyJWT's decode `options` dict. We match ONLY the aud/iss/nbf
21
+ # keys; `verify_signature` is reported by auth.py.jwt.no-verify and
22
+ # `verify_exp` by auth.py.jwt.no-expiration, so neither is matched here and
23
+ # we avoid duplicate findings.
24
+ patterns:
25
+ - pattern-either:
26
+ - pattern: jwt.decode(..., options=$OPTS, ...)
27
+ - pattern: decode(..., options=$OPTS, ...)
28
+ - metavariable-pattern:
29
+ metavariable: $OPTS
30
+ pattern-either:
31
+ - pattern: '{..., "verify_aud": False, ...}'
32
+ - pattern: '{..., "verify_iss": False, ...}'
33
+ - pattern: '{..., "verify_nbf": False, ...}'
34
+ metadata:
35
+ oauthlint-rule-id: AUTH-PY-JWT-008
36
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-jwt-verify-claims-disabled
37
+ category: security
38
+ cwe: CWE-347
39
+ owasp: API2:2023
40
+ llm-prevalence: MEDIUM
41
+ technology:
42
+ - pyjwt
43
+ references:
44
+ - https://pyjwt.readthedocs.io/en/stable/api.html#jwt.decode
45
+ - https://cwe.mitre.org/data/definitions/347.html
@@ -4,9 +4,9 @@ rules:
4
4
  - python
5
5
  severity: ERROR
6
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
7
+ `OAUTHLIB_INSECURE_TRANSPORT` is set, disabling oauthlib's HTTPS
8
+ requirement for OAuth flows. This affects `requests-oauthlib`, Authlib's
9
+ requests integration, and Django OAuth Toolkit. oauthlib raises
10
10
  `InsecureTransportError` to stop you exchanging codes and tokens over
11
11
  cleartext; setting this variable silences that guard, so authorization
12
12
  codes, `client_secret`, and access/refresh tokens travel over plain
@@ -21,10 +21,20 @@ rules:
21
21
  # `verify="/path/ca.pem"` are not flagged. This is distinct from
22
22
  # auth.py.flow.requests-verify-disabled, which covers the `requests` HTTP
23
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, ...)
24
+ patterns:
25
+ - pattern-either:
26
+ - pattern: $C.fetch_token(..., verify=$V, ...)
27
+ - pattern: $C.refresh_token(..., verify=$V, ...)
28
+ - pattern: $C.fetch_access_token(..., verify=$V, ...)
29
+ # Fire only on the literal `False` (unchanged detection), and focus the
30
+ # match/fix on that value token so the autofix flips just it.
31
+ - metavariable-regex:
32
+ metavariable: $V
33
+ regex: ^False$
34
+ - focus-metavariable: $V
35
+ # Safe, deterministic autofix: `True` is the default (verification on) and the
36
+ # exact value the rule treats as compliant, so it resolves the finding.
37
+ fix: "True"
28
38
  metadata:
29
39
  oauthlint-rule-id: AUTH-PY-OAUTH-005
30
40
  oauthlint-doc-url: https://oauthlint.dev/rules/py-oauth-token-request-verify-disabled
@@ -4,11 +4,12 @@ rules:
4
4
  - rust
5
5
  severity: ERROR
6
6
  message: |
7
- A password is being hashed with a fast, general-purpose digest (MD5 via
8
- the `md5` crate, or SHA-1/SHA-256/SHA-512 via the RustCrypto `sha1`/`sha2`
9
- crates). These algorithms are designed to be fast, which makes offline
10
- brute-force and rainbow-table attacks cheap they are NOT suitable for
11
- storing passwords (CWE-916).
7
+ A password is hashed with a fast, general-purpose digest unsuitable for
8
+ password storage. The digest is MD5 (via the `md5` crate) or
9
+ SHA-1/SHA-256/SHA-512 (via the RustCrypto `sha1`/`sha2` crates). These
10
+ algorithms are designed to be fast, which makes offline brute-force and
11
+ rainbow-table attacks cheap; they are NOT suitable for storing passwords
12
+ (CWE-916).
12
13
 
13
14
  Use a dedicated, slow password-hashing function with a per-password salt
14
15
  and a tunable work factor: Argon2 (`argon2` crate,
@@ -4,13 +4,14 @@ rules:
4
4
  - rust
5
5
  severity: ERROR
6
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.
7
+ A `jsonwebtoken` `Validation` accepts both HMAC and asymmetric algorithms,
8
+ enabling algorithm confusion. The accepted-algorithm list MIXES an HMAC
9
+ family (`Algorithm::HS256`/`HS384`/`HS512`) with an asymmetric family
10
+ (`Algorithm::RS*`/`ES*`/`PS*`). When both families are accepted, an
11
+ attacker takes your RSA/EC PUBLIC key (which is not secret) and signs a
12
+ forged token with HS*, using the public key bytes as the HMAC shared
13
+ secret. `decode` then verifies that forged token as valid, letting the
14
+ attacker mint arbitrary identities and claims.
14
15
 
15
16
  Pin `validation.algorithms` to a SINGLE family you actually use, e.g.
16
17
  `validation.algorithms = vec![Algorithm::RS256];` when your issuer signs
@@ -4,10 +4,11 @@ rules:
4
4
  - rust
5
5
  severity: ERROR
6
6
  message: |
7
- A JWT HMAC signing/verification key is hardcoded as a literal in a call
8
- to jsonwebtoken's `EncodingKey::from_secret` / `DecodingKey::from_secret`.
9
- Anyone who can read the source or git history can forge or tamper with
10
- tokens, which is a complete authentication bypass.
7
+ A JWT HMAC signing/verification key is hardcoded as a literal. It is
8
+ passed directly to jsonwebtoken's `EncodingKey::from_secret` /
9
+ `DecodingKey::from_secret`. Anyone who can read the source or git history
10
+ can forge or tamper with tokens, which is a complete authentication
11
+ bypass.
11
12
 
12
13
  Load the secret at runtime from the environment or a secret manager
13
14
  instead, e.g. `let key = std::env::var("JWT_SECRET")?;` followed by
@@ -4,13 +4,13 @@ rules:
4
4
  - rust
5
5
  severity: WARNING
6
6
  message: |
7
- A JWT is decoded with a `jsonwebtoken` `Validation` that never sets the
8
- expected issuer, so `decode` accepts a token minted by ANY issuer. The
9
- `jsonwebtoken` crate does not validate the `iss` claim unless you opt in,
10
- so a token signed by an attacker-controlled or otherwise untrusted issuer
11
- passes validation as long as the signature checks out. For OAuth/OIDC this
12
- lets a token from the wrong authorization server be replayed against this
13
- API.
7
+ A JWT is decoded with a `jsonwebtoken` `Validation` that never sets the expected issuer.
8
+ Because the issuer is not pinned, `decode` accepts a token minted by ANY
9
+ issuer: the `jsonwebtoken` crate does not validate the `iss` claim unless
10
+ you opt in, so a token signed by an attacker-controlled or otherwise
11
+ untrusted issuer passes validation as long as the signature checks out.
12
+ For OAuth/OIDC this lets a token from the wrong authorization server be
13
+ replayed against this API.
14
14
 
15
15
  Pin the issuer before decoding, e.g.
16
16
  `validation.set_issuer(&["https://issuer.example.com"])` (or set
@@ -5,9 +5,9 @@ rules:
5
5
  - typescript
6
6
  severity: ERROR
7
7
  message: |
8
- An `express-session` / `cookie-session` `secret` is set to a hard-coded
9
- string literal (the infamous `secret: 'keyboard cat'` is the canonical
10
- AI-generated example). This key signs the session cookie: anyone who
8
+ An `express-session` / `cookie-session` `secret` is a hard-coded string
9
+ literal. The infamous `secret: 'keyboard cat'` is the canonical
10
+ AI-generated example. This key signs the session cookie: anyone who
11
11
  reads it from your source or git history can forge arbitrary session
12
12
  cookies and impersonate any user.
13
13
 
@@ -5,11 +5,12 @@ rules:
5
5
  - typescript
6
6
  severity: WARNING
7
7
  message: |
8
- The user is being marked as logged in (`req.session.user = ...`,
9
- `req.session.userId = ...`, etc.) WITHOUT first regenerating the
10
- session id. This opens the door to session-fixation attacks: an
11
- attacker who plants a known session id in the victim's browser
12
- before login retains access after authentication succeeds.
8
+ The user is marked as logged in without first regenerating the session id.
9
+ The identity is written onto the session (`req.session.user = ...`,
10
+ `req.session.userId = ...`, etc.) before the session id is rotated. This
11
+ opens the door to session-fixation attacks: an attacker who plants a
12
+ known session id in the victim's browser before login retains access
13
+ after authentication succeeds.
13
14
 
14
15
  Call `req.session.regenerate(cb)` (or your framework's equivalent)
15
16
  between authenticating the credentials and writing the user
@@ -5,16 +5,17 @@ rules:
5
5
  - typescript
6
6
  severity: ERROR
7
7
  message: |
8
- Disabling TLS certificate validation (`rejectUnauthorized: false` or
9
- `NODE_TLS_REJECT_UNAUTHORIZED=0`) makes the connection accept ANY
10
- certificate, including self-signed or attacker-supplied ones. This
11
- removes the protection TLS provides against man-in-the-middle attacks:
12
- anyone able to intercept the network path can present a forged
13
- certificate, then read and modify the traffic (credentials, tokens, data).
8
+ TLS certificate validation is disabled for this connection.
9
+ Setting `rejectUnauthorized: false` or `NODE_TLS_REJECT_UNAUTHORIZED=0`
10
+ makes the connection accept ANY certificate, including self-signed or
11
+ attacker-supplied ones. This removes the protection TLS provides against
12
+ man-in-the-middle attacks: anyone able to intercept the network path can
13
+ present a forged certificate, then read and modify the traffic
14
+ (credentials, tokens, data).
14
15
 
15
16
  Keep certificate validation enabled. If the server uses a private or
16
17
  self-signed CA, supply that CA explicitly instead of turning validation
17
- off e.g. `new https.Agent({ ca: fs.readFileSync('ca.pem') })`. See
18
+ off, e.g. `new https.Agent({ ca: fs.readFileSync('ca.pem') })`. See
18
19
  CWE-295 and the Node.js TLS docs.
19
20
  # Matches an options object literal that contains `rejectUnauthorized: false`,
20
21
  # wherever it appears (https.request, new https.Agent, tls.connect, axios