oauthlint-rules 0.2.4 → 0.2.6
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/secret-in-response.yml +70 -0
- package/rules/flow/ssrf.yml +85 -0
- package/rules/go/flow/open-redirect.yml +58 -0
- package/rules/go/flow/secret-in-response.yml +69 -0
- package/rules/go/flow/ssrf.yml +76 -0
- package/rules/py/flow/open-redirect.yml +82 -0
- package/rules/py/flow/secret-in-response.yml +88 -0
- package/rules/py/flow/ssrf.yml +118 -0
- package/rules/py/jwt/algorithm-confusion.yml +48 -0
- package/rules/rust/jwt/algorithm-confusion.yml +50 -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,70 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.flow.secret-in-response
|
|
3
|
+
languages:
|
|
4
|
+
- javascript
|
|
5
|
+
- typescript
|
|
6
|
+
severity: ERROR
|
|
7
|
+
message: |
|
|
8
|
+
A server-side secret read from `process.env` flows into an HTTP
|
|
9
|
+
response body. Whatever you put in `res.send` / `res.json` / `res.end`
|
|
10
|
+
ships straight to the client, so returning a credential here publishes
|
|
11
|
+
it to every caller — it ends up in the browser, in proxies, and in any
|
|
12
|
+
logged response (CWE-200, Sensitive Information Exposure).
|
|
13
|
+
|
|
14
|
+
Never send a server secret to the client. Return only the data the
|
|
15
|
+
caller needs; if the response must reference a credential, send a
|
|
16
|
+
non-sensitive identifier (a key id, the last four characters) or a
|
|
17
|
+
redacted/masked value instead. Keep API keys, passwords, tokens, and
|
|
18
|
+
private keys server-side only.
|
|
19
|
+
# Reverse-direction taint rule: the SOURCE is the secret (a `process.env.*`
|
|
20
|
+
# value whose NAME looks like a credential) and the SINK is the Express
|
|
21
|
+
# response body — the opposite of the request->sink flow rules (ssrf,
|
|
22
|
+
# open-redirect). Taint mode catches indirection: `const k =
|
|
23
|
+
# process.env.API_KEY; res.send(k)` flags, not just the inline form.
|
|
24
|
+
#
|
|
25
|
+
# Low-FP control is the source NAME regex: it requires a credential-shaped
|
|
26
|
+
# name AND excludes the client-public prefixes (PUBLIC_, NEXT_PUBLIC_,
|
|
27
|
+
# VITE_, REACT_APP_, EXPO_PUBLIC_) whose values are exposed to the browser
|
|
28
|
+
# by design — sending those back is not a leak. Distinct from
|
|
29
|
+
# auth.secret.public-env-secret, which is a build-time *exposure* check on
|
|
30
|
+
# the env-var name alone; this rule is a *runtime dataflow* into a response.
|
|
31
|
+
mode: taint
|
|
32
|
+
pattern-sources:
|
|
33
|
+
# process.env.SECRET_NAME — member-access form.
|
|
34
|
+
- patterns:
|
|
35
|
+
- pattern: process.env.$KEY
|
|
36
|
+
- metavariable-regex:
|
|
37
|
+
metavariable: $KEY
|
|
38
|
+
regex: (?i)^(?!(?:NEXT_PUBLIC|EXPO_PUBLIC|REACT_APP|PUBLIC|VITE)_).*(?:secret|password|passwd|token|api[_-]?key|private[_-]?key|client[_-]?secret|credential|access[_-]?key).*$
|
|
39
|
+
# process.env['SECRET_NAME'] — index form.
|
|
40
|
+
- patterns:
|
|
41
|
+
- pattern: process.env[$K]
|
|
42
|
+
- metavariable-regex:
|
|
43
|
+
metavariable: $K
|
|
44
|
+
regex: (?i)^(?!(?:NEXT_PUBLIC|EXPO_PUBLIC|REACT_APP|PUBLIC|VITE)_).*(?:secret|password|passwd|token|api[_-]?key|private[_-]?key|client[_-]?secret|credential|access[_-]?key).*$
|
|
45
|
+
pattern-sanitizers:
|
|
46
|
+
# Routing the value through a redaction/masking helper clears the taint:
|
|
47
|
+
# the masked/redacted form is no longer the live secret.
|
|
48
|
+
- pattern: redact(...)
|
|
49
|
+
- pattern: mask(...)
|
|
50
|
+
pattern-sinks:
|
|
51
|
+
- patterns:
|
|
52
|
+
- pattern-either:
|
|
53
|
+
- pattern: $RES.send($X)
|
|
54
|
+
- pattern: $RES.json($X)
|
|
55
|
+
- pattern: $RES.jsonp($X)
|
|
56
|
+
- pattern: $RES.end($X)
|
|
57
|
+
- pattern: $RES.write($X)
|
|
58
|
+
- focus-metavariable: $X
|
|
59
|
+
metadata:
|
|
60
|
+
oauthlint-rule-id: AUTH-FLOW-012
|
|
61
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/flow-secret-in-response
|
|
62
|
+
category: security
|
|
63
|
+
cwe: CWE-200
|
|
64
|
+
owasp: API3:2023
|
|
65
|
+
llm-prevalence: HIGH
|
|
66
|
+
technology:
|
|
67
|
+
- express
|
|
68
|
+
references:
|
|
69
|
+
- https://cwe.mitre.org/data/definitions/200.html
|
|
70
|
+
- https://owasp.org/API-Security/editions/2023/en/0xa3-broken-object-property-level-authorization/
|
|
@@ -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,69 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.go.flow.secret-in-response
|
|
3
|
+
languages:
|
|
4
|
+
- go
|
|
5
|
+
severity: ERROR
|
|
6
|
+
message: |
|
|
7
|
+
A server-side secret read from the environment flows into an HTTP
|
|
8
|
+
response body, leaking it to the client. Values such as an API key,
|
|
9
|
+
client secret, access key, or private key are meant to stay on the
|
|
10
|
+
server; writing one to the `http.ResponseWriter` (via `Write`,
|
|
11
|
+
`fmt.Fprint(f)`, `io.WriteString`, or a JSON encoder) publishes it to
|
|
12
|
+
every caller — including attackers probing your endpoints.
|
|
13
|
+
|
|
14
|
+
Never return a credential to the client. Send only the data the caller
|
|
15
|
+
legitimately needs; if a secret must appear in a debug/diagnostic path,
|
|
16
|
+
redact or mask it first. Read secrets exclusively in server-internal
|
|
17
|
+
code and keep them out of any response payload. See CWE-200.
|
|
18
|
+
# Reverse-direction taint: the SOURCE is the secret (an os.Getenv /
|
|
19
|
+
# os.LookupEnv read whose key name looks like a credential) and the SINK is
|
|
20
|
+
# the HTTP response. Taint mode so indirection (s := os.Getenv("API_KEY");
|
|
21
|
+
# w.Write([]byte(s))) is caught, not just the inline form. The taint is
|
|
22
|
+
# cleared by a redaction/mask helper before the value reaches the response.
|
|
23
|
+
mode: taint
|
|
24
|
+
pattern-sources:
|
|
25
|
+
# $KEY is the env-var name literal. The metavariable-regex keeps only
|
|
26
|
+
# names that look like a credential, while a single negative lookahead
|
|
27
|
+
# excludes client-public prefixes (PUBLIC_ / NEXT_PUBLIC_) — those ship to
|
|
28
|
+
# the browser by design and are not server secrets.
|
|
29
|
+
- patterns:
|
|
30
|
+
- pattern-either:
|
|
31
|
+
- pattern: os.Getenv($KEY)
|
|
32
|
+
- pattern: os.LookupEnv($KEY)
|
|
33
|
+
- metavariable-regex:
|
|
34
|
+
metavariable: $KEY
|
|
35
|
+
regex: (?i)^(?!"?(next_)?public_)"?.*(secret|password|passwd|token|api[_-]?key|private[_-]?key|client[_-]?secret|credential|access[_-]?key).*"?$
|
|
36
|
+
pattern-sanitizers:
|
|
37
|
+
# A redaction/mask helper clears the taint: the value written to the
|
|
38
|
+
# response is no longer the raw secret.
|
|
39
|
+
- pattern: redact(...)
|
|
40
|
+
- pattern: mask(...)
|
|
41
|
+
pattern-sinks:
|
|
42
|
+
# Focus the value argument so the finding lands on the leaked secret, not
|
|
43
|
+
# the whole call. Plain-call forms (no aliasing) match reliably in taint
|
|
44
|
+
# mode. $W is the http.ResponseWriter.
|
|
45
|
+
- patterns:
|
|
46
|
+
- pattern-either:
|
|
47
|
+
- pattern: $W.Write($X)
|
|
48
|
+
- pattern: fmt.Fprint($W, $X)
|
|
49
|
+
- pattern: fmt.Fprintf($W, $FMT, $X)
|
|
50
|
+
- pattern: io.WriteString($W, $X)
|
|
51
|
+
- pattern: json.NewEncoder($W).Encode($X)
|
|
52
|
+
# The env-read SOURCE shapes are disjoint from these response sinks,
|
|
53
|
+
# but exclude them explicitly so an env read is only ever a source,
|
|
54
|
+
# never re-counted as a sink.
|
|
55
|
+
- pattern-not: os.Getenv(...)
|
|
56
|
+
- pattern-not: os.LookupEnv(...)
|
|
57
|
+
- focus-metavariable: $X
|
|
58
|
+
metadata:
|
|
59
|
+
oauthlint-rule-id: AUTH-GO-FLOW-004
|
|
60
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/go-flow-secret-in-response
|
|
61
|
+
category: security
|
|
62
|
+
cwe: CWE-200
|
|
63
|
+
owasp: API3:2023
|
|
64
|
+
llm-prevalence: HIGH
|
|
65
|
+
technology:
|
|
66
|
+
- net/http
|
|
67
|
+
references:
|
|
68
|
+
- https://cwe.mitre.org/data/definitions/200.html
|
|
69
|
+
- https://owasp.org/API-Security/editions/2023/en/0xa3-broken-object-property-level-authorization/
|
|
@@ -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,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,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,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,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,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/
|