From 9a3af72f80bbffb93fe3dc055f23e10ff3c77585 Mon Sep 17 00:00:00 2001 From: Shivam Purohit Date: Tue, 7 Nov 2023 11:58:12 +0530 Subject: [PATCH] fix(connector-quorum/ethereum): strengthen contract parameter validation Peter's updates: 1. Made improvements to the test case verifying that the parameters with incorrect types are indeed being rejected with useful error messaging 2. Added a new library (which I also had to re-publish with CJS exports) Fixes #2760 Signed-off-by: Shivam Purohit Signed-off-by: Peter Somogyvari (cherry picked from commit 407bcf4fd0b0bbb2b68b1c8d66275a9dae3317e7) --- .../package.json | 1 + .../plugin-ledger-connector-ethereum.ts | 49 ++++++++++++ ...invoke-raw-web3eth-contract-v1-endpoint.ts | 1 - .../package.json | 3 + .../plugin-ledger-connector-quorum.ts | 74 ++++++++++++++++--- ...invoke-raw-web3eth-contract-v1-endpoint.ts | 1 - .../v21.4.1-invoke-contract.test.ts | 13 ++-- .../v21.4.1-invoke-web3-contract-v1.test.ts | 19 +++++ yarn.lock | 63 ++++++++++++++++ 9 files changed, 208 insertions(+), 16 deletions(-) diff --git a/packages/cactus-plugin-ledger-connector-ethereum/package.json b/packages/cactus-plugin-ledger-connector-ethereum/package.json index 37450f88bbd..bc40e7168d9 100644 --- a/packages/cactus-plugin-ledger-connector-ethereum/package.json +++ b/packages/cactus-plugin-ledger-connector-ethereum/package.json @@ -96,6 +96,7 @@ "@types/uuid": "9.0.6", "body-parser": "1.20.2", "chalk": "4.1.2", + "ethers": "6.8.1", "js-yaml": "4.1.0", "socket.io": "4.5.4", "uuid": "9.0.1", diff --git a/packages/cactus-plugin-ledger-connector-ethereum/src/main/typescript/plugin-ledger-connector-ethereum.ts b/packages/cactus-plugin-ledger-connector-ethereum/src/main/typescript/plugin-ledger-connector-ethereum.ts index 75ff90d91bf..f44ae5d1951 100644 --- a/packages/cactus-plugin-ledger-connector-ethereum/src/main/typescript/plugin-ledger-connector-ethereum.ts +++ b/packages/cactus-plugin-ledger-connector-ethereum/src/main/typescript/plugin-ledger-connector-ethereum.ts @@ -16,6 +16,8 @@ import { PayableMethodObject } from "web3-eth-contract"; import OAS from "../json/openapi.json"; +import { Interface, FunctionFragment, isAddress } from "ethers"; + import { ConsensusAlgorithmFamily, IPluginLedgerConnector, @@ -1135,6 +1137,53 @@ export class PluginLedgerConnectorEthereum `Invalid method name provided in request. ${args.contractMethod} does not exist on the Web3 contract object's "methods" property.`, ); } + const abiInterface = new Interface(args.abi); + const methodFragment: FunctionFragment | null = abiInterface.getFunction( + args.contractMethod, + ); + if (!methodFragment) { + throw new RuntimeError( + `Method ${args.contractMethod} not found in ABI interface.`, + ); + } + + // validation for the contractMethod + if (methodFragment.inputs.length !== contractMethodArgs.length) { + throw new Error( + `Incorrect number of arguments for ${args.contractMethod}`, + ); + } + methodFragment.inputs.forEach((input, index) => { + const argValue = contractMethodArgs[index]; + const isValidType = typeof argValue === input.type; + + if (!isValidType) { + throw new Error( + `Invalid type for argument ${index + 1} in ${args.contractMethod}`, + ); + } + }); + + //validation for the invocationParams + const invocationParams = args.invocationParams as Record; + const allowedKeys = ["from", "gasLimit", "gasPrice", "value"]; + + if (invocationParams) { + Object.keys(invocationParams).forEach((key) => { + if (!allowedKeys.includes(key)) { + throw new Error(`Invalid key '${key}' in invocationParams`); + } + if (key === "from" && !isAddress(invocationParams[key])) { + throw new Error(`Invalid type for 'from' in invocationParams`); + } + if (key === "gasLimit" && typeof invocationParams[key] !== "number") { + throw new Error(`Invalid type for '${key}' in invocationParams`); + } + if (key === "gasPrice" && typeof invocationParams[key] !== "number") { + throw new Error(`Invalid type for '${key}'in invocationParams`); + } + }); + } const methodRef = contract.methods[args.contractMethod] as ( ...args: unknown[] diff --git a/packages/cactus-plugin-ledger-connector-ethereum/src/main/typescript/web-services/invoke-raw-web3eth-contract-v1-endpoint.ts b/packages/cactus-plugin-ledger-connector-ethereum/src/main/typescript/web-services/invoke-raw-web3eth-contract-v1-endpoint.ts index 11f5225f97e..040119f4d6e 100644 --- a/packages/cactus-plugin-ledger-connector-ethereum/src/main/typescript/web-services/invoke-raw-web3eth-contract-v1-endpoint.ts +++ b/packages/cactus-plugin-ledger-connector-ethereum/src/main/typescript/web-services/invoke-raw-web3eth-contract-v1-endpoint.ts @@ -87,7 +87,6 @@ export class InvokeRawWeb3EthContractEndpoint implements IWebServiceEndpoint { public async handleRequest(req: Request, res: Response): Promise { const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; this.log.debug(reqTag); - try { const methodResponse = await this.options.connector.invokeRawWeb3EthContract(req.body); diff --git a/packages/cactus-plugin-ledger-connector-quorum/package.json b/packages/cactus-plugin-ledger-connector-quorum/package.json index add11121de7..e6776a74080 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/package.json +++ b/packages/cactus-plugin-ledger-connector-quorum/package.json @@ -62,6 +62,7 @@ "@hyperledger/cactus-core-api": "2.0.0-alpha.2", "axios": "1.6.0", "express": "4.18.2", + "http-errors-enhanced-cjs": "2.0.0", "minimist": "1.2.8", "prom-client": "13.2.0", "run-time-error-cjs": "1.4.0", @@ -80,11 +81,13 @@ "@hyperledger/cactus-test-tooling": "2.0.0-alpha.2", "@types/body-parser": "1.19.4", "@types/express": "4.17.19", + "@types/http-errors": "2.0.4", "@types/minimist": "1.2.2", "@types/sanitize-html": "2.6.2", "@types/uuid": "9.0.6", "body-parser": "1.20.2", "chalk": "4.1.2", + "ethers": "6.8.1", "socket.io": "4.5.4", "uuid": "9.0.1", "web3-core": "1.6.1", 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 aa2bad3b4c2..bfcc3bde5c0 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 @@ -21,8 +21,12 @@ import { Contract } from "web3-eth-contract"; import { ContractSendMethod } from "web3-eth-contract"; import { TransactionReceipt } from "web3-eth"; +import { BadRequestError } from "http-errors-enhanced-cjs"; + import OAS from "../json/openapi.json"; +import { Interface, FunctionFragment, isAddress } from "ethers"; + import { ConsensusAlgorithmFamily, IPluginLedgerConnector, @@ -884,9 +888,8 @@ export class PluginLedgerConnectorQuorum args.invocationType, ) ) { - throw new Error( - `Unknown invocationType (${args.invocationType}), must be specified in EthContractInvocationWeb3Method`, - ); + const eMsg = `Unknown invocationType (${args.invocationType}), must be specified in EthContractInvocationWeb3Method`; + throw new BadRequestError(eMsg); } const contract = new this.web3.eth.Contract( @@ -899,13 +902,66 @@ export class PluginLedgerConnectorQuorum args.contractMethod, ); if (!isSafeToCall) { - throw new RuntimeError( - `Invalid method name provided in request. ${args.contractMethod} does not exist on the Web3 contract object's "methods" property.`, - ); + const msg = `Invalid method name provided in request. ${args.contractMethod} does not exist on the Web3 contract object's "methods" property.`; + throw new BadRequestError(msg); + } + const abiInterface = new Interface(args.abi); + const methodFragment: FunctionFragment | null = abiInterface.getFunction( + args.contractMethod, + ); + if (!methodFragment) { + const msg = `Method ${args.contractMethod} not found in ABI interface.`; + throw new BadRequestError(msg); + } + + // validation for the contractMethod + if (methodFragment.inputs.length !== contractMethodArgs.length) { + const msg = `Incorrect number of arguments for ${args.contractMethod}`; + throw new BadRequestError(msg); + } + methodFragment.inputs.forEach((input, index) => { + const argValue = contractMethodArgs[index]; + const isValidType = typeof argValue === input.type; + + if (!isValidType) { + const msg = `Invalid type for argument ${index + 1} in ${ + args.contractMethod + }`; + throw new BadRequestError(msg); + } + }); + + //validation for the invocationParams + const invocationParams = args.invocationParams as Record; + const allowedKeys = ["from", "gasLimit", "gasPrice", "value"]; + + if (invocationParams) { + Object.keys(invocationParams).forEach((key) => { + if (!allowedKeys.includes(key)) { + throw new BadRequestError(`Invalid key '${key}' in invocationParams`); + } + if (key === "from" && !isAddress(invocationParams[key])) { + throw new BadRequestError( + `Invalid type for 'from' in invocationParams`, + ); + } + if (key === "gasLimit" && typeof invocationParams[key] !== "number") { + throw new BadRequestError( + `Invalid type for '${key}' in invocationParams`, + ); + } + if (key === "gasPrice" && typeof invocationParams[key] !== "number") { + throw new BadRequestError( + `Invalid type for '${key}'in invocationParams`, + ); + } + }); } - return contract.methods[args.contractMethod](...contractMethodArgs)[ - args.invocationType - ](args.invocationParams); + const txObjectFactory = contract.methods[args.contractMethod]; + const txObject = txObjectFactory(...contractMethodArgs); + const executor = txObject[args.invocationType]; + const output = await executor(args.invocationParams); + return output; } } diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/invoke-raw-web3eth-contract-v1-endpoint.ts b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/invoke-raw-web3eth-contract-v1-endpoint.ts index bbd3d571e02..7e3800b3bc5 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/invoke-raw-web3eth-contract-v1-endpoint.ts +++ b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/invoke-raw-web3eth-contract-v1-endpoint.ts @@ -85,7 +85,6 @@ export class InvokeRawWeb3EthContractEndpoint implements IWebServiceEndpoint { public async handleRequest(req: Request, res: Response): Promise { const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; this.log.debug(reqTag); - try { const methodResponse = await this.options.connector.invokeRawWeb3EthContract(req.body); diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-invoke-contract.test.ts b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-invoke-contract.test.ts index aca4f6bbc2c..176e5626eaa 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-invoke-contract.test.ts +++ b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-invoke-contract.test.ts @@ -27,17 +27,20 @@ const contractName = "HelloWorld"; const testCase = "Quorum Ledger Connector Plugin"; describe(testCase, () => { + const containerImageVersion = "2021-05-03-quorum-v21.4.1"; + const ledgerOptions = { containerImageVersion }; + const ledger = new QuorumTestLedger(ledgerOptions); + afterAll(async () => { await ledger.stop(); await ledger.destroy(); }); - const containerImageVersion = "2021-05-03-quorum-v21.4.1"; - const ledgerOptions = { containerImageVersion }; - const ledger = new QuorumTestLedger(ledgerOptions); - - test(testCase, async () => { + beforeAll(async () => { await ledger.start(); + }); + + test("can invoke contract methods", async () => { const rpcApiHttpHost = await ledger.getRpcApiHttpHost(); const quorumGenesisOptions: IQuorumGenesisOptions = await ledger.getGenesisJsObject(); diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-invoke-web3-contract-v1.test.ts b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-invoke-web3-contract-v1.test.ts index 7ef51b96e3c..5a6cdf720ba 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-invoke-web3-contract-v1.test.ts +++ b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-invoke-web3-contract-v1.test.ts @@ -110,6 +110,7 @@ describe("invokeRawWeb3EthContract Tests", () => { }, gas: 1000000, }); + log.debug("Contract deployed OK"); expect(deployOut).toBeTruthy(); expect(deployOut.transactionReceipt).toBeTruthy(); expect(deployOut.transactionReceipt.contractAddress).toBeTruthy(); @@ -199,4 +200,22 @@ describe("invokeRawWeb3EthContract Tests", () => { await expect(connector.invokeRawWeb3EthContract(callInvokeArgs)).toReject(); }); + + it("validates input parameters based on their solidity type declarations", async () => { + const req: InvokeRawWeb3EthContractV1Request = { + abi: contractAbi, + address: contractAddress, + invocationType: EthContractInvocationWeb3Method.Call, + contractMethod: "setName", + contractMethodArgs: [42], + invocationParams: { + gasLimit: 999999, + }, + }; + + const eMsgExpected = `Invalid type for argument ${0 + 1} in ${"setName"}`; + + const theInvocation = connector.invokeRawWeb3EthContract(req); + await expect(theInvocation).rejects.toThrowWithMessage(Error, eMsgExpected); + }); }); diff --git a/yarn.lock b/yarn.lock index 0f17b6cc475..e2c418180c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19,6 +19,13 @@ __metadata: languageName: node linkType: hard +"@adraffy/ens-normalize@npm:1.10.0": + version: 1.10.0 + resolution: "@adraffy/ens-normalize@npm:1.10.0" + checksum: af0540f963a2632da2bbc37e36ea6593dcfc607b937857133791781e246d47f870d5e3d21fa70d5cfe94e772c284588c81ea3f5b7f4ea8fbb824369444e4dbcb + languageName: node + linkType: hard + "@adraffy/ens-normalize@npm:1.9.0": version: 1.9.0 resolution: "@adraffy/ens-normalize@npm:1.9.0" @@ -7574,6 +7581,7 @@ __metadata: axios: 1.6.0 body-parser: 1.20.2 chalk: 4.1.2 + ethers: 6.8.1 express: 4.18.2 http-proxy-middleware: 2.0.6 js-yaml: 4.1.0 @@ -7765,13 +7773,16 @@ __metadata: "@hyperledger/cactus-test-tooling": 2.0.0-alpha.2 "@types/body-parser": 1.19.4 "@types/express": 4.17.19 + "@types/http-errors": 2.0.4 "@types/minimist": 1.2.2 "@types/sanitize-html": 2.6.2 "@types/uuid": 9.0.6 axios: 1.6.0 body-parser: 1.20.2 chalk: 4.1.2 + ethers: 6.8.1 express: 4.18.2 + http-errors-enhanced-cjs: 2.0.0 minimist: 1.2.8 prom-client: 13.2.0 run-time-error-cjs: 1.4.0 @@ -10119,6 +10130,15 @@ __metadata: languageName: node linkType: hard +"@noble/curves@npm:1.2.0": + version: 1.2.0 + resolution: "@noble/curves@npm:1.2.0" + dependencies: + "@noble/hashes": 1.3.2 + checksum: bb798d7a66d8e43789e93bc3c2ddff91a1e19fdb79a99b86cd98f1e5eff0ee2024a2672902c2576ef3577b6f282f3b5c778bebd55761ddbb30e36bf275e83dd0 + languageName: node + linkType: hard + "@noble/ed25519@npm:^1.6.0": version: 1.7.3 resolution: "@noble/ed25519@npm:1.7.3" @@ -10147,6 +10167,13 @@ __metadata: languageName: node linkType: hard +"@noble/hashes@npm:1.3.2": + version: 1.3.2 + resolution: "@noble/hashes@npm:1.3.2" + checksum: fe23536b436539d13f90e4b9be843cc63b1b17666a07634a2b1259dded6f490be3d050249e6af98076ea8f2ea0d56f578773c2197f2aa0eeaa5fba5bc18ba474 + languageName: node + linkType: hard + "@noble/secp256k1@npm:1.7.1, @noble/secp256k1@npm:^1.5.4, @noble/secp256k1@npm:~1.7.0": version: 1.7.1 resolution: "@noble/secp256k1@npm:1.7.1" @@ -13374,6 +13401,13 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:18.15.13": + version: 18.15.13 + resolution: "@types/node@npm:18.15.13" + checksum: 79cc5a2b5f98e8973061a4260a781425efd39161a0e117a69cd089603964816c1a14025e1387b4590c8e82d05133b7b4154fa53a7dffb3877890a66145e76515 + languageName: node + linkType: hard + "@types/node@npm:20.4.7": version: 20.4.7 resolution: "@types/node@npm:20.4.7" @@ -15108,6 +15142,13 @@ __metadata: languageName: node linkType: hard +"aes-js@npm:4.0.0-beta.5": + version: 4.0.0-beta.5 + resolution: "aes-js@npm:4.0.0-beta.5" + checksum: cc2ea969d77df939c32057f7e361b6530aa6cb93cb10617a17a45cd164e6d761002f031ff6330af3e67e58b1f0a3a8fd0b63a720afd591a653b02f649470e15b + languageName: node + linkType: hard + "agent-base@npm:6, agent-base@npm:^6.0.2": version: 6.0.2 resolution: "agent-base@npm:6.0.2" @@ -24560,6 +24601,21 @@ __metadata: languageName: node linkType: hard +"ethers@npm:6.8.1": + version: 6.8.1 + resolution: "ethers@npm:6.8.1" + dependencies: + "@adraffy/ens-normalize": 1.10.0 + "@noble/curves": 1.2.0 + "@noble/hashes": 1.3.2 + "@types/node": 18.15.13 + aes-js: 4.0.0-beta.5 + tslib: 2.4.0 + ws: 8.5.0 + checksum: 78b48d2fd13cf77c7041379595a3155eaf6e4da423999b782e6ff14cc58e452ef8c52f3e2f6995fcd2b34dfdd55eaf5ece6067cd754fe0c72388b12e1768f1b3 + languageName: node + linkType: hard + "ethers@npm:^4.0.32, ethers@npm:^4.0.40": version: 4.0.49 resolution: "ethers@npm:4.0.49" @@ -28216,6 +28272,13 @@ __metadata: languageName: node linkType: hard +"http-errors-enhanced-cjs@npm:2.0.0": + version: 2.0.0 + resolution: "http-errors-enhanced-cjs@npm:2.0.0" + checksum: 460b8279e00c3f4d653fdb35d8cd3d6a0235915ae7e8fdd35a3b1ea8a233a0333d6b7c98a46ddde10c31be1a2a59a99aff4d44031695651de4f315cb0fcf0861 + languageName: node + linkType: hard + "http-errors@npm:1.6.3, http-errors@npm:~1.6.2": version: 1.6.3 resolution: "http-errors@npm:1.6.3"