یک Parser مبلغ، ورودی‌های معمول مثل RIAL:1! و RIAL:9! را بدون خطا پردازش می‌کرد و تست‌های مثال‌محور هم سبز بودند. اما موتور Fuzz در کمتر از یک‌دهم ثانیه به RIAL:0! رسید و برنامه با تقسیم بر صفر متوقف شد. نکته این نبود که «داده‌ای تصادفی خوش‌شانس بود»؛ Instrumentation مسیر تازه را دید، Mutation را نگه داشت و همان ورودی را به Artifact قابل‌بازتولید تبدیل کرد.

فاز تستینگ یا Fuzz Testing یک فرایند مهندسی برای دست‌کاری پیوستهٔ ورودی و جست‌وجوی خودکار Failure است. در شکل مدرن آن، Harness → Seed Corpus → Mutation/Generation → Coverage Feedback → Oracle/Sanitizer → Crash Artifact → Triage → Regression یک Loop واحد می‌سازند. بنابراین Fuzzing فقط «فرستادن دادهٔ تصادفی» و فقط «ابزار امنیت» نیست.

در این راهنمای عملی، معماری کمپین، طراحی Fuzz Harness، ساخت Corpus، Fuzzing مبتنی بر پوشش، Sanitizerها و Crash Triage را مرحله‌به‌مرحله می‌سازیم. سپس یک نمونهٔ واقعی با Go Fuzzing، یک سناریوی تسویهٔ ریالی در ایران، طراحی CI و چک‌لیست شروع ارائه می‌شود.

فاز تستینگ چیست؟

Fuzz Testing روشی خودکار است که ورودی‌های فراوان و غیرمنتظره را به یک Target می‌دهد و با سیگنال‌هایی مثل Crash، Panic، Hang، مصرف کنترل‌نشدهٔ منبع، گزارش Sanitizer یا نقض Invariant دنبال باگ می‌گردد. مستندات رسمی Go Fuzzing نیز آن را دست‌کاری مداوم ورودی با هدایت پوشش برای رسیدن به Edge caseهایی می‌داند که انسان ممکن است از قلم بیندازد.

تعریف عملی: Fuzzing یک Search loop بودجه‌دار است؛ ورودی را تغییر می‌دهد، بازخورد اجرای Target را می‌سنجد و نمونه‌های دارای رفتار تازه یا Failure را برای ادامهٔ جست‌وجو و بازتولید نگه می‌دارد.

کلمهٔ «فاز» در فارسی ممکن است با Phase اشتباه شود. در این مقاله «فاز تستینگ» برگردان رایج Fuzz Testing/Fuzzing است و به تولید یا جهش ورودی برای کشف رفتار غیرمنتظره اشاره دارد. «تست فازی» نیز گاهی نوشته می‌شود، اما ارتباطی با منطق فازی ندارد.

فازینگ فقط دادهٔ تصادفی نیست

یک Random generator ساده ممکن است میلیون‌ها Byte بسازد که همه در اولین شرط Parser رد شوند. Coverage-guided fuzzer رفتار اجرای برنامه را می‌بیند؛ اگر یک Mutation Edge یا Comparison تازه‌ای را باز کند، آن را به Corpus کاری اضافه می‌کند و از آن نقطه جلو می‌رود. این حافظهٔ بازخوردی تفاوت میان نویز تصادفی و جست‌وجوی تکاملی است.

در Targetهای ساختاریافته نیز جهش کور ممکن است Magic byte، Length، Checksum یا Syntax را خراب کند و هیچ‌گاه به منطق عمیق نرسد. آنجا Dictionary، Custom mutator، Grammar، Generation یا Structure-aware fuzzing لازم می‌شود. هدف همیشه بیشینه‌کردن تعداد ورودی نیست؛ هدف رسیدن به State و Path پرریسک با Evidence قابل‌تکرار است.

فاز تستینگ چه باگ‌هایی پیدا می‌کند؟

  • Buffer overflow، use-after-free، out-of-bounds و خطاهای حافظه در زبان‌های Native؛
  • Panic، Exception، Assertion و حالت‌های غیرقابل‌دسترسیِ فرض‌شده؛
  • Loop بی‌نهایت، Timeout، ReDoS، Memory blow-up و Decompression bomb؛
  • ناسازگاری Parser و Serializer، Truncation، Integer overflow و Encoding bug؛
  • اختلاف میان دو پیاده‌سازی یا نسخه در Differential testing؛
  • نقض Invariant مثل اثر مالی دوباره، State نامعتبر یا Partial write؛
  • خطای پردازش Format، Protocol، فایل، تصویر، Archive، Query و Message؛
  • مسیرهای Security-relevant که یک ورودی ساخته‌شده می‌تواند فعال کند.

فازینگ به‌تنهایی Authorization bypass، منطق تخفیف اشتباه یا UX بد را تضمین نمی‌کند؛ مگر Harness و Oracle دقیقاً آن رفتار را مشاهده کنند. برای مدل تهدید، کنترل دسترسی و پوشش گسترده‌تر، آن را داخل برنامهٔ تست امنیت نرم‌افزار قرار دهید.

قبل از اجرا؛ مجوز، Scope و مهار اثر جانبی

Fuzzing می‌تواند CPU، RAM، Storage، Queue و Downstream را تحت فشار بگذارد یا دادهٔ مخرب تولید کند. فقط Target متعلق به خودتان یا سامانه‌ای را Fuzz کنید که مجوز صریح آن را دارید. پیش‌فرض امن، اجرای In-process یا Lab ایزوله با دادهٔ مصنوعی است؛ نه API عمومی، Production، درگاه پرداخت یا سرویس شخص ثالث.

کارت Rules of Engagement

