From bdc1aba982cc2ec1a74b0458c98ceeacc7acb9e7 Mon Sep 17 00:00:00 2001 From: Peter Somogyvari Date: Sun, 13 Mar 2022 21:46:51 -0700 Subject: [PATCH] fix(connector-besu/quorum/xdai): unvalidated dynamic method call Added checks to make sure that the Web3 Contract instances "methods" object has a property of their own called the same way the method is called by the request object. This way if someone tries to execute malicious code by providing method names that are designed to execute something other than the smart contract methods we throw back an error to them instead of complying. This is needed to fix the following CodeQL security advisories: https://github.com/hyperledger/cactus/security/code-scanning/23 https://github.com/hyperledger/cactus/security/code-scanning/24 https://github.com/hyperledger/cactus/security/code-scanning/25 https://github.com/hyperledger/cactus/security/code-scanning/26 Todo for later: create a web3-common package that can be used to house re-usable pieces of code such as the function that validates if a contract really has a certain method or not. Right now this method is copy pasted to all 3 web3 flavored connectors which is not very nice. Fixes #1911 Signed-off-by: Peter Somogyvari --- .../plugin-ledger-connector-besu.ts | 54 +++++++++++++++++++ .../plugin-ledger-connector-quorum.ts | 43 +++++++++++++++ .../plugin-ledger-connector-xdai.ts | 43 +++++++++++++++ 3 files changed, 140 insertions(+) diff --git a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/plugin-ledger-connector-besu.ts b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/plugin-ledger-connector-besu.ts index a2931f12bc..d10df91638 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/plugin-ledger-connector-besu.ts +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/plugin-ledger-connector-besu.ts @@ -305,6 +305,39 @@ export class PluginLedgerConnectorBesu return consensusHasTransactionFinality(currentConsensusAlgorithmFamily); } + + /** + * Verifies that it is safe to call a specific method of a Web3 Contract. + * + * @param contract The Web3 Contract instance to check whether it has a method with a specific name or not. + * @param name The name of the method that will be checked if it's usable on `contract` or not. + * @returns Boolean `true` when it IS safe to call the method named `name` on the contract. + * @throws If the contract instance is falsy or it's methods object is falsy. Also throws if the method name is a blank string. + */ + public async isSafeToCallContractMethod( + contract: Contract, + name: string, + ): Promise { + Checks.truthy( + contract, + `${this.className}#isSafeToCallContractMethod():contract`, + ); + + Checks.truthy( + contract.methods, + `${this.className}#isSafeToCallContractMethod():contract.methods`, + ); + + Checks.nonBlankString( + name, + `${this.className}#isSafeToCallContractMethod():name`, + ); + + const { methods } = contract; + + return Object.prototype.hasOwnProperty.call(methods, name); + } + public async invokeContract( req: InvokeContractV1Request, ): Promise { @@ -392,6 +425,16 @@ export class PluginLedgerConnectorBesu contractInstance = new this.web3.eth.Contract(abi, contractAddress); } + const isSafeToCall = await this.isSafeToCallContractMethod( + contractInstance, + req.methodName, + ); + if (!isSafeToCall) { + throw new RuntimeError( + `Invalid method name provided in request. ${req.methodName} does not exist on the Web3 contract object's "methods" property.`, + ); + } + const methodRef = contractInstance.methods[req.methodName]; Checks.truthy(methodRef, `${fnTag} YourContract.${req.methodName}`); const method: ContractSendMethod = methodRef(...req.params); @@ -945,6 +988,17 @@ export class PluginLedgerConnectorBesu } const { contractAddress } = request.invokeCall; const contractInstance = new this.web3.eth.Contract(abi, contractAddress); + + const isSafeToCall = await this.isSafeToCallContractMethod( + contractInstance, + request.invokeCall.methodName, + ); + if (!isSafeToCall) { + throw new RuntimeError( + `Invalid method name provided in request. ${request.invokeCall.methodName} does not exist on the Web3 contract object's "methods" property.`, + ); + } + const methodRef = contractInstance.methods[request.invokeCall.methodName]; Checks.truthy( methodRef, diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/plugin-ledger-connector-quorum.ts b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/plugin-ledger-connector-quorum.ts index 1740e2e8f0..72e30bacfb 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/plugin-ledger-connector-quorum.ts +++ b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/plugin-ledger-connector-quorum.ts @@ -62,6 +62,7 @@ import { GetPrometheusExporterMetricsEndpointV1, IGetPrometheusExporterMetricsEndpointV1Options, } from "./web-services/get-prometheus-exporter-metrics-endpoint-v1"; +import { RuntimeError } from "run-time-error"; export interface IPluginLedgerConnectorQuorumOptions extends ICactusPluginOptions { @@ -222,6 +223,38 @@ export class PluginLedgerConnectorQuorum return consensusHasTransactionFinality(currentConsensusAlgorithmFamily); } + /** + * Verifies that it is safe to call a specific method of a Web3 Contract. + * + * @param contract The Web3 Contract instance to check whether it has a method with a specific name or not. + * @param name The name of the method that will be checked if it's usable on `contract` or not. + * @returns Boolean `true` when it IS safe to call the method named `name` on the contract. + * @throws If the contract instance is falsy or it's methods object is falsy. Also throws if the method name is a blank string. + */ + public async isSafeToCallContractMethod( + contract: InstanceType, + name: string, + ): Promise { + Checks.truthy( + contract, + `${this.className}#isSafeToCallContractMethod():contract`, + ); + + Checks.truthy( + contract.methods, + `${this.className}#isSafeToCallContractMethod():contract.methods`, + ); + + Checks.nonBlankString( + name, + `${this.className}#isSafeToCallContractMethod():name`, + ); + + const { methods } = contract; + + return Object.prototype.hasOwnProperty.call(methods, name); + } + public async getContractInfoKeychain( req: InvokeContractV1Request, ): Promise { @@ -306,6 +339,16 @@ export class PluginLedgerConnectorQuorum contractAddress, ); + const isSafeToCall = await this.isSafeToCallContractMethod( + contractInstance, + req.methodName, + ); + if (!isSafeToCall) { + throw new RuntimeError( + `Invalid method name provided in request. ${req.methodName} does not exist on the Web3 contract object's "methods" property.`, + ); + } + const methodRef = contractInstance.methods[req.methodName]; Checks.truthy(methodRef, `${fnTag} YourContract.${req.methodName}`); diff --git a/packages/cactus-plugin-ledger-connector-xdai/src/main/typescript/plugin-ledger-connector-xdai.ts b/packages/cactus-plugin-ledger-connector-xdai/src/main/typescript/plugin-ledger-connector-xdai.ts index ab3866f83e..365496dbed 100644 --- a/packages/cactus-plugin-ledger-connector-xdai/src/main/typescript/plugin-ledger-connector-xdai.ts +++ b/packages/cactus-plugin-ledger-connector-xdai/src/main/typescript/plugin-ledger-connector-xdai.ts @@ -59,6 +59,7 @@ import { } from "./web-services/get-prometheus-exporter-metrics-endpoint-v1"; import { DeployContractSolidityBytecodeJsonObjectEndpoint } from "./web-services/deploy-contract-solidity-bytecode-json-object-endpoint"; import { InvokeContractJsonObjectEndpoint } from "./web-services/invoke-contract-json-object-endpoint"; +import { RuntimeError } from "run-time-error"; export const E_KEYCHAIN_NOT_FOUND = "cactus.connector.xdai.keychain_not_found"; @@ -227,6 +228,38 @@ export class PluginLedgerConnectorXdai return ConsensusAlgorithmFamily.Authority; } + /** + * Verifies that it is safe to call a specific method of a Web3 Contract. + * + * @param contract The Web3 Contract instance to check whether it has a method with a specific name or not. + * @param name The name of the method that will be checked if it's usable on `contract` or not. + * @returns Boolean `true` when it IS safe to call the method named `name` on the contract. + * @throws If the contract instance is falsy or it's methods object is falsy. Also throws if the method name is a blank string. + */ + public async isSafeToCallContractMethod( + contract: InstanceType, + name: string, + ): Promise { + Checks.truthy( + contract, + `${this.className}#isSafeToCallContractMethod():contract`, + ); + + Checks.truthy( + contract.methods, + `${this.className}#isSafeToCallContractMethod():contract.methods`, + ); + + Checks.nonBlankString( + name, + `${this.className}#isSafeToCallContractMethod():name`, + ); + + const { methods } = contract; + + return Object.prototype.hasOwnProperty.call(methods, name); + } + async runInvoke(req: InvokeRequestBaseV1): Promise { const fnTag = `${this.className}#runInvoke()`; @@ -248,6 +281,16 @@ export class PluginLedgerConnectorXdai contractAddress, ); + const isSafeToCall = await this.isSafeToCallContractMethod( + contractInstance, + req.methodName, + ); + if (!isSafeToCall) { + throw new RuntimeError( + `Invalid method name provided in request. ${req.methodName} does not exist on the Web3 contract object's "methods" property.`, + ); + } + const methodRef = contractInstance.methods[req.methodName]; Checks.truthy(methodRef, `${fnTag} YourContract.${req.methodName}`);