oauthlint-rules 0.2.3 → 0.2.5
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/flow/open-redirect.yml +76 -0
- package/rules/flow/ssrf.yml +85 -0
- package/rules/go/flow/open-redirect.yml +58 -0
- package/rules/go/flow/ssrf.yml +76 -0
- package/rules/java/crypto/weak-hash.yml +41 -0
- package/rules/oauth/access-token-in-url.yml +39 -0
- package/rules/py/flow/open-redirect.yml +82 -0
- package/rules/py/flow/ssrf.yml +118 -0
- package/rules/rust/jwt/no-issuer-validation.yml +59 -0
- package/rules/rust/tls/accept-invalid-certs.yml +5 -0
- package/rules/rust/tls/accept-invalid-hostnames.yml +5 -0
package/package.json
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.flow.open-redirect
|
|
3
|
+
languages:
|
|
4
|
+
- javascript
|
|
5
|
+
- typescript
|
|
6
|
+
severity: ERROR
|
|
7
|
+
message: |
|
|
8
|
+
Untrusted request input flows into a redirect destination. Because the
|
|
9
|
+
target URL is attacker-controlled, this is an open redirect (CWE-601):
|
|
10
|
+
an attacker can craft a link to your trusted host that bounces the victim
|
|
11
|
+
to an arbitrary external site. That fuels phishing, and in OAuth flows it
|
|
12
|
+
can be chained to steal authorization codes or access tokens by sending
|
|
13
|
+
the victim (and their callback) to a server you do not control.
|
|
14
|
+
|
|
15
|
+
Never redirect to a raw `req.query` / `req.body` / `req.params` /
|
|
16
|
+
`req.cookies` / `req.headers` value. Validate the destination against an
|
|
17
|
+
explicit allow-list of hosts or route names, or only allow relative paths
|
|
18
|
+
you control (reject anything containing a scheme or `//`).
|
|
19
|
+
# Taint mode so indirection is caught — `const dest = req.body.url;
|
|
20
|
+
# res.redirect(dest)` flags, not just the direct `res.redirect(req.query.x)`
|
|
21
|
+
# form. Passing the value through an allow-list / validation call
|
|
22
|
+
# (isAllowedUrl, allowlist.includes, Set.has) clears the taint, so a
|
|
23
|
+
# genuinely validated redirect does not fire.
|
|
24
|
+
mode: taint
|
|
25
|
+
pattern-sources:
|
|
26
|
+
- pattern: $REQ.query
|
|
27
|
+
- pattern: $REQ.params
|
|
28
|
+
- pattern: $REQ.body
|
|
29
|
+
- pattern: $REQ.cookies
|
|
30
|
+
- pattern: $REQ.headers
|
|
31
|
+
- pattern: $REQ.query.$X
|
|
32
|
+
- pattern: $REQ.params.$X
|
|
33
|
+
- pattern: $REQ.body.$X
|
|
34
|
+
- pattern: $REQ.cookies.$X
|
|
35
|
+
- pattern: $REQ.headers.$X
|
|
36
|
+
- pattern: $REQ.query[$K]
|
|
37
|
+
- pattern: $REQ.params[$K]
|
|
38
|
+
- pattern: $REQ.body[$K]
|
|
39
|
+
- pattern: $REQ.cookies[$K]
|
|
40
|
+
- pattern: $REQ.headers[$K]
|
|
41
|
+
pattern-sanitizers:
|
|
42
|
+
# Routing the value through an allow-list / validation helper clears the
|
|
43
|
+
# taint: only the vetted result (not the raw request input) reaches the
|
|
44
|
+
# sink. Matched by a conventional validation-helper call, or by an
|
|
45
|
+
# allow-list membership test (`Set.has` / `Array.includes`) that the
|
|
46
|
+
# value is passed through.
|
|
47
|
+
- pattern: isAllowedUrl(...)
|
|
48
|
+
- pattern: validateRedirect(...)
|
|
49
|
+
- pattern: sanitizeRedirect(...)
|
|
50
|
+
- pattern: $ALLOW.includes(...)
|
|
51
|
+
- pattern: $ALLOW.has(...)
|
|
52
|
+
- pattern: $ALLOW.indexOf(...)
|
|
53
|
+
pattern-sinks:
|
|
54
|
+
- patterns:
|
|
55
|
+
- pattern-either:
|
|
56
|
+
- pattern: $RES.redirect($SINK)
|
|
57
|
+
- pattern: $RES.redirect($CODE, $SINK)
|
|
58
|
+
- pattern: $RES.location($SINK)
|
|
59
|
+
- pattern: "$RES.set('Location', $SINK)"
|
|
60
|
+
- pattern: '$RES.set("Location", $SINK)'
|
|
61
|
+
- pattern: "$RES.setHeader('Location', $SINK)"
|
|
62
|
+
- pattern: '$RES.setHeader("Location", $SINK)'
|
|
63
|
+
- pattern: "$RES.writeHead($S, {..., Location: $SINK, ...})"
|
|
64
|
+
- focus-metavariable: $SINK
|
|
65
|
+
metadata:
|
|
66
|
+
oauthlint-rule-id: AUTH-FLOW-010
|
|
67
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/flow-open-redirect
|
|
68
|
+
category: security
|
|
69
|
+
cwe: CWE-601
|
|
70
|
+
owasp: A01:2021
|
|
71
|
+
llm-prevalence: HIGH
|
|
72
|
+
technology:
|
|
73
|
+
- express
|
|
74
|
+
references:
|
|
75
|
+
- https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html
|
|
76
|
+
- https://cwe.mitre.org/data/definitions/601.html
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.flow.ssrf
|
|
3
|
+
languages:
|
|
4
|
+
- javascript
|
|
5
|
+
- typescript
|
|
6
|
+
severity: ERROR
|
|
7
|
+
message: |
|
|
8
|
+
Untrusted request input flows into the URL of an outbound HTTP request.
|
|
9
|
+
Because the destination is attacker-controlled, this is a Server-Side
|
|
10
|
+
Request Forgery (CWE-918): an attacker can point the request at internal
|
|
11
|
+
services behind your firewall, or at the cloud metadata endpoint
|
|
12
|
+
(http://169.254.169.254/...) to steal IAM/instance credentials and pivot
|
|
13
|
+
deeper into your infrastructure.
|
|
14
|
+
|
|
15
|
+
Never build a request URL straight from a raw `req.query` / `req.body` /
|
|
16
|
+
`req.params` / `req.cookies` / `req.headers` value. Validate the
|
|
17
|
+
destination against an explicit allow-list of hosts (resolve the URL and
|
|
18
|
+
check its host against the allow-list, rejecting private/loopback ranges)
|
|
19
|
+
before issuing the request.
|
|
20
|
+
# Taint mode so indirection is caught — `const u = req.query.endpoint;
|
|
21
|
+
# http.get(u)` flags, not just the direct `fetch(req.query.url)` form.
|
|
22
|
+
# Passing the value through a host allow-list / validation call
|
|
23
|
+
# (isAllowedUrl, allowlist.includes, Set.has) clears the taint, so a
|
|
24
|
+
# genuinely vetted outbound request does not fire.
|
|
25
|
+
mode: taint
|
|
26
|
+
pattern-sources:
|
|
27
|
+
- pattern: $REQ.query
|
|
28
|
+
- pattern: $REQ.params
|
|
29
|
+
- pattern: $REQ.body
|
|
30
|
+
- pattern: $REQ.cookies
|
|
31
|
+
- pattern: $REQ.headers
|
|
32
|
+
- pattern: $REQ.query.$X
|
|
33
|
+
- pattern: $REQ.params.$X
|
|
34
|
+
- pattern: $REQ.body.$X
|
|
35
|
+
- pattern: $REQ.cookies.$X
|
|
36
|
+
- pattern: $REQ.headers.$X
|
|
37
|
+
- pattern: $REQ.query[$K]
|
|
38
|
+
- pattern: $REQ.params[$K]
|
|
39
|
+
- pattern: $REQ.body[$K]
|
|
40
|
+
- pattern: $REQ.cookies[$K]
|
|
41
|
+
- pattern: $REQ.headers[$K]
|
|
42
|
+
pattern-sanitizers:
|
|
43
|
+
# Routing the value through a host allow-list / validation helper clears
|
|
44
|
+
# the taint: only the vetted destination (not the raw request input)
|
|
45
|
+
# reaches the request. Matched by a conventional validation-helper call,
|
|
46
|
+
# or by an allow-list membership test (`Set.has` / `Array.includes`) that
|
|
47
|
+
# the value is passed through.
|
|
48
|
+
- pattern: isAllowedUrl(...)
|
|
49
|
+
- pattern: validateUrl(...)
|
|
50
|
+
- pattern: assertAllowedHost(...)
|
|
51
|
+
- pattern: $ALLOW.includes(...)
|
|
52
|
+
- pattern: $ALLOW.has(...)
|
|
53
|
+
- pattern: $ALLOW.indexOf(...)
|
|
54
|
+
pattern-sinks:
|
|
55
|
+
- patterns:
|
|
56
|
+
- pattern-either:
|
|
57
|
+
- pattern: fetch($SINK, ...)
|
|
58
|
+
- pattern: nodeFetch($SINK, ...)
|
|
59
|
+
- pattern: request($SINK, ...)
|
|
60
|
+
- pattern: got($SINK, ...)
|
|
61
|
+
- pattern: axios($SINK)
|
|
62
|
+
- pattern: axios.get($SINK, ...)
|
|
63
|
+
- pattern: axios.post($SINK, ...)
|
|
64
|
+
- pattern: axios.put($SINK, ...)
|
|
65
|
+
- pattern: axios.delete($SINK, ...)
|
|
66
|
+
- pattern: "axios.request({..., url: $SINK, ...})"
|
|
67
|
+
- pattern: http.get($SINK, ...)
|
|
68
|
+
- pattern: https.get($SINK, ...)
|
|
69
|
+
- pattern: http.request($SINK, ...)
|
|
70
|
+
- pattern: https.request($SINK, ...)
|
|
71
|
+
- focus-metavariable: $SINK
|
|
72
|
+
metadata:
|
|
73
|
+
oauthlint-rule-id: AUTH-FLOW-011
|
|
74
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/flow-ssrf
|
|
75
|
+
category: security
|
|
76
|
+
cwe: CWE-918
|
|
77
|
+
owasp: API7:2023
|
|
78
|
+
llm-prevalence: HIGH
|
|
79
|
+
technology:
|
|
80
|
+
- express
|
|
81
|
+
- axios
|
|
82
|
+
- fetch
|
|
83
|
+
references:
|
|
84
|
+
- https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
|
|
85
|
+
- https://cwe.mitre.org/data/definitions/918.html
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.go.flow.open-redirect
|
|
3
|
+
languages:
|
|
4
|
+
- go
|
|
5
|
+
severity: ERROR
|
|
6
|
+
message: |
|
|
7
|
+
Untrusted request data flows into an HTTP redirect destination. An
|
|
8
|
+
attacker who controls the redirect target (via a query parameter, form
|
|
9
|
+
field, or request header) can forward the victim to an arbitrary
|
|
10
|
+
external site while the link still appears to point at your trusted
|
|
11
|
+
domain — a classic open redirect, commonly abused to bypass OAuth
|
|
12
|
+
`redirect_uri` checks and to mount convincing phishing.
|
|
13
|
+
|
|
14
|
+
Do not pass request-derived values straight into `http.Redirect(...)`
|
|
15
|
+
or a `Location` header. Validate the destination against an explicit
|
|
16
|
+
allow-list, or restrict it to a known relative path (reject absolute
|
|
17
|
+
URLs, scheme-relative `//host` values, and back-references) before
|
|
18
|
+
redirecting. See CWE-601.
|
|
19
|
+
# Taint mode so indirection (dest := r.FormValue("url"); http.Redirect(w,
|
|
20
|
+
# r, dest, ...)) is caught, not just the inline form. The taint is cleared
|
|
21
|
+
# by an allow-list / validation helper that vets the destination, or by a
|
|
22
|
+
# url.Parse(...)-then-checked flow where the parsed value is inspected
|
|
23
|
+
# before use.
|
|
24
|
+
mode: taint
|
|
25
|
+
pattern-sources:
|
|
26
|
+
# $R is the *http.Request. Cover the common net/http input shapes.
|
|
27
|
+
- pattern: $R.URL.Query().Get(...)
|
|
28
|
+
- pattern: $R.URL.Query()
|
|
29
|
+
- pattern: $R.URL.Query()[$K]
|
|
30
|
+
- pattern: $R.FormValue(...)
|
|
31
|
+
- pattern: $R.PostFormValue(...)
|
|
32
|
+
- pattern: $R.Header.Get(...)
|
|
33
|
+
pattern-sanitizers:
|
|
34
|
+
# Allow-list / validation helper that vets the destination string.
|
|
35
|
+
- pattern: validateRedirect(...)
|
|
36
|
+
- pattern: isAllowedRedirect(...)
|
|
37
|
+
- pattern: $ALLOW.MatchString(...)
|
|
38
|
+
# url.Parse-then-checked: a parsed URL that has been inspected is treated
|
|
39
|
+
# as cleared (the result is rebuilt/validated rather than reflected raw).
|
|
40
|
+
- pattern: url.Parse(...)
|
|
41
|
+
pattern-sinks:
|
|
42
|
+
- patterns:
|
|
43
|
+
- pattern-either:
|
|
44
|
+
- pattern: http.Redirect($W, $R, $DEST, $CODE)
|
|
45
|
+
- pattern: $W.Header().Set("Location", $DEST)
|
|
46
|
+
- focus-metavariable: $DEST
|
|
47
|
+
metadata:
|
|
48
|
+
oauthlint-rule-id: AUTH-GO-FLOW-002
|
|
49
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/go-flow-open-redirect
|
|
50
|
+
category: security
|
|
51
|
+
cwe: CWE-601
|
|
52
|
+
owasp: A01:2021
|
|
53
|
+
llm-prevalence: HIGH
|
|
54
|
+
technology:
|
|
55
|
+
- net/http
|
|
56
|
+
references:
|
|
57
|
+
- https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html
|
|
58
|
+
- https://cwe.mitre.org/data/definitions/601.html
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.go.flow.ssrf
|
|
3
|
+
languages:
|
|
4
|
+
- go
|
|
5
|
+
severity: ERROR
|
|
6
|
+
message: |
|
|
7
|
+
Untrusted request data flows into the URL of an outbound HTTP request.
|
|
8
|
+
An attacker who controls the request target (via a query parameter, form
|
|
9
|
+
field, or request header) can coerce your server into making requests to
|
|
10
|
+
arbitrary destinations — Server-Side Request Forgery (SSRF). This is
|
|
11
|
+
routinely abused to reach internal-only services behind your network
|
|
12
|
+
perimeter and, most damagingly, the cloud instance metadata endpoint
|
|
13
|
+
(e.g. http://169.254.169.254/...), letting an attacker steal short-lived
|
|
14
|
+
credentials and pivot into your cloud account.
|
|
15
|
+
|
|
16
|
+
Never pass a request-derived value straight into `http.Get`,
|
|
17
|
+
`http.Post`, `http.NewRequest`, or a client's `Get`/`Post`. Validate the
|
|
18
|
+
destination host against an explicit allow-list (parse the URL and check
|
|
19
|
+
the resolved host/scheme), and reject requests to private, loopback, and
|
|
20
|
+
link-local address ranges before dialing. See CWE-918.
|
|
21
|
+
# Taint mode so indirection (target := r.FormValue("endpoint");
|
|
22
|
+
# http.Get(target)) is caught, not just the inline form. The taint is
|
|
23
|
+
# cleared by an allow-list / host-validation helper, or by a
|
|
24
|
+
# url.Parse(...)-then-checked flow where the parsed host is vetted before
|
|
25
|
+
# the request is made.
|
|
26
|
+
mode: taint
|
|
27
|
+
pattern-sources:
|
|
28
|
+
# $R is the *http.Request. Cover the common net/http input shapes.
|
|
29
|
+
- pattern: $R.URL.Query().Get(...)
|
|
30
|
+
- pattern: $R.URL.Query()
|
|
31
|
+
- pattern: $R.URL.Query()[$K]
|
|
32
|
+
- pattern: $R.FormValue(...)
|
|
33
|
+
- pattern: $R.PostFormValue(...)
|
|
34
|
+
- pattern: $R.Header.Get(...)
|
|
35
|
+
pattern-sanitizers:
|
|
36
|
+
# Allow-list / host-validation helper that vets the URL string.
|
|
37
|
+
- pattern: validateURL(...)
|
|
38
|
+
- pattern: isAllowedHost(...)
|
|
39
|
+
- pattern: $ALLOW.MatchString(...)
|
|
40
|
+
# url.Parse-then-checked: a parsed URL whose host has been inspected is
|
|
41
|
+
# treated as cleared (the request is built from a vetted destination
|
|
42
|
+
# rather than the raw untrusted string).
|
|
43
|
+
- pattern: url.Parse(...)
|
|
44
|
+
pattern-sinks:
|
|
45
|
+
# Focus the URL argument so the finding lands on the tainted destination,
|
|
46
|
+
# not the whole call. The plain call form is used (no aliasing) because
|
|
47
|
+
# it matches reliably in taint mode.
|
|
48
|
+
- patterns:
|
|
49
|
+
- pattern-either:
|
|
50
|
+
- pattern: http.Get($URL)
|
|
51
|
+
- pattern: http.Post($URL, ...)
|
|
52
|
+
- pattern: http.Head($URL)
|
|
53
|
+
- pattern: http.NewRequest($M, $URL, ...)
|
|
54
|
+
- pattern: http.NewRequestWithContext($CTX, $M, $URL, ...)
|
|
55
|
+
- pattern: $CLIENT.Get($URL)
|
|
56
|
+
- pattern: $CLIENT.Post($URL, ...)
|
|
57
|
+
# The bare `$CLIENT.Get(...)` shape collides with the request-input
|
|
58
|
+
# SOURCES that are themselves `.Get(...)` calls (`r.URL.Query().Get(...)`
|
|
59
|
+
# and `r.Header.Get(...)`), which over-matched the source-assignment
|
|
60
|
+
# line. Exclude those accessors so each is only ever a source, leaving
|
|
61
|
+
# `client.Get(url)` as the genuine outbound-request sink.
|
|
62
|
+
- pattern-not: $Q.Query().Get(...)
|
|
63
|
+
- pattern-not: $H.Header.Get(...)
|
|
64
|
+
- focus-metavariable: $URL
|
|
65
|
+
metadata:
|
|
66
|
+
oauthlint-rule-id: AUTH-GO-FLOW-003
|
|
67
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/go-flow-ssrf
|
|
68
|
+
category: security
|
|
69
|
+
cwe: CWE-918
|
|
70
|
+
owasp: API7:2023
|
|
71
|
+
llm-prevalence: HIGH
|
|
72
|
+
technology:
|
|
73
|
+
- net/http
|
|
74
|
+
references:
|
|
75
|
+
- https://owasp.org/www-community/attacks/Server_Side_Request_Forgery
|
|
76
|
+
- https://cwe.mitre.org/data/definitions/918.html
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.java.crypto.weak-hash
|
|
3
|
+
languages:
|
|
4
|
+
- java
|
|
5
|
+
severity: WARNING
|
|
6
|
+
message: |
|
|
7
|
+
A broken hash algorithm (MD5 or SHA-1) is being instantiated via JCA
|
|
8
|
+
`MessageDigest.getInstance(...)`. MD5 and SHA-1 are cryptographically
|
|
9
|
+
broken: practical collision attacks exist, so they must NOT be used for
|
|
10
|
+
any security purpose — integrity checks, content/token fingerprints,
|
|
11
|
+
digital signatures, HMAC keys, or deduplication that a trust decision
|
|
12
|
+
depends on (CWE-328 / CWE-327).
|
|
13
|
+
|
|
14
|
+
Use SHA-256 or stronger (SHA-384, SHA-512, SHA-3):
|
|
15
|
+
`MessageDigest.getInstance("SHA-256")`. Note: this rule covers the general
|
|
16
|
+
weak-digest case; storing *passwords* needs a dedicated slow hasher
|
|
17
|
+
(BCrypt/Argon2/PBKDF2), which is enforced separately.
|
|
18
|
+
# Match only the algorithm string passed to MessageDigest.getInstance(...).
|
|
19
|
+
# The metavariable-regex flags the broken digests MD5, SHA-1 and the SHA1
|
|
20
|
+
# alias (case-insensitive, optional hyphen). It is scoped to the
|
|
21
|
+
# getInstance(...) instantiation site on purpose so it is low-FP and does
|
|
22
|
+
# NOT overlap auth.java.crypto.weak-password-hash, which instead anchors on
|
|
23
|
+
# a password-named digest()/update() call (and also flags SHA-256/SHA-512 in
|
|
24
|
+
# that password context). "SHA-256"/"SHA-512" are silent here.
|
|
25
|
+
patterns:
|
|
26
|
+
- pattern: java.security.MessageDigest.getInstance($ALG, ...)
|
|
27
|
+
- metavariable-regex:
|
|
28
|
+
metavariable: $ALG
|
|
29
|
+
regex: (?i)^"(md5|sha-?1)"$
|
|
30
|
+
metadata:
|
|
31
|
+
oauthlint-rule-id: AUTH-JAVA-CRYPTO-004
|
|
32
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/java-crypto-weak-hash
|
|
33
|
+
category: security
|
|
34
|
+
cwe: CWE-328
|
|
35
|
+
owasp: A02:2021
|
|
36
|
+
llm-prevalence: MEDIUM
|
|
37
|
+
technology:
|
|
38
|
+
- java
|
|
39
|
+
references:
|
|
40
|
+
- https://cwe.mitre.org/data/definitions/328.html
|
|
41
|
+
- https://csrc.nist.gov/news/2022/nist-transitioning-away-from-sha-1-for-all-apps
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.oauth.access-token-in-url
|
|
3
|
+
languages:
|
|
4
|
+
- javascript
|
|
5
|
+
- typescript
|
|
6
|
+
severity: WARNING
|
|
7
|
+
message: |
|
|
8
|
+
An OAuth `access_token` (or `refresh_token` / `id_token`) is placed in a
|
|
9
|
+
URL query string. URLs leak: the full URL — token included — is recorded
|
|
10
|
+
in server and reverse-proxy access logs, saved in browser history, and
|
|
11
|
+
sent in the `Referer` header to every third-party CDN, analytics, and ad
|
|
12
|
+
script loaded by the destination page. A token in a URL is a leaked token.
|
|
13
|
+
|
|
14
|
+
Send the token in the `Authorization: Bearer …` header, or in a POST
|
|
15
|
+
request body. Never in the URL query string.
|
|
16
|
+
|
|
17
|
+
CWE-598: Use of GET Request Method With Sensitive Query Strings.
|
|
18
|
+
# Anchored to the query-param shape `?<name>=` / `&<name>=` (the `[?&]…=`
|
|
19
|
+
# form) so we fire on token-carrying URLs built as string or template
|
|
20
|
+
# literals, while staying silent on: a token in an `Authorization` header,
|
|
21
|
+
# a POST body / object property `{ access_token: t }` (no leading `?`/`&`
|
|
22
|
+
# and no trailing `=`), `params.set('access_token', …)` builders, and the
|
|
23
|
+
# bare word `access_token` in a comment or non-URL string. Case-insensitive
|
|
24
|
+
# on the param name. This is deliberately distinct from
|
|
25
|
+
# auth.flow.credentials-in-url (which excludes `access_token`) and
|
|
26
|
+
# auth.jwt.in-url (which only matches JWT-shaped `eyJ…` values).
|
|
27
|
+
pattern-regex: '[?&](?i:access_token|refresh_token|id_token)='
|
|
28
|
+
metadata:
|
|
29
|
+
oauthlint-rule-id: AUTH-OAUTH-013
|
|
30
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/oauth-access-token-in-url
|
|
31
|
+
category: security
|
|
32
|
+
cwe: CWE-598
|
|
33
|
+
owasp: A05:2021
|
|
34
|
+
llm-prevalence: MEDIUM
|
|
35
|
+
technology:
|
|
36
|
+
- oauth
|
|
37
|
+
references:
|
|
38
|
+
- https://owasp.org/Top10/A05_2021-Security_Misconfiguration/
|
|
39
|
+
- https://cwe.mitre.org/data/definitions/598.html
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.py.flow.open-redirect
|
|
3
|
+
languages:
|
|
4
|
+
- python
|
|
5
|
+
severity: ERROR
|
|
6
|
+
message: |
|
|
7
|
+
Untrusted request data flows into a Flask `redirect(...)` without
|
|
8
|
+
validation — an open redirect (CWE-601). An attacker can craft a link
|
|
9
|
+
to your trusted domain that bounces the victim to an attacker-controlled
|
|
10
|
+
site, enabling phishing and OAuth redirect/authorization-code abuse (the
|
|
11
|
+
victim trusts your URL, then lands on the attacker's page).
|
|
12
|
+
|
|
13
|
+
Never redirect to a raw `request.args`/`request.form`/`request.values`/
|
|
14
|
+
`request.cookies`/`request.headers` value. Build the destination with
|
|
15
|
+
`url_for(...)` (safe by construction), or validate the target against an
|
|
16
|
+
explicit allow-list / an `is_safe_url(...)`-style same-host check before
|
|
17
|
+
redirecting.
|
|
18
|
+
# Taint mode so indirection (dest = request.args['url']; return redirect(dest))
|
|
19
|
+
# and `.get(...)` accessors are caught, not just the direct form. Passing the
|
|
20
|
+
# value through url_for(...) or an is_safe_url(...)/allow-list check clears the
|
|
21
|
+
# taint. The sink focuses the redirect target argument (and a Location header).
|
|
22
|
+
mode: taint
|
|
23
|
+
pattern-sources:
|
|
24
|
+
- pattern: flask.request.args
|
|
25
|
+
- pattern: flask.request.form
|
|
26
|
+
- pattern: flask.request.values
|
|
27
|
+
- pattern: flask.request.cookies
|
|
28
|
+
- pattern: flask.request.headers
|
|
29
|
+
- pattern: request.args
|
|
30
|
+
- pattern: request.form
|
|
31
|
+
- pattern: request.values
|
|
32
|
+
- pattern: request.cookies
|
|
33
|
+
- pattern: request.headers
|
|
34
|
+
- pattern: request.args.get(...)
|
|
35
|
+
- pattern: request.form.get(...)
|
|
36
|
+
- pattern: request.values.get(...)
|
|
37
|
+
- pattern: request.cookies.get(...)
|
|
38
|
+
- pattern: request.headers.get(...)
|
|
39
|
+
- pattern: request.args[$K]
|
|
40
|
+
- pattern: request.form[$K]
|
|
41
|
+
- pattern: request.values[$K]
|
|
42
|
+
- pattern: request.cookies[$K]
|
|
43
|
+
- pattern: request.headers[$K]
|
|
44
|
+
pattern-sanitizers:
|
|
45
|
+
# `url_for(...)` builds the URL from a known endpoint — safe by construction.
|
|
46
|
+
- pattern: url_for(...)
|
|
47
|
+
- pattern: flask.url_for(...)
|
|
48
|
+
# Allow-list / same-host validators used as a guard:
|
|
49
|
+
# if is_safe_url(target): return redirect(target)
|
|
50
|
+
# A value used inside such an `if` body is treated as validated, so the
|
|
51
|
+
# guarded redirect does not fire.
|
|
52
|
+
- patterns:
|
|
53
|
+
- pattern: $V
|
|
54
|
+
- pattern-inside: |
|
|
55
|
+
if is_safe_url($V):
|
|
56
|
+
...
|
|
57
|
+
- patterns:
|
|
58
|
+
- pattern: $V
|
|
59
|
+
- pattern-inside: |
|
|
60
|
+
if url_has_allowed_host_and_scheme($V, ...):
|
|
61
|
+
...
|
|
62
|
+
pattern-sinks:
|
|
63
|
+
- patterns:
|
|
64
|
+
- pattern-either:
|
|
65
|
+
- pattern: redirect($SINK)
|
|
66
|
+
- pattern: redirect($SINK, ...)
|
|
67
|
+
- pattern: flask.redirect($SINK)
|
|
68
|
+
- pattern: flask.redirect($SINK, ...)
|
|
69
|
+
- pattern: 'Response(..., headers={..., "Location": $SINK, ...}, ...)'
|
|
70
|
+
- focus-metavariable: $SINK
|
|
71
|
+
metadata:
|
|
72
|
+
oauthlint-rule-id: AUTH-PY-FLOW-006
|
|
73
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/py-flow-open-redirect
|
|
74
|
+
category: security
|
|
75
|
+
cwe: CWE-601
|
|
76
|
+
owasp: A01:2021
|
|
77
|
+
llm-prevalence: HIGH
|
|
78
|
+
technology:
|
|
79
|
+
- flask
|
|
80
|
+
references:
|
|
81
|
+
- https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html
|
|
82
|
+
- https://cwe.mitre.org/data/definitions/601.html
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.py.flow.ssrf
|
|
3
|
+
languages:
|
|
4
|
+
- python
|
|
5
|
+
severity: ERROR
|
|
6
|
+
message: |
|
|
7
|
+
Untrusted request data flows into an outbound HTTP request without
|
|
8
|
+
validation — a Server-Side Request Forgery (SSRF, CWE-918). An attacker
|
|
9
|
+
who controls the target URL can make your server reach internal-only
|
|
10
|
+
services (admin panels, databases, other microservices behind your
|
|
11
|
+
firewall) or the cloud metadata endpoint (e.g. http://169.254.169.254/),
|
|
12
|
+
stealing IAM/instance credentials and pivoting deeper into your network.
|
|
13
|
+
|
|
14
|
+
Never pass a raw `request.args`/`request.form`/`request.values`/
|
|
15
|
+
`request.json`/`request.cookies`/`request.headers` value to
|
|
16
|
+
`requests`/`urllib`/`httpx`/`aiohttp`. Validate the destination host
|
|
17
|
+
against an explicit allow-list (`is_allowed_url(...)` /
|
|
18
|
+
`validate_host(...)` / `url_has_allowed_host_and_scheme(...)`) before the
|
|
19
|
+
request, and reject link-local / private / metadata addresses.
|
|
20
|
+
# Taint mode so indirection (url = request.json['endpoint']; httpx.get(url))
|
|
21
|
+
# and `.get(...)` accessors are caught, not just the direct form. Passing the
|
|
22
|
+
# value through an allow-list / host validator clears the taint. The sink
|
|
23
|
+
# focuses the URL argument of each HTTP client call.
|
|
24
|
+
mode: taint
|
|
25
|
+
pattern-sources:
|
|
26
|
+
- pattern: flask.request.args
|
|
27
|
+
- pattern: flask.request.form
|
|
28
|
+
- pattern: flask.request.values
|
|
29
|
+
- pattern: flask.request.json
|
|
30
|
+
- pattern: flask.request.cookies
|
|
31
|
+
- pattern: flask.request.headers
|
|
32
|
+
- pattern: request.args
|
|
33
|
+
- pattern: request.form
|
|
34
|
+
- pattern: request.values
|
|
35
|
+
- pattern: request.json
|
|
36
|
+
- pattern: request.cookies
|
|
37
|
+
- pattern: request.headers
|
|
38
|
+
- pattern: request.args.get(...)
|
|
39
|
+
- pattern: request.form.get(...)
|
|
40
|
+
- pattern: request.values.get(...)
|
|
41
|
+
- pattern: request.json.get(...)
|
|
42
|
+
- pattern: request.cookies.get(...)
|
|
43
|
+
- pattern: request.headers.get(...)
|
|
44
|
+
- pattern: request.args[$K]
|
|
45
|
+
- pattern: request.form[$K]
|
|
46
|
+
- pattern: request.values[$K]
|
|
47
|
+
- pattern: request.json[$K]
|
|
48
|
+
- pattern: request.cookies[$K]
|
|
49
|
+
- pattern: request.headers[$K]
|
|
50
|
+
pattern-sanitizers:
|
|
51
|
+
# Allow-list / host validators used as a guard:
|
|
52
|
+
# if is_allowed_url(url): requests.get(url)
|
|
53
|
+
# A value used inside such an `if` body is treated as validated, so the
|
|
54
|
+
# guarded request does not fire. `by-side-effect` is unsupported on
|
|
55
|
+
# semgrep 1.157, so we use the `pattern-inside` if-guard approach.
|
|
56
|
+
- patterns:
|
|
57
|
+
- pattern: $V
|
|
58
|
+
- pattern-inside: |
|
|
59
|
+
if is_allowed_url($V):
|
|
60
|
+
...
|
|
61
|
+
- patterns:
|
|
62
|
+
- pattern: $V
|
|
63
|
+
- pattern-inside: |
|
|
64
|
+
if validate_host($V):
|
|
65
|
+
...
|
|
66
|
+
- patterns:
|
|
67
|
+
- pattern: $V
|
|
68
|
+
- pattern-inside: |
|
|
69
|
+
if url_has_allowed_host_and_scheme($V, ...):
|
|
70
|
+
...
|
|
71
|
+
pattern-sinks:
|
|
72
|
+
- patterns:
|
|
73
|
+
- pattern-either:
|
|
74
|
+
- pattern: 'requests.get($URL, ...)'
|
|
75
|
+
- pattern: 'requests.post($URL, ...)'
|
|
76
|
+
- pattern: 'requests.put($URL, ...)'
|
|
77
|
+
- pattern: 'requests.delete($URL, ...)'
|
|
78
|
+
- pattern: 'requests.patch($URL, ...)'
|
|
79
|
+
- pattern: 'requests.head($URL, ...)'
|
|
80
|
+
- pattern: 'requests.request(..., $URL, ...)'
|
|
81
|
+
- pattern: 'urllib.request.urlopen($URL, ...)'
|
|
82
|
+
- pattern: urllib.request.urlopen($URL)
|
|
83
|
+
- pattern: 'httpx.get($URL, ...)'
|
|
84
|
+
- pattern: 'httpx.post($URL, ...)'
|
|
85
|
+
- pattern: 'httpx.put($URL, ...)'
|
|
86
|
+
- pattern: 'httpx.delete($URL, ...)'
|
|
87
|
+
- pattern: 'httpx.patch($URL, ...)'
|
|
88
|
+
- pattern: 'httpx.head($URL, ...)'
|
|
89
|
+
# httpx.Client()/aiohttp session calls. Excluding a `request`
|
|
90
|
+
# receiver keeps these from matching the `request.X.get(...)`
|
|
91
|
+
# SOURCE accessors (which would otherwise self-report).
|
|
92
|
+
- patterns:
|
|
93
|
+
- pattern-either:
|
|
94
|
+
- pattern: '$CLIENT.get($URL, ...)'
|
|
95
|
+
- pattern: '$CLIENT.post($URL, ...)'
|
|
96
|
+
- pattern: '$CLIENT.put($URL, ...)'
|
|
97
|
+
- pattern: '$CLIENT.delete($URL, ...)'
|
|
98
|
+
- pattern: '$CLIENT.patch($URL, ...)'
|
|
99
|
+
- pattern: '$CLIENT.head($URL, ...)'
|
|
100
|
+
- pattern: '$CLIENT.request(..., $URL, ...)'
|
|
101
|
+
- metavariable-regex:
|
|
102
|
+
metavariable: $CLIENT
|
|
103
|
+
regex: '^(?!request(\.|$)).*'
|
|
104
|
+
- focus-metavariable: $URL
|
|
105
|
+
metadata:
|
|
106
|
+
oauthlint-rule-id: AUTH-PY-FLOW-007
|
|
107
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/py-flow-ssrf
|
|
108
|
+
category: security
|
|
109
|
+
cwe: CWE-918
|
|
110
|
+
owasp: API7:2023
|
|
111
|
+
llm-prevalence: HIGH
|
|
112
|
+
technology:
|
|
113
|
+
- flask
|
|
114
|
+
- requests
|
|
115
|
+
- httpx
|
|
116
|
+
references:
|
|
117
|
+
- https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
|
|
118
|
+
- https://cwe.mitre.org/data/definitions/918.html
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.rust.jwt.no-issuer-validation
|
|
3
|
+
languages:
|
|
4
|
+
- rust
|
|
5
|
+
severity: WARNING
|
|
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.
|
|
14
|
+
|
|
15
|
+
Pin the issuer before decoding, e.g.
|
|
16
|
+
`validation.set_issuer(&["https://issuer.example.com"])` (or set
|
|
17
|
+
`validation.iss`), so only tokens whose `iss` claim matches your trusted
|
|
18
|
+
authorization server are accepted.
|
|
19
|
+
# Modeled on auth.rust.jwt.no-aud-validation: detect a `Validation`
|
|
20
|
+
# (`Validation::new(...)` or `Validation::default()`) that flows into
|
|
21
|
+
# `decode(...)` but never has its issuer pinned. Unlike `aud`/`exp` (default
|
|
22
|
+
# `true`, disabled via `validate_* = false`), the issuer check is OFF by
|
|
23
|
+
# default and must be opted into via `set_issuer(...)` / `validation.iss`,
|
|
24
|
+
# so the vulnerable shape is the ABSENCE of that call. We bind the validator
|
|
25
|
+
# to `$V` at construction, require a `decode` that uses it, and suppress the
|
|
26
|
+
# finding with `pattern-not-inside` when `set_issuer` / `validation.iss` is
|
|
27
|
+
# set on the same `$V`. Matching on the construction (not the `decode` call)
|
|
28
|
+
# mirrors the sibling rule's low-FP, AST-only profile.
|
|
29
|
+
patterns:
|
|
30
|
+
- pattern-either:
|
|
31
|
+
- pattern: let mut $V = Validation::new(...);
|
|
32
|
+
- pattern: let mut $V = Validation::default();
|
|
33
|
+
- pattern-not-inside: |
|
|
34
|
+
let mut $V = ...;
|
|
35
|
+
...
|
|
36
|
+
$V.set_issuer(...);
|
|
37
|
+
...
|
|
38
|
+
- pattern-not-inside: |
|
|
39
|
+
let mut $V = ...;
|
|
40
|
+
...
|
|
41
|
+
$V.iss = $X;
|
|
42
|
+
...
|
|
43
|
+
- pattern-inside: |
|
|
44
|
+
let mut $V = ...;
|
|
45
|
+
...
|
|
46
|
+
decode($TOKEN, $KEY, &$V)
|
|
47
|
+
metadata:
|
|
48
|
+
oauthlint-rule-id: AUTH-RUST-JWT-005
|
|
49
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/rust-jwt-no-issuer-validation
|
|
50
|
+
category: security
|
|
51
|
+
cwe: CWE-345
|
|
52
|
+
owasp: API2:2023
|
|
53
|
+
llm-prevalence: MEDIUM
|
|
54
|
+
technology:
|
|
55
|
+
- jsonwebtoken
|
|
56
|
+
references:
|
|
57
|
+
- https://docs.rs/jsonwebtoken/latest/jsonwebtoken/struct.Validation.html#method.set_issuer
|
|
58
|
+
- https://cwe.mitre.org/data/definitions/345.html
|
|
59
|
+
- https://cwe.mitre.org/data/definitions/287.html
|
|
@@ -16,6 +16,11 @@ rules:
|
|
|
16
16
|
# Matches only the literal `true`. `danger_accept_invalid_certs(false)` and
|
|
17
17
|
# the method's absence are not flagged. `$B` is any builder expression.
|
|
18
18
|
pattern: $B.danger_accept_invalid_certs(true)
|
|
19
|
+
# Safe, deterministic autofix: flip the boolean literal to `false`, which is
|
|
20
|
+
# the secure default and fully resolves the finding. `$B` (the builder
|
|
21
|
+
# expression) is preserved verbatim, so the surrounding chain is untouched —
|
|
22
|
+
# only `true` -> `false` changes.
|
|
23
|
+
fix: $B.danger_accept_invalid_certs(false)
|
|
19
24
|
metadata:
|
|
20
25
|
oauthlint-rule-id: AUTH-RUST-TLS-001
|
|
21
26
|
oauthlint-doc-url: https://oauthlint.dev/rules/rust-tls-accept-invalid-certs
|
|
@@ -18,6 +18,11 @@ rules:
|
|
|
18
18
|
# Matches only the literal `true`. `danger_accept_invalid_hostnames(false)`
|
|
19
19
|
# and the method's absence are not flagged. `$B` is any builder expression.
|
|
20
20
|
pattern: $B.danger_accept_invalid_hostnames(true)
|
|
21
|
+
# Safe, deterministic autofix: flip the boolean literal to `false`, which is
|
|
22
|
+
# the secure default and fully resolves the finding. `$B` (the builder
|
|
23
|
+
# expression) is preserved verbatim, so the surrounding chain is untouched —
|
|
24
|
+
# only `true` -> `false` changes.
|
|
25
|
+
fix: $B.danger_accept_invalid_hostnames(false)
|
|
21
26
|
metadata:
|
|
22
27
|
oauthlint-rule-id: AUTH-RUST-TLS-002
|
|
23
28
|
oauthlint-doc-url: https://oauthlint.dev/rules/rust-tls-accept-invalid-hostnames
|