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 berperan penting dalam pengalaman pengguna secara keseluruhan, dan konten buatan pengguna terkadang bahkan lebih penting daripada konten native situs. Tidak menggunakan keduanya bukanlah suatu opsi, tetapi keduanya meningkatkan risiko terjadinya Hal Buruk™ 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. Terkadang, akan lebih baik jika Anda mengatakan "Saya tidak yakin saya benar-benar mempercayai sumber konten ini, tetapi kontennya sangat bagus. Sebaiknya browser, sematkan, tetapi jangan sampai merusak situs saya."

Hak Istimewa Terendah

Pada dasarnya, kita mencari mekanisme yang akan 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, kami tidak lagi harus percaya 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 dalam bingkai tidak akan memiliki akses ke DOM halaman, atau data yang Anda simpan secara lokal, dan juga tidak akan dapat menggambar ke posisi arbitrer di halaman; cakupannya terbatas pada garis batas bingkai. 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 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 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 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. Kami telah memberikan semua kemampuan yang diperlukan ke frame, dan browser akan menolak aksesnya ke hak istimewa yang tidak kami 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 yang memiliki atribut sandbox kosong, dokumen berbingkai akan sepenuhnya di-sandbox, sehingga dokumen tersebut akan dikenai batasan berikut:

  • JavaScript tidak akan dieksekusi dalam dokumen berbingkai. Hal 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 noscript akan ditampilkan, persis seperti 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 bahwa dokumen tidak memiliki akses ke data yang disimpan dalam cookie asal atau mekanisme penyimpanan lainnya (penyimpanan DOM, DB Terindek, 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, setiap 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 (kejutan!) penguncian kursor.
  • 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 dipicu secara otomatis (karena mudah diterapkan melalui JavaScript).
  • allow-top-navigation memungkinkan dokumen keluar dari bingkai dengan membuka jendela tingkat atas.

Dengan mempertimbangkan hal ini, kita dapat mengevaluasi dengan tepat mengapa kita mendapatkan kumpulan flag sandboxing 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 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. Semua pihak akan diuntungkan.

Pemisahan Hak Istimewa

Men-sandbox konten pihak ketiga untuk menjalankan kode yang tidak tepercaya di lingkungan dengan hak istimewa rendah jelas bermanfaat. 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? Pada kondisi terbaik, ini adalah hak istimewa yang tidak pernah Anda gunakan, dan pada kondisi terburuk, ini adalah vektor potensial bagi penyerang untuk masuk. Kode semua orang memiliki bug, dan hampir setiap aplikasi rentan terhadap eksploitasi dengan cara tertentu. Dengan menerapkan sandbox 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, 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 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 celah di berbagai bagian sistem agar dapat melakukan kerusakan, yang sangat mengurangi risiko keberhasilan pwnage.

Men-sandbox eval() dengan aman

Dengan sandboxing dan postMessage API, keberhasilan model ini cukup mudah diterapkan ke 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 di salah satu bagian aplikasi melakukan kerusakan minimum. 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 bahwa semua data yang ditawarkan origin dapat diambil. Kita akan memitigasi risiko Bad Things™ yang terjadi 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 terpicu, sehingga memberi kita akses ke string yang ingin dijalankan oleh induk.

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. Hal ini cukup mudah.

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 menghubungkan semuanya untuk dieksekusi. Pertama, kita akan memproses respons dari iframe dan alert() ke pengguna. Mungkin aplikasi sebenarnya 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, 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 dengan baik.

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 kini tersedia untuk Anda di berbagai browser: Firefox 17+, IE10+, dan Chrome pada saat penulisan (caniuse, 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 serangkaian layanan dengan 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. Fitur ini menawarkan pertahanan menyeluruh, 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 -- selamat!). 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>