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

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

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

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

ما أهمية وضع الحماية؟

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

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

إنشاء وضع حماية واستخدامه

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

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

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

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

تحميل الملف المحمي في وضع الحماية

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

service-factor.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، يتم تحميل مكتبة "الأشرطة المقودة" وتنشئ نموذجًا مضمّنًا وتجمعه بالطريقة التي تقترحها "الأشرطة المقودة":

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 خطرًا أمنيًا كبيرًا لأننا نثق في المحتوى الذي تم عرضه في وضع الحماية.

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