Skip to content

Commit 26f02da

Browse files
authored
Merge pull request #2543 from AmbireTech/fix-typed-data-block-bypassed
Fix / block AmbireOperation typed-data bypass after account switch
2 parents 6c43e78 + ab2f63a commit 26f02da

6 files changed

Lines changed: 179 additions & 16 deletions

File tree

src/controllers/requests/requests.test.ts

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -688,14 +688,15 @@ describe('RequestsController ', () => {
688688

689689
const buildSignTypedDataRequest = (
690690
controller: Awaited<ReturnType<typeof prepareTest>>['controller'],
691-
typedData: object
691+
typedData: object,
692+
signerAddress: string = FROM
692693
) =>
693694
controller.build({
694695
type: 'dappRequest',
695696
params: {
696697
request: {
697698
method: 'eth_signTypedData_v4',
698-
params: [FROM, JSON.stringify(typedData)],
699+
params: [signerAddress, JSON.stringify(typedData)],
699700
session: MOCK_SESSION
700701
},
701702
dappPromise: {
@@ -779,5 +780,65 @@ describe('RequestsController ', () => {
779780
expect(req.kind).toBe('typedMessage')
780781
expect(req.meta.params.domain.chainId).toBe(1n)
781782
})
783+
784+
const SELECTED_ACCOUNT = FROM
785+
const OTHER_ACCOUNT = '0xa07D75aacEFd11b425AF7181958F0F85c312f143'
786+
787+
const AMBIRE_OPERATION_TYPED_DATA = {
788+
types: {
789+
EIP712Domain: [
790+
{ name: 'name', type: 'string' },
791+
{ name: 'version', type: 'string' },
792+
{ name: 'chainId', type: 'uint256' },
793+
{ name: 'verifyingContract', type: 'address' },
794+
{ name: 'salt', type: 'bytes32' }
795+
],
796+
AmbireOperation: [
797+
{ name: 'account', type: 'address' },
798+
{ name: 'hash', type: 'bytes32' }
799+
]
800+
},
801+
primaryType: 'AmbireOperation',
802+
domain: {
803+
name: 'Ambire',
804+
version: '1',
805+
chainId: 1,
806+
verifyingContract: SELECTED_ACCOUNT,
807+
salt: '0x0000000000000000000000000000000000000000000000000000000000000000'
808+
},
809+
message: {
810+
account: SELECTED_ACCOUNT,
811+
hash: '0x1111111111111111111111111111111111111111111111111111111111111111'
812+
}
813+
}
814+
815+
test('rejects AmbireOperation typed data for the selected account', async () => {
816+
const { controller } = await prepareTest(true)
817+
await expect(
818+
buildSignTypedDataRequest(controller, AMBIRE_OPERATION_TYPED_DATA, SELECTED_ACCOUNT)
819+
).rejects.toThrow('Signing an AmbireOperation is not allowed')
820+
expect(controller.userRequests.length).toBe(0)
821+
})
822+
823+
test('rejects AmbireOperation typed data for a non-selected account', async () => {
824+
const { controller } = await prepareTest(true)
825+
const otherAccountTypedData = {
826+
...AMBIRE_OPERATION_TYPED_DATA,
827+
domain: {
828+
...AMBIRE_OPERATION_TYPED_DATA.domain,
829+
verifyingContract: OTHER_ACCOUNT
830+
},
831+
message: {
832+
account: OTHER_ACCOUNT,
833+
hash: '0x1111111111111111111111111111111111111111111111111111111111111111'
834+
}
835+
}
836+
837+
await expect(
838+
buildSignTypedDataRequest(controller, otherAccountTypedData, OTHER_ACCOUNT)
839+
).rejects.toThrow('Signing an AmbireOperation is not allowed')
840+
expect(controller.userRequests.length).toBe(0)
841+
expect(controller.userRequestsWaitingAccountSwitch.length).toBe(0)
842+
})
782843
})
783844
})

src/controllers/requests/requests.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ import {
6767
messageOnNewRequest
6868
} from '../../libs/requests/requests'
6969
import { parse } from '../../libs/richJson/richJson'
70+
import {
71+
AMBIRE_OPERATION_SIGNING_NOT_ALLOWED_MESSAGE,
72+
isAmbireOperationTypedData
73+
} from '../../libs/signMessage/signMessage'
7074
import { getSwapAndBridgeRequestParams } from '../../libs/swapAndBridge/swapAndBridge'
7175
import {
7276
getClaimWalletRequestParams,
@@ -396,6 +400,14 @@ export class RequestsController extends EventEmitter implements IRequestsControl
396400
for (const req of reqs) {
397401
const { kind, meta, dappPromises } = req
398402

403+
if (
404+
kind === 'typedMessage' &&
405+
isAmbireOperationTypedData((meta as TypedMessageUserRequest['meta']).params)
406+
) {
407+
this.#rejectAmbireOperationTypedDataRequest(req as TypedMessageUserRequest)
408+
continue
409+
}
410+
399411
if (allowAccountSwitch && isSignRequest(kind)) {
400412
if ((meta as SignUserRequest['meta']).accountAddr !== this.#selectedAccount.account?.addr) {
401413
await this.#addSwitchAccountUserRequest(req as SignUserRequest)
@@ -913,6 +925,15 @@ export class RequestsController extends EventEmitter implements IRequestsControl
913925

914926
requestsToAddOrRemove.forEach((r) => {
915927
this.userRequestsWaitingAccountSwitch.splice(this.userRequests.indexOf(r), 1)
928+
929+
if (
930+
r.kind === 'typedMessage' &&
931+
isAmbireOperationTypedData((r as TypedMessageUserRequest).meta.params)
932+
) {
933+
this.#rejectAmbireOperationTypedDataRequest(r as TypedMessageUserRequest)
934+
return
935+
}
936+
916937
userRequestsToAdd.push(r)
917938
})
918939
}
@@ -1303,11 +1324,8 @@ export class RequestsController extends EventEmitter implements IRequestsControl
13031324
throw ethErrors.rpc.invalidParams('The message contents did not match the provided types.')
13041325
}
13051326

1306-
if (
1307-
msgAddress === this.#selectedAccount.account.addr &&
1308-
(typedData.primaryType === 'AmbireOperation' || !!typedData.types.AmbireOperation)
1309-
) {
1310-
throw ethErrors.rpc.methodNotSupported('Signing an AmbireOperation is not allowed')
1327+
if (isAmbireOperationTypedData(typedData)) {
1328+
throw ethErrors.rpc.methodNotSupported(AMBIRE_OPERATION_SIGNING_NOT_ALLOWED_MESSAGE)
13111329
}
13121330

13131331
userRequest = {
@@ -1716,7 +1734,18 @@ export class RequestsController extends EventEmitter implements IRequestsControl
17161734
if (userRequest) await this.addUserRequests([userRequest])
17171735
}
17181736

1737+
#rejectAmbireOperationTypedDataRequest(req: TypedMessageUserRequest) {
1738+
req.dappPromises.forEach((p) => {
1739+
p.reject(ethErrors.rpc.methodNotSupported(AMBIRE_OPERATION_SIGNING_NOT_ALLOWED_MESSAGE))
1740+
})
1741+
}
1742+
17191743
async #addSwitchAccountUserRequest(req: SignUserRequest) {
1744+
if (req.kind === 'typedMessage' && isAmbireOperationTypedData(req.meta.params)) {
1745+
this.#rejectAmbireOperationTypedDataRequest(req)
1746+
return
1747+
}
1748+
17201749
this.userRequestsWaitingAccountSwitch.push(req)
17211750
await this.addUserRequests(
17221751
[

src/controllers/signAccountOp/signAccountOp.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3189,7 +3189,10 @@ export class SignAccountOpController
31893189
this.account,
31903190
accountState,
31913191
signer,
3192-
this.#network
3192+
this.#network,
3193+
false,
3194+
undefined,
3195+
true
31933196
)
31943197
)
31953198
if (!this.accountOp.meta) {

src/controllers/signMessage/signMessage.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,15 @@ import {
4040
getSigningRequestDisplayData
4141
} from '../../libs/signingRequest/signingRequest'
4242
import {
43+
AMBIRE_OPERATION_SIGNING_NOT_ALLOWED_MESSAGE,
4344
getAppFormatted,
4445
getEIP712Hash,
4546
getEIP712Signature,
4647
getPlainTextSignature,
4748
getSafeMessageTypedData,
4849
getVerifyMessageSignature,
49-
verifyMessage
50+
verifyMessage,
51+
isAmbireOperationTypedData
5052
} from '../../libs/signMessage/signMessage'
5153
import hexStringToUint8Array from '../../utils/hexStringToUint8Array'
5254
import { SignedMessage } from '../activity/types'
@@ -560,6 +562,10 @@ export class SignMessageController
560562
}
561563

562564
if (this.messageToSign.content.kind === 'typedMessage') {
565+
if (isAmbireOperationTypedData(this.messageToSign.content)) {
566+
throw new Error(AMBIRE_OPERATION_SIGNING_NOT_ALLOWED_MESSAGE)
567+
}
568+
563569
if (this.#account.creation && this.messageToSign.content.primaryType === 'Permit') {
564570
throw new Error(
565571
'It looks like that this app doesn\'t detect Smart Account wallets, and requested incompatible approval type. Please, go back to the app and change the approval type to "Transaction", which is supported by Smart Account wallets.'

src/libs/signMessage/signMessage.test.ts

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,8 @@ describe('Sign Message, Keystore with key dedicatedToOneSA: true ', () => {
359359
accountState,
360360
signer,
361361
polygonNetwork,
362+
true,
363+
undefined,
362364
true
363365
)
364366

@@ -658,7 +660,10 @@ describe('Sign Message, Keystore with key dedicatedToOneSA: true ', () => {
658660
smartAccount,
659661
accountState,
660662
signer,
661-
polygonNetwork
663+
polygonNetwork,
664+
false,
665+
undefined,
666+
true
662667
)
663668
// the key should be dedicatedToOneSA, so we expect the signature to end in 00
664669
expect(eip712Sig.signature.slice(-2)).toEqual('00')
@@ -697,7 +702,10 @@ describe('Sign Message, Keystore with key dedicatedToOneSA: true ', () => {
697702
v1Account,
698703
accountState,
699704
signer,
700-
ethereumNetwork
705+
ethereumNetwork,
706+
false,
707+
undefined,
708+
true
701709
)
702710
// the key is for a v1 acc so it should be 00
703711
expect(eip712Sig.signature.slice(-2)).toEqual('00')
@@ -752,7 +760,10 @@ describe('Sign Message, Keystore with key dedicatedToOneSA: true ', () => {
752760
v1Account,
753761
accountState,
754762
signer,
755-
ethereumNetwork
763+
ethereumNetwork,
764+
false,
765+
undefined,
766+
true
756767
)
757768
// the key is for a v1 acc so it should be 00
758769
expect(eip712Sig.signature.slice(-2)).toEqual('00')
@@ -777,7 +788,16 @@ describe('Sign Message, Keystore with key dedicatedToOneSA: true ', () => {
777788
hashMessage('test')
778789
)
779790
try {
780-
await getEIP712Signature(typedData, v1Account, accountState, signer, polygonNetwork)
791+
await getEIP712Signature(
792+
typedData,
793+
v1Account,
794+
accountState,
795+
signer,
796+
polygonNetwork,
797+
false,
798+
undefined,
799+
true
800+
)
781801
console.log('No error was thrown for [V1 SA]: eip-712, but it should have')
782802
expect(true).toEqual(false)
783803
} catch (e: any) {
@@ -855,7 +875,10 @@ describe('Sign Message, Keystore with key dedicatedToOneSA: true ', () => {
855875
smartAccount,
856876
v2AccountState,
857877
signer,
858-
polygonNetwork
878+
polygonNetwork,
879+
false,
880+
undefined,
881+
true
859882
)
860883
expect(eip712Sig.signature.slice(-2)).toEqual('00')
861884

@@ -1056,7 +1079,16 @@ describe('Sign Message, Keystore with key dedicatedToOneSA: false', () => {
10561079
hashMessage('test')
10571080
)
10581081
try {
1059-
await getEIP712Signature(typedData, smartAccount, accountState, signer, polygonNetwork)
1082+
await getEIP712Signature(
1083+
typedData,
1084+
smartAccount,
1085+
accountState,
1086+
signer,
1087+
polygonNetwork,
1088+
false,
1089+
undefined,
1090+
true
1091+
)
10601092
console.log('No error was thrown for [Not dedicated to one SA]: eip-712, but it should have')
10611093
expect(true).toEqual(false)
10621094
} catch (e: any) {
@@ -1065,6 +1097,21 @@ describe('Sign Message, Keystore with key dedicatedToOneSA: false', () => {
10651097
)
10661098
}
10671099
})
1100+
test('Signing [V2 SA]: rejects AmbireOperation typed data from untrusted sources by default', async () => {
1101+
const accountStates = await getAccountsInfo([smartAccount])
1102+
const accountState = accountStates[smartAccount.addr]![polygonNetwork.chainId.toString()]!
1103+
const signer = await keystore.getSigner(eoaSigner.keyPublicAddress, 'internal')
1104+
1105+
const typedData = getTypedData(
1106+
polygonNetwork.chainId,
1107+
accountState.accountAddr,
1108+
hashMessage('test')
1109+
)
1110+
1111+
await expect(
1112+
getEIP712Signature(typedData, smartAccount, accountState, signer, polygonNetwork)
1113+
).rejects.toThrow('Signing an AmbireOperation is not allowed')
1114+
})
10681115
})
10691116

10701117
describe('Sign Message, Safe accounts', () => {

src/libs/signMessage/signMessage.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,18 @@ export const EIP_1271_NOT_SUPPORTED_BY = [
6363
'bitrefill.com'
6464
]
6565

66+
export const AMBIRE_OPERATION_SIGNING_NOT_ALLOWED_MESSAGE =
67+
'Signing an AmbireOperation is not allowed'
68+
69+
export const isAmbireOperationTypedData = (typedData: {
70+
primaryType: string
71+
types: Record<string, unknown>
72+
}) => {
73+
if ('AmbireReadableOperation' in typedData.types) return false
74+
75+
return typedData.primaryType === 'AmbireOperation' || 'AmbireOperation' in typedData.types
76+
}
77+
6678
/**
6779
* For Unprotected signatures, we need to append 00 at the end
6880
* for ambire to recognize it
@@ -607,7 +619,8 @@ export async function getEIP712Signature(
607619
signer: KeystoreSignerInterface,
608620
network: Network,
609621
isOG = false,
610-
withHardwareWalletSigningRequest?: WithHardwareWalletSigningRequest
622+
withHardwareWalletSigningRequest?: WithHardwareWalletSigningRequest,
623+
allowAmbireOperation = false
611624
): Promise<{ signature: Hex; hash?: Hex }> {
612625
if (!message.types.EIP712Domain) {
613626
throw new Error(
@@ -642,6 +655,10 @@ export async function getEIP712Signature(
642655
)) as Hex
643656
}
644657

658+
if (isAmbireOperationTypedData(message) && !allowAmbireOperation) {
659+
throw new Error(AMBIRE_OPERATION_SIGNING_NOT_ALLOWED_MESSAGE)
660+
}
661+
645662
if (!accountState.isV2) {
646663
if (
647664
// @NOTE: isOG is to allow tem members to sign anything with v1 accounts regardless of safety and security

0 commit comments

Comments
 (0)