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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oauthlint-rules",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Semgrep rules catching the OAuth/OIDC/JWT anti-patterns that AI coding tools systematically produce.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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/