SolidityでのBroken Access Control攻撃防止方法

この記事の著者

破られたアクセスコントロール攻撃とは?

Solidityコントラクトにおいて、信頼されていない呼び出し元が資産移動、実装変更、ロールの割り当て、信用アドレスの設定などの状態変更を制限すべき操作にアクセスできてしまう状況を指します。攻撃者は暗号解読の破壊を必要とせず、欠落・誤った・過度に広範な認可チェックの脆弱性を突いて、権限昇格を実現します。

この種の脆弱性は、特権操作がプールされた資金やガバナンスに関わる重要な設定を制御していることから、プロトコル全体に影響を及ぼす可能性があります。例として、Poly Networkは2021年8月10日にEthereum、BSC、Polygon上で合計$611.0Mを失う事件を起こし、DefiLlamaはこれを「不適切なアクセスコントロール」と分類しています。したがって、認可は単なるUIや管理者の詳細事項ではありません。それはコントラクトのセキュリティ境界の核心部分です。

Solidityにおける破損したアクセスコントロール脆弱性とは?

この脆弱性は、コントラクトがmsg.senderの認可を確実に検証しない場合や、認可モデルが必要以上の権限を許している場合に存在します。特に、敏感なストレージを変更したり、資産を転送したり、コードをアップグレードしたり、信用対象を変更したりする外部から呼び出される関数において顕著です。

たとえコントラクトにowner変数があっても、脆弱性は存在しえます。本質的な問題は、すべての特権実行パス、補助関数、初期化、ロール管理関数、アップグレードフックにおいて、認可が正しく検証されているかどうかです。

典型的な攻撃は次の流れをたどります:

  1. 攻撃者は特権的な結果をもたらす外部呼び出し可能な関数を特定します。
  2. その関数には呼び出し者の検証がない、間違ったアドレスを検証している、または攻撃者コントロールの状態に依存している。
  3. 攻撃者はフロントエンドの制約を回避して直接関数を呼び出す。
  4. その呼び出しによってロールが付与されたり、信用アドレスが変更されたり、資産移動操作が行われる。
  5. 攻撃者は新たに得た操作能力を利用して資金を盗み出したり、プロトコルの挙動を変更したりする。
  6. これらの呼び出しは、EVMがオンチェーンのチェックだけを行うため、コントラクトはこれを受け入れてしまいます。

フロントエンドのボタンやドキュメント、操作手順だけでは、直接コントラクト呼び出しを制約できません。公開または外部関数に対して任意のアカウントからコールデータを送信可能です。そのため、Solidityコードは、敏感な状態遷移が行われる箇所で、ポリシーの検証を確実に行う必要があります。

どのようなSolidityパターンがアクセスコントロール攻撃や権限昇格を引き起こすか?

不適切なアクセスコントロールは、設定変更や管理支援、初期化パスを公開してしまう場合に多発します。特に、「全ての公開関数が攻撃者向けのAPIになる」という認識欠如に起因します。

危険なパターンの例は以下の通りです:

  • 誰でも treasuryや oracle、実装、オペレーター、受益者を置き換えられるpublicなsetter
  • 管理者権限を持たないロール管理関数
  • 展開後に呼び出せる初期化関数や意図せぬアカウントからの呼び出し
  • tx.originを用いて認可を行う関数
  • 未保護の外部ラッパーを通じてアクセス可能な特権内部関数
  • すべての操作を一人の管理者ロールに委ねる設計(権限の範囲拡大)
  • ユーザーが提供したアドレスに基づく認可(不変または管理されたロールマッピングではない)
  • 呼び出し側を検証しないアップグレード関数やコントラクト

このパターンの例:

function setOperator(address newOperator) external {
    operator = newOperator;
}

このコードでは、msg.senderがoperatorであることを確認する必要がありますが、実際には誰でも自分自身をoperatorに任命できてしまいます。このように、セキュリティ境界を崩すポイントを追跡することが重要です。

Sokenの手法では、ロール割り当てや信用アドレス設定も、独立した権限付与操作とみなします。安全なアクセスコントロールは、資産の最終的な移動だけでなく、それを解き放つ全てのルートも保護します。

Solidityコードにおける破られたアクセスコントロールの攻撃例は?

