oauthlint-rules 0.2.2 → 0.2.4

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.2",
3
+ "version": "0.2.4",
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,50 @@
1
+ rules:
2
+ - id: auth.cookie.samesite-none-insecure
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: WARNING
7
+ message: |
8
+ A cookie is being set with `SameSite=None` but WITHOUT `Secure`.
9
+ `SameSite=None` *requires* `Secure`: modern browsers reject a
10
+ `SameSite=None` cookie that is not also `Secure`, so the cookie is
11
+ silently dropped. Worse, if anything does accept it, the cookie is a
12
+ cross-site cookie travelling over plaintext — it can be sent over plain
13
+ HTTP and offers no CSRF protection at all.
14
+
15
+ Fix: add `secure: true`, and only use `SameSite=None` when you genuinely
16
+ need the cookie sent on cross-site requests; otherwise prefer
17
+ `SameSite=Strict` or `Lax`. See CWE-1275.
18
+ pattern-either:
19
+ # An options object that sets sameSite: 'none' (any casing) but does NOT
20
+ # also set secure: true. The `pattern-not` requires the ABSENCE of
21
+ # `secure: true`, which covers both "secure omitted entirely" and the
22
+ # explicit `secure: false` case — neither satisfies the negation.
23
+ - patterns:
24
+ - pattern-either:
25
+ - pattern: "{..., sameSite: 'none', ...}"
26
+ - pattern: '{..., sameSite: "none", ...}'
27
+ - pattern: "{..., sameSite: 'None', ...}"
28
+ - pattern: '{..., sameSite: "None", ...}'
29
+ - pattern-not: '{..., secure: true, ...}'
30
+ # Raw Set-Cookie header string carrying SameSite=None with no Secure
31
+ # attribute. We match the whole quoted/backtick cookie string so the
32
+ # Secure check spans the *entire* string (Secure may legitimately appear
33
+ # before OR after SameSite=None) rather than the narrow `SameSite=None`
34
+ # span. The negative lookahead `(?!...secure)` is anchored at the opening
35
+ # quote, so it fails the match if a `secure` attribute appears anywhere
36
+ # inside the literal — leaving only SameSite=None strings that lack it.
37
+ - pattern-regex: '(?i)(["''`])(?![^"''`\n]*\bsecure\b)[^"''`\n]*\bsamesite\s*=\s*none\b[^"''`\n]*\1'
38
+ metadata:
39
+ oauthlint-rule-id: AUTH-COOKIE-005
40
+ oauthlint-doc-url: https://oauthlint.dev/rules/cookie-samesite-none-insecure
41
+ category: security
42
+ cwe: CWE-1275
43
+ owasp: A05:2021
44
+ llm-prevalence: MEDIUM
45
+ technology:
46
+ - express
47
+ - cookie
48
+ references:
49
+ - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#samesitenone_requires_secure
50
+ - https://cwe.mitre.org/data/definitions/1275.html
@@ -0,0 +1,36 @@
1
+ rules:
2
+ - id: auth.go.jwt.skip-claims-validation
3
+ languages:
4
+ - go
5
+ severity: WARNING
6
+ message: |
7
+ A JWT parser is configured with `jwt.WithoutClaimsValidation()`, which
8
+ turns OFF the standard registered-claims validation golang-jwt performs by
9
+ default — the `exp` (expiry), `nbf` (not-before) and `iat` (issued-at)
10
+ checks. With validation disabled an expired or not-yet-valid token still
11
+ parses successfully, so a stolen or long-expired token is accepted as if
12
+ it were current (CWE-613).
13
+
14
+ Remove `jwt.WithoutClaimsValidation()` and let golang-jwt validate the
15
+ time-based claims. If a specific claim must be relaxed, scope it narrowly
16
+ (e.g. `jwt.WithLeeway(...)`) instead of disabling all claims validation.
17
+ # Presence-based: `jwt.WithoutClaimsValidation()` is a parser option whose
18
+ # only purpose is to disable claims validation, so matching the call
19
+ # expression anywhere it is passed (to `jwt.Parse`, `jwt.ParseWithClaims`
20
+ # or `jwt.NewParser`) is unambiguous and low false-positive. Normal
21
+ # `jwt.Parse(...)` / `jwt.ParseWithClaims(...)` calls without the option are
22
+ # NOT matched. `jwt` is the golang-jwt/jwt/v5 import used in the sibling
23
+ # rules.
24
+ pattern: jwt.WithoutClaimsValidation()
25
+ metadata:
26
+ oauthlint-rule-id: AUTH-GO-JWT-005
27
+ oauthlint-doc-url: https://oauthlint.dev/rules/go-jwt-skip-claims-validation
28
+ category: security
29
+ cwe: CWE-613
30
+ owasp: API2:2023
31
+ llm-prevalence: MEDIUM
32
+ technology:
33
+ - golang-jwt
34
+ references:
35
+ - https://pkg.go.dev/github.com/golang-jwt/jwt/v5#ParserOption
36
+ - https://cwe.mitre.org/data/definitions/613.html
@@ -0,0 +1,41 @@
1
+ rules:
2
+ - id: auth.java.crypto.weak-hash
3
+ languages:
4
+ - java
5
+ severity: WARNING
6
+ message: |
7
+ A broken hash algorithm (MD5 or SHA-1) is being instantiated via JCA
8
+ `MessageDigest.getInstance(...)`. MD5 and SHA-1 are cryptographically
9
+ broken: practical collision attacks exist, so they must NOT be used for
10
+ any security purpose — integrity checks, content/token fingerprints,
11
+ digital signatures, HMAC keys, or deduplication that a trust decision
12
+ depends on (CWE-328 / CWE-327).
13
+
14
+ Use SHA-256 or stronger (SHA-384, SHA-512, SHA-3):
15
+ `MessageDigest.getInstance("SHA-256")`. Note: this rule covers the general
16
+ weak-digest case; storing *passwords* needs a dedicated slow hasher
17
+ (BCrypt/Argon2/PBKDF2), which is enforced separately.
18
+ # Match only the algorithm string passed to MessageDigest.getInstance(...).
19
+ # The metavariable-regex flags the broken digests MD5, SHA-1 and the SHA1
20
+ # alias (case-insensitive, optional hyphen). It is scoped to the
21
+ # getInstance(...) instantiation site on purpose so it is low-FP and does
22
+ # NOT overlap auth.java.crypto.weak-password-hash, which instead anchors on
23
+ # a password-named digest()/update() call (and also flags SHA-256/SHA-512 in
24
+ # that password context). "SHA-256"/"SHA-512" are silent here.
25
+ patterns:
26
+ - pattern: java.security.MessageDigest.getInstance($ALG, ...)
27
+ - metavariable-regex:
28
+ metavariable: $ALG
29
+ regex: (?i)^"(md5|sha-?1)"$
30
+ metadata:
31
+ oauthlint-rule-id: AUTH-JAVA-CRYPTO-004
32
+ oauthlint-doc-url: https://oauthlint.dev/rules/java-crypto-weak-hash
33
+ category: security
34
+ cwe: CWE-328
35
+ owasp: A02:2021
36
+ llm-prevalence: MEDIUM
37
+ technology:
38
+ - java
39
+ references:
40
+ - https://cwe.mitre.org/data/definitions/328.html
41
+ - https://csrc.nist.gov/news/2022/nist-transitioning-away-from-sha-1-for-all-apps
@@ -0,0 +1,43 @@
1
+ rules:
2
+ - id: auth.jwt.ignore-expiration
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: WARNING
7
+ message: |
8
+ `jwt.verify(token, key, { ignoreExpiration: true })` from `jsonwebtoken`
9
+ disables the `exp` (expiry) claim check, so an expired token is accepted
10
+ as valid forever. A stolen or long-old token then never stops working,
11
+ defeating the whole point of short-lived access tokens.
12
+
13
+ Remove `ignoreExpiration: true` so the `exp` claim is enforced, and set a
14
+ sane `expiresIn` when signing (`jwt.sign(payload, key, { expiresIn: '15m'
15
+ })`). See CWE-613 (Insufficient Session Expiration).
16
+ # Scoped to the `jsonwebtoken` library via the common alias `jwt`, mirroring
17
+ # decode-without-verify.yml. We only flag `verify(...)` calls that explicitly
18
+ # pass `ignoreExpiration: true`; `verify(...)` without that option, and
19
+ # `ignoreExpiration: false`, are never matched. We also support the
20
+ # destructured import `import { verify } from 'jsonwebtoken'`, scoped to that
21
+ # import so an unrelated `verify()` is safe. The options object is matched
22
+ # with `{ ..., ignoreExpiration: true, ... }` so the flag is caught
23
+ # regardless of which other options (algorithms, maxAge, audience, ...)
24
+ # appear alongside it.
25
+ pattern-either:
26
+ - pattern: 'jwt.verify($T, $K, { ..., ignoreExpiration: true, ... })'
27
+ - patterns:
28
+ - pattern-inside: |
29
+ import { ..., verify, ... } from 'jsonwebtoken'
30
+ ...
31
+ - pattern: 'verify($T, $K, { ..., ignoreExpiration: true, ... })'
32
+ metadata:
33
+ oauthlint-rule-id: AUTH-JWT-011
34
+ oauthlint-doc-url: https://oauthlint.dev/rules/jwt-ignore-expiration
35
+ category: security
36
+ cwe: CWE-613
37
+ owasp: API2:2023
38
+ llm-prevalence: HIGH
39
+ technology:
40
+ - jsonwebtoken
41
+ references:
42
+ - https://github.com/auth0/node-jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback
43
+ - https://cwe.mitre.org/data/definitions/613.html
@@ -0,0 +1,39 @@
1
+ rules:
2
+ - id: auth.oauth.access-token-in-url
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: WARNING
7
+ message: |
8
+ An OAuth `access_token` (or `refresh_token` / `id_token`) is placed in a
9
+ URL query string. URLs leak: the full URL — token included — is recorded
10
+ in server and reverse-proxy access logs, saved in browser history, and
11
+ sent in the `Referer` header to every third-party CDN, analytics, and ad
12
+ script loaded by the destination page. A token in a URL is a leaked token.
13
+
14
+ Send the token in the `Authorization: Bearer …` header, or in a POST
15
+ request body. Never in the URL query string.
16
+
17
+ CWE-598: Use of GET Request Method With Sensitive Query Strings.
18
+ # Anchored to the query-param shape `?<name>=` / `&<name>=` (the `[?&]…=`
19
+ # form) so we fire on token-carrying URLs built as string or template
20
+ # literals, while staying silent on: a token in an `Authorization` header,
21
+ # a POST body / object property `{ access_token: t }` (no leading `?`/`&`
22
+ # and no trailing `=`), `params.set('access_token', …)` builders, and the
23
+ # bare word `access_token` in a comment or non-URL string. Case-insensitive
24
+ # on the param name. This is deliberately distinct from
25
+ # auth.flow.credentials-in-url (which excludes `access_token`) and
26
+ # auth.jwt.in-url (which only matches JWT-shaped `eyJ…` values).
27
+ pattern-regex: '[?&](?i:access_token|refresh_token|id_token)='
28
+ metadata:
29
+ oauthlint-rule-id: AUTH-OAUTH-013
30
+ oauthlint-doc-url: https://oauthlint.dev/rules/oauth-access-token-in-url
31
+ category: security
32
+ cwe: CWE-598
33
+ owasp: A05:2021
34
+ llm-prevalence: MEDIUM
35
+ technology:
36
+ - oauth
37
+ references:
38
+ - https://owasp.org/Top10/A05_2021-Security_Misconfiguration/
39
+ - https://cwe.mitre.org/data/definitions/598.html
@@ -0,0 +1,74 @@
1
+ rules:
2
+ - id: auth.oauth.token-in-localstorage
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: WARNING
7
+ message: |
8
+ An OAuth/OIDC token is being written to `localStorage` /
9
+ `sessionStorage` under a token-named key (`access_token`,
10
+ `refresh_token`, `id_token`, …). Web storage is readable by any
11
+ script on the origin, so any XSS — including a compromised
12
+ third-party dependency — can exfiltrate the token via
13
+ `getItem(...)`. There is no browser-side mitigation, unlike
14
+ `HttpOnly` cookies.
15
+
16
+ Keep access/refresh/id tokens in memory only, or have the server
17
+ issue them in a `Secure; HttpOnly; SameSite=Strict` cookie.
18
+ `sessionStorage` is no safer than `localStorage` against XSS — it
19
+ grants the same attacker capability.
20
+
21
+ See CWE-922: Insecure Storage of Sensitive Information.
22
+ # We scope to common OAuth token KEY NAMES (case-insensitively) so this
23
+ # stays low-FP: `localStorage.setItem("theme", …)` and other non-token
24
+ # keys are NOT flagged. The key is bound to a metavariable and constrained
25
+ # with metavariable-regex `(?i)(access|refresh|id)[_-]?token`, which matches
26
+ # access_token / refresh_token / id_token and the camelCase accessToken /
27
+ # refreshToken / idToken. This deliberately overlaps as little as possible
28
+ # with auth.jwt.localstorage, which keys off the strong word `token`/`jwt`
29
+ # in either key or value (incl. variable keys); this rule is presence-based
30
+ # on a fixed set of OAuth/OIDC token key names.
31
+ pattern-either:
32
+ # setItem("access_token", v) on localStorage / sessionStorage.
33
+ - patterns:
34
+ - pattern-either:
35
+ - pattern: localStorage.setItem("$KEY", $V)
36
+ - pattern: sessionStorage.setItem("$KEY", $V)
37
+ - pattern: window.localStorage.setItem("$KEY", $V)
38
+ - pattern: window.sessionStorage.setItem("$KEY", $V)
39
+ - metavariable-regex:
40
+ metavariable: $KEY
41
+ regex: (?i)^(access|refresh|id)[_-]?token$
42
+ # Property assignment: localStorage.accessToken = v
43
+ - patterns:
44
+ - pattern-either:
45
+ - pattern: localStorage.$KEY = $V
46
+ - pattern: sessionStorage.$KEY = $V
47
+ - pattern: window.localStorage.$KEY = $V
48
+ - pattern: window.sessionStorage.$KEY = $V
49
+ - metavariable-regex:
50
+ metavariable: $KEY
51
+ regex: (?i)^(access|refresh|id)[_-]?token$
52
+ # Bracket assignment: localStorage["access_token"] = v
53
+ - patterns:
54
+ - pattern-either:
55
+ - pattern: localStorage["$KEY"] = $V
56
+ - pattern: sessionStorage["$KEY"] = $V
57
+ - pattern: window.localStorage["$KEY"] = $V
58
+ - pattern: window.sessionStorage["$KEY"] = $V
59
+ - metavariable-regex:
60
+ metavariable: $KEY
61
+ regex: (?i)^(access|refresh|id)[_-]?token$
62
+ metadata:
63
+ oauthlint-rule-id: AUTH-OAUTH-012
64
+ oauthlint-doc-url: https://oauthlint.dev/rules/oauth-token-in-localstorage
65
+ category: security
66
+ cwe: CWE-922
67
+ owasp: A05:2021
68
+ llm-prevalence: HIGH
69
+ technology:
70
+ - localStorage
71
+ - sessionStorage
72
+ references:
73
+ - https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html#local-storage
74
+ - https://cwe.mitre.org/data/definitions/922.html
@@ -0,0 +1,55 @@
1
+ rules:
2
+ - id: auth.py.cors.allow-all
3
+ languages:
4
+ - python
5
+ severity: ERROR
6
+ message: |
7
+ Flask-CORS is configured with `supports_credentials=True` together with a
8
+ wildcard origin (`origins="*"`, `origins=["*"]`, or — since Flask-CORS
9
+ defaults to `*` — no `origins` argument at all). The CORS spec forbids the
10
+ `Access-Control-Allow-Origin: *` + `Access-Control-Allow-Credentials: true`
11
+ combination, so browsers will block it; the dangerous "fix" is to leave the
12
+ wildcard in place while keeping credentials on, which exposes credentialed
13
+ cross-origin access to ANY website (CWE-942). For OAuth/OIDC this leaks
14
+ cookies, session tokens and CSRF protections cross-origin.
15
+
16
+ Credentialed requests must use an explicit allow-list of trusted origins,
17
+ e.g. `CORS(app, origins=["https://app.example.com"], supports_credentials=True)`.
18
+ If you genuinely need a public, wildcard endpoint, drop credentials:
19
+ `CORS(app, origins="*")` (the default, `supports_credentials=False`).
20
+ # Match presence of `supports_credentials=True` paired with a wildcard origin
21
+ # (`origins="*"` / `origins=["*"]`) OR no `origins` argument (Flask-CORS then
22
+ # defaults to `*`). The explicit-wildcard arms fire on `CORS(...)` and the
23
+ # `@cross_origin(...)` decorator; the default-wildcard arm uses `pattern-not`
24
+ # to require that NO `origins=...` is present, so an explicit allow-list such
25
+ # as `origins=["https://app.example.com"]` is never flagged.
26
+ pattern-either:
27
+ # Explicit wildcard origin + credentials — CORS(...) call.
28
+ - pattern: CORS(..., origins="*", ..., supports_credentials=True, ...)
29
+ - pattern: CORS(..., supports_credentials=True, ..., origins="*", ...)
30
+ - pattern: CORS(..., origins=["*"], ..., supports_credentials=True, ...)
31
+ - pattern: CORS(..., supports_credentials=True, ..., origins=["*"], ...)
32
+ # Explicit wildcard origin + credentials — @cross_origin(...) decorator.
33
+ - pattern: '@cross_origin(..., origins="*", ..., supports_credentials=True, ...)'
34
+ - pattern: '@cross_origin(..., supports_credentials=True, ..., origins="*", ...)'
35
+ - pattern: '@cross_origin(..., origins=["*"], ..., supports_credentials=True, ...)'
36
+ - pattern: '@cross_origin(..., supports_credentials=True, ..., origins=["*"], ...)'
37
+ # Credentials on, NO origins argument → Flask-CORS defaults to wildcard.
38
+ - patterns:
39
+ - pattern: CORS(..., supports_credentials=True, ...)
40
+ - pattern-not: CORS(..., origins=$ORIGINS, ...)
41
+ - patterns:
42
+ - pattern: '@cross_origin(..., supports_credentials=True, ...)'
43
+ - pattern-not: '@cross_origin(..., origins=$ORIGINS, ...)'
44
+ metadata:
45
+ oauthlint-rule-id: AUTH-PY-CORS-001
46
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-cors-allow-all
47
+ category: security
48
+ cwe: CWE-942
49
+ owasp: A05:2021
50
+ llm-prevalence: MEDIUM
51
+ technology:
52
+ - flask-cors
53
+ references:
54
+ - https://flask-cors.readthedocs.io/en/latest/configuration.html
55
+ - https://cwe.mitre.org/data/definitions/942.html
@@ -0,0 +1,36 @@
1
+ rules:
2
+ - id: auth.py.jwt.no-expiration
3
+ languages:
4
+ - python
5
+ severity: WARNING
6
+ message: |
7
+ A JWT is decoded with `options={"verify_exp": False}`, which turns off
8
+ PyJWT's `exp` (expiration) check. With expiry verification disabled, an
9
+ expired — or stolen and long-since-revoked — token is still accepted, so
10
+ tokens effectively never expire.
11
+
12
+ Remove the `"verify_exp": False` option; PyJWT verifies `exp` by default,
13
+ e.g. `jwt.decode(token, key, algorithms=["RS256"])`. If a token legitimately
14
+ carries no `exp`, prefer `options={"require": ["exp"]}` to mandate one.
15
+ # Scoped to PyJWT's `jwt.decode(...)`. Matches the `options` dict containing
16
+ # `"verify_exp": False`, covering both `jwt.decode(...)` and a destructured
17
+ # `from jwt import decode` -> `decode(..., options=...)` call shape.
18
+ patterns:
19
+ - pattern-either:
20
+ - pattern: jwt.decode(..., options=$OPTS, ...)
21
+ - pattern: decode(..., options=$OPTS, ...)
22
+ - metavariable-pattern:
23
+ metavariable: $OPTS
24
+ pattern: '{..., "verify_exp": False, ...}'
25
+ metadata:
26
+ oauthlint-rule-id: AUTH-PY-JWT-005
27
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-jwt-no-expiration
28
+ category: security
29
+ cwe: CWE-613
30
+ owasp: API2:2023
31
+ llm-prevalence: HIGH
32
+ technology:
33
+ - PyJWT
34
+ references:
35
+ - https://pyjwt.readthedocs.io/en/stable/api.html#jwt.decode
36
+ - https://cwe.mitre.org/data/definitions/613.html
@@ -0,0 +1,59 @@
1
+ rules:
2
+ - id: auth.rust.jwt.no-issuer-validation
3
+ languages:
4
+ - rust
5
+ severity: WARNING
6
+ message: |
7
+ A JWT is decoded with a `jsonwebtoken` `Validation` that never sets the
8
+ expected issuer, so `decode` accepts a token minted by ANY issuer. The
9
+ `jsonwebtoken` crate does not validate the `iss` claim unless you opt in,
10
+ so a token signed by an attacker-controlled or otherwise untrusted issuer
11
+ passes validation as long as the signature checks out. For OAuth/OIDC this
12
+ lets a token from the wrong authorization server be replayed against this
13
+ API.
14
+
15
+ Pin the issuer before decoding, e.g.
16
+ `validation.set_issuer(&["https://issuer.example.com"])` (or set
17
+ `validation.iss`), so only tokens whose `iss` claim matches your trusted
18
+ authorization server are accepted.
19
+ # Modeled on auth.rust.jwt.no-aud-validation: detect a `Validation`
20
+ # (`Validation::new(...)` or `Validation::default()`) that flows into
21
+ # `decode(...)` but never has its issuer pinned. Unlike `aud`/`exp` (default
22
+ # `true`, disabled via `validate_* = false`), the issuer check is OFF by
23
+ # default and must be opted into via `set_issuer(...)` / `validation.iss`,
24
+ # so the vulnerable shape is the ABSENCE of that call. We bind the validator
25
+ # to `$V` at construction, require a `decode` that uses it, and suppress the
26
+ # finding with `pattern-not-inside` when `set_issuer` / `validation.iss` is
27
+ # set on the same `$V`. Matching on the construction (not the `decode` call)
28
+ # mirrors the sibling rule's low-FP, AST-only profile.
29
+ patterns:
30
+ - pattern-either:
31
+ - pattern: let mut $V = Validation::new(...);
32
+ - pattern: let mut $V = Validation::default();
33
+ - pattern-not-inside: |
34
+ let mut $V = ...;
35
+ ...
36
+ $V.set_issuer(...);
37
+ ...
38
+ - pattern-not-inside: |
39
+ let mut $V = ...;
40
+ ...
41
+ $V.iss = $X;
42
+ ...
43
+ - pattern-inside: |
44
+ let mut $V = ...;
45
+ ...
46
+ decode($TOKEN, $KEY, &$V)
47
+ metadata:
48
+ oauthlint-rule-id: AUTH-RUST-JWT-005
49
+ oauthlint-doc-url: https://oauthlint.dev/rules/rust-jwt-no-issuer-validation
50
+ category: security
51
+ cwe: CWE-345
52
+ owasp: API2:2023
53
+ llm-prevalence: MEDIUM
54
+ technology:
55
+ - jsonwebtoken
56
+ references:
57
+ - https://docs.rs/jsonwebtoken/latest/jsonwebtoken/struct.Validation.html#method.set_issuer
58
+ - https://cwe.mitre.org/data/definitions/345.html
59
+ - https://cwe.mitre.org/data/definitions/287.html
@@ -16,6 +16,11 @@ rules:
16
16
  # Matches only the literal `true`. `danger_accept_invalid_certs(false)` and
