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 +1 -1
- package/rules/cookie/samesite-none-insecure.yml +50 -0
- package/rules/go/jwt/skip-claims-validation.yml +36 -0
- package/rules/java/crypto/weak-hash.yml +41 -0
- package/rules/jwt/ignore-expiration.yml +43 -0
- package/rules/oauth/access-token-in-url.yml +39 -0
- package/rules/oauth/token-in-localstorage.yml +74 -0
- package/rules/py/cors/allow-all.yml +55 -0
- package/rules/py/jwt/no-expiration.yml +36 -0
- package/rules/rust/jwt/no-issuer-validation.yml +59 -0
- package/rules/rust/tls/accept-invalid-certs.yml +5 -0
- package/rules/rust/tls/accept-invalid-hostnames.yml +5 -0
- package/rules/tls/reject-unauthorized.yml +42 -0
package/package.json
CHANGED
|
@@ -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
|