ecdsa-scan 0.1.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/LICENSE +21 -0
- package/README.md +303 -0
- package/package.json +43 -0
- package/src/index.js +212 -0
- package/src/lib/glob.js +62 -0
- package/src/lib/mask.js +199 -0
- package/src/lib/text.js +86 -0
- package/src/report.js +298 -0
- package/src/rules/_shared.js +113 -0
- package/src/rules/crypto-inventory.js +35 -0
- package/src/rules/curve-mixing.js +71 -0
- package/src/rules/hardcoded-private-key.js +66 -0
- package/src/rules/index.js +42 -0
- package/src/rules/insecure-nonce-source.js +80 -0
- package/src/rules/jwt-alg-from-token.js +53 -0
- package/src/rules/jwt-decode-without-verification.js +72 -0
- package/src/rules/jwt-verify-missing-algorithms.js +99 -0
- package/src/rules/key-file-outside-tests.js +56 -0
- package/src/rules/non-constant-time-comparison.js +99 -0
- package/src/rules/secp256k1-low-s.js +46 -0
- package/src/rules/signature-encoding.js +83 -0
- package/src/rules/tls-verification-disabled.js +75 -0
- package/src/rules/unchecked-verification-result.js +60 -0
- package/src/rules/unvalidated-public-key-point.js +72 -0
- package/src/rules/weak-signature-hash.js +83 -0
- package/src/scan.js +308 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Vocabulary shared by several rules: language groups, library markers and a
|
|
2
|
+
// few helpers for looking at imports and call arguments.
|
|
3
|
+
|
|
4
|
+
export const JS_LANGS = ["js", "ts"];
|
|
5
|
+
export const CODE_LANGS = ["js", "ts", "python", "go"];
|
|
6
|
+
|
|
7
|
+
/** Words that mean "this file is doing cryptography", used to gate noisy rules. */
|
|
8
|
+
export const CRYPTO_CONTEXT_RE =
|
|
9
|
+
/\b(sign|signing|signature|signer|verify|verification|privateKey|private_key|secret|seed|nonce|entropy|keypair|key_pair|hmac|digest|ecdsa|secp256|p-?256|ed25519|rsa|jwt|jws|token|cipher|encrypt|decrypt|salt|mnemonic)\b/i;
|
|
10
|
+
|
|
11
|
+
/** Markers of the secp256k1 (blockchain) world. */
|
|
12
|
+
export const K1_RE = /\b(secp256k1|k256|ES256K|SECP256K1|Secp256k1)\b/;
|
|
13
|
+
|
|
14
|
+
/** Markers of the NIST P-256 world. */
|
|
15
|
+
export const P256_RE = /\b(p256|P-256|P256|prime256v1|secp256r1|SECP256R1|ES256(?!K))\b/;
|
|
16
|
+
|
|
17
|
+
/** Ethereum / Bitcoin markers. */
|
|
18
|
+
export const CHAIN_RE =
|
|
19
|
+
/\b(ethers|web3|viem|ethereum|eth_(?:sign|sendTransaction|accounts)|keccak256|personal_sign|bitcoin|bitcoinjs|bip32|bip39|wallet|EIP-?191|EIP-?712|0x[a-fA-F0-9]{40}\b)/;
|
|
20
|
+
|
|
21
|
+
const JS = ["js", "ts"];
|
|
22
|
+
const PY = ["python"];
|
|
23
|
+
const GO = ["go"];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Known crypto libraries for the inventory rule. `langs` keeps a JavaScript
|
|
27
|
+
* `import jwt from "jsonwebtoken"` from being reported as Python's PyJWT.
|
|
28
|
+
*/
|
|
29
|
+
export const LIBRARY_MARKERS = [
|
|
30
|
+
{ name: "jsonwebtoken", re: /['"]jsonwebtoken['"]/, ecosystem: "npm", langs: JS },
|
|
31
|
+
{ name: "jose", re: /['"]jose['"]/, ecosystem: "npm", langs: JS },
|
|
32
|
+
{ name: "jwt-decode", re: /['"]jwt-decode['"]/, ecosystem: "npm", langs: JS },
|
|
33
|
+
{ name: "@noble/curves", re: /['"]@noble\/curves[^'"]*['"]/, ecosystem: "npm", langs: JS },
|
|
34
|
+
{ name: "@noble/hashes", re: /['"]@noble\/hashes[^'"]*['"]/, ecosystem: "npm", langs: JS },
|
|
35
|
+
{ name: "@noble/secp256k1", re: /['"]@noble\/secp256k1['"]/, ecosystem: "npm", langs: JS },
|
|
36
|
+
{ name: "elliptic", re: /['"]elliptic['"]/, ecosystem: "npm", langs: JS },
|
|
37
|
+
{ name: "secp256k1", re: /['"]secp256k1['"]/, ecosystem: "npm", langs: JS },
|
|
38
|
+
{ name: "ethers", re: /['"]ethers(?:\/[^'"]*)?['"]/, ecosystem: "npm", langs: JS },
|
|
39
|
+
{ name: "viem", re: /['"]viem(?:\/[^'"]*)?['"]/, ecosystem: "npm", langs: JS },
|
|
40
|
+
{ name: "web3", re: /['"]web3(?:\/[^'"]*)?['"]/, ecosystem: "npm", langs: JS },
|
|
41
|
+
{ name: "bitcoinjs-lib", re: /['"]bitcoinjs-lib['"]/, ecosystem: "npm", langs: JS },
|
|
42
|
+
{ name: "tweetnacl", re: /['"]tweetnacl['"]/, ecosystem: "npm", langs: JS },
|
|
43
|
+
{ name: "node-forge", re: /['"]node-forge['"]/, ecosystem: "npm", langs: JS },
|
|
44
|
+
{ name: "@peculiar/x509", re: /['"]@peculiar\/x509['"]/, ecosystem: "npm", langs: JS },
|
|
45
|
+
{ name: "node:crypto", re: /['"](?:node:)?crypto['"]/, ecosystem: "node", langs: JS },
|
|
46
|
+
{ name: "WebCrypto (SubtleCrypto)", re: /\bcrypto\.subtle\./, ecosystem: "web", langs: JS },
|
|
47
|
+
{ name: "PyJWT", re: /^\s*(?:import\s+jwt\b|from\s+jwt\s+import)/m, ecosystem: "pypi", langs: PY },
|
|
48
|
+
{ name: "cryptography", re: /\bfrom\s+cryptography[.\s]/, ecosystem: "pypi", langs: PY },
|
|
49
|
+
{ name: "pycryptodome", re: /\bfrom\s+Crypto[.\s]/, ecosystem: "pypi", langs: PY },
|
|
50
|
+
{ name: "ecdsa (python)", re: /^\s*(?:import\s+ecdsa\b|from\s+ecdsa\s+import)/m, ecosystem: "pypi", langs: PY },
|
|
51
|
+
{ name: "requests", re: /^\s*(?:import\s+requests\b|from\s+requests\s+import)/m, ecosystem: "pypi", langs: PY },
|
|
52
|
+
{ name: "crypto/ecdsa", re: /["']crypto\/ecdsa["']/, ecosystem: "go", langs: GO },
|
|
53
|
+
{ name: "crypto/elliptic", re: /["']crypto\/elliptic["']/, ecosystem: "go", langs: GO },
|
|
54
|
+
{ name: "crypto/tls", re: /["']crypto\/tls["']/, ecosystem: "go", langs: GO },
|
|
55
|
+
{ name: "crypto/rsa", re: /["']crypto\/rsa["']/, ecosystem: "go", langs: GO },
|
|
56
|
+
{ name: "golang-jwt", re: /["']github\.com\/golang-jwt\/jwt[^"']*["']/, ecosystem: "go", langs: GO },
|
|
57
|
+
{ name: "btcec", re: /["'][^"']*btcec[^"']*["']/, ecosystem: "go", langs: GO },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/** Algorithms and curves worth listing in the inventory / future CBOM. */
|
|
61
|
+
export const ALGORITHM_MARKERS = [
|
|
62
|
+
{ name: "ES256", re: /\bES256\b(?!K)/, kind: "algorithm" },
|
|
63
|
+
{ name: "ES256K", re: /\bES256K\b/, kind: "algorithm" },
|
|
64
|
+
{ name: "ES384", re: /\bES384\b/, kind: "algorithm" },
|
|
65
|
+
{ name: "ES512", re: /\bES512\b/, kind: "algorithm" },
|
|
66
|
+
{ name: "EdDSA", re: /\b(?:EdDSA|Ed25519|ed25519)\b/, kind: "algorithm" },
|
|
67
|
+
{ name: "RS256", re: /\bRS256\b/, kind: "algorithm" },
|
|
68
|
+
{ name: "PS256", re: /\bPS256\b/, kind: "algorithm" },
|
|
69
|
+
{ name: "HS256", re: /\bHS(?:256|384|512)\b/, kind: "algorithm" },
|
|
70
|
+
{ name: "RSA-PSS", re: /\b(?:RSA-PSS|rsa_pss|PSS\()/, kind: "algorithm" },
|
|
71
|
+
{ name: "ECDSA", re: /\b(?:ecdsa|ECDSA)\b/, kind: "algorithm" },
|
|
72
|
+
{ name: "SHA-1", re: /\b(?:sha1|SHA1|SHA-1)\b/, kind: "algorithm" },
|
|
73
|
+
{ name: "MD5", re: /\b(?:md5|MD5)\b/, kind: "algorithm" },
|
|
74
|
+
{ name: "SHA-256", re: /\b(?:sha256|SHA256|SHA-256)\b/, kind: "algorithm" },
|
|
75
|
+
{ name: "SHA-384", re: /\b(?:sha384|SHA384|SHA-384)\b/, kind: "algorithm" },
|
|
76
|
+
{ name: "SHA-512", re: /\b(?:sha512|SHA512|SHA-512)\b/, kind: "algorithm" },
|
|
77
|
+
{ name: "secp256k1", re: K1_RE, kind: "curve" },
|
|
78
|
+
{ name: "P-256", re: /\b(?:p256|P-256|P256|prime256v1|secp256r1|SECP256R1)\b/, kind: "curve" },
|
|
79
|
+
{ name: "P-384", re: /\b(?:p384|P-384|P384|secp384r1|SECP384R1)\b/, kind: "curve" },
|
|
80
|
+
{ name: "P-521", re: /\b(?:p521|P-521|P521|secp521r1|SECP521R1)\b/, kind: "curve" },
|
|
81
|
+
{ name: "Curve25519", re: /\b(?:x25519|X25519|curve25519)\b/, kind: "curve" },
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
/** True when the JS/TS file imports `name` from a module matching `moduleRe`. */
|
|
85
|
+
export function jsImportsBinding(ctx, moduleSource, name) {
|
|
86
|
+
const escaped = moduleSource.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
87
|
+
const patterns = [
|
|
88
|
+
new RegExp(`import\\s*\\{([^}]*)\\}\\s*from\\s*['"]${escaped}['"]`),
|
|
89
|
+
new RegExp(`(?:const|let|var)\\s*\\{([^}]*)\\}\\s*=\\s*require\\(\\s*['"]${escaped}['"]`),
|
|
90
|
+
];
|
|
91
|
+
for (const re of patterns) {
|
|
92
|
+
const m = ctx.code.match(re);
|
|
93
|
+
if (m && new RegExp(`\\b${name}\\b`).test(m[1])) return true;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** True when a Python file imports `name` (`from mod import name` / `import mod`). */
|
|
99
|
+
export function pyImports(ctx, moduleName) {
|
|
100
|
+
const escaped = moduleName.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
101
|
+
return new RegExp(`^\\s*(?:import\\s+${escaped}\\b|from\\s+${escaped}[.\\s])`, "m").test(ctx.code);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** True when the argument text of a call sets `key` (`key:`, `key=` or `"key":`). */
|
|
105
|
+
export function argsHaveOption(args, key) {
|
|
106
|
+
return new RegExp(`(?:^|[\\s,{(])["']?${key}["']?\\s*[:=]`).test(args);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Strip a call's argument text down to one line for a readable message. */
|
|
110
|
+
export function condense(text, maxLen = 90) {
|
|
111
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
112
|
+
return flat.length > maxLen ? `${flat.slice(0, maxLen - 1)}…` : flat;
|
|
113
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Inventory collector (not a defect rule): which crypto libraries, algorithms
|
|
2
|
+
// and curves each file touches. This is the seed of the CBOM — a cryptographic
|
|
3
|
+
// bill of materials — that the hosted product builds on.
|
|
4
|
+
|
|
5
|
+
import { ALGORITHM_MARKERS, CODE_LANGS, LIBRARY_MARKERS } from "./_shared.js";
|
|
6
|
+
|
|
7
|
+
const OPERATIONS = [
|
|
8
|
+
{ name: "sign", re: /\b(?:createSign|\.sign\s*\(|signSync|SignASN1|sign_digest|sign\s*\(data|Sign\s*\()/ },
|
|
9
|
+
{ name: "verify", re: /\b(?:createVerify|\.verify\s*\(|verifySync|VerifyASN1|verify_digest|Verify\s*\()/ },
|
|
10
|
+
{ name: "keygen", re: /\b(?:generateKeyPair(?:Sync)?|GenerateKey|generate_private_key|randomPrivateKey|keygen)\b/ },
|
|
11
|
+
{ name: "kms", re: /\b(?:KMSClient|kms\.sign|SignCommand|CloudKMS|KeyVault|Vault(?:Client)?|hsm|HSM|pkcs11)\b/ },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
id: "crypto-inventory",
|
|
16
|
+
title: "Cryptographic inventory",
|
|
17
|
+
kind: "inventory",
|
|
18
|
+
languages: CODE_LANGS,
|
|
19
|
+
why: "Not a defect: the libraries, algorithms, curves and signing operations found in the scanned tree.",
|
|
20
|
+
|
|
21
|
+
collect(ctx) {
|
|
22
|
+
const items = [];
|
|
23
|
+
for (const lib of LIBRARY_MARKERS) {
|
|
24
|
+
if (lib.langs && !lib.langs.includes(ctx.lang)) continue;
|
|
25
|
+
if (lib.re.test(ctx.code)) items.push({ kind: "library", name: lib.name, detail: lib.ecosystem });
|
|
26
|
+
}
|
|
27
|
+
for (const alg of ALGORITHM_MARKERS) {
|
|
28
|
+
if (alg.re.test(ctx.code)) items.push({ kind: alg.kind, name: alg.name });
|
|
29
|
+
}
|
|
30
|
+
for (const op of OPERATIONS) {
|
|
31
|
+
if (op.re.test(ctx.code)) items.push({ kind: "operation", name: op.name });
|
|
32
|
+
}
|
|
33
|
+
return items;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Rule: secp256k1 and P-256 handled in the same module, or a P-256 key created
|
|
2
|
+
// in blockchain code.
|
|
3
|
+
|
|
4
|
+
import { CHAIN_RE, CODE_LANGS, K1_RE, P256_RE, condense } from "./_shared.js";
|
|
5
|
+
|
|
6
|
+
const SIGNING_OP = /\b(sign|verify|generateKey|generateKeyPair|GenerateKey|getPublicKey|keyFrom|from_private|from_public|randomPrivateKey)/;
|
|
7
|
+
|
|
8
|
+
/** Ways of creating a P-256 key across the supported languages. */
|
|
9
|
+
const P256_KEYGEN = [
|
|
10
|
+
/namedCurve\s*:\s*["'](?:P-256|prime256v1|secp256r1)["']/,
|
|
11
|
+
/generateKeyPair(?:Sync)?\s*\(\s*["']ec["'][^)]{0,120}?(?:P-256|prime256v1|secp256r1)/,
|
|
12
|
+
/\bp256\s*\.\s*(?:utils\s*\.\s*randomPrivateKey|keygen|getPublicKey)\s*\(/,
|
|
13
|
+
/\bnew\s+(?:EC|ec)\s*\(\s*["'](?:p256|prime256v1|secp256r1)["']\s*\)/,
|
|
14
|
+
/ec\s*\.\s*generate_private_key\s*\(\s*ec\s*\.\s*SECP256R1/,
|
|
15
|
+
/ecdsa\.GenerateKey\s*\(\s*elliptic\.P256\s*\(\s*\)/,
|
|
16
|
+
/\bSECP256r1\b|\bNIST256p\b/,
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export default {
|
|
20
|
+
id: "curve-mixing",
|
|
21
|
+
title: "secp256k1 and P-256 used together, or P-256 used in blockchain code",
|
|
22
|
+
severity: "medium",
|
|
23
|
+
confidence: "advisory",
|
|
24
|
+
languages: CODE_LANGS,
|
|
25
|
+
why:
|
|
26
|
+
"secp256k1 (Bitcoin/Ethereum) and P-256 alias secp256r1 (TLS, JWT ES256, passkeys) are different curves with almost identical names, and libraries rarely stop you from feeding a key of one curve into an operation for the other. " +
|
|
27
|
+
"The symptoms are confusing: signatures that never verify, addresses that do not match, or — worse — a key silently interpreted on the wrong curve. " +
|
|
28
|
+
"Keep each curve in its own module, or name variables so the curve is unmistakable.",
|
|
29
|
+
fix: [
|
|
30
|
+
"// name the curve at every boundary",
|
|
31
|
+
"import { secp256k1 } from '@noble/curves/secp256k1.js'; // chain keys",
|
|
32
|
+
"import { p256 } from '@noble/curves/nist.js'; // ES256 / passkeys",
|
|
33
|
+
"const chainKey = secp256k1.getPublicKey(chainPriv);",
|
|
34
|
+
"const es256Key = p256.getPublicKey(es256Priv);",
|
|
35
|
+
].join("\n"),
|
|
36
|
+
docs: "https://www.rfc-editor.org/rfc/rfc7518#section-3.4",
|
|
37
|
+
|
|
38
|
+
match(ctx) {
|
|
39
|
+
const out = [];
|
|
40
|
+
|
|
41
|
+
// (a) A P-256 key created in a file that also talks to a blockchain.
|
|
42
|
+
if (CHAIN_RE.test(ctx.code)) {
|
|
43
|
+
for (const re of P256_KEYGEN) {
|
|
44
|
+
for (const m of ctx.matchAll(new RegExp(re.source, "g"))) {
|
|
45
|
+
if (ctx.isMasked(m.index)) continue;
|
|
46
|
+
out.push({
|
|
47
|
+
index: m.index,
|
|
48
|
+
confidence: "suspected",
|
|
49
|
+
message: `A P-256 (secp256r1) key is created here (\`${condense(m[0], 60)}\`) while this file also works with Ethereum/Bitcoin, which use secp256k1. Chain keys and addresses derived from a P-256 key will not match.`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// (b) Both curves in one module. Only identifiers and imports count here:
|
|
56
|
+
// curve names inside strings, types and prose (converter drop-downs,
|
|
57
|
+
// documentation, marketing copy) are matched but not reported.
|
|
58
|
+
const k1 = ctx.structure.match(new RegExp(K1_RE.source));
|
|
59
|
+
const p256 = ctx.structure.match(new RegExp(P256_RE.source));
|
|
60
|
+
if (k1 && p256 && SIGNING_OP.test(ctx.structure) && out.length === 0) {
|
|
61
|
+
const index = Math.min(k1.index, p256.index);
|
|
62
|
+
out.push({
|
|
63
|
+
index,
|
|
64
|
+
confidence: "advisory",
|
|
65
|
+
message: `This module references both secp256k1 (\`${k1[0]}\`) and P-256 (\`${p256[0]}\`). Check that keys, signatures and hashes of the two curves cannot reach each other's code paths. Files that deliberately support several curves (converters, debuggers, curve tables) will match this rule legitimately.`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return out;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Rule: private key material embedded in source code.
|
|
2
|
+
|
|
3
|
+
import { CODE_LANGS } from "./_shared.js";
|
|
4
|
+
|
|
5
|
+
// The header must be followed by real base64 key material. A bare
|
|
6
|
+
// "-----BEGIN OPENSSH PRIVATE KEY-----" in UI copy or in a scanner pattern is
|
|
7
|
+
// not a key, and reporting it would be pure noise.
|
|
8
|
+
const PEM_PRIVATE = /-----BEGIN\s+(?:[A-Z0-9]+\s+)*PRIVATE KEY-----[^\S\n]*\\?[nr]?\s*\n?[\s\\n]*[A-Za-z0-9+/=]{40}/g;
|
|
9
|
+
const PEM_HEADER = /-----BEGIN\s+(?:[A-Z0-9]+\s+)*PRIVATE KEY-----/;
|
|
10
|
+
const HEX_ASSIGN = /([A-Za-z_$][\w$]*)\s*(?::=|=|:)\s*["'`](?:0x)?([0-9a-fA-F]{64})["'`]/g;
|
|
11
|
+
|
|
12
|
+
/** Variable names that mean "this is key material". */
|
|
13
|
+
const SECRET_NAME = /priv|secret|signing|sign_key|signkey|seed|mnemonic|master_?key|wif|(?:^|_|\b)sk(?:$|_|\b)/i;
|
|
14
|
+
/** Names that look secret but are not (public keys, digests, addresses). */
|
|
15
|
+
const NOT_SECRET_NAME = /public|pub_|_pub|address|hash|digest|checksum|txid|blockhash|commit|sha256|fingerprint|thumbprint|expected|salt$/i;
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
id: "hardcoded-private-key",
|
|
19
|
+
title: "Private key material embedded in source",
|
|
20
|
+
severity: "high",
|
|
21
|
+
confidence: "confirmed",
|
|
22
|
+
languages: [...CODE_LANGS, "keyfile"],
|
|
23
|
+
why:
|
|
24
|
+
"A private key checked into a repository is compromised the moment anyone clones, forks or mirrors it, and rotating it means re-issuing every signature, certificate or account that depends on it. " +
|
|
25
|
+
"Git keeps the value in history even after the line is deleted, so removal is never enough on its own. " +
|
|
26
|
+
"Keys belong in a KMS, an HSM or (at minimum) an environment variable injected at deploy time.",
|
|
27
|
+
fix: [
|
|
28
|
+
"// read the key at run time; keep the value out of the repository",
|
|
29
|
+
"const privateKey = process.env.SIGNING_PRIVATE_KEY;",
|
|
30
|
+
"// better: never hold the key at all — sign through KMS/HSM",
|
|
31
|
+
"const signature = await kms.sign({ KeyId, Message, SigningAlgorithm: 'ECDSA_SHA_256' });",
|
|
32
|
+
].join("\n"),
|
|
33
|
+
docs: "https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html",
|
|
34
|
+
|
|
35
|
+
match(ctx) {
|
|
36
|
+
const out = [];
|
|
37
|
+
// PEM blocks are matched against the raw text on purpose: a key pasted into
|
|
38
|
+
// a comment is just as leaked as one in a string literal.
|
|
39
|
+
for (const m of ctx.text.matchAll(PEM_PRIVATE)) {
|
|
40
|
+
const header = m[0].match(PEM_HEADER)[0];
|
|
41
|
+
out.push({
|
|
42
|
+
index: m.index,
|
|
43
|
+
confidence: ctx.isTestPath ? "advisory" : "confirmed",
|
|
44
|
+
message: ctx.isTestPath
|
|
45
|
+
? `A private key block (\`${header}\`) with real key material is embedded here. The path looks like test/fixture material, so this is probably intentional — make sure the key is disposable and never used outside tests.`
|
|
46
|
+
: `A private key block (\`${header}\`) is embedded in source. Treat this key as compromised: rotate it, then remove the value from git history.`,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (ctx.lang === "keyfile") return out;
|
|
51
|
+
|
|
52
|
+
for (const m of ctx.matchAll(HEX_ASSIGN)) {
|
|
53
|
+
const name = m[1];
|
|
54
|
+
if (!SECRET_NAME.test(name) || NOT_SECRET_NAME.test(name)) continue;
|
|
55
|
+
out.push({
|
|
56
|
+
index: m.index,
|
|
57
|
+
confidence: ctx.isTestPath ? "advisory" : "suspected",
|
|
58
|
+
message: `\`${name}\` is assigned a 64-character hex literal — the exact shape of a secp256k1/P-256 private key or a symmetric signing key.${
|
|
59
|
+
ctx.isTestPath ? " The path looks like test material, so this may be a deliberate test vector." : " If this is a real key, rotate it and load the value from the environment or a KMS instead."
|
|
60
|
+
}`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return out;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// The rule set. Order here is the order rules are documented in `--help` and
|
|
2
|
+
// the README; findings are sorted by file/line, not by rule.
|
|
3
|
+
|
|
4
|
+
import jwtVerifyMissingAlgorithms from "./jwt-verify-missing-algorithms.js";
|
|
5
|
+
import jwtDecodeWithoutVerification from "./jwt-decode-without-verification.js";
|
|
6
|
+
import jwtAlgFromToken from "./jwt-alg-from-token.js";
|
|
7
|
+
import curveMixing from "./curve-mixing.js";
|
|
8
|
+
import signatureEncoding from "./signature-encoding.js";
|
|
9
|
+
import secp256k1LowS from "./secp256k1-low-s.js";
|
|
10
|
+
import insecureNonceSource from "./insecure-nonce-source.js";
|
|
11
|
+
import hardcodedPrivateKey from "./hardcoded-private-key.js";
|
|
12
|
+
import keyFileOutsideTests from "./key-file-outside-tests.js";
|
|
13
|
+
import nonConstantTimeComparison from "./non-constant-time-comparison.js";
|
|
14
|
+
import uncheckedVerificationResult from "./unchecked-verification-result.js";
|
|
15
|
+
import weakSignatureHash from "./weak-signature-hash.js";
|
|
16
|
+
import unvalidatedPublicKeyPoint from "./unvalidated-public-key-point.js";
|
|
17
|
+
import tlsVerificationDisabled from "./tls-verification-disabled.js";
|
|
18
|
+
import cryptoInventory from "./crypto-inventory.js";
|
|
19
|
+
|
|
20
|
+
export const rules = [
|
|
21
|
+
jwtVerifyMissingAlgorithms,
|
|
22
|
+
jwtDecodeWithoutVerification,
|
|
23
|
+
jwtAlgFromToken,
|
|
24
|
+
curveMixing,
|
|
25
|
+
signatureEncoding,
|
|
26
|
+
secp256k1LowS,
|
|
27
|
+
insecureNonceSource,
|
|
28
|
+
hardcodedPrivateKey,
|
|
29
|
+
keyFileOutsideTests,
|
|
30
|
+
nonConstantTimeComparison,
|
|
31
|
+
uncheckedVerificationResult,
|
|
32
|
+
weakSignatureHash,
|
|
33
|
+
unvalidatedPublicKeyPoint,
|
|
34
|
+
tlsVerificationDisabled,
|
|
35
|
+
cryptoInventory,
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export const defectRules = rules.filter((rule) => rule.kind !== "inventory");
|
|
39
|
+
|
|
40
|
+
export function ruleById(id) {
|
|
41
|
+
return rules.find((rule) => rule.id === id);
|
|
42
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Rule: non-cryptographic randomness, or a caller-supplied ECDSA nonce.
|
|
2
|
+
|
|
3
|
+
import { CODE_LANGS, CRYPTO_CONTEXT_RE, condense } from "./_shared.js";
|
|
4
|
+
|
|
5
|
+
const WEAK_RANDOM = [
|
|
6
|
+
{ re: /\bMath\s*\.\s*random\s*\(/, langs: ["js", "ts"], name: "Math.random()" },
|
|
7
|
+
{ re: /\brandom\s*\.\s*(?:random|randint|randrange|choice|choices|getrandbits|sample|shuffle|uniform)\s*\(/, langs: ["python"], name: "random module" },
|
|
8
|
+
{ re: /\bmath\/rand\b/, langs: ["go"], name: 'math/rand' },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
/** Words that must appear near the call for it to be a crypto nonce/secret. */
|
|
12
|
+
const SENSITIVE_NEARBY =
|
|
13
|
+
/\b(sign|signing|signature|nonce|entropy|seed|salt|iv|privateKey|private_key|priv|secret|key|keyPair|keypair|token|otp|csrf|session|password|mnemonic|challenge|apiKey|api_key)\b/i;
|
|
14
|
+
|
|
15
|
+
/** Explicit nonce/k handed to a signing routine. */
|
|
16
|
+
const EXPLICIT_K = [
|
|
17
|
+
{ re: /\bsign\w*\s*\([^)]{0,200}?\bk\s*=\s*[^),\s]/, note: "explicit k= argument" },
|
|
18
|
+
{ re: /\bextraEntropy\s*[:=]\s*(?!false\b)[^,)\s}]+/, note: "extraEntropy" },
|
|
19
|
+
{ re: /\b(?:deterministic|rfc6979)\s*[:=]\s*(?:false|False|0)\b/, note: "deterministic signing disabled" },
|
|
20
|
+
{ re: /\bsign\w*\s*\([^)]{0,200}?\bnonce\s*[:=]\s*[^),\s]/, note: "explicit nonce argument" },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export default {
|
|
24
|
+
id: "insecure-nonce-source",
|
|
25
|
+
title: "Weak randomness or a caller-supplied nonce in signing code",
|
|
26
|
+
severity: "high",
|
|
27
|
+
confidence: "confirmed",
|
|
28
|
+
languages: CODE_LANGS,
|
|
29
|
+
why:
|
|
30
|
+
"ECDSA leaks the private key if the per-signature nonce k is predictable, biased, or ever reused — two signatures sharing a k are enough to recover the key with school algebra (this is how the PS3 and several early wallets were broken). " +
|
|
31
|
+
"`Math.random()` and Python's `random` module are seeded PRNGs meant for simulations, not for key material. " +
|
|
32
|
+
"Let the library derive k deterministically (RFC 6979) or take it from the OS CSPRNG; never pass your own.",
|
|
33
|
+
fix: [
|
|
34
|
+
"// JS: crypto.getRandomValues / crypto.randomBytes, never Math.random",
|
|
35
|
+
"const bytes = crypto.getRandomValues(new Uint8Array(32));",
|
|
36
|
+
"",
|
|
37
|
+
"# Python: secrets, never random",
|
|
38
|
+
"import secrets; nonce = secrets.token_bytes(32)",
|
|
39
|
+
"",
|
|
40
|
+
"// and let the library pick k (RFC 6979 deterministic ECDSA)",
|
|
41
|
+
"const sig = secp256k1.sign(msgHash, privateKey);",
|
|
42
|
+
].join("\n"),
|
|
43
|
+
docs: "https://www.rfc-editor.org/rfc/rfc6979",
|
|
44
|
+
|
|
45
|
+
match(ctx) {
|
|
46
|
+
const out = [];
|
|
47
|
+
const fileIsCrypto = CRYPTO_CONTEXT_RE.test(ctx.code);
|
|
48
|
+
|
|
49
|
+
if (fileIsCrypto) {
|
|
50
|
+
for (const source of WEAK_RANDOM) {
|
|
51
|
+
if (!source.langs.includes(ctx.lang)) continue;
|
|
52
|
+
if (source.name === "math/rand" && !/["']math\/rand["']/.test(ctx.code)) continue;
|
|
53
|
+
const re = source.name === "math/rand" ? /\brand\s*\.\s*(?:Int\w*|Read|Float\w*|Perm|Seed)\s*\(/g : new RegExp(source.re.source, "g");
|
|
54
|
+
for (const m of ctx.matchAll(re)) {
|
|
55
|
+
if (ctx.isMasked(m.index)) continue; // documentation, not a call
|
|
56
|
+
const around = ctx.window(m.index, 5, 5);
|
|
57
|
+
if (!SENSITIVE_NEARBY.test(around)) continue;
|
|
58
|
+
out.push({
|
|
59
|
+
index: m.index,
|
|
60
|
+
confidence: "confirmed",
|
|
61
|
+
message: `\`${condense(m[0], 40)}\` is a non-cryptographic pseudo-random generator, and it is used next to key/nonce/token material. Its output is predictable from a handful of samples.`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const pattern of EXPLICIT_K) {
|
|
68
|
+
for (const m of ctx.matchAll(new RegExp(pattern.re.source, "g"))) {
|
|
69
|
+
if (ctx.isMasked(m.index)) continue;
|
|
70
|
+
out.push({
|
|
71
|
+
index: m.index,
|
|
72
|
+
confidence: "suspected",
|
|
73
|
+
message: `The signing call takes its randomness from the caller (${pattern.note}: \`${condense(m[0], 60)}\`). Supplying k yourself removes the library's RFC 6979 protection; if that value is ever repeated or biased, the private key can be recovered. Extra entropy taken from a CSPRNG on top of deterministic k is fine — verify which case this is.`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return out;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Rule: the algorithm used for verification is taken from the token itself.
|
|
2
|
+
|
|
3
|
+
import { CODE_LANGS, condense } from "./_shared.js";
|
|
4
|
+
|
|
5
|
+
// algorithms: [decoded.header.alg] / algorithm: header["alg"] / algorithms=[unverified["alg"]]
|
|
6
|
+
const DOT_ALG =
|
|
7
|
+
/\balgorithms?\s*[:=]\s*\[?\s*[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*\.(?:alg|algorithm)\b/;
|
|
8
|
+
const INDEX_ALG =
|
|
9
|
+
/\balgorithms?\s*[:=]\s*\[?\s*[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*\s*\[\s*["']alg["']\s*\]/;
|
|
10
|
+
const UNVERIFIED_HEADER =
|
|
11
|
+
/\balgorithms?\s*[:=]\s*\[?[^\]\n;]{0,120}?(?:get_unverified_header|decodeProtectedHeader|decode\s*\([^)]*complete\s*:\s*true)/;
|
|
12
|
+
const GO_TOKEN_METHOD =
|
|
13
|
+
/\bjwt\.GetSigningMethod\s*\(\s*[A-Za-z_][\w.]*\.Header\s*\[\s*"alg"\s*\]/;
|
|
14
|
+
|
|
15
|
+
/** Receivers that clearly denote token-supplied data. */
|
|
16
|
+
const TOKEN_SOURCE = /header|headers|decoded|unverified|token|payload|jose|protected|claims/i;
|
|
17
|
+
|
|
18
|
+
export default {
|
|
19
|
+
id: "jwt-alg-from-token",
|
|
20
|
+
title: "Verification algorithm taken from the token header",
|
|
21
|
+
severity: "high",
|
|
22
|
+
confidence: "confirmed",
|
|
23
|
+
languages: CODE_LANGS,
|
|
24
|
+
why:
|
|
25
|
+
"Passing the token's own `alg` value into the verifier turns the allow-list into a no-op: the attacker picks the algorithm and the library obeys. " +
|
|
26
|
+
"With an RSA or EC public key this becomes the textbook HS256 confusion attack — the public key is used as an HMAC secret and a forged token verifies. " +
|
|
27
|
+
"The accepted algorithm must be a constant in your code, decided by the key you hold, never read from untrusted input.",
|
|
28
|
+
fix: [
|
|
29
|
+
"// bad",
|
|
30
|
+
"const { header } = jwt.decode(token, { complete: true });",
|
|
31
|
+
"jwt.verify(token, key, { algorithms: [header.alg] });",
|
|
32
|
+
"",
|
|
33
|
+
"// good — the key type decides the algorithm",
|
|
34
|
+
'jwt.verify(token, ecPublicKey, { algorithms: ["ES256"] });',
|
|
35
|
+
].join("\n"),
|
|
36
|
+
docs: "https://datatracker.ietf.org/doc/html/rfc8725#section-2.1",
|
|
37
|
+
|
|
38
|
+
match(ctx) {
|
|
39
|
+
const out = [];
|
|
40
|
+
const patterns = [DOT_ALG, INDEX_ALG, UNVERIFIED_HEADER, GO_TOKEN_METHOD];
|
|
41
|
+
for (const re of patterns) {
|
|
42
|
+
for (const m of ctx.matchAll(new RegExp(re.source, "g"))) {
|
|
43
|
+
if (ctx.isMasked(m.index)) continue; // a code sample inside a string
|
|
44
|
+
if ((re === DOT_ALG || re === INDEX_ALG) && !TOKEN_SOURCE.test(m[0])) continue;
|
|
45
|
+
out.push({
|
|
46
|
+
index: m.index,
|
|
47
|
+
message: `The algorithm list is built from token-supplied data (\`${condense(m[0], 70)}\`) — an attacker chooses how their own token is verified.`,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Rule: reading JWT claims without checking the signature.
|
|
2
|
+
|
|
3
|
+
import { CODE_LANGS, JS_LANGS, condense } from "./_shared.js";
|
|
4
|
+
|
|
5
|
+
const JS_DECODE = /\b(?:jwt|jsonwebtoken|JWT)\s*\.\s*decode\b|\b(?:decodeJwt|jwtDecode|jwt_decode|decodeProtectedHeader)\b/;
|
|
6
|
+
const PY_DECODE = /\bjwt\s*\.\s*decode\b/;
|
|
7
|
+
const PY_VERIFY_OFF = /verify_signature["']?\s*[:=]\s*False|(?:^|[\s,(])verify\s*=\s*False/;
|
|
8
|
+
|
|
9
|
+
/** Identifiers that suggest the decoded claims drive an access decision. */
|
|
10
|
+
const DECISION_RE =
|
|
11
|
+
/\b(role|roles|isAdmin|is_admin|admin|permission|permissions|scope|scopes|userId|user_id|currentUser|req\.user|ctx\.user|tenant|account|authoriz|authorize|allow|deny|grant|session|principal|sub\b|claims\.)\b/i;
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
id: "jwt-decode-without-verification",
|
|
15
|
+
title: "JWT decoded without verifying the signature",
|
|
16
|
+
severity: "high",
|
|
17
|
+
confidence: "suspected",
|
|
18
|
+
languages: CODE_LANGS,
|
|
19
|
+
why:
|
|
20
|
+
"`decode` only base64url-decodes the token: anyone can craft a JWT with any claims, and decoding it will happily return them. " +
|
|
21
|
+
"That is fine for logging or for reading `kid` before fetching a key, but if the claims then decide who the user is or what they may do, the whole authentication check is bypassed. " +
|
|
22
|
+
"The same applies to PyJWT with `verify_signature: False`.",
|
|
23
|
+
fix: [
|
|
24
|
+
'// instead of: const claims = jwt.decode(token);',
|
|
25
|
+
'const claims = jwt.verify(token, publicKey, { algorithms: ["ES256"] });',
|
|
26
|
+
'# PyJWT',
|
|
27
|
+
'claims = jwt.decode(token, key, algorithms=["ES256"]) # not options={"verify_signature": False}',
|
|
28
|
+
].join("\n"),
|
|
29
|
+
docs: "https://datatracker.ietf.org/doc/html/rfc8725#section-3.2",
|
|
30
|
+
|
|
31
|
+
match(ctx) {
|
|
32
|
+
const out = [];
|
|
33
|
+
const calls = [];
|
|
34
|
+
|
|
35
|
+
if (JS_LANGS.includes(ctx.lang)) {
|
|
36
|
+
for (const call of ctx.findCalls(new RegExp(JS_DECODE.source, "g"))) {
|
|
37
|
+
// A `jwt.decode(` inside a documentation string is not a call.
|
|
38
|
+
if (ctx.isMasked(call.index)) continue;
|
|
39
|
+
calls.push({ ...call, kind: "js" });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (ctx.lang === "python") {
|
|
43
|
+
for (const call of ctx.findCalls(new RegExp(PY_DECODE.source, "g"))) {
|
|
44
|
+
if (ctx.isMasked(call.index)) continue;
|
|
45
|
+
if (PY_VERIFY_OFF.test(call.args)) calls.push({ ...call, kind: "py-off" });
|
|
46
|
+
}
|
|
47
|
+
// `verify=False` on a bare decode helper, e.g. jwt.decode(token, verify=False)
|
|
48
|
+
for (const m of ctx.matchAll(/options\s*=\s*\{[^}]*verify_signature["']?\s*:\s*False[^}]*\}/g)) {
|
|
49
|
+
if (!calls.some((c) => Math.abs(c.index - m.index) < 200)) {
|
|
50
|
+
calls.push({ index: m.index, callee: "jwt.decode", args: m[0], kind: "py-off" });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const call of calls) {
|
|
56
|
+
const after = ctx.window(call.index, 1, 12);
|
|
57
|
+
const decisionNearby = DECISION_RE.test(after);
|
|
58
|
+
const message =
|
|
59
|
+
call.kind === "py-off"
|
|
60
|
+
? `Signature verification is switched off (\`${condense(call.args, 70)}\`) — the claims are attacker-controlled.`
|
|
61
|
+
: `\`${call.callee}\` does not verify the signature; the claims it returns are attacker-controlled.`;
|
|
62
|
+
out.push({
|
|
63
|
+
index: call.index,
|
|
64
|
+
confidence: decisionNearby ? "suspected" : "advisory",
|
|
65
|
+
message: decisionNearby
|
|
66
|
+
? `${message} Identity/authorization identifiers appear right after this call.`
|
|
67
|
+
: `${message} No authorization decision was detected nearby, so this may be a legitimate inspection (logging, reading \`kid\`) — confirm before acting.`,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Rule: JWT verification without an explicit allow-list of algorithms.
|
|
2
|
+
|
|
3
|
+
import { CODE_LANGS, JS_LANGS, argsHaveOption, condense, jsImportsBinding } from "./_shared.js";
|
|
4
|
+
|
|
5
|
+
const JS_JWT_VERIFY = /\b(?:jwt|jsonwebtoken|jwtLib|JWT|jsonWebToken)\s*\.\s*verify\b/;
|
|
6
|
+
const JOSE_VERIFY = /\b(?:jwtVerify|compactVerify|flattenedVerify|generalVerify)\b/;
|
|
7
|
+
const BARE_VERIFY = /(?<![.\w$])verify\b/;
|
|
8
|
+
const PY_JWT_DECODE = /\bjwt\s*\.\s*decode\b/;
|
|
9
|
+
const GO_JWT_PARSE = /\bjwt\s*\.\s*(?:Parse|ParseWithClaims)\b/;
|
|
10
|
+
|
|
11
|
+
const NONE_ALG = /algorithms?\s*[:=]\s*\[\s*(?:\]|["'](?:none|NONE|None)["'])/;
|
|
12
|
+
const PY_VERIFY_OFF = /verify_signature["']?\s*[:=]\s*False|(?:^|[\s,(])verify\s*=\s*False/;
|
|
13
|
+
|
|
14
|
+
/** Algorithm lists that are present but useless. */
|
|
15
|
+
function emptyOrNone(args) {
|
|
16
|
+
return NONE_ALG.test(args);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default {
|
|
20
|
+
id: "jwt-verify-missing-algorithms",
|
|
21
|
+
title: "JWT verified without an explicit algorithm allow-list",
|
|
22
|
+
severity: "high",
|
|
23
|
+
confidence: "confirmed",
|
|
24
|
+
languages: CODE_LANGS,
|
|
25
|
+
why:
|
|
26
|
+
"A JWT names its own algorithm in the header, so a verifier that does not pin the accepted algorithms lets the token choose how it is checked. " +
|
|
27
|
+
"The classic result is algorithm confusion: an attacker re-signs the token with HS256 using your RSA/EC public key as the HMAC secret, or supplies alg:none, and verification succeeds. " +
|
|
28
|
+
"Always pass the exact algorithms your issuer uses.",
|
|
29
|
+
fix: [
|
|
30
|
+
'jsonwebtoken: jwt.verify(token, publicKey, { algorithms: ["ES256"] })',
|
|
31
|
+
'jose: await jwtVerify(token, key, { algorithms: ["ES256"] })',
|
|
32
|
+
'PyJWT: jwt.decode(token, key, algorithms=["ES256"])',
|
|
33
|
+
'golang-jwt: jwt.Parse(s, keyFunc, jwt.WithValidMethods([]string{"ES256"}))',
|
|
34
|
+
].join("\n"),
|
|
35
|
+
docs: "https://datatracker.ietf.org/doc/html/rfc8725#section-3.1",
|
|
36
|
+
|
|
37
|
+
match(ctx) {
|
|
38
|
+
const out = [];
|
|
39
|
+
if (JS_LANGS.includes(ctx.lang)) {
|
|
40
|
+
const bareVerifyIsJwt = jsImportsBinding(ctx, "jsonwebtoken", "verify");
|
|
41
|
+
const calleeRe = bareVerifyIsJwt
|
|
42
|
+
? new RegExp(`${JS_JWT_VERIFY.source}|${JOSE_VERIFY.source}|${BARE_VERIFY.source}`, "g")
|
|
43
|
+
: new RegExp(`${JS_JWT_VERIFY.source}|${JOSE_VERIFY.source}`, "g");
|
|
44
|
+
for (const call of ctx.findCalls(calleeRe)) {
|
|
45
|
+
if (ctx.isMasked(call.index)) continue; // documentation, not a call
|
|
46
|
+
if (argsHaveOption(call.args, "algorithms")) {
|
|
47
|
+
if (!emptyOrNone(call.args)) continue;
|
|
48
|
+
out.push({
|
|
49
|
+
index: call.index,
|
|
50
|
+
message: `\`${call.callee}\` accepts an empty algorithm list or "none" — every token, including unsigned ones, will pass.`,
|
|
51
|
+
});
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
out.push({
|
|
55
|
+
index: call.index,
|
|
56
|
+
message: `\`${call.callee}(${condense(call.args, 60)})\` does not pass \`algorithms\`, so the token header decides how the signature is checked.`,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (ctx.lang === "python") {
|
|
62
|
+
for (const call of ctx.findCalls(new RegExp(PY_JWT_DECODE.source, "g"))) {
|
|
63
|
+
if (ctx.isMasked(call.index)) continue;
|
|
64
|
+
if (PY_VERIFY_OFF.test(call.args)) continue; // reported by jwt-decode-without-verification
|
|
65
|
+
if (argsHaveOption(call.args, "algorithms")) {
|
|
66
|
+
if (!emptyOrNone(call.args)) continue;
|
|
67
|
+
out.push({
|
|
68
|
+
index: call.index,
|
|
69
|
+
message: 'PyJWT `jwt.decode` is given an empty `algorithms` list or "none" — the signature check becomes meaningless.',
|
|
70
|
+
});
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
out.push({
|
|
74
|
+
index: call.index,
|
|
75
|
+
message: `PyJWT \`jwt.decode(${condense(call.args, 60)})\` has no \`algorithms=[...]\` argument, so the accepted algorithm is not pinned.`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (ctx.lang === "go") {
|
|
81
|
+
// golang-jwt checks the algorithm only if the key function inspects
|
|
82
|
+
// token.Method or the parser is given WithValidMethods. If neither the
|
|
83
|
+
// call nor the rest of the file mentions them, nothing pins the alg.
|
|
84
|
+
const fileChecksMethod = /WithValidMethods|SigningMethod|\.Method\b/.test(ctx.code);
|
|
85
|
+
for (const call of ctx.findCalls(new RegExp(GO_JWT_PARSE.source, "g"))) {
|
|
86
|
+
if (ctx.isMasked(call.index)) continue;
|
|
87
|
+
if (/WithValidMethods|SigningMethod|\.Method\b/.test(call.args)) continue;
|
|
88
|
+
if (fileChecksMethod) continue;
|
|
89
|
+
out.push({
|
|
90
|
+
index: call.index,
|
|
91
|
+
message:
|
|
92
|
+
"`jwt.Parse` is called without `jwt.WithValidMethods([...])`, and neither the key function nor this file inspects `token.Method` — the token's own header selects the algorithm.",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return out;
|
|
98
|
+
},
|
|
99
|
+
};
|