تحدث هجمات عدم التحقق من صحة المدخلات عندما يقبل عقد Solidity عناوين، كميات، معرفات، أسعار، أو قيم إعدادات يتحكم فيها المستخدم بدون إثبات تلاؤمها مع الافتراضات الاقتصادية والأمنية للعقد. لمنعها، يجب التحقق من كل إدخال خارجي عند حدود الثقة باستخدام قوائم السماح، فحوصات الحدود، فحوصات العنوان صفر، التصريح، والثوابت الحالة قبل تحرك الأصول أو حدوث تغييرات محاسبية.
الأثر ملموس: فقد Qubit 80.0 مليون دولار على شبكة BSC في 28-01-2022 في حادثة صنفت كـ “عدم التحقق من صحة المدخلات”. هذه الفشلات ليست مجرد مشكلات معاملات غير صحيحة. يمكن لقيمة غير مدققة أن تجعل كود Solidity الصحيح ينفذ إجراء عمل غير صحيح، مثل تحقق من رهن غير موجود، نقل أصول إلى وجهة غير موثوق بها، أو إنشاء دين يتجاوز الحد الحقيقي للمقترض.
ما هو ثغرة التحقق من صحة المدخلات في Solidity، وكيف يحدث الهجوم؟
توجد ثغرة التحقق من صحة المدخلات في Solidity عندما يعتبر عقد القيمة التي يزوّده بها الطرف المصرح به موثوقة رغم أن تلك القيمة تتحكم في صلاحيات، محاسبة الأصول، التسعير، أو انتقالات الحالة. يقوم المهاجم بتزويد قيمة صحيحة من الناحية التركيبية لـ ABI ولكنها غير صحيحة من الناحية الاقتصادية للبروتوكول، ثم يستخدم الحالة الناتجة لسحب الأصول، إصدار مطالبات، أو تجاوز القيود.
يقوم Solidity بفرض أنواع البيانات، وليس نية البروتوكول. يمكن أن يكون uint256 أي رقم غير متسبب في revert، وaddress يمكن أن يحدد عنوان كيان خارجي، عقد خبيث، العنوان صفر، أو رمز غير مدعوم. يجب على العقد تطبيق القواعد الدلالية المفقودة.
تسلسل الهجوم النموذي يكون كالآتي:
- يحدد المهاجم وظيفة عامة تقبل مبلغًا غير محقق، أو رمزا، أو مستلمًا، أو سعرًا، أو معرفًا.
- يختار قيمة تخالف الافتراض المقصود للبروتوكول.
- يسجل العقد رصيدًا غير آمن، أو حقًا، أو حد دين، أو حالة ترخيص غير صحيحة.
- يتصل المهاجم بعد ذلك بوظيفة لاحقة تثق في الحالة المسمومة.
- ينقل البروتوكول الأصول أو يصدر مطالبة قابلة للنقل استنادًا إلى تلك الحالة الزائفة.
- يتحمل المستخدمون الآخرون الخسارة عندما تظهر العجز في المحاسبة.
السؤال الحاسم هو: ما هو الحقيقة التي تدعي هذه المدخلات تمثيلها، وأين يتحقق بشكل مستقل أن العقد يحققها؟ إذا كانت الإجابة “المتصل هو الذي زودها”، فإن العقد يواجه ثغرة عدم التحقق من صحة المدخلات بشكل غير صحيح.
ما هي الأنماط في Solidity التي تسبب هجمات التحقق من صحة المدخلات؟
عادةً ما تنشأ هجمات التحقق من صحة المدخلات حيث يسمح عقد بمرونة للتكاملات أو تجربة المستخدم لكنه لا يقيد تلك المرونة ضمن نطاق البروتوكول المدعوم. غالبًا ما يكون المعامل الخطر ليس مسمى بوضوح price أو amount، بل قد يكون عنوان رمز، مسار، مستلم، معرف tranche، إثبات، أو عنوان عقد خارجي.
الأنماط غير الآمنة الشائعة تشمل:
- القيمة المعلن عنها من قبل المتصل: يقدم المقترض القيمة التي تُستخدم لحساب قدرته على الاقتراض.
- عناوين الرموز العشوائية: وظيفة الإيداع تقبل أي ERC-20، بما في ذلك الرموز ذات سلوك النقل غير القياسي.
- عناوين المستلمين غير المحققة: وظيفة تقبل
address(0)، أو العقد ذاته، أو عنوان يتجاوز قواعد الحيازة. - نسب ومعدلات غير محدودة: رسم، نسبة قرض لقيمة، خصم، انزلاق سعر، أو معدل مكافأة يتجاوز النطاق الأساسي المحدد.
- علاقات مصفوفة غير مُحققة: مصفوفتان متوازيتان تختلفان في الطول، أو يتم قبول تكرار الإدخالات، أو يتكرر مسار asset بشكل غير متوقع.
- انتقالات حالة بدون شروط مسبقة: إجراء مطالبة أو إلغاء أو استرداد أو ترحيل يحدث في الحالة غير الصحيحة لدورة الحياة.
- البيانات الخارجية كمصدر للحقيقة: وظيفة تثق بأسعار أو أرصدة، أو توقيعات، أو قيم يُزود بها المتصل بدون التحقق.
يجب أن يكون التحقق سياقيًا. require(amount > 0) مفيد، لكنه لا يثبت أن amount يمكن تحمله، أو مضمون كضمان، أو ضمن الحد الأقصى، أو متسق مع الحالة الحالية. بالمثل، لا يثبت التحقق من أن عنوان الرمز غير صفر أن الرمز معتمد، ويعمل كما هو متوقع، أو ينتمي إلى السوق الصحيح.
تتعامل منهجية مراجعة Soken مع كل معلمة خارجية على أنها محاولة لانتهاك الحالة: تحديد الثابت الذي يمكن أن يؤثر عليه، ثم التحقق من أن الوظيفة تفرض هذا الثابت قبل كتابة البيانات.
كيف يبدو مثال هجوم التحقق من صحة المدخلات في Solidity؟
يتيح مثال هجوم التحقق من صحة المدخلات هذا لمقترض أن يزود قيمة collateralValue الخاصة به. يتحقق العقد أن القرض المطلوب لا يتجاوز تلك القيمة، لكنه لا يثبت أبدًا أن المقترض وضع ضمانًا بقيمة مساوية لها. لذلك يمكن لمتصل أن يضبط كلا القيمتين إلى مستوى عشوائي ويقترض الأصول الممولة من المقرض.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC20 {
function transfer(address to, uint256 value) external returns (bool);
}
contract UnsafeLender {
IERC20 public immutable asset;
mapping(address => uint256) public debt;
constructor(IERC20 asset_) {
require(address(asset_) != address(0), "zero asset");
asset = asset_;
}
function borrow(
uint256 collateralValue,
uint256 amount
) external {
require(collateralValue > 0, "no collateral");
require(amount <= collateralValue, "insufficient collateral");
// ثغرة: المقترض يزود القيمة التي تدعم القرض.
debt[msg.sender] += amount;
require(asset.transfer(msg.sender, amount), "transfer failed");
}
}
تصميم الحل الثابت يستمد قدرة الاقتراض من الضمان الذي استلمه العقد فعليًا. عناوين الأصول الثابتة تمثل قائمة واضحة، فحوصات المُنشئ ترفض العناوين صفر، نسبة LTV محدودة، والتحقق من الفرق في الرصيد يثبت فرضية النقل بدلاً من الاعتماد على المبلغ المطلوب للإيداع.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC20 {
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
contract ValidatedLender {
IERC20 public immutable loanAsset;
IERC20 public immutable collateralAsset;
uint256 public immutable maxLtvBps;
mapping(address => uint256) public collateral;
mapping(address => uint256) public debt;
constructor(IERC20 loanAsset_, IERC20 collateralAsset_, uint256 maxLtvBps_) {
require(address(loanAsset_) != address(0), "zero loan asset");
require(address(collateralAsset_) != address(0), "zero collateral asset");
require(maxLtvBps_ > 0 && maxLtvBps_ <= 10_000, "invalid LTV");
loanAsset = loanAsset_;
collateralAsset = collateralAsset_;
maxLtvBps = maxLtvBps_;
}
function depositCollateral(uint256 amount) external {
require(amount > 0, "zero amount");
uint256 beforeBalance = collateralAsset.balanceOf(address(this));
require(collateralAsset.transferFrom(msg.sender, address(this), amount), "transfer failed");
uint256 received = collateralAsset.balanceOf(address(this)) - beforeBalance;
// تصحيح: فقط احتساب الضمان الذي تم استلامه فعليًا.
require(received == amount, "unsupported token behavior");
collateral[msg.sender] += received;
}
function borrow(uint256 amount) external {
require(amount > 0, "zero amount");
uint256 limit = (collateral[msg.sender] * maxLtvBps) / 10_000;
require(debt[msg.sender] + amount <= limit, "borrow limit exceeded");
debt[msg.sender] += amount;
require(loanAsset.transfer(msg.sender, amount), "transfer failed");
}
}
إذا كان بروتوكولك يقبل مرونة في الأصول، أو المسارات، أو الإدخالات المحاسبية، فإن تدقيق أمان ذكي من طرف ثالث واختبارات الاختراق يمكن أن تتبع مدى استقرار كل قيمة يُطالب بها قبل أن تؤثر على الأموال.
كيف تمنع هجمات التحقق من صحة المدخلات في Solidity؟
منع هجمات التحقق من صحة المدخلات يتطلب التحقق من الإدخالات عند المدخل، ثم الحفاظ على الافتراضات الناتجة بواسطة ثوابت واضحة طوال دورة حياة العقد. أفضل وسيلة دفاع ليست عبارة modifier واحدة، بل تصميم يتضمن أن كل تغيير مهم في الحالة يتضمن مجموعة واضحة من الإدخالات المسموح بها، مع شرط يمكن اختباره ويبقى صحيحًا بعده.
| التقنية | ما توقفه | تكلفة الغاز أو التعقيد | الحد الأقصى |
|---|---|---|---|
| قوائم السماح للأصول والتنفيذ | ERC-20 غير مدعوم، موصلات خبيثة، اتصالات خارجية عشوائية | البحث في التخزين أو المقارنة لعناوين ثابتة | الرموز المعتمدة لا تزال تُمثّل خطرًا اقتصاديًا |
| فحوصات الحدود | مبالغ صفر، معدلات مفرطة، نقاط أساس غير صحيحة، مهل غير آمنة | منخفضة، غالبًا بعض المقارنات | الحدود وحدها لا تبرر صحة اقتصادية |
| فحوصات العنوان صفر والمستلم | حرق الأصول، ملكية غير قابلة للاستخدام، وجهات حيازة غير صحيحة | منخفضة، مقارنة عنوان واحد | عنوان غير صفر قد لا يكون موثوقًا |
| التصريح وفحوصات دورة الحياة | مكالمات في الحالة الخاطئة أو من قبل جهات غير مخولة | قراءة من التخزين وتصميم الأدوار | الأدوار تتطلب إدارة آمنة |
| تحقق من فرق الرصيد | ائتمانات إيداع زائفة وسلوك نقل رموز غير مدعوم | قراءات إضافية لرصيد الرموز | يتطلب التعامل الدقيق مع الأصول القابلة لمعادلة السعر |
| التحقق من الثوابت | حل مسائل الملاءة، العرض، الدين، والعلاقات الاحتياطية | قراءات حسابية إضافية | يجب أن تغطي الثوابت النموذج الاقتصادي الكامل |
بالنسبة للبروتوكولات القابلة للتخصيص، يجب التحقق من التكوين عند إنشائه وعند استخدامه. معلمة تم إعدادها من قبل الحوكمة قد تكون آمنة عند الإنشاء لكنها غير آمنة بعد تغيير الاعتماد عليها. بالنسبة للأنظمة القابلة للترقية، تحقق من معلمات المهيئ بنفس الصرامة التي تتحقق بها من مدخلات الوظائف العامة.
رأي أمني: تبدأ منهجية Soken من الثابت، وليس من عبارة
require. إذا لم يتمكن المراجعون من تحديد ما يجب أن يظل صحيحًا بعد معالجة إدخال يتحكم فيه المستخدم، فلن يستطيعوا تحديد ما إذا كانت عملية التحقق كافية.
أي حوادث حقيقية صُنفت على أنها فقدان التحقق من صحة المدخلات؟
ارتبط فقدان التحقق من صحة المدخلات بخسائر مادية في العقود الذكية عبر مجالات الإقراض، المقايضة، وإدارة الأصول. تشمل فئة الحادثة شبكات وتصميمات بروتوكول مختلفة، مما يُظهر أن عدم التحقق الصحيح هو خطأ في منطق البروتوكول وليس في Solidity الخاص بالشبكة.
| البروتوكول | التاريخ | المبلغ المفقود | الشبكة |
|---|---|---|---|
| Qubit | 2022-01-28 | 80.0 مليون دولار | BSC |
| Hedgey | 2024-04-19 | 44.7 مليون دولار | Arbitrum، إيثيريوم |
| Transit Swap | 2022-10-02 | 28.9 مليون دولار | إيثيريوم، BSC |
| Pickle | 2020-11-22 | 19.7 مليون دولار | إيثيريوم |
تشترك هذه الحوادث في درس أساسي: لا يمكن للعقود استنتاج صدق البروتوكول بأمان من قيمة لأن معاملة ناجحة قامت بترميز تلك القيمة وتمريرها فقط. تحتاج أنظمة الأصول إلى تحقق دلالي حول الخاصية المحددة التي تحمي القيمة، سواء كانت ضمانًا تم استلامه، أو أصلًا مدعومًا، أو مسارًا معتمدًا، أو انتقالًا صحيحًا لدورة الحياة.
تصنّف شركة DefiLlama 52 حادثة عقود ذكية من 2020 حتى 2026 تحت تصنيف فقدان التحقق من صحة المدخلات، مع خسائر مجمعة بلغت 330.5 مليون دولار. لذلك، تعتبر منهجية Soken مراجعة التحقق كتمرين اقتصادي-أمني، وليس قائمة فحص لعبارات require سطحية.
كيف يمكن اكتشاف ثغرات التحقق من صحة المدخلات غير الصحيحة؟
يمكنك اكتشاف ثغرات التحقق غير الصحيح عن طريق تتبع كل معلمة خارجية قابلة للوصول إلى عمليات الكتابة في التخزين، أو المكالمات الخارجية، أو نقل الأصول التي تؤثر عليها، ثم اختبار إمكانية أن قيمة غير صحيحة ولكنها صحيحة من حيث ABI يمكن أن تنتهك ثابتًا. التحليل الثابت يُظهر بعض الفحوصات المفقودة، لكن الاختبارات بناءً على السيناريو ضرورية للافتراضات الخاصة بالبروتوكول.
استخدم قائمة التحقق التالية للمراجعة:
- ادرج كل معلمة
externalوpublicتؤثر على القيمة، السلطة، أو التوجيه. - تحقق مما إذا كان كل عنوان غير صفر، ومعتمد، وملائم لدوره.
- فحص كل مبلغ مقابل حدود البروتوكول، والأرصدة، و حدود الدين، وحدود العرض، وافتراضات العُشرية.
- تأكد من أن الأسعار، قيم الضمان، والأرصدة تأتي من مصادر موثوقة أو تم التحقق منها بشكل مستقل.
- تحقق من طول المصفوفات، والتفرد، وترتيبها، والعلاقات بين الفهارس.
- ضمّن أن كل وظيفة تسمح فقط بالحالة الصحيحة لدورة الحياة والمتصلين المصرح لهم.
- اختبر حساب فرق الرصيد فيما يتعلق برسوم النقل، والتعديل، والسلوك غير المعتاد لـ ERC-20.
- صِف واختبر الحالة النهائية لأي عملية نقل أصول.
تستطيع أداة Slither التعرف على أنماط مثل المكالمات الخارجية الخطرة، القيم المخرجة غير المراجعة، وضوابط الوصول الضعيفة التي غالبًا تحيط بفشلات التحقق. لكنها غير قادرة على استنتاج جميع ثوابت البروتوكول، لذا استخدم أدوات Fтьndry لخلق قيم صفرية، وأقصى قيم، ومصفوفات مكررة، وعناوين غير متوقعة، ورموز غير مدعومة.
اختبارات الثوابت فعالة بشكل خاص، على سبيل المثال، اختبار أن إجمالي الدين الخارجي لا يتجاوز قدرة الاقتراض المجهزة، وأن الضمان المصدق لا يتجاوز الأصول المستلمة، وأن الأصول المعتمدة فقط يمكنها تغيير محاسبة السوق. استكشف Soken Hub عند بناء عملية مراجعة أمنية أوسع حول تلك الخاصيات.
التحقق من صحة المدخلات هو الحد الفاصل بين نية المستخدم وصدق البروتوكول. يجب ألا يقرن العقد أو يصرح أو يسعر أو ينقل استنادًا فقط إلى ما يدعيه المتصل. استخلص القيم الحساسة من حالة موثوقة، قيد النطاقات المقبول، وفرض الثابت الذي يجعل الحالة الناتجة آمنة من الناحية الاقتصادية.
الخطوة العملية التالية هي إنشاء خريطة معلمة – ثابت لكل وظيفة نقل أصول ضمن قاعدة كودك. أي معلمة بدون مصدر ثقة موضح، أو مدى مسموح به، أو حالة بعدية تستحق مراجعة فورية.