AnchorはSolanaエコシステムで最も主流な開発フレームワークです。宣言的アカウント検証、自動シリアライズ、組み込みセキュリティチェックなどの機能により、開発のハードルが大幅に低下します。しかし、このフレームワークは便利さを提供する一方で、開発者が認識していない内部命令を各プログラムに裏で注入しています。これらの「隠された」命令は特定の条件下で攻撃者に悪用され、重大な資金損失を引き起こす可能性があります。
Beosinセキュリティチームは、低バージョンのAnchorにおいて、開発者がAccountInfoを使用してプログラム所有のPDAアカウントを宣言すると、攻撃者がAnchorによって自動的に注入されるIDL命令を用いて、2ステップでアカウントの制御権を奪い、その中のすべてのSOLを空にするという重要な脆弱性パターンを明らかにします。このプロセスには、任何の特権を取得する必要はありません。
一、IDL指令および関連メカニズムの分析
1.1 IDL 指令
Anchorは、ビルド時にno-idl機能を明示的に有効にしない限り、すべてのプログラムにIDL(Interface Definition Language、インターフェース定義言語)管理命令をインジェクトします。これらの命令には以下が含まれます:
IdlCreateAccount:チェーン上のIDLアカウントを作成する
IdlWrite:IDLアカウント/バッファアカウントにデータを書き込む
IdlSetAuthority:IDLアカウントのauthority(管理者)を変更
IdlCloseAccount:IDLアカウントを閉鎖し、すべてのlamportsを指定された受信者に転送します
IdlResizeAccount:IDLアカウントのサイズを調整
IdlCreateBuffer:IDLバッファアカウント(IDL Buffer)の作成
IdlSetBuffer:バッファアカウントのデータで正式なIDLアカウントを上書きします
これらのコマンドはオンチェーンIDL管理を目的として設計されましたが、プログラムが所有するアカウントに対して特別な操作権限(データの読み書き、コントローラーの変更、クローズおよびlamportsの移転)を有しており、これが脆弱性の核心です。攻撃者は開発者が作成した任何のビジネスコマンドを呼び出す必要なく、これらの組み込みコマンドを直接呼び出すことができます。
1.2 IDL バッファーアカウント
IDLバッファーアカウント(IDL Buffer)は、Anchorが大型のIDLデータを段階的にアップロードするために導入した一時的なアカウントです。完全なIDL(JSON圧縮後)は単一トランザクションのサイズ制限を超える可能性があるため、AnchorはまずIdlCreateBufferを使用してバッファーを生成し、複数のIdlWriteコマンドで段階的に書き込み、最後にIdlSetBufferで一度にコミットすることを許可しています。
重要な点は、IDLアカウント/バッファーアカウントのデータ構造です:これは固定レイアウトのヘッダーで始まり、ヘッダーにはauthority: Pubkeyフィールドが含まれています(本テスト出力ではcontrollerと呼ばれます)。IDL命令はこのフィールドを通じて「誰がこのアカウントを操作する権限を持っているか」を判断します。
しかし問題は、低バージョンのAnchorのIdlCreateBuffer処理ロジックが、本プログラムが所有する任意のアカウントをバッファーアカウントとして初期化し、authorityを直接トランザクションの署名者に設定してしまうことです。つまり、あるアカウントのownerが本プログラム(例えばプログラムのPDAウォレット)である場合、攻撃者はそのアカウントのcontrollerを自分自身に書き換えることができ、その後IdlCloseAccountを用いて、アカウント内のSOLを正当な手続きとしてすべて引き出すことができます。
1.3 バグ発生条件
この脆弱性を発動するには、以下の条件が同時に満たされる必要があります:
- 古いバージョンのAnchorを使用:危険なIDL命令がアカウントの判別を十分に行わず、業務アカウントをIDLアカウントと誤って操作できる可能性があります。
- no-idlが有効になっていません:プログラムのビルド時にデフォルトで注入されたIDL指令エントリが保持され、攻撃面が外部に公開されています。
- AccountInfo を使用してプログラムが所有する PDA を宣言する:開発者は、裸の AccountInfo を資金アカウント(例:バンク PDA)に使用し、Anchor の型付きアカウント(例:Account)が持つ discriminator / owner の判別を欠いているため、このアカウントは IDL 指令から見ると IDL アカウントと「区別できない」。
- アカウントはプログラム所有であり、lamportsを保有していること:owner == 本プログラムは、IDL命令がそれを操作できる前提である;SOLを保有している場合にのみ、空にされる価値が生じる。
上記の条件を満たした後、攻撃者は2つの通常のトランザクションだけで済みます。まずIdlCreateBufferでcontrollerを奪い、次にIdlCloseAccountですべてのSOLを移転します。この攻撃は、ターゲットプログラムからいかなる権限も付与される必要がありません。
1.4 アタックチェーン
以下は、Anchorの内部実装を踏まえて、攻撃者が2つの組み込み命令のみを使用して、通常の資金バウトをIDLアカウントに「偽装」し、それを空にする方法を段階的に解説します。
攻撃フローの概要
ステップ1 IdlCreateBuffer(treasury, signer = attacker)
└─ treasury.controller ==> attacker(コントローラーになる)
ステップ2 IdlCloseAccount(treasury, authority = attacker, dest = attacker)
└─ treasury.lamports ==> attacker(treasuryの残高を吸い取る)
ステップ1:IdlCreateBufferがauthorityを奪取する
Anchorの内部実装は以下のようになっています:
#[derive(Accounts)]pub struct IdlCreateBuffer {
#[account(zero)] // ← Key: discriminatorがすべて0のアカウント pub buffer: Account,
pub authority: Signer,}
pub fn idl_create_buffer(ctx: Context) -> Result
{
let idl = &mut ctx.accounts.buffer; idl.authority = *ctx.accounts.authority.key; // attackをauthorityとして設定 Ok(())}
#[account(zero)] の意味は、ディスクリミネータがすべてゼロで、プログラムが所有するアカウントを、初期化されていないIDLアカウントとして初期化することです。そして、vaultはちょうどこの両方の条件を満たしています:
vaultの状態
条件
プログラムが所有
init_if_needed 後、オーナーは本プログラムに設定されます
ディスクリミネーターがすべてゼロ
AccountInfo型を使用し、Anchorはdiscriminatorを書き込まず、データはすべてゼロです。
その後、攻撃者はvaultをIdlCreateBufferに渡します:
- Anchorはvault前に8バイトにIdlAccountのdiscriminatorを書き込む;
- 攻撃者の公開鍵をauthorityフィールドに書き込みます。
これで、vaultは攻撃者をauthorityとするIdlAccountに「偽装」され、その内部のSOLは一切動いていない。
第二ステップ:IdlCloseAccount —— 資金を清算する
#[derive(Accounts)]pub struct IdlCloseAccount { #[account(mut, has_one = authority)] // ← authority == signer を確認 pub account: Account, pub authority: Signer, #[account(mut)] pub destination: AccountInfo, // ← 攻撃ウォレット}
この時点でのvaultは完全に検証を通過しました:
- discriminator が IdlAccount と一致します
- authorityフィールド = 攻撃者の公開鍵
- has_one = authority 検証が通過しました
したがって、バウト内すべてのlamports(ユーザーが預けたすべてのSOL)が合法的に攻撃者のアカウントに転送され、バウトはゼロになりました。
根本原因:
deposit.rs で型付きの Anchor Account ではなく AccountInfo を使用することが、この脆弱性の致命的な根源である:
(1) 型付きの口座(例:Account)は初期化時に、その構造体固有の8バイトのディスクリミネーターを書き込みます;
(2) 自身の discriminator を書き込んだ後、Anchor はそれを IdlAccount として扱えなくなり(discriminator が一致しない)、最初の IdlCreateBuffer が失敗し、攻撃チェーンが断たれる。
二、ケーススタディ
以下のテストは、Anchor 0.31.0 で構築されたクロスチェーンブリッジコントラクトのPoCに基づいています。テストは、所有者がブリッジプログラム自身であり、ユーザーが預けた1.001281 SOLを保有するリアルなブリッジ財務(Bridge Treasury)をシミュレートします。攻撃者のウォレットは初期状態で2 SOLを保有しており、特権は一切持ちません。
プロジェクト
数値 / 説明
ブリッジプログラム
CJutNlr8d3oxdb11ReOLaZd5jPsqUxgwt2mv5e7equtE
金庫アカウント(Treasury)
DxGkzaMhP5Wy824GhoehErdD6MudfuEPGPwaV2s77FsM
金庫オーナー
ブリッジプログラム(プログラム所有PDA)
ウォレットの初期残高
1.001281 SOL(ユーザーの入金)
初期コントローラー
0x0000...0000(すべてゼロ、未設定)
攻撃者のウォレットの初期残高
2.000000 SOL
必要な特権
NONE(認可は必要ありません)
取引回数
2筆
金庫の最終残高
0.000000 SOL(清算済み)
攻撃者が利益を得る
+1.001281 SOL
PoCの実行スクリーンショット(tests/poc-idl-hijack.ts):
ステップ1のIdlCreateBufferでは、攻撃者がウォレットのコントローラーに設定されます(残高は変化なし);ステップ2のIdlCloseAccountでは、ウォレットの1.001281 SOLがすべて攻撃者のウォレットに転送され、ウォレットの残高はゼロになります。
修正/防御提案:
(1) Anchorのバージョンをアップグレード:最新バージョンではこの問題が修正されています(IDL命令はIDLアカウントとビジネスアカウントを厳密に区別します)。低バージョンのAnchorを使用しないことが、最も直接的で根本的な防御手段です。
(2) ビルド時に no-idl を有効化:本番環境のアプリケーションで IDL 指令のインジェクションを明示的に無効化し、攻撃面を根本的に排除します。
(3) 裸のAccountInfoではなく、型付けされた口座を使用:discriminatorおよびownerの検証を備えたAccount / SystemAccountなどの型で資金口座を扱い、IDL命令による誤認識を防ぎます。
(4) 最小化プログラムが所有する清算可能なアカウント:資金を保管するPDAに明確なowner/seeds/discriminator制約を追加し、重要な命令でアカウントディスクリミネータを検証する。
まとめ
この脆弱性の本質は「フレームワークによる隠蔽指令+アカウントタイプ判別の欠如」の組み合わせです。開発者は、AnchorがデフォルトでインジェクトするIDL指令が実在する攻撃面であることを認識し、フレームワークのバージョンをアップグレードし、資金アカウントに強型制約を適用することで、このような不正な資金引き出しリスクを効果的に排除できます。
Beosinは、プロジェクト上場前のスマートコントラクトセキュリティ監査、運用中のセキュリティリスク監視・遮断、盗難資産の回収、仮想資産の反マネーロンダリング(AML)、および調査・追跡に特化した、世界をリードするブロックチェーンセキュリティおよび規制コンプライアンステクノロジー企業です。Beosinは、世界20カ国以上における規制・法執行機関、200社以上の仮想資産サービスプロバイダー、および4,500社以上のWeb3プロジェクトに「ワンストップ」ブロックチェーンコンプライアンス製品およびセキュリティサービスを提供しています。公式アカウントのメッセージボックスより、お気軽にお問い合わせください。

