Skip to content

Commit

Permalink
fix(connector-quorum/ethereum): strengthen contract parameter validation
Browse files Browse the repository at this point in the history
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 hyperledger-cacti#2760

Signed-off-by: Shivam Purohit <shivampurohit900@gmail.com>
Signed-off-by: Peter Somogyvari <peter.somogyvari@accenture.com>
(cherry picked from commit 407bcf4)
  • Loading branch information
shivam-Purohit authored and petermetz committed Jan 9, 2024
1 parent 1fb2551 commit 9a3af72
Show file tree
Hide file tree
Showing 9 changed files with 208 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<string, any>;
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[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ export class InvokeRawWeb3EthContractEndpoint implements IWebServiceEndpoint {
public async handleRequest(req: Request, res: Response): Promise<void> {
const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`;
this.log.debug(reqTag);

try {
const methodResponse =
await this.options.connector.invokeRawWeb3EthContract(req.body);
Expand Down
3 changes: 3 additions & 0 deletions packages/cactus-plugin-ledger-connector-quorum/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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<string, unknown>;
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ export class InvokeRawWeb3EthContractEndpoint implements IWebServiceEndpoint {
public async handleRequest(req: Request, res: Response): Promise<void> {
const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`;
this.log.debug(reqTag);

try {
const methodResponse =
await this.options.connector.invokeRawWeb3EthContract(req.body);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
});
});
63 changes: 63 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 9a3af72

Please sign in to comment.