Mainkan dengan aman di IFrame dengan sandbox

Membangun pengalaman yang kaya di web saat ini hampir pasti melibatkan penyematan komponen dan konten yang tidak Anda kontrol sepenuhnya. Widget pihak ketiga dapat mendorong interaksi dan memainkan peran penting dalam keseluruhan pengalaman pengguna, dan konten buatan pengguna terkadang bahkan lebih penting daripada konten native situs. Tidak melakukan keduanya bukanlah pilihan, tetapi keduanya meningkatkan risiko bahwa Sesuatu yang Buruk™ dapat terjadi di situs Anda. Setiap widget yang Anda sematkan -- setiap iklan, setiap widget media sosial -- adalah vektor serangan potensial bagi mereka yang berniat jahat:

Kebijakan Keamanan Konten (CSP) dapat mengurangi risiko yang terkait dengan kedua jenis konten ini dengan memberi Anda kemampuan untuk mengizinkan sumber skrip dan konten lainnya yang dipercaya secara khusus. Ini adalah langkah besar ke arah yang benar, tetapi perlu diperhatikan bahwa perlindungan yang ditawarkan sebagian besar perintah CSP bersifat biner: resource diizinkan, atau tidak. Terkadang, Anda mungkin berpikir, "Saya tidak yakin apakah saya benar-benar memercayai sumber konten ini, tetapi kontennya sangat menarik!" Tolong sematkan, Browser, tetapi jangan sampai merusak situs saya."

Hak istimewa terendah

Intinya, kami mencari mekanisme yang memungkinkan kami memberikan konten yang kami sematkan hanya tingkat kemampuan minimum yang diperlukan untuk melakukan tugasnya. Jika widget tidak perlu memunculkan jendela baru, menghapus akses ke window.open tidak akan berbahaya. Jika tidak memerlukan Flash, menonaktifkan dukungan plugin seharusnya tidak menjadi masalah. Kita akan seaman mungkin jika mengikuti prinsip hak istimewa terendah, dan memblokir setiap fitur yang tidak relevan secara langsung dengan fungsi yang ingin kita gunakan. Hasilnya, kita tidak perlu lagi memercayai secara membabi buta bahwa beberapa bagian konten sematan tidak akan memanfaatkan hak istimewa yang tidak seharusnya digunakan. Aplikasi tersebut tidak akan memiliki akses ke fungsi tersebut sejak awal.

Elemen iframe adalah langkah pertama menuju framework yang baik untuk solusi tersebut. Memuat beberapa komponen yang tidak tepercaya dalam iframe memberikan ukuran pemisahan antara aplikasi Anda dan konten yang ingin Anda muat. Konten yang dibingkai tidak akan memiliki akses ke DOM halaman Anda, atau data yang Anda simpan secara lokal, dan juga tidak akan dapat menggambar ke posisi arbitrer di halaman; cakupannya terbatas pada kerangka bingkai. Namun, pemisahan ini tidak sepenuhnya kuat. Halaman yang dimuat masih memiliki sejumlah opsi untuk perilaku yang mengganggu atau berbahaya: video yang diputar otomatis, plugin, dan pop-up hanyalah sebagian kecil dari masalahnya.

Atribut sandbox dari elemen iframe memberi kita hal yang kita perlukan untuk memperketat batasan pada konten yang di-frame. Kita dapat menginstruksikan browser untuk memuat konten frame tertentu di lingkungan dengan hak istimewa rendah, sehingga hanya mengizinkan subset kemampuan yang diperlukan untuk melakukan pekerjaan apa pun yang perlu dilakukan.

Percaya, tetapi tetap lakukan verifikasi

Tombol "Tweet" Twitter adalah contoh bagus fungsi yang dapat disematkan dengan lebih aman di situs Anda melalui sandbox. Twitter memungkinkan Anda menyematkan tombol melalui iframe dengan kode berikut:

<iframe src="https://platform.twitter.com/widgets/tweet_button.html"
        style="border: 0; width:130px; height:20px;"></iframe>

Untuk mengetahui apa yang dapat kita kunci, mari kita periksa dengan cermat kemampuan yang diperlukan tombol. HTML yang dimuat ke dalam frame menjalankan sedikit JavaScript dari server Twitter, dan menghasilkan pop-up yang diisi dengan antarmuka tweet saat diklik. Antarmuka tersebut memerlukan akses ke cookie Twitter untuk mengaitkan tweet ke akun yang benar, dan memerlukan kemampuan untuk mengirimkan formulir tweet. Hanya itu; frame tidak perlu memuat plugin apa pun, tidak perlu membuka jendela tingkat atas, atau sejumlah fungsi lainnya. Karena tidak memerlukan hak istimewa tersebut, mari kita hapus dengan melakukan sandbox pada konten frame.

