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.
- package/dist/loader.d.ts +1 -1
- package/dist/schema.d.ts +19 -19
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +1 -4
- package/dist/schema.js.map +1 -1
- package/package.json +1 -1
- package/rules/cookie/no-samesite.yml +1 -1
- package/rules/cookie/samesite-none-insecure.yml +1 -1
- package/rules/cors/null-origin.yml +1 -1
- package/rules/cors/reflect-origin.yml +1 -1
- package/rules/express/cookie-insecure.yml +44 -0
- package/rules/flow/basic-auth-in-log.yml +99 -0
- package/rules/flow/open-redirect.yml +42 -21
- package/rules/flow/ssrf.yml +43 -22
- package/rules/flow/timing-unsafe-compare.yml +1 -1
- package/rules/go/cookie/insecure.yml +1 -1
- package/rules/go/cors/allow-all.yml +1 -1
- package/rules/go/flow/open-redirect.yml +16 -6
- package/rules/go/flow/ssrf.yml +16 -7
- package/rules/java/cookie/insecure.yml +1 -1
- package/rules/java/cors/allow-all.yml +1 -1
- package/rules/java/cors/credentialed-wildcard.yml +1 -1
- package/rules/java/crypto/noop-password-encoder.yml +38 -0
- package/rules/java/flow/ssrf.yml +113 -0
- package/rules/java/session/fixation-disabled.yml +1 -1
- package/rules/java/tls/trust-all-certs.yml +18 -7
- package/rules/java/web/security-ignoring-all.yml +38 -0
- package/rules/java/web/wildcard-permit-all.yml +37 -0
- package/rules/nextauth/hardcoded-secret.yml +62 -0
- package/rules/oauth/no-state.yml +9 -8
- package/rules/oauth/open-redirect-callback.yml +18 -3
- package/rules/oauth/static-state.yml +8 -7
- package/rules/passport/jwt-ignore-expiration.yml +58 -0
- package/rules/py/cookie/insecure-flags.yml +1 -1
- package/rules/py/cors/allow-all.yml +1 -1
- package/rules/py/cors/fastapi-wildcard-credentials.yml +49 -0
- package/rules/py/crypto/passlib-weak-scheme.yml +40 -0
- package/rules/py/django/cors-allow-all.yml +34 -0
- package/rules/py/drf/default-authentication-empty.yml +32 -0
- package/rules/py/drf/default-permission-allowany.yml +35 -0
- package/rules/py/drf/view-authentication-disabled.yml +34 -0
- package/rules/py/flask/session-cookie-insecure.yml +42 -0
- package/rules/py/flow/requests-verify-disabled.yml +30 -18
- package/rules/py/jwt/verify-claims-disabled.yml +45 -0
- package/rules/py/oauth/token-request-verify-disabled.yml +14 -4
- package/rules/rust/cookie/insecure.yml +1 -1
- package/rules/rust/cors/permissive.yml +1 -1
- package/rules/rust/flow/ssrf.yml +90 -0
- package/rules/tls/reject-unauthorized.yml +1 -1
package/rules/go/flow/ssrf.yml
CHANGED
|
@@ -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
|
|
24
|
-
#
|
|
25
|
-
# the
|
|
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
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
-
|
|
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
|
|
@@ -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
|
|
@@ -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
|
|
25
|
-
-
|
|
26
|
-
#
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
package/rules/oauth/no-state.yml
CHANGED
|
@@ -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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
32
|
-
# (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
@@ -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
|