diff --git a/.changeset/yummy-llamas-wear.md b/.changeset/yummy-llamas-wear.md new file mode 100644 index 00000000..64a91df2 --- /dev/null +++ b/.changeset/yummy-llamas-wear.md @@ -0,0 +1,5 @@ +--- +"@nodesecure/js-x-ray": minor +--- + +add weak-argon2 detection probe diff --git a/docs/weak-argon2.md b/docs/weak-argon2.md new file mode 100644 index 00000000..98c60d35 --- /dev/null +++ b/docs/weak-argon2.md @@ -0,0 +1,49 @@ +# Weak argon2 + +| Code | Severity | i18n | Experimental | +| --- | --- | --- | :-: | +| weak-argon2 | `Warning` | `sast_warnings.weak_argon2` | :x: | + +## Introduction + +Detect usage of **weak Argon2** parameters with the Node.js core `crypto.argon2()` / `crypto.argon2Sync()` functions. This probe checks for: + +- **wrong-algorithm**: using `argon2d` or `argon2i` instead of the recommended `argon2id`. +- **weak-parameters**: memory, passes, or parallelism values that do not meet [OWASP minimum recommendations](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id). +- **hardcoded-nonce**: nonce (salt) is a hardcoded string literal (should be randomly generated). + +## Example + +```js +import crypto from "crypto"; + +// wrong-algorithm: argon2d is vulnerable to side-channel attacks +crypto.argon2("argon2d", { + message: "password", + nonce: crypto.randomBytes(16), + memory: 47104, + passes: 1, + parallelism: 1, + tagLength: 64 +}); + +// weak-parameters: memory and passes are below OWASP minimum +crypto.argon2("argon2id", { + message: "password", + nonce: crypto.randomBytes(16), + memory: 512, + passes: 1, + parallelism: 1, + tagLength: 64 +}); + +// hardcoded-nonce: nonce should be randomly generated +crypto.argon2("argon2id", { + message: "password", + nonce: "hardcoded-salt", + memory: 47104, + passes: 1, + parallelism: 1, + tagLength: 64 +}); +``` diff --git a/workspaces/js-x-ray/README.md b/workspaces/js-x-ray/README.md index e76c62d6..8dabd835 100644 --- a/workspaces/js-x-ray/README.md +++ b/workspaces/js-x-ray/README.md @@ -124,7 +124,8 @@ Alternatively, you can use `EntryFilesAnalyser` directly for multi-file analysis type OptionalWarningName = | "synchronous-io" | "log-usage" - | "weak-scrypt"; + | "weak-scrypt" + | "weak-argon2"; type WarningName = | "parsing-error" @@ -177,7 +178,7 @@ const scanner = new AstAnalyser({ // Or enable specific optional warnings const scannerSpecific = new AstAnalyser({ - optionalWarnings: ["synchronous-io", "log-usage", "weak-scrypt"] + optionalWarnings: ["synchronous-io", "log-usage", "weak-scrypt", "weak-argon2"] }); ``` @@ -185,6 +186,7 @@ The following warnings are optional: - `synchronous-io` - Detects synchronous I/O operations that could impact performance - `log-usage` - Tracks usage of logging functions (console.log, logger.info, etc.) - `weak-scrypt` - Detects weak scrypt parameters (low cost, short or hardcoded salt) +- `weak-argon2` - Detects weak Argon2 parameters (wrong algorithm, weak parameters, hardcoded nonce) ### Internationalization (i18n) @@ -231,6 +233,7 @@ Click on the warning **name** for detailed documentation and examples. | [sql-injection](https://github.com/NodeSecure/js-x-ray/blob/master/docs/sql-injection.md) | No | Potential SQL injection vulnerability detected | | [monkey-patch](https://github.com/NodeSecure/js-x-ray/blob/master/docs/monkey-patch.md) | No | Modification of built-in JavaScript prototype properties | | [weak-scrypt](https://github.com/NodeSecure/js-x-ray/blob/master/docs/weak-scrypt.md) ⚠️ | **Yes** | Usage of weak scrypt parameters (low cost, short or hardcoded salt) | +| [weak-argon2](https://github.com/NodeSecure/js-x-ray/blob/master/docs/weak-argon2.md) ⚠️ | **Yes** | Usage of weak Argon2 parameters (wrong algorithm, weak parameters, hardcoded nonce) | #### Information Severity diff --git a/workspaces/js-x-ray/src/ProbeRunner.ts b/workspaces/js-x-ray/src/ProbeRunner.ts index 94583890..401b90a8 100644 --- a/workspaces/js-x-ray/src/ProbeRunner.ts +++ b/workspaces/js-x-ray/src/ProbeRunner.ts @@ -26,6 +26,7 @@ import isMonkeyPatch from "./probes/isMonkeyPatch.ts"; import isRandom from "./probes/isRandom.ts"; import isPrototypePollution from "./probes/isPrototypePollution.ts"; import isWeakScrypt from "./probes/isWeakScrypt.ts"; +import isWeakArgon2 from "./probes/isWeakArgon2.ts"; import type { TracedIdentifierReport } from "./VariableTracer.ts"; import type { SourceFile } from "./SourceFile.ts"; @@ -114,7 +115,8 @@ export class ProbeRunner { "synchronous-io": isSyncIO, "log-usage": logUsage, "insecure-random": isRandom, - "weak-scrypt": isWeakScrypt + "weak-scrypt": isWeakScrypt, + "weak-argon2": isWeakArgon2 }; constructor( diff --git a/workspaces/js-x-ray/src/i18n/arabic.js b/workspaces/js-x-ray/src/i18n/arabic.js index fb3fb222..98279dad 100644 --- a/workspaces/js-x-ray/src/i18n/arabic.js +++ b/workspaces/js-x-ray/src/i18n/arabic.js @@ -21,7 +21,8 @@ const sast_warnings = { sql_injection: "قالب نصي (Template literal) يحتوي على تعبيرات مدرجة في استعلامات SQL (SELECT, INSERT, UPDATE, DELETE) بدون معالجة صحيحة، مما يخلق ثغرات حقن SQL محتملة.", monkey_patch: "تعديل النماذج الأصلية (native prototypes) أو الكائنات العالمية في وقت التشغيل، مما يؤدي إلى مخاطر أمنية تشمل اختطاف التدفق، والآثار الجانبية العالمية، والإخفاء المحتمل للأنشطة الضارة.", insecure_random: "استخدام توليد أرقام عشوائية غير آمن باستخدام Math.random(). إن Math.random() ليس آمناً من الناحية التشفيرية ولا ينبغي استخدامه للعمليات الحساسة أمنياً.", - weak_scrypt: "استخدام crypto.scrypt() أو crypto.scryptSync() مع معلمات غير آمنة مثل ملح مضمن في الكود، أو ملح قصير (أقل من 16 بايت)، أو معلمة تكلفة غير كافية (أقل من 16384). هذه التكوينات الضعيفة تُضعف أمان اشتقاق المفاتيح المبني على كلمة المرور." + weak_scrypt: "استخدام crypto.scrypt() أو crypto.scryptSync() مع معلمات غير آمنة مثل ملح مضمن في الكود، أو ملح قصير (أقل من 16 بايت)، أو معلمة تكلفة غير كافية (أقل من 16384). هذه التكوينات الضعيفة تُضعف أمان اشتقاق المفاتيح المبني على كلمة المرور.", + weak_argon2: "استخدام crypto.argon2() أو crypto.argon2Sync() مع معلمات غير آمنة: استخدام argon2d أو argon2i بدلاً من argon2id، أو استخدام nonce (ملح) مضمن في الكود، أو قيم memory/passes/parallelism أقل من الحد الأدنى الموصى به من OWASP. هذه التكوينات الضعيفة تُضعف أمان اشتقاق المفاتيح المبني على كلمة المرور." }; export default { diff --git a/workspaces/js-x-ray/src/i18n/english.js b/workspaces/js-x-ray/src/i18n/english.js index ea5e55ac..194fbbc2 100644 --- a/workspaces/js-x-ray/src/i18n/english.js +++ b/workspaces/js-x-ray/src/i18n/english.js @@ -21,7 +21,8 @@ const sast_warnings = { sql_injection: "Template literals with interpolated expressions in SQL queries (SELECT, INSERT, UPDATE, DELETE) without proper parameterization, creating potential SQL injection vulnerabilities.", monkey_patch: "Modification of native prototypes or global objects at runtime, which introduces security risks including flow hijacking, global side effects, and potential concealment of malicious activities.", insecure_random: "Usage of insecure random number generation using Math.random(). Math.random() is not cryptographically secure and should not be used for security-sensitive operations.", - weak_scrypt: "Usage of crypto.scrypt() or crypto.scryptSync() with insecure parameters such as hardcoded salt, short salt (less than 16 bytes), or insufficient cost parameter (below 16384). These weak configurations compromise the security of password-based key derivation." + weak_scrypt: "Usage of crypto.scrypt() or crypto.scryptSync() with insecure parameters such as hardcoded salt, short salt (less than 16 bytes), or insufficient cost parameter (below 16384). These weak configurations compromise the security of password-based key derivation.", + weak_argon2: "Usage of crypto.argon2() or crypto.argon2Sync() with insecure parameters: use of argon2d or argon2i instead of argon2id, hardcoded nonce (salt), or memory/passes/parallelism values below OWASP minimum recommendations. These weak configurations compromise the security of password-based key derivation." }; export default { diff --git a/workspaces/js-x-ray/src/i18n/french.js b/workspaces/js-x-ray/src/i18n/french.js index 2405f269..1da31bc8 100644 --- a/workspaces/js-x-ray/src/i18n/french.js +++ b/workspaces/js-x-ray/src/i18n/french.js @@ -23,7 +23,8 @@ const sast_warnings = { sql_injection: "Littéraux de gabarit avec expressions interpolées dans les requêtes SQL (SELECT, INSERT, UPDATE, DELETE) sans paramétrisation appropriée, créant des vulnérabilités potentielles d'injection SQL.", monkey_patch: "Modification des prototypes natifs ou objets globaux à l'exécution, ce qui introduit des risques de sécurité incluant le détournement de flux, des effets secondaires globaux et la dissimulation potentielle d'activités malveillantes.", insecure_random: "Utilisation d'une génération de nombres aléatoires non sécurisée à l'aide de Math.random(). Math.random() n'est pas cryptographiquement sûr et ne doit pas être utilisé pour des opérations sensibles en matière de sécurité.", - weak_scrypt: "Utilisation de crypto.scrypt() ou crypto.scryptSync() avec des paramètres non sécurisés tels qu'un sel codé en dur, un sel trop court (moins de 16 octets), ou un paramètre de coût insuffisant (inférieur à 16384). Ces configurations faibles compromettent la sécurité de la dérivation de clé basée sur un mot de passe." + weak_scrypt: "Utilisation de crypto.scrypt() ou crypto.scryptSync() avec des paramètres non sécurisés tels qu'un sel codé en dur, un sel trop court (moins de 16 octets), ou un paramètre de coût insuffisant (inférieur à 16384). Ces configurations faibles compromettent la sécurité de la dérivation de clé basée sur un mot de passe.", + weak_argon2: "Utilisation de crypto.argon2() ou crypto.argon2Sync() avec des paramètres non sécurisés : utilisation de argon2d ou argon2i au lieu de argon2id, nonce (sel) codé en dur, ou valeurs de memory/passes/parallelism inférieures aux recommandations minimales OWASP. Ces configurations faibles compromettent la sécurité de la dérivation de clé basée sur un mot de passe." }; export default { diff --git a/workspaces/js-x-ray/src/i18n/turkish.js b/workspaces/js-x-ray/src/i18n/turkish.js index 1d597d05..68f85b3b 100644 --- a/workspaces/js-x-ray/src/i18n/turkish.js +++ b/workspaces/js-x-ray/src/i18n/turkish.js @@ -21,7 +21,8 @@ const sast_warnings = { sql_injection: "SQL sorgularında (SELECT, INSERT, UPDATE, DELETE) uygun parametreleştirme yapılmadan kullanılan ifadeler içeren şablon dizeleri, potansiyel SQL enjeksiyonu güvenlik açıkları oluşturur.", monkey_patch: "Çalışma zamanında yerel prototiplerin veya global nesnelerin değiştirilmesi; akış ele geçirme, global yan etkiler ve kötü niyetli faaliyetlerin gizlenmesi dahil olmak üzere güvenlik riskleri oluşturur.", insecure_random: "Math.random() kullanılarak güvensiz rastgele sayı üretimi. Math.random() kriptografik olarak güvenli değildir ve güvenliğe duyarlı işlemler için kullanılmamalıdır.", - weak_scrypt: "crypto.scrypt() veya crypto.scryptSync() fonksiyonlarının sabit kodlanmış tuz, kısa tuz (16 bayttan az) veya yetersiz maliyet parametresi (16384'ün altında) gibi güvensiz parametrelerle kullanımı. Bu zayıf yapılandırmalar, parola tabanlı anahtar türetme güvenliğini tehlikeye atar." + weak_scrypt: "crypto.scrypt() veya crypto.scryptSync() fonksiyonlarının sabit kodlanmış tuz, kısa tuz (16 bayttan az) veya yetersiz maliyet parametresi (16384'ün altında) gibi güvensiz parametrelerle kullanımı. Bu zayıf yapılandırmalar, parola tabanlı anahtar türetme güvenliğini tehlikeye atar.", + weak_argon2: "crypto.argon2() veya crypto.argon2Sync() fonksiyonlarının güvensiz parametrelerle kullanımı: argon2id yerine argon2d veya argon2i kullanımı, sabit kodlanmış nonce (tuz) veya OWASP minimum önerilerinin altında memory/passes/parallelism değerleri. Bu zayıf yapılandırmalar, parola tabanlı anahtar türetme güvenliğini tehlikeye atar." }; export default { diff --git a/workspaces/js-x-ray/src/probes/isWeakArgon2.ts b/workspaces/js-x-ray/src/probes/isWeakArgon2.ts new file mode 100644 index 00000000..a473f6b2 --- /dev/null +++ b/workspaces/js-x-ray/src/probes/isWeakArgon2.ts @@ -0,0 +1,142 @@ +// Import Third-party Dependencies +import type { ESTree } from "meriyah"; + +// Import Internal Dependencies +import type { ProbeContext } from "../ProbeRunner.ts"; +import { CALL_EXPRESSION_DATA } from "../contants.ts"; +import { isLiteral } from "../estree/types.ts"; +import { generateWarning } from "../warnings.ts"; + +const kOWASPMinParams: [minMemory: number, minIteration: number, minParallelism: number][] = [ + [47104, 1, 1], + [19456, 2, 1], + [12288, 3, 1], + [9216, 4, 1], + [7168, 5, 1] +]; + +const tracedFunctions = new Set(["crypto.argon2", "crypto.argon2Sync"]); + +function extractNumericParam( + properties: ESTree.Property[], + names: string[] +): number | null { + for (const prop of properties) { + if ( + prop.key.type === "Identifier" && + names.includes(prop.key.name) && + prop.value.type === "Literal" && + typeof prop.value.value === "number" + ) { + return prop.value.value; + } + } + + return null; +} + +function isWeakArgon2Params(memory: number, iteration: number, parallelism: number): boolean { + for (const [minMemory, minIteration, minParallelism] of kOWASPMinParams) { + if (memory >= minMemory) { + return parallelism < minParallelism || iteration < minIteration; + } + } + + return true; +} + +function validateNode( + _node: ESTree.Node, + ctx: ProbeContext): [boolean, any?] { + const { tracer } = ctx.sourceFile; + + if (!tracer.importedModules.has("crypto")) { + return [false]; + } + + return [ + tracedFunctions.has(ctx.context![CALL_EXPRESSION_DATA]?.identifierOrMemberExpr) + ]; +} + +function initialize(ctx: ProbeContext) { + const { tracer } = ctx.sourceFile; + + for (const identifierOrMemberExpr of tracedFunctions) { + tracer.trace(identifierOrMemberExpr, { + followConsecutiveAssignment: true, + moduleName: "crypto" + }); + } +} + +function main(node: ESTree.CallExpression, ctx: ProbeContext) { + const { sourceFile } = ctx; + const algorithm = node.arguments.at(0); + + if (algorithm && algorithm.type === "Identifier") { + const algorithmName = sourceFile.tracer.literalIdentifiers.get(algorithm.name)?.value; + if (algorithmName && algorithmName !== "argon2id") { + sourceFile.warnings.push( + generateWarning("weak-argon2", { + value: `wrong-algorithm : ${algorithmName}`, + location: node.loc + }) + ); + } + } + + if (isLiteral(algorithm)) { + if (algorithm.value !== "argon2id") { + sourceFile.warnings.push( + generateWarning("weak-argon2", { + value: `wrong-algorithm : ${algorithm.value}`, + location: node.loc + }) + ); + } + } + + const parameters = node.arguments.at(1); + if (parameters && parameters.type === "ObjectExpression") { + const properties = parameters.properties.filter( + (prop): prop is ESTree.Property => prop.type === "Property" + ); + const memory = extractNumericParam(properties, ["memory"]); + const iteration = extractNumericParam(properties, ["passes"]); + const parallelism = extractNumericParam(properties, ["parallelism"]); + + if (memory && iteration && parallelism) { + if (isWeakArgon2Params(memory, iteration, parallelism)) { + sourceFile.warnings.push( + generateWarning("weak-argon2", { + value: "weak-parameters", + location: node.loc + }) + ); + } + } + + const nonce = properties.find( + (prop) => prop.key.type === "Identifier" && prop.key.name === "nonce" + ); + + if (nonce && isLiteral(nonce.value)) { + sourceFile.warnings.push( + generateWarning("weak-argon2", { + value: "hardcoded-nonce", + location: node.loc + }) + ); + } + } +} + +export default { + name: "isWeakArgon2", + validateNode, + main, + initialize, + breakOnMatch: false, + context: {} +}; diff --git a/workspaces/js-x-ray/src/warnings.ts b/workspaces/js-x-ray/src/warnings.ts index d8ab0a29..63b49d58 100644 --- a/workspaces/js-x-ray/src/warnings.ts +++ b/workspaces/js-x-ray/src/warnings.ts @@ -13,7 +13,8 @@ export type OptionalWarningName = | "synchronous-io" | "log-usage" | "insecure-random" - | "weak-scrypt"; + | "weak-scrypt" + | "weak-argon2"; export type WarningName = | "parsing-error" @@ -34,6 +35,7 @@ export type WarningName = | "monkey-patch" | "insecure-random" | "prototype-pollution" + | "weak-argon2" | OptionalWarningName; export interface Warning { @@ -152,6 +154,11 @@ export const warnings = Object.freeze({ i18n: "sast_warnings.weak_scrypt", severity: "Warning", experimental: true + }, + "weak-argon2": { + i18n: "sast_warnings.weak_argon2", + severity: "Warning", + experimental: true } }) satisfies Record>; diff --git a/workspaces/js-x-ray/test/probes/isWeakArgon2.spec.ts b/workspaces/js-x-ray/test/probes/isWeakArgon2.spec.ts new file mode 100644 index 00000000..fbbe529d --- /dev/null +++ b/workspaces/js-x-ray/test/probes/isWeakArgon2.spec.ts @@ -0,0 +1,324 @@ +// Import Node.js Dependencies +import assert from "node:assert"; +import { describe, it } from "node:test"; + +// Import Internal Dependencies +import { AstAnalyser } from "../../src/index.ts"; + +const safeParams = [ + "message: \"password\"", + "nonce: crypto.randomBytes(16)", + "memory: 47104", + "passes: 1", + "parallelism: 1", + "tagLength: 64" +].join(", "); + +function makeCode( + algorithm: string, + paramsOverride?: string, + fn = "argon2" +) { + const params = paramsOverride ?? safeParams; + + return ` + import crypto from "crypto"; + crypto.${fn}("${algorithm}", { ${params} }) + `; +} + +describe("isWeakArgon2", () => { + describe("wrong-algorithm", () => { + it("should warn when algorithm is argon2d", () => { + const { warnings: outputWarnings } = new AstAnalyser({ + optionalWarnings: ["weak-argon2"] + }).analyse(makeCode("argon2d")); + + assert.strictEqual(outputWarnings.length, 1); + assert.strictEqual(outputWarnings[0].kind, "weak-argon2"); + assert.strictEqual(outputWarnings[0].value, "wrong-algorithm : argon2d"); + }); + + it("should warn when algorithm is argon2i", () => { + const { warnings: outputWarnings } = new AstAnalyser({ + optionalWarnings: ["weak-argon2"] + }).analyse(makeCode("argon2i")); + + assert.strictEqual(outputWarnings.length, 1); + assert.strictEqual(outputWarnings[0].kind, "weak-argon2"); + assert.strictEqual(outputWarnings[0].value, "wrong-algorithm : argon2i"); + }); + + it("should warn when algorithm is an identifier assigned to argon2d", () => { + const code = ` + import crypto from "crypto"; + const algo = "argon2d"; + crypto.argon2(algo, { ${safeParams} }) + `; + const { warnings: outputWarnings } = new AstAnalyser({ + optionalWarnings: ["weak-argon2"] + }).analyse(code); + + assert.strictEqual(outputWarnings.length, 1); + assert.strictEqual(outputWarnings[0].kind, "weak-argon2"); + assert.strictEqual(outputWarnings[0].value, "wrong-algorithm : argon2d"); + }); + + it("should not warn when algorithm is an identifier assigned to argon2id", () => { + const code = ` + import crypto from "crypto"; + const algo = "argon2id"; + crypto.argon2(algo, { ${safeParams} }) + `; + const { warnings: outputWarnings } = new AstAnalyser({ + optionalWarnings: ["weak-argon2"] + }).analyse(code); + + assert.strictEqual(outputWarnings.length, 0); + }); + + it("should not warn when algorithm is an unresolvable identifier", () => { + const code = ` + import crypto from "crypto"; + crypto.argon2(unknownVar, { ${safeParams} }) + `; + const { warnings: outputWarnings } = new AstAnalyser({ + optionalWarnings: ["weak-argon2"] + }).analyse(code); + + assert.strictEqual(outputWarnings.length, 0); + }); + + it("should not warn when algorithm is argon2id", () => { + const { warnings: outputWarnings } = new AstAnalyser({ + optionalWarnings: ["weak-argon2"] + }).analyse(makeCode("argon2id")); + + assert.strictEqual(outputWarnings.length, 0); + }); + }); + + describe("weak-parameters", () => { + it("should warn when memory is below all OWASP minimums (memory < 7168)", () => { + const params = [ + "message: \"password\"", + "nonce: crypto.randomBytes(16)", + "memory: 512, passes: 1, parallelism: 1, tagLength: 64" + ].join(", "); + const { warnings: outputWarnings } = new AstAnalyser({ + optionalWarnings: ["weak-argon2"] + }).analyse(makeCode("argon2id", params)); + + assert.strictEqual(outputWarnings.length, 1); + assert.strictEqual(outputWarnings[0].value, "weak-parameters"); + }); + + it("should warn when passes is below OWASP minimum (memory=7168, passes=4)", () => { + const params = [ + "message: \"password\"", + "nonce: crypto.randomBytes(16)", + "memory: 7168, passes: 4, parallelism: 1, tagLength: 64" + ].join(", "); + const { warnings: outputWarnings } = new AstAnalyser({ + optionalWarnings: ["weak-argon2"] + }).analyse(makeCode("argon2id", params)); + + assert.strictEqual(outputWarnings.length, 1); + assert.strictEqual(outputWarnings[0].value, "weak-parameters"); + }); + + it("should not warn when params meet OWASP minimum (memory=7168, passes=5, parallelism=1)", () => { + const params = [ + "message: \"password\"", + "nonce: crypto.randomBytes(16)", + "memory: 7168, passes: 5, parallelism: 1, tagLength: 64" + ].join(", "); + const { warnings: outputWarnings } = new AstAnalyser({ + optionalWarnings: ["weak-argon2"] + }).analyse(makeCode("argon2id", params)); + + assert.strictEqual(outputWarnings.length, 0); + }); + + it("should not warn when params meet OWASP minimum (memory=47104, passes=1, parallelism=1)", () => { + const { warnings: outputWarnings } = new AstAnalyser({ + optionalWarnings: ["weak-argon2"] + }).analyse(makeCode("argon2id")); + + assert.strictEqual(outputWarnings.length, 0); + }); + + it("should not warn when params meet OWASP minimum (memory=19456, passes=2, parallelism=1)", () => { + const params = [ + "message: \"password\"", + "nonce: crypto.randomBytes(16)", + "memory: 19456, passes: 2, parallelism: 1, tagLength: 64" + ].join(", "); + const { warnings: outputWarnings } = new AstAnalyser({ + optionalWarnings: ["weak-argon2"] + }).analyse(makeCode("argon2id", params)); + + assert.strictEqual(outputWarnings.length, 0); + }); + + it("should warn when passes is below OWASP minimum (memory=19456, passes=1)", () => { + const params = [ + "message: \"password\"", + "nonce: crypto.randomBytes(16)", + "memory: 19456, passes: 1, parallelism: 1, tagLength: 64" + ].join(", "); + const { warnings: outputWarnings } = new AstAnalyser({ + optionalWarnings: ["weak-argon2"] + }).analyse(makeCode("argon2id", params)); + + assert.strictEqual(outputWarnings.length, 1); + assert.strictEqual(outputWarnings[0].value, "weak-parameters"); + }); + + it("should warn when using argon2Sync with weak parameters", () => { + const params = [ + "message: \"password\"", + "nonce: crypto.randomBytes(16)", + "memory: 512, passes: 1, parallelism: 1, tagLength: 64" + ].join(", "); + const { warnings: outputWarnings } = new AstAnalyser({ + optionalWarnings: ["weak-argon2"] + }).analyse(makeCode("argon2id", params, "argon2Sync")); + + assert.strictEqual(outputWarnings.length, 1); + assert.strictEqual(outputWarnings[0].value, "weak-parameters"); + }); + }); + + describe("hardcoded-nonce", () => { + it("should warn when nonce is a string literal", () => { + const params = [ + "message: \"password\"", + "nonce: \"hardcoded-salt\"", + "memory: 47104, passes: 1, parallelism: 1, tagLength: 64" + ].join(", "); + const { warnings: outputWarnings } = new AstAnalyser({ + optionalWarnings: ["weak-argon2"] + }).analyse(makeCode("argon2id", params)); + + assert.strictEqual(outputWarnings.length, 1); + assert.strictEqual(outputWarnings[0].value, "hardcoded-nonce"); + }); + + it("should not warn when nonce is crypto.randomBytes()", () => { + const { warnings: outputWarnings } = new AstAnalyser({ + optionalWarnings: ["weak-argon2"] + }).analyse(makeCode("argon2id")); + + assert.strictEqual(outputWarnings.length, 0); + }); + + it("should not warn when nonce is a variable reference", () => { + const code = ` + import crypto from "crypto"; + const salt = crypto.randomBytes(16); + crypto.argon2("argon2id", { + message: "password", nonce: salt, + memory: 47104, passes: 1, + parallelism: 1, tagLength: 64 + }) + `; + const { warnings: outputWarnings } = new AstAnalyser({ + optionalWarnings: ["weak-argon2"] + }).analyse(code); + + assert.strictEqual(outputWarnings.length, 0); + }); + }); + + describe("combined warnings", () => { + it("should emit both wrong-algorithm and weak-parameters warnings", () => { + const params = [ + "message: \"password\"", + "nonce: crypto.randomBytes(16)", + "memory: 512, passes: 1, parallelism: 1, tagLength: 64" + ].join(", "); + const { warnings: outputWarnings } = new AstAnalyser({ + optionalWarnings: ["weak-argon2"] + }).analyse(makeCode("argon2d", params)); + + assert.strictEqual(outputWarnings.length, 2); + const values = outputWarnings.map((w) => w.value); + assert.ok(values.includes("wrong-algorithm : argon2d")); + assert.ok(values.includes("weak-parameters")); + }); + + it("should emit both wrong-algorithm and hardcoded-nonce warnings", () => { + const params = [ + "message: \"password\"", + "nonce: \"salt\"", + "memory: 47104, passes: 1, parallelism: 1, tagLength: 64" + ].join(", "); + const { warnings: outputWarnings } = new AstAnalyser({ + optionalWarnings: ["weak-argon2"] + }).analyse(makeCode("argon2d", params)); + + assert.strictEqual(outputWarnings.length, 2); + const values = outputWarnings.map((w) => w.value); + assert.ok(values.includes("wrong-algorithm : argon2d")); + assert.ok(values.includes("hardcoded-nonce")); + }); + + it("should emit all three warnings simultaneously", () => { + const params = [ + "message: \"password\"", + "nonce: \"salt\"", + "memory: 512, passes: 1, parallelism: 1, tagLength: 64" + ].join(", "); + const { warnings: outputWarnings } = new AstAnalyser({ + optionalWarnings: ["weak-argon2"] + }).analyse(makeCode("argon2d", params)); + + assert.strictEqual(outputWarnings.length, 3); + const values = outputWarnings.map((w) => w.value); + assert.ok(values.includes("wrong-algorithm : argon2d")); + assert.ok(values.includes("weak-parameters")); + assert.ok(values.includes("hardcoded-nonce")); + }); + }); + + describe("no warnings (proper usage)", () => { + it("should not warn with proper argon2id usage", () => { + const { warnings: outputWarnings } = new AstAnalyser({ + optionalWarnings: ["weak-argon2"] + }).analyse(makeCode("argon2id")); + + assert.strictEqual(outputWarnings.length, 0); + }); + + it("should not warn when crypto module is not imported", () => { + const code = ` + const crypto = { argon2() {} }; + crypto.argon2("argon2d", { + message: "password", nonce: "salt", + memory: 512, passes: 1, + parallelism: 1, tagLength: 64 + }) + `; + const { warnings: outputWarnings } = new AstAnalyser({ + optionalWarnings: ["weak-argon2"] + }).analyse(code); + + assert.strictEqual(outputWarnings.length, 0); + }); + }); + + describe("optional warning behavior", () => { + it("should NOT report warnings when weak-argon2 is not enabled", () => { + const params = [ + "message: \"password\"", + "nonce: \"salt\"", + "memory: 512, passes: 1, parallelism: 1, tagLength: 64" + ].join(", "); + const { warnings: outputWarnings } = new AstAnalyser() + .analyse(makeCode("argon2d", params)); + + assert.strictEqual(outputWarnings.length, 0); + }); + }); +});