فیلد تصمیم لازم
Target/Build تابع، Binary، Commit، Flag و Dependency دقیق
Authorization مالک، بازهٔ مجاز، محیط و روش‌های مجاز
Data فقط Synthetic/Redacted؛ بدون PII، Token و Secret
Side effect Email/SMS، پرداخت، برداشت، Billing و Webhook خاموش یا Stub
Network Egress بسته یا Allowlist؛ Rate و مقصد محدود
Budget CPU، RAM، Disk، Input size، Timeout و زمان کمپین
Stop condition Crash storm، مصرف غیرعادی، نشت داده یا اختلال محیط
Artifact محل امن، TTL، دسترسی، Redaction و روش حذف

Harness خوب باید I/O خارجی را حذف یا شبیه‌سازی کند. اگر تست شبکه ضروری است، Sandbox با Rate limit، Account مصنوعی، Kill switch و مشاهده‌پذیری مستقل بسازید. «چون هدف تست است» مجوز ایجاد هزینه یا اختلال نیست.

معماری یک کمپین Fuzz Testing

  1. Target: API یا کد Production که قرار است اجرا شود.
  2. Harness: ورودی خام را به Target وصل و Environment را کنترل می‌کند.
  3. Seed corpus: مجموعهٔ کوچک ورودی‌های نماینده برای شروع جست‌وجو.
  4. Mutator/Generator: ورودی‌های تازه را با جهش، Dictionary یا ساختار تولید می‌کند.
  5. Instrumentation/Feedback: Edge، Block، Comparison یا سیگنال دیگری را اندازه می‌گیرد.
  6. Executor/Isolation: Run را با Timeout و محدودیت منبع اجرا و Reset می‌کند.
  7. Oracle: Crash، Sanitizer، Hang، اختلاف یا نقض Invariant را تشخیص می‌دهد.
  8. Corpus manager: ورودی‌های دارای پوشش تازه را نگه، Merge و Minimize می‌کند.
  9. Crash pipeline: Artifact را ثبت، Minimize، Deduplicate و Reproduce می‌کند.
  10. Fix/Regression: Root cause را اصلاح و ورودی شکست را به تست پایدار تبدیل می‌کند.

اگر فقط «ابزار را اجرا کنید» اما Target عمیق را صدا نزنید، Corpus بی‌کیفیت باشد یا Artifact قابل Replay نباشد، Dashboard فعال است ولی کمپین ارزش تصمیم‌گیری ندارد.

انواع Fuzzing و زمان استفاده

رویکرد بازخورد/دانش مزیت محدودیت
Black-box random تقریباً بدون Visibility شروع سریع برای Interface بسته گیرکردن در Validation سطحی
Mutation-based Seedهای موجود حفظ بخشی از ساختار واقعی وابسته به کیفیت Corpus
Generation/Grammar-based Schema یا Grammar ساخت Input معتبر و عمیق هزینهٔ مدل و خطر Bias
Coverage-guided grey-box Edge/Block/Cmp feedback جست‌وجوی مسیرهای تازه نیاز به Instrumentation و Build مناسب
Structure-aware AST/Proto/Custom mutator عبور از Parserهای پیچیده پیاده‌سازی و نگهداری بیشتر
Differential اختلاف دو Oracle/Implementation کشف ناسازگاری بدون Expected کامل اختلاف همیشه Bug نیست
Directed/White-box Distance، constraint یا analysis تمرکز روی Sink/Change مشخص هزینه و پیچیدگی تخصصی

این دسته‌ها انحصاری نیستند. یک کمپین می‌تواند Coverage-guided باشد، از Corpus واقعی جهش کند، Dictionary داشته باشد و نتیجه را با پیاده‌سازی مرجع مقایسه کند.

انتخاب Target؛ کجا بیشترین بازده را داریم؟

فازینگ را از سطحی شروع کنید که Exposure، پیچیدگی ورودی و اثر Failure بالا، اما اجرای هر Case ارزان است:

  • Parser/Decoder/Encoder برای JSON، XML، CSV، PDF، تصویر، Archive و Protocol؛
  • Deserializer، Validator، Canonicalizer، Normalizer و Template engine؛
  • Regex، Query parser، Compiler، Interpreter و Conversion library؛
  • Message consumer، File upload، Import/Export و Gateway adapter؛
  • Crypto API و Certificate/Key parser با Oracle درست؛
  • کد تازه‌تغییریافته، Crash-prone یا نزدیک Trust boundary.

ماتریس امتیازدهی Target

معیار سؤال امتیاز نمونه ۱ تا ۵
Exposure ورودی از کاربر/فایل/شبکه می‌آید؟ بیشتر = اولویت بالاتر
Parser complexity Format، nesting، state یا encoding پیچیده است؟ بیشتر = اولویت بالاتر
Impact Crash، RCE، فساد داده یا اثر مالی ممکن است؟ بیشتر = اولویت بالاتر
Reachability Harness می‌تواند کد اصلی را مستقیم صدا بزند؟ بیشتر = شروع آسان‌تر
Speed/isolation هر Run سریع و بدون Side effect است؟ بیشتر = بازده بالاتر
Oracle strength Failure معنادار قابل‌تشخیص است؟ بیشتر = Evidence بهتر

یک UI end-to-end کند را Target اول نکنید اگر همان Parser را می‌توان در حافظه و هزاران بار سریع‌تر اجرا کرد. Harness باریک معمولاً Coverage عمیق‌تر، بازتولید ساده‌تر و Triage ارزان‌تر می‌دهد.

Fuzz Harness چیست و چه قراردادی دارد؟

Harness تابعی است که دادهٔ تولیدشده را می‌گیرد، آن را با کمترین تبدیل لازم به API واقعی می‌رساند و Failure را قابل‌مشاهده می‌کند. راهنمای Good Fuzz Target گوگل توصیه می‌کند Target هر ورودی—including خالی و malformed—را تحمل کند، سریع و تا حد ممکن deterministic باشد، exit() نکند و Global state را حداقل سازد.