17
17
  # the method's absence are not flagged. `$B` is any builder expression.
18
18
  pattern: $B.danger_accept_invalid_certs(true)
19
+ # Safe, deterministic autofix: flip the boolean literal to `false`, which is
20
+ # the secure default and fully resolves the finding. `$B` (the builder
21
+ # expression) is preserved verbatim, so the surrounding chain is untouched —
22
+ # only `true` -> `false` changes.
23
+ fix: $B.danger_accept_invalid_certs(false)
19
24
  metadata:
20
25
  oauthlint-rule-id: AUTH-RUST-TLS-001
21
26
  oauthlint-doc-url: https://oauthlint.dev/rules/rust-tls-accept-invalid-certs
@@ -18,6 +18,11 @@ rules:
18
18
  # Matches only the literal `true`. `danger_accept_invalid_hostnames(false)`
19
19
  # and the method's absence are not flagged. `$B` is any builder expression.
20
20
  pattern: $B.danger_accept_invalid_hostnames(true)
21
+ # Safe, deterministic autofix: flip the boolean literal to `false`, which is
22
+ # the secure default and fully resolves the finding. `$B` (the builder
23
+ # expression) is preserved verbatim, so the surrounding chain is untouched —
24
+ # only `true` -> `false` changes.
25
+ fix: $B.danger_accept_invalid_hostnames(false)
21
26
  metadata:
