oauthlint-rules 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/rules/cookie/no-httponly.yml +5 -1
- package/rules/cookie/no-samesite.yml +5 -1
- package/rules/cookie/no-secure.yml +8 -5
- package/rules/flow/oauth-credential-in-log.yml +97 -0
- package/rules/flow/secret-in-response.yml +70 -0
- package/rules/go/cookie/insecure.yml +15 -6
- package/rules/go/flow/oauth-credential-in-log.yml +90 -0
- package/rules/go/flow/secret-in-response.yml +69 -0
- package/rules/go/jwt/untrusted-verify-key.yml +68 -0
- package/rules/go/oauth/insecure-token-endpoint.yml +39 -0
- package/rules/go/oauth/ropc-grant.yml +49 -0
- package/rules/go/oauth/static-state.yml +39 -0
- package/rules/go/tls/insecure-skip-verify.yml +14 -3
- package/rules/go/tls/min-version.yml +12 -3
- package/rules/java/cors/credentialed-wildcard.yml +44 -0
- package/rules/java/jwt/none-algorithm.yml +37 -0
- package/rules/java/jwt/untrusted-verify-key.yml +54 -0
- package/rules/java/oauth/insecure-token-endpoint.yml +39 -0
- package/rules/java/oauth/ropc-grant.yml +45 -0
- package/rules/java/oauth/static-state.yml +38 -0
- package/rules/java/web/permit-all-actuator.yml +43 -0
- package/rules/jwt/ignore-expiration.yml +21 -11
- package/rules/jwt/untrusted-verify-key.yml +77 -0
- package/rules/oauth/insecure-token-endpoint.yml +41 -0
- package/rules/oauth/ropc-grant.yml +43 -0
- package/rules/oauth/static-state.yml +49 -0
- package/rules/py/flow/oauth-credential-in-log.yml +91 -0
- package/rules/py/flow/secret-in-response.yml +88 -0
- package/rules/py/jwt/algorithm-confusion.yml +48 -0
- package/rules/py/jwt/untrusted-verify-key.yml +75 -0
- package/rules/py/oauth/insecure-token-endpoint.yml +40 -0
- package/rules/py/oauth/insecure-transport-env.yml +43 -0
- package/rules/py/oauth/ropc-grant.yml +50 -0
- package/rules/py/oauth/static-state.yml +45 -0
- package/rules/py/oauth/token-request-verify-disabled.yml +41 -0
- package/rules/rust/jwt/algorithm-confusion.yml +50 -0
- package/rules/rust/oauth/insecure-token-endpoint.yml +39 -0
- package/rules/rust/oauth/ropc-grant.yml +40 -0
- package/rules/rust/oauth/static-state.yml +38 -0
package/package.json
CHANGED
|
@@ -39,7 +39,11 @@ rules:
|
|
|
39
39
|
- metavariable-regex:
|
|
40
40
|
metavariable: $NAME
|
|
41
41
|
regex: ^(['"])(?:[a-zA-Z0-9_-]*(?:session|sid|sess|auth|token|jwt|refresh|access)[a-zA-Z0-9_-]*)\1$
|
|
42
|
-
|
|
42
|
+
# No autofix. As with `auth.cookie.no-secure`, a single rule-level `fix:`
|
|
43
|
+
# cannot cover all three matched shapes safely: a spread template corrupts
|
|
44
|
+
# the 2-arg form (unbound `$OPTS` is emitted literally) and does not resolve
|
|
45
|
+
# an explicit `httpOnly: false`. Inserting a missing key is not a clean
|
|
46
|
+
# literal replacement, so this rule deliberately ships no `fix:`.
|
|
43
47
|
metadata:
|
|
44
48
|
oauthlint-rule-id: AUTH-COOKIE-002
|
|
45
49
|
oauthlint-doc-url: https://oauthlint.dev/rules/cookie-no-httponly
|
|
@@ -41,7 +41,11 @@ rules:
|
|
|
41
41
|
- pattern: "{..., sameSite: 'None', ...}"
|
|
42
42
|
- pattern: '{..., sameSite: "None", ...}'
|
|
43
43
|
- pattern-not: '{..., secure: true, ...}'
|
|
44
|
-
|
|
44
|
+
# No autofix. The correct `SameSite` value is context-dependent — `Strict`
|
|
45
|
+
# suits most auth flows but breaks OAuth callbacks, which need `Lax`, while a
|
|
46
|
+
# cookie deliberately set cross-site needs `None` plus `Secure`. The tool
|
|
47
|
+
# cannot know which, and a spread template would also corrupt the 2-arg form
|
|
48
|
+
# (unbound `$OPTS`). So this rule deliberately ships no `fix:`.
|
|
45
49
|
metadata:
|
|
46
50
|
oauthlint-rule-id: AUTH-COOKIE-003
|
|
47
51
|
oauthlint-doc-url: https://oauthlint.dev/rules/cookie-no-samesite
|
|
@@ -39,11 +39,14 @@ rules:
|
|
|
39
39
|
- metavariable-regex:
|
|
40
40
|
metavariable: $NAME
|
|
41
41
|
regex: ^(['"])(?:[a-zA-Z0-9_-]*(?:session|sid|sess|auth|token|jwt|refresh|access)[a-zA-Z0-9_-]*)\1$
|
|
42
|
-
#
|
|
43
|
-
# options object
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
|
|
42
|
+
# No autofix. A single rule-level `fix:` cannot be made safe here: this rule
|
|
43
|
+
# matches three shapes — an options object missing `secure`, the 2-arg form
|
|
44
|
+
# with no options object at all, and an explicit `secure: false`. A spread
|
|
45
|
+
# template like `{ ...$OPTS, secure: true }` corrupts the 2-arg form (`$OPTS`
|
|
46
|
+
# is unbound, so Semgrep emits the literal text `$OPTS`) and leaves an
|
|
47
|
+
# explicit `secure: false` key in place (the finding re-fires). Inserting a
|
|
48
|
+
# brand-new argument/key is not a clean literal replacement, so we ship no
|
|
49
|
+
# `fix:` rather than a rewrite that can break source.
|
|
47
50
|
metadata:
|
|
48
51
|
oauthlint-rule-id: AUTH-COOKIE-001
|
|
49
52
|
oauthlint-doc-url: https://oauthlint.dev/rules/cookie-no-secure
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.flow.oauth-credential-in-log
|
|
3
|
+
languages:
|
|
4
|
+
- javascript
|
|
5
|
+
- typescript
|
|
6
|
+
severity: ERROR
|
|
7
|
+
message: |
|
|
8
|
+
An OAuth/OIDC credential taken from the request — an authorization
|
|
9
|
+
`code`, an `access_token` / `refresh_token` / `id_token`, a bearer
|
|
10
|
+
`token`, a `client_secret`, or the raw `Authorization` header — flows
|
|
11
|
+
into a logging call (`console.*` or `logger.*`). Logs are written to
|
|
12
|
+
files, shipped to aggregators (Datadog, Splunk, CloudWatch) and read by
|
|
13
|
+
people and systems that should never see live credentials. A leaked
|
|
14
|
+
authorization code or token can be replayed to impersonate the user or
|
|
15
|
+
complete the OAuth exchange (CWE-532).
|
|
16
|
+
|
|
17
|
+
Never log the raw credential. Redact or mask it before logging
|
|
18
|
+
(`token.slice(0, 4) + '…'`), log a non-sensitive identifier instead
|
|
19
|
+
(a user id, a key id), or drop the field entirely.
|
|
20
|
+
# Taint mode so indirection is caught — `const at = req.query.access_token;
|
|
21
|
+
# logger.info(at)` flags, not just the inline `console.log(req.query.code)`
|
|
22
|
+
# form. Distinct from auth.flow.secret-in-log (a search-mode rule keyed on
|
|
23
|
+
# the NAME of the logged identifier): this is a dataflow rule keyed on the
|
|
24
|
+
# request SOURCE, so it fires even when the credential is carried through an
|
|
25
|
+
# arbitrarily-named intermediate variable. The source list is narrowed to
|
|
26
|
+
# OAuth/OIDC credential fields (and the Authorization header), so logging a
|
|
27
|
+
# benign request field such as `req.query.page` does not fire. Routing the
|
|
28
|
+
# value through a redaction/masking helper or a truncating slice clears the
|
|
29
|
+
# taint.
|
|
30
|
+
mode: taint
|
|
31
|
+
pattern-sources:
|
|
32
|
+
# Dotted accessor: req.query.code / req.body.access_token / …
|
|
33
|
+
- patterns:
|
|
34
|
+
- pattern-either:
|
|
35
|
+
- pattern: $REQ.query.$K
|
|
36
|
+
- pattern: $REQ.body.$K
|
|
37
|
+
- pattern: $REQ.params.$K
|
|
38
|
+
- metavariable-regex:
|
|
39
|
+
metavariable: $K
|
|
40
|
+
regex: (?i)^(?:code|access[_-]?token|accesstoken|refresh[_-]?token|refreshtoken|id[_-]?token|idtoken|token|client[_-]?secret|clientsecret)$
|
|
41
|
+
# Index accessor: req.query['id_token'] / req.body["code"] / …
|
|
42
|
+
- patterns:
|
|
43
|
+
- pattern-either:
|
|
44
|
+
- pattern: $REQ.query[$K]
|
|
45
|
+
- pattern: $REQ.body[$K]
|
|
46
|
+
- pattern: $REQ.params[$K]
|
|
47
|
+
- metavariable-regex:
|
|
48
|
+
metavariable: $K
|
|
49
|
+
regex: (?i)^["'](?:code|access[_-]?token|accesstoken|refresh[_-]?token|refreshtoken|id[_-]?token|idtoken|token|client[_-]?secret|clientsecret)["']$
|
|
50
|
+
# Raw Authorization header (carries the bearer token / basic credentials).
|
|
51
|
+
- pattern: $REQ.headers.authorization
|
|
52
|
+
- pattern: $REQ.headers['authorization']
|
|
53
|
+
- pattern: $REQ.headers["authorization"]
|
|
54
|
+
- pattern: $REQ.get('authorization')
|
|
55
|
+
- pattern: $REQ.get('Authorization')
|
|
56
|
+
- pattern: $REQ.header('authorization')
|
|
57
|
+
- pattern: $REQ.header('Authorization')
|
|
58
|
+
pattern-sanitizers:
|
|
59
|
+
# Redaction / masking helpers, or a truncating slice/substring — the value
|
|
60
|
+
# that reaches the log is no longer the live credential.
|
|
61
|
+
- pattern: redact(...)
|
|
62
|
+
- pattern: mask(...)
|
|
63
|
+
- pattern: maskToken(...)
|
|
64
|
+
- pattern: $S.slice(...)
|
|
65
|
+
- pattern: $S.substring(...)
|
|
66
|
+
- pattern: $S.substr(...)
|
|
67
|
+
pattern-sinks:
|
|
68
|
+
# console.log/info/debug/warn/error(...) — any tainted argument fires.
|
|
69
|
+
- patterns:
|
|
70
|
+
- pattern-either:
|
|
71
|
+
- pattern: console.log(...)
|
|
72
|
+
- pattern: console.info(...)
|
|
73
|
+
- pattern: console.debug(...)
|
|
74
|
+
- pattern: console.warn(...)
|
|
75
|
+
- pattern: console.error(...)
|
|
76
|
+
# logger.<level>(...) — a log-named receiver with a log-level method.
|
|
77
|
+
- patterns:
|
|
78
|
+
- pattern: $LOG.$LEVEL(...)
|
|
79
|
+
- metavariable-regex:
|
|
80
|
+
metavariable: $LOG
|
|
81
|
+
regex: (?i)^.*log(?:ger)?$
|
|
82
|
+
- metavariable-regex:
|
|
83
|
+
metavariable: $LEVEL
|
|
84
|
+
regex: ^(?:log|info|debug|warn|warning|error|trace|fatal|verbose|silly)$
|
|
85
|
+
metadata:
|
|
86
|
+
oauthlint-rule-id: AUTH-FLOW-013
|
|
87
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/flow-oauth-credential-in-log
|
|
88
|
+
category: security
|
|
89
|
+
cwe: CWE-532
|
|
90
|
+
owasp: API8:2023
|
|
91
|
+
llm-prevalence: MEDIUM
|
|
92
|
+
technology:
|
|
93
|
+
- express
|
|
94
|
+
references:
|
|
95
|
+
- https://cwe.mitre.org/data/definitions/532.html
|
|
96
|
+
- https://datatracker.ietf.org/doc/html/rfc6749#section-10.3
|
|
97
|
+
- https://owasp.org/API-Security/editions/2023/en/0xa8-security-misconfiguration/
|
|
@@ -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/
|
|
@@ -14,13 +14,22 @@ rules:
|
|
|
14
14
|
Set `Secure: true` and `HttpOnly: true` on auth cookies, and add an
|
|
15
15
|
appropriate `SameSite` mode (for example `SameSite: http.SameSiteLaxMode`).
|
|
16
16
|
# Matches only the literal `false`. `Secure: true`, `HttpOnly: true`, and
|
|
17
|
-
# the absence of the field are not flagged.
|
|
18
|
-
#
|
|
19
|
-
#
|
|
17
|
+
# the absence of the field are not flagged. The `pattern-inside` keeps
|
|
18
|
+
# detection scoped to an `http.Cookie{...}` (and `&http.Cookie{...}`)
|
|
19
|
+
# composite literal, so an unrelated struct exposing a `Secure`/`HttpOnly`
|
|
20
|
+
# bool is never matched. `$FIELD` captures the offending field name so the
|
|
21
|
+
# narrow match — and the autofix below — targets just that field.
|
|
20
22
|
patterns:
|
|
21
|
-
- pattern-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
- pattern-inside: 'http.Cookie{...}'
|
|
24
|
+
- pattern: '$FIELD: false'
|
|
25
|
+
- metavariable-regex:
|
|
26
|
+
metavariable: $FIELD
|
|
27
|
+
regex: ^(Secure|HttpOnly)$
|
|
28
|
+
# Safe, deterministic autofix: flip the disabled flag to `true`. `$FIELD` is
|
|
29
|
+
# preserved, so `Secure: false` becomes `Secure: true` and `HttpOnly: false`
|
|
30
|
+
# becomes `HttpOnly: true` — each the secure value that resolves the finding,
|
|
31
|
+
# with every other field in the literal left untouched.
|
|
32
|
+
fix: '$FIELD: true'
|
|
24
33
|
metadata:
|
|
25
34
|
oauthlint-rule-id: AUTH-GO-COOKIE-001
|
|
26
35
|
oauthlint-doc-url: https://oauthlint.dev/rules/go-cookie-insecure
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.go.flow.oauth-credential-in-log
|
|
3
|
+
languages:
|
|
4
|
+
- go
|
|
5
|
+
severity: ERROR
|
|
6
|
+
message: |
|
|
7
|
+
An OAuth/OIDC credential taken from the HTTP request — an authorization
|
|
8
|
+
`code`, an `access_token` / `refresh_token` / `id_token`, a bearer
|
|
9
|
+
`token`, a `client_secret`, or the raw `Authorization` header — flows
|
|
10
|
+
into a logging call (`log.*`, `slog.*`, `fmt.Print*`, or a `logger.*`
|
|
11
|
+
method). Logs are written to files, shipped to aggregators (Datadog,
|
|
12
|
+
Splunk, CloudWatch) and read by people and systems that should never see
|
|
13
|
+
live credentials. A leaked authorization code or token can be replayed
|
|
14
|
+
to impersonate the user or complete the OAuth exchange (CWE-532).
|
|
15
|
+
|
|
16
|
+
Never log the raw credential. Redact or mask it before logging, log a
|
|
17
|
+
non-sensitive identifier instead (a user id, a key id), or drop the
|
|
18
|
+
field entirely.
|
|
19
|
+
# Taint mode so indirection is caught — `at := r.FormValue("access_token");
|
|
20
|
+
# log.Println(at)` flags, not just the inline form. The source list is
|
|
21
|
+
# narrowed to OAuth/OIDC credential field names (and the Authorization
|
|
22
|
+
# header), so logging a benign request field such as `r.FormValue("page")`
|
|
23
|
+
# does not fire. Routing the value through a redaction/masking helper clears
|
|
24
|
+
# the taint.
|
|
25
|
+
mode: taint
|
|
26
|
+
pattern-sources:
|
|
27
|
+
# Request getters keyed by a credential-looking parameter name.
|
|
28
|
+
- patterns:
|
|
29
|
+
- pattern-either:
|
|
30
|
+
- pattern: $R.URL.Query().Get($K)
|
|
31
|
+
- pattern: $R.FormValue($K)
|
|
32
|
+
- pattern: $R.PostFormValue($K)
|
|
33
|
+
- metavariable-regex:
|
|
34
|
+
metavariable: $K
|
|
35
|
+
regex: (?i)^"(code|access[_-]?token|accesstoken|refresh[_-]?token|refreshtoken|id[_-]?token|idtoken|token|client[_-]?secret|clientsecret)"$
|
|
36
|
+
# The raw Authorization header carries the bearer / basic credential.
|
|
37
|
+
- patterns:
|
|
38
|
+
- pattern: $R.Header.Get($K)
|
|
39
|
+
- metavariable-regex:
|
|
40
|
+
metavariable: $K
|
|
41
|
+
regex: (?i)^"authorization"$
|
|
42
|
+
pattern-sanitizers:
|
|
43
|
+
# Redaction / masking helpers — the value reaching the log is no longer
|
|
44
|
+
# the live credential.
|
|
45
|
+
- pattern: redact(...)
|
|
46
|
+
- pattern: mask(...)
|
|
47
|
+
- pattern: maskToken(...)
|
|
48
|
+
pattern-sinks:
|
|
49
|
+
# Standard library log / slog / fmt print sinks — any tainted argument.
|
|
50
|
+
- patterns:
|
|
51
|
+
- pattern-either:
|
|
52
|
+
- pattern: log.Print(...)
|
|
53
|
+
- pattern: log.Printf(...)
|
|
54
|
+
- pattern: log.Println(...)
|
|
55
|
+
- pattern: log.Fatal(...)
|
|
56
|
+
- pattern: log.Fatalf(...)
|
|
57
|
+
- pattern: log.Fatalln(...)
|
|
58
|
+
- pattern: log.Panic(...)
|
|
59
|
+
- pattern: log.Panicf(...)
|
|
60
|
+
- pattern: slog.Info(...)
|
|
61
|
+
- pattern: slog.Debug(...)
|
|
62
|
+
- pattern: slog.Warn(...)
|
|
63
|
+
- pattern: slog.Error(...)
|
|
64
|
+
- pattern: fmt.Print(...)
|
|
65
|
+
- pattern: fmt.Printf(...)
|
|
66
|
+
- pattern: fmt.Println(...)
|
|
67
|
+
# A log-named receiver with a log-level method: logger.Info(...),
|
|
68
|
+
# myLog.Printf(...). The receiver-name constraint keeps this off
|
|
69
|
+
# unrelated method calls.
|
|
70
|
+
- patterns:
|
|
71
|
+
- pattern: $LOG.$LEVEL(...)
|
|
72
|
+
- metavariable-regex:
|
|
73
|
+
metavariable: $LOG
|
|
74
|
+
regex: (?i)^.*log(ger)?$
|
|
75
|
+
- metavariable-regex:
|
|
76
|
+
metavariable: $LEVEL
|
|
77
|
+
regex: ^(Print|Printf|Println|Info|Infof|Infow|Debug|Debugf|Debugw|Warn|Warnf|Warning|Error|Errorf|Errorw|Fatal|Fatalf|Panic|Panicf|Trace|Tracef)$
|
|
78
|
+
metadata:
|
|
79
|
+
oauthlint-rule-id: AUTH-GO-FLOW-005
|
|
80
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/go-flow-oauth-credential-in-log
|
|
81
|
+
category: security
|
|
82
|
+
cwe: CWE-532
|
|
83
|
+
owasp: API8:2023
|
|
84
|
+
llm-prevalence: MEDIUM
|
|
85
|
+
technology:
|
|
86
|
+
- net/http
|
|
87
|
+
references:
|
|
88
|
+
- https://cwe.mitre.org/data/definitions/532.html
|
|
89
|
+
- https://datatracker.ietf.org/doc/html/rfc6749#section-10.3
|
|
90
|
+
- https://owasp.org/API-Security/editions/2023/en/0xa8-security-misconfiguration/
|
|
@@ -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,68 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.go.jwt.untrusted-verify-key
|
|
3
|
+
languages:
|
|
4
|
+
- go
|
|
5
|
+
severity: ERROR
|
|
6
|
+
message: |
|
|
7
|
+
Untrusted request input flows into the verification key returned by a
|
|
8
|
+
`golang-jwt` `Keyfunc` (or into the `WithValidMethods` allowlist). When
|
|
9
|
+
the attacker controls the key, they sign their own forged token and
|
|
10
|
+
supply the matching key, so every token "verifies" — a complete
|
|
11
|
+
authentication bypass. When the attacker controls the accepted methods,
|
|
12
|
+
they can downgrade verification and defeat the signature check (CWE-347,
|
|
13
|
+
Improper Verification of Cryptographic Signature).
|
|
14
|
+
|
|
15
|
+
The verification key and the accepted algorithms must be fixed
|
|
16
|
+
server-side. Return the key from trusted configuration or a vetted key
|
|
17
|
+
set keyed by a validated `kid`, and pin accepted methods to a constant
|
|
18
|
+
allowlist — never resolve them from `r.URL.Query()`, `r.FormValue`, or a
|
|
19
|
+
request header.
|
|
20
|
+
# Taint mode. The source is request input; the sink is the value RETURNED
|
|
21
|
+
# from a keyfunc (focused on the key expression, not the token) or the
|
|
22
|
+
# argument to `jwt.WithValidMethods`. Focusing on the key/methods — never
|
|
23
|
+
# the token, which is supposed to come from the request — keeps this from
|
|
24
|
+
# firing on every correct call. Distinct from auth.go.jwt.unchecked-method
|
|
25
|
+
# (a keyfunc that skips the `token.Method` check): this is about a
|
|
26
|
+
# request-CONTROLLED key or method list. Routing the value through an
|
|
27
|
+
# allow-list / validation helper clears the taint.
|
|
28
|
+
mode: taint
|
|
29
|
+
pattern-sources:
|
|
30
|
+
- pattern: $R.URL.Query().Get(...)
|
|
31
|
+
- pattern: $R.URL.Query()[$K]
|
|
32
|
+
- pattern: $R.FormValue(...)
|
|
33
|
+
- pattern: $R.PostFormValue(...)
|
|
34
|
+
- pattern: $R.Header.Get(...)
|
|
35
|
+
- pattern: $R.PathValue(...)
|
|
36
|
+
pattern-sanitizers:
|
|
37
|
+
- pattern: isAllowedKey(...)
|
|
38
|
+
- pattern: validateKey(...)
|
|
39
|
+
- pattern: isAllowedAlgorithm(...)
|
|
40
|
+
- pattern: validateAlgorithm(...)
|
|
41
|
+
pattern-sinks:
|
|
42
|
+
# The key returned from a golang-jwt Keyfunc literal. Scoped with
|
|
43
|
+
# pattern-inside so only a keyfunc return is a sink, and focused on the
|
|
44
|
+
# returned key expression so the token argument is never the trigger.
|
|
45
|
+
- patterns:
|
|
46
|
+
- pattern-inside: |
|
|
47
|
+
func($T *jwt.Token) ($RET, error) {
|
|
48
|
+
...
|
|
49
|
+
}
|
|
50
|
+
- pattern: return $SINK, $E
|
|
51
|
+
- focus-metavariable: $SINK
|
|
52
|
+
# Request input used as the accepted signing methods.
|
|
53
|
+
- patterns:
|
|
54
|
+
- pattern: jwt.WithValidMethods($SINK)
|
|
55
|
+
- focus-metavariable: $SINK
|
|
56
|
+
metadata:
|
|
57
|
+
oauthlint-rule-id: AUTH-GO-JWT-006
|
|
58
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/go-jwt-untrusted-verify-key
|
|
59
|
+
category: security
|
|
60
|
+
cwe: CWE-347
|
|
61
|
+
owasp: API2:2023
|
|
62
|
+
llm-prevalence: LOW
|
|
63
|
+
technology:
|
|
64
|
+
- golang-jwt
|
|
65
|
+
references:
|
|
66
|
+
- https://cwe.mitre.org/data/definitions/347.html
|
|
67
|
+
- https://pkg.go.dev/github.com/golang-jwt/jwt/v5#Keyfunc
|
|
68
|
+
- https://datatracker.ietf.org/doc/html/rfc7518#section-3.1
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.go.oauth.insecure-token-endpoint
|
|
3
|
+
languages:
|
|
4
|
+
- go
|
|
5
|
+
severity: ERROR
|
|
6
|
+
message: |
|
|
7
|
+
An OAuth/OIDC endpoint is being contacted over cleartext `http://`.
|
|
8
|
+
Authorization codes, `client_secret`, access/refresh tokens, and the
|
|
9
|
+
`code_verifier` then travel unencrypted — a network attacker can read
|
|
10
|
+
or rewrite them and take over the flow.
|
|
11
|
+
|
|
12
|
+
RFC 6749 §3.1 / §10.9 require TLS for the authorization and token
|
|
13
|
+
endpoints. Use `https://` for every authorize, token, and userinfo
|
|
14
|
+
URL (including `oauth2.Endpoint{AuthURL, TokenURL}`).
|
|
15
|
+
`http://localhost` is fine for local development and is not flagged.
|
|
16
|
+
# A string literal that targets an OAuth/OIDC endpoint over http://.
|
|
17
|
+
# Required OAuth markers keep this precise: a generic http URL is NOT
|
|
18
|
+
# flagged, only one carrying an authorize/token request or an /oauth path.
|
|
19
|
+
# `https://` cannot match (the scheme is `http://` literally), and the
|
|
20
|
+
# localhost / loopback dev hosts are subtracted below.
|
|
21
|
+
patterns:
|
|
22
|
+
- pattern-regex: |-
|
|
23
|
+
"http://[^"\s]+(?:response_type=|client_id=|client_secret=|grant_type=|code_challenge=|/oauth2?/|/connect/token|/o/oauth2|/authorize|/oauth/token)[^"]*"
|
|
24
|
+
- pattern-not-regex: |-
|
|
25
|
+
http://(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])
|
|
26
|
+
metadata:
|
|
27
|
+
oauthlint-rule-id: AUTH-GO-OAUTH-002
|
|
28
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/go-oauth-insecure-token-endpoint
|
|
29
|
+
category: security
|
|
30
|
+
cwe: CWE-319
|
|
31
|
+
owasp: A02:2021
|
|
32
|
+
llm-prevalence: MEDIUM
|
|
33
|
+
technology:
|
|
34
|
+
- oauth2
|
|
35
|
+
- oidc
|
|
36
|
+
references:
|
|
37
|
+
- https://datatracker.ietf.org/doc/html/rfc6749#section-3.1
|
|
38
|
+
- https://datatracker.ietf.org/doc/html/rfc6749#section-10.9
|
|
39
|
+
- https://cwe.mitre.org/data/definitions/319.html
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.go.oauth.ropc-grant
|
|
3
|
+
languages:
|
|
4
|
+
- go
|
|
5
|
+
severity: ERROR
|
|
6
|
+
message: |
|
|
7
|
+
OAuth token request uses the Resource Owner Password Credentials
|
|
8
|
+
grant (`grant_type=password`). The app collects the user's password
|
|
9
|
+
and replays it to the authorization server — exactly what OAuth was
|
|
10
|
+
designed to avoid. It cannot support federation, MFA, or step-up
|
|
11
|
+
auth, and any compromise of your service exposes raw user passwords.
|
|
12
|
+
|
|
13
|
+
The OAuth 2.0 Security BCP (RFC 9700 §2.4) forbids ROPC and OAuth 2.1
|
|
14
|
+
removes it entirely. Use the authorization-code flow with PKCE
|
|
15
|
+
(`grant_type=authorization_code`) for user login, or
|
|
16
|
+
`client_credentials` for machine-to-machine. With `golang.org/x/oauth2`,
|
|
17
|
+
avoid `Config.PasswordCredentialsToken` and use `AuthCodeURL` /
|
|
18
|
+
`Exchange` instead.
|
|
19
|
+
pattern-either:
|
|
20
|
+
# golang.org/x/oauth2: Config.PasswordCredentialsToken(ctx, user, pass)
|
|
21
|
+
# is the ROPC helper — its only purpose is the password grant.
|
|
22
|
+
- pattern: $C.PasswordCredentialsToken(...)
|
|
23
|
+
# url.Values builders: v.Set("grant_type", "password") /
|
|
24
|
+
# v.Add("grant_type", "password"). The value is bounded so
|
|
25
|
+
# `password_reset` and friends are not matched.
|
|
26
|
+
- pattern-regex: |-
|
|
27
|
+
"grant_type"\s*,\s*"password"
|
|
28
|
+
# url.Values / map composite literal: {"grant_type": {"password"}} or
|
|
29
|
+
# {"grant_type": "password"}.
|
|
30
|
+
- pattern-regex: |-
|
|
31
|
+
"grant_type"\s*:\s*\[?\s*\{?\s*"password"
|
|
32
|
+
# URL-encoded request body string: "grant_type=password&username=…"
|
|
33
|
+
# (double-quoted or raw backtick string). The trailing class bounds the
|
|
34
|
+
# value so only the password grant matches.
|
|
35
|
+
- pattern-regex: |-
|
|
36
|
+
[?&"`]grant_type=password(?:[&"`\s\\]|$)
|
|
37
|
+
metadata:
|
|
38
|
+
oauthlint-rule-id: AUTH-GO-OAUTH-001
|
|
39
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/go-oauth-ropc-grant
|
|
40
|
+
category: security
|
|
41
|
+
cwe: CWE-522
|
|
42
|
+
owasp: API2:2023
|
|
43
|
+
llm-prevalence: MEDIUM
|
|
44
|
+
technology:
|
|
45
|
+
- oauth2
|
|
46
|
+
references:
|
|
47
|
+
- https://datatracker.ietf.org/doc/html/rfc9700#section-2.4
|
|
48
|
+
- https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1#section-2.4
|
|
49
|
+
- https://cwe.mitre.org/data/definitions/522.html
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.go.oauth.static-state
|
|
3
|
+
languages:
|
|
4
|
+
- go
|
|
5
|
+
severity: WARNING
|
|
6
|
+
message: |
|
|
7
|
+
OAuth authorization request is built with a hardcoded, constant `state`
|
|
8
|
+
value. A static `state` provides ZERO CSRF protection — the whole point
|
|
9
|
+
is an unguessable, per-request value that you store and then compare on
|
|
10
|
+
the callback. A literal that ships in your source is known to everyone
|
|
11
|
+
and identical on every request, so an attacker can forge a matching
|
|
12
|
+
callback.
|
|
13
|
+
|
|
14
|
+
Generate `state` fresh per request from a CSPRNG (e.g.
|
|
15
|
+
`crypto/rand` -> `base64.URLEncoding.EncodeToString(b)`), persist it in
|
|
16
|
+
the session/cookie, and verify it when the provider redirects back. With
|
|
17
|
+
`golang.org/x/oauth2`, pass that random value as the first argument to
|
|
18
|
+
`Config.AuthCodeURL(state, ...)`.
|
|
19
|
+
# `Config.AuthCodeURL(state, ...)` is the golang.org/x/oauth2 authorize-URL
|
|
20
|
+
# builder; its first argument is the CSRF `state`. A string LITERAL there is
|
|
21
|
+
# constant on every request and cannot provide CSRF protection. A
|
|
22
|
+
# per-request value is a variable (not a quoted literal) and does not match.
|
|
23
|
+
# The empty-string form is excluded — that is a missing state, covered
|
|
24
|
+
# elsewhere — so this fires only on a genuinely hardcoded value.
|
|
25
|
+
patterns:
|
|
26
|
+
- pattern: $C.AuthCodeURL("...", ...)
|
|
27
|
+
- pattern-not: $C.AuthCodeURL("", ...)
|
|
28
|
+
metadata:
|
|
29
|
+
oauthlint-rule-id: AUTH-GO-OAUTH-003
|
|
30
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/go-oauth-static-state
|
|
31
|
+
category: security
|
|
32
|
+
cwe: CWE-330
|
|
33
|
+
owasp: API1:2023
|
|
34
|
+
llm-prevalence: MEDIUM
|
|
35
|
+
technology:
|
|
36
|
+
- oauth2
|
|
37
|
+
references:
|
|
38
|
+
- https://datatracker.ietf.org/doc/html/rfc6749#section-10.12
|
|
39
|
+
- https://cwe.mitre.org/data/definitions/330.html
|
|
@@ -15,9 +15,20 @@ rules:
|
|
|
15
15
|
To trust a private CA in development, set `RootCAs` to a `*x509.CertPool`
|
|
16
16
|
loaded with that CA instead.
|
|
17
17
|
# Matches only the literal `true`. `InsecureSkipVerify: false` and the
|
|
18
|
-
# field's absence are not flagged.
|
|
19
|
-
#
|
|
20
|
-
|
|
18
|
+
# field's absence are not flagged. The `pattern-inside` keeps detection
|
|
19
|
+
# scoped to a `tls.Config{...}` (and `&tls.Config{...}`) composite literal,
|
|
20
|
+
# so an unrelated struct that happens to expose an `InsecureSkipVerify`
|
|
21
|
+
# field is never matched. Narrowing the matched node to the field itself
|
|
22
|
+
# (rather than the whole literal) is what lets the autofix below rewrite
|
|
23
|
+
# just that field and leave the surrounding `...` fields untouched.
|
|
24
|
+
patterns:
|
|
25
|
+
- pattern-inside: 'tls.Config{...}'
|
|
26
|
+
- pattern: 'InsecureSkipVerify: true'
|
|
27
|
+
# Safe, deterministic autofix: flip the boolean to `false`, the secure
|
|
28
|
+
# default that restores certificate verification and fully resolves the
|
|
29
|
+
# finding. Only the `true` literal changes; every other field in the
|
|
30
|
+
# `tls.Config` is preserved verbatim.
|
|
31
|
+
fix: 'InsecureSkipVerify: false'
|
|
21
32
|
metadata:
|
|
22
33
|
oauthlint-rule-id: AUTH-GO-TLS-001
|
|
23
34
|
oauthlint-doc-url: https://oauthlint.dev/rules/go-tls-insecure-skip-verify
|