fix: typed data with a missing field or a bool that is not true or false is refused (closes #552)
ethers signs a bool field by whether its value is truthy, so the string "false" was shown as false and signed as true, and a bool field left out was shown as nothing and signed as false. The sign screen now refuses typed data whose message leaves out a declared field, at any level of nesting, or holds a bool that is not true or false: the error line names the field and Sign is disabled. The background refuses a signature for it too. Presence is an own-key test, so a field named like a built-in property such as constructor is not found by accident. signedPrimaryType() moves to the shared module so the background reads the message as the same type. Model: opus-5-5
This commit is contained in:
@@ -28,7 +28,7 @@ globalThis.chrome = {
|
||||
storage: { local: { get: async () => ({}), set: async () => {} } },
|
||||
};
|
||||
|
||||
const { getAddress, MaxUint256 } = require("ethers");
|
||||
const { getAddress, MaxUint256, TypedDataEncoder } = require("ethers");
|
||||
const { state } = require("../src/shared/state");
|
||||
const { decryptWithPassword } = require("../src/shared/vault");
|
||||
const approval = require("../src/popup/views/approval");
|
||||
@@ -662,3 +662,107 @@ describe("the sign screen refuses typed data for another chain", () => {
|
||||
expect(node("btn-approve-sign").disabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ethers signs a `bool` field by whether its value is truthy in JavaScript:
|
||||
// the string "false" is signed as true, and a field left out as false. The
|
||||
// screen would show one value while another is signed, so such typed data is
|
||||
// refused, and so is any declared field left out of the message
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/552). DAI's older permit
|
||||
// declares the `bool` field `allowed`.
|
||||
describe("the sign screen refuses a message ethers would sign differently", () => {
|
||||
const NOT_A_BOOL =
|
||||
'The field "allowed" is not true or false, so this typed data cannot be signed.';
|
||||
const MISSING =
|
||||
'The field "allowed" is missing from the message, so this typed data cannot be signed.';
|
||||
|
||||
function daiPermit(allowed) {
|
||||
return { ...DAI_PERMIT, message: { ...DAI_PERMIT.message, allowed } };
|
||||
}
|
||||
|
||||
const withoutAllowed = {
|
||||
...DAI_PERMIT,
|
||||
message: { ...DAI_PERMIT.message },
|
||||
};
|
||||
delete withoutAllowed.message.allowed;
|
||||
|
||||
beforeEach(() => {
|
||||
decryptWithPassword.mockClear();
|
||||
});
|
||||
|
||||
test('ethers signs "false" as true, and a bool left out as false', () => {
|
||||
const hash = (data) =>
|
||||
TypedDataEncoder.hash(
|
||||
data.domain,
|
||||
{ Permit: data.types.Permit },
|
||||
data.message,
|
||||
);
|
||||
expect(hash(daiPermit("false"))).toBe(hash(daiPermit(true)));
|
||||
expect(hash(withoutAllowed)).toBe(hash(daiPermit(false)));
|
||||
});
|
||||
|
||||
test('a bool sent as the string "false" shows the error, with Sign disabled', async () => {
|
||||
await openSignScreen(daiPermit("false"));
|
||||
expect(node("approve-sign-error").textContent).toBe(NOT_A_BOOL);
|
||||
expect(node("btn-approve-sign").disabled).toBe(true);
|
||||
});
|
||||
|
||||
test("a bool left out shows the error, with Sign disabled", async () => {
|
||||
await openSignScreen(withoutAllowed);
|
||||
expect(node("approve-sign-error").textContent).toBe(MISSING);
|
||||
expect(node("btn-approve-sign").disabled).toBe(true);
|
||||
});
|
||||
|
||||
test("Sign clicked anyway decrypts and signs nothing", async () => {
|
||||
const sent = await openSignScreen(daiPermit("false"));
|
||||
node("approve-sign-password").value = "any password";
|
||||
await click("btn-approve-sign");
|
||||
|
||||
expect(decryptWithPassword).not.toHaveBeenCalled();
|
||||
expect(sent.map((msg) => msg.type)).not.toContain(
|
||||
"AUTISTMASK_SIGN_RESPONSE",
|
||||
);
|
||||
expect(node("approve-sign-error").textContent).toBe(NOT_A_BOOL);
|
||||
});
|
||||
|
||||
test("a well-formed message leaves Sign enabled", async () => {
|
||||
for (const allowed of [true, false]) {
|
||||
await openSignScreen(daiPermit(allowed));
|
||||
expect(node("approve-sign-error").textContent).toBe("");
|
||||
expect(node("btn-approve-sign").disabled).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test("a field left out inside a list item is named by its place", () => {
|
||||
const [first, second] = PERMIT_BATCH.message.details;
|
||||
const withoutNonce = { ...second };
|
||||
delete withoutNonce.nonce;
|
||||
const data = {
|
||||
...PERMIT_BATCH,
|
||||
message: {
|
||||
...PERMIT_BATCH.message,
|
||||
details: [first, withoutNonce],
|
||||
},
|
||||
};
|
||||
expect(typedDataRefusal(request(data))).toBe(
|
||||
'The field "details[1].nonce" is missing from the message, so this typed data cannot be signed.',
|
||||
);
|
||||
});
|
||||
|
||||
// A plain lookup of a field named after a built-in object property reads
|
||||
// back a function, which ethers would sign as true.
|
||||
test("a field named after a built-in object property is missing when left out", () => {
|
||||
const data = {
|
||||
...LOGIN,
|
||||
types: {
|
||||
...LOGIN.types,
|
||||
Login: [
|
||||
...LOGIN.types.Login,
|
||||
{ name: "constructor", type: "bool" },
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(typedDataRefusal(request(data))).toBe(
|
||||
'The field "constructor" is missing from the message, so this typed data cannot be signed.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user