dAppとウォレットの「信頼」をハックする:セッション管理とCSRFの残酷な現実
やあ。現場で泥をすすりながらインシデント対応をしてきた経験から言わせてもらうと、Web3界隈のエンジニアは「ブロックチェーン自体は改ざん不可能だから安全」という妄信に陥りすぎている。
だが、現実はどうか? dAppのフロントエンドや、ウォレットを繋ぐセッション管理の甘さを突かれ、ユーザーの資産が根こそぎ抜かれる事件が後を絶たない。特に「ウォレット接続」というプロセスは、認証の境界線が曖昧になりがちで、攻撃者にとっては格好の食い物だ。
今日は、dApp開発における「セッション管理の盲点」と、誰もがやりがちな「CSRF対策の不備」について、現場の視点で切り込む。
—
1. 攻撃者の視点:なぜ「セッション維持」が狙われるのか
dAppにおいて、ウォレットアドレスはIDそのものだ。しかし、Webアプリケーションとしてセッションを維持しようとする際、多くのエンジニアが「JWT(JSON Web Token)をフロントエンドのローカルストレージに放置する」という致命的なミスを犯す。
もし、攻撃者がXSS(クロスサイトスクリプティング)を仕込んだ場合、そのトークンは簡単に盗まれる。さらに深刻なのは、「Origin検証の欠如」だ。
悪意あるPoCのシナリオ
攻撃者は、ユーザーがログインしているdAppと同一ドメイン、あるいは信頼されているサブドメインに細工したページを配置する。ユーザーがそのページを開くと、攻撃者が用意したスクリプトが、バックエンドの認証APIに対して「勝手に」リクエストを投げる。
ここで、SameSite属性やOriginチェックが適切でない場合、バックエンドは「ユーザーが意図した操作」だと誤認してトランザクションや署名を許可してしまう。これがWeb3におけるCSRFの正体だ。
—
2. 防御の鉄則:ステートレスと厳格なOrigin検証
「セッション管理をJWTでやるな」とは言わない。だが、「署名による検証(EIP-4361: Sign-In with Ethereum)」を徹底しろ。
単にトークンを渡すのではなく、バックエンドで生成した「Nonce(使い捨ての乱数)」にユーザーが署名することで、初めてセッションを確立する。これを行わない限り、あなたのdAppは「誰でもなりすまし放題のザル」だ。
実装サンプル:Node.js (Express) でのセッション検証
以下は、Nonceベースの認証ロジックの一部だ。これをベースに、JWTをHttpOnlyかつSecureなCookieで管理するのが正攻法だ。
// セッションのNonceを生成するエンドポイント
app.post('/api/auth/nonce', (req, res) => {
const nonce = crypto.randomBytes(16).toString('hex');
// セッションDBまたはRedisに保存し、有効期限を短く設定する
req.session.nonce = nonce;
res.json({ nonce });
});
// 署名を検証してセッションを開始するエンドポイント
app.post('/api/auth/verify', async (req, res) => {
const { message, signature } = req.body;
// 1. セッション内のNonceと一致するか確認
// 2. ethers.jsなどで署名者アドレスを復元
const address = ethers.utils.verifyMessage(message, signature);
if (address === expectedAddress) {
// JWTをHttpOnly Cookieとしてセット
res.cookie('session_token', token, {
httpOnly: true, // JSからアクセス禁止
secure: true, // HTTPSのみ
sameSite: 'strict' // CSRF対策の要
});
res.send({ status: 'success' });
}
});
—
3. インフラレベルでの防御:NginxによるOriginガード
アプリケーションコードだけで防御するのは限界がある。インフラ層で「不正なOriginからのリクエスト」を遮断する設定を入れろ。
nginx.confで以下のように設定することで、意図しないドメインからのAPI叩き込みを防ぐことができる。
# APIへのリクエストに対する厳しいOriginチェック
location /api/ {
# 許可するドメイン以外からのリクエストは即座に弾く
if ($http_origin !~* ^(https://your-dapp.com|https://app.your-dapp.com)$) {
return 403;
}
# CORSの設定も厳格に
add_header 'Access-Control-Allow-Origin' 'https://your-dapp.com';
add_header 'Access-Control-Allow-Credentials' 'true';
proxy_pass http://backend_upstream;
}
—
最後に:現場のエンジニアへ
セキュリティとは「一度設定して終わり」の静的なものではない。特にWeb3はライブラリの更新が激しく、昨日まで安全だった実装が今日には脆弱性として報告されることもある。
1. セッションには必ずNonceを含めろ(使い回しは厳禁)。
2. トークンは必ず HttpOnly Cookieで管理しろ(LocalStorageへの保存は自殺行為)。
3. Originはホワイトリストで管理しろ(ワイルドカード指定は論外)。
この3つを守るだけでも、あなたのdAppのセキュリティ強度は格段に上がるはずだ。もし、チームの中に「面倒だから」と言ってこれらの実装を省こうとする奴がいたら、この記事のURLを叩きつけてやってくれ。
技術は常に攻撃者と共にある。泥臭く、しかし理路整然と防衛を続けていこう。それが我々プロの仕事だ。
コメント