استخدام eval() في إطارات iframe في وضع الحماية

يفرض نظام إضافات Chrome سياسة أمان المحتوى (CSP) تلقائية صارمة إلى حدٍ ما. إنّ قيود السياسة واضحة: يجب نقل النص البرمجي خارج النص إلى ملفات JavaScript منفصلة، ويجب تحويل معالجات الأحداث المضمّنة لاستخدام addEventListener، ويجب إيقاف eval().

ندرك مع ذلك أنّ مجموعة متنوعة من المكتبات تستخدم بنى مشابهة لـ eval() وeval، مثل new Function() لتحسين الأداء وسهولة التعبير. تكون مكتبات النماذج معرضة بشكل خاص لهذا النمط من التنفيذ. على الرغم من أنّ بعض الإطارات (مثل Angular.js) تتيح استخدام CSP بدون أي إعدادات، لم يتم تعديل العديد من الإطارات الشائعة بعد لتتوافق مع eval في الإضافات. لذلك، تبيّن أنّ إزالة إمكانية استخدام هذه الوظيفة أكثر إزعاجًا للمطوّرين مما كان متوقّعًا.

يقدّم هذا المستند وضع الحماية كآلية آمنة لتضمين هذه المكتبات في مشاريعك بدون التأثير في الأمان.

لماذا نستخدم وضع "الصندوق الرمّل"؟

إنّ eval خطير داخل إحدى الإضافات لأنّ الرمز البرمجي الذي تنفّذه يمكنه الوصول إلى كل شيء في بيئة الإضافات التي تتمتع بأذونات عالية المستوى. تتوفّر مجموعة كبيرة من واجهات برمجة التطبيقات القوية في chrome.* والتي يمكن أن تؤثر بشكل كبير في أمان المستخدم وخصوصيته، وتعدّ عملية استخراج البيانات البسيطة أقل ما يقلقنا. يتمثل الحل المعروض في مساحة محاكاة يمكن فيها لـ eval تنفيذ رمز بدون الوصول إلى data الإضافة أو واجهات برمجة التطبيقات العالية القيمة للإضافة. لا تتوفّر بيانات أو واجهات برمجة تطبيقات، ولكن لا داعي للقلق.

نحقّق ذلك من خلال إدراج ملفات HTML معيّنة داخل حزمة الإضافة على أنّها مُعلّقة في مساحة مغلقة. عند تحميل صفحة في وضع الحماية، سيتم نقلها إلى مصدر فريد، وسيتم منعها من الوصول إلى واجهات برمجة تطبيقات chrome.*. إذا حمّلنا هذه الصفحة في وضع الحماية في مساحة معيّنة في الإضافات من خلال iframe، يمكننا إرسال الرسائل إليها والسماح لها بالتصرّف وفقًا لهذه الرسائل بطريقة معيّنة، ثم الانتظار إلى أن ترسل إلينا النتيجة. تمنحنا آلية المراسلة البسيطة هذه كل ما نحتاجه لتضمين رمز برمجي مستند إلى eval بأمان في سير عمل الإضافة.

إنشاء مساحة محاكاة واستخدامها

إذا كنت تريد البدء مباشرةً في استخدام الرموز البرمجية، يمكنك الحصول على نموذج إضافة وضع الحماية وبدء استخدامه. وهو مثال عملي لواجهة برمجة تطبيقات صغيرة للمراسلة تم إنشاؤها على مكتبة نماذج Handlebars، ومن المفترض أن يوفّر لك كل ما تحتاجه للبدء. إذا أردت معرفة المزيد من المعلومات، لنطّلِع على هذا المثال معًا.

إدراج الملفات في البيان

يجب إدراج كل ملف يجب تشغيله داخل مساحة محاكاة في بيان إضافة عن طريق إضافة سمة sandbox. هذه خطوة حاسمة، ومن السهل نسيانها، لذا تحقّق جيدًا مما إذا كان ملفك المُدرَج في مساحة المحاكاة مُدرَجًا في البيان. في هذا المثال، نضع الملف في مساحة مغلقة بذكاء ويُسمى "sandbox.html". يبدو إدخال البيان على النحو التالي:

{
  ...,
  "sandbox": {
     "pages": ["sandbox.html"]
  },
  ...
}

تحميل الملف المحمي

لإجراء إجراء مثير للاهتمام باستخدام الملف المحصور في مساحة اختبار، علينا تحميله في سياق يمكن فيه معالجة رمز الإضافة. في هذه الحالة، تم تحميل sandbox.html في صفحة إضافة من خلال iframe. يحتوي ملف JavaScript للصفحة على رمز يُرسِل رسالة إلى مساحة وضع الحماية كلما تم النقر على إجراء المتصفّح من خلال العثور على iframe في الصفحة واستدعاء postMessage() في contentWindow. الرسالة هي عنصر يحتوي على ثلاث سمات: context وtemplateName وcommand. سنتناول context وcommand بعد قليل.

service-worker.js:

chrome.action.onClicked.addListener(() => {
  chrome.tabs.create({
    url: 'mainpage.html'
  });
  console.log('Opened a tab with a sandboxed page!');
});

extension-page.js:

