قابلیت ترمیم خودکار Semgrep

در این نوشتار می‌خواهم در مورد قابلیت ترمیمِ خودکار (autofix) کُد Semgrep صحبت کنم. این قابلیت آزمایشی به ما امکان می‌دهد که بعد از یافتن نتایج در فایلهای منبع، آنها را به صورت اتوماتیک دستکاری کنیم.مثل همیشه میتوانید کپی این نسخه را در بلاگ فارسی من بخوانید:https://parsiya.github.io/parsiya.fa/post/2022/semgrep-autofix/اگر علاقه به خواندن نسخه انگلیسی دارید:https://parsiya.net/blog/2021-10-25-a-hands-on-intro-to-semgreps-autofix/همه نمونه‌ها در Semgrep playground هستند ولی می‌توانید آنها را آفلاین نیز اجرا کنید:https://github.com/parsiya/Parsia-Code/tree/master/semgrep-autofixپیش‌نیازهای این بلاگبرای استفاده بهینه از این بلاگ، بهتر است این موارد را بدانید:استفاده از Semgrep و نوشتن rule (قانون؟). چرا از Semgrep برای Static Analysis استفاده کنیم؟ را بخوانید.آشنایی سَرسَری با خواندن کدُ Java، Python و Go.آشنایی مختصر با بعضی مفاهیم امنیت مانند HttpOnly و XSS.نکته: ترمیمِ خودکار، یکی از قابلیت‌های آزمایشی Semgrep است. در زمان نگارش (آوریل 2022 برای نسخه فارسی و اکتبر 2021 برای نسخه انگلیسی) همه مثال‌ها درست هستند. اگر از آینده میایید، شاید بعضی چیزها متفاوت باشند.اجرای قوانینبرای اجرا و تست قوانین دو راه داریم: استفاده از Semgrep playground در آدرس https://semgrep.dev/playground. من بیشتر از این روش استفاده می‌کنم چون راحت و سریع می‏‌توانم قانون را عوض کنم و نتیجه را ببینم.اجرا در خطِ فرمان (ترجمه فارسی command line).بعد از نوشتن یک قانون می‌توانید درستی ساختار فایل yaml آن را تست کنید:semgrep-crule1.yaml--validate اگر یک قانون دارای بخش autofix باشد، هنگام اجرا فایل اصلی دستکاری نمی‌شود ولی می‌توانید تغییرات را ببینید. برای تغییر فایل اصلی از سویچ autofix--استفاده کنید. برای مشاهده تغییرات بدون تغییر فایل اصلی از سویچ dry-run-- استفاده کنید (حضور همزمان autofix و dry-run معادل نبودِ autofix است یعنی تغییرات فقط نشان داده می‌شوند):semgrep -c rule1.yaml example.java --autofix --dryrun استفاده از سویچ‌های مختلفروش‌های استفاده از autofixدو روش مختلف برای استفاده از autofix داریم:fixfix-regexفرمان Fixهر چیزی که قانون پیدا کرده است را با مقدار این فرمان جایگزین میکنیم. بهترین کاندیدها برای این قابلیت، توابع یا رشته (string) های ناامنی هستند که باید با معادل امن جایگزین شوند. چند مثال با این دستور ببینیم.Python - sys.exitاین مثال را از آموزش Semgrep در آدرس (https://semgrep.dev/s/R6g) قرض گرفته‌ام. در این قانون ما به دنبال تابع exit هستیم و می‌خواهیم آن را با sys.exit جایگزین کنیم. Semgrep تفاوت تابع exit با دیگر استفاده‌های این کلمه را در کُد می‌فهمد و می‎‌تواند راحت آن را جایگزین کنید. بعد از اجرای قانون در وب‌سایت بالا می بینید که دکمه Apply Fix فعال شده و می‌توانید به صورت خودکار کد برنامه را دستکاری کنید.تغییر اتوماتیک exit به sys.exitیکی از نکات جالب این است که می‌توانیم پارامتر تابع را توسط یک metavariable (در اینجا با نام X) در قانون ذخیره کنیم و در autofix استفاده کنیم. دیگر لازم نیست حتی نگران مقدار پارامتر باشیم.Java - CBC Padding Oracleبرای این مثال، قانون cbc-padding-oracle زبان جاوا را از رجیستری Semgrep دستکاری کرده‌ام تا راحت‌تر خوانده شود.# java-cbc-padding-oracle/cbc-padding-oracle.yaml rules: - id: cbc-padding-oracle severity: WARNING message: Match found languages: - java pattern: $CIPHER.getInstance(&quot=~/.*\/CBC\/PKCS5Padding/&quot) fix: $CIPHER.getInstance(&quotAES/GCM/NoPadding&quot)این قانون به دنبال هر چیزی که شبیه object.getInstance("string")باشد می‌گردد و در صورتی که رشته داخل شامل CBC/PKCS5Paddingباشد، نتیجه را گزارش می‌دهد. این قانون از قابلیت قدیمی string matching استفاده می‌کند که دیگر پشتیبانی نمی‌شود. برای دست‌گرمی باید آن را با metavariable-regex جایگزین کنیم. این کار ساده است، اول یک metavariable به عنوان پارامتر تعریف می‌کنیم (در اینجاINS$) و بعد regex را روی آن اجرا می‌کنیم و نتیجه همان است:https://semgrep.dev/s/parsiya:java-cbc-padding-oracle-metavariable-regex# java-cbc-padding-oracle/cbc-padding-oracle-metavariable-regex.yaml rules: - id: cbc-padding-oracle-metavariable-regex message: Match found languages: - java severity: WARNING patterns: - pattern: $CIPHER.getInstance($INS) - metavariable-regex: metavariable: $INS regex: .*\/CBC\/PKCS5Padding fix: $CIPHER.getInstance(&quotAES/GCM/NoPadding&quot)مقدار "محاسبه" قانون جدید در playground عدد بزرگتری بود و من خواستم آن را آزمایش کنم. قانون جدید خیلی پیچیده‌تر از قانون قدیمی نیست.$ multitime -q -n 50 ./cbc-padding-oracle.sh ===> multitime results 1: -q ./cbc-padding-oracle.sh Mean Std.Dev. Min Median Max real 0.781 0.006 0.773 0.780 0.806 user 0.501 0.041 0.406 0.500 0.609 sys 0.256 0.044 0.172 0.258 0.359$ multitime -q -n 50 ./cbc-padding-oracle-metavariable-regex.sh ===> multitime results 1: -q ./cbc-padding-oracle-metavariable-regex.sh Mean Std.Dev. Min Median Max real 0.788 0.007 0.778 0.786 0.813 user 0.516 0.047 0.406 0.516 0.609 sys 0.247 0.048 0.156 0.250 0.359 این مقدار محاسبه و عددی که در playground نشان داده می‌شود بحثی است که بعدا باید به آن بپردازم. خلاصه بگم، معمولاً نباید نگران عملکرد قانون خود باشید. مقدار زیادی از وقت Semgrep صرف خواندن و پردازش فایل‌ها و سپس درست کردن Abstract Syntax Tree (AST) کُد می‌شود. خیلی regex های پیچیده ننویسید اما دیگر نگران ذره ذره مسائل هم نباشید. برای دیدن زمانی که صرف هر بخش شده از سوییچ time--استفاده کنید.Java - HttpOnly Cookiesمی‌خواهیم چک کنیم که آیا کوکی ما دارای خصوصیت HttpOnlyاست و اگر نیست آن را درست کنیم. من قانون cookie-missing-httponly جاوا را خلاصه کرده‌ام:# java-httponly/httponly-practice.yaml rules: - id: cookie-missing-httponly message: Match found severity: WARNING languages: [java] patterns: - pattern-not-inside: $COOKIE.setValue(&quot&quot); ... - pattern-either: - pattern: $COOKIE.setHttpOnly(false); - patterns: - pattern-not-inside: $COOKIE.setHttpOnly(...); ... - pattern: $RESPONSE.addCookie($COOKIE);این قانون (https://semgrep.dev/s/parsiya:java-httponly-practice) چک می‌کند که آیا:ما به صورت دستی مقدار HttpOnly را false کرده‌ایم. در این صورت باید مقدار false را به true تغییر دهیم.یک کوکی به response (پاسخ؟) اضافه کرده‌ایم ولی HttpOnly را سِت نکرده‌ایم. در این صورت باید مقدار HttpOnly را به true تغییر دهیمبرای رفع این مشکل باید این قانون را به این دو بخش بشکانیم و دو قانون مجزا درست کنیم زیرا بخش fix برای همه قوانین اجرا می‌شود ولی ما دو نوع ترمیم مختلف داریم.ترمیم HttpOnly اولدر این ترمیم تنها باید مقدار false در ;COOKIE.setHttpOnly(false)$را با true جایگزین کنیم (برای تمرین از https://semgrep.dev/s/parsiya:java-httponly-practice-1 استفاده کنید):# java-httponly/httponly-practice-1.yaml rules: - id: cookie-missing-httponly-1 message: Match found severity: WARNING languages: [java] patterns: - pattern-not-inside: $COOKIE.setValue(&quot&quot); ... - pattern: $COOKIE.setHttpOnly(false); fix: $COOKIE.setHttpOnly(true);semgrep -c httponly-practice-1.yaml httponly.javaترمیم HttpOnly دومدر این بخش ما ;RESPONSE.addCookie($COOKIE)$را می‌بینیم اما خبری از setHttpOnlyنیست. باید آن را اضافه کنیم. این قانون (https://semgrep.dev/s/parsiya:java-httponly-practice-2) این کار را انجام می‌دهد اما کد اضافه شده "خوشگل" نیست (برای جاوا فاصله و غیره مهم نیستند و شاید بگویید که این قانون برای من کافی است زیرا ابزارهای دیگری اتوماتیک کُد شما را فُرمَت می‎‌کنند).کُدِ زشتما می‌توانیم این مشکل را با فرمان fix-regex حل کنیم.فرمان fix-regexهمانطور که دیدیم برای جابجایی‌های ساده (مثلا تغییر badFuncبه goodFunc) فرمان fix کافی است. اما fix-regex به ما قدرت مانور بیشتری می‌دهد.https://semgrep.dev/docs/experiments/overview/#autofix-with-regular-expression-replacementاین فرمان سه بخش دارد: بخش regex که regular فارسیش واقعا چی میشه؟ اصلاً معادل داره؟) را روی بخشی از کد که توسط قانون پیدا شده است، اجرا می‌کند.بخش replacement: چیزی که باید با بخش بالا جایگزین شود.بخش count: تعداد جایگزینی‌ها.نکته: در حال حاضر (آوریل 2022) Semgrep از metavariable در fix-regex پشتیبانی نمی‌کند.با استفاده از fix-regex می‌توانیم قانون قبلی را دوباره بنویسیم. برای این کار اول باید ببینیم که چه چیزی capture می‌شود. این قانون را می‌توانید در https://semgrep.dev/s/parsiya:java-httponly-fix-regex-practice اجرا کنید.# java-httponly/httponly-fix-regex-practice.yaml rules: - id: cookie-missing-httponly-fix-regex-practice message: Match found severity: WARNING languages: - java patterns: - pattern-not-inside: $COOKIE.setValue(&quot&quot); ... - pattern-not-inside: $COOKIE.setHttpOnly(...); ... - pattern: $RESPONSE.addCookie($COOKIE); fix-regex: regex: (.*) replacement: //\1نتیجه اجرای قانون بالادر جواب، //را دو بار می‌بینیم. چون regex ما greedy (حریص) است. برای این کار باید از فیلد count با مقدار یک استفاده کنیم که فقط اولین جایگزینی انجام شود.اجرای قانون با count برابر یکولی هنوز کُدِ خوشگل نداریم. برای این کار در بخش regex باید فاصله بین ابتدای خط تا اول کُد را capture کنیم. بعد در بخش replacement اول فاصله را جایگزین کنیم (1\) و بعد // و در انتها خود کُد (2\). fix-regex: regex: (\s*)(.*) replacement: \1// \2 count: 1حالا باید در خط جدید، کُدِ درست را وارد کنیم. اگر می‌توانستیم از metavariable ها استفاده کنیم کار ما خیلی راحت‌تر بود اما ترمیم پایین درست اجرا نمی‌شود. این را در https://semgrep.dev/s/parsiya:java-httponly-fix-regex-practice-2 می‌توانید امتحان کنید.# java-httponly/httponly-fix-regex-practice-2.yaml fix-regex: regex: (\s*)(.*) replacement: | \1$COOKIE.setHttpOnly(true); \1\2 count: 1مقدار COOKIE$ در ترمیم جایگزین نشده استبرای این کار باید از قانون زیر استفاده کنیم (واقعا حوصله توضیح دوباره آن را ندارم ?):# java-httponly/httponly-fix-regex-practice-final.yaml rules: - id: cookie-missing-httponly-fix-regex-practice-final message: Match found severity: WARNING languages: - java patterns: - pattern-not-inside: $COOKIE.setValue(&quot&quot); ... - pattern-not-inside: $COOKIE.setHttpOnly(...); ... - pattern: $RESPONSE.addCookie($COOKIE); fix-regex: regex: (\s*)(.*addCookie\((.*)\).*) replacement: | \1\3.setHttpOnly(true); \1\2 count: 1بقیه بلاگ دیگر چیز جدیدی به شما یاد نمی‎‌دهد. اما می‌توانید در بلاگ انگلیسی بخوانید.چه یاد گرفتم؟قابلیت ترمیم خودکار Semgrep بسیار جالب است. من از آن برای اضافه کردن comment به کد استفاده می‌کنم تا هنگام مرور آن بدانم هر بخش چه مشکلی دارد.