oauthlint-rules 0.3.1 → 0.5.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 (49) hide show
  1. package/dist/loader.d.ts +1 -1
  2. package/dist/schema.d.ts +19 -19
  3. package/dist/schema.d.ts.map +1 -1
  4. package/dist/schema.js +1 -4
  5. package/dist/schema.js.map +1 -1
  6. package/package.json +1 -1
  7. package/rules/cookie/no-samesite.yml +1 -1
  8. package/rules/cookie/samesite-none-insecure.yml +1 -1
  9. package/rules/cors/null-origin.yml +1 -1
  10. package/rules/cors/reflect-origin.yml +1 -1
  11. package/rules/express/cookie-insecure.yml +44 -0
  12. package/rules/flow/basic-auth-in-log.yml +99 -0
  13. package/rules/flow/open-redirect.yml +42 -21
  14. package/rules/flow/ssrf.yml +43 -22
  15. package/rules/flow/timing-unsafe-compare.yml +1 -1
  16. package/rules/go/cookie/insecure.yml +1 -1
  17. package/rules/go/cors/allow-all.yml +1 -1
  18. package/rules/go/flow/open-redirect.yml +16 -6
  19. package/rules/go/flow/ssrf.yml +16 -7
  20. package/rules/java/cookie/insecure.yml +1 -1
  21. package/rules/java/cors/allow-all.yml +1 -1
  22. package/rules/java/cors/credentialed-wildcard.yml +1 -1
  23. package/rules/java/crypto/noop-password-encoder.yml +38 -0
  24. package/rules/java/flow/ssrf.yml +113 -0
  25. package/rules/java/session/fixation-disabled.yml +1 -1
  26. package/rules/java/tls/trust-all-certs.yml +18 -7
  27. package/rules/java/web/security-ignoring-all.yml +38 -0
  28. package/rules/java/web/wildcard-permit-all.yml +37 -0
  29. package/rules/nextauth/hardcoded-secret.yml +62 -0
  30. package/rules/oauth/no-state.yml +9 -8
  31. package/rules/oauth/open-redirect-callback.yml +18 -3
  32. package/rules/oauth/static-state.yml +8 -7
  33. package/rules/passport/jwt-ignore-expiration.yml +58 -0
  34. package/rules/py/cookie/insecure-flags.yml +1 -1
  35. package/rules/py/cors/allow-all.yml +1 -1
  36. package/rules/py/cors/fastapi-wildcard-credentials.yml +49 -0
  37. package/rules/py/crypto/passlib-weak-scheme.yml +40 -0
  38. package/rules/py/django/cors-allow-all.yml +34 -0
  39. package/rules/py/drf/default-authentication-empty.yml +32 -0
  40. package/rules/py/drf/default-permission-allowany.yml +35 -0
  41. package/rules/py/drf/view-authentication-disabled.yml +34 -0
  42. package/rules/py/flask/session-cookie-insecure.yml +42 -0
  43. package/rules/py/flow/requests-verify-disabled.yml +30 -18
  44. package/rules/py/jwt/verify-claims-disabled.yml +45 -0
  45. package/rules/py/oauth/token-request-verify-disabled.yml +14 -4
  46. package/rules/rust/cookie/insecure.yml +1 -1
  47. package/rules/rust/cors/permissive.yml +1 -1
  48. package/rules/rust/flow/ssrf.yml +90 -0
  49. package/rules/tls/reject-unauthorized.yml +1 -1
@@ -20,9 +20,11 @@ rules:
20
20
  link-local address ranges before dialing. See CWE-918.
21
21
  # Taint mode so indirection (target := r.FormValue("endpoint");
22
22
  # http.Get(target)) is caught, not just the inline form. The taint is
23
- # cleared by an allow-list / host-validation helper, or by a
24
- # url.Parse(...)-then-checked flow where the parsed host is vetted before
25
- # the request is made.
23
+ # cleared by an allow-list / host-validation helper, or inside an
24
+ # `if`-guard that checks the parsed host against an allow-list. A bare
25
+ # `url.Parse(...)` is NOT a sanitizer: the parsed struct's Host/Path/Scheme
26
+ # are still attacker-controlled, so parse-then-use without a host check is a
27
+ # real SSRF and must still fire.
26
28
  mode: taint
27
29
  pattern-sources:
28
30
  # $R is the *http.Request. Cover the common net/http input shapes.
@@ -37,10 +39,17 @@ rules:
37
39
  - pattern: validateURL(...)
38
40
  - pattern: isAllowedHost(...)
39
41
  - pattern: $ALLOW.MatchString(...)