قرارداد Harness سالم

  • Entry point واقعی Production را صدا می‌زند، نه یک کپی ساده‌شده از منطق؛
  • هر Byte sequence را بدون Crash خود Harness می‌پذیرد؛
  • روی ورودی یکسان، Build یکسان و State یکسان رفتار تکرارپذیر دارد؛
  • Log، Disk I/O، Thread creation و Initialization تکراری را کم می‌کند؛
  • State میان Iterationها پاک یا Snapshot/Restore می‌شود؛
  • Input size، recursion، allocation و زمان را آگاهانه محدود می‌کند؛
  • Exception عمومی را نمی‌بلعد و Sanitizer/Panic را پنهان نمی‌کند؛
  • هیچ Email، SMS، پرداخت، درخواست خارجی یا دادهٔ ماندگار واقعی نمی‌سازد؛
  • Oracle مشخص دارد؛ «فقط Crash نکرد» در صورت نیاز کافی نیست؛
  • نام، Owner، Build recipe و Corpus version ثبت شده است.

باگ محصول یا باگ Harness؟

اگر Harness قبل از Target روی data[0] بدون بررسی طول دسترسی کند، Crash یافتهٔ محصول نیست. اگر Harness یک Constraint واقعی را اشتباه مدل کند، False signal می‌سازد. Triage باید Stack، نقطهٔ Failure، Entry point و رفتار API واقعی را بررسی کند و یافته را به Product، Harness، Environment یا Duplicate طبقه‌بندی کند.

Seed Corpus؛ کم، متنوع و قابل‌اعتماد

Corpus نقطهٔ شروع و حافظهٔ جست‌وجو است. Seedهای خوب Format و مسیرهای معنی‌دار را معرفی می‌کنند، اما نباید هزاران فایل مشابه و بزرگ باشند. Corpus را از Unit test fixture، نمونهٔ کوچک استاندارد، Regression artifact و ورودی مصنوعی معتبر/نامعتبر بسازید؛ نه Dump Production.

قواعد نگهداری Corpus

  • هر Seed دلیل دارد: Path، Feature، Token، Boundary یا Regression؛
  • نمونهٔ کوچک با Coverage برابر بر نمونهٔ بزرگ ترجیح دارد؛
  • Merge/Cmin ورودی‌های بدون Contribution تازه را حذف می‌کند؛
  • Valid و Invalid، کوتاه و بلند، ASCII و Unicode و Versionهای Format دیده می‌شوند؛
  • Corpus با Target/Build versioned و در CI قابل‌بازیابی است؛
  • Secret، PII، شمارهٔ واقعی، Cookie و سند مشتری در آن نیست؛
  • Crashهای اصلاح‌شده به Regression corpus یا تست مستقل اضافه می‌شوند؛
  • فایل جدید قبل از اشتراک، Redact و License/مالکیتش بررسی می‌شود.

برای ساخت و حاکمیت دادهٔ امن، اصول مدیریت دادهٔ تست و دادهٔ مصنوعی را روی Corpus هم اعمال کنید. Corpus یک Artifact مهندسی است، نه پوشه‌ای بی‌مالک از فایل‌های تصادفی.

Mutation، Dictionary و Grammar چگونه کمک می‌کنند؟

Mutation عمومی

Bit flip، Byte insertion/deletion، block splice، arithmetic change و token replacement روی Seedها اعمال می‌شود. جهش‌های کوچک ساختار نسبی را حفظ می‌کنند و Coverage feedback ورودی مفید را نگه می‌دارد.

Dictionary

برای Keyword، Magic value و Separatorهایی مثل Content-Length، ../، NaN، RIAL: یا Field nameهای Protocol، Dictionary شانس عبور از Comparison را بالا می‌برد. Dictionary باید از Grammar/کد و Risk واقعی بیاید؛ فهرست عظیم Tokenهای نامرتبط Search space را شلوغ می‌کند.

Grammar و Structure-aware fuzzing

وقتی هر Mutation خام Checksum یا Syntax را می‌شکند، Generator می‌تواند AST/Message معتبر بسازد و سپس Fieldهای مهم را تغییر دهد. راهنمای رسمی Structure-aware fuzzing این روش را برای Formatهای پیچیده توضیح می‌دهد. خطر آن Bias مدل است: اگر Grammar فقط Happy path را توصیف کند، Malformed inputهای مهم حذف می‌شوند. معمولاً یک Lane ساختاریافته و یک Lane Byte-level کنار هم بهتر است.

Instrumentation و Coverage Feedback

Instrumentation به Fuzzer می‌گوید کدام قسمت از Control flow اجرا شده است. Edge coverage انتقال میان Blockها را می‌سنجد؛ Block coverage صرفاً اجرای Block را. Comparison/value profiling نیز کمک می‌کند Mutation به Constant یا شرط چندبایتی نزدیک شود. بعضی موتورها Context یا N-gram path را هم استفاده می‌کنند.

Coverage چه چیزی نمی‌گوید؟

  • اجرای یک خط، صحت نتیجه یا مجوز دسترسی را ثابت نمی‌کند؛
  • Coverage بالا با Seed ثابت، Discoverability توسط Mutation را تضمین نمی‌کند؛
  • کد Unreachable یا Feature خاموش مخرج را تحریف می‌کند؛
  • یک Edge ممکن است با State و داده‌های بسیار متفاوت اجرا شود؛
  • Coverage درصدی جای Threat model و Risk coverage را نمی‌گیرد.

Coverage را سیگنال ناوبری و تشخیص Harness blocker بدانید. اگر رشد متوقف شد، سؤال درست این است: Validation، Checksum، Encoding، State setup یا Dependency کدام Path را مسدود کرده است؟ بالا بردن کورِ زمان همیشه پاسخ نیست.

