- CVE-2023-39333.patch: Code injection via WebAssembly export names (CVE-2023-39333, bsc#1216273) - CVE-2023-44487.patch: nghttp2 Security Release (CVE-2023-44487, bsc#1216190) - CVE-2023-45143.patch: undici Security Release (CVE-2023-39333, bsc#1216273) OBS-URL: https://build.opensuse.org/package/show/devel:languages:nodejs/nodejs16?expand=0&rev=99
226 lines
8.4 KiB
Diff
226 lines
8.4 KiB
Diff
commit 1c538938ccadfd35fbc699d8e85102736cd5945c
|
|
Author: Tobias Nießen <tniessen@tnie.de>
|
|
Date: Sun Aug 6 12:56:02 2023 +0000
|
|
|
|
policy: use tamper-proof integrity check function
|
|
|
|
Using the JavaScript Hash class is unsafe because its internals can be
|
|
tampered with. In particular, an application can cause
|
|
Hash.prototype.digest() to return arbitrary values, thus allowing to
|
|
circumvent the integrity verification that policies are supposed to
|
|
guarantee.
|
|
|
|
Add and use a new C++ binding internalVerifyIntegrity() that (hopefully)
|
|
cannot be tampered with from JavaScript.
|
|
|
|
PR-URL: https://github.com/nodejs-private/node-private/pull/462
|
|
Backport-PR-URL: https://github.com/nodejs-private/node-private/pull/493
|
|
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
|
|
CVE-ID: CVE-2023-38552
|
|
|
|
Index: node-v16.20.2/lib/internal/policy/manifest.js
|
|
===================================================================
|
|
--- node-v16.20.2.orig/lib/internal/policy/manifest.js
|
|
+++ node-v16.20.2/lib/internal/policy/manifest.js
|
|
@@ -16,7 +16,6 @@ const {
|
|
StringPrototypeEndsWith,
|
|
StringPrototypeStartsWith,
|
|
Symbol,
|
|
- uncurryThis,
|
|
} = primordials;
|
|
const {
|
|
ERR_MANIFEST_ASSERT_INTEGRITY,
|
|
@@ -28,13 +27,8 @@ let debug = require('internal/util/debug
|
|
debug = fn;
|
|
});
|
|
const SRI = require('internal/policy/sri');
|
|
-const crypto = require('crypto');
|
|
-const { Buffer } = require('buffer');
|
|
const { URL } = require('internal/url');
|
|
-const { createHash, timingSafeEqual } = crypto;
|
|
-const HashUpdate = uncurryThis(crypto.Hash.prototype.update);
|
|
-const HashDigest = uncurryThis(crypto.Hash.prototype.digest);
|
|
-const BufferToString = uncurryThis(Buffer.prototype.toString);
|
|
+const { internalVerifyIntegrity } = internalBinding('crypto');
|
|
const kRelativeURLStringPattern = /^\.{0,2}\//;
|
|
const { getOptionValue } = require('internal/options');
|
|
const shouldAbortOnUncaughtException = getOptionValue(
|
|
@@ -589,16 +583,13 @@ class Manifest {
|
|
// Avoid clobbered Symbol.iterator
|
|
for (let i = 0; i < integrityEntries.length; i++) {
|
|
const { algorithm, value: expected } = integrityEntries[i];
|
|
- const hash = createHash(algorithm);
|
|
// TODO(tniessen): the content should not be passed as a string in the
|
|
// first place, see https://github.com/nodejs/node/issues/39707
|
|
- HashUpdate(hash, content, 'utf8');
|
|
- const digest = HashDigest(hash, 'buffer');
|
|
- if (digest.length === expected.length &&
|
|
- timingSafeEqual(digest, expected)) {
|
|
+ const mismatchedIntegrity = internalVerifyIntegrity(algorithm, content, expected);
|
|
+ if (mismatchedIntegrity === undefined) {
|
|
return true;
|
|
}
|
|
- realIntegrities.set(algorithm, BufferToString(digest, 'base64'));
|
|
+ realIntegrities.set(algorithm, mismatchedIntegrity);
|
|
}
|
|
}
|
|
|
|
Index: node-v16.20.2/src/crypto/crypto_hash.cc
|
|
===================================================================
|
|
--- node-v16.20.2.orig/src/crypto/crypto_hash.cc
|
|
+++ node-v16.20.2/src/crypto/crypto_hash.cc
|
|
@@ -69,6 +69,9 @@ void Hash::Initialize(Environment* env,
|
|
SetMethodNoSideEffect(context, target, "getHashes", GetHashes);
|
|
|
|
HashJob::Initialize(env, target);
|
|
+
|
|
+ SetMethodNoSideEffect(
|
|
+ context, target, "internalVerifyIntegrity", InternalVerifyIntegrity);
|
|
}
|
|
|
|
void Hash::RegisterExternalReferences(ExternalReferenceRegistry* registry) {
|
|
@@ -78,6 +81,8 @@ void Hash::RegisterExternalReferences(Ex
|
|
registry->Register(GetHashes);
|
|
|
|
HashJob::RegisterExternalReferences(registry);
|
|
+
|
|
+ registry->Register(InternalVerifyIntegrity);
|
|
}
|
|
|
|
void Hash::New(const FunctionCallbackInfo<Value>& args) {
|
|
@@ -322,5 +327,50 @@ bool HashTraits::DeriveBits(
|
|
return true;
|
|
}
|
|
|
|
+void InternalVerifyIntegrity(const v8::FunctionCallbackInfo<v8::Value>& args) {
|
|
+ Environment* env = Environment::GetCurrent(args);
|
|
+
|
|
+ CHECK_EQ(args.Length(), 3);
|
|
+
|
|
+ CHECK(args[0]->IsString());
|
|
+ Utf8Value algorithm(env->isolate(), args[0]);
|
|
+
|
|
+ CHECK(args[1]->IsString() || IsAnyByteSource(args[1]));
|
|
+ ByteSource content = ByteSource::FromStringOrBuffer(env, args[1]);
|
|
+
|
|
+ CHECK(args[2]->IsArrayBufferView());
|
|
+ ArrayBufferOrViewContents<unsigned char> expected(args[2]);
|
|
+
|
|
+ const EVP_MD* md_type = EVP_get_digestbyname(*algorithm);
|
|
+ unsigned char digest[EVP_MAX_MD_SIZE];
|
|
+ unsigned int digest_size;
|
|
+ if (md_type == nullptr || EVP_Digest(content.data<const char*>(),
|
|
+ content.size(),
|
|
+ digest,
|
|
+ &digest_size,
|
|
+ md_type,
|
|
+ nullptr) != 1) {
|
|
+ return ThrowCryptoError(
|
|
+ env, ERR_get_error(), "Digest method not supported");
|
|
+ }
|
|
+
|
|
+ if (digest_size != expected.size() ||
|
|
+ CRYPTO_memcmp(digest, expected.data(), digest_size) != 0) {
|
|
+ Local<Value> error;
|
|
+ MaybeLocal<Value> rc =
|
|
+ StringBytes::Encode(env->isolate(),
|
|
+ reinterpret_cast<const char*>(digest),
|
|
+ digest_size,
|
|
+ BASE64,
|
|
+ &error);
|
|
+ if (rc.IsEmpty()) {
|
|
+ CHECK(!error.IsEmpty());
|
|
+ env->isolate()->ThrowException(error);
|
|
+ return;
|
|
+ }
|
|
+ args.GetReturnValue().Set(rc.FromMaybe(Local<Value>()));
|
|
+ }
|
|
+}
|
|
+
|
|
} // namespace crypto
|
|
} // namespace node
|
|
Index: node-v16.20.2/src/crypto/crypto_hash.h
|
|
===================================================================
|
|
--- node-v16.20.2.orig/src/crypto/crypto_hash.h
|
|
+++ node-v16.20.2/src/crypto/crypto_hash.h
|
|
@@ -82,6 +82,8 @@ struct HashTraits final {
|
|
|
|
using HashJob = DeriveBitsJob<HashTraits>;
|
|
|
|
+void InternalVerifyIntegrity(const v8::FunctionCallbackInfo<v8::Value>& args);
|
|
+
|
|
} // namespace crypto
|
|
} // namespace node
|
|
|
|
Index: node-v16.20.2/test/fixtures/policy/crypto-hash-tampering/.gitattributes
|
|
===================================================================
|
|
--- /dev/null
|
|
+++ node-v16.20.2/test/fixtures/policy/crypto-hash-tampering/.gitattributes
|
|
@@ -0,0 +1 @@
|
|
+*.js text eol=lf
|
|
Index: node-v16.20.2/test/fixtures/policy/crypto-hash-tampering/main.js
|
|
===================================================================
|
|
--- /dev/null
|
|
+++ node-v16.20.2/test/fixtures/policy/crypto-hash-tampering/main.js
|
|
@@ -0,0 +1,8 @@
|
|
+const h = require('crypto').createHash('sha384');
|
|
+const fakeDigest = h.digest();
|
|
+
|
|
+const kHandle = Object.getOwnPropertySymbols(h)
|
|
+ .find((s) => s.description === 'kHandle');
|
|
+h[kHandle].constructor.prototype.digest = () => fakeDigest;
|
|
+
|
|
+require('./protected.js');
|
|
Index: node-v16.20.2/test/fixtures/policy/crypto-hash-tampering/policy.json
|
|
===================================================================
|
|
--- /dev/null
|
|
+++ node-v16.20.2/test/fixtures/policy/crypto-hash-tampering/policy.json
|
|
@@ -0,0 +1,15 @@
|
|
+{
|
|
+ "resources": {
|
|
+ "./main.js": {
|
|
+ "integrity": true,
|
|
+ "dependencies": {
|
|
+ "./protected.js": true,
|
|
+ "crypto": true
|
|
+ }
|
|
+ },
|
|
+ "./protected.js": {
|
|
+ "integrity": "sha384-OLBgp1GsljhM2TJ+sbHjaiH9txEUvgdDTAzHv2P24donTt6/529l+9Ua0vFImLlb",
|
|
+ "dependencies": true
|
|
+ }
|
|
+ }
|
|
+}
|
|
Index: node-v16.20.2/test/fixtures/policy/crypto-hash-tampering/protected.js
|
|
===================================================================
|
|
--- /dev/null
|
|
+++ node-v16.20.2/test/fixtures/policy/crypto-hash-tampering/protected.js
|
|
@@ -0,0 +1 @@
|
|
+console.log(require('fs').readFileSync('/etc/passwd').length);
|
|
Index: node-v16.20.2/test/parallel/test-policy-crypto-hash-tampering.js
|
|
===================================================================
|
|
--- /dev/null
|
|
+++ node-v16.20.2/test/parallel/test-policy-crypto-hash-tampering.js
|
|
@@ -0,0 +1,21 @@
|
|
+'use strict';
|
|
+
|
|
+const common = require('../common');
|
|
+if (!common.hasCrypto)
|
|
+ common.skip('missing crypto');
|
|
+common.requireNoPackageJSONAbove();
|
|
+
|
|
+const fixtures = require('../common/fixtures');
|
|
+
|
|
+const assert = require('assert');
|
|
+const { spawnSync } = require('child_process');
|
|
+
|
|
+const mainPath = fixtures.path('policy', 'crypto-hash-tampering', 'main.js');
|
|
+const policyPath = fixtures.path(
|
|
+ 'policy',
|
|
+ 'crypto-hash-tampering',
|
|
+ 'policy.json');
|
|
+const { status, stderr } =
|
|
+ spawnSync(process.execPath, ['--experimental-policy', policyPath, mainPath], { encoding: 'utf8' });
|
|
+assert.strictEqual(status, 1);
|
|
+assert(stderr.includes('sha384-Bnp/T8gFNzT9mHj2G/AeuMH8LcAQ4mljw15nxBNl5yaGM7VgbMzDT7O4+dXZTJJn'));
|