22
27
  oauthlint-rule-id: AUTH-RUST-TLS-002
23
28
  oauthlint-doc-url: https://oauthlint.dev/rules/rust-tls-accept-invalid-hostnames
@@ -0,0 +1,42 @@
1
+ rules:
2
+ - id: auth.tls.reject-unauthorized
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: ERROR
7
+ message: |
8
+ Disabling TLS certificate validation (`rejectUnauthorized: false` or
9
+ `NODE_TLS_REJECT_UNAUTHORIZED=0`) makes the connection accept ANY
10
+ certificate, including self-signed or attacker-supplied ones. This
11
+ removes the protection TLS provides against man-in-the-middle attacks:
12
+ anyone able to intercept the network path can present a forged
13
+ certificate, then read and modify the traffic (credentials, tokens, data).
14
+
15
+ Keep certificate validation enabled. If the server uses a private or
16
+ self-signed CA, supply that CA explicitly instead of turning validation
17
+ off — e.g. `new https.Agent({ ca: fs.readFileSync('ca.pem') })`. See
18
+ CWE-295 and the Node.js TLS docs.
19
+ # Matches an options object literal that contains `rejectUnauthorized: false`,
20
+ # wherever it appears (https.request, new https.Agent, tls.connect, axios
21
+ # `httpsAgent`, node-fetch `agent`, etc.). `rejectUnauthorized` is a
22
+ # TLS-specific key, so matching the literal directly is very low FP. We also
23
+ # flag setting the global escape hatch `NODE_TLS_REJECT_UNAUTHORIZED` to "0".
24
+ # `rejectUnauthorized: true` and objects without the key are never matched.
25
+ pattern-either:
26
+ - pattern: '{..., rejectUnauthorized: false, ...}'
27
+ - pattern: 'process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"'
28
+ - pattern: "process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'"
29
+ metadata:
30
+ oauthlint-rule-id: AUTH-TLS-001
31
+ oauthlint-doc-url: https://oauthlint.dev/rules/tls-reject-unauthorized
32
+ category: security
33
+ cwe: CWE-295
34
+ owasp: A05:2021
35
+ llm-prevalence: HIGH
36
+ technology:
37
+ - node
38
+ - https
39
+ - axios
40
+ references:
41
+ - https://nodejs.org/api/tls.html#tlsconnectoptions-callback
42
+ - https://cwe.mitre.org/data/definitions/295.html