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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oauthlint-rules",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Semgrep rules catching the OAuth/OIDC/JWT anti-patterns that AI coding tools systematically produce.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,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
- pattern-either:
23
- - pattern: requests.get(..., verify=False, ...)
24
- - pattern: requests.post(..., verify=False, ...)
25
- - pattern: requests.put(..., verify=False, ...)
26
- - pattern: requests.delete(..., verify=False, ...)
27
- - pattern: requests.patch(..., verify=False, ...)
28
- - pattern: requests.head(..., verify=False, ...)
29
- - pattern: requests.options(..., verify=False, ...)
30
- - pattern: requests.request(..., verify=False, ...)
31
- - pattern: $SESSION.get(..., verify=False, ...)
32
- - pattern: $SESSION.post(..., verify=False, ...)
33
- - pattern: $SESSION.put(..., verify=False, ...)
34
- - pattern: $SESSION.delete(..., verify=False, ...)
35
- - pattern: $SESSION.patch(..., verify=False, ...)
36
- - pattern: $SESSION.head(..., verify=False, ...)
37
- - pattern: $SESSION.options(..., verify=False, ...)
38
- - pattern: $SESSION.request(..., verify=False, ...)
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
- pattern-either:
25
- - pattern: $C.fetch_token(..., verify=False, ...)
26
- - pattern: $C.refresh_token(..., verify=False, ...)
27
- - pattern: $C.fetch_access_token(..., verify=False, ...)
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