Oracle؛ فازر از کجا می‌فهمد شکست رخ داده؟

Oracle سیگنال نکتهٔ Triage
Crash/Panic/Assertion Process signal، stack یا test failure ریشه می‌تواند Product یا Harness باشد
Sanitizer Memory/UB/Thread report Build، symbol و runtime دقیق حفظ شود
Timeout/Hang عبور از زمان Case Slow path را از deadlock/ReDoS جدا کنید
OOM/resource Memory، allocation، fd یا recursion Limit غیرواقعی False signal نسازد
Differential اختلاف دو implementation/version Oracleها ممکن است هر دو ناقص باشند
Invariant State یا Relation نقض می‌شود قانون باید مستقل و دامنه‌دار باشد
Round-trip decode(encode(x)) ناسازگار Canonicalization/Lossy behavior را لحاظ کنید
No-side-effect ورودی ردشده اثر پایدار ساخته Transaction و Event history را مشاهده کنید

Broad catch در Harness که همهٔ Exceptionها را «ورودی نامعتبر» حساب می‌کند، یافته را می‌بلعد. در مقابل، هر Validation error هم باگ نیست. Oracle باید Expected failureهای قرارداد را از Failure غیرمنتظره جدا کند.

Sanitizerها؛ سیگنال‌هایی فراتر از Crash معمولی

Sanitizer با Instrumentation هنگام Build/Runtime خطاهایی را آشکار می‌کند که شاید همان لحظه Process را متوقف نکنند. مستندات AddressSanitizer در Clang ASan را برای خطاهایی مانند out-of-bounds و use-after-free توضیح می‌دهد و هشدار می‌دهد Runtime آن برای Binary تولیدیِ حساس طراحی نشده است.

Sanitizer نمونهٔ یافته ملاحظه
ASan Heap/stack overflow، use-after-free RAM و زمان بیشتر؛ Build آزمایشگاهی
UBSan Undefined behavior مثل overflow/shift نامعتبر Recovery/Trap mode روی Triage اثر دارد
MSan خواندن Memory مقداردهی‌نشده Dependencyها نیز معمولاً باید instrument شوند
TSan Data race کندتر و مناسب Lane جدا؛ Schedule پیچیده
LSan Memory leak Lifetime و one-time allocation را تفکیک کنید

همهٔ Sanitizerها را بی‌فکر در یک Build جمع نکنید؛ سازگاری، هزینه و هدف هر Lane را بسنجید. راهنمای Fuzzing in Depth در AFL++ نیز اثر قابل‌توجه Sanitizer بر CPU/RAM و لزوم مدیریت instance و resource را مطرح می‌کند. Stack نمادگذاری‌شده، Build ID و ورودی دقیق را کنار گزارش نگه دارید.

مثال واقعی Go Fuzzing؛ کشف تقسیم بر صفر

این مثال در ۲۰ مرداد ۱۴۰۵ با Go ۱.۲۶.۵ روی یک محیط محلی اجرا شده است. Parser ساختگی فقط الگوی هفت‌بایتی RIAL:d! را می‌پذیرد و عدد 1000 را بر رقم تقسیم می‌کند. نسخهٔ معیوب شرط زیر را داشت:

package amount

import "bytes"

func ParseAmount(data []byte) int {
    if len(data) != 7 ||
        !bytes.Equal(data[:5], []byte("RIAL:")) ||
        data[6] != '!' {
        return 0
    }

    digit := int(data[5] - '0')
    if digit < 0 || digit > 9 { // باگ: صفر رد نمی‌شود
        return 0
    }

    return 1_000 / digit
}

Harness و Seed corpus

package amount

import "testing"

func FuzzParseAmount(f *testing.F) {
    f.Add([]byte("RIAL:1!"))
    f.Add([]byte("RIAL:9!"))
    f.Add([]byte("invalid"))

    f.Fuzz(func(t *testing.T, data []byte) {
        _ = ParseAmount(data)
    })
}

Harness عمداً کوچک است: همان تابع Production را مستقیم فراخوانی می‌کند، سه Seed کوتاه دارد و هیچ Network/Disk/Clock در Loop نیست. طبق Tutorial رسمی Fuzzing در Go، Seedها با f.Add وارد Corpus می‌شوند و Fuzz target با داده‌های تولیدشده تکرار می‌شود.

یافتهٔ واقعی

go test -run=^$ -fuzz=FuzzParseAmount -fuzztime=5s

fuzz: minimizing 41-byte failing input file
--- FAIL: FuzzParseAmount
    panic: runtime error: integer divide by zero

Failing input: []byte("RIAL:0!")
Artifact: testdata/fuzz/FuzzParseAmount/345e1a530d9c096a
Executions before failure: 417
Elapsed: حدود 0.02s

ورودی RIAL:0! از نظر Shape بسیار نزدیک Seedهای معتبر بود و Branch اشتباه را فعال کرد. مهم‌تر از Crash، Artifact پایدار است: Go ورودی شکست را در مسیر Regression ذخیره می‌کند تا اجرای معمول تست نیز بعداً آن را بازبینی کند.

اصلاح و Retest

digit := int(data[5] - '0')
if digit <= 0 || digit > 9 {
    return 0
}

return 1_000 / digit

پس از تغییر < 0 به <= 0، همان تست با همان Seedها و Artifact اجرا شد. کمپین پنج‌ثانیه‌ای ۸۷۷٬۰۲۶ اجرا با نرخ تقریبی ۱۷۳٬۷۶۳ اجرا در ثانیه انجام داد، یک ورودی جالب تازه به Corpus افزود و Pass شد.

تفسیر درست: Retest نشان می‌دهد Failure شناخته‌شده روی Build اصلاح‌شده بازتولید نشد و در این بودجه Failure دیگری دیده نشد. این خروجی اثبات نمی‌کند Parser برای همهٔ ورودی‌ها یا همهٔ Oracleها بدون باگ است.

