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.
- package/package.json +1 -1
- package/rules/cookie/long-lived.yml +2 -2
- package/rules/express/cookie-insecure.yml +44 -0
- package/rules/flow/insecure-random.yml +5 -5
- package/rules/flow/oauth-credential-in-log.yml +9 -8
- package/rules/flow/secret-in-log.yml +7 -6
- package/rules/go/flow/oauth-credential-in-log.yml +9 -8
- package/rules/go/flow/weak-rand.yml +3 -3
- package/rules/go/jwt/skip-claims-validation.yml +6 -6
- package/rules/go/jwt/unchecked-method.yml +6 -6
- package/rules/go/tls/insecure-skip-verify.yml +6 -6
- package/rules/java/crypto/noop-password-encoder.yml +38 -0
- package/rules/java/web/security-ignoring-all.yml +38 -0
- package/rules/java/web/wildcard-permit-all.yml +37 -0
- package/rules/jwt/ignore-expiration.yml +4 -4
- package/rules/nextauth/hardcoded-secret.yml +62 -0
- package/rules/oauth/implicit-flow.yml +3 -3
- package/rules/oauth/long-token-lifetime.yml +3 -3
- package/rules/oauth/no-pkce.yml +4 -4
- package/rules/oauth/token-in-localstorage.yml +6 -7
- package/rules/passport/jwt-ignore-expiration.yml +58 -0
- package/rules/py/cors/allow-all.yml +8 -7
- package/rules/py/cors/fastapi-wildcard-credentials.yml +49 -0
- package/rules/py/crypto/passlib-weak-scheme.yml +40 -0
- package/rules/py/django/cors-allow-all.yml +34 -0
- package/rules/py/drf/default-authentication-empty.yml +32 -0
- package/rules/py/drf/default-permission-allowany.yml +35 -0
- package/rules/py/drf/view-authentication-disabled.yml +34 -0
- package/rules/py/flask/session-cookie-insecure.yml +42 -0
- package/rules/py/flow/insecure-random-token.yml +6 -5
- package/rules/py/flow/oauth-credential-in-log.yml +9 -8
- package/rules/py/flow/requests-verify-disabled.yml +29 -17
- package/rules/py/jwt/algorithm-confusion.yml +9 -8
- package/rules/py/jwt/verify-claims-disabled.yml +45 -0
- package/rules/py/oauth/insecure-transport-env.yml +3 -3
- package/rules/py/oauth/token-request-verify-disabled.yml +14 -4
- package/rules/rust/crypto/weak-password-hash.yml +6 -5
- package/rules/rust/jwt/algorithm-confusion.yml +8 -7
- package/rules/rust/jwt/hardcoded-secret.yml +5 -4
- package/rules/rust/jwt/no-issuer-validation.yml +7 -7
- package/rules/session/hardcoded-secret.yml +3 -3
- package/rules/session/no-regeneration.yml +6 -5
- 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
|
|
8
|
-
|
|
9
|
-
`random` is a pseudo-random number generator
|
|
10
|
-
and is NOT cryptographically secure
|
|
11
|
-
reproduced by an attacker, defeating the
|
|
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
|
|
8
|
-
`code`, an `access_token` /
|
|
9
|
-
`
|
|
10
|
-
|
|
11
|
-
written to files, shipped to aggregators
|
|
12
|
-
and read by people and systems that should
|
|
13
|
-
leaked authorization code or token can be
|
|
14
|
-
user or complete the OAuth exchange
|
|
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
|
-
|
|
23
|
-
- pattern:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
8
|
-
HMAC algorithm (HS256/HS384/HS512) with an
|
|
9
|
-
This enables the "algorithm confusion"
|
|
10
|
-
RSA/EC PUBLIC key can sign a forged
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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
|
|
8
|
-
|
|
9
|
-
requests integration, Django OAuth Toolkit
|
|
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
|
-
|
|
25
|
-
- pattern:
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
storing passwords
|
|
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
|
|
8
|
-
|
|
9
|
-
(`Algorithm::
|
|
10
|
-
|
|
11
|
-
(which is not secret) and signs a
|
|
12
|
-
key bytes as the HMAC shared
|
|
13
|
-
token as valid, letting the
|
|
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
|
|
8
|
-
to jsonwebtoken's `EncodingKey::from_secret` /
|
|
9
|
-
Anyone who can read the source or git history
|
|
10
|
-
tokens, which is a complete authentication
|
|
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
|
-
|
|
9
|
-
`jsonwebtoken` crate does not validate the `iss` claim unless
|
|
10
|
-
so a token signed by an attacker-controlled or otherwise
|
|
11
|
-
passes validation as long as the signature checks out.
|
|
12
|
-
lets a token from the wrong authorization server be
|
|
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
|
|
9
|
-
|
|
10
|
-
AI-generated example
|
|
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
|
|
9
|
-
`req.session.
|
|
10
|
-
session
|
|
11
|
-
|
|
12
|
-
before login retains access
|
|
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
|
-
|
|
9
|
-
`NODE_TLS_REJECT_UNAUTHORIZED=0`
|
|
10
|
-
certificate, including self-signed or
|
|
11
|
-
removes the protection TLS provides against
|
|
12
|
-
anyone able to intercept the network path can
|
|
13
|
-
certificate, then read and modify the traffic
|
|
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
|
|
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
|