40
- # url.Parse-then-checked: a parsed URL whose host has been inspected is
41
- # treated as cleared (the request is built from a vetted destination
42
- # rather than the raw untrusted string).
43
- - pattern: url.Parse(...)
42
+ # parse-then-host-checked: a value used inside an `if`-guard that looks the
43
+ # parsed host up in an allow-list map is treated as vetted, mirroring the
44
+ # Python rule's `if is_allowed_url(...):` guard. A bare `url.Parse(...)` is
45
+ # deliberately NOT listed — it returns an attacker-controlled struct and
46
+ # does not validate anything, so parse-then-use still fires.
47
+ - patterns:
48
+ - pattern: $V
49
+ - pattern-inside: |
50
+ if $ALLOW[$U.Host] {
51
+ ...
52
+ }
44
53
  pattern-sinks:
45
54
  # Focus the URL argument so the finding lands on the tainted destination,
46
55
  # not the whole call. The plain call form is used (no aliasing) because
@@ -22,7 +22,7 @@ rules:
22
22
  oauthlint-doc-url: https://oauthlint.dev/rules/java-cookie-insecure
23
23
  category: security
24
24
  cwe: CWE-614
25
- owasp: A05:2021
25
+ owasp: API8:2023
26
26
  llm-prevalence: HIGH
27
27
  technology:
28
28
  - servlet
@@ -38,7 +38,7 @@ rules:
38
38
  oauthlint-doc-url: https://oauthlint.dev/rules/java-cors-allow-all
39
39
  category: security
40
40
  cwe: CWE-942
41
- owasp: A05:2021
41
+ owasp: API8:2023
42
42
  llm-prevalence: HIGH
43
43
  technology:
44
44
  - spring-web
@@ -34,7 +34,7 @@ rules:
34
34
  oauthlint-doc-url: https://oauthlint.dev/rules/java-cors-credentialed-wildcard
35
35
  category: security
36
36
  cwe: CWE-942
37
- owasp: A05:2021
37
+ owasp: API8:2023
38
38
  llm-prevalence: MEDIUM
39
39
  technology:
40
40
  - spring-web
