oauthlint-rules 0.4.0 → 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/package.json +1 -1
- package/rules/express/cookie-insecure.yml +44 -0
- package/rules/java/crypto/noop-password-encoder.yml +38 -0
- 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/passport/jwt-ignore-expiration.yml +58 -0
- 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 +29 -17
- package/rules/py/jwt/verify-claims-disabled.yml +45 -0
- package/rules/py/oauth/token-request-verify-disabled.yml +14 -4
package/package.json
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.express.cookie-insecure
|
|
3
|
+
languages:
|
|
4
|
+
- javascript
|
|
5
|
+
- typescript
|
|
6
|
+
severity: WARNING
|
|
7
|
+
message: |
|
|
8
|
+
An Express session cookie is explicitly configured as insecure. Setting
|
|
9
|
+
`secure: false` lets the browser send the session cookie over plain HTTP,
|
|
10
|
+
and `httpOnly: false` exposes it to `document.cookie` (any XSS reads it).
|
|
11
|
+
|
|
12
|
+
Either drop the flag (the framework default may still need hardening) or
|
|
13
|
+
set `secure: true` and `httpOnly: true` for production. If you need
|
|
14
|
+
insecure cookies in dev, gate the value on `NODE_ENV !== 'production'`
|
|
15
|
+
rather than hard-coding `false`.
|
|
16
|
+
# Targets the session-middleware config object only, never `res.cookie(...)`
|
|
17
|
+
# (that shape is covered by the auth.cookie.* rules). We match ONLY an
|
|
18
|
+
# explicit `secure: false` / `httpOnly: false`; a MISSING key is left alone
|
|
19
|
+
# because express-session already defaults `secure` to false and dev setups
|
|
20
|
+
# legitimately omit it, so a missing key is undecidable prod-vs-dev and would
|
|
21
|
+
# be false-positive prone.
|
|
22
|
+
pattern-either:
|
|
23
|
+
# express-session: the flags live on a nested `cookie:` object.
|
|
24
|
+
- pattern: 'session({..., cookie: {..., secure: false, ...}, ...})'
|
|
25
|
+
- pattern: 'session({..., cookie: {..., httpOnly: false, ...}, ...})'
|
|
26
|
+
- pattern: 'expressSession({..., cookie: {..., secure: false, ...}, ...})'
|
|
27
|
+
- pattern: 'expressSession({..., cookie: {..., httpOnly: false, ...}, ...})'
|
|
28
|
+
# cookie-session: the flags live directly on the options object.
|
|
29
|
+
- pattern: 'cookieSession({..., secure: false, ...})'
|
|
30
|
+
- pattern: 'cookieSession({..., httpOnly: false, ...})'
|
|
31
|
+
metadata:
|
|
32
|
+
oauthlint-rule-id: AUTH-EXPRESS-001
|
|
33
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/express-cookie-insecure
|
|
34
|
+
category: security
|
|
35
|
+
cwe: CWE-614
|
|
36
|
+
owasp: A05:2021
|
|
37
|
+
llm-prevalence: MEDIUM
|
|
38
|
+
technology:
|
|
39
|
+
- express-session
|
|
40
|
+
- cookie-session
|
|
41
|
+
references:
|
|
42
|
+
- https://github.com/expressjs/session#cookiesecure
|
|
43
|
+
- https://github.com/expressjs/cookie-session#options
|
|
44
|
+
- https://cwe.mitre.org/data/definitions/614.html
|
|
@@ -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,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
|
|
@@ -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
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.py.django.cors-allow-all
|
|
3
|
+
languages:
|
|
4
|
+
- python
|
|
5
|
+
severity: WARNING
|
|
6
|
+
message: |
|
|
7
|
+
django-cors-headers is configured to allow every origin, disabling
|
|
8
|
+
cross-origin access control.
|
|
9
|
+
|
|
10
|
+
`CORS_ALLOW_ALL_ORIGINS = True` (or the legacy `CORS_ORIGIN_ALLOW_ALL =
|
|
11
|
+
True`) reflects any site's `Origin`, so ANY website can make cross-origin
|
|
12
|
+
requests to your API; combined with credentialed sessions this leaks
|
|
13
|
+
cookies, tokens and CSRF protections cross-origin (CWE-942, OWASP
|
|
14
|
+
A05:2021). Set it to `False` and list trusted origins explicitly, e.g.
|
|
15
|
+
`CORS_ALLOWED_ORIGINS = ["https://app.example.com"]`.
|
|
16
|
+
# Matches ONLY the literal `True` on the django-cors-headers settings keys.
|
|
17
|
+
# `= False` and an explicit `CORS_ALLOWED_ORIGINS = [...]` allow-list are
|
|
18
|
+
# never flagged. Distinct from `auth.py.cors.allow-all`, which targets the
|
|
19
|
+
# Flask-CORS `CORS(...)` / `@cross_origin(...)` call forms.
|
|
20
|
+
pattern-either:
|
|
21
|
+
- pattern: CORS_ALLOW_ALL_ORIGINS = True
|
|
22
|
+
- pattern: CORS_ORIGIN_ALLOW_ALL = True
|
|
23
|
+
metadata:
|
|
24
|
+
oauthlint-rule-id: AUTH-PY-DJANGO-001
|
|
25
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/py-django-cors-allow-all
|
|
26
|
+
category: security
|
|
27
|
+
cwe: CWE-942
|
|
28
|
+
owasp: A05:2021
|
|
29
|
+
llm-prevalence: MEDIUM
|
|
30
|
+
technology:
|
|
31
|
+
- django-cors-headers
|
|
32
|
+
references:
|
|
33
|
+
- https://github.com/adamchainz/django-cors-headers#cors_allow_all_origins-bool
|
|
34
|
+
- https://cwe.mitre.org/data/definitions/942.html
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.py.drf.default-authentication-empty
|
|
3
|
+
languages:
|
|
4
|
+
- python
|
|
5
|
+
severity: ERROR
|
|
6
|
+
message: |
|
|
7
|
+
DRF disables authentication globally with an empty
|
|
8
|
+
`DEFAULT_AUTHENTICATION_CLASSES` list.
|
|
9
|
+
|
|
10
|
+
An empty list means no authentication scheme runs, so `request.user` is
|
|
11
|
+
never populated from a credential and every view falls back to anonymous
|
|
12
|
+
access (CWE-306, OWASP A01:2021). Permission checks that rely on
|
|
13
|
+
`request.user` being authenticated then have nothing to enforce against.
|
|
14
|
+
|
|
15
|
+
Populate the list with the schemes you use, for example
|
|
16
|
+
`rest_framework.authentication.SessionAuthentication` and
|
|
17
|
+
`rest_framework.authentication.TokenAuthentication`.
|
|
18
|
+
# Scoped to the `REST_FRAMEWORK` settings dict; matches ONLY the empty list.
|
|
19
|
+
# A populated list such as `[SessionAuthentication]` is not matched.
|
|
20
|
+
pattern: 'REST_FRAMEWORK = {..., "DEFAULT_AUTHENTICATION_CLASSES": [], ...}'
|
|
21
|
+
metadata:
|
|
22
|
+
oauthlint-rule-id: AUTH-PY-DRF-002
|
|
23
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/py-drf-default-authentication-empty
|
|
24
|
+
category: security
|
|
25
|
+
cwe: CWE-306
|
|
26
|
+
owasp: A01:2021
|
|
27
|
+
llm-prevalence: MEDIUM
|
|
28
|
+
technology:
|
|
29
|
+
- djangorestframework
|
|
30
|
+
references:
|
|
31
|
+
- https://www.django-rest-framework.org/api-guide/authentication/#setting-the-authentication-scheme
|
|
32
|
+
- https://cwe.mitre.org/data/definitions/306.html
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.py.drf.default-permission-allowany
|
|
3
|
+
languages:
|
|
4
|
+
- python
|
|
5
|
+
severity: ERROR
|
|
6
|
+
message: |
|
|
7
|
+
DRF makes every endpoint public because `DEFAULT_PERMISSION_CLASSES` is set
|
|
8
|
+
to `AllowAny`.
|
|
9
|
+
|
|
10
|
+
With `AllowAny` as the project-wide default, every view that does not
|
|
11
|
+
override `permission_classes` skips authorization entirely, so any
|
|
12
|
+
unauthenticated caller can reach it (CWE-862, OWASP A01:2021). This is easy
|
|
13
|
+
to ship by accident because it silently exposes future endpoints too.
|
|
14
|
+
|
|
15
|
+
Set the global default to a real permission such as
|
|
16
|
+
`rest_framework.permissions.IsAuthenticated` and opt specific views out to
|
|
17
|
+
public only when you mean to.
|
|
18
|
+
# Scoped to the `REST_FRAMEWORK` settings dict. Matches `AllowAny` both as the
|
|
19
|
+
# imported symbol and as the `'rest_framework.permissions.AllowAny'` string.
|
|
20
|
+
# `IsAuthenticated` (or any other class), or the key being absent, is not matched.
|
|
21
|
+
pattern-either:
|
|
22
|
+
- pattern: 'REST_FRAMEWORK = {..., "DEFAULT_PERMISSION_CLASSES": [..., AllowAny, ...], ...}'
|
|
23
|
+
- pattern: 'REST_FRAMEWORK = {..., "DEFAULT_PERMISSION_CLASSES": [..., "rest_framework.permissions.AllowAny", ...], ...}'
|
|
24
|
+
metadata:
|
|
25
|
+
oauthlint-rule-id: AUTH-PY-DRF-001
|
|
26
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/py-drf-default-permission-allowany
|
|
27
|
+
category: security
|
|
28
|
+
cwe: CWE-862
|
|
29
|
+
owasp: A01:2021
|
|
30
|
+
llm-prevalence: HIGH
|
|
31
|
+
technology:
|
|
32
|
+
- djangorestframework
|
|
33
|
+
references:
|
|
34
|
+
- https://www.django-rest-framework.org/api-guide/permissions/#setting-the-permission-policy
|
|
35
|
+
- https://cwe.mitre.org/data/definitions/862.html
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.py.drf.view-authentication-disabled
|
|
3
|
+
languages:
|
|
4
|
+
- python
|
|
5
|
+
severity: ERROR
|
|
6
|
+
message: |
|
|
7
|
+
A DRF view disables authentication with an empty `authentication_classes`
|
|
8
|
+
list.
|
|
9
|
+
|
|
10
|
+
Setting `authentication_classes = []` on a view (or the
|
|
11
|
+
`@authentication_classes([])` decorator on a function view) turns off every
|
|
12
|
+
authentication scheme for that endpoint, so `request.user` is always
|
|
13
|
+
anonymous and any permission tied to an authenticated user cannot hold
|
|
14
|
+
(CWE-306, OWASP A01:2021).
|
|
15
|
+
|
|
16
|
+
List the schemes the view should accept, for example
|
|
17
|
+
`authentication_classes = [TokenAuthentication]`, instead of emptying it.
|
|
18
|
+
# Matches ONLY an explicitly emptied `authentication_classes`, as a class
|
|
19
|
+
# attribute or via the decorator. A populated list is not matched.
|
|
20
|
+
pattern-either:
|
|
21
|
+
- pattern: authentication_classes = []
|
|
22
|
+
- pattern: "@authentication_classes([])"
|
|
23
|
+
metadata:
|
|
24
|
+
oauthlint-rule-id: AUTH-PY-DRF-003
|
|
25
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/py-drf-view-authentication-disabled
|
|
26
|
+
category: security
|
|
27
|
+
cwe: CWE-306
|
|
28
|
+
owasp: A01:2021
|
|
29
|
+
llm-prevalence: MEDIUM
|
|
30
|
+
technology:
|
|
31
|
+
- djangorestframework
|
|
32
|
+
references:
|
|
33
|
+
- https://www.django-rest-framework.org/api-guide/views/#api_view
|
|
34
|
+
- https://cwe.mitre.org/data/definitions/306.html
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.py.flask.session-cookie-insecure
|
|
3
|
+
languages:
|
|
4
|
+
- python
|
|
5
|
+
severity: ERROR
|
|
6
|
+
message: |
|
|
7
|
+
A Flask cookie security flag is disabled through `app.config`, weakening
|
|
8
|
+
session and remember-me cookie protection.
|
|
9
|
+
|
|
10
|
+
`SESSION_COOKIE_SECURE = False` lets the session cookie travel over plain
|
|
11
|
+
HTTP where it can be sniffed on the wire, and `SESSION_COOKIE_HTTPONLY =
|
|
12
|
+
False` exposes it to JavaScript so an XSS payload can read and exfiltrate
|
|
13
|
+
it; the `REMEMBER_COOKIE_*` flags do the same for Flask-Login's long-lived
|
|
14
|
+
remember-me token (CWE-614, OWASP A05:2021). Keep these `True` (or drive
|
|
15
|
+
them from an environment check), e.g. `app.config["SESSION_COOKIE_SECURE"]
|
|
16
|
+
= True` and `app.config["SESSION_COOKIE_HTTPONLY"] = True`.
|
|
17
|
+
# Matches ONLY the literal `False` set via `app.config[...] = False` (the
|
|
18
|
+
# subscript form) or `app.config.update(...=False)` (the keyword form) — the
|
|
19
|
+
# `app.config` variants that the bare module-level assignment rule
|
|
20
|
+
# `auth.py.cookie.insecure-flags` does NOT see. `= True` and env-driven
|
|
21
|
+
# values are never flagged. `$APP` matches any Flask app variable name.
|
|
22
|
+
pattern-either:
|
|
23
|
+
- pattern: $APP.config['SESSION_COOKIE_SECURE'] = False
|
|
24
|
+
- pattern: $APP.config['SESSION_COOKIE_HTTPONLY'] = False
|
|
25
|
+
- pattern: $APP.config['REMEMBER_COOKIE_SECURE'] = False
|
|
26
|
+
- pattern: $APP.config['REMEMBER_COOKIE_HTTPONLY'] = False
|
|
27
|
+
- pattern: $APP.config.update(..., SESSION_COOKIE_SECURE=False, ...)
|
|
28
|
+
- pattern: $APP.config.update(..., REMEMBER_COOKIE_SECURE=False, ...)
|
|
29
|
+
metadata:
|
|
30
|
+
oauthlint-rule-id: AUTH-PY-FLASK-001
|
|
31
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/py-flask-session-cookie-insecure
|
|
32
|
+
category: security
|
|
33
|
+
cwe: CWE-614
|
|
34
|
+
owasp: A05:2021
|
|
35
|
+
llm-prevalence: MEDIUM
|
|
36
|
+
technology:
|
|
37
|
+
- flask
|
|
38
|
+
- flask-login
|
|
39
|
+
references:
|
|
40
|
+
- https://flask.palletsprojects.com/en/stable/config/#SESSION_COOKIE_SECURE
|
|
41
|
+
- https://flask-login.readthedocs.io/en/latest/#cookie-settings
|
|
42
|
+
- https://cwe.mitre.org/data/definitions/614.html
|
|
@@ -19,23 +19,35 @@ rules:
|
|
|
19
19
|
# Scoped to `requests.<method>(...)`, `requests.request(...)` and Session
|
|
20
20
|
# objects (`$SESSION.<method>(...)`). Fires only on the literal
|
|
21
21
|
# `verify=False`; `verify=True` and `verify="/path/ca.pem"` are not flagged.
|
|
22
|
-
|
|
23
|
-
- pattern:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
22
|
+
patterns:
|
|
23
|
+
- pattern-either:
|
|
24
|
+
- pattern: requests.get(..., verify=$V, ...)
|
|
25
|
+
- pattern: requests.post(..., verify=$V, ...)
|
|
26
|
+
- pattern: requests.put(..., verify=$V, ...)
|
|
27
|
+
- pattern: requests.delete(..., verify=$V, ...)
|
|
28
|
+
- pattern: requests.patch(..., verify=$V, ...)
|
|
29
|
+
- pattern: requests.head(..., verify=$V, ...)
|
|
30
|
+
- pattern: requests.options(..., verify=$V, ...)
|
|
31
|
+
- pattern: requests.request(..., verify=$V, ...)
|
|
32
|
+
- pattern: $SESSION.get(..., verify=$V, ...)
|
|
33
|
+
- pattern: $SESSION.post(..., verify=$V, ...)
|
|
34
|
+
- pattern: $SESSION.put(..., verify=$V, ...)
|
|
35
|
+
- pattern: $SESSION.delete(..., verify=$V, ...)
|
|
36
|
+
- pattern: $SESSION.patch(..., verify=$V, ...)
|
|
37
|
+
- pattern: $SESSION.head(..., verify=$V, ...)
|
|
38
|
+
- pattern: $SESSION.options(..., verify=$V, ...)
|
|
39
|
+
- pattern: $SESSION.request(..., verify=$V, ...)
|
|
40
|
+
# Fire only on the literal `False` (unchanged detection), and focus the
|
|
41
|
+
# match/fix on that value token so the autofix flips just it, leaving the
|
|
42
|
+
# rest of the call intact.
|
|
43
|
+
- metavariable-regex:
|
|
44
|
+
metavariable: $V
|
|
45
|
+
regex: ^False$
|
|
46
|
+
- focus-metavariable: $V
|
|
47
|
+
# Safe, deterministic autofix: `True` is the library default (verification
|
|
48
|
+
# on) and the exact value the rule treats as compliant, so it fully resolves
|
|
49
|
+
# the finding. Only the value token is rewritten (`verify=False` -> `verify=True`).
|
|
50
|
+
fix: "True"
|
|
39
51
|
metadata:
|
|
40
52
|
oauthlint-rule-id: AUTH-PY-FLOW-002
|
|
41
53
|
oauthlint-doc-url: https://oauthlint.dev/rules/py-flow-requests-verify-disabled
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.py.jwt.verify-claims-disabled
|
|
3
|
+
languages:
|
|
4
|
+
- python
|
|
5
|
+
severity: WARNING
|
|
6
|
+
message: |
|
|
7
|
+
PyJWT decode disables audience or issuer checks.
|
|
8
|
+
|
|
9
|
+
`jwt.decode(...)` is called with an `options` dict that turns off a claim
|
|
10
|
+
check: `"verify_aud": False`, `"verify_iss": False`, or
|
|
11
|
+
`"verify_nbf": False`. Skipping these lets a token minted for a different
|
|
12
|
+
audience or issuer (for example one from another tenant or a lower-trust
|
|
13
|
+
service) be accepted here, defeating the boundary those claims are meant
|
|
14
|
+
to enforce (CWE-347).
|
|
15
|
+
|
|
16
|
+
Remove the disabling option and validate the claim, e.g.
|
|
17
|
+
`jwt.decode(token, key, algorithms=["RS256"], audience="api",
|
|
18
|
+
issuer="https://issuer.example.com")`. PyJWT only checks `aud`/`iss` when
|
|
19
|
+
you pass the expected value, so supply it rather than disabling the check.
|
|
20
|
+
# Scoped to PyJWT's decode `options` dict. We match ONLY the aud/iss/nbf
|
|
21
|
+
# keys; `verify_signature` is reported by auth.py.jwt.no-verify and
|
|
22
|
+
# `verify_exp` by auth.py.jwt.no-expiration, so neither is matched here and
|
|
23
|
+
# we avoid duplicate findings.
|
|
24
|
+
patterns:
|
|
25
|
+
- pattern-either:
|
|
26
|
+
- pattern: jwt.decode(..., options=$OPTS, ...)
|
|
27
|
+
- pattern: decode(..., options=$OPTS, ...)
|
|
28
|
+
- metavariable-pattern:
|
|
29
|
+
metavariable: $OPTS
|
|
30
|
+
pattern-either:
|
|
31
|
+
- pattern: '{..., "verify_aud": False, ...}'
|
|
32
|
+
- pattern: '{..., "verify_iss": False, ...}'
|
|
33
|
+
- pattern: '{..., "verify_nbf": False, ...}'
|
|
34
|
+
metadata:
|
|
35
|
+
oauthlint-rule-id: AUTH-PY-JWT-008
|
|
36
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/py-jwt-verify-claims-disabled
|
|
37
|
+
category: security
|
|
38
|
+
cwe: CWE-347
|
|
39
|
+
owasp: API2:2023
|
|
40
|
+
llm-prevalence: MEDIUM
|
|
41
|
+
technology:
|
|
42
|
+
- pyjwt
|
|
43
|
+
references:
|
|
44
|
+
- https://pyjwt.readthedocs.io/en/stable/api.html#jwt.decode
|
|
45
|
+
- https://cwe.mitre.org/data/definitions/347.html
|
|
@@ -21,10 +21,20 @@ rules:
|
|
|
21
21
|
# `verify="/path/ca.pem"` are not flagged. This is distinct from
|
|
22
22
|
# auth.py.flow.requests-verify-disabled, which covers the `requests` HTTP
|
|
23
23
|
# verbs (`get`/`post`/…); here the sink is the OAuth token-exchange call.
|
|
24
|
-
|
|
25
|
-
- pattern:
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
patterns:
|
|
25
|
+
- pattern-either:
|
|
26
|
+
- pattern: $C.fetch_token(..., verify=$V, ...)
|
|
27
|
+
- pattern: $C.refresh_token(..., verify=$V, ...)
|
|
28
|
+
- pattern: $C.fetch_access_token(..., verify=$V, ...)
|
|
29
|
+
# Fire only on the literal `False` (unchanged detection), and focus the
|
|
30
|
+
# match/fix on that value token so the autofix flips just it.
|
|
31
|
+
- metavariable-regex:
|
|
32
|
+
metavariable: $V
|
|
33
|
+
regex: ^False$
|
|
34
|
+
- focus-metavariable: $V
|
|
35
|
+
# Safe, deterministic autofix: `True` is the default (verification on) and the
|
|
36
|
+
# exact value the rule treats as compliant, so it resolves the finding.
|
|
37
|
+
fix: "True"
|
|
28
38
|
metadata:
|
|
29
39
|
oauthlint-rule-id: AUTH-PY-OAUTH-005
|
|
30
40
|
oauthlint-doc-url: https://oauthlint.dev/rules/py-oauth-token-request-verify-disabled
|