以下の脆弱なVaultは、支払い前に呼び出し者を検証していますが、そのsetPayoutOperator関数はpublicであり、攻撃者は自己をオペレーターに割り当てたうえでpayを呼び出し、後の認可チェックを無意味にしてしまいます。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract VulnerablePayoutVault {
    address public payoutOperator;

    constructor(address initialOperator) {
        payoutOperator = initialOperator;
    }

    receive() external payable {}

    function setPayoutOperator(address newOperator) external {
        // 脆弱点:誰でも自己を支払いオペレーターに設定できる
        payoutOperator = newOperator;
    }

    function pay(address payable recipient, uint256 amount) external {
        require(msg.sender == payoutOperator, "not operator");
        require(address(this).balance >= amount, "insufficient balance");

        (bool sent, ) = recipient.call{value: amount}("");
        require(sent, "payment failed");
    }
}

これを改善するには、管理と運用を分離します。所有者だけが特定権限(支払いロール)を付与・剥奪できるようにし、支払いオペレーターはロールの管理や所有権の変更を行えないようにします。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract FixedPayoutVault {
    address public owner;
    mapping(address => bool) public payoutOperators;

    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _;
    }

    modifier onlyPayoutOperator() {
        require(payoutOperators[msg.sender], "not payout operator");
        _;
    }

    constructor(address initialOwner, address initialOperator) {
        require(initialOwner != address(0), "zero owner");
        require(initialOperator != address(0), "zero operator");
        owner = initialOwner;
        payoutOperators[initialOperator] = true;
    }

    receive() external payable {}

    function setPayoutOperator(address operator, bool allowed) external onlyOwner {
        require(operator != address(0), "zero operator");
        // 修正点:所有者のみが特権ロールの変更を行える
        payoutOperators[operator] = allowed;
    }

    function transferOwnership(address newOwner) external onlyOwner {
        require(newOwner != address(0), "zero owner");
        owner = newOwner;
    }

    function pay(address payable recipient, uint256 amount)
        external
        onlyPayoutOperator
    {
        require(address(this).balance >= amount, "insufficient balance");
        (bool sent, ) = recipient.call{value: amount}("");
        require(sent, "payment failed");
    }
}

ロール変更やアップグレード権限、資金引き出しなどに関して、アクセスコントロールに特化したスマートコントラクト監査とセキュリティ評価によって、そのルートが適切に制限されているかを追跡できます。

Solidity開発者はどのようにして不適切なアクセスコントロール脆弱性を防ぐべきか?

破損したアクセスコントロールを防ぐには、単なる機能後付けのrequire文の散在ではなく、明確な「許可モデル」を策定します。どのアクターがどの操作を行えるのか、その権限は何のために存在するのか、日常的なタスクはどのロールに委ねるのかを定義してください。

技術 防げる内容 コスト 制約事項
onlyOwner修飾子 オーナーだけが管理可能な関数の不正実行 一回のストレージリード オーナーが侵害されると全権掌握可能
ロールマッピングやOpenZeppelinのAccessControl 操作ロールの不正使用 ストレージリードとロール管理ロジック 不適切なロール設計は過剰な権限を招く
最小権限のロール 関係のない管理操作への不正アクセス 追加のロールと関数定義 明確な運用境界の設定が必須
タイムロック 即時の不正アップグレードやパラメータ変更 遅延実行と追加コントラクト 遅延期間中の対応策次第
マルチシグ管理 1つの鍵で権限変更を独断で実行 複数署名者の調整と運用負荷 共有鍵や弱いポリシーは危険
一度だけの初期化ガード 二重・不正な初期化を防止 ストレージフラグと初期化ロジック デプロイ時の適正設定が必要

適切なOwnership primitiveとRole-based access controlを用いるのが望ましいですが、ライブラリだけに頼るのではなく、実際のセキュリティモデルに合わせて設計してください。例えば、OpenZeppelinのOwnableは所有権の仕組みを提供しますが、パウジングやアップグレード、手数料徴収、オラクル置換、緊急復旧といったコントロールは、別途決定と実装が必要です。

認可にtx.originを使うことは避けてください。tx.originは中間コントラクトなどを経由して影響を及ぼせるため、呼び出し元を正確に特定できません。代わりにmsg.senderを用いて、その値を適切な所有者、ロール、マルチシグ、コントラクト固有の認可ルールと照合します。

どのような実事例からアクセスコントロール攻撃のインパクトが証明されるか?

DefiLlamaは2020年から2026年までに130件以上のスマートコントラクト事故を記録しており、そのうち「不適切なアクセスコントロール」と分類されたものは、取引所やウォレットの侵害を除いて総損失額は$909.7Mにのぼります。その中からいくつかの事例を示します。