Sandbox bekerja berdasarkan daftar yang diizinkan. Kita mulai dengan menghapus semua izin yang memungkinkan, lalu mengaktifkan kembali kemampuan individual dengan menambahkan flag tertentu ke konfigurasi sandbox. Untuk widget Twitter, kami telah memutuskan untuk mengaktifkan JavaScript, pop-up, pengiriman formulir, dan cookie twitter.com. Kita dapat melakukannya dengan menambahkan atribut sandbox ke iframe dengan nilai berikut:

<iframe sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
    src="https://platform.twitter.com/widgets/tweet_button.html"
    style="border: 0; width:130px; height:20px;"></iframe>

Selesai. Kita telah memberikan semua kemampuan yang diperlukan ke frame, dan browser akan membantu menolak akses ke hak istimewa apa pun yang tidak kita berikan secara eksplisit melalui nilai atribut sandbox.

Kontrol terperinci atas kemampuan

Kita telah melihat beberapa kemungkinan flag sandbox dalam contoh di atas, sekarang mari kita pelajari cara kerja internal atribut ini secara lebih mendetail.

Jika diberi iframe dengan atribut sandbox kosong, dokumen yang dibingkai akan di-sandbox sepenuhnya, sehingga tunduk pada batasan berikut:

  • JavaScript tidak akan dieksekusi dalam dokumen yang di-frame. Hal ini tidak hanya mencakup JavaScript yang dimuat secara eksplisit melalui tag skrip, tetapi juga pengendali peristiwa inline dan URL javascript:. Hal ini juga berarti bahwa konten yang terdapat dalam tag noscript akan ditampilkan, seolah-olah pengguna telah menonaktifkan skrip itu sendiri.
  • Dokumen yang di-frame dimuat ke origin unik, yang berarti semua pemeriksaan origin yang sama akan gagal; origin unik tidak pernah cocok dengan origin lain, bahkan dengan dirinya sendiri. Di antara dampak lainnya, hal ini berarti dokumen tidak memiliki akses ke data yang disimpan di cookie origin mana pun atau mekanisme penyimpanan lainnya (penyimpanan DOM, Indexed DB, dll.).
  • Dokumen yang dibingkai tidak dapat membuat jendela atau dialog baru (misalnya, melalui window.open atau target="_blank").
  • Formulir tidak dapat dikirim.
  • Plugin tidak akan dimuat.
  • Dokumen yang di-frame hanya dapat menavigasi dirinya sendiri, bukan induk tingkat teratasnya. Menetapkan window.top.location akan menampilkan pengecualian, dan mengklik link dengan target="_top" tidak akan berpengaruh.
  • Fitur yang dipicu secara otomatis (elemen formulir yang difokuskan otomatis, video yang diputar otomatis, dll.) akan diblokir.
  • Kunci kursor tidak dapat diperoleh.
  • Atribut seamless diabaikan pada iframes yang terdapat dalam dokumen yang dibingkai.

Hal ini sangat ketat, dan dokumen yang dimuat ke dalam iframe yang di-sandbox sepenuhnya memang menimbulkan risiko yang sangat kecil. Tentu saja, sandbox ini juga tidak dapat melakukan banyak hal yang berguna: Anda mungkin dapat menggunakan sandbox penuh untuk beberapa konten statis, tetapi sering kali Anda perlu sedikit melonggarkan pembatasan.

Kecuali untuk plugin, setiap batasan ini dapat dicabut dengan menambahkan flag ke nilai atribut sandbox. Dokumen dalam sandbox tidak akan pernah dapat menjalankan plugin, karena plugin adalah kode native yang tidak di-sandbox, tetapi hal lainnya dapat dilakukan:

  • allow-forms mengizinkan pengiriman formulir.
  • allow-popups mengizinkan (kejutan!) pop-up.
  • allow-pointer-lock mengizinkan (kejutan!) penguncian kursor.
  • allow-same-origin memungkinkan dokumen mempertahankan origin-nya; halaman yang dimuat dari https://example.com/ akan mempertahankan akses ke data origin tersebut.
  • allow-scripts mengizinkan eksekusi JavaScript, dan juga mengizinkan fitur untuk dipicu secara otomatis (karena fitur tersebut akan mudah diterapkan melalui JavaScript).
  • allow-top-navigation memungkinkan dokumen keluar dari frame dengan menavigasi jendela tingkat atas.

