到目前為止,我們學了多種技術漏洞(像重入、溢位等),今天要談的是「最常被忽略」但最危險的一種——權限錯誤(Access Control Bug)。
這類錯誤不是因為數學錯,而是因為誰可以做什麼事沒有定義好。結果往往是一行程式,毀了一整個專案。
🔹 基本概念:身份驗證(Authentication)與授權(Authorization)
• 身份驗證:你是誰?(確認發送交易的人身份)
• 授權:你能做什麼?(確認這個人有沒有權限執行該操作)
在智能合約中,這兩者通常透過**地址(address)與函數修飾器(modifier)**控制。
如果邏輯沒寫對,就會變成「誰都能改 owner」、「誰都能提錢」的災難。
🔹 常見錯誤類型
類型 說明 範例
❌ 缺少權限修飾器 忘記onlyOwner或onlyAdmin function withdraw() public {
msg.sender.transfer(address(this).balance); }
⚠️ 使用 tx.origin 判斷身份 可被釣魚合約欺騙 require(tx.origin == owner);
❌ 多人共享權限混亂 管理多重角色時未分層 Admin / Owner 權限重疊或錯誤分配
⚠️ 沒有初始化 owner 部署時忘記設定 owner,導致任何人可取得控制權 owner 未在 constructor 初始化
🔹 tx.origin vs msg.sender 差異
屬性 說明
msg.sender 目前這筆呼叫的直接來源(可能是使用者或另一個合約)
tx.origin 交易最初發起者(永遠是人類使用者帳戶)
contract A {
address public owner;
constructor() { owner = msg.sender; }
function secureWithdraw() public {
require(msg.sender == owner, "Not owner"); // ✅ 安全
}
function insecureWithdraw() public {
require(tx.origin == owner, "Not owner"); // ❌ 可被釣魚攻擊
}
}
contract Attacker {
A victim;
constructor(address _victim) { victim = A(_victim); }
function attack() public {
victim.insecureWithdraw(); // tx.origin 為使用者,但 msg.sender 為 attacker
}
}
解釋:
駭客建立惡意合約誘導使用者互動(例如簽送 U、mint NFT),在背後呼叫 victim 的 insecureWithdraw()。
由於 tx.origin 是使用者,檢查會通過,造成資金外流。
🔹 權限控制正確寫法
Solidity 提供 modifier 讓我們簡化權限檢查邏輯:
pragma solidity ^0.8.0;
contract SecureVault {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function withdraw(uint amount) public onlyOwner {
payable(owner).transfer(amount);
}
}
🔹 進階多角色權限模型(OpenZeppelin)
實務上常使用 OpenZeppelin 的 AccessControl 進行角色分層:
import "@openzeppelin/contracts/access/AccessControl.sol";
contract MultiRole is AccessControl {
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
constructor() {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
function mint(address to) public onlyRole(MINTER_ROLE) {
// mint token
}
}
這樣可以:
• 明確區分角色權限
• 支援多重管理者
• 更容易進行審計與升級
🔹 常見檢查清單(Audit Checklist)
✅ 是否所有敏感操作都有權限保護?
✅ 是否只使用 msg.sender 進行身份驗證?
✅ 部署時是否初始化了所有關鍵變數(例如 owner)?
✅ 是否使用 OpenZeppelin 等成熟模組?
✅ 是否考慮權限轉移、撤銷等場景?
這類權限錯誤往往是最「無辜卻最致命」的漏洞。
開發者以為只是少一行 onlyOwner,結果可能導致整個資金池被提光。
我覺得在寫智能合約的過程中,應該養成一個「零信任」思維:
永遠假設沒人值得信任,連自己都不例外。
權限邏輯應該明確、最小化、模組化。當安全和方便衝突時,優先選擇安全。