Crash Triage؛ از Artifact تا Root Cause

  1. Freeze evidence: ورودی، hash، Build/Commit، command، engine version، flags، sanitizer و environment را ثبت کنید.
  2. Reproduce: Artifact را روی همان Build و Harness، خارج از Campaign و چندبار اجرا کنید.
  3. Minimize: کوچک‌ترین ورودی‌ای را پیدا کنید که همان Failure signature را حفظ می‌کند.
  4. Symbolize: Stack trace را با Symbol/Debug info همان Build خوانا کنید.
  5. Classify: Product bug، Harness bug، Environment، Expected reject یا Duplicate.
  6. Assess: Reachability، impact، exploitability و وجود Mitigation را جدا از Crashability بررسی کنید.
  7. Fix root cause: فقط Input خاص را Block نکنید؛ invariant یا validation ناقص را اصلاح کنید.
  8. Regression: Artifact حداقلی را به Corpus/Test suite اضافه و Campaign را دوباره اجرا کنید.
  9. Monitor: Signature، Owner، SLA و بازگشت خطا در Versionهای بعدی را پیگیری کنید.

اگر Failure متناوب است، نرخ وقوع، State، Timing و Artifact اولین شکست را پیش از Retry حفظ کنید. راهنمای بازتولید باگ متناوب و Cannot Reproduce برای ساخت ماتریس شواهد و شرط Reopen مناسب است.

Minimization و Deduplication؛ با احتیاط

Minimize input

Minimizer Byteها یا ساختارهای غیرضروری را حذف می‌کند تا ورودی کوچک‌تر با همان Failure باقی بماند. این کار Root cause را خواناتر و Regression را سریع‌تر می‌کند. «حداقل» نسبت به الگوریتم، زمان و Signature است و لزوماً Global minimum نیست.

Deduplicate crash

صدها ورودی ممکن است یک Root cause را فعال کنند. Stack hash، top frames، sanitizer type و behavior برای Grouping مفیدند، اما Hash برابر/متفاوت حکم قطعی نیست: Inlining، ASLR، Build یا نقطهٔ مشاهده می‌تواند Signature را تغییر دهد؛ دو Root cause نیز ممکن است در یک Sink مشترک Crash کنند. نمونه‌های خام را تا پایان Triage دور نریزید.

Crashability با Exploitability یکی نیست

هر Crash امنیتی نیست و هر آسیب‌پذیری هم Crash نمی‌دهد. شدت باید از Reachability واقعی، کنترل مهاجم، اثر بر Confidentiality/Integrity/Availability، Sandbox و Deployment تعیین شود. «تعداد Crash» KPI امنیت نیست.

تفاوت Fuzzing با Unit، PBT، SAST و Penetration Testing

رویکرد ورودی/مدل Oracle غالب خروجی متمایز
Unit/Example مثال ثابت و Expected دقیق Assertion مشخص رفتار خوانا و سریع
Property-Based Testing Generator/Domain و Property Invariant/Relation/Model Counterexample و Shrink
Fuzzing Corpus/Mutation/Grammar + feedback Crash/Sanitizer/Hang/Invariant Artifact و مسیر غیرمنتظره
SAST کد/IR بدون اجرای ورودی Rule/Data flow هشدار محل‌محور
Penetration testing Threat/Technique و سیستم یکپارچه اثر قابل‌اثبات Attack path و ریسک

تست مبتنی بر ویژگی معمولاً قانون Domain و Shrink معنایی را مالک است؛ Fuzzing معمولاً Harness، Corpus، Coverage feedback، Sanitizer و Crash pipeline را. می‌توان Property را داخل Fuzz target به‌عنوان Oracle اجرا کرد. تحلیل استاتیک کد برای QA نیز مکمل است: SAST مسیر مشکوک را پیشنهاد می‌کند و Directed fuzzing می‌تواند Reachability آن را در Runtime بیازماید.

Fuzzing API و شبکه؛ In-process اولویت دارد

اگر API در نهایت JSON/XML/Header را به Parser داخلی می‌دهد، ابتدا همان Parser را In-process Fuzz کنید؛ سریع‌تر، deterministicتر و بی‌اثرتر است. سپس Integration lane محدود برای Routing، Proxy، Auth middleware و Serialization بسازید. تست شبکه باید در Lab با Account و Downstream مصنوعی اجرا شود.

Guardrailهای API fuzzing

  • Base URL ثابت و Allowlist‌شده؛ DNS و Redirect محدود؛
  • Rate، concurrency، timeout، payload و response size سقف‌دار؛
  • Credential کم‌اختیار و کوتاه‌عمر؛ بدون Token در Artifact؛
  • Endpointهای Billing، Delete، SMS، Email و Payment Stub یا Block؛
  • Correlation ID per case و Cleanup قابل‌اعتماد؛
  • تفکیک Expected 4xx از 5xx، hang، policy bypass و partial effect؛
  • SSRF/XXE sink بدون Egress آزاد و فقط با Callback داخلی مجاز.

برای Payloadهایی که ممکن است URL یا Entity خارجی بسازند، کنترل‌های تست امن SSRF و XXE را رعایت کنید. Fuzzer را روی سامانهٔ ثالث یا مقصد اینترنتی رها نکنید.

Stateful و Protocol Fuzzing

بعضی Failureها با یک Byte string پیدا نمی‌شوند؛ به Sequence نیاز دارند: connect → authenticate → create → retry → cancel. Stateful fuzzer Command و Transition تولید می‌کند و Harness وضعیت را میان Stepهای یک Case حفظ، اما میان Caseها Reset می‌کند.

