китаев.tech

Phishing через eth_sign

Почему eth_sign опасен для пользователей, как фишинговый сайт маскирует подпись и как заменить её на EIP-712 typed data.

Почему eth_sign опасен

eth_sign просит кошелёк подписать произвольные байты. Пользователь часто не видит понятный текст и не понимает, что именно подтверждает.

Фишинговый сайт может показать в интерфейсе "Подпишите вход", а в кошелёк отправить данные, которые другой контракт или backend примет как разрешение на действие.


Показываем плохой и безопасный запрос

Создайте файл scripts/phishing-eth-sign.ts:

typescript
import { ethers } from "ethers";

const wallet = ethers.Wallet.createRandom();

const dangerousPayload = ethers.solidityPackedKeccak256(
  ["address", "address", "uint256"],
  [
    wallet.address,
    "0x000000000000000000000000000000000000dEaD",
    ethers.parseEther("10"),
  ],
);

const ethSignSignature = await wallet.signingKey.sign(dangerousPayload).serialized;

console.log("Адрес пользователя:", wallet.address);
console.log("Сырые данные для подписи:", dangerousPayload);
console.log("Подпись как в eth_sign:", ethSignSignature);

const recoveredFromRawHash = ethers.recoverAddress(dangerousPayload, ethSignSignature);

console.log("Восстановленный адрес из raw hash:", recoveredFromRawHash);

const domain = {
  name: "Kitaev Docs Demo",
  version: "1",
  chainId: 1,
  verifyingContract: "0x0000000000000000000000000000000000000001",
};

const types = {
  Login: [
    { name: "purpose", type: "string" },
    { name: "wallet", type: "address" },
    { name: "nonce", type: "uint256" },
  ],
};

const value = {
  purpose: "Вход на сайт kitaev.tech",
  wallet: wallet.address,
  nonce: 1n,
};

const typedDataSignature = await wallet.signTypedData(domain, types, value);

const recoveredFromTypedData = ethers.verifyTypedData(
  domain,
  types,
  value,
  typedDataSignature,
);

console.log("Подпись EIP-712:", typedDataSignature);
console.log("Восстановленный адрес из EIP-712:", recoveredFromTypedData);

Запуск:

bash
npx tsx scripts/phishing-eth-sign.ts

В первом случае пользователь подписывает непонятный hash. Во втором кошелёк может показать домен, тип сообщения и поля: purpose, wallet, nonce.


Как выглядит проверка на backend

Создайте файл scripts/verify-login.ts:

typescript
import { ethers } from "ethers";

const wallet = ethers.Wallet.createRandom();
const nonce = 42n;

const domain = {
  name: "Kitaev Docs Demo",
  version: "1",
  chainId: 1,
  verifyingContract: "0x0000000000000000000000000000000000000001",
};

const types = {
  Login: [
    { name: "purpose", type: "string" },
    { name: "wallet", type: "address" },
    { name: "nonce", type: "uint256" },
  ],
};

const message = {
  purpose: "Вход на сайт kitaev.tech",
  wallet: wallet.address,
  nonce,
};

const signature = await wallet.signTypedData(domain, types, message);

const recoveredAddress = ethers.verifyTypedData(domain, types, message, signature);

if (recoveredAddress.toLowerCase() !== wallet.address.toLowerCase()) {
  throw new Error("Подпись не принадлежит этому адресу");
}

console.log("Подпись валидна для:", recoveredAddress);

Здесь backend проверяет не просто подпись, а конкретные структурированные данные. Если злоумышленник поменяет purpose, wallet, nonce, chainId или verifyingContract, восстановленный адрес уже не будет подходить к ожидаемому сообщению.


Как это работает

eth_sign — низкоуровневый метод подписи raw bytes. В современных кошельках он часто отключён или помечен как опасный.

wallet.signingKey.sign() в примере — имитация raw-подписи. Она показывает суть eth_sign: подпись привязана к hash, но не объясняет пользователю смысл данных.

signTypedData() — подпись EIP-712. Она добавляет домен приложения и структуру сообщения, поэтому кошелёк может показать читаемые поля.

verifyTypedData() — восстанавливает адрес подписанта из typed data и подписи. Backend сравнивает его с адресом пользователя и проверяет nonce.


Что использовать вместо eth_sign

Для входа на сайт используйте EIP-4361 Sign-In with Ethereum или EIP-712 typed data с одноразовым nonce.

Для подтверждения действия показывайте человеку конкретные поля: контракт, сеть, сумму, срок действия и получателя.

Для транзакций не просите подпись сообщения вместо обычной транзакции. Если действие меняет state, пользователь должен видеть transaction request в кошельке.


Частые ошибки

Просить eth_sign для логина → используйте SIWE или EIP-712 с nonce, доменом и сроком действия.

Подписывать hash без контекста → пользователь не может проверить, что именно он подтверждает.

Не проверять nonce на backend → старую подпись можно переиспользовать для повторного входа.


Что дальше

  • Подпись сообщений EIP-191 — как работает человекочитаемая подпись обычного сообщения
  • EIP-712 typed data — как подписывать структурированные данные для кошельков и backend
  • MetaMask Browser Provider — как безопасно запрашивать аккаунт и подпись в браузере

Материалы китаev.tech публикуются в образовательных целях и не являются инвестиционной рекомендацией. Примеры кода и описания протоколов — для обучения; использование в продакшне на ваш собственный риск. Дисклеймер и политика конфиденциальности.