Dengan mempertimbangkan hal ini, kita dapat mengevaluasi alasan kita mendapatkan kumpulan flag sandbox tertentu dalam contoh Twitter di atas:

  • allow-scripts diperlukan, karena halaman yang dimuat ke dalam frame menjalankan beberapa JavaScript untuk menangani interaksi pengguna.
  • allow-popups diperlukan, karena tombol akan memunculkan formulir tweet di jendela baru.
  • allow-forms diperlukan, karena formulir tweet harus dapat dikirim.
  • allow-same-origin diperlukan, karena jika tidak, cookie twitter.com tidak akan dapat diakses, dan pengguna tidak dapat login untuk memposting formulir.

Satu hal penting yang perlu diperhatikan adalah bahwa flag sandboxing yang diterapkan ke frame juga berlaku untuk jendela atau frame apa pun yang dibuat di sandbox. Artinya, kita harus menambahkan allow-forms ke sandbox frame, meskipun formulir hanya ada di jendela yang dimunculkan frame.

Dengan atribut sandbox, widget hanya mendapatkan izin yang diperlukan, dan kemampuan seperti plugin, navigasi teratas, dan penguncian kursor tetap diblokir. Kami telah mengurangi risiko penyematan widget, tanpa efek buruk. Ini adalah kemenangan bagi semua pihak yang terlibat.

Pemisahan hak istimewa

Menjalankan konten pihak ketiga dalam sandbox untuk menjalankan kode yang tidak tepercaya di lingkungan dengan hak istimewa rendah memberikan manfaat yang cukup jelas. Namun, bagaimana dengan kode Anda sendiri? Anda percaya pada diri sendiri, bukan? Jadi, mengapa harus khawatir tentang sandbox?

Kami akan balik bertanya: jika kode Anda tidak memerlukan plugin, mengapa memberinya akses ke plugin? Paling tidak, ini adalah hak istimewa yang tidak pernah Anda gunakan, dan paling buruk, ini adalah vektor potensial bagi penyerang untuk masuk. Kode setiap orang memiliki bug, dan hampir setiap aplikasi rentan terhadap eksploitasi dengan satu atau lain cara. Melakukan sandbox pada kode Anda sendiri berarti bahwa meskipun penyerang berhasil mengganggu aplikasi Anda, mereka tidak akan diberi akses penuh ke origin aplikasi; mereka hanya akan dapat melakukan hal-hal yang dapat dilakukan aplikasi. Tetap buruk, tetapi tidak seburuk yang seharusnya.

Anda dapat mengurangi risiko lebih lanjut dengan memecah aplikasi menjadi bagian-bagian logis dan melakukan sandbox pada setiap bagian dengan hak istimewa seminimal mungkin. Teknik ini sangat umum dalam kode native: Chrome, misalnya, membagi dirinya menjadi proses browser dengan hak istimewa tinggi yang memiliki akses ke hard drive lokal dan dapat membuat koneksi jaringan, serta banyak proses perender dengan hak istimewa rendah yang melakukan tugas berat mengurai konten yang tidak tepercaya. Perender tidak perlu menyentuh disk, browser akan memberikan semua informasi yang diperlukan untuk merender halaman. Meskipun peretas yang cerdas menemukan cara untuk merusak perender, dia tidak akan berhasil, karena perender tidak dapat melakukan banyak hal menarik dengan sendirinya: semua akses hak istimewa tinggi harus dirutekan melalui proses browser. Penyerang harus menemukan beberapa celah di berbagai bagian sistem untuk melakukan kerusakan, yang sangat mengurangi risiko keberhasilan pwnage.

Menjalankan eval() dalam sandbox dengan aman

Dengan sandbox dan postMessage API, keberhasilan model ini cukup mudah diterapkan ke web. Bagian-bagian aplikasi Anda dapat berada di iframe yang di-sandbox, dan dokumen induk dapat menjadi perantara komunikasi di antara bagian-bagian tersebut dengan memposting pesan dan memproses respons. Struktur semacam ini memastikan bahwa eksploitasi di bagian mana pun dari aplikasi akan menimbulkan kerusakan seminimal mungkin. Hal ini juga memiliki keuntungan karena memaksa Anda untuk membuat titik integrasi yang jelas, sehingga Anda tahu persis di mana Anda harus berhati-hati dalam memvalidasi input dan output. Mari kita pelajari contoh mainan, hanya untuk melihat cara kerjanya.

