oauthlint-rules 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/schema.d.ts.map +1 -1
  2. package/dist/schema.js +10 -4
  3. package/dist/schema.js.map +1 -1
  4. package/package.json +9 -2
  5. package/rules/cors/null-origin.yml +53 -0
  6. package/rules/cors/reflect-origin.yml +78 -0
  7. package/rules/flow/credentials-in-url.yml +37 -0
  8. package/rules/flow/secret-in-log.yml +95 -0
  9. package/rules/flow/timing-unsafe-compare.yml +62 -16
  10. package/rules/flow/weak-bcrypt-rounds.yml +41 -0
  11. package/rules/flow/weak-password-hash.yml +74 -0
  12. package/rules/go/cookie/insecure.yml +35 -0
  13. package/rules/go/cors/allow-all.yml +44 -0
  14. package/rules/go/crypto/bcrypt-low-cost.yml +37 -0
  15. package/rules/go/crypto/weak-cipher.yml +38 -0
  16. package/rules/go/crypto/weak-password-hash.yml +47 -0
  17. package/rules/go/flow/weak-rand.yml +61 -0
  18. package/rules/go/jwt/hardcoded-secret.yml +37 -0
  19. package/rules/go/jwt/none-algorithm.yml +38 -0
  20. package/rules/go/jwt/parse-unverified.yml +33 -0
  21. package/rules/go/jwt/unchecked-method.yml +72 -0
  22. package/rules/go/tls/insecure-skip-verify.yml +32 -0
  23. package/rules/go/tls/min-version.yml +36 -0
  24. package/rules/java/cookie/insecure.yml +31 -0
  25. package/rules/java/cors/allow-all.yml +47 -0
  26. package/rules/java/crypto/ecb-mode.yml +42 -0
  27. package/rules/java/crypto/insecure-random.yml +62 -0
  28. package/rules/java/crypto/weak-password-hash.yml +47 -0
  29. package/rules/java/jwt/hardcoded-secret.yml +43 -0
  30. package/rules/java/jwt/unsigned-jwt.yml +41 -0
  31. package/rules/java/session/fixation-disabled.yml +34 -0
  32. package/rules/java/tls/trust-all-certs.yml +48 -0
  33. package/rules/java/web/csrf-disabled.yml +35 -0
  34. package/rules/java/web/frame-options-disabled.yml +37 -0
  35. package/rules/java/web/permit-all.yml +36 -0
  36. package/rules/jwt/decode-without-verify.yml +56 -0
  37. package/rules/jwt/localstorage.yml +13 -1
  38. package/rules/jwt/no-algorithms-allowlist.yml +38 -0
  39. package/rules/oauth/no-nonce.yml +55 -0
  40. package/rules/oauth/no-state-validation.yml +28 -6
  41. package/rules/oauth/pkce-plain.yml +40 -0
  42. package/rules/py/cookie/insecure-flags.yml +41 -0
  43. package/rules/py/flow/csrf-exempt.yml +39 -0
  44. package/rules/py/flow/debug-enabled.yml +37 -0
  45. package/rules/py/flow/insecure-random-token.yml +51 -0
  46. package/rules/py/flow/requests-verify-disabled.yml +50 -0
  47. package/rules/py/flow/weak-password-hash.yml +64 -0
  48. package/rules/py/jwt/alg-none.yml +41 -0
  49. package/rules/py/jwt/hardcoded-secret.yml +40 -0
  50. package/rules/py/jwt/no-algorithms.yml +45 -0
  51. package/rules/py/jwt/no-verify.yml +37 -0
  52. package/rules/py/secret/django-hardcoded-key.yml +32 -0
  53. package/rules/py/secret/flask-hardcoded-key.yml +33 -0
  54. package/rules/rust/cookie/insecure.yml +35 -0
  55. package/rules/rust/cors/permissive.yml +38 -0
  56. package/rules/rust/crypto/bcrypt-low-cost.yml +40 -0
  57. package/rules/rust/crypto/weak-cipher.yml +41 -0
  58. package/rules/rust/crypto/weak-password-hash.yml +46 -0
  59. package/rules/rust/flow/timing-unsafe-compare.yml +77 -0
  60. package/rules/rust/jwt/disable-signature-validation.yml +31 -0
  61. package/rules/rust/jwt/hardcoded-secret.yml +41 -0
  62. package/rules/rust/jwt/no-aud-validation.yml +34 -0
  63. package/rules/rust/jwt/no-expiration-validation.yml +35 -0
  64. package/rules/rust/tls/accept-invalid-certs.yml +30 -0
  65. package/rules/rust/tls/accept-invalid-hostnames.yml +32 -0
  66. package/rules/secret/public-env-secret.yml +58 -0
  67. package/rules/session/hardcoded-secret.yml +42 -0
