From 8b7036fac15498bc1aae95d5548ef9093e9839e6 Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Mon, 2 Mar 2026 09:35:48 -0800 Subject: [PATCH] WebCrypto: validate JWK key type against algorithm in importKey(). Previously, importKey() did not verify that the JWK "kty" field matched the requested algorithm. For example, importing a JWK with kty "oct" (symmetric) while specifying an asymmetric algorithm like ECDH caused a SEGV in EVP_PKEY_free() during cleanup. This happened because the symmetric key data written into the union's "raw" member overlapped with the "pkey" pointer, corrupting it. The fix validates kty before calling any JWK import function: - "RSA" is only accepted for RSA-OAEP, RSA-PSS, RSASSA-PKCS1-v1_5 - "EC" is only accepted for ECDSA, ECDH - "oct" is only accepted for HMAC, AES-GCM, AES-CTR, AES-CBC Found by Akshay Jain (akshaythe@gmail.com). --- external/njs_webcrypto_module.c | 31 ++++++++++++++++++- external/qjs_webcrypto_module.c | 26 ++++++++++++++++ test/webcrypto/import.t.mjs | 54 +++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 1 deletion(-) diff --git a/external/njs_webcrypto_module.c b/external/njs_webcrypto_module.c index b9a74353..33d5ff3d 100644 --- a/external/njs_webcrypto_module.c +++ b/external/njs_webcrypto_module.c @@ -3634,12 +3634,31 @@ njs_ext_import_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, } if (njs_strstr_eq(&kty, &njs_str_value("RSA"))) { + if (alg->type != NJS_ALGORITHM_RSASSA_PKCS1_v1_5 + && alg->type != NJS_ALGORITHM_RSA_PSS + && alg->type != NJS_ALGORITHM_RSA_OAEP) + { + njs_vm_type_error(vm, "JWK kty \"RSA\" doesn't match " + "algorithm \"%V\"", + njs_algorithm_string(alg)); + goto fail; + } + pkey = njs_import_jwk_rsa(vm, jwk, key); if (njs_slow_path(pkey == NULL)) { goto fail; } } else if (njs_strstr_eq(&kty, &njs_str_value("EC"))) { + if (alg->type != NJS_ALGORITHM_ECDSA + && alg->type != NJS_ALGORITHM_ECDH) + { + njs_vm_type_error(vm, "JWK kty \"EC\" doesn't match " + "algorithm \"%V\"", + njs_algorithm_string(alg)); + goto fail; + } + ret = njs_algorithm_curve(vm, options, &key->u.a.curve); if (njs_slow_path(ret == NJS_ERROR)) { goto fail; @@ -3651,6 +3670,13 @@ njs_ext_import_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, } } else if (njs_strstr_eq(&kty, &njs_str_value("oct"))) { + if (!alg->raw) { + njs_vm_type_error(vm, "JWK kty \"oct\" doesn't match " + "algorithm \"%V\"", + njs_algorithm_string(alg)); + goto fail; + } + ret = njs_import_jwk_oct(vm, jwk, key); if (njs_slow_path(ret != NJS_OK)) { goto fail; @@ -4609,7 +4635,10 @@ njs_webcrypto_cleanup_pkey(void *data) njs_webcrypto_key_t *key = data; if (!key->alg->raw) { - EVP_PKEY_free(key->u.a.pkey); + if (key->u.a.pkey != NULL) { + EVP_PKEY_free(key->u.a.pkey); + } + } } diff --git a/external/qjs_webcrypto_module.c b/external/qjs_webcrypto_module.c index b9c645d9..996f2a84 100644 --- a/external/qjs_webcrypto_module.c +++ b/external/qjs_webcrypto_module.c @@ -3487,6 +3487,16 @@ qjs_webcrypto_import_key(JSContext *cx, JSValueConst this_val, int argc, switch (kty) { case QJS_KEY_JWK_KTY_RSA: + if (alg->type != QJS_ALGORITHM_RSASSA_PKCS1_v1_5 + && alg->type != QJS_ALGORITHM_RSA_PSS + && alg->type != QJS_ALGORITHM_RSA_OAEP) + { + JS_ThrowTypeError(cx, "JWK kty \"RSA\" doesn't " + "match algorithm \"%s\"", + qjs_algorithm_string(alg)); + goto fail; + } + pkey = qjs_import_jwk_rsa(cx, jwk, wkey); if (pkey == NULL) { goto fail; @@ -3495,6 +3505,15 @@ qjs_webcrypto_import_key(JSContext *cx, JSValueConst this_val, int argc, break; case QJS_KEY_JWK_KTY_EC: + if (alg->type != QJS_ALGORITHM_ECDSA + && alg->type != QJS_ALGORITHM_ECDH) + { + JS_ThrowTypeError(cx, "JWK kty \"EC\" doesn't " + "match algorithm \"%s\"", + qjs_algorithm_string(alg)); + goto fail; + } + ret = qjs_algorithm_curve(cx, options, &wkey->u.a.curve); if (JS_IsException(ret)) { goto fail; @@ -3509,6 +3528,13 @@ qjs_webcrypto_import_key(JSContext *cx, JSValueConst this_val, int argc, case QJS_KEY_JWK_KTY_OCT: default: + if (!alg->raw) { + JS_ThrowTypeError(cx, "JWK kty \"oct\" doesn't " + "match algorithm \"%s\"", + qjs_algorithm_string(alg)); + goto fail; + } + ret = qjs_import_jwk_oct(cx, jwk, wkey); if (JS_IsException(ret)) { goto fail; diff --git a/test/webcrypto/import.t.mjs b/test/webcrypto/import.t.mjs index 9ec0fb60..8ea544d6 100644 --- a/test/webcrypto/import.t.mjs +++ b/test/webcrypto/import.t.mjs @@ -178,6 +178,18 @@ let aes_tsuite = { alg: { name: "AES-CBC" }, usage: [ "encrypt", "decrypt" ] }, exception: "TypeError: Invalid JWK oct alg" }, + { key: { fmt: "jwk", + key: { kty: 'RSA' }, + alg: { name: "AES-CBC" }, + extractable: true, + usage: [ "encrypt" ] }, + exception: "TypeError: JWK kty \"RSA\" doesn't match algorithm \"AES-CBC\"" }, + { key: { fmt: "jwk", + key: { kty: 'EC' }, + alg: { name: "AES-GCM" }, + extractable: true, + usage: [ "encrypt" ] }, + exception: "TypeError: JWK kty \"EC\" doesn't match algorithm \"AES-GCM\"" }, ]}; let ec_tsuite = { @@ -341,6 +353,24 @@ let ec_tsuite = { extractable: true, usage: [ "verify" ] }, exception: "TypeError: EC_POINT_oct2point()" }, + { key: { fmt: "jwk", + key: { kty: 'oct', k: 'AQIDBAUG', alg: 'HS256' }, + alg: { name: "ECDSA", namedCurve: "P-256" }, + extractable: true, + usage: [ "sign" ] }, + exception: "TypeError: JWK kty \"oct\" doesn't match algorithm \"ECDSA\"" }, + { key: { fmt: "jwk", + key: { kty: 'oct', k: 'AQIDBAUG', alg: 'HS256' }, + alg: { name: "ECDH", namedCurve: "P-256" }, + extractable: true, + usage: [ "deriveBits" ] }, + exception: "TypeError: JWK kty \"oct\" doesn't match algorithm \"ECDH\"" }, + { key: { fmt: "jwk", + key: { kty: 'RSA' }, + alg: { name: "ECDSA", namedCurve: "P-256" }, + extractable: true, + usage: [ "sign" ] }, + exception: "TypeError: JWK kty \"RSA\" doesn't match algorithm \"ECDSA\"" }, ]}; let hmac_tsuite = { @@ -455,6 +485,18 @@ let hmac_tsuite = { extractable: true, usage: [ "verify" ]}, exception: "TypeError: Key operations and usage mismatch" }, + { key: { fmt: "jwk", + key: { kty: 'RSA' }, + alg: { name: "HMAC", hash: "SHA-256" }, + extractable: true, + usage: [ "sign" ] }, + exception: "TypeError: JWK kty \"RSA\" doesn't match algorithm \"HMAC\"" }, + { key: { fmt: "jwk", + key: { kty: 'EC' }, + alg: { name: "HMAC", hash: "SHA-256" }, + extractable: true, + usage: [ "sign" ] }, + exception: "TypeError: JWK kty \"EC\" doesn't match algorithm \"HMAC\"" }, ]}; let rsa_tsuite = { @@ -631,6 +673,18 @@ let rsa_tsuite = { extractable: true, usage: [ "encrypt" ] }, exception: "TypeError: d2i_PUBKEY() failed" }, + { key: { fmt: "jwk", + key: { kty: 'oct', k: 'AQIDBAUG', alg: 'HS256' }, + alg: { name: "RSA-OAEP", hash: "SHA-256" }, + extractable: true, + usage: [ "encrypt" ] }, + exception: "TypeError: JWK kty \"oct\" doesn't match algorithm \"RSA-OAEP\"" }, + { key: { fmt: "jwk", + key: { kty: 'EC' }, + alg: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, + extractable: true, + usage: [ "sign" ] }, + exception: "TypeError: JWK kty \"EC\" doesn't match algorithm \"RSASSA-PKCS1-v1_5\"" }, ]}; run([ -- 2.47.3