@@ -0,0 +1,38 @@
1
+ rules:
2
+ - id: auth.java.crypto.noop-password-encoder
3
+ languages:
4
+ - java
5
+ severity: ERROR
6
+ message: |
7
+ Spring stores passwords with no hashing.
8
+
9
+ `NoOpPasswordEncoder` keeps passwords in plaintext and
10
+ `withDefaultPasswordEncoder()` is a builder helper that Spring explicitly
11
+ marks for non-production use only. Either way the stored credential is not
12
+ hashed, so anyone who reads the database or a backup recovers every
13
+ password directly (CWE-256). This is a common AI-generated shortcut: the
14
+ no-op encoder is pasted in to "get login working" and never replaced.
15
+
16
+ Hash passwords with a dedicated, slow, salted algorithm. Use
17
+ `new BCryptPasswordEncoder()`, `Argon2PasswordEncoder`, or
18
+ `Pbkdf2PasswordEncoder` instead. A `DelegatingPasswordEncoder` built via
19
+ `PasswordEncoderFactories.createDelegatingPasswordEncoder()` is the
20
+ recommended default.
21
+ # Two literal sinks: the singleton no-op encoder and the deprecated
22
+ # User.withDefaultPasswordEncoder() builder helper. Both are tight enough
23
+ # that a match is always a real plaintext-password configuration.
24
+ pattern-either:
25
+ - pattern: NoOpPasswordEncoder.getInstance()
26
+ - pattern: $U.withDefaultPasswordEncoder()
27
+ metadata:
28
+ oauthlint-rule-id: AUTH-JAVA-CRYPTO-005
29
+ oauthlint-doc-url: https://oauthlint.dev/rules/java-crypto-noop-password-encoder
30
+ category: security
31
+ cwe: CWE-256
32
+ owasp: A02:2021
33
+ llm-prevalence: HIGH
34
+ technology:
35
+ - spring-security
36
+ references:
37
+ - https://docs.spring.io/spring-security/reference/features/authentication/password-storage.html
38
+ - https://cwe.mitre.org/data/definitions/256.html
@@ -0,0 +1,113 @@
1
+ rules:
2
+ - id: auth.java.flow.ssrf
3
+ languages:
4
+ - java
5
+ severity: ERROR
6
+ # Non-production code (example apps, demos, docs, vendored copies, tests)
7
+ # is not the library surface users ship, so findings there are noise for a
8
+ # low-FP linter. Globs intentionally omit `**/test/**`-style fixture paths
9
+ # so the rule still fires on its own fixtures under rules/tests/fixtures/.
10
+ paths:
11
+ exclude:
12
+ - "**/src/test/**"
13
+ - "**/example/**"
14
+ - "**/examples/**"
15
+ - "**/demo/**"
16
+ - "**/docs/**"
17
+ message: |
18
+ Untrusted request input flows into the URL of an outbound HTTP request.
19
+ Because the destination is attacker-controlled, this is a Server-Side
20
+ Request Forgery (CWE-918): an attacker can point the request at internal
21
+ services behind your firewall, or at the cloud metadata endpoint
22
+ (http://169.254.169.254/...) to steal IAM/instance credentials and pivot
23
+ deeper into your infrastructure.
24
+
25
+ Never build a request URL straight from a Spring `@RequestParam` /
26
+ `@RequestBody` value or a raw `HttpServletRequest.getParameter(...)` /
27
+ `getHeader(...)` value. Validate the destination against an explicit
28
+ allow-list of hosts (resolve the URL and check its host against the
29
+ allow-list, rejecting private/loopback ranges) before issuing the request
30
+ with `RestTemplate`, `WebClient`, OkHttp, or Apache HttpClient.
31
+ # Taint mode so indirection is caught — `String u = req.getParameter("url");
32
+ # restTemplate.getForObject(u, ...)` flags, not just the inline form.
33
+ # Passing the value through a host allow-list / validation guard
34
+ # (isAllowedUrl, validateUrl, assertAllowedHost, or an allow-list
35
+ # `contains(...)` if-guard) clears the taint, so a genuinely vetted
36
+ # outbound request does not fire.
37
+ mode: taint
38
+ pattern-sources:
39
+ # Spring MVC handler parameters annotated with @RequestParam / @RequestBody
40
+ # carry attacker-controlled request data. Focus the parameter so the taint
41
+ # tracks the value, not the whole method.
42
+ - patterns:
43
+ - pattern-either:
44
+ - pattern: "$RET $M(..., @RequestParam $T $P, ...) {...}"
45
+ - pattern: "$RET $M(..., @RequestParam(...) $T $P, ...) {...}"
46
+ - pattern: "$RET $M(..., @RequestBody $T $P, ...) {...}"
47
+ - focus-metavariable: $P
48
+ # Servlet API request accessors. Constrained to an HttpServletRequest
49
+ # receiver so unrelated `.getParameter(...)` / `.getHeader(...)` calls on
50
+ # other objects are not treated as untrusted input.
51
+ - pattern: (HttpServletRequest $REQ).getParameter(...)
52
+ - pattern: (HttpServletRequest $REQ).getParameterValues(...)
53
+ - pattern: (HttpServletRequest $REQ).getHeader(...)
54
+ pattern-sanitizers:
55
+ # Routing the value through a host allow-list / validation helper clears
56
+ # the taint: only the vetted destination (not the raw request input)
57
+ # reaches the request.
58
+ - pattern: isAllowedUrl(...)
59
+ - pattern: isAllowedHost(...)
60
+ - pattern: validateUrl(...)
61
+ - pattern: assertAllowedHost(...)
62
+ # An inline allow-list membership guard vets the value: a value used inside
63
+ # `if (allow.contains(...)) { ... }` is treated as validated, mirroring the
64
+ # Python rule's `if is_allowed_url(v): ...`. The boolean-returning
65
+ # membership call itself does NOT sanitize (that clears only the boolean),
66
+ # so taint is cleared by the if-guard.
67
+ - patterns:
68
+ - pattern: $V
69
+ - pattern-inside: |
70
+ if (<... $ALLOW.contains(<... $V ...>) ...>) { ... }
71
+ pattern-sinks:
72
+ # Focus the URL argument so the finding lands on the tainted destination,
73
+ # not the whole call.
74
+ - patterns:
75
+ - pattern-either:
76
+ # Spring RestTemplate.
77
+ - pattern: $RT.getForObject($URL, ...)
78
+ - pattern: $RT.getForEntity($URL, ...)
79
+ - pattern: $RT.postForObject($URL, ...)
80
+ - pattern: $RT.postForEntity($URL, ...)
81
+ - pattern: $RT.exchange($URL, ...)
82
+ - pattern: $RT.execute($URL, ...)
83
+ # Spring WebClient fluent URI.
84
+ - pattern: $WC.uri($URL)
85
+ - pattern: $WC.uri($URL, ...)
86
+ # OkHttp Request.Builder.
87
+ - pattern: $B.url($URL)
88
+ # Apache HttpClient request objects.
89
+ - pattern: new HttpGet($URL)
90
+ - pattern: new HttpPost($URL)
91
+ - pattern: new HttpPut($URL)
92
+ - pattern: new HttpDelete($URL)
93
+ - pattern: new HttpHead($URL)
94
+ # JDK URL/URI opened directly.
95
+ - pattern: new URL($URL)
96
+ - pattern: new URI($URL)
97
+ - focus-metavariable: $URL
98
+ metadata:
99
+ oauthlint-rule-id: AUTH-JAVA-FLOW-001
100
+ oauthlint-doc-url: https://oauthlint.dev/rules/java-flow-ssrf
101
+ category: security
102
+ cwe: CWE-918
103
+ owasp: API7:2023
104
+ llm-prevalence: HIGH
105
+ technology:
106
+ - spring
107
+ - resttemplate
108
+ - webclient
109
+ - okhttp
110
+ - apache-httpclient
111
+ references:
112
+ - https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
113
+ - https://cwe.mitre.org/data/definitions/918.html
@@ -25,7 +25,7 @@ rules:
25
25
  oauthlint-doc-url: https://oauthlint.dev/rules/java-session-fixation-disabled
26
26
  category: security
27
27
  cwe: CWE-384
28
- owasp: A07:2021
28
+ owasp: API2:2023
29
29
  llm-prevalence: MEDIUM
30
30
  technology:
31
31
  - spring-security
@@ -21,13 +21,24 @@ rules:
21
21
  # HttpClient's NoopHostnameVerifier. A HostnameVerifier with real
22
22
  # (non-trivial) logic and the default verifier are not matched.
23
23
  pattern-either:
24
- # Lambda that always returns true: (hostname, session) -> true
25
- - pattern: ($HOST, $SESSION) -> true
26
- # Anonymous HostnameVerifier whose verify(...) just returns true.
27
- - pattern: |
28
- new HostnameVerifier() {
29
- public boolean verify(String $H, SSLSession $S) { return true; }
30
- }
24
+ # Lambda that always returns true, scoped to a hostname-verifier context
25
+ # so a generic two-arg lambda (BiPredicate, comparator, etc.) returning
26
+ # true is not mistaken for a permissive verifier.
27
+ - patterns:
28
+ - pattern: ($HOST, $SESSION) -> true
29
+ - pattern-either:
30
+ - pattern-inside: $X.setHostnameVerifier(...)
31
+ - pattern-inside: $X.setDefaultHostnameVerifier(...)
32
+ # Anonymous HostnameVerifier whose verify(...) just returns true, scoped to
33
+ # where it is actually installed as the verifier.
34
+ - patterns:
35
+ - pattern: |
36
+ new HostnameVerifier() {
37
+ public boolean verify(String $H, SSLSession $S) { return true; }
38
+ }
39
+ - pattern-either:
40
+ - pattern-inside: $X.setHostnameVerifier(...)
41
+ - pattern-inside: $X.setDefaultHostnameVerifier(...)
31
42
  # Apache HttpClient NoopHostnameVerifier (constructor or INSTANCE).
32
43
  - pattern: new org.apache.http.conn.ssl.NoopHostnameVerifier()
33
44
  - pattern: new NoopHostnameVerifier()
@@ -0,0 +1,38 @@
1
+ rules:
2
+ - id: auth.java.web.security-ignoring-all
3
+ languages:
4
+ - java
5
+ severity: ERROR
6
+ message: |
7
+ Spring excludes all paths from the security filter chain.
8
+
9
+ `WebSecurity.ignoring()` removes the matched paths from the Spring
10
+ Security filter chain entirely, so they get no authentication,
11
+ authorization, CSRF, or header protection at all. Passing the `/**`
12
+ wildcard excludes every request, leaving the whole application unprotected
13
+ (CWE-862). This is a common AI-generated shortcut to silence security
14
+ errors during development that then ships to production.
15
+
16
+ Never `ignoring()` a broad wildcard. Limit it to genuinely static,
17
+ non-sensitive assets, e.g.
18
+ `web.ignoring().requestMatchers("/css/**", "/js/**")`, or better, handle
19
+ authorization inside the filter chain with `permitAll()` on scoped paths
20
+ so the security headers still apply.
21
+ # Literal `/**` passed to ignoring() across the Security 6 (requestMatchers)
22
+ # and legacy (antMatchers) APIs. A scoped asset path like "/css/**" does not
23
+ # match the literal "/**", so static-resource excludes do not fire.
24
+ pattern-either:
25
+ - pattern: $WEB.ignoring().requestMatchers("/**")
26
+ - pattern: $WEB.ignoring().antMatchers("/**")
27
+ metadata:
28
+ oauthlint-rule-id: AUTH-JAVA-WEB-006
29
+ oauthlint-doc-url: https://oauthlint.dev/rules/java-web-security-ignoring-all
30
+ category: security
31
+ cwe: CWE-862
32
+ owasp: A01:2021
33
+ llm-prevalence: MEDIUM
34
+ technology:
35
+ - spring-security
36
+ references:
37
+ - https://docs.spring.io/spring-security/reference/servlet/configuration/java.html
38
+ - https://cwe.mitre.org/data/definitions/862.html
@@ -0,0 +1,37 @@
1
+ rules:
2
+ - id: auth.java.web.wildcard-permit-all
3
+ languages:
4
+ - java
5
+ severity: ERROR
6
+ message: |
7
+ Spring permits every request via a catch-all matcher.
8
+
9
+ The `/**` matcher matches every path, so granting it `permitAll()` makes
10
+ the whole application reachable without authentication, including
11
+ state-changing and sensitive endpoints (CWE-862, broken access control).
12
+ This is a common AI-generated Spring mistake: a wide-open matcher is
13
+ pasted in to "make it work" and the intended access rules are never added.
14
+
15
+ Open only the specific public routes explicitly, e.g.
16
+ `requestMatchers("/public/**").permitAll()`, and require authentication by
17
+ default with `anyRequest().authenticated()`. Granting `permitAll()` on a
18
+ scoped path is fine; granting it on `/**` is not.
19
+ # Literal `/**` wildcard granted permitAll across the three matcher APIs
20
+ # (requestMatchers in Security 6, antMatchers/mvcMatchers in the legacy
21
+ # chain). Scoped paths like "/public/**" do not match the literal "/**".
22
+ pattern-either:
23
+ - pattern: $X.requestMatchers("/**").permitAll()
24
+ - pattern: $X.antMatchers("/**").permitAll()
25
+ - pattern: $X.mvcMatchers("/**").permitAll()
26
+ metadata:
27
+ oauthlint-rule-id: AUTH-JAVA-WEB-005
28
+ oauthlint-doc-url: https://oauthlint.dev/rules/java-web-wildcard-permit-all
29
+ category: security
30
+ cwe: CWE-862
31
+ owasp: A01:2021
32
+ llm-prevalence: HIGH
33
+ technology:
34
+ - spring-security
35
+ references:
36
+ - https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-http-requests.html
37
+ - https://cwe.mitre.org/data/definitions/862.html
@@ -0,0 +1,62 @@
1
+ rules:
2
+ - id: auth.nextauth.hardcoded-secret
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: ERROR
7
+ message: |
8
+ The NextAuth/Auth.js `secret` is set to a hard-coded string literal.
9
+
10
+ This value signs and encrypts every session JWT and CSRF token. Committed
11
+ to git it is one search away from compromise, letting an attacker forge
12
+ sessions for any user. Read it from the environment instead:
13
+ `secret: process.env.AUTH_SECRET` (or `NEXTAUTH_SECRET`) and add the
14
+ variable to `.env.example` with a placeholder.
15
+ # Anchored to a NextAuth/Auth.js config so a bare `secret:` in unrelated
16
+ # code never fires: the property must sit inside a `NextAuth(...)` /
17
+ # `Auth(...)` call, an `authOptions`/`authConfig` object, or an object typed
18
+ # as `NextAuthOptions` / `NextAuthConfig` / `AuthOptions`. The value is an
19
+ # AST string literal (`"..."`), so `process.env.*` reads are structurally
20
+ # excluded; the regex allow-list drops `${ENV}` templates, `<placeholders>`,
21
+ # and obvious doc/test stubs. `paths.exclude` keeps the rule off test and
22
+ # example trees (where dev secrets like `"secret"` are intentional), while
23
+ # still firing on its own fixtures under rules/tests/fixtures/.
24
+ patterns:
25
+ - pattern: 'secret: "..."'
26
+ - pattern-not-regex: |-
27
+ (?i)secret\s*:\s*['"]\$\{?[A-Za-z_]+\}?['"]
28
+ - pattern-not-regex: |-
29
+ (?i)secret\s*:\s*['"]<[^'"]*>['"]
30
+ - pattern-not-regex: |-
31
+ (?i)secret\s*:\s*['"](?:your[-_]|my[-_]|example|placeholder|xxx+|todo|fixme|test|dummy|fake|sample|changeme|change[-_]?me|redacted|replace)
32
+ - pattern-either:
33
+ - pattern-inside: 'NextAuth({...})'
34
+ - pattern-inside: 'NextAuth($A, {...})'
35
+ - pattern-inside: 'NextAuth($A, $B, {...})'
36
+ - pattern-inside: 'Auth($A, {...})'
37
+ - pattern-inside: 'authOptions = {...}'
38
+ - pattern-inside: 'authConfig = {...}'
39
+ - pattern-inside: '$X: NextAuthOptions = {...}'
40
+ - pattern-inside: '$X: NextAuthConfig = {...}'
41
+ - pattern-inside: '$X: AuthOptions = {...}'
42
+ paths:
43
+ exclude:
44
+ - "**/test/**"
45
+ - "**/__tests__/**"
46
+ - "**/*.test.*"
47
+ - "**/*.spec.*"
48
+ - "**/example/**"
49
+ - "**/examples/**"
50
+ - "**/demo/**"
51
+ metadata:
52
+ oauthlint-rule-id: AUTH-NEXTAUTH-001
53
+ oauthlint-doc-url: https://oauthlint.dev/rules/nextauth-hardcoded-secret
54
+ category: security
55
+ cwe: CWE-798
56
+ owasp: API8:2023
57
+ llm-prevalence: HIGH
58
+ technology:
59
+ - next-auth
60
+ references:
61
+ - https://authjs.dev/getting-started/deployment#auth_secret
62
+ - https://cwe.mitre.org/data/definitions/798.html
@@ -14,14 +14,15 @@ rules:
14
14
  substitute for `state` when handling browser sessions.
15
15
  pattern-either:
16
16
  # Inline authorize URL carrying both client_id and response_type but no
17
- # state. The path is not hardcoded (Google uses /o/oauth2/v2/auth, etc.).
18
- - patterns:
19
- - pattern-regex: |-
20
- ['"]https?://[^'"\s]+\?[^'"]*client_id=[^'"]*['"]
21
- - pattern-regex: |-
22
- response_type=
23
- - pattern-not-regex: |-
24
- state=
17
+ # state, evaluated per quoted URL literal (the lookaheads/lookbehind are
18
+ # bounded to the contents of one string by `[^'"]`). This avoids the
19
+ # file-level false negative where a `state=` elsewhere in the file (a
20
+ # comment, a different correct call) would suppress a genuinely
21
+ # state-less URL. The path is not hardcoded (Google uses
22
+ # /o/oauth2/v2/auth, etc.). Single/double quotes only — a dynamic
23
+ # template-literal URL (backticks) is excluded.
24
+ - pattern-regex: |-
25
+ ['"]https?://(?=[^'"]*[?&]client_id=)(?=[^'"]*[?&]response_type=)(?![^'"]*[?&]state=)[^'"]+['"]
25
26
  # Programmatic URLSearchParams build (AST object match — robust to key
26
27
  # order and length). Fires when client_id + response_type are present but
27
28
  # state is not (covers both `state: x` and the `state` shorthand).
@@ -25,9 +25,24 @@ rules:
25
25
  - pattern: '$REQ.query[$K]'
26
26
  - pattern: '$REQ.body[$K]'
27
27
  pattern-sanitizers:
28
- - pattern: '$SET.has(...)'
29
- - pattern: '$ARR.includes(...)'
30
- - pattern: '$ARR.indexOf(...)'
28
+ # An inline allow-list membership guard vets the value: a value used inside
29
+ # `if (set.has(x)) { ... }` / `if (arr.includes(x)) { ... }` /
30
+ # `if (arr.indexOf(x) ...) { ... }` is treated as validated. The
31
+ # boolean-returning membership call itself does NOT sanitize its argument
32
+ # (that would only clear the boolean, not the value), so we clear taint by
33
+ # the if-guard, mirroring the Python rule's `if is_safe_url(v): ...`.
34
+ - patterns:
35
+ - pattern: $X
36
+ - pattern-inside: |
37
+ if (<... $SET.has($X) ...>) { ... }
38
+ - patterns:
39
+ - pattern: $X
40
+ - pattern-inside: |
41
+ if (<... $ARR.includes($X) ...>) { ... }
42
+ - patterns:
43
+ - pattern: $X
44
+ - pattern-inside: |
45
+ if (<... $ARR.indexOf($X) ...>) { ... }
31
46
  pattern-sinks:
32
47
  - patterns:
33
48
  - pattern-either:
@@ -28,13 +28,14 @@ rules:
28
28
  - pattern: 'new URLSearchParams({..., state: "$S", ...})'
29
29
  - pattern: "new URLSearchParams({..., state: '$S', ...})"
30
30
  # Inline authorize URL string literal carrying both response_type and a
31
- # constant state value. A dynamic state would be a template literal
32
- # (backticks) with `${...}`, which this single/double-quoted form excludes.
33
- - patterns:
34
- - pattern-regex: |-
35
- ['"]https?://[^'"\s]+\?[^'"]*response_type=[^'"]*['"]
36
- - pattern-regex: |-
37
- ['"]https?://[^'"\s]+\?[^'"]*state=[A-Za-z0-9._~%-]+[^'"]*['"]
31
+ # constant state value WITHIN THE SAME quoted literal. Using a single
32
+ # regex (lookaheads bounded by `[^'"]`) prevents the file-level false
33
+ # positive where `response_type=` in one string and a constant `state=`
34
+ # in a different string would otherwise combine into a finding. A dynamic
35
+ # state would be a template literal (backticks) with `${...}`, which this
36
+ # single/double-quoted form excludes.
37
+ - pattern-regex: |-
38
+ ['"]https?://(?=[^'"]*[?&]response_type=)(?=[^'"]*[?&]state=[A-Za-z0-9._~%-]+)[^'"]+['"]
38
39
  metadata:
39
40
  oauthlint-rule-id: AUTH-OAUTH-016
40
41
  oauthlint-doc-url: https://oauthlint.dev/rules/oauth-static-state
@@ -0,0 +1,58 @@
1
+ rules:
2
+ - id: auth.passport.jwt-ignore-expiration
3
+ languages:
4
+ - javascript
5
+ - typescript
6
+ severity: WARNING
7
+ message: |
8
+ A `passport-jwt` strategy is configured with `ignoreExpiration: true`.
9
+
10
+ This disables the `exp` claim check, so the strategy authenticates expired
11
+ tokens forever. A leaked or long-old JWT then never stops working,
12
+ defeating short-lived access tokens. Remove `ignoreExpiration: true` (the
13
+ default is `false`, which enforces `exp`) and issue tokens with a short
14
+ lifetime. See CWE-613 (Insufficient Session Expiration).
15
+ # Scoped to `passport-jwt` so an unrelated `ignoreExpiration: true` (in some
16
+ # other library's options) never fires. The file must import the
17
+ # `passport-jwt` Strategy (named, aliased, default, or via `require`), and
18
+ # the option must sit inside a `new ...Strategy({...}, ...)` construction.
19
+ # `new $S({...}, ...)` matches passport-jwt's `(opts, verify)` constructor
20
+ # for any binding name, so the common `new JwtStrategy({ ... }, verify)`
21
+ # alias spelling is covered without relying on Semgrep alias resolution. The
22
+ # match is narrowed to the `ignoreExpiration: true` property itself, so it
23
+ # is caught regardless of which sibling options appear. Non-overlapping with
24
+ # auth.jwt.ignore-expiration, which is scoped to `jsonwebtoken`'s
25
+ # `jwt.verify(...)`.
26
+ patterns:
27
+ - pattern-either:
28
+ - pattern-inside: |
29
+ import { ..., Strategy, ... } from 'passport-jwt'
30
+ ...
31
+ - pattern-inside: |
32
+ import Strategy from 'passport-jwt'
33
+ ...
34
+ - pattern-inside: |
35
+ $S = require('passport-jwt')
36
+ ...
37
+ - pattern-inside: |
38
+ $S = require('passport-jwt').Strategy
39
+ ...
40
+ - pattern-inside: |
41
+ { ..., Strategy, ... } = require('passport-jwt')
42
+ ...
43
+ - pattern-either:
44
+ - pattern-inside: 'new $S({...}, ...)'
45
+ - pattern-inside: 'new $S.Strategy({...}, ...)'
46
+ - pattern: 'ignoreExpiration: true'
47
+ metadata:
48
+ oauthlint-rule-id: AUTH-PASSPORT-001
49
+ oauthlint-doc-url: https://oauthlint.dev/rules/passport-jwt-ignore-expiration
50
+ category: security
51
+ cwe: CWE-613
52
+ owasp: API2:2023
53
+ llm-prevalence: MEDIUM
54
+ technology:
55
+ - passport-jwt
56
+ references:
57
+ - https://www.passportjs.org/packages/passport-jwt/
58
+ - https://cwe.mitre.org/data/definitions/613.html
@@ -30,7 +30,7 @@ rules:
30
30
  oauthlint-doc-url: https://oauthlint.dev/rules/py-cookie-insecure-flags
31
31
  category: security
32
32
  cwe: CWE-614
33
- owasp: A05:2021
33
+ owasp: API8:2023
34
34
  llm-prevalence: HIGH
35
35
  technology:
36
36
  - flask
@@ -46,7 +46,7 @@ rules:
46
46
  oauthlint-doc-url: https://oauthlint.dev/rules/py-cors-allow-all
47
47
  category: security
48
48
  cwe: CWE-942
49
- owasp: A05:2021
49
+ owasp: API8:2023
50
50
  llm-prevalence: MEDIUM
51
51
  technology:
52
52
  - flask-cors
@@ -0,0 +1,49 @@
1
+ rules:
2
+ - id: auth.py.cors.fastapi-wildcard-credentials
3
+ languages:
4
+ - python
5
+ severity: ERROR
6
+ message: |
7
+ FastAPI CORS allows any origin with credentials.
8
+
9
+ Starlette's `CORSMiddleware` is configured with a wildcard origin
10
+ (`allow_origins=["*"]` or `allow_origin_regex=".*"`) together with
11
+ `allow_credentials=True`. The CORS spec forbids
12
+ `Access-Control-Allow-Origin: *` alongside
13
+ `Access-Control-Allow-Credentials: true`, so Starlette silently reflects
14
+ the caller's `Origin` instead, turning the wildcard into "allow every
15
+ site" for credentialed requests. Any website can then read authenticated
16
+ responses, leaking cookies, session and OAuth tokens cross-origin
17
+ (CWE-942).
18
+
19
+ Credentialed CORS needs an explicit allow-list of trusted origins, e.g.
20
+ `allow_origins=["https://app.example.com"], allow_credentials=True`. If
21
+ the endpoint is genuinely public, drop credentials:
22
+ `allow_origins=["*"], allow_credentials=False`.
23
+ # Require the co-occurrence of a wildcard origin AND credentials on the SAME
24
+ # CORSMiddleware configuration. The first arm scopes to the `CORSMiddleware`
25
+ # symbol (either `app.add_middleware(CORSMiddleware, ...)` or a direct
26
+ # `CORSMiddleware(...)`); the AND-ed arms then demand both
27
+ # `allow_credentials=True` and a wildcard origin, so argument order does not
28
+ # matter and an explicit allow-list (or credentials disabled) never fires.
29
+ patterns:
30
+ - pattern-either:
31
+ - pattern: $APP.add_middleware(CORSMiddleware, ...)
32
+ - pattern: CORSMiddleware(...)
33
+ - pattern: $F(..., allow_credentials=True, ...)
34
+ - pattern-either:
35
+ - pattern: $F(..., allow_origins=["*"], ...)
36
+ - pattern: $F(..., allow_origin_regex=".*", ...)
37
+ metadata:
38
+ oauthlint-rule-id: AUTH-PY-CORS-002
39
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-cors-fastapi-wildcard-credentials
40
+ category: security
41
+ cwe: CWE-942
42
+ owasp: API8:2023
43
+ llm-prevalence: HIGH
44
+ technology:
45
+ - fastapi
46
+ references:
47
+ - https://www.starlette.io/middleware/#corsmiddleware
48
+ - https://fastapi.tiangolo.com/tutorial/cors/
49
+ - https://cwe.mitre.org/data/definitions/942.html
@@ -0,0 +1,40 @@
1
+ rules:
2
+ - id: auth.py.crypto.passlib-weak-scheme
3
+ languages:
4
+ - python
5
+ severity: ERROR
6
+ message: |
7
+ passlib configured with a weak or plaintext password scheme.
8
+
9
+ A `CryptContext` lists a password hashing scheme that is broken for
10
+ storing credentials: unsalted or fast digests (`hex_md5`, `hex_sha1`,
11
+ `hex_sha256`, `md5_crypt`, `ldap_md5`), legacy `des_crypt`, or outright
12
+ `plaintext` / `ldap_plaintext`. These are trivially brute-forced or
13
+ reversed, so any leaked hash exposes the underlying password (CWE-916).
14
+
15
+ Use a slow, salted, memory-hard scheme as the default, e.g.
16
+ `CryptContext(schemes=["argon2"])` or
17
+ `CryptContext(schemes=["bcrypt"])`. Keep a weak scheme only as a
18
+ `deprecated` verifier during migration, never as an active hashing scheme.
19
+ # Match the passlib `CryptContext(schemes=[...])` config where any element of
20
+ # the schemes list is in a CLOSED list of genuinely broken schemes. The
21
+ # `"$SCHEME"` string-literal metavariable captures each list element's text,
22
+ # and the anchored metavariable-regex matches ONLY the weak names, so modern
23
+ # schemes (bcrypt, argon2, scrypt, pbkdf2_sha256, ...) never fire.
24
+ patterns:
25
+ - pattern: CryptContext(..., schemes=[..., "$SCHEME", ...], ...)
26
+ - metavariable-regex:
27
+ metavariable: $SCHEME
28
+ regex: ^(md5_crypt|des_crypt|plaintext|hex_md5|hex_sha1|hex_sha256|ldap_md5|ldap_plaintext)$
29
+ metadata:
30
+ oauthlint-rule-id: AUTH-PY-CRYPTO-001
31
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-crypto-passlib-weak-scheme
32
+ category: security
33
+ cwe: CWE-916
34
+ owasp: A02:2021
35
+ llm-prevalence: MEDIUM
36
+ technology:
37
+ - passlib
38
+ references:
39
+ - https://passlib.readthedocs.io/en/stable/lib/passlib.context.html
40
+ - https://cwe.mitre.org/data/definitions/916.html