let counter = 0;
document.addEventListener('DOMContentLoaded', () => {
  document.getElementById('reset').addEventListener('click', function () {
    counter = 0;
    document.querySelector('#result').innerHTML = '';
  });

  document.getElementById('sendMessage').addEventListener('click', function () {
    counter++;
    let message = {
      command: 'render',
      templateName: 'sample-template-' + counter,
      context: { counter: counter }
    };
    document.getElementById('theFrame').contentWindow.postMessage(message, '*');
  });

تنفيذ إجراءات خطيرة

عند تحميل sandbox.html، يتم تحميل مكتبة Handlebars، وإنشاء ملف برمجي ملفّق وتنسيقه بالطريقة التي تقترحها Handlebars:

extension-page.html:

<!DOCTYPE html>
<html>
  <head>
    <script src="mainpage.js"></script>
    <link href="styles/main.css" rel="stylesheet" />
  </head>
  <body>
    <div id="buttons">
      <button id="sendMessage">Click me</button>
      <button id="reset">Reset counter</button>
    </div>

    <div id="result"></div>

    <iframe id="theFrame" src="sandbox.html" style="display: none"></iframe>
  </body>
</html>

sandbox.html:

   <script id="sample-template-1" type="text/x-handlebars-template">
      <div class='entry'>
        <h1>Hello</h1>
        <p>This is a Handlebar template compiled inside a hidden sandboxed
          iframe.</p>
        <p>The counter parameter from postMessage() (outer frame) is:
          </p>
      </div>
    </script>

    <script id="sample-template-2" type="text/x-handlebars-template">
      <div class='entry'>
        <h1>Welcome back</h1>
        <p>This is another Handlebar template compiled inside a hidden sandboxed
          iframe.</p>
        <p>The counter parameter from postMessage() (outer frame) is:
          </p>
      </div>
    </script>

لا يحدث خطأ. على الرغم من أنّ Handlebars.compile ينتهي باستخدام new Function، تعمل الأمور تمامًا على النحو المتوقّع، وينتهي بنا المطاف بنموذج مجمّع في templates['hello'].

إعادة عرض النتيجة

سنوفّر هذا النموذج للاستخدام من خلال إعداد أداة معالجة الرسائل التي تقبل الأوامر من صفحة الإضافة. سنستخدم command الذي تم تمريره لتحديد الإجراءات التي يجب اتّخاذها (يمكنك تخيُّل تنفيذ إجراءات أكثر من مجرد التقديم، مثل إنشاء قوالب؟ Perhaps managing them in some way?), and the context will be passed into the template directly for rendering. سيتم تمرير محتوى HTML المعروض إلى صفحة الإضافة حتى تتمكّن من إجراء إجراء مفيد به لاحقًا:

 <script>
      const templatesElements = document.querySelectorAll(
        "script[type='text/x-handlebars-template']"
      );
      let templates = {},
        source,
        name;

      // precompile all templates in this page
      for (let i = 0; i < templatesElements.length; i++) {
        source = templatesElements[i].innerHTML;
        name = templatesElements[i].id;
        templates[name] = Handlebars.compile(source);
      }

      // Set up message event handler:
      window.addEventListener('message', function (event) {
        const command = event.data.command;
        const template = templates[event.data.templateName];
        let result = 'invalid request';

       // if we don't know the templateName requested, return an error message
        if (template) {
          switch (command) {
            case 'render':
              result = template(event.data.context);
              break;
            // you could even do dynamic compilation, by accepting a command
            // to compile a new template instead of using static ones, for example:
            // case 'new':
            //   template = Handlebars.compile(event.data.templateSource);
            //   result = template(event.data.context);
            //   break;
              }
        } else {
            result = 'Unknown template: ' + event.data.templateName;
        }
        event.source.postMessage({ result: result }, event.origin);
      });
    </script>

في صفحة الإضافة، سنتلقّى هذه الرسالة وننفّذ إجراءً مثيرًا للاهتمام باستخدام html البيانات التي تم تمريرها إلينا. في هذه الحالة، سنرسل إليك إشعارًا ، ولكن من الممكن تمامًا استخدام رمز HTML هذا بأمان كجزء من واجهة مستخدم الإضافة. ولا يشكّل إدراجها من خلال innerHTML خطرًا أمنيًا كبيرًا لأنّنا نثق بالمحتوى الذي تم عرضه داخل مساحة وضع الحماية.

تجعل هذه الآلية إنشاء النماذج أمرًا سهلاً، ولكنّها لا تقتصر بالطبع على ذلك. يمكن وضع أي رمز برمجي في بيئة الحماية من البرامج الضارة إذا لم يكن يعمل بشكل تلقائي بموجب سياسة صارمة لأمان المحتوى. وفي الواقع، من المفيد غالبًا وضع مكونات الإضافات التي ستتم إدارتها بشكل صحيح في بيئة الحماية من البرامج الضارة بهدف حصر كل جزء من برنامجك بأصغر مجموعة من الأذونات اللازمة لتنفيذه بشكل سليم. يقدّم عرض كتابة تطبيقات ويب وإضافات Chrome آمنة من Google Developer Conference لعام 2012 بعض الأمثلة الجيدة على هذه الأساليب أثناء تنفيذها، ويستحق 56 دقيقة من وقتك.