Mainkan dengan aman di IFrame dengan sandbox

Membuat pengalaman yang kaya di web saat ini hampir tidak dapat dihindari melibatkan penyematan komponen dan konten yang tidak dapat 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 menggunakan salah satunya bukanlah opsi yang tepat, tetapi keduanya akan meningkatkan risiko error tertentu dapat terjadi di situs Anda. Setiap widget yang Anda sematkan -- setiap iklan, setiap widget media sosial -- adalah potensi vektor serangan bagi mereka yang memiliki niat 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 lain yang tepercaya 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. Ada kalanya Anda perlu mengatakan "Saya tidak yakin saya benar-benar memercayai sumber konten ini, tapi konten ini sangat bagus! Sematkan, Browser, tapi jangan biarkan itu merusak situs saya."

Hak Istimewa Terendah

Pada dasarnya, kita mencari mekanisme yang memungkinkan kita memberikan konten yang disematkan hanya tingkat kemampuan minimum yang diperlukan untuk melakukan tugasnya. Jika widget tidak perlu memunculkan jendela baru, menghapus akses ke window.open tidak akan bermasalah. Jika tidak memerlukan Flash, menonaktifkan dukungan plugin seharusnya tidak menjadi masalah. Kami akan semaksimal mungkin aman jika mengikuti prinsip hak istimewa terendah, dan memblokir setiap fitur yang tidak relevan secara langsung dengan fungsi yang ingin kami gunakan. Hasilnya, kita tidak perlu lagi memercayai secara membabi buta bahwa beberapa bagian konten tersemat tidak akan memanfaatkan hak istimewa yang tidak boleh 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 di iframe memberikan ukuran pemisahan antara aplikasi dan konten yang ingin Anda muat. Konten berbingkai tidak akan memiliki akses ke DOM halaman, atau data yang telah Anda simpan secara lokal, juga tidak akan dapat menggambar ke posisi arbitrer di halaman; cakupannya terbatas pada garis batas frame. Namun, pemisahan ini tidak benar-benar andal. Halaman yang dimuat masih memiliki sejumlah opsi untuk perilaku yang mengganggu atau berbahaya: video, plugin, dan pop-up yang diputar otomatis adalah puncak gunung es.

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

Percaya, tetapi verifikasi

Tombol "Tweet" Twitter adalah contoh bagus fungsi yang dapat disematkan dengan lebih aman di situs Anda melalui sandbox. Twitter memungkinkan Anda menyisipkan 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 mengeksekusi 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 saja; frame tidak perlu memuat plugin apa pun, tidak perlu menavigasi jendela tingkat atas, atau sejumlah fungsi lainnya. Karena tidak memerlukan hak istimewa tersebut, mari kita hapus dengan membuat sandbox konten frame.

Sandboxing berfungsi berdasarkan daftar yang diizinkan. Kita mulai dengan menghapus semua izin yang memungkinkan, lalu mengaktifkan kembali setiap kemampuan dengan menambahkan tanda 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 memberi frame semua kemampuan yang diperlukan, 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 sandboxing dalam contoh di atas, sekarang mari kita pelajari cara kerja atribut secara lebih mendetail.

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

  • JavaScript tidak akan dijalankan dalam dokumen berbingkai. Ini tidak hanya mencakup JavaScript yang dimuat secara eksplisit melalui tag skrip, tetapi juga pengendali peristiwa inline dan URL javascript:. Ini juga berarti bahwa konten yang terdapat dalam tag tanpa skrip akan ditampilkan, persis seolah-olah pengguna telah menonaktifkan skrip sendiri.
  • Dokumen berbingkai dimuat ke dalam origin unik, yang berarti semua pemeriksaan origin yang sama akan gagal; origin unik tidak cocok dengan origin lain, bahkan dengan dirinya sendiri. Di antara dampak lainnya, hal ini berarti dokumen tidak memiliki akses ke data yang disimpan dalam cookie origin apa pun atau mekanisme penyimpanan lainnya (penyimpanan DOM, Indexed DB, dll.).
  • Dokumen berbingkai tidak dapat membuat jendela atau dialog baru (misalnya, melalui window.open atau target="_blank").
  • Formulir tidak dapat dikirim.
  • Plugin tidak akan dimuat.
  • Dokumen berbingkai hanya dapat menavigasi dirinya sendiri, bukan induk tingkat atasnya. 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 secara otomatis, video yang diputar secara otomatis, dsb.) akan diblokir.
  • Kunci kursor tidak dapat diperoleh.
  • Atribut seamless diabaikan pada iframes yang berisi dokumen berbingkai.

