پارسیا
پارسیا
خواندن ۹ دقیقه·۳ سال پیش

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

در این نوشتار می‌خواهم در مورد قابلیت ترمیمِ خودکار (autofix) کُد Semgrep صحبت کنم. این قابلیت آزمایشی به ما امکان می‌دهد که بعد از یافتن نتایج در فایلهای منبع، آنها را به صورت اتوماتیک دستکاری کنیم.

مثل همیشه میتوانید کپی این نسخه را در بلاگ فارسی من بخوانید:

اگر علاقه به خواندن نسخه انگلیسی دارید:

همه نمونه‌ها در Semgrep playground هستند ولی می‌توانید آنها را آفلاین نیز اجرا کنید:

پیش‌نیازهای این بلاگ

برای استفاده بهینه از این بلاگ، بهتر است این موارد را بدانید:

  1. استفاده از Semgrep و نوشتن rule (قانون؟). چرا از Semgrep برای Static Analysis استفاده کنیم؟ را بخوانید.
  2. آشنایی سَرسَری با خواندن کدُ Java، Python و Go.
  3. آشنایی مختصر با بعضی مفاهیم امنیت مانند HttpOnly و XSS.

نکته: ترمیمِ خودکار، یکی از قابلیت‌های آزمایشی Semgrep است. در زمان نگارش (آوریل 2022 برای نسخه فارسی و اکتبر 2021 برای نسخه انگلیسی) همه مثال‌ها درست هستند. اگر از آینده میایید، شاید بعضی چیزها متفاوت باشند.

اجرای قوانین

برای اجرا و تست قوانین دو راه داریم:

  1. استفاده از Semgrep playground در آدرس https://semgrep.dev/playground. من بیشتر از این روش استفاده می‌کنم چون راحت و سریع می‏‌توانم قانون را عوض کنم و نتیجه را ببینم.
  2. اجرا در خطِ فرمان (ترجمه فارسی 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 داریم:

  1. fix
  2. fix-regex

فرمان Fix

هر چیزی که قانون پیدا کرده است را با مقدار این فرمان جایگزین میکنیم. بهترین کاندیدها برای این قابلیت، توابع یا رشته (string) های ناامنی هستند که باید با معادل امن جایگزین شوند. چند مثال با این دستور ببینیم.

Python - sys.exit

این مثال را از آموزش Semgrep در آدرس (https://semgrep.dev/s/R6g) قرض گرفته‌ام. در این قانون ما به دنبال تابع exit هستیم و می‌خواهیم آن را با sys.exit جایگزین کنیم. Semgrep تفاوت تابع exit با دیگر استفاده‌های این کلمه را در کُد می‌فهمد و می‎‌تواند راحت آن را جایگزین کنید. بعد از اجرای قانون در وب‌سایت بالا می بینید که دکمه Apply Fix فعال شده و می‌توانید به صورت خودکار کد برنامه را دستکاری کنید.

تغییر اتوماتیک exit به sys.exit
تغییر اتوماتیک 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 را روی آن اجرا می‌کنیم و نتیجه همان است:

# 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) چک می‌کند که آیا:

  1. ما به صورت دستی مقدار HttpOnly را false کرده‌ایم. در این صورت باید مقدار false را به true تغییر دهیم.
  2. یک کوکی به 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
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 به ما قدرت مانور بیشتری می‌دهد.

این فرمان سه بخش دارد:

  1. بخش regex که regular فارسیش واقعا چی میشه؟ اصلاً معادل داره؟) را روی بخشی از کد که توسط قانون پیدا شده است، اجرا می‌کند.
  2. بخش replacement: چیزی که باید با بخش بالا جایگزین شود.
  3. بخش 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 برابر یک
اجرای قانون با 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$ در ترمیم جایگزین نشده است
مقدار 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 به کد استفاده می‌کنم تا هنگام مرور آن بدانم هر بخش چه مشکلی دارد.

امنیت
مهندس بیکار امنیت بازیهای کامپیوتری. نوشته‌های انگلیسی من در parsiya.net هستند.
شاید از این پست‌ها خوشتان بیاید