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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oauthlint-rules",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
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,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