اجزای یک Harness حالت‌مند

  • State model ساده و Precondition هر Command؛
  • Grammar پیام و Correlation/Sequence number؛
  • Snapshot/restore یا Namespace تازه برای هر Case؛
  • حداکثر طول Sequence و Timeout کل؛
  • Oracle پس از هر Step و در State نهایی؛
  • Shrinker یا Minimizerی که Sequence معتبر را حفظ کند؛
  • Replay شامل Seed، command list، timing inputs و Build.

در این سطح، Fuzzing با تست یکپارچه‌سازی هم‌مرز می‌شود. قرارداد سرویس‌ها، خطاهای Retry و ترتیب Event باید با Stub و State history مشاهده شوند.

سناریوی ایرانی؛ Fuzzing فایل تسویهٔ ریالی

فرض کنید سامانهٔ یک PSP ساختگی فایل ZIP شامل CSV تسویه را می‌گیرد و هر ردیف را به Ledger آزمایشگاهی می‌فرستد. Target مناسب، Parser و Validator داخل حافظه است؛ نه پنل واقعی PSP. همهٔ شناسه‌ها و شماره‌ها مصنوعی‌اند.

Seed corpus پیشنهادی

  • یک فایل معتبر با مبلغ IRR و تاریخ UTC؛
  • رقم لاتین، فارسی و عربیِ معادل؛
  • UTF-۸ و نمونهٔ مصنوعی Windows-۱۲۵۶؛
  • عنوان دارای نیم‌فاصله، RTL mark و فاصلهٔ ابتدا/انتها؛
  • تاریخ جلالیِ مجاز در Field مشخص و تاریخ نامعتبر؛
  • مبلغ صفر، منفی، بسیار بزرگ و جداکنندهٔ هزارگان؛
  • ردیف تکراری با شناسهٔ تسویهٔ یکسان؛
  • CSV کوتاه، Quote ناقص، ستون اضافه و newline متفاوت؛
  • ZIP کوچک معتبر و Archive با نسبت فشرده‌سازی غیرعادیِ کنترل‌شده.

Oracleهای کسب‌وکاری و امنیتی

  • Parser هرگز Crash یا allocation نامحدود نکند؛
  • IRR با تومان یا Decimal شناور مخلوط نشود؛
  • رقم‌های هم‌معنا Canonical شوند، اما متن مبهم Silent تبدیل نشود؛
  • جمع ردیف‌های پذیرفته‌شده با Summary فایل سازگار باشد؛
  • ردیف Invalid هیچ اثر نیمه‌کاره در Ledger نسازد؛
  • Retry با Settlement ID یکسان اثر مالی تکراری نسازد؛
  • Timeout پس از Commit با Idempotency و State history قابل‌تشخیص باشد؛
  • Path traversal، XXE، Formula injection و Zip bomb در Boundary مهار شوند؛
  • Error message دادهٔ حساس یا محتوای کامل فایل را Log نکند.

برای Riskهایی مثل اثر مالی تکراری، صرف Crash oracle کافی نیست؛ Harness باید Fake ledger و Event history داشته باشد و Invariant را پس از هر Run بسنجد. داده و Artifact هم باید ساختگی و دسترسی‌محدود بمانند.

Fuzzing در CI/CD؛ سه Lane با بودجه متفاوت

Lane زمان/Trigger Scope Policy
PR smoke هر تغییر، ثانیه تا چند دقیقه Regression corpus + Targetهای تغییرکرده Crash پایدار Merge را Block کند
Nightly شبانه، بودجهٔ عمیق‌تر Targetهای Tier ۱ و Sanitizer laneها Artifact خودکار + Owner/Triage SLA
Continuous service ساعت/روز روی Coreهای جدا Corpus sharing، چند engine/build Dedup، retention، quota و escalation
Release gate پیش از انتشار Regression known + risk target عدم Crash شناخته‌شده؛ waiver مستند

در PR لازم نیست Campaign بی‌پایان اجرا شود؛ Replay همهٔ Artifactهای قبلی و یک Budget کوتاه روی Target تغییرکرده ارزش بیشتری دارد. مقالهٔ تست مداوم در CI/CD برای طراحی Feedback سریع، Quality gate و Ownership مکمل این بخش است.

ClusterFuzzLite نمونه‌ای برای واردکردن Fuzzing در CI است؛ OSS-Fuzz نیز سرویس Fuzzing پیوسته برای پروژه‌های متن‌باز واجد شرایط ارائه می‌کند. انتخاب سرویس جای طراحی Harness و Triage process را نمی‌گیرد.

انتخاب ابزار Fuzz Testing

اکوسیستم/نیاز گزینهٔ رایج نقطهٔ تصمیم
Go Native go test -fuzz Integration با testing و Artifact regression
C/C++ in-process libFuzzer-compatible harness سرعت، Sanitizer و API حافظه‌ای
C/C++/binary و چند mode AFL++ Instrumentation، forkserver، Corpus و parallel campaign
JVM Jazzer JUnit integration و coverage-guided JVM
Python Atheris/Hypothesis target Extension/native boundary و Oracle معنایی
Rust cargo-fuzz/libFuzzer ecosystem Target crate و Sanitizer support
Format ساختاریافته Custom mutator/grammar/Proto عبور از Syntax و حفظ malformed lane

مستندات LLVM libFuzzer آن را یک موتور in-process، coverage-guided و تکاملی معرفی می‌کند؛ همان صفحه در وضعیت فعلی می‌گوید نگهداری برای رفع باگ‌های مهم ادامه دارد، اما توسعهٔ قابلیت‌های عمده دیگر محور اصلی نیست. بنابراین API سازگار آن هنوز در پروژه‌های موجود مهم است، ولی انتخاب امروز باید با اکوسیستم، Maintainer activity، CI، Platform و نیاز کمپین سنجیده شود—نه با یک فهرست قدیمی ابزار.

Fuzz Introspector و پیدا کردن مانع Coverage

