Replacement-String Injection Explained: عندما يسرق `$`` الـTemplate
دالة escape تبدو آمنة، لكن JavaScript نفسها قد تفسر replacement string بطريقة غير متوقعة. هنا نفكك الـbug، والـXSS chain، وليه استخدام String.replace() كـtemplate engine فكرة سيئة.
كثير من مراجعات الأمان تتوقف عند دالة الـ escape.
يرى المطور أن < أصبحت <، والـ quotes أصبحت Entities، فيغلق ملف XSS ويكمل يومه. المشكلة أن JavaScript لديه تفصيلة صغيرة جدًا تستطيع تحويل string يبدو آمنًا إلى أداة لتخريب الـ template.
المشكلة باختصار أن String.prototype.replace() لا يتعامل دائمًا مع الـ replacement كأنه نص عادي. عندما يكون الـ replacement عبارة عن string، هناك sequences خاصة مثل $& و$`` و$’`. وفي المقابل، OWASP يوضح أن الـ output encoding لا يكون آمنًا لمجرد وجوده، بل يجب أن يتوافق مع الـ context الذي ستصل إليه البيانات.
النتيجة؟ helper بسيط لعمل templating قد يتحول إلى primitive لـ browser-side injection.
في هذا المقال سنشرح الفكرة من البداية، ونبني سيناريو عام، ونراجع الكود الضعيف، ثم نصل إلى الإصلاح الصحيح.
ما هو Replacement-String Injection أصلًا؟
تخيل تطبيقًا صغيرًا يبني صفحة من Template:
{{ username }}
ويستبدلها بقيمة يكتبها المستخدم.
قد يكتب المطور:
function render(template, username) {
return template.replace("{{ username }}", escape(username));
}
من النظرة الأولى يبدو كل شيء منطقيًا.
لكن هنا تبدأ المشكلة. JavaScript يعطي الـ replacement string معنى خاصًا لبعض الأنماط:
| Pattern | ماذا يعني؟ |
|---|---|
$$ | يضع $ حرفيًا |
$& | يضع النص الذي تمت مطابقته |
| `$“ | يضع الجزء الذي يسبق الـ match |
$' | يضع الجزء الذي يلي الـ match |
هذه هي الفكرة الخطرة.
لو استطاع المهاجم تمرير $`` داخل replacement، فإن محرك replace()` يستطيع إعادة إدخال جزء من النص الأصلي في النتيجة، رغم أن هذا الجزء لم يكن موجودًا حرفيًا في input.
MDN توثق هذه الـ replacement patterns بشكل مباشر. citeturn617491search3
لماذا نهتم بهذه الثغرة؟
الخطر يزيد عندما تجتمع عدة ظروف:
- قيمة غير موثوقة تدخل كـ second argument إلى
String.replace(). - الـ replacement عبارة عن string وليس callback.
- التطبيق يستخدم
replace()كبديل عن template engine. - النتيجة النهائية تدخل إلى HTML أو JavaScript أو attribute أو browser sink آخر.
- الفريق يعتمد على CSP لتغطية خطأ injection نفسه.
الأثر قد يصل إلى reflected XSS أو stored XSS، ثم استخدام صلاحيات المتصفح لإرسال requests داخل نفس الـ origin، أو تنفيذ عمليات مصادق عليها، أو تسريب بيانات حساسة. XSS قد يسمح أيضًا بإعادة توجيه المستخدم أو تنفيذ عمليات باستخدام صلاحيات موقع الضحية. citeturn617491search0turn617491search6
مثال عملي عام: كيف تبدأ المشكلة؟
لنفترض أن تطبيقًا يعرض رسالة شخصية:
<title>Message for {{ sender }}</title>
<p>{{ message }}</p>
وعنده helper بسيط للـ HTML escaping:
function escapeHtml(str) {
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function render(template, data) {
for (const key in data) {
const value = escapeHtml(data[key]);
template = template.replace(`{{ ${key} }}`, value);
}
return template;
}
المطور هنا فعل أشياء تبدو صحيحة:
- هرب
<و>و&والـ quotes. - استخدم renderer صغير جدًا.
- لم يسمح للمستخدم بإدخال template syntax كامل.
لكن المشكلة مخفية في السطر الأخير.
الخطوة الأولى: افهم سلوك الـ API
دالة الـ escape لا تحذف $.
وهذا طبيعي لأن $ ليس special داخل HTML.
لكنه special داخل replacement string في JavaScript.
مثلًا:
const template = "A {{ value }} B";
console.log(
template.replace("{{ value }}", "$`ATTACK")
);
هنا $`` تعني لمحرك replace()`:
ضع الجزء الذي كان موجودًا قبل الـ match.
وبالتالي النتيجة النهائية قد تحتوي على نص إضافي من الـ template الأصلي.
المشكلة هنا ليست مجرد HTML escaping ناقص.
المشكلة هي أن هناك طبقة تفسير ثانية لم تكن محسوبة.
الخطوة الثانية: لماذا يصبح الأمر أخطر داخل <script>؟
تخيل أن التطبيق يضع قيمة المستخدم داخل JavaScript:
<script nonce="{{ nonce }}">
let message = "{{ message }}";
</script>
قد يتم تحويل quotes إلى HTML entities.
ومع ذلك، هذا لا يعني أن سياق JavaScript أصبح آمنًا إذا كانت عملية templating نفسها تستطيع إعادة تشكيل النص المصدر.
OWASP يفرق بوضوح بين HTML encoding وJavaScript-context encoding ويحذر من وضع untrusted data داخل <script> وسياقات أخرى خطرة. citeturn617491search0
الخطوة الثالثة: تتبع الـ data flow كاملًا
أفضل طريقة لمراجعة هذا النوع من الأخطاء هي رسم المسار:
HTTP parameter
↓
validation
↓
escapeHtml()
↓
String.replace(template, replacement)
↓
rendered HTML
↓
browser parser
↓
script / DOM / navigation sink
الخطأ الشائع هو اعتبار escapeHtml() آخر خطوة تفسير.
هي ليست كذلك.
String.replace() نفسه يفسر الـ replacement string.
شكل الـ Attack Chain
في تطبيق حقيقي، قد لا تكون أول XSS هي النهاية. يمكن أن تتحول إلى سلسلة من عدة مراحل:
attacker-controlled value
↓
replacement-string expansion
↓
template boundary changes
↓
JavaScript execution
↓
same-origin request
↓
privileged server-side action
↓
sensitive value in URL
↓
cross-origin navigation
↓
Referer / URL leakage
وهنا تظهر فكرة مهمة جدًا:
الثغرة الأولى لا تحتاج أن تعطي المهاجم الـ secret مباشرة.
يكفي أن تمنحه JavaScript execution كافية لتنفيذ المرحلة التالية.
لماذا الـ browser مهم؟
افترض أن endpoint داخلي يقبل الطلب فقط من loopback browser context ويتحقق أيضًا من Origin الخاص بالتطبيق.
الـ curl من جهاز خارجي قد يفشل.
لكن JavaScript يعمل داخل browser في نفس origin يستطيع إنشاء الطلب من البيئة الموثوقة التي يعتمد عليها السيرفر.
وهنا تتحول حماية تبدو مفيدة إلى جزء من الـ attack chain بدل أن تكون نهاية القصة.
الخطأ الشائع: الـ Escape الصحيح في المكان الخطأ
لدينا نسخة أخرى من نفس المشكلة:
function renderValue(template, value) {
const safe = escapeHtml(value);
return template.replace("{{ value }}", safe);
}
قد يقول المطور:
“الـ input escaped، إذن لا يمكن استغلاله.”
لكن هذا يتجاهل خطوة كاملة.
السؤال الصحيح هو:
“كل API يمر عليه هذا النص، ماذا يفعل به قبل أن يراه الـ browser؟”
هنا يوجد مفسران:
String.replace()يفسر replacement patterns.- الـ browser يفسر الناتج كـ document.
مراجعة الأمان يجب أن تضع الاثنين في الحساب.
الكود الضعيف
function escapeHtml(str) {
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function renderMessage(template, userInput) {
const safe = escapeHtml(userInput);
// Dangerous: `safe` is a replacement string.
return template.replace("{{ message }}", safe);
}
لاحظ أن المشكلة ليست أن escapeHtml() نسيت <.
المشكلة أن الـ application أعطى attacker-controlled string إلى API يفسر replacement metacharacters.
الكود المصحح
أبسط إصلاح هو استخدام callback:
function escapeHtml(str) {
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function renderMessage(template, userInput) {
const safe = escapeHtml(userInput);
return template.replace("{{ message }}", () => safe);
}
لماذا هذا أفضل؟
لأن $`` و$& و$’` تكون special عندما يكون الـ replacement عبارة عن string. أما callback فيعيد النص كقيمة، فلا يمر عبر replacement-string parsing. MDN توضح هذا الفرق بشكل مباشر. citeturn617491search3
والأفضل: استخدم Template Engine حقيقي
إذا كان التطبيق يحتاج templating فعليًا، استخدم engine ناضجًا وcontext-aware بدل بناء واحد باستخدام String.replace().
الهدف ليس “نهرب حروف أكثر”.
الهدف هو أن تبقى البيانات غير الموثوقة Data في الـ output context الصحيح.
وماذا عن CSP؟
CSP دفاع ممتاز كـ defense in depth، لكنه ليس بديلًا عن إصلاح injection.
شكل nonce-based CSP مثلًا:
Content-Security-Policy:
script-src 'nonce-random-value';
الـ browser يسمح بالـ script فقط إذا كان الـ nonce في العنصر مطابقًا للـ nonce في policy. MDN تشرح أن السيرفر يولد nonce جديدًا لكل response ثم يربطه بالـ script الذي يريد السماح به. citeturn617491search1turn617491search2
هذه حماية قوية.
لكنها لا تجعل XSS غير موجود.
إذا وجد attacker مسارًا لتشغيل JavaScript من trusted same-origin code، أو استغل DOM sink آخر، أو نفذ navigation إلى صفحة ذات صلاحيات أعلى، فقد تحد CSP بعض الـ payloads دون إزالة أصل المشكلة. OWASP تتعامل مع CSP كـ defense in depth وليس كحل وحيد لمنع XSS. citeturn617491search0turn617491search5
Testing و Audit
عند مراجعة renderer مخصص، ابحث عن:
.replace(templateMarker, userValue)
replace("{{ key }}", value)
replace(`{{ ${key} }}`, value)
ثم اختبر replacement values تحتوي:
$$
$&
$`
$'
ولا تتوقف بعد التأكد من أن <script> يتحول إلى نص.
اسأل أيضًا:
- هل replacement argument string أم callback؟
- ما الـ context الذي تستقبله القيمة النهائية؟
- هل تصل إلى
<script>؟ - هل تصل إلى URL أو attribute؟
- هل هناك browser أو bot يزور الصفحة؟
- هل الصفحة تستطيع تنفيذ same-origin requests؟
- هل توجد endpoints privileged تعتمد على خصائص المتصفح؟
- هل يمكن وضع secrets داخل query strings؟
- هل يمكن أن تتسرب URL كاملة عبر Referer؟
هذه الأسئلة غالبًا تكشف السلسلة أسرع من قائمة payloads ضخمة.
Defense Checklist
| الفئة | الإجراء المقترح |
|---|---|
| Templating | استخدم Template Engine حقيقيًا مع context-aware escaping |
| Replacement API | استخدم callback عندما تحتاج replacement ديناميكي |
| HTML output | استخدم HTML encoding في HTML context |
| JavaScript output | تجنب إدخال untrusted data مباشرة داخل <script> |
| URLs | اعمل URL encoding وvalidate للوجهة |
| CSP | احتفظ بـ nonce-based CSP كـ defense in depth |
| Trusted Types | استخدمها مع DOM sinks الحساسة |
| Testing | اختبر $`` و$‘ و$& و$$` في replacement inputs |
| Privileged flows | لا تعتبر browser-side JavaScript حدًا موثوقًا للـ authorization |
| Secrets | لا تضع البيانات الحساسة داخل URLs أو أماكن يمكن أن تظهر في Referer |
الخلاصة
هذا النوع من الأخطاء يوضح أن أمان التطبيق يعتمد على تركيب كل الطبقات معًا، وليس على كون كل سطر يبدو سليمًا وحده.
قد تكون دالة الـ escape صحيحة.
قد تكون CSP صارمة.
قد يكون endpoint الداخلي يرفض الاتصالات الخارجية.
ومع ذلك يمكن للتطبيق كله أن يسقط بسبب API صغيرة فسرّت replacement string بطريقة لم تكن في الحسبان.
الإصلاح هنا ممل، وهذا شيء جيد: استخدم renderer حقيقيًا، طبّق encoding حسب الـ context، ولا تعطِ attacker-controlled strings إلى replacement semantics عندما كنت تقصد “نصًا حرفيًا”.
String.replace() ممتازة كـ utility. لكنها اختيار سيئ جدًا كـ template engine.
المراجع
- OWASP, Cross Site Scripting Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
- OWASP, Content Security Policy Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html
- MDN,
String.prototype.replace(): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace - MDN, Content-Security-Policy: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy
- MDN,
nonceHTML global attribute: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/nonce