การโจมตีแบบการควบคุมการเข้าถึงที่ผิดพลาด (Broken access control) เกิดขึ้นเมื่อสัญญา Solidity อนุญาตให้ผู้ที่ไม่ได้รับอนุญาตเข้าถึงความสามารถในการเปลี่ยนแปลงสถานะซึ่งควรจะถูกจำกัด เช่น การโยกย้ายสินทรัพย์ การเปลี่ยนแปลงการติดตั้ง การมอบหมายบทบาท หรือการปรับตั้งค่าที่อยู่ที่เชื่อถือได้ ผู้โจมตีไม่จำเป็นต้องแก้ไขการเข้ารหัสลับ: พวกเขาใช้จุดอ่อนจากการขาด การตรวจสอบสิทธิ์ที่ผิดพลาด หรือการกำหนดสิทธิ์ที่กว้างเกินไปเพื่อการเพิ่มสิทธิพิเศษอย่างมีประสิทธิภาพ
ผลกระทบอาจครอบคลุมทั้งโปรโตคอล เนื่องจากฟังก์ชันที่มีสิทธิ์มักควบคุมกองทุนรวม หรือการตั้งค่าที่สำคัญต่อการกำกับดูแล ตัวอย่างเช่น Poly Network สูญเสียเงินกว่า 611.0 ล้านดอลลาร์ในวันที่ 10 สิงหาคม 2021 ในเหตุการณ์ที่รวม Ethereum, BSC และ Polygon ซึ่งจัดอยู่ในกลุ่มที่ DefiLlama จัดเป็นการควบคุมการเข้าถึงที่ไม่เหมาะสม การอนุญาตจึงไม่ใช่แค่ปัญหาหน้าจอผู้ใช้ (UI) หรือรายละเอียดด้านการบริหารจัดการ แต่เป็นส่วนหนึ่งของขอบเขตความปลอดภัยหลักของสัญญา
การตรวจสอบช่องโหว่ของการควบคุมการเข้าถึงแบบผิดพลาดใน Solidity คืออะไร?
ช่องโหว่ของการควบคุมการเข้าถึงที่ผิดพลาดเกิดขึ้นเมื่อสัญญาไม่สามารถยืนยันได้อย่างน่าเชื่อถือว่า msg.sender มีสิทธิ์ในการดำเนินการที่มีสิทธิพิเศษ หรือเมื่อโมเดลสิทธิ์อนุญาตให้มีอำนาจมากกว่าที่จำเป็นในแต่ละคำสั่ง ใน Solidity การล้มเหลวมักปรากฏในฟังก์ชันที่สามารถเรียกใช้งานได้จากภายนอก ซึ่งสามารถแก้ไขข้อมูลสำคัญ โอนทรัพย์สิน อัปเกรดโค้ด หรือเปลี่ยนคู่สัญญาที่เชื่อถือได้
สัญญาอาจมีความเปราะบางแม้จะมีตัวแปร owner ก็ตาม คำถามสำคัญคือ ทุกเส้นทางการปฏิบัติการที่มีสิทธิ์ต้องตรวจสอบความถูกต้องของอำนาจอย่างถูกต้อง ครอบคลุมทั้งฟังก์ชัน helper, การเริ่มต้น (initializer), ฟังก์ชันจัดการบทบาท และ hooks สำหรับการอัปเกรด
ตัวอย่างลำดับการโจมตีแบบการควบคุมการเข้าถึงที่ผิดพลาดทั่วไปประกอบด้วย:
- ผู้โจมตีระบุฟังก์ชันที่สามารถเรียกใช้งานจากภายนอกที่มีผลกระทบเชิงสิทธิ์
- ฟังก์ชันนั้นไม่มีการตรวจสอบผู้เรียก (caller), ตรวจสอบที่อยู่ผิด หรืออาศัยสถานะที่ควบคุมโดยผู้โจมตี
- ผู้โจมตีเรียกใช้งานฟังก์ชันโดยตรง ละเว้นข้อจำกัดบนหน้าจอหน้าเว็บ (frontend)
- การเรียกใช้นั้นมอบบทบาท เปลี่ยนที่อยู่ที่เชื่อถือได้ หรือดำเนินการย้ายทรัพย์สิน
- ผู้โจมตีใช้ความสามารถใหม่ที่ได้มาเพื่อดึงเงินหรือเปลี่ยนพฤติกรรมของโปรโตคอล
- สัญญายังยอมรับธุรกรรม เพราะ EVM บังคับใช้การตรวจสอบเฉพาะในเชิงบนบล็อกเท่านั้น
ปุ่มบนหน้าจอ, เอกสารประกอบ และกระบวนการดำเนินงานที่ออกแบบไว้ ไม่มีแนวทางป้องกันการเรียกสัญญาโดยตรงได้ นักลงทุนควรตรวจสอบว่าโค้ด Solidity ตรวจสอบความถูกต้องของสิทธิ์ในจุดที่สำคัญที่สุดเท่านั้น
รูปแบบของ Solidity ที่ส่งผลให้เกิดการโจมตีแบบการควบคุมการเข้าถึงผิดพลาดและการเพิ่มสิทธิ์
ช่องโหว่ด้านการควบคุมการเข้าถึงที่ผิดพลาดมักเกิดขึ้นเมื่อผู้พัฒนาทำให้การตั้งค่าการกำหนดบทบาท การช่วยเหลือด้านการบริหารจัดการ หรือเส้นทางการเริ่มต้น (initialization) เปิดเผยโดยไม่พิจารณาว่า ฟังก์ชันสาธารณะเหล่านั้นอาจเป็น API ที่ถูกโจมตีได้ รูปแบบอันตรายไม่ได้จำกัดแค่ว่าโมดิไฟร์ onlyOwner ขาดหายเท่านั้น แต่รวมถึงการอนุญาตที่สามารถแก้ไขได้โดยผู้ร้องขอ (arbitrary callers), การสืบทอดที่ผิดพลาด, การใช้งานที่ไม่สอดคล้องกัน, หรือการกว้างเกินไปสำหรับการดำเนินการ
ตัวอย่างรูปแบบความล้มเหลวที่พบบ่อย:
- ตัวตั้งค่าที่เปิดให้ใครก็ได้เปลี่ยนแปลงบัญชีธนาคาร, ออราเคิล, การติดตั้ง, ผู้ดำเนินการ หรือผู้รับผลประโยชน์
- ฟังก์ชันจัดการบทบาทที่ขาดการตรวจสอบผู้ดูแลระบบ
- ตัวเริ่มต้น (initializers) ที่สามารถเรียกใช้งานหลังการ deploy หรือโดยบัญชีที่ไม่ตั้งใจ
- ฟังก์ชันที่ตรวจสอบ
tx.originแทนmsg.sender - ฟังก์ชันภายในที่มีสิทธิ์สูงที่เข้าถึงผ่าน wrapper ภายนอกที่ไม่ปลอดภัย
- บทบาทผู้ดูแลระบบเดียวที่สามารถดำเนินการได้ทุกกิจกรรม เพิ่มความเสี่ยงในการถูกโจมตีอย่างกว้างขวาง
- การอนุญาตโดยอิงจากที่อยู่ที่ผู้ใช้อื่นให้มา แทนการใช้บทบาทที่ไม่เปลี่ยนแปลงได้ (immutable role) หรือการแมปบทบาทที่ควบคุมได้
- ฟังก์ชันอัปเกรดที่ตรวจสอบผู้เรียกใช้งาน แต่ไม่ตรวจสอบรายละเอียดของการอัปเกรด (implementation หรือ pathway)
ตัวอย่างที่ฝังอยู่ในโค้ด:
function setOperator(address newOperator) external {
operator = newOperator;
}
แม้ว่าฟังก์ชันนี้อาจตรวจสอบ msg.sender == operator อย่างถูกต้อง แต่กรอบความปลอดภัยด้านการเข้าถึงได้ล้มเหลวแล้ว เพราะใครก็สามารถแต่งตั้งตัวเองเป็น Operator ได้ ควรย้อนรอยจากฟังก์ชันที่สำคัญเพื่อระบุว่าใครสามารถได้รับบทบาทที่จำเป็น
แนวทางของ Soken ให้ความสำคัญกับการมอบหมายบทบาทและการกำหนดค่าที่อยู่ที่เชื่อถือได้ในฐานะการดำเนินการที่มีสิทธิพิเศษอย่างหนึ่ง การออกแบบการควบคุมการเข้าถึงที่ปลอดภัยจึงต้องคุ้มครองทั้งการโอนทรัพย์สินในที่สุดและเส้นทางทุกเส้นทางที่อาจปลดล็อกการเข้าถึงเหล่านั้น
รูปแบบของโค้ด Solidity ที่แสดงภาพการโจมตีแบบการควบคุมการเข้าถึงที่ผิดพลาด
vault ที่มีช่องโหว่จะตรวจสอบผู้เรียกใช้งานก่อนชำระเงิน แต่ฟังก์ชัน setPayoutOperator เป็นแบบสาธารณะ (public) ผู้โจมตีสามารถกำหนดตัวเองเป็น operator แล้วเรียกใช้งาน 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");
}
}
การแก้ไขที่ดีควรแยกการบริหารและการดำเนินการออกจากกัน โดยเฉพาะให้เฉพาะเจ้าของสัญญาสามารถแต่งตั้งหรือเพิกถอนบทบาทการจ่ายเงินแบบจำกัดขอบเขตเท่านั้น ส่วน operator สำหรับการจ่ายเงินจะไม่สามารถบริหารบทบาทหรือเปลี่ยนเจ้าของได้อีกต่อไป
// 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 โมดิไฟเออร์ |
การบริหารจัดการโดยผู้อื่นนอกเหนือจากเจ้าของ | การอ่านข้อมูลจาก storage หนึ่งครั้งต่อการเรียกที่คุ้มกัน | เจ้าของที่ถูกโจมตียังคงมีสิทธิ์เต็มที่อยู่ดี |
การแมปบทบาทหรือกลไก AccessControl ของ OpenZeppelin |
การใช้งานบทบาทแยกต่างหากโดยไม่ได้ตรวจสอบอย่างรอบคอบ | อ่านข้อมูลจาก storage และตรรกะการจัดการบทบาท | การออกแบบบทบาทผิดอาจก่อให้เกิดสิทธิ์เกินสมควร |
| บทบาทแบบ Least-privilege | การมอบหมายสิทธิ์ให้โอเปอเรเตอร์หรือผู้ดูแลระบบใช้พลังงานที่ไม่เกี่ยวข้องกัน | ต้องการฟังก์ชันและการกำหนดบทบาทเพิ่มเติม | ต้องมีขอบเขตการดำเนินงานที่ชัดเจน |
| การตั้งเวลา (Timelocks) สำหรับการเปลี่ยนแปลงสำคัญ | การอัปเกรดหรือเปลี่ยนพารามิเตอร์ทันทีที่อาจเป็นอันตราย | การล่าช้าการดำเนินการและการใช้สัญญาเพิ่มเติม | ไม่สามารถป้องกันการกระทำที่เป็นอันตรายในช่วงเวลารอคอยโดยที่ผู้ใช้อาจตอบโต้ได้ |
| การบริหารแบบ multisig | คีย์เดียวสามารถเปลี่ยนสถานะที่มีสิทธิ์สูงได้โดยลำพัง | การประสานงานของ signer และภาระการดำเนินการ | Signers ร่วมกัน หรือแนวทางนโยบายอ่อนแอ ยังคงเป็นความเสี่ยง |
| การตั้งค่าการเริ่มต้นครั้งเดียว (One-time init) | การเริ่มต้นซ้ำซ้อนหรือไม่ได้รับอนุญาต | สถานะบน storage และตรรกะ initializers | กระบวนการ deploy ต้องกำหนดอำนาจอย่างชัดเจน |
ใช้ primitives การเป็นเจ้าของ (ownership primitives) และบทบาท (role primitives) ที่ได้รับการตรวจสอบอย่างดีเท่านั้น แต่ไม่ควรใช้ไลบรารีเป็นแบบจำลองความปลอดภัยครบถ้วน ตัวอย่างเช่น OpenZeppelin Ownable ให้กลไกของความเป็นเจ้าของ ในขณะเดียวกัน ทีมโปรโตคอลยังต้องตัดสินใจด้วยว่าจะให้ความเป็นเจ้าของควบคุมการหยุดชะงัก, การอัปเกรด, การเก็บค่าธรรมเนียม, การแทนที่ออราเคิล หรือการกู้คืนฉุกเฉินหรือไม่
หลีกเลี่ยงการใช้ tx.origin สำหรับการอนุญาต เนื่องจากสามารถถูกชักจูงผ่านสัญญาตัวกลาง และไม่สามารถระบุผู้เรียก (immediate caller) ได้ ใช้ msg.sender แล้วตรวจสอบว่าตรงกับเจ้าของ, บทบาท, multisig หรือกฎการอนุญาตเฉพาะของสัญญา
เหตุการณ์จริงใดแสดงให้เห็นถึงผลกระทบของการโจมตีด้วยการควบคุมการเข้าถึง?
ข้อมูลจาก DefiLlama ระบุว่า ตั้งแต่ปี 2020 ถึง 2026 มีเหตุการณ์ในสมาร์ทคอนแทรกต์ 130 เหตุการณ์ที่จัดอยู่ในกลุ่มผิดพลาดด้านการควบคุมการเข้าถึง (Improper Access Control) โดยไม่รวมเหตุการณ์การละเมิดจากตลาดแลกเปลี่ยนและกระเป๋าเงิน รวมความสูญเสียกว่า 909.7 ล้านดอลลาร์ เหตุการณ์ที่ได้รับการยืนยันเหล่านี้พิสูจน์ให้เห็นว่าความล้มเหลวด้านการควบคุมการเข้าถึงส่งผลต่อสะพาน (bridges), โปรโตคอล DeFi, บอท และแอปพลิเคชันต่าง ๆ ได้ในหลาย Chain
| โปรโตคอล | วันที่ | จำนวนเงิน | Chain |
|---|---|---|---|
| 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 |
เหตุการณ์เหล่านี้มีบทเรียนด้านความปลอดภัยร่วมกัน: มูลค่าทางเศรษฐกิจของโปรโตคอลสามารถถูกเปิดเผยได้เมื่อขอบเขตอำนาจของสัญญาไม่ตรงกับโมเดลความไว้วางใจที่ตั้งไว้ ฟังก์ชันเฉพาะที่ได้รับผลกระทบอาจแตกต่างกันไป แต่เป็นกลุ่มความล้มเหลวเดียวกันคือ ผู้ดำเนินการได้รับหรือใช้อำนาจที่ควรจะเป็นของเขาไม่ได้รับอนุญาตตามการออกแบบ
คุณจะตรวจพบช่องโหว่การควบคุมการเข้าถึงที่ผิดพลาดก่อนการ deploy ได้อย่างไร?
การตรวจจับช่องโหว่ด้านการควบคุมการเข้าถึงต้องทำแผนที่การเปลี่ยนแปลงสถานะสำคัญไปยังเงื่อนไขการอนุญาต จากนั้นพิสูจน์ว่าไม่มีเส้นทางเรียกใช้งานอื่นที่หลีกเลี่ยงเงื่อนไขเหล่านั้น เครื่องมืออัตโนมัติช่วยระบุรูปแบบน่าสงสัย แต่การตรวจสอบด้วยมนุษย์ยังจำเป็นเพื่อเข้าใจความเชื่อถือด้านธุรกิจ, สมมติฐานความไว้วางใจ, และห่วงโซ่การเพิ่มสิทธิ
แนวทางการตรวจสอบประกอบด้วย:
- รายงานฟังก์ชันทุกฟังก์ชันที่เกี่ยวข้องกับการโอนทรัพย์สิน, การเปลี่ยนบทบาท, การอัปเกรดโค้ด, การหยุดกิจกรรม หรือการเปลี่ยนที่อยู่ที่เชื่อถือได้
- ระบุผู้เรียกและกฎการอนุญาตของแต่ละฟังก์ชัน
- ติดตามเส้นทางฟังก์ชันที่สามารถมอบ, เพิกถอน, โอน หรือเริ่มต้นอำนาจนั้น
- ตรวจสอบ wrapper ภายนอกทั้งหมดที่เข้าถึงฟังก์ชันภายในที่มีสิทธิ์
- ยืนยันว่าการเริ่มต้นสามารถเกิดขึ้นได้เพียงครั้งเดียวและภายใต้กระบวนการ deploy ที่ตั้งใจไว้
- ตรวจสอบให้แน่ใจว่าสิทธิ์ฉุกเฉิน (emergency) ไม่สามารถดำเนินการเทียบเท่าหรือเกี่ยวข้องกับการกำกับดูแลผิดวัตถุประสงค์ได้
- ทดสอบการเพิกถอนบทบาท, การโอนกรรมสิทธิ์, และการจัดการกับที่อยู่ศูนย์
- ตรวจสอบ proxies, สัญญาที่สืบทอด (inherited), และเส้นทาง delegatecall เป็นส่วนหนึ่งของขอบเขตอำนาจเดียวกัน
Slither สามารถสแกนหารูปแบบการขาดสิทธิ์, การใช้ tx.origin ที่อันตราย, และ modifiers ที่ไม่สอดคล้องกันได้ แต่ผลลัพธ์ต้องผ่านการตรวจสอบด้วยมือ การ fuzzing ด้วย Foundry ช่วยสร้างการเรียกจากที่อยู่แบบสุ่ม และทดสอบว่า caller ที่ไม่ได้รับอนุญาตไม่สามารถเปลี่ยนแปลงสถานะที่มีสิทธิ์ได้
การทดสอบ invariants ถือเป็นวิธีที่มีประสิทธิภาพมาก เช่น กฎของ vault สามารถระบุได้ว่าที่อยู่ที่ไม่มีบทบาท payout ต้องไม่ทำให้ยอดใน vault ลดลงผ่าน pay อีกทั้ง ยังสามารถตรวจสอบได้ว่าเจ้าของเท่านั้นสามารถเปลี่ยนแปลงบทบาทได้ กฎเหล่านี้ทดสอบความปลอดภัยโดยตรง แทนที่จะทดสอบเฉพาะเส้นทางปกติของผู้ใช้
ข้อมูลเชิงลึกด้านความปลอดภัย, วิธีการของ Soken: ให้พิจารณาเครือข่ายความสามารถ (capability graph) ของการอนุญาต โดยไม่มองเป็นเพียงรายการ modifiers ทุกผลลัพธ์ที่มีสิทธิ์ควรมีเส้นทางที่ตั้งใจไว้และสามารถตรวจสอบได้ และเส้นทางที่ไม่ได้ตั้งใจคือเส้นทางที่อาจเป็นช่องทางการเพิ่มสิทธิ์ได้
การโจมตีแบบการควบคุมการเข้าถึงที่ผิดพลาดสามารถป้องกันได้ด้วยการทำให้อำนาจชัดเจนที่สุด, ลดพลังของแต่ละบทบาทให้เหลือน้อยที่สุด, และปกป้องฟังก์ชันที่เกี่ยวข้องกับการมอบอำนาจเหล่านั้นอย่างระมัดระวัง ฟังก์ชันถอนเงินที่มีการจำกัดสิทธิ์อย่างถูกต้องจะไม่ปลอดภัยหากใครก็สามารถแต่งตั้งบุคคลที่เกี่ยวข้องได้
เริ่มต้นด้วยการสร้างแผนที่อำนาจ (authority map) สำหรับสัญญาที่ deploy และที่วางแผนไว้ รวมถึง proxies และบัญชีปฏิบัติการ จากนั้นเปลี่ยนแต่ละกฎเป็นโมดิไฟเออร์และทดสอบ invariants สำหรับความปลอดภัย เพื่อให้ครอบคลุมทั้งระบบตามพฤติกรรมและเงื่อนไขที่คาดหวัง สำหรับข้อมูลเชิงลึกด้านความปลอดภัยของ Solidity และคลาสความเสี่ยงใกล้เคียงกัน ค้นหาเพิ่มเติมได้ที่ Soken Hub.