プロTOCOL 日付 金額 チェーン
Poly Network 2021-08-10 $611.0M Ethereum, BSC, Polygon
Mirror 2021-10-08 $90.0M Terra
Wintermute 2022-06-05 $17.6M Optimism
ValueDefi 2021-05-05 $10.0M BSC
Cork V1 2025-05-28 $12.0M Ethereum
SUPERFORTUNE AI 2026-05-27 $15.2M Ethereum
The Sandbox 2026-08-22 $675K Base, BSC, Ethereum
Radix 2026-08-31 $1.2M Radix

これらの事件は、コントラクトの権限境界と信頼モデルの乖離が経済的価値を露出させうる重要な課題を示しています。具体的な脆弱な関数は異なる場合がありますが、失敗のクラスは同じです。それは、設計上本来権限を持たせるべきでなかった者が権限を獲得または行使してしまうことです。

デプロイ前にアクセスコントロール脆弱性を検出するには?

アクセスコントロールの脆弱性を検出するには、敏感な状態遷移とそれに対応する認可条件をマッピングし、他の呼び出し経路でそれらの条件を回避できないことを証明します。自動化ツールはパターンを検出しますが、人間によるレビューも不可欠です。業務の権限や信頼の前提、権限昇格のチェーンに関して深い理解が必要です。

以下の確認リストを参考にしてください:

  1. 資産移転、ロール変更、コードアップグレード、活動停止、信用アドレス変更などを行う全ての関数を洗い出す。
  2. それぞれの関数において、想定される呼び出し者と認可ルールを明確にする。
  3. その権限を付与、剥奪、移譲、初期化できるすべての関数を追跡。
  4. 特権的な内部関数へ到達可能な外部ラッパーを確認。
  5. 初期化が一度だけ行われることと、適切なデプロイ過程のみに限定されていることを検証。
  6. 緊急ロールが日常的またはガバナンスの操作に誤って使われていないかを確認。
  7. ロール剥奪や所有権移譲、ゼロアドレス対応を試験。
  8. プロキシ、継承コントラクト、delegatecall経路も同じ権限境界の一部としてレビュー。

Slitherは欠落した認可パターンや危険なtx.origin利用、不整合な修飾子を指摘しますが、最終判断は人的な検証です。Foundryのファジングは、任意のアドレスからのコールを模擬し、不正呼び出しによる意図しない状態変更を防ぐのに有効です。

特に有効な方法として、インバリアントテストがあります。例として、Vaultの不変条件として、「 payoutロールを持たないアドレスはpayを通じてVaultの残高を減少させてはならない」などがあります。これらのテストは、期待されるユーザーフローだけではなく、敵対者の行動も検証します。

セキュリティの知見、Sokenの方法論: 認可を修飾子のリストとして捉えるのではなく「能力グラフ」として見直すこと。特権的効果には明示的かつ監査可能な経路が必要であり、意図しない経路は潜在的な権限昇格ルートです。

破損したアクセスコントロールを防ぐには、権限を明示化し、役割の権限を最小限に絞り、それらの権限割当や資金移動の関数を厳重に保護します。たとえ、誰でも指定できる引き出し者設定があったとしても、適切に制限されていなければ安全とは言えません。

展開済みおよび計画中のコントラクトの権限マップを作成し、そのルールを修飾子やインバリアントテストに落とし込みましょう。さらに深いSolidityセキュリティ研究や類似脆弱性の種類については、Soken Hubを探索してください。

この記事の著者

よくある質問

SolidityでのBroken Access Control脆弱性とは何ですか?

Broken Access Control脆弱性は、Smart Contractがmsg.senderの権限を確実に確認できず、未信用の呼び出し者が状態変更を行える状態です。これにより、クリプトの安全性を犠牲にし、権限エスカレーションが可能になります。

Broken Access Control攻撃はどのように権限エスカレーションを引き起こしますか?

この攻撃は、許可された権限を持つ呼び出し者向けの機能に未信用の呼び出し者がアクセスし、資産移動や役割変更などの状態変更を実行できることにより発生します。

どのようなSolidityアクションにアクセス制御を設けるべきですか?

資産移動や実装変更、役割割当、信頼済みアドレスの設定など、状態を変える能力はすべてアクセス制御の対象です。信頼できる認証が必要です。

Poly Networkのアクセスコントロール事件で何が起きましたか?

2021年8月10日にEthereum、BSC、Polygonで611Mドルが失われ、DefiLlamaは不適切なアクセス制御と評価。この事件は認証チェックの重要性と、管理外の能力が資金やガバナンスに影響を及ぼす例となりました。

なぜ認証がSolidityのセキュリティの核心なのですか?

認証は、特権関数が資金やガバナンス設定を制御できるため、重要な安全境界です。不正確や過剰な認証は、管理の弱点をセキュリティリスクに変え、プロトコル全体の安全性を損ないます。

チャット