Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Add validation for types sign message primary type #350

Merged
merged 8 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/utils/common.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { stripArrayTypeIfPresent } from './common';

describe('CommonUtils', () => {
describe('stripArrayTypeIfPresent', () => {
it('remove array brackets from the type if present', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: It looks like stripArrayTypeIfPresent will ignore a case like string [], where there is a space before []. Perhaps we can add a test case for that as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it was correct before. You've now updated it to allow Type [], which is unspecified behaviour (the spec only references Type or Type[]). I didn't mean to suggest you update the regex, just that there was a test case missing

From https://eips.ethereum.org/EIPS/eip-712:

Arrays are either fixed size or dynamic and denoted by Type[n] or Type[] respectively

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the code and also changed regex to handle fixed size array like string[5].

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh nice, never thought about fixed-sized arrays, and they were right in the quote. Good catch

expect(stripArrayTypeIfPresent('string[]')).toBe('string');
expect(stripArrayTypeIfPresent('string[5]')).toBe('string');
});

it('return types which are not array without any change', () => {
expect(stripArrayTypeIfPresent('string')).toBe('string');
expect(stripArrayTypeIfPresent('string []')).toBe('string []');
});
});
});
12 changes: 12 additions & 0 deletions src/utils/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Function to stripe array brackets if string defining the type has it.
*
* @param typeString - String defining type from which array brackets are required to be removed.
* @returns Parameter string with array brackets [] removed.
*/
export const stripArrayTypeIfPresent = (typeString: string) => {
Gudahtt marked this conversation as resolved.
Show resolved Hide resolved
if (typeString?.match(/\S\[\d*\]$/u) !== null) {
return typeString.replace(/\[\d*\]$/gu, '').trim();
}
return typeString;
};
57 changes: 57 additions & 0 deletions src/wallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,63 @@ describe('wallet', () => {
'0x68dc980608bceb5f99f691e62c32caccaee05317309015e9454eba1a14c3cd4505d1dd098b8339801239c9bcaac3c4df95569dcf307108b92f68711379be14d81c',
});
});

it('should throw if message does not have types defined', async () => {
const { engine } = createTestSetup();
const getAccounts = async () => testAddresses.slice();
const witnessedMsgParams: TypedMessageParams[] = [];
const processTypedMessageV4 = async (msgParams: TypedMessageParams) => {
witnessedMsgParams.push(msgParams);
// Assume testMsgSig is the expected signature result
return testMsgSig;
};

engine.push(
createWalletMiddleware({ getAccounts, processTypedMessageV4 }),
);

const messageParams = getMsgParams();
const payload = {
method: 'eth_signTypedData_v4',
params: [
testAddresses[0],
JSON.stringify({ ...messageParams, types: undefined }),
],
};

const promise = pify(engine.handle).call(engine, payload);
await expect(promise).rejects.toThrow('Invalid input.');
});

it('should throw if type of primaryType is not defined', async () => {
const { engine } = createTestSetup();
const getAccounts = async () => testAddresses.slice();
const witnessedMsgParams: TypedMessageParams[] = [];
const processTypedMessageV4 = async (msgParams: TypedMessageParams) => {
witnessedMsgParams.push(msgParams);
// Assume testMsgSig is the expected signature result
return testMsgSig;
};

engine.push(
createWalletMiddleware({ getAccounts, processTypedMessageV4 }),
);

const messageParams = getMsgParams();
const payload = {
method: 'eth_signTypedData_v4',
params: [
testAddresses[0],
JSON.stringify({
...messageParams,
types: { ...messageParams.types, Permit: undefined },
}),
],
};

const promise = pify(engine.handle).call(engine, payload);
await expect(promise).rejects.toThrow('Invalid input.');
});
});

describe('sign', () => {
Expand Down
24 changes: 24 additions & 0 deletions src/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
} from '@metamask/utils';

import type { Block } from './types';
import { stripArrayTypeIfPresent } from './utils/common';
import { normalizeTypedMessage, parseTypedMessage } from './utils/normalize';

/*
Expand Down Expand Up @@ -243,6 +244,7 @@

const address = await validateAndNormalizeKeyholder(params[0], req);
const message = normalizeTypedMessage(params[1]);
validatePrimaryType(message);
Copy link
Member

@OGPoyraz OGPoyraz Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we not want to validate this before signing process? Like we did it in the No PrimaryType defined example in the test dapp?

If this is a malformed signature and not applying the rules of EIP-712 I think we should not even show the request in the extension, must be rejected immediately.

Copy link
Member

@OGPoyraz OGPoyraz Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example if we change TYPED_MESSAGE_SCHEMA in this package to

{
  "type": "object",
  "properties": {
    "types": {
      "type": "object",
      "additionalProperties": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "name": { "type": "string" },
            "type": { "type": "string" }
          },
          "required": ["name", "type"]
        }
      }
    },
    "primaryType": { "type": "string" },
    "domain": { "type": "object" },
    "message": { "type": "object" }
  },
  "required": ["types", "primaryType", "domain", "message"],
  "dependencies": {
    "primaryType": {
      "properties": {
        "types": {
          "type": "object",
          "required": {
            "$data": "1/primaryType"
          }
        }
      }
    }
  }
}

This schema will be validated before signature is getting added in signature-controller. (Not entirely sure with the schema - this needs to be checked)

Copy link
Contributor Author

@jpuri jpuri Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we are doing same, rejecting before request reaches extension. Rejecting in RPC middleware.

Copy link
Member

@Gudahtt Gudahtt Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, in this PR the validation is happening before the user is shown the request.

Though I do wonder whether we should be using a schema instead of manually validating each expectation we have.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am fine with both approaches, just wanted to highlight that schema validation is also happening today. On both approaches, I think it make sense to centralise them in one place, do it on the middleware functions make sense as this PR do

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree @OGPoyraz , but schema validation is currently happening in SignatureController, may be we move it here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly, there are multiple tasks in the backlog for that, we will handle that next year

validateVerifyingContract(message);
const version = 'V3';
const msgParams: TypedMessageParams = {
Expand Down Expand Up @@ -274,6 +276,7 @@

const address = await validateAndNormalizeKeyholder(params[0], req);
const message = normalizeTypedMessage(params[1]);
validatePrimaryType(message);
validateVerifyingContract(message);
const version = 'V4';
const msgParams: TypedMessageParams = {
Expand Down Expand Up @@ -426,7 +429,7 @@
*
* @param address - The address to validate and normalize.
* @param req - The request object.
* @returns {string} - The normalized address, if valid. Otherwise, throws

Check warning on line 432 in src/wallet.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

There must be no hyphen before @returns description

Check warning on line 432 in src/wallet.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

There must be no hyphen before @returns description

Check warning on line 432 in src/wallet.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (22.x)

There must be no hyphen before @returns description
* an error
*/
async function validateAndNormalizeKeyholder(
Expand Down Expand Up @@ -457,6 +460,27 @@
}
}

/**
* Validates primary of typedSignMessage, to ensure that it's type definition is present in message.
*
* @param data - The data passed in typedSign request.
*/
function validatePrimaryType(data: string) {
const { primaryType, types } = parseTypedMessage(data);
if (!types) {
throw rpcErrors.invalidInput();
}

// Primary type can be an array.
const baseType = stripArrayTypeIfPresent(primaryType);

// Return if the base type is not defined in the types
const baseTypeDefinitions = types[baseType];
if (!baseTypeDefinitions) {
throw rpcErrors.invalidInput();
}
}

/**
* Validates verifyingContract of typedSignMessage.
*
Expand Down
Loading