Evalbox adalah aplikasi menarik yang mengambil string, dan mengevaluasinya sebagai JavaScript. Luar biasa, bukan? Sesuatu yang telah Anda tunggu-tunggu selama bertahun-tahun. Tentu saja, ini adalah aplikasi yang cukup berbahaya, karena mengizinkan eksekusi JavaScript arbitrer berarti semua data yang ditawarkan oleh origin dapat diambil. Kita akan memitigasi risiko Hal Buruk™ yang terjadi dengan memastikan bahwa kode dieksekusi di dalam sandbox, yang membuatnya jauh lebih aman. Kita akan mempelajari kode dari dalam ke luar, dimulai dengan konten frame:

<!-- frame.html -->
<!DOCTYPE html>
<html>
    <head>
    <title>Evalbox's Frame</title>
    <script>
        window.addEventListener('message', function (e) {
        var mainWindow = e.source;
        var result = '';
        try {
            result = eval(e.data);
        } catch (e) {
            result = 'eval() threw an exception.';
        }
        mainWindow.postMessage(result, event.origin);
        });
    </script>
    </head>
</html>

Di dalam frame, kita memiliki dokumen minimal yang hanya memproses pesan dari induknya dengan mengaitkan ke peristiwa message dari objek window. Setiap kali induk mengeksekusi postMessage pada konten iframe, peristiwa ini akan dipicu, sehingga memberi kita akses ke string yang ingin dieksekusi oleh induk.

Di pengendali, kita mengambil atribut source peristiwa, yang merupakan jendela induk. Kita akan menggunakannya untuk mengirimkan hasil kerja keras kita kembali setelah selesai. Kemudian, kita akan melakukan pekerjaan berat, dengan meneruskan data yang telah diberikan ke eval(). Panggilan ini telah digabungkan dalam blok try, karena operasi yang dilarang di dalam iframe dengan sandbox akan sering menghasilkan pengecualian DOM; kami akan menangkapnya dan melaporkan pesan error yang mudah dipahami. Terakhir, kita memposting hasilnya kembali ke jendela induk. Hal ini cukup mudah.

Induknya juga tidak rumit. Kita akan membuat UI kecil dengan textarea untuk kode, dan button untuk eksekusi, dan kita akan menarik frame.html melalui iframe dengan sandbox, yang hanya mengizinkan eksekusi skrip:

<textarea id='code'></textarea>
<button id='safe'>eval() in a sandboxed frame.</button>
<iframe sandbox='allow-scripts'
        id='sandboxed'
        src='frame.html'></iframe>

Sekarang kita akan menghubungkan semuanya untuk dieksekusi. Pertama, kita akan memproses respons dari iframe dan alert() respons tersebut kepada pengguna. Mungkin aplikasi sungguhan akan melakukan sesuatu yang tidak terlalu mengganggu:

window.addEventListener('message',
    function (e) {
        // Sandboxed iframes which lack the 'allow-same-origin'
        // header have "null" rather than a valid origin. This means you still
        // have to be careful about accepting data via the messaging API you
        // create. Check that source, and validate those inputs!
        var frame = document.getElementById('sandboxed');
        if (e.origin === "null" &amp;&amp; e.source === frame.contentWindow)
        alert('Result: ' + e.data);
    });

Selanjutnya, kita akan menghubungkan pengendali peristiwa ke klik pada button. Saat pengguna mengklik, kita akan mengambil konten textarea saat ini, dan meneruskannya ke frame untuk dieksekusi:

function evaluate() {
    var frame = document.getElementById('sandboxed');
    var code = document.getElementById('code').value;
    // Note that we're sending the message to "*", rather than some specific
    // origin. Sandboxed iframes which lack the 'allow-same-origin' header
    // don't have an origin which you can target: you'll have to send to any
    // origin, which might alow some esoteric attacks. Validate your output!
    frame.contentWindow.postMessage(code, '*');
}

document.getElementById('safe').addEventListener('click', evaluate);

