oauthlint-rules 0.3.1 → 0.5.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/dist/loader.d.ts +1 -1
- package/dist/schema.d.ts +19 -19
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +1 -4
- package/dist/schema.js.map +1 -1
- package/package.json +1 -1
- package/rules/cookie/no-samesite.yml +1 -1
- package/rules/cookie/samesite-none-insecure.yml +1 -1
- package/rules/cors/null-origin.yml +1 -1
- package/rules/cors/reflect-origin.yml +1 -1
- package/rules/express/cookie-insecure.yml +44 -0
- package/rules/flow/basic-auth-in-log.yml +99 -0
- package/rules/flow/open-redirect.yml +42 -21
- package/rules/flow/ssrf.yml +43 -22
- package/rules/flow/timing-unsafe-compare.yml +1 -1
- package/rules/go/cookie/insecure.yml +1 -1
- package/rules/go/cors/allow-all.yml +1 -1
- package/rules/go/flow/open-redirect.yml +16 -6
- package/rules/go/flow/ssrf.yml +16 -7
- package/rules/java/cookie/insecure.yml +1 -1
- package/rules/java/cors/allow-all.yml +1 -1
- package/rules/java/cors/credentialed-wildcard.yml +1 -1
- package/rules/java/crypto/noop-password-encoder.yml +38 -0
- package/rules/java/flow/ssrf.yml +113 -0
- package/rules/java/session/fixation-disabled.yml +1 -1
- package/rules/java/tls/trust-all-certs.yml +18 -7
- package/rules/java/web/security-ignoring-all.yml +38 -0
- package/rules/java/web/wildcard-permit-all.yml +37 -0
- package/rules/nextauth/hardcoded-secret.yml +62 -0
- package/rules/oauth/no-state.yml +9 -8
- package/rules/oauth/open-redirect-callback.yml +18 -3
- package/rules/oauth/static-state.yml +8 -7
- package/rules/passport/jwt-ignore-expiration.yml +58 -0
- package/rules/py/cookie/insecure-flags.yml +1 -1
- package/rules/py/cors/allow-all.yml +1 -1
- 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/requests-verify-disabled.yml +30 -18
- package/rules/py/jwt/verify-claims-disabled.yml +45 -0
- package/rules/py/oauth/token-request-verify-disabled.yml +14 -4
- package/rules/rust/cookie/insecure.yml +1 -1
- package/rules/rust/cors/permissive.yml +1 -1
- package/rules/rust/flow/ssrf.yml +90 -0
- package/rules/tls/reject-unauthorized.yml +1 -1
|
@@ -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
|
|
@@ -19,29 +19,41 @@ 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
|
|
42
54
|
category: security
|
|
43
55
|
cwe: CWE-295
|
|
44
|
-
owasp:
|
|
56
|
+
owasp: A02:2021
|
|
45
57
|
llm-prevalence: HIGH
|
|
46
58
|
technology:
|
|
47
59
|
- requests
|
|
@@ -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
|
|
@@ -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
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.rust.flow.ssrf
|
|
3
|
+
languages:
|
|
4
|
+
- rust
|
|
5
|
+
severity: ERROR
|
|
6
|
+
message: |
|
|
7
|
+
Untrusted request input flows into the URL of an outbound HTTP request.
|
|
8
|
+
Because the destination is attacker-controlled, this is a Server-Side
|
|
9
|
+
Request Forgery (CWE-918): an attacker can point the request at internal
|
|
10
|
+
services behind your firewall, or at the cloud metadata endpoint
|
|
11
|
+
(http://169.254.169.254/...) to steal IAM/instance credentials and pivot
|
|
12
|
+
deeper into your infrastructure.
|
|
13
|
+
|
|
14
|
+
Never pass a request-derived `String` (an axum/actix handler parameter, or
|
|
15
|
+
a value taken from request input) straight into `reqwest::get(...)` or a
|
|
16
|
+
`Client::get(...)` / `Client::post(...).send()`. Validate the destination
|
|
17
|
+
host against an explicit allow-list (parse the URL and check the resolved
|
|
18
|
+
host/scheme, rejecting private/loopback/link-local ranges) before issuing
|
|
19
|
+
the request.
|
|
20
|
+
# Taint mode so indirection (let u = params.url; reqwest::get(u)) is caught,
|
|
21
|
+
# not just the inline form. The source is narrowed to function PARAMETERS
|
|
22
|
+
# (the shape an axum/actix handler takes its request data in) so an
|
|
23
|
+
# arbitrary local `let url = "https://constant"` is not treated as untrusted
|
|
24
|
+
# — only data arriving as a handler/function parameter is. The taint is
|
|
25
|
+
# cleared by an allow-list / host-validation if-guard, mirroring the Python
|
|
26
|
+
# and Go SSRF rules.
|
|
27
|
+
mode: taint
|
|
28
|
+
pattern-sources:
|
|
29
|
+
# A function parameter carrying request data: a bare `String` (the typical
|
|
30
|
+
# axum `String` / extracted field), or an axum `Query`/`Path` extractor
|
|
31
|
+
# (both the type-position and the destructured forms). Focus the parameter
|
|
32
|
+
# so the taint tracks the value rather than the whole function.
|
|
33
|
+
- patterns:
|
|
34
|
+
- pattern-either:
|
|
35
|
+
- pattern: "fn $F(..., $P: String, ...) {...}"
|
|
36
|
+
- pattern: "fn $F(..., $P: Query<$T>, ...) {...}"
|
|
37
|
+
- pattern: "fn $F(..., $P: Path<$T>, ...) {...}"
|
|
38
|
+
- pattern: "fn $F(..., Query($P): Query<$T>, ...) {...}"
|
|
39
|
+
- pattern: "fn $F(..., Path($P): Path<$T>, ...) {...}"
|
|
40
|
+
- focus-metavariable: $P
|
|
41
|
+
pattern-sanitizers:
|
|
42
|
+
# A value used inside an allow-list / host-validation if-guard is treated
|
|
43
|
+
# as vetted, so the guarded request does not fire. A bare `Url::parse(...)`
|
|
44
|
+
# is deliberately NOT a sanitizer: the parsed URL is still
|
|
45
|
+
# attacker-controlled, so parse-then-use without a host check must fire.
|
|
46
|
+
- patterns:
|
|
47
|
+
- pattern: $V
|
|
48
|
+
- pattern-inside: |
|
|
49
|
+
if is_allowed_url(<... $V ...>) {
|
|
50
|
+
...
|
|
51
|
+
}
|
|
52
|
+
- patterns:
|
|
53
|
+
- pattern: $V
|
|
54
|
+
- pattern-inside: |
|
|
55
|
+
if validate_host(<... $V ...>) {
|
|
56
|
+
...
|
|
57
|
+
}
|
|
58
|
+
- patterns:
|
|
59
|
+
- pattern: $V
|
|
60
|
+
- pattern-inside: |
|
|
61
|
+
if $ALLOW.contains(<... $V ...>) {
|
|
62
|
+
...
|
|
63
|
+
}
|
|
64
|
+
pattern-sinks:
|
|
65
|
+
# Focus the URL argument so the finding lands on the tainted destination.
|
|
66
|
+
- patterns:
|
|
67
|
+
- pattern-either:
|
|
68
|
+
- pattern: reqwest::get($URL)
|
|
69
|
+
- pattern: reqwest::blocking::get($URL)
|
|
70
|
+
- pattern: $C.get($URL).send()
|
|
71
|
+
- pattern: $C.post($URL).send()
|
|
72
|
+
- pattern: $C.put($URL).send()
|
|
73
|
+
- pattern: $C.delete($URL).send()
|
|
74
|
+
- pattern: $C.head($URL).send()
|
|
75
|
+
- pattern: $C.request($M, $URL).send()
|
|
76
|
+
- focus-metavariable: $URL
|
|
77
|
+
metadata:
|
|
78
|
+
oauthlint-rule-id: AUTH-RUST-FLOW-002
|
|
79
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/rust-flow-ssrf
|
|
80
|
+
category: security
|
|
81
|
+
cwe: CWE-918
|
|
82
|
+
owasp: API7:2023
|
|
83
|
+
llm-prevalence: HIGH
|
|
84
|
+
technology:
|
|
85
|
+
- reqwest
|
|
86
|
+
- axum
|
|
87
|
+
- actix-web
|
|
88
|
+
references:
|
|
89
|
+
- https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
|
|
90
|
+
- https://cwe.mitre.org/data/definitions/918.html
|