بایپس فیلتر XSS با رمزگذاری دوبل (Double URL Encoding)
یک WAF ساده فقط یکبار URL-decode میکند و دنبال <script> خام میگردد؛ با انکود کردن دوبار پیلود، فیلتر رد میشود ولی مرورگر همچنان اجرایش میکند.
فرض کنید سمت سرور یک فیلتر سادهی ضد-XSS دارد که بهجای escape درست خروجی، سعی میکند الگوهای خطرناک را بلاک کند:
// فیلتر ناقص (PHP)
$comment = $_POST['comment'];
if (stripos($comment, '<script') !== false) {
die('درخواست مسدود شد (تلاش برای XSS)');
}
echo "<div class='comment'>$comment</div>"; // هنوز هم escape نشده!
مشکل اول: فیلتر روی رشتهی خام، نه روی خروجی رندر شده
این فیلتر فقط دنبال رشتهی دقیق <script میگردد. یک payload بدون تگ script هم کاملاً خطرناک است و از فیلتر رد میشود:
comment=<img src=x onerror=alert(document.cookie)>
comment=<svg onload=alert(1)>
مشکل دوم: رمزگذاری دوبل اگر سرور decode میکند
در بعضی معماریها (مثلاً وقتی یک WAF یا proxy جلوی سرور، یکبار URL-decode انجام میدهد و بعد خودِ اپلیکیشن هم دوباره decode میکند)، میشود با انکود کردن payload بهصورت double-encoded، لایهی فیلتر را رد کرد در حالیکه نتیجهی نهایی که به مرورگر میرسد دستنخورده است:
// بجای:
<script>alert(1)</script>
// یکبار انکود:
%3Cscript%3Ealert(1)%3C%2Fscript%3E
// دوبار انکود (هر % با %25 جایگزین میشود):
%253Cscript%253Ealert(1)%253C%252Fscript%253E
اگر فیلتر امنیتی فقط یک لایه decode انجام میدهد و دنبال <script خام میگردد، رشتهی double-encoded از آن رد میشود؛ اما اگر اپلیکیشن (یا مرورگر هنگام پردازش URL) یک لایهی دوم decode انجام دهد، payload واقعی در نهایت در صفحه اجرا میشود.
راهحل درست
فیلتر کردن روی الگوهای خاص (blacklist) همیشه قابل دور زدن است. راهحل واقعی، escape کردن context-aware در لحظهی خروجی است، نه بلاک کردن ورودی:
// نسخهی امن
$comment = $_POST['comment'];
echo "<div class='comment'>" . htmlspecialchars($comment, ENT_QUOTES, 'UTF-8') . "</div>";
در Blade (لاراول) این کار بهصورت پیشفرض با {{ $comment }} انجام میشود (نه {!! !!}) — همیشه مطمئن شوید از نسخهی escapeشده استفاده میکنید مگر اینکه واقعاً به HTML خام نیاز باشد و منبع آن کاملاً قابلاعتماد باشد.