Ini sangat ketat, dan dokumen yang dimuat ke dalam iframe dengan sandbox penuh memang sangat sedikit berisiko. Tentu saja, sandbox juga tidak dapat melakukan banyak hal yang bernilai: Anda mungkin dapat menggunakan sandbox penuh untuk beberapa konten statis, tetapi sebagian besar waktu Anda ingin sedikit melonggarkan hal-hal.

Dengan pengecualian plugin, masing-masing batasan ini dapat dicabut dengan menambahkan tanda ke nilai atribut sandbox. Dokumen dengan sandbox tidak akan pernah dapat menjalankan plugin, karena plugin adalah kode native tanpa sandbox, tetapi yang lainnya adalah game yang adil:

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

Dengan mengingat hal ini, kita dapat mengevaluasi dengan tepat mengapa kita mendapatkan serangkaian tanda sandboxing tertentu pada 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 ini akan memunculkan formulir tweet di jendela baru.
  • allow-forms diperlukan, karena formulir tweet harus dapat dikirim.
  • allow-same-origin diperlukan, karena cookie twitter.com tidak akan dapat diakses, dan pengguna tidak dapat login untuk memposting formulir.

Satu hal penting yang perlu diperhatikan adalah tanda 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 bingkai, meskipun formulir hanya ada di jendela tempat bingkai muncul.

Dengan atribut sandbox yang diterapkan, widget hanya mendapatkan izin yang diperlukan, dan kemampuan seperti plugin, navigasi atas, dan kunci pointer tetap diblokir. Kami telah mengurangi risiko penyematan widget, tanpa efek buruk. Hal ini menguntungkan bagi semua orang yang peduli.

Pemisahan Hak Istimewa

Men-sandbox konten pihak ketiga untuk menjalankan kode tidak tepercayanya di lingkungan dengan hak istimewa rendah jelas akan menguntungkan. Namun, bagaimana dengan kode Anda sendiri? Anda percaya diri, bukan? Jadi, mengapa harus khawatir dengan sandboxing?

Saya akan membalikkan pertanyaan tersebut: jika kode Anda tidak memerlukan plugin, mengapa memberinya akses ke plugin? Yang terbaik, ini adalah hak istimewa yang tidak pernah Anda gunakan, paling buruknya adalah vektor potensial bagi penyerang untuk menginjakkan kaki Anda. Kode setiap orang memiliki bug, dan pada dasarnya setiap aplikasi rentan terhadap eksploitasi dengan satu atau cara lain. Dengan melakukan sandboxing pada kode Anda sendiri, meskipun penyerang berhasil menyusup ke aplikasi Anda, mereka tidak akan diberi akses penuh ke asal aplikasi; mereka hanya dapat melakukan hal-hal yang dapat dilakukan aplikasi. Masih buruk, tetapi tidak seburuk yang mungkin terjadi.

Anda dapat mengurangi risiko lebih jauh dengan membagi aplikasi menjadi bagian-bagian logis dan membuat sandbox untuk setiap bagian dengan hak istimewa minimal. Teknik ini sangat umum dalam kode native: Chrome, misalnya, memecah dirinya sendiri 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 dalam mengurai konten yang tidak tepercaya. Perender tidak perlu menyentuh disk, browser akan memberikan semua informasi yang diperlukan untuk merender halaman. Meskipun peretas cerdas menemukan cara untuk merusak perender, dia belum berhasil, karena perender tidak dapat melakukan banyak hal yang menarik dengan sendirinya: semua akses dengan hak istimewa tinggi harus dirutekan melalui proses browser. Penyerang harus menemukan beberapa lubang di bagian sistem yang berbeda agar dapat menimbulkan kerusakan, yang sangat mengurangi risiko serangan pwnage.

Men-sandbox eval() dengan aman

