در این نوشتار میخواهم در مورد قابلیت ترمیمِ خودکار (autofix) کُد Semgrep صحبت کنم. این قابلیت آزمایشی به ما امکان میدهد که بعد از یافتن نتایج در فایلهای منبع، آنها را به صورت اتوماتیک دستکاری کنیم.
اگر علاقه به خواندن نسخه انگلیسی دارید:
همه نمونهها در Semgrep playground هستند ولی میتوانید آنها را آفلاین نیز اجرا کنید:
پیشنیازهای این بلاگ
برای استفاده بهینه از این بلاگ، بهتر است این موارد را بدانید:
- استفاده از 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 داریم:
- fix
- fix-regex
فرمان Fix
هر چیزی که قانون پیدا کرده است را با مقدار این فرمان جایگزین میکنیم. بهترین کاندیدها برای این قابلیت، توابع یا رشته (string) های ناامنی هستند که باید با معادل امن جایگزین شوند. چند مثال با این دستور ببینیم.
Python - sys.exit
این مثال را از آموزش Semgrep در آدرس (https://semgrep.dev/s/R6g) قرض گرفتهام. در این قانون ما به دنبال تابع exit هستیم و میخواهیم آن را با sys.exit جایگزین کنیم. Semgrep تفاوت تابع exit با دیگر استفادههای این کلمه را در کُد میفهمد و میتواند راحت آن را جایگزین کنید. بعد از اجرای قانون در وبسایت بالا می بینید که دکمه Apply Fix فعال شده و میتوانید به صورت خودکار کد برنامه را دستکاری کنید.

یکی از نکات جالب این است که میتوانیم پارامتر تابع را توسط یک 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("=~/.*\/CBC\/PKCS5Padding/")
fix: $CIPHER.getInstance("AES/GCM/NoPadding")
این قانون به دنبال هر چیزی که شبیه 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("AES/GCM/NoPadding")
مقدار “محاسبه” قانون جدید در 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(""); ...
- 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(""); ...
- pattern: $COOKIE.setHttpOnly(false);
fix: $COOKIE.setHttpOnly(true);

ترمیم HttpOnly دوم
در این بخش ما ;RESPONSE.addCookie($COOKIE)$
را میبینیم اما خبری از setHttpOnly
نیست.باید آن را اضافه کنیم. این قانون (https://semgrep.dev/s/parsiya:java-httponly-practice-2) این کار را انجام میدهد اما کد اضافه شده “خوشگل” نیست (برای جاوا فاصله و غیره مهم نیستند و شاید شما بگویید که این قانون برای من کافی است زیرا ابزارهای دیگری دارید که اتوماتیک کُد شما را فُرمَت میکنند).

ما میتوانیم این مشکل را با فرمان fix-regex
حل کنیم.
فرمان fix-regex
همانطور که دیدیم برای جابجاییهای ساده (مثلا تغییر badFunc
به goodFunc
) فرمان fix کافی است. اما fix-regex به ما قدرت مانور بیشتری میدهد.
این فرمان سه بخش دارد:
- بخش regex که regular expression (فارسیش واقعا چی میشه؟ اصلاً معادل داره؟) را روی بخشی از کد که توسط قانون پیدا شده است، اجرا میکند.
- بخش 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(""); ...
- pattern-not-inside: $COOKIE.setHttpOnly(...); ...
- pattern: $RESPONSE.addCookie($COOKIE);
fix-regex:
regex: (.*)
replacement: //\1

در جواب، //
را دو بار میبینیم. چون regex ما greedy (حریص) است. برای این کار باید از فیلد 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

برای این کار باید از قانون زیر استفاده کنیم (واقعا حوصله توضیح دوباره آن را ندارم 😅):

# 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(""); ...
- 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 به کد استفاده میکنم تا هنگام مرور آن بدانم هر بخش چه مشکلی دارد.