oauthlint-rules 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +10 -4
- package/dist/schema.js.map +1 -1
- package/package.json +10 -3
- package/rules/cors/null-origin.yml +53 -0
- package/rules/cors/reflect-origin.yml +68 -0
- package/rules/flow/credentials-in-url.yml +37 -0
- package/rules/flow/secret-in-log.yml +95 -0
- package/rules/flow/timing-unsafe-compare.yml +62 -16
- package/rules/flow/weak-bcrypt-rounds.yml +41 -0
- package/rules/flow/weak-password-hash.yml +74 -0
- package/rules/go/cookie/insecure.yml +35 -0
- package/rules/go/cors/allow-all.yml +44 -0
- package/rules/go/crypto/bcrypt-low-cost.yml +37 -0
- package/rules/go/crypto/weak-cipher.yml +38 -0
- package/rules/go/crypto/weak-password-hash.yml +47 -0
- package/rules/go/flow/weak-rand.yml +61 -0
- package/rules/go/jwt/hardcoded-secret.yml +37 -0
- package/rules/go/jwt/none-algorithm.yml +38 -0
- package/rules/go/jwt/parse-unverified.yml +33 -0
- package/rules/go/jwt/unchecked-method.yml +72 -0
- package/rules/go/tls/insecure-skip-verify.yml +32 -0
- package/rules/go/tls/min-version.yml +36 -0
- package/rules/java/cookie/insecure.yml +31 -0
- package/rules/java/cors/allow-all.yml +47 -0
- package/rules/java/crypto/ecb-mode.yml +42 -0
- package/rules/java/crypto/insecure-random.yml +62 -0
- package/rules/java/crypto/weak-password-hash.yml +47 -0
- package/rules/java/jwt/hardcoded-secret.yml +43 -0
- package/rules/java/jwt/unsigned-jwt.yml +41 -0
- package/rules/java/session/fixation-disabled.yml +34 -0
- package/rules/java/tls/trust-all-certs.yml +48 -0
- package/rules/java/web/csrf-disabled.yml +35 -0
- package/rules/java/web/frame-options-disabled.yml +37 -0
- package/rules/java/web/permit-all.yml +36 -0
- package/rules/jwt/decode-without-verify.yml +56 -0
- package/rules/jwt/no-algorithms-allowlist.yml +38 -0
- package/rules/oauth/no-nonce.yml +55 -0
- package/rules/oauth/pkce-plain.yml +40 -0
- package/rules/py/cookie/insecure-flags.yml +41 -0
- package/rules/py/flow/csrf-exempt.yml +39 -0
- package/rules/py/flow/debug-enabled.yml +37 -0
- package/rules/py/flow/insecure-random-token.yml +51 -0
- package/rules/py/flow/requests-verify-disabled.yml +50 -0
- package/rules/py/flow/weak-password-hash.yml +64 -0
- package/rules/py/jwt/alg-none.yml +41 -0
- package/rules/py/jwt/hardcoded-secret.yml +40 -0
- package/rules/py/jwt/no-algorithms.yml +45 -0
- package/rules/py/jwt/no-verify.yml +37 -0
- package/rules/py/secret/django-hardcoded-key.yml +32 -0
- package/rules/py/secret/flask-hardcoded-key.yml +33 -0
- package/rules/rust/cookie/insecure.yml +35 -0
- package/rules/rust/cors/permissive.yml +38 -0
- package/rules/rust/crypto/bcrypt-low-cost.yml +40 -0
- package/rules/rust/crypto/weak-cipher.yml +41 -0
- package/rules/rust/crypto/weak-password-hash.yml +46 -0
- package/rules/rust/flow/timing-unsafe-compare.yml +77 -0
- package/rules/rust/jwt/disable-signature-validation.yml +31 -0
- package/rules/rust/jwt/hardcoded-secret.yml +41 -0
- package/rules/rust/jwt/no-aud-validation.yml +34 -0
- package/rules/rust/jwt/no-expiration-validation.yml +35 -0
- package/rules/rust/tls/accept-invalid-certs.yml +30 -0
- package/rules/rust/tls/accept-invalid-hostnames.yml +32 -0
- package/rules/secret/public-env-secret.yml +58 -0
- package/rules/session/hardcoded-secret.yml +42 -0
|
@@ -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
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: auth.rust.crypto.weak-password-hash
|
|
3
|
+
languages:
|
|
4
|
+
- rust
|
|
5
|
+
severity: ERROR
|
|
6
|
+
message: |
|
|
7
|
+
A password is being hashed with a fast, general-purpose digest (MD5 via
|
|
8
|
+
the `md5` crate, or SHA-1/SHA-256/SHA-512 via the RustCrypto `sha1`/`sha2`
|
|
9
|
+
crates). These algorithms are designed to be fast, which makes offline
|
|
10
|
+
brute-force and rainbow-table attacks cheap — they are NOT suitable for
|
|
11
|
+
storing passwords (CWE-916).
|
|
12
|
+
|
|
13
|
+
Use a dedicated, slow password-hashing function with a per-password salt
|
|
14
|
+
and a tunable work factor: Argon2 (`argon2` crate,
|
|
15
|
+
`Argon2::default().hash_password(...)`), bcrypt (`bcrypt::hash(...)`), or
|
|
16
|
+
scrypt (`scrypt` crate). These resist brute-force by design.
|
|
17
|
+
# Anchored to the *password* character of the input: the digested/updated
|
|
18
|
+
# argument must be named like a password (metavariable-regex on $PW). This
|
|
19
|
+
# avoids flagging `Sha256::digest(file_bytes)` used for file checksums or
|
|
20
|
+
# non-password fingerprints, and does not touch real password hashers
|
|
21
|
+
# (argon2 / bcrypt / scrypt).
|
|
22
|
+
patterns:
|
|
23
|
+
- pattern-either:
|
|
24
|
+
- pattern: md5::compute($PW)
|
|
25
|
+
- pattern: Md5::digest($PW)
|
|
26
|
+
- pattern: Sha1::digest($PW)
|
|
27
|
+
- pattern: Sha256::digest($PW)
|
|
28
|
+
- pattern: Sha512::digest($PW)
|
|
29
|
+
- pattern: $H.update($PW)
|
|
30
|
+
- metavariable-regex:
|
|
31
|
+
metavariable: $PW
|
|
32
|
+
regex: (?i).*(password|passwd|pwd).*
|
|
33
|
+
metadata:
|
|
34
|
+
oauthlint-rule-id: AUTH-RUST-CRYPTO-001
|
|
35
|
+
oauthlint-doc-url: https://oauthlint.dev/rules/rust-crypto-weak-password-hash
|
|
36
|
+
category: security
|
|
37
|
+
cwe: CWE-916
|
|
38
|
+
owasp: A02:2021
|
|
39
|
+
llm-prevalence: HIGH
|
|
40
|
+
technology:
|
|
41
|
+
- sha2
|
|
42
|
+
- md5
|
|
43
|
+
references:
|
|
44
|
+
- https://cwe.mitre.org/data/definitions/916.html
|
|
45
|
+
- https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
|
|
46
|
+
- https://docs.rs/argon2/latest/argon2/
|