گاهی Dashboard می‌گوید ساعت‌ها Fuzzing انجام شده، اما Harness فقط Wrapper سطحی را اجرا کرده است. Fuzz Introspector در OSS-Fuzz برای تحلیل سطح دسترسی Fuzz target، Coverage و Blockerهای احتمالی به کار می‌رود. خروجی آن را برای سؤال‌های زیر استفاده کنید:

  • کدام Function پرریسک از هیچ Harnessی قابل‌دسترسی نیست؟
  • کدام Targetها هم‌پوشانی زیاد و Gap مشترک دارند؟
  • Validation یا Call graph کجا جست‌وجو را متوقف کرده است؟
  • آیا Target باید باریک‌تر، Seed بهتر یا Dictionary/structure awareness بگیرد؟

گزارش Introspection حقیقت امنیتی نهایی نیست؛ راهنمای سرمایه‌گذاری روی Harness است و باید با Threat model، Code review و Failureهای واقعی تفسیر شود.

متریک‌های غیرقابل‌بازی برای کمپین Fuzzing

متریک پرسش تصمیم دام
Executions/sec Harness کند یا I/O-bound است؟ مقایسهٔ Targetهای متفاوت بی‌معناست
Coverage growth/plateau جست‌وجو هنوز Path تازه می‌یابد؟ درصد بالا صحت را ثابت نمی‌کند
Corpus contribution هر Seed چه Coverage/Feature تازه‌ای دارد؟ اندازهٔ خام Corpus ارزش نیست
Reachability gap کد پرریسک بدون Target مانده؟ همهٔ کد وزن یکسان ندارد
Reproducible unique root causes چند باگ مستقل و قابل‌اقدام داریم؟ Crash count و stack hash خام گمراه می‌کند
Flaky artifact rate Harness/Environment پایدار است؟ حذف Artifact به‌جای رفع ناپایداری
Time to triage/fix/regression Pipeline یافته را به اصلاح تبدیل می‌کند؟ سرعت بدون شدت/کیفیت
Sanitizer/root-cause mix کدام Lane و Component بازده دارد؟ KPI فردی یا رقابت تعداد باگ

Exec/s یک Diagnostic است: ۱۷۳ هزار اجرا در ثانیه برای Parser هفت‌بایتی این مثال خوب است، ولی با Stateful protocol یا Image decoder قابل‌مقایسه نیست. معیار نهایی، کاهش Risk با یافتهٔ بازتولیدپذیر و Regression پایدار است.

چرا Coverage بالا می‌تواند احساس امنیت کاذب بدهد؟

فرض کنید Seed معتبرِ رمزنگاری‌شده تقریباً همهٔ Decoder را اجرا می‌کند، اما کوچک‌ترین Mutation در Header رد می‌شود. Seed coverage بالا است، ولی Fuzzer توان کشف مسیرهای تازه را ندارد. یا Harness تمام Branchهای Parser را می‌زند، اما هیچ Assertion دربارهٔ اثر مالی ندارد. در هر دو حالت عدد پوشش سبز است و Risk اصلی دیده نمی‌شود.

سه آزمون سلامت Coverage

  1. Empty/minimal corpus run: موتور بدون Seedهای قوی تا کجا پیش می‌رود؟
  2. Corpus-only coverage: Seedها چه Pathهایی را پوشش می‌دهند و کدامشان Contribution ندارند؟
  3. Discoverability experiment: آیا Dictionary، split target یا structure-aware lane Gap را واقعاً کم می‌کند؟

Budget و نتیجهٔ هر آزمایش را ثبت کنید. تغییر Harness باید با Coverage diff، exec/s، Stability و کیفیت Artifact سنجیده شود، نه با احساس.

اشتباه‌های رایج در فاز تستینگ

  • «Random input بفرست»: بدون feedback، Corpus و Oracle، بیشتر Runها سطحی‌اند.
  • Target بسیار بزرگ: Initialization و Pathهای نامرتبط بودجه را می‌خورند.
  • Harness غیرواقعی: Wrapper کپی‌شده باگ دارد یا Entry point اصلی را دور می‌زند.
  • بلعیدن Exception: Catch عمومی همهٔ Failureها را Expected می‌کند.
  • Corpus Production: PII/Secret وارد Artifact و CI می‌شود.
  • Network آزاد: SSRF، هزینه، Rate abuse یا اثر جانبی روی ثالث ایجاد می‌شود.
  • بدون Limit: یک Input، RAM/Disk/CPU کل Runner را می‌گیرد.
  • Crash count KPI: Duplicateها پاداش می‌گیرند و Root cause فراموش می‌شود.
  • Coverage مساوی Quality: Oracle و Risk coverage نادیده می‌ماند.
  • حذف Artifact بعد از Fix: Regression آینده راه بازگشت ندارد.
  • Fuzz فقط قبل Release: زمان Triage و اصلاح دیر است.
  • Pass مساوی امن: Budget محدود به ادعای مطلق تبدیل می‌شود.

برنامهٔ ۳۰روزه برای شروع Fuzzing

هفتهٔ اول؛ Scope و اولین Target

  • Trust boundaryها و Parserهای ورودی را فهرست و امتیازدهی کنید.
  • یک Target باریک، سریع و پراثر انتخاب کنید.
  • Rules of Engagement، Owner، Budget و محل Artifact را تصویب کنید.
  • سه تا ده Seed کوچک و کاملاً مصنوعی بسازید.

هفتهٔ دوم؛ Harness و Oracle

  • Entry point واقعی را In-process وصل کنید.
  • Side effect و Network را Stub/Block کنید.
  • Crash، Timeout، resource و حداقل یک Invariant را مشاهده‌پذیر کنید.
  • Determinism، reset، input limit و exec/s را Baseline بگیرید.

