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

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

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

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

لماذا وضع الحماية؟

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

نحقّق ذلك من خلال إدراج ملفات 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 الذي تم تمريره لتحديد ما يجب تنفيذه (يمكنك تخيّل تنفيذ أكثر من مجرد العرض، ربما إنشاء قوالب؟). ربما تتم إدارتها بطريقة ما)، وسيتم تمرير context إلى النموذج مباشرةً لعرضه. سيتم إعادة محتوى 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 خطرًا أمنيًا كبيرًا لأنّنا نثق بالمحتوى الذي تم عرضه في وضع الحماية.

تسهّل هذه الآلية إنشاء النماذج، ولكنّها بالطبع لا تقتصر على ذلك. يمكن وضع أي رمز برمجي لا يعمل بشكلٍ مباشر ضمن سياسة صارمة لأمان المحتوى في بيئة معزولة. وفي الواقع، من المفيد غالبًا وضع مكوّنات الإضافات التي قد تعمل بشكلٍ صحيح في بيئة معزولة من أجل حصر كل جزء من البرنامج في أصغر مجموعة من الامتيازات اللازمة لتنفيذه بشكلٍ سليم. تقدّم презентация Writing Secure Web Apps and Chrome Extensions من مؤتمر Google I/O لعام 2012 بعض الأمثلة الجيدة على هذه التقنية أثناء العمل، وهي تستحق 56 دقيقة من وقتك.