Skip to content
• دقيقة واحدة للقراءة

Replacement-String Injection Explained: عندما يسرق `$`` الـTemplate

دالة escape تبدو آمنة، لكن JavaScript نفسها قد تفسر replacement string بطريقة غير متوقعة. هنا نفكك الـbug، والـXSS chain، وليه استخدام String.replace() كـtemplate engine فكرة سيئة.

#XSS #JavaScript #CSP #Template Injection #Secure Coding
Replacement-String Injection Explained: عندما يسرق `$`` الـTemplate hero illustration
تسجيل صوتي للمقال
قراءة آلية
0:00 / --:--

كثير من مراجعات الأمان تتوقف عند دالة الـ escape.

يرى المطور أن < أصبحت &lt;، والـ 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 بشكل مباشر. citeturn617491search3

لماذا نهتم بهذه الثغرة؟

الخطر يزيد عندما تجتمع عدة ظروف:

  • قيمة غير موثوقة تدخل كـ second argument إلى String.replace().
  • الـ replacement عبارة عن string وليس callback.
  • التطبيق يستخدم replace() كبديل عن template engine.
  • النتيجة النهائية تدخل إلى HTML أو JavaScript أو attribute أو browser sink آخر.
  • الفريق يعتمد على CSP لتغطية خطأ injection نفسه.

الأثر قد يصل إلى reflected XSS أو stored XSS، ثم استخدام صلاحيات المتصفح لإرسال requests داخل نفس الـ origin، أو تنفيذ عمليات مصادق عليها، أو تسريب بيانات حساسة. XSS قد يسمح أيضًا بإعادة توجيه المستخدم أو تنفيذ عمليات باستخدام صلاحيات موقع الضحية. citeturn617491search0turn617491search6

مثال عملي عام: كيف تبدأ المشكلة؟

لنفترض أن تطبيقًا يعرض رسالة شخصية:

<title>Message for {{ sender }}</title>
<p>{{ message }}</p>

وعنده helper بسيط للـ HTML escaping:

function escapeHtml(str) {
  return str
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

function render(template, data) {
  for (const key in data) {
    const value = escapeHtml(data[key]);

    template = template.replace(`{{ ${key} }}`, value);
  }

  return template;
}

المطور هنا فعل أشياء تبدو صحيحة:

  1. هرب < و> و& والـ quotes.
  2. استخدم renderer صغير جدًا.
  3. لم يسمح للمستخدم بإدخال 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> وسياقات أخرى خطرة. citeturn617491search0

الخطوة الثالثة: تتبع الـ 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؟”

هنا يوجد مفسران:

  1. String.replace() يفسر replacement patterns.
  2. الـ browser يفسر الناتج كـ document.

مراجعة الأمان يجب أن تضع الاثنين في الحساب.

الكود الضعيف

function escapeHtml(str) {
  return str
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

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, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

function renderMessage(template, userInput) {
  const safe = escapeHtml(userInput);

  return template.replace("{{ message }}", () => safe);
}

لماذا هذا أفضل؟

لأن $`` و$& و$’` تكون special عندما يكون الـ replacement عبارة عن string. أما callback فيعيد النص كقيمة، فلا يمر عبر replacement-string parsing. MDN توضح هذا الفرق بشكل مباشر. citeturn617491search3

والأفضل: استخدم 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 الذي يريد السماح به. citeturn617491search1turn617491search2

هذه حماية قوية.

لكنها لا تجعل XSS غير موجود.

إذا وجد attacker مسارًا لتشغيل JavaScript من trusted same-origin code، أو استغل DOM sink آخر، أو نفذ navigation إلى صفحة ذات صلاحيات أعلى، فقد تحد CSP بعض الـ payloads دون إزالة أصل المشكلة. OWASP تتعامل مع CSP كـ defense in depth وليس كحل وحيد لمنع XSS. citeturn617491search0turn617491search5

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.

المراجع

⌘
Suggested Searches