Mudah, kan? Kami telah membuat API evaluasi yang sangat sederhana, dan kami dapat memastikan bahwa kode yang dievaluasi tidak memiliki akses ke informasi sensitif seperti cookie atau penyimpanan DOM. Demikian pula, kode yang dievaluasi tidak dapat memuat plugin, memunculkan jendela baru, atau melakukan sejumlah aktivitas mengganggu atau berbahaya lainnya.

Anda dapat melakukan hal yang sama untuk kode Anda sendiri dengan memecah aplikasi monolitik menjadi komponen-komponen dengan tujuan tunggal. Masing-masing dapat digabungkan dalam API pesan sederhana, seperti yang telah kami tulis di atas. Jendela induk dengan hak istimewa tinggi dapat bertindak sebagai pengontrol dan dispatcher, mengirim pesan ke modul tertentu yang masing-masing memiliki hak istimewa sesedikit mungkin untuk melakukan tugasnya, memproses hasil, dan memastikan bahwa setiap modul diberi informasi yang diperlukan saja.

Namun, perhatikan bahwa Anda harus sangat berhati-hati saat menangani konten dalam frame yang berasal dari origin yang sama dengan induknya. Jika halaman di https://example.com/ membingkai halaman lain di origin yang sama dengan sandbox yang menyertakan flag allow-same-origin dan allow-scripts, maka halaman yang dibingkai dapat menjangkau induk, dan menghapus atribut sandbox seluruhnya.

Bermain di sandbox Anda

Sandboxing kini tersedia untuk Anda di berbagai browser: Firefox 17+, IE10+, dan Chrome pada saat penulisan (caniuse, tentu saja, memiliki tabel dukungan terbaru). Menerapkan atribut sandbox ke iframes yang Anda sertakan memungkinkan Anda memberikan hak istimewa tertentu ke konten yang ditampilkan, hanya hak istimewa yang diperlukan agar konten berfungsi dengan benar. Hal ini memberi Anda peluang untuk mengurangi risiko yang terkait dengan penyertaan konten pihak ketiga, di atas dan di luar apa yang sudah dimungkinkan dengan Kebijakan Keamanan Konten.

Selain itu, sandbox adalah teknik yang efektif untuk mengurangi risiko bahwa penyerang yang cerdas akan dapat mengeksploitasi celah dalam kode Anda sendiri. Dengan memisahkan aplikasi monolitik menjadi serangkaian layanan dalam sandbox, yang masing-masing bertanggung jawab atas sebagian kecil fungsi mandiri, penyerang akan dipaksa untuk tidak hanya membahayakan konten frame tertentu, tetapi juga pengontrolnya. Tugas tersebut jauh lebih sulit, terutama karena cakupan pengontrol dapat sangat dikurangi. Anda dapat menghabiskan upaya terkait keamanan untuk mengaudit kode tersebut jika Anda meminta bantuan browser untuk sisanya.

Hal ini tidak berarti bahwa sandbox adalah solusi lengkap untuk masalah keamanan di internet. Fitur ini menawarkan defense in depth, dan kecuali jika Anda memiliki kontrol atas klien pengguna, Anda belum dapat mengandalkan dukungan browser untuk semua pengguna (jika Anda mengontrol klien pengguna -- misalnya, lingkungan perusahaan -- hore!). Suatu hari nanti… tetapi untuk saat ini, sandboxing adalah lapisan perlindungan lain untuk memperkuat pertahanan Anda, bukan pertahanan lengkap yang dapat Anda andalkan sepenuhnya. Namun, lapisan tetap sangat baik. Sebaiknya gunakan yang satu ini.

Bacaan lebih lanjut

  • "Privilege Separation in HTML5 Applications" adalah makalah menarik yang membahas desain framework kecil, dan penerapannya pada tiga aplikasi HTML5 yang ada.

  • Sandbox dapat menjadi lebih fleksibel jika digabungkan dengan dua atribut iframe baru lainnya: srcdoc, dan seamless. Yang pertama memungkinkan Anda mengisi frame dengan konten tanpa overhead permintaan HTTP, dan yang kedua memungkinkan gaya mengalir ke konten yang dibingkai. Keduanya memiliki dukungan browser yang cukup buruk saat ini (Chrome dan WebKit nightly), tetapi akan menjadi kombinasi yang menarik di masa mendatang. Misalnya, Anda dapat melakukan sandbox komentar pada artikel melalui kode berikut:

        <iframe sandbox seamless
                srcdoc="<p>This is a user's comment!
                           It can't execute script!
                           Hooray for safety!</p>"></iframe>