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.
- package/package.json +1 -1
- package/rules/cookie/no-httponly.yml +5 -1
- package/rules/cookie/no-samesite.yml +5 -1
- package/rules/cookie/no-secure.yml +8 -5
- package/rules/flow/oauth-credential-in-log.yml +97 -0
- package/rules/flow/secret-in-response.yml +70 -0
- package/rules/go/cookie/insecure.yml +15 -6
- package/rules/go/flow/oauth-credential-in-log.yml +90 -0
- package/rules/go/flow/secret-in-response.yml +69 -0
- package/rules/go/jwt/untrusted-verify-key.yml +68 -0
- package/rules/go/oauth/insecure-token-endpoint.yml +39 -0
- package/rules/go/oauth/ropc-grant.yml +49 -0
- package/rules/go/oauth/static-state.yml +39 -0
- package/rules/go/tls/insecure-skip-verify.yml +14 -3
- package/rules/go/tls/min-version.yml +12 -3
- package/rules/java/cors/credentialed-wildcard.yml +44 -0
- package/rules/java/jwt/none-algorithm.yml +37 -0
- package/rules/java/jwt/untrusted-verify-key.yml +54 -0
- package/rules/java/oauth/insecure-token-endpoint.yml +39 -0
- package/rules/java/oauth/ropc-grant.yml +45 -0
- package/rules/java/oauth/static-state.yml +38 -0
- package/rules/java/web/permit-all-actuator.yml +43 -0
- package/rules/jwt/ignore-expiration.yml +21 -11
- package/rules/jwt/untrusted-verify-key.yml +77 -0
- package/rules/oauth/insecure-token-endpoint.yml +41 -0
- package/rules/oauth/ropc-grant.yml +43 -0
- package/rules/oauth/static-state.yml +49 -0
- package/rules/py/flow/oauth-credential-in-log.yml +91 -0
- package/rules/py/flow/secret-in-response.yml +88 -0
- package/rules/py/jwt/algorithm-confusion.yml +48 -0
- package/rules/py/jwt/untrusted-verify-key.yml +75 -0
- package/rules/py/oauth/insecure-token-endpoint.yml +40 -0
- package/rules/py/oauth/insecure-transport-env.yml +43 -0
- package/rules/py/oauth/ropc-grant.yml +50 -0
- package/rules/py/oauth/static-state.yml +45 -0
- package/rules/py/oauth/token-request-verify-disabled.yml +41 -0
- package/rules/rust/jwt/algorithm-confusion.yml +50 -0
- package/rules/rust/oauth/insecure-token-endpoint.yml +39 -0
- package/rules/rust/oauth/ropc-grant.yml +40 -0
- 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
|