oauthlint-rules 0.2.2 → 0.2.3
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/jwt/ignore-expiration.yml +43 -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/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,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,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,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
|