هفتهٔ سوم؛ Campaign و Triage drill

  • PR smoke و Nightly را با Budget مشخص اجرا کنید.
  • Sanitizer lane مرتبط را جدا بسازید.
  • یک Artifact عمدی را Reproduce، Minimize و به Regression تبدیل کنید.
  • Corpus merge و Retention policy را آزمایش کنید.

هفتهٔ چهارم؛ Scale با شواهد

  • Coverage plateau و unreachable risk را بررسی کنید.
  • در صورت نیاز Dictionary، Target split یا structure-aware lane اضافه کنید.
  • Dashboard را روی root cause، reproducibility و triage time تنظیم کنید.
  • Target دوم را فقط پس از داشتن Owner و ظرفیت Triage وارد کنید.

چک‌لیست نهایی فاز تستینگ

  • Target و Risk روشن، باریک و اولویت‌بندی شده‌اند.
  • مجوز، محیط، Build و Stop condition ثبت شده است.
  • Network و Side effect خارجی بسته یا شبیه‌سازی شده‌اند.
  • Corpus کوچک، متنوع، مصنوعی، Versioned و بدون Secret است.
  • Harness Entry point واقعی، سریع، deterministic و Resetپذیر است.
  • Input size، Timeout، CPU، RAM، Disk و concurrency محدودند.
  • Crash، Sanitizer، Hang و Invariantهای لازم Oracle دارند.
  • Coverage برای ناوبری و Gap analysis استفاده می‌شود، نه ادعای صحت.
  • Dictionary/Grammar فقط برای مانع واقعی اضافه شده‌اند.
  • Artifact شامل Input، Build، Flags، Engine و Stack قابل‌بازتولید است.
  • Minimization و Dedup نمونهٔ خام را زود حذف نمی‌کنند.
  • Product/Harness/Environment/Duplicate در Triage جدا می‌شوند.
  • شدت از Reachability و Impact می‌آید، نه صرف Crash.
  • Fix با Artifact قبلی، Regression و Campaign مجدد تأیید می‌شود.
  • PR، Nightly و Continuous lane بودجه و Policy جدا دارند.
  • Owner، SLA، Retention و معیارهای غیرقابل‌بازی تعریف شده‌اند.

سوالات متداول Fuzz Testing

آیا فاز تستینگ همان تست تصادفی است؟

نه. Random input می‌تواند بخشی از تولید داده باشد، اما Fuzzing مدرن معمولاً Corpus، Mutation، Coverage/behavior feedback، Oracle و Crash artifact دارد. ارزش اصلی از Loop جست‌وجو و بازتولید می‌آید، نه صرفاً تصادفی‌بودن.

چقدر باید Fuzzing را اجرا کنیم؟

عدد جهانی وجود ندارد. PR ممکن است چند ثانیه تا چند دقیقه Regression و Target تغییرکرده را اجرا کند؛ Nightly و Continuous بودجهٔ بزرگ‌تر دارند. تصمیم را از Risk، سرعت Target، رشد Coverage، نرخ یافته، Plateau و ظرفیت Triage بگیرید و Budget را کنار نتیجه گزارش کنید.

آیا Pass شدن Fuzz test یعنی نرم‌افزار امن است؟

خیر. Pass فقط می‌گوید در Build، Harness، Oracle، Corpus و بودجهٔ ثبت‌شده Failure مشاهده نشد. Threat model، منطق مجوز، Dependency، Configuration و Pathهای خارج از Reach همچنان به روش‌های دیگر نیاز دارند.

Corpus خوب چند فایل دارد؟

تعداد ثابت مهم نیست. Corpus خوب کمینه و متنوع است: هر Seed Path، Feature، Boundary یا Regression تازه‌ای معرفی می‌کند. Merge/minimize باید نمونه‌های بدون Contribution را حذف کند و هیچ دادهٔ شخصی یا Secret در Corpus نباشد.

با هر Crash چه کنیم؟

Input و Build/Flags را Freeze کنید، روی همان محیط بازتولید و Minimize کنید، Stack را Symbolize و علت را Product/Harness/Environment/Duplicate طبقه‌بندی کنید. سپس Reachability/Impact را بسنجید، Root cause را اصلاح و Artifact حداقلی را به Regression تبدیل کنید.

منابع و یادداشت بازبینی

تعریف و رفتار Go Fuzzing با مستندات رسمی Go، معماری coverage-guided با LLVM/libFuzzer، کمپین و Corpus با AFL++، Sanitizer با Clang، Structure-aware target با راهنمای گوگل و CI/Introspection با مستندات OSS-Fuzz تطبیق داده شد. مخزن راهنمای عمومی google/fuzzing از ۶ دی ۱۴۰۴ آرشیو و Read-only شده است؛ لینک‌های آن در این مقاله برای اصول پایدار طراحی Harness و Structure-aware fuzzing استفاده شده‌اند، نه برای ادعای وضعیت ابزارهای امروز.

مثال Go این مقاله واقعاً با نسخهٔ معیوب و اصلاح‌شده اجرا شد؛ اعداد ۴۱۷ و ۸۷۷٬۰۲۶ گزارش همان اجرا هستند و Benchmark عمومی محسوب نمی‌شوند. آخرین بازبینی محتوایی: ۲۰ مرداد ۱۴۰۵.

جمع‌بندی: فاز تستینگ موفق با نصب ابزار شروع نمی‌شود؛ با Target پرریسک، Harness واقعی و مهارشده، Corpus هدفمند و Oracle قابل‌اعتماد شروع می‌شود. Coverage مسیر را نشان می‌دهد، Sanitizer Failure پنهان را آشکار می‌کند و Crash Triage یافته را به Root cause و Regression تبدیل می‌کند. اگر Artifact، Owner و Retest ندارید، فقط ورودی تولید کرده‌اید؛ هنوز یک چرخهٔ Fuzz Testing قابل‌اتکا نساخته‌اید.

دیدگاهتان را بنویسید