Commit 36b76ca2eec for nodejs

commit 36b76ca2eeceaf29ae7a1a14182d12818b0f4242
Author: Filip Skokan <panva.ip@gmail.com>
Date:   Tue Sep 22 22:27:08 2026 +0200

    crypto: handle PBKDF2 iteration limits

    Report OperationError for unsupported iteration counts and return an
    empty result before invoking the backend when no output is requested.

    Signed-off-by: Filip Skokan <panva.ip@gmail.com>
    Assisted-by: Codex
    PR-URL: https://github.com/nodejs/node/pull/66237
    Reviewed-By: James M Snell <jasnell@gmail.com>
    Reviewed-By: Aviv Keller <me@aviv.sh>

diff --git a/lib/internal/crypto/pbkdf2.js b/lib/internal/crypto/pbkdf2.js
index f42ce3bbd13..b9f6b59c85b 100644
--- a/lib/internal/crypto/pbkdf2.js
+++ b/lib/internal/crypto/pbkdf2.js
@@ -16,6 +16,7 @@ const {
 } = internalBinding('crypto');

 const {
+  isInt32,
   validateFunction,
   validateInt32,
   validateString,
@@ -98,7 +99,7 @@ function check(password, salt, iterations, keylen, digest) {
   return { password, salt, iterations, keylen, digest };
 }

-function validatePbkdf2DeriveBitsLength(length) {
+function validatePbkdf2DeriveBits({ iterations }, length) {
   if (length === null)
     throw lazyDOMException('length cannot be null', 'OperationError');

@@ -107,10 +108,16 @@ function validatePbkdf2DeriveBitsLength(length) {
       'length must be a multiple of 8',
       'OperationError');
   }
+  if (iterations === 0)
+    throw lazyDOMException('iterations cannot be zero', 'OperationError');
+  if (length !== 0 && !isInt32(iterations)) {
+    throw lazyDOMException(
+      'iterations exceeds the implementation limit', 'OperationError');
+  }
 }

 function pbkdf2DeriveBits(algorithm, baseKey, length) {
-  validatePbkdf2DeriveBitsLength(length);
+  validatePbkdf2DeriveBits(algorithm, length);
   const { iterations, hash, salt } = algorithm;

   if (length === 0)
@@ -129,5 +136,5 @@ module.exports = {
   pbkdf2,
   pbkdf2Sync,
   pbkdf2DeriveBits,
-  validatePbkdf2DeriveBitsLength,
+  validatePbkdf2DeriveBits,
 };
diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js
index 73ae08fa1e1..ae45d8a9ec2 100644
--- a/lib/internal/crypto/webcrypto.js
+++ b/lib/internal/crypto/webcrypto.js
@@ -1937,7 +1937,7 @@ function check(op, alg, length) {
       }

       if (normalizedAlgorithm.name === 'PBKDF2') {
-        require('internal/crypto/pbkdf2').validatePbkdf2DeriveBitsLength(length);
+        require('internal/crypto/pbkdf2').validatePbkdf2DeriveBits(normalizedAlgorithm, length);
       }

       if (StringPrototypeStartsWith(normalizedAlgorithm.name, 'Argon2')) {
diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js
index 5b3fc8d605b..1d35ce69f56 100644
--- a/lib/internal/crypto/webidl.js
+++ b/lib/internal/crypto/webidl.js
@@ -14,7 +14,6 @@ const {
   lazyDOMException,
 } = require('internal/util');
 const {
-  isInt32,
   isUint32,
 } = require('internal/validators');
 const {
@@ -468,15 +467,6 @@ converters.Pbkdf2Params = createDictionaryConverter(
         key: 'iterations',
         converter: (V, opts) =>
           converters['unsigned long'](V, enforceRangeOptions(opts)),
-        validator: (V, dict) => {
-          if (V === 0)
-            throw lazyDOMException('iterations cannot be zero', 'OperationError');
-          if (!isInt32(V)) {
-            throw lazyDOMException(
-              'iterations exceeds the implementation limit',
-              'NotSupportedError');
-          }
-        },
         required: true,
       },
       {
diff --git a/test/parallel/test-webcrypto-derivebits.js b/test/parallel/test-webcrypto-derivebits.js
index 99de1e6ba6d..ee4de1c26a2 100644
--- a/test/parallel/test-webcrypto-derivebits.js
+++ b/test/parallel/test-webcrypto-derivebits.js
@@ -136,7 +136,7 @@ const rejectsXCurves = hasFIPS(3, 5);
         salt: new Uint8Array([2]),
         iterations: 2 ** 31,
       }, key, 8),
-      { name: 'NotSupportedError' });
+      { name: 'OperationError' });
   }

   test().then(common.mustCall());
diff --git a/test/parallel/test-webcrypto-pbkdf2-iteration-limits.js b/test/parallel/test-webcrypto-pbkdf2-iteration-limits.js
new file mode 100644
index 00000000000..168ee83a641
--- /dev/null
+++ b/test/parallel/test-webcrypto-pbkdf2-iteration-limits.js
@@ -0,0 +1,21 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+const assert = require('assert');
+const { subtle } = globalThis.crypto;
+
+(async () => {
+  const key = await subtle.importKey('raw', new Uint8Array(16), 'PBKDF2', false, ['deriveBits']);
+  for (const iterations of [2 ** 31, 2 ** 32 - 1]) {
+    const algorithm = { name: 'PBKDF2', hash: 'SHA-256', salt: new Uint8Array(16), iterations };
+    assert.strictEqual((await subtle.deriveBits(algorithm, key, 0)).byteLength, 0);
+    assert.strictEqual(SubtleCrypto.supports('deriveBits', algorithm, 0), true);
+    assert.strictEqual(SubtleCrypto.supports('deriveBits', algorithm, 8), false);
+    await assert.rejects(subtle.deriveBits(algorithm, key, 8), { name: 'OperationError' });
+  }
+  const algorithm = { name: 'PBKDF2', hash: 'SHA-256', salt: new Uint8Array(16), iterations: 0 };
+  await assert.rejects(subtle.deriveBits(algorithm, key, 0), { name: 'OperationError' });
+})().then(common.mustCall());