Dengan sandboxing dan postMessage API, keberhasilan model ini cukup mudah diterapkan di web. Bagian aplikasi Anda dapat berada di iframe dengan sandbox, dan dokumen induk dapat melakukan mediasi komunikasi di antara keduanya dengan memposting pesan dan memproses respons. Struktur semacam ini memastikan bahwa eksploitasi dalam salah satu bagian aplikasi melakukan kerusakan seminimal mungkin. Hal ini juga memiliki keuntungan untuk memaksa Anda 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 menggunakan string, dan mengevaluasinya sebagai JavaScript. Wow, bukan? Tepat seperti yang Anda tunggu selama bertahun-tahun. Tentu saja ini adalah aplikasi yang cukup berbahaya karena mengizinkan JavaScript arbitrer untuk dieksekusi berarti setiap dan semua data yang ditawarkan origin akan dapat diperoleh. Kita akan memitigasi risiko terjadinya Hal Buruk™ dengan memastikan bahwa kode dieksekusi di dalam sandbox, sehingga 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 menghubungkan ke peristiwa message objek window. Setiap kali induk mengeksekusi postMessage pada konten iframe, peristiwa ini akan dipicu, sehingga memberi kita akses ke string yang perlu dijalankan oleh induk kita.

Di pengendali, kita mengambil atribut source peristiwa, yang merupakan jendela induk. Kita akan menggunakannya untuk mengirim hasil kerja keras kita 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; kita akan menangkapnya dan melaporkan pesan error yang mudah dipahami. Terakhir, kita memposting hasil kembali ke jendela induk. Ini adalah hal yang cukup sederhana.

Induk juga tidak rumit. Kita akan membuat UI kecil dengan textarea untuk kode, dan button untuk eksekusi, dan kita akan mengambil 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 menyiapkan semuanya untuk dieksekusi. Pertama, kita akan memproses respons dari iframe dan alert() ke pengguna. Aplikasi sungguhan mungkin 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 sejumlah aktivitas menjengkelkan atau berbahaya lainnya.

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

Namun, perhatikan bahwa Anda harus sangat berhati-hati saat menangani konten berbingkai yang berasal dari asal yang sama dengan induk. 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 sepenuhnya.

Bermain di sandbox

Sandboxing tersedia untuk Anda sekarang di berbagai browser: Firefox 17+, IE10+, dan Chrome pada saat penulisan ini (kaniuse, tentu saja, memiliki tabel dukungan terbaru). Dengan menerapkan atribut sandbox ke iframes yang Anda sertakan, Anda dapat memberikan hak istimewa tertentu ke konten yang ditampilkannya, 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 yang sudah dapat dilakukan dengan Kebijakan Keamanan Konten.

Selain itu, sandboxing adalah teknik yang efektif untuk mengurangi risiko penyerang pintar yang dapat mengeksploitasi celah dalam kode Anda sendiri. Dengan memisahkan aplikasi monolitik menjadi sekumpulan 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. Itu adalah tugas yang jauh lebih sulit, terutama karena cakupan pengontrol dapat dikurangi secara signifikan. Anda dapat menghabiskan upaya terkait keamanan untuk mengaudit kode tersebut jika meminta bantuan browser untuk hal lainnya.

Namun, bukan berarti sandboxing adalah solusi lengkap untuk masalah keamanan di internet. Layanan ini menawarkan pertahanan yang mendalam, dan jika Anda tidak memiliki kontrol atas klien pengguna, Anda belum dapat mengandalkan dukungan browser untuk semua pengguna (jika Anda mengontrol klien pengguna Anda -- lingkungan perusahaan, misalnya -- 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 sangat bagus. Sebaiknya gunakan kode ini.

Bacaan Lebih Lanjut

  • "Pemisahan Hak Istimewa dalam Aplikasi HTML5" adalah makalah menarik yang membahas desain framework kecil, dan penerapannya ke tiga aplikasi HTML5 yang ada.

  • Sandboxing 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 berbingkai. Keduanya memiliki dukungan browser yang cukup buruk saat ini (Chrome dan WebKit nightly). Namun, keduanya akan menjadi kombinasi yang menarik di masa mendatang. Misalnya, Anda dapat menempatkan komentar dalam sandbox di 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>