התקפות של בקרת גישה שבורה מתרחשות כאשר חוזה Solidity מאפשר לקריאה בלתי אמינה להגיע ליכולת שמשנה מצב שחשובה לשאלות של אבטחה, כגון העברת נכסים, שינוי יישום, הקצאת תפקידים או קונפיגורציה של כתובות מוכרות. התוקף לא חייב לשבור את הצפנה: הוא מנצל חוסר, שגיאה או הגדרה רחבה מדי של בדיקת ההרשאה כדי להשיג העלאת הרשאות אפקטיבית.
ההשפעה יכולה להיות נרחבת יותר בפרוטוקול כולו, מכיוון שפונקציות בעלי הרשאה לעיתים קרובות מפקחות על כספים מצטברים או קונפיגורציה קריטית לניהול. Poly Network אבדו סכום של 611.0 מיליון דולר ב-10 באוגוסט 2021 באירוע שזוהה על ידי DefiLlama כ”בקרת גישה לא תקינה”. לכן, ההרשאה אינה עניין של ממשק משתמש בלבד או פרטים מנהלתיים, אלא חלק בלתי נפרד מגבול הביטחון המרכזי של החוזה.
מהי פרצה בבקרת גישה שבורה ב-Solidity?
פריצת בקרת גישה שבורה קיימת כאשר חוזה אינו מאשר באופן אמין ש-msg.sender מורשה לביצוע פעולה בעלת הרשאה, או כאשר מודל ההרשאה שלו מעניק יותר הרשאות מכפי שדרושות באותה פעולה. בדרך כלל, הכישלון מופיע בפונקציות שניתן להזמין מבחוץ ושמשנות מצב רגיש, מעבירות נכסים, משדרגות קוד או משנות גופים אמינים.
גם אם לחוזה יש משתנה owner, הוא עלול להיות פגיע. השאלה החשובה היא האם כל נתיב ביצוע בעל הרשאה מבצע בדיקה נכונה של ההרשאה, כולל פונקציות עזר, מפעילי אתחול, פונקציות ניהול תפקידים ו-hooks לשדרוג.
דוגמה לתקיפה נפוצה של בקרת גישה שבורה נראית כך:
- התוקף מזהה פונקציה שניתן להזמין מבחוץ שיש בה השלכות בעלות הרשאה.
- אין בה בדיקת קריאה, או שהיא בודקת את הכתובת הלא נכונה, או נשענת על מצב שניתן לשליטה על ידי התוקף.
- התוקף קורא לפונקציה ישירות, מ绕 שום הגבלה בממשק ה-front-end.
- הקריאה מעניקה תפקיד, משנה כתובת נתונה בטוחה, או מפעילה פעולה של העברת נכסים.
- התוקף משתמש ביכולת החדשה כדי למשוך כספים או לשנות את התנהגות הפרוטוקול.
- החוזה מקבל את העסקה כי ה-EVM מיישם רק בדיקות על הבלוק עצמו.
כפתורי ממשק, תיעוד ונהלי תפעול אינם מונעים קריאות ישירות לחוזה. כל חשבון יכול להגיש קריאת calldata לפונקציה ציבורית או חיצונית. לכן, קוד Solidity חייב לאכוף את המדיניות בדיוק בנקודה בה מתבצע שינוי מצב רגיש.
אילו תבניות ב-Solidity גורמות לתקיפות של בקרת גישה ושדרוג הרשאות?
פרצות של בקרת גישה לא תקינה מופיעות לעיתים קרובות כאשר מפתחים חושפים setters להגדרות תצורה, עוזרים ניהוליים או מסלולי אתחול מבלי לשקול שכל פונקציה ציבורית היא API שאינו בטוח בפני תוקף. התבנית המסוכנת אינה מוגבלת רק להיעדרות של מיידע onlyOwner: היא כוללת הרשאות שניתן לשנות על ידי קריאות חיצוניות, תפיסות שגויות שיורשות אוטומטית הרשאות מיותרות, או הגדרות רחבות מדי.
תבניות כשל נפוצות כוללות:
- setters ציבוריים שמאפשרים לכל אחד להחליף קרן, oracles, יישום, מפעיל או זוכה.
- פונקציית ניהול תפקידים בלי בדיקת מנהל.
- אתחולונים שניתן לקרוא לאחר פריסה, או נקראים על ידי חשבון לא מתוכנן.
- פונקציות שבודקות
tx.originבמקוםmsg.sender. - פונקציה פנימית עם הרשאה גבוהה שמגיע through wrapper חיצוני בלתי מוגן.
- תפקיד של מנהל יחיד שיכול לבצע כל פעולה תפעולית, ומגדיל את טווח הפגיעות.
- הרשאות המבוססות על כתובת משתמש מספק, במקום מיפוי תפקידים שאינו משתנה.
- פונקציות שדרוג שמאשרים את הקריאה אך לא את היישום החדש או את מסלול השדרוג.
תבנית פגומה זו, במיוחד כשמאוד עדינה, נראית כך:
function setOperator(address newOperator) external {
operator = newOperator;
}
הפונקציה לחיוב תשלום אולי דורשת ש-msg.sender == operator, אך הגבול של בקרת ההרשאות כבר נפרץ מכיוון שכל אחד יכול להכניס את עצמו כ’מפעיל’. בודקים צריכים לעקוב אחורה מכל פונקציה רגישה כדי לאתר מי יכול לקבל את התפקיד הדרוש.
שיטת Soken רואה בהקצאת תפקידים והגדרת כתובות אמינות כפעולות בעלות הרשאה בפני עצמן. עיצוב בטוח של בקרת גישה מגנה על שני תהליך ההעברה של הנכס הסופי ו של כל נתיב שיכול לשחררו.
איך נראה התקפת בקרת גישה שבורה בקוד Solidity?
דוגמה זו מראה חוזה שמבצע בדיקת קריאה לפני מכירה, אך הפונקציה setPayoutOperator שלה היא פומבית. תוקף יכול להקצות את עצמו כ-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, "לא אופרטור");
require(address(this).balance >= amount, "אוברדראפט");
(bool sent, ) = recipient.call{value: amount}("");
require(sent, "כשל בתשלום");
}
}
התיקון מפריד בין ניהול לפעילות. רק המפעיל יכול להעניק או לבטל את תפקיד התשלום ההיקפי, בעוד שהמפעיל לא יוכל לנהל תפקידים אחרים או לשנות בעלות.
// 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, "לא בעלים");
_;
}
modifier onlyPayoutOperator() {
require(payoutOperators[msg.sender], "לא מפעיל תשלום");
_;
}
constructor(address initialOwner, address initialOperator) {
require(initialOwner != address(0), "כתובת אפס");
require(initialOperator != address(0), "כתובת אפס");
owner = initialOwner;
payoutOperators[initialOperator] = true;
}
receive() external payable {}
function setPayoutOperator(address operator, bool allowed) external onlyOwner {
require(operator != address(0), "כתובת אפס");
// תיקון: רק הבעלים יכול לשנות חברות בתפקידי ההרשאה.
payoutOperators[operator] = allowed;
}
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "כתובת אפס");
owner = newOwner;
}
function pay(address payable recipient, uint256 amount)
external
onlyPayoutOperator
{
require(address(this).balance >= amount, "חוסר באון");
(bool sent, ) = recipient.call{value: amount}("");
require(sent, "כשל בתשלום");
}
}
אם קיימות שינוים בתפקידים, שדרוג הרשאות או משיכות קרן, סקר אבטחה והערכת אבטחה ממוקדת בקרות גישה [יכולה לשרת כבדיקת ניקיון ואימות של כל נתיב גישה לפעילות].
איך מונעים מפתחים ב-Solidity פרצות של בקרת גישה לא תקינה?
הימנעות מתקיפות של בקרת גישה שבורה דורשת מודל הרשאה מפורש, ולא סדרה של require שנוספו לאחר השלמת הפונקציות. יש להגדיר במפורש איזה שחקן מורשה לבצע כל פעולה רגיש למה המאמץ של ההרשאה, ולמה roles קטנות יותר יכולים לבצע פעולות שגרתיות בביטחון.
| טכניקה | מה היא מונעת | עלות בגז או מורכבות | מגבלות |
|---|---|---|---|
onlyOwner modifier |
ניהול בלתי מורשה של פונקציות שרק לבעלים | קריאה לקרן אחת לפני כל בדיקה | בעל שהוחלש עדיין שומר על כל ההרשאות שלו |
מיפויי תפקידים או AccessControl של OpenZeppelin |
שימוש לא מאושר בתפקידים תפעוליים נפרדים | קריאות לקרן ולוגיקה של ניהול תפקידים | תכנונים לא נכונים עלולים לייצר הרשאות עודפות |
| תפקידים עם מינימום הרשאות | שמירה או מפעיל שמבצע גם פעולות ניהול בלתי קשורות | יותר פונקציות והגדרות תפקידים | דורש קווי גבול ברורים לתפעול |
| Timelocks לשינויים קריטיים | שדרוגים זדוניים מיידיים או שינויים בפרמטרים | המתנה לביצוע והוספת חוזים | לא מונע פעולות זדוניות לאחר ההשהיה אם לא קיימת יכולת תגובה של משתמשים |
| ניהול רב חתימות | שינוי בסטטוס בעלות או הרשאות על ידי מפתח אחד בלבד | תיאום בין החותמים ועלות תפעולית | חותמים משותפים או מדיניות רכות עלולים להוות סיכון |
| הגנות אתחול חד-פעמיות | אתחול בלתי מורשה או חוזר | דגל באחסון ומבנה אתחול | תהליכי פריסה צריכים להבטיח הגדרה נכונה של בעלות ואחריות |
שימוש במודולים של שליטה בבעלות ותפקידים שאושרו בקפידה הוא רצוי, אך אין להריץ ספרייה שלמה כמודל ביטחון יחיד. לדוגמה, Ownable של OpenZeppelin מגדיר אוטומטית מנגנון בעלות, אך הצוות צריך להחליט אם הבעלות תשלוט גם בהשהיה, שדרוג, איסוף דמי פרסום, החלפת oracle, או חירום.
פרקטיקה אסורה היא שימוש ב-tx.origin לאישור. היא יכולה להיות מושפעת דרך חוזים מתווכים ואינה מזהה את הקריאה המיידית. עדיף להשתמש ב-msg.sender ולהשוות לכתובת המוגדרת של בעלים, תפקיד או חוזה מפוקח.
אילו תקריות אמיתיות ממחישות את ההשפעה של התקפות על בקרת גישה?
מדד DefiLlama מתעד 130 תקריות חוזה חכם המוגדרות כ”פריצת בקרת גישה לא תקינה” בין 2020 ל-2026, בלי כלכלות חוץ של חורי חילופי כספים או ארנקים, בסכום כולל של 909.7 מיליון דולר. אירועים אלה מראים שהכשלות בבקרת הגישה משפיעות על גשרים, פרוטוקולי DeFi, בוטים ויישומים במספר רשתות.
| פרוטוקול | תאריך | סכום | רשת |
|---|---|---|---|
| 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 |
תקריות אלו מלמדות שלפרוטוקול יש ערך כלכלי שיכול להיפגע כאשר גבול ההרשאה של החוזה אינו תואם את מודל האמון הייעודי שלו. הפונקציה הפגומה הספציפית עשויה להשתנות, אך סוג הכשל הוא עקבי: שחקן מקבל או מ Exercise הרשאה שהעיצוב היה צריך להימנע ממנה.
איך מזהים פרצות בבקרת גישה לפני הפריצה?
זיהוי פרצות בקרת גישה דורש למפות שינויים במצב רגיש בהתאם לתנאי ההרשאה שלהם, ואז להוכיח שלא קיים נתיב קריאה חלופי שיכול לעקוף את התנאים. כלים אוטומטיים עוזרים לזהות תבניות חשודות, אך ביקורת ידנית הכרחית להבנת ההנחות העסקיות, אמון, ושרשראות העלאת ההרשאות.
רשימת בדיקה לבדיקה:
- רשום את כל הפונקציות שמעבירות נכסים, משנות תפקידים, משדרגות קוד, עוצרות פעילות או משנות כתובות אמינות.
- זהה את הקריאה המיועדת ואת כלל ההרשאה לכל פונקציה רשומה.
- עקוב אחר כל פונקציה שיכולה להעניק, לבטל, להעביר או לאתחל את ההרשאה הזו.
- בדוק את כל עטיפות החוץ שמגיעות לפונקציות פנימיות עם הרשאות.
- תאשר שאתחול מתרחש פעם אחת בלבד ותוך תהליך הפריסה המתוכנן.
- ודא שפונקציות חירום לא יוכלו לבצע פעולות שגרתיות או ניהול שאינן רלוונטיות.
- בדוק ביטול תפקיד, העברת בעלות, וטיפול בכתובת אפס.
- עיין בפרוקסיז, חוזים שיורשים ומסלולי delegatecall כחלק מאותה גבול הרשאה.
Slither יכול לסמן דפוסי הרשאה חסרים, שימוש מסוכן ב-tx.origin, ומודיפיקורים לא עקביים, אך תוצאותיו דורשות אימות ידני. פומבינג ב-Founry עוזר ליצירת קריאות מקריאות אקראיות ולוודא שלא ניתן לשנות מצב בעל הרשאה על ידי קוראים בלתי מורשים.
בדיקות אינבדנטיות הן היעילות ביותר: למשל, שומר על מצב בלתי משתנה של כספים שלא אמורים להישלף על ידי חשבון שאינו בעל תפקיד התשלום, או שתפקיד הבעלים שולט בדרישת שייכות לתפקיד. תכונות אלה בוחנות את כללים הביטחוניים ישירות במקום לבדוק רק תהליכי משתמש צפויים.
תובנה ביטחונית, שיטת Soken: עיין באישור ההרשאה בתור גרף יכולות, ולא כרשימת מודיפיקורים. כל תוצאה עם הרשאה חייבת להיות עם נתיב מודע לבדיקה, וכל נתיב בלתי מכוון הוא מסלול אפשרי להעלאת הרשאות.
הגנה על חוזה מפני תקיפות של בקרת גישה שבורה נעשית על ידי הפיכת ההרשאה לברורה, הפחתת הכוחות של כל תפקיד והגנה על הפונקציות שמעניקות את ההרשאות בדיוק כמו על הפונקציות שמעבירות את הנכסים. פונקציה לתרומה נכונה תהפוך לפחות בטוחה אם מישהו יכול להקים לעצמו את המפעיל.
התחל ביצירת מפת הרשאה לחוזים פרוסים ותוכננים, כולל proxies וחשבונות תפעוליים, והמר כל כלל למודיפיקור ובדיקת אינבאריאנט. עבור מחקר מעמיק יותר באבטחת Solidity וקוד קשור לבעיות פגיעות, עיין ב-Soken Hub.