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.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/rules/cookie/no-httponly.yml +5 -1
  3. package/rules/cookie/no-samesite.yml +5 -1
  4. package/rules/cookie/no-secure.yml +8 -5
  5. package/rules/flow/oauth-credential-in-log.yml +97 -0
  6. package/rules/flow/secret-in-response.yml +70 -0
  7. package/rules/go/cookie/insecure.yml +15 -6
  8. package/rules/go/flow/oauth-credential-in-log.yml +90 -0
  9. package/rules/go/flow/secret-in-response.yml +69 -0
  10. package/rules/go/jwt/untrusted-verify-key.yml +68 -0
  11. package/rules/go/oauth/insecure-token-endpoint.yml +39 -0
  12. package/rules/go/oauth/ropc-grant.yml +49 -0
  13. package/rules/go/oauth/static-state.yml +39 -0
  14. package/rules/go/tls/insecure-skip-verify.yml +14 -3
  15. package/rules/go/tls/min-version.yml +12 -3
  16. package/rules/java/cors/credentialed-wildcard.yml +44 -0
  17. package/rules/java/jwt/none-algorithm.yml +37 -0
  18. package/rules/java/jwt/untrusted-verify-key.yml +54 -0
  19. package/rules/java/oauth/insecure-token-endpoint.yml +39 -0
  20. package/rules/java/oauth/ropc-grant.yml +45 -0
  21. package/rules/java/oauth/static-state.yml +38 -0
  22. package/rules/java/web/permit-all-actuator.yml +43 -0
  23. package/rules/jwt/ignore-expiration.yml +21 -11
  24. package/rules/jwt/untrusted-verify-key.yml +77 -0
  25. package/rules/oauth/insecure-token-endpoint.yml +41 -0
  26. package/rules/oauth/ropc-grant.yml +43 -0
  27. package/rules/oauth/static-state.yml +49 -0
  28. package/rules/py/flow/oauth-credential-in-log.yml +91 -0
  29. package/rules/py/flow/secret-in-response.yml +88 -0
  30. package/rules/py/jwt/algorithm-confusion.yml +48 -0
  31. package/rules/py/jwt/untrusted-verify-key.yml +75 -0
  32. package/rules/py/oauth/insecure-token-endpoint.yml +40 -0
  33. package/rules/py/oauth/insecure-transport-env.yml +43 -0
  34. package/rules/py/oauth/ropc-grant.yml +50 -0
  35. package/rules/py/oauth/static-state.yml +45 -0
  36. package/rules/py/oauth/token-request-verify-disabled.yml +41 -0
  37. package/rules/rust/jwt/algorithm-confusion.yml +50 -0
  38. package/rules/rust/oauth/insecure-token-endpoint.yml +39 -0
  39. package/rules/rust/oauth/ropc-grant.yml +40 -0
  40. package/rules/rust/oauth/static-state.yml +38 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oauthlint-rules",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
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",
@@ -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
- fix: '$RES.cookie($NAME, $VAL, { ...$OPTS, httpOnly: true })'
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
- fix: "$RES.cookie($NAME, $VAL, { ...$OPTS, sameSite: 'strict' })"
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
- # Auto-fix applies to the first (3-arg) pattern: spread the existing
43
- # options object and inject secure:true. Verbose but always-correct;
44
- # users can run prettier afterwards. The 2-arg branch is left alone
45
- # because rewriting it requires inserting a brand-new argument.
46
- fix: '$RES.cookie($NAME, $VAL, { ...$OPTS, secure: true })'
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. Works for `http.Cookie{...}`
18
- # and `&http.Cookie{...}` (the composite literal matches inside the
19
- # address-of).
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-either:
22
- - pattern: 'http.Cookie{..., Secure: false, ...}'
23
- - pattern: 'http.Cookie{..., HttpOnly: false, ...}'
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. Works for `tls.Config{...}` and
19
- # `&tls.Config{...}` (the composite literal matches inside the address-of).
20
- pattern: 'tls.Config{..., InsecureSkipVerify: true, ...}'
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