@@ -0,0 +1,39 @@
1
+ rules:
2
+ - id: auth.py.flow.csrf-exempt
3
+ languages:
4
+ - python
5
+ severity: WARNING
6
+ message: |
7
+ A Django view disables CSRF protection. The `@csrf_exempt` decorator
8
+ (from `django.views.decorators.csrf`), or `@method_decorator(csrf_exempt,
9
+ ...)` on a class-based view, turns off Django's CSRF middleware check for
10
+ that endpoint. An attacker can then forge cross-site requests that the
11
+ victim's browser submits with their session cookie — a CSRF vulnerability.
12
+
13
+ Do not exempt views from CSRF. Keep the default protection and submit the
14
+ CSRF token from your client. For machine-to-machine endpoints such as
15
+ webhooks, validate a signed request signature (e.g. an HMAC header)
16
+ instead of disabling CSRF wholesale.
17
+ # Scoped to the `@csrf_exempt` decorator and `@method_decorator(csrf_exempt, ...)`.
18
+ # A bare `import csrf_exempt` or an unrelated decorator is not matched.
19
+ pattern-either:
20
+ - patterns:
21
+ - pattern: |
22
+ @csrf_exempt
23
+ def $VIEW(...): ...
24
+ - patterns:
25
+ - pattern: |
26
+ @method_decorator(csrf_exempt, ...)
27
+ class $VIEW(...): ...
28
+ metadata:
29
+ oauthlint-rule-id: AUTH-PY-FLOW-005
30
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-flow-csrf-exempt
31
+ category: security
32
+ cwe: CWE-352
33
+ owasp: A01:2021
34
+ llm-prevalence: HIGH
35
+ technology:
36
+ - django
37
+ references:
38
+ - https://docs.djangoproject.com/en/stable/ref/csrf/
39
+ - https://cwe.mitre.org/data/definitions/352.html
@@ -0,0 +1,37 @@
1
+ rules:
2
+ - id: auth.py.flow.debug-enabled
3
+ languages:
4
+ - python
5
+ severity: WARNING
6
+ message: |
7
+ Debug mode is hard-coded to `True`. In production this leaks the
8
+ `SECRET_KEY`, environment variables, and full tracebacks, and Flask's
9
+ Werkzeug debugger additionally exposes an interactive console that
10
+ allows remote code execution.
11
+
12
+ Never enable debug mode in production. Drive it from an environment
13
+ variable that defaults to off, e.g.
14
+ `debug=os.environ.get("FLASK_DEBUG") == "1"` (Flask) or
15
+ `DEBUG = os.environ.get("DJANGO_DEBUG") == "1"` (Django).
16
+ # Matches only the literal `True`. `debug=False`, `DEBUG = False`,
17
+ # `DEBUG = os.environ.get(...)`, and `config(...)` are NOT matched.
18
+ # `$APP` is a metavariable for any application object name.
19
+ pattern-either:
20
+ - pattern: $APP.run(..., debug=True, ...)
21
+ - pattern: $APP.config["DEBUG"] = True
22
+ - pattern: $APP.debug = True
23
+ - pattern: DEBUG = True
24
+ metadata:
25
+ oauthlint-rule-id: AUTH-PY-FLOW-003
26
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-flow-debug-enabled
27
+ category: security
28
+ cwe: CWE-489
29
+ owasp: A05:2021
30
+ llm-prevalence: HIGH
31
+ technology:
32
+ - flask
33
+ - django
34
+ references:
35
+ - https://flask.palletsprojects.com/en/stable/config/#DEBUG
36
+ - https://docs.djangoproject.com/en/stable/ref/settings/#debug
37
+ - https://cwe.mitre.org/data/definitions/489.html
@@ -0,0 +1,51 @@
1
+ rules:
2
+ - id: auth.py.flow.insecure-random-token
3
+ languages:
4
+ - python
5
+ severity: ERROR
6
+ message: |
7
+ A security-sensitive value (token, secret, password, OTP, nonce, API key,
8
+ reset/verification code) is being generated with the `random` module.
9
+ `random` is a pseudo-random number generator seeded from predictable state
10
+ and is NOT cryptographically secure — its output can be predicted or
11
+ reproduced by an attacker, defeating the secret entirely.
12
+
13
+ Use the `secrets` module or `os.urandom` instead:
14
+ `secrets.token_urlsafe(32)`, `secrets.token_hex(16)`,
15
+ `secrets.choice(alphabet)`, or `os.urandom(32)`. These draw from the
16
+ operating system's CSPRNG.
17
+ # Anchored on the NAME of the assigned target (must look like a secret) plus
18
+ # a value coming from `random.`. This avoids flagging `random.random()` used
19
+ # for jitter/sampling/colors, and never matches `secrets....` or `os.urandom`.
20
+ patterns:
21
+ - pattern-either:
22
+ - pattern: $VAR = random.random()
23
+ - pattern: $VAR = random.randint(...)
24
+ - pattern: $VAR = random.randrange(...)
25
+ - pattern: $VAR = random.choice(...)
26
+ - pattern: $VAR = random.choices(...)
27
+ - pattern: $VAR = random.sample(...)
28
+ - pattern: $VAR = random.getrandbits(...)
29
+ - patterns:
30
+ - pattern: $VAR = "".join($X)
31
+ - metavariable-pattern:
32
+ metavariable: $X
33
+ patterns:
34
+ - pattern-either:
35
+ - pattern: random.choice(...)
36
+ - pattern: random.choices(...)
37
+ - metavariable-regex:
38
+ metavariable: $VAR
39
+ regex: (?i).*(token|secret|password|passwd|otp|nonce|api_?key|reset|verification).*
40
+ metadata:
41
+ oauthlint-rule-id: AUTH-PY-FLOW-004
42
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-flow-insecure-random-token
43
+ category: security
44
+ cwe: CWE-330
45
+ owasp: A02:2021
46
+ llm-prevalence: HIGH
47
+ technology:
48
+ - random
49
+ references:
50
+ - https://docs.python.org/3/library/secrets.html
51
+ - https://cwe.mitre.org/data/definitions/330.html
@@ -0,0 +1,50 @@
1
+ rules:
2
+ - id: auth.py.flow.requests-verify-disabled
3
+ languages:
4
+ - python
5
+ severity: ERROR
6
+ message: |
7
+ A `requests` call disables TLS certificate verification with
8
+ `verify=False`. This silences the validation of the server's
9
+ certificate, so an attacker who can intercept the connection can
10
+ present any certificate and read or tamper with the traffic — a
11
+ classic man-in-the-middle exposure. For OAuth/OIDC this leaks
12
+ authorization codes, access tokens and client secrets.
13
+
14
+ Never set `verify=False`. Leave verification on (the default) so the
15
+ system CA bundle is used. In development against a private CA, point
16
+ `verify` at the CA bundle instead, e.g.
17
+ `requests.get(url, verify="/path/to/ca-bundle.pem")` or set the
18
+ `REQUESTS_CA_BUNDLE` env var (certifi).
19
+ # Scoped to `requests.<method>(...)`, `requests.request(...)` and Session
20
+ # objects (`$SESSION.<method>(...)`). Fires only on the literal
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, ...)
39
+ metadata:
40
+ oauthlint-rule-id: AUTH-PY-FLOW-002
41
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-flow-requests-verify-disabled
42
+ category: security
43
+ cwe: CWE-295
44
+ owasp: API8:2023
45
+ llm-prevalence: HIGH
46
+ technology:
47
+ - requests
48
+ references:
49
+ - https://requests.readthedocs.io/en/latest/user/advanced/#ssl-cert-verification
50
+ - https://cwe.mitre.org/data/definitions/295.html
@@ -0,0 +1,64 @@
1
+ rules:
2
+ - id: auth.py.flow.weak-password-hash
3
+ languages:
4
+ - python
5
+ severity: ERROR
6
+ message: |
7
+ A password is being hashed with a fast, general-purpose digest from
8
+ `hashlib` (MD5, SHA-1, SHA-256, SHA-512). These algorithms are designed
9
+ to be fast, which makes offline brute-force and rainbow-table attacks
10
+ cheap — they are NOT suitable for storing passwords.
11
+
12
+ Use a dedicated, slow password-hashing function with a per-password salt
13
+ and a tunable work factor: `bcrypt` (`bcrypt.hashpw`), `argon2`
14
+ (`argon2.PasswordHasher().hash`), `scrypt`, or a wrapper such as
15
+ `passlib`. These resist brute-force by design.
16
+ # Scoped to hashlib digests whose input is named like a password
17
+ # (metavariable-regex on the argument). `password.encode()` is handled
18
+ # because the base name `password` still matches. Checksums, file digests
19
+ # (`hashlib.sha256(file_bytes)`), `hmac.new(...)`, and real password
20
+ # hashers (bcrypt/argon2/passlib) are NOT matched.
21
+ pattern-either:
22
+ - patterns:
23
+ - pattern-either:
24
+ - pattern: hashlib.md5($PW)
25
+ - pattern: hashlib.sha1($PW)
26
+ - pattern: hashlib.sha256($PW)
27
+ - pattern: hashlib.sha512($PW)
28
+ - metavariable-regex:
29
+ metavariable: $PW
30
+ regex: (?i)^.*(password|passwd|pwd).*$
31
+ - patterns:
32
+ - pattern-either:
33
+ - pattern: |
34
+ $H = hashlib.md5(...)
35
+ ...
36
+ $H.update($PW)
37
+ - pattern: |
38
+ $H = hashlib.sha1(...)
39
+ ...
40
+ $H.update($PW)
41
+ - pattern: |
42
+ $H = hashlib.sha256(...)
43
+ ...
44
+ $H.update($PW)
45
+ - pattern: |
46
+ $H = hashlib.sha512(...)
47
+ ...
48
+ $H.update($PW)
49
+ - metavariable-regex:
50
+ metavariable: $PW
51
+ regex: (?i)^.*(password|passwd|pwd).*$
52
+ metadata:
53
+ oauthlint-rule-id: AUTH-PY-FLOW-001
54
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-flow-weak-password-hash
55
+ category: security
56
+ cwe: CWE-916
57
+ owasp: A02:2021
58
+ llm-prevalence: HIGH
59
+ technology:
60
+ - hashlib
61
+ references:
62
+ - https://cwe.mitre.org/data/definitions/916.html
63
+ - https://argon2-cffi.readthedocs.io/en/stable/
64
+ - https://passlib.readthedocs.io/en/stable/
@@ -0,0 +1,41 @@
1
+ rules:
2
+ - id: auth.py.jwt.alg-none
3
+ languages:
4
+ - python
5
+ severity: ERROR
6
+ message: |
7
+ A JWT is decoded or signed with the `none` algorithm. The `none`
8
+ algorithm means the token is NOT cryptographically signed, so any
9
+ attacker can forge a token with arbitrary claims and have it accepted —
10
+ a complete authentication bypass (CVE-class JWT alg=none vulnerability).
11
+
12
+ Never allow `none`. Pin a strong signing algorithm explicitly:
13
+ `jwt.decode(token, key, algorithms=["RS256"])` for verification, or
14
+ `jwt.encode(claims, key, algorithm="RS256")` (also ES256 / HS256) when
15
+ issuing tokens. Never include `"none"` in the `algorithms` allowlist.
16
+ # Scoped to PyJWT's `jwt.decode(...)` / `jwt.encode(...)`. Uses a
17
+ # case-insensitive metavariable-regex so "none", "None", and "NONE" all
18
+ # match, while strong algs like RS256/ES256/HS256 are left untouched.
19
+ pattern-either:
20
+ - patterns:
21
+ - pattern: jwt.decode(..., algorithms=$ALGS, ...)
22
+ - metavariable-regex:
23
+ metavariable: $ALGS
24
+ regex: (?i).*['"]none['"].*
25
+ - patterns:
26
+ - pattern: jwt.encode(..., algorithm=$ALG, ...)
27
+ - metavariable-regex:
28
+ metavariable: $ALG
29
+ regex: (?i)^['"]none['"]$
30
+ metadata:
31
+ oauthlint-rule-id: AUTH-PY-JWT-002
32
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-jwt-alg-none
33
+ category: security
34
+ cwe: CWE-347
35
+ owasp: API2:2023
36
+ llm-prevalence: HIGH
37
+ technology:
38
+ - pyjwt
39
+ references:
40
+ - https://pyjwt.readthedocs.io/en/stable/api.html#jwt.decode
41
+ - https://cwe.mitre.org/data/definitions/347.html
@@ -0,0 +1,40 @@
1
+ rules:
2
+ - id: auth.py.jwt.hardcoded-secret
3
+ languages:
4
+ - python
5
+ severity: ERROR
6
+ message: |
7
+ A JWT signing/verification key is hardcoded as a string literal in the
8
+ call to PyJWT. Anyone who can read the source or git history can forge or
9
+ tamper with tokens, which is a complete authentication bypass.
10
+
11
+ Load the secret from the environment or a secret manager instead, e.g.
12
+ `key = os.environ["JWT_SECRET"]` and `jwt.encode(payload, key, ...)`.
13
+ Never commit signing keys to source control.
14
+ # Scoped to PyJWT's `jwt.encode(...)` / `jwt.decode(...)`. The key argument
15
+ # is the second positional argument; we only flag it when it is a string
16
+ # literal (metavariable-regex requires it to start/end with a quote), so
17
+ # `os.environ[...]`, `settings.SECRET_KEY`, and plain variables are ignored.
18
+ pattern-either:
19
+ - patterns:
20
+ - pattern: jwt.encode($PAYLOAD, $KEY, ...)
21
+ - metavariable-regex:
22
+ metavariable: $KEY
23
+ regex: ^["'].*["']$
24
+ - patterns:
25
+ - pattern: jwt.decode($TOKEN, $KEY, ...)
26
+ - metavariable-regex:
27
+ metavariable: $KEY
28
+ regex: ^["'].*["']$
29
+ metadata:
30
+ oauthlint-rule-id: AUTH-PY-JWT-003
31
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-jwt-hardcoded-secret
32
+ category: security
33
+ cwe: CWE-798
34
+ owasp: API2:2023
35
+ llm-prevalence: HIGH
36
+ technology:
37
+ - pyjwt
38
+ references:
39
+ - https://pyjwt.readthedocs.io/en/stable/api.html#jwt.encode
40
+ - https://cwe.mitre.org/data/definitions/798.html
@@ -0,0 +1,45 @@
1
+ rules:
2
+ - id: auth.py.jwt.no-algorithms
3
+ languages:
4
+ - python
5
+ severity: WARNING
6
+ message: |
7
+ A JWT is decoded with a verification key but WITHOUT an explicit
8
+ `algorithms` allowlist. Without pinning the accepted algorithms, PyJWT
9
+ may accept a token signed with an unexpected algorithm, enabling
10
+ algorithm-confusion attacks (e.g. an RS256 verifier tricked into
11
+ treating an attacker-supplied HS256 token as valid by using the public
12
+ key as an HMAC secret).
13
+
14
+ Always pass an explicit allowlist: `jwt.decode(token, key,
15
+ algorithms=["RS256"])` (or the exact algorithm you expect). List only the
16
+ algorithms your application actually uses.
17
+ # Scoped to PyJWT's `jwt.decode(token, key, ...)` with a verification key
18
+ # and NO `algorithms=` kwarg. The `verify_signature: False` /
19
+ # `verify=False` cases are intentionally excluded here — those are reported
20
+ # by auth.py.jwt.no-verify, so we avoid a duplicate finding.
21
+ #
22
+ # The `import jwt` guard pins this to PyJWT. Other libraries expose a
23
+ # `jwt.decode(token, key)` that DOES verify and takes no `algorithms`
24
+ # kwarg — notably joserfc (`from joserfc import jwt`), used by Authlib.
25
+ # Requiring the bare `import jwt` excludes those (real-world FP on Authlib).
26
+ patterns:
27
+ - pattern: jwt.decode($T, $K, ...)
28
+ - pattern-not: jwt.decode($T, $K, ..., algorithms=$A, ...)
29
+ - pattern-not: jwt.decode($T, ..., verify=False, ...)
30
+ - pattern-not: jwt.decode($T, ..., options=$OPTS, ...)
31
+ - pattern-inside: |
32
+ import jwt
33
+ ...
34
+ metadata:
35
+ oauthlint-rule-id: AUTH-PY-JWT-004
36
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-jwt-no-algorithms
37
+ category: security
38
+ cwe: CWE-347
39
+ owasp: API2:2023
40
+ llm-prevalence: HIGH
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
@@ -0,0 +1,37 @@
1
+ rules:
2
+ - id: auth.py.jwt.no-verify
3
+ languages:
4
+ - python
5
+ severity: ERROR
6
+ message: |
7
+ A JWT is decoded with signature verification disabled. PyJWT's
8
+ `jwt.decode(token, verify=False)` (legacy) or
9
+ `options={"verify_signature": False}` parses the token WITHOUT checking
10
+ the signature, so any attacker-forged token is accepted — a complete
11
+ authentication bypass.
12
+
13
+ Always verify: `jwt.decode(token, key, algorithms=["RS256"])`. If you
14
+ only need to read an unverified header (e.g. the `kid` before fetching
15
+ the key), use `jwt.get_unverified_header(token)` and treat the claims
16
+ as untrusted.
17
+ # Scoped to PyJWT's `jwt.decode(...)`. Matches both the legacy
18
+ # `verify=False` keyword and the modern `options={"verify_signature": False}`.
19
+ pattern-either:
20
+ - pattern: jwt.decode(..., verify=False, ...)
21
+ - patterns:
22
+ - pattern: jwt.decode(..., options=$OPTS, ...)
23
+ - metavariable-pattern:
24
+ metavariable: $OPTS
25
+ pattern: '{..., "verify_signature": False, ...}'
26
+ metadata:
27
+ oauthlint-rule-id: AUTH-PY-JWT-001
28
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-jwt-no-verify
29
+ category: security
30
+ cwe: CWE-347
31
+ owasp: API2:2023
32
+ llm-prevalence: HIGH
33
+ technology:
34
+ - pyjwt
35
+ references:
36
+ - https://pyjwt.readthedocs.io/en/stable/api.html#jwt.decode
37
+ - https://cwe.mitre.org/data/definitions/347.html
@@ -0,0 +1,32 @@
1
+ rules:
2
+ - id: auth.py.secret.django-hardcoded-key
3
+ languages:
4
+ - python
5
+ severity: ERROR
6
+ message: |
7
+ The Django `SECRET_KEY` is set to a hard-coded string literal in
8
+ settings. This is typically the auto-generated `django-insecure-...`
9
+ value committed by mistake. `SECRET_KEY` signs sessions, CSRF tokens
10
+ and password-reset tokens — anyone who reads the source or a leaked
11
+ repo can forge them and bypass authentication entirely (CWE-798).
12
+
13
+ Load it from the environment or a secret manager instead, e.g.
14
+ `SECRET_KEY = os.environ["SECRET_KEY"]`, `django-environ`
15
+ (`env("SECRET_KEY")`), or `config("SECRET_KEY")`. Generate the value
16
+ with a CSPRNG and never commit it.
17
+ # Scoped to a module-level `SECRET_KEY = "<literal>"` assignment. The
18
+ # `"..."` pattern matches ONLY a string-literal value, so secrets loaded
19
+ # from os.environ / env() / config() / a variable are NOT flagged.
20
+ pattern: SECRET_KEY = "..."
21
+ metadata:
22
+ oauthlint-rule-id: AUTH-PY-SECRET-002
23
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-secret-django-hardcoded-key
24
+ category: security
25
+ cwe: CWE-798
26
+ owasp: A07:2021
27
+ llm-prevalence: HIGH
28
+ technology:
29
+ - django
30
+ references:
31
+ - https://docs.djangoproject.com/en/stable/ref/settings/#secret-key
32
+ - https://cwe.mitre.org/data/definitions/798.html
@@ -0,0 +1,33 @@
1
+ rules:
2
+ - id: auth.py.secret.flask-hardcoded-key
3
+ languages:
4
+ - python
5
+ severity: ERROR
6
+ message: |
7
+ The Flask `SECRET_KEY` (used to sign session cookies and CSRF tokens) is
8
+ set to a hard-coded string literal. Anyone who reads the source — or a
9
+ leaked repo — can forge session cookies and impersonate any user, a
10
+ complete authentication bypass (CWE-798).
11
+
12
+ Load the secret from the environment or a secret manager instead, e.g.
13
+ `app.secret_key = os.environ["SECRET_KEY"]`, and generate it with a CSPRNG
14
+ such as `secrets.token_hex(32)` / `os.urandom(32)`. Never commit the value.
15
+ # Scoped to Flask's secret-key assignment. `$APP` matches any app variable
16
+ # name. The `"..."` pattern matches ONLY a string-literal value, so secrets
17
+ # loaded from os.environ / config refs / os.urandom() are NOT flagged.
18
+ pattern-either:
19
+ - pattern: $APP.secret_key = "..."
20
+ - pattern: $APP.config["SECRET_KEY"] = "..."
21
+ - pattern: $APP.config.update(..., SECRET_KEY="...", ...)
22
+ metadata:
23
+ oauthlint-rule-id: AUTH-PY-SECRET-001
24
+ oauthlint-doc-url: https://oauthlint.dev/rules/py-secret-flask-hardcoded-key
25
+ category: security
26
+ cwe: CWE-798
27
+ owasp: A07:2021
28
+ llm-prevalence: HIGH
29
+ technology:
30
+ - flask
31
+ references:
32
+ - https://flask.palletsprojects.com/en/stable/config/#SECRET_KEY
33
+ - https://cwe.mitre.org/data/definitions/798.html
@@ -0,0 +1,35 @@
1
+ rules:
2
+ - id: auth.rust.cookie.insecure
3
+ languages:
4
+ - rust
5
+ severity: ERROR
6
+ message: |
7
+ A session/auth cookie is built with a security attribute explicitly
8
+ disabled (`secure(false)` or `http_only(false)`). With `secure(false)`
9
+ the cookie is sent over plain HTTP, so a network attacker can read the
10
+ session token. With `http_only(false)` the cookie is readable from
11
+ JavaScript, so any XSS can steal it. For OAuth/OIDC this exposes session
12
+ and token cookies to theft and hijacking.
13
+
14
+ Set `secure(true)` and `http_only(true)` on auth cookies, and add an
15
+ appropriate `SameSite` mode (for example
16
+ `same_site(SameSite::Lax)`).
17
+ # Matches only the literal `false`. `secure(true)`, `http_only(true)`, and
18
+ # the absence of the call are not flagged. `$C` is any cookie builder
19
+ # expression (for example `Cookie::build(...)`).
20
+ patterns:
21
+ - pattern-either:
22
+ - pattern: $C.secure(false)
23
+ - pattern: $C.http_only(false)
24
+ metadata:
25
+ oauthlint-rule-id: AUTH-RUST-COOKIE-001
26
+ oauthlint-doc-url: https://oauthlint.dev/rules/rust-cookie-insecure
27
+ category: security
28
+ cwe: CWE-614
29
+ owasp: A05:2021
30
+ llm-prevalence: HIGH
31
+ technology:
32
+ - cookie
33
+ references:
34
+ - https://docs.rs/cookie/latest/cookie/struct.CookieBuilder.html
35
+ - https://cwe.mitre.org/data/definitions/614.html
@@ -0,0 +1,38 @@
1
+ rules:
2
+ - id: auth.rust.cors.permissive
3
+ languages:
4
+ - rust
5
+ severity: ERROR
6
+ message: |
7
+ A wide-open CORS policy is configured. `Cors::permissive()` (actix-web),
8
+ `CorsLayer::permissive()` / `CorsLayer::very_permissive()` (tower-http),
9
+ and `CorsLayer::new().allow_origin(Any)` all allow requests from any
10
+ origin. Combined with credentialed requests this lets any website read
11
+ authenticated responses — including OAuth/OIDC tokens, session data, and
12
+ user info exposed by your API.
13
+
14
+ Restrict CORS to an explicit allowlist of trusted origins instead, e.g.
15
+ `allow_origin("https://app.example.com".parse().unwrap())` or
16
+ `allow_origin(["https://app.example.com".parse().unwrap()])`.
17
+ # Targets only the fully-open helpers and `allow_origin(Any)`. An explicit
18
+ # origin or a list of origins is not flagged.
19
+ patterns:
20
+ - pattern-either:
21
+ - pattern: Cors::permissive()
22
+ - pattern: CorsLayer::permissive()
23
+ - pattern: CorsLayer::very_permissive()
24
+ - pattern: $B.allow_origin(Any)
25
+ metadata:
26
+ oauthlint-rule-id: AUTH-RUST-CORS-001
27
+ oauthlint-doc-url: https://oauthlint.dev/rules/rust-cors-permissive
28
+ category: security
29
+ cwe: CWE-942
30
+ owasp: A05:2021
31
+ llm-prevalence: MEDIUM
32
+ technology:
33
+ - actix-web
34
+ - tower-http
35
+ references:
36
+ - https://docs.rs/actix-cors/latest/actix_cors/struct.Cors.html#method.permissive
37
+ - https://docs.rs/tower-http/latest/tower_http/cors/struct.CorsLayer.html
38
+ - https://cwe.mitre.org/data/definitions/942.html
@@ -0,0 +1,40 @@
1
+ rules:
2
+ - id: auth.rust.crypto.bcrypt-low-cost
3
+ languages:
4
+ - rust
5
+ severity: WARNING
6
+ message: |
7
+ `bcrypt::hash` (or `bcrypt::hash_with_result`) is called with a cost
8
+ factor below 10. A low work factor makes each hash cheap to compute,
9
+ which lets an attacker brute-force stolen password hashes far too
10
+ quickly. OWASP recommends a bcrypt cost of at least 10, and ≥ 12 for new
11
+ applications, tuned so a single hash takes roughly 250ms on your
12
+ hardware.
13
+
14
+ Common LLM-generated mistake: `bcrypt::hash(password, 8)` because the
15
+ literal "looks fast enough". Use `bcrypt::DEFAULT_COST` (12) or raise the
16
+ cost factor to 12 or higher.
17
+ # Matches only a numeric literal cost < 10 passed to `bcrypt::hash` /
18
+ # `bcrypt::hash_with_result`. `metavariable-comparison` constrains $N to
19
+ # numeric literals only, so `bcrypt::DEFAULT_COST`, a cost ≥ 10, or a
20
+ # variable (`bcrypt::hash(password, cost)`) are NOT flagged.
21
+ patterns:
22
+ - pattern-either:
23
+ - pattern: bcrypt::hash($PW, $N)
24
+ - pattern: bcrypt::hash_with_result($PW, $N)
25
+ - metavariable-comparison:
26
+ metavariable: $N
27
+ comparison: $N < 10
28
+ metadata:
29
+ oauthlint-rule-id: AUTH-RUST-CRYPTO-002
30
+ oauthlint-doc-url: https://oauthlint.dev/rules/rust-crypto-bcrypt-low-cost
31
+ category: security
32
+ cwe: CWE-916
33
+ owasp: A02:2021
34
+ llm-prevalence: MEDIUM
35
+ technology:
36
+ - bcrypt
37
+ references:
38
+ - https://docs.rs/bcrypt/latest/bcrypt/fn.hash.html
39
+ - https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
40
+ - https://cwe.mitre.org/data/definitions/916.html
@@ -0,0 +1,41 @@
1
+ rules:
2
+ - id: auth.rust.crypto.weak-cipher
3
+ languages:
4
+ - rust
5
+ severity: ERROR
6
+ message: |
7
+ A broken or deprecated cipher from the RustCrypto ecosystem is used to
8
+ protect data. DES (`Des::new`, crate `des`) and 3DES (`TdesEde3::new` /
9
+ `TdesEde2::new`) have a 64-bit block and are considered insecure
10
+ (Sweet32, brute-force), while RC4 (`Rc4::new`, crate `rc4`) has
11
+ well-known keystream biases and is forbidden by RFC 7465. For OAuth/OIDC
12
+ this means tokens, client secrets, and other sensitive material are not
13
+ adequately protected and may be recovered by an attacker.
14
+
15
+ Use an authenticated AEAD cipher instead: AES-GCM via the `aes-gcm`
16
+ crate (`Aes256Gcm::new(key)`) or ChaCha20-Poly1305
17
+ (`ChaCha20Poly1305::new(key)`), both of which provide confidentiality
18
+ and integrity.
19
+ # Flags only the broken ciphers DES, 3DES, and RC4. Modern AEAD
20
+ # constructors such as `Aes256Gcm::new`, `Aes128::new`, and
21
+ # `ChaCha20Poly1305::new` are intentionally not matched.
22
+ patterns:
23
+ - pattern-either:
24
+ - pattern: Des::new(...)
25
+ - pattern: TdesEde3::new(...)
26
+ - pattern: TdesEde2::new(...)
27
+ - pattern: Rc4::new(...)
28
+ metadata:
29
+ oauthlint-rule-id: AUTH-RUST-CRYPTO-003
30
+ oauthlint-doc-url: https://oauthlint.dev/rules/rust-crypto-weak-cipher
31
+ category: security
32
+ cwe: CWE-327
33
+ owasp: A02:2021
34
+ llm-prevalence: MEDIUM
35
+ technology:
36
+ - des
37
+ - rc4
38
+ references:
39
+ - https://cwe.mitre.org/data/definitions/327.html
40
+ - https://docs.rs/aes-gcm/latest/aes_gcm/
41
+ - https://datatracker.ietf.org/doc/html/rfc7465