Bagikan melalui


Mencegah Scripting Lintas Situs (XSS) di ASP.NET Core

Oleh Rick Anderson

Scripting Lintas Situs (XSS) adalah kerentanan keamanan yang memungkinkan penyerang menempatkan skrip sisi klien (biasanya JavaScript) ke halaman web. Ketika pengguna lain memuat halaman yang terpengaruh, skrip penyerang berjalan, memungkinkan penyerang untuk mencuri cookietoken s dan sesi, mengubah konten halaman web melalui manipulasi DOM, atau mengalihkan browser ke halaman lain. Kerentanan XSS umumnya terjadi ketika aplikasi mengambil input pengguna dan mengeluarkannya ke halaman tanpa memvalidasi, mengodekan, atau melarikannya.

Artikel ini berlaku terutama untuk ASP.NET Core MVC dengan tampilan, Razor Pages, dan aplikasi lain yang mengembalikan HTML yang mungkin rentan terhadap XSS. API Web yang mengembalikan data dalam bentuk HTML, XML, atau JSON dapat memicu serangan XSS di aplikasi klien mereka jika mereka tidak membersihkan input pengguna dengan benar, tergantung pada seberapa banyak kepercayaan tempat aplikasi klien di API. Misalnya, jika API menerima konten yang dihasilkan pengguna dan mengembalikannya dalam respons HTML, penyerang dapat menyuntikkan skrip berbahaya ke dalam konten yang dijalankan saat respons dirender di browser pengguna.

Untuk mencegah serangan XSS, API web harus menerapkan validasi input dan pengodean output. Validasi input memastikan bahwa input pengguna memenuhi kriteria yang diharapkan dan tidak menyertakan kode berbahaya. Pengodean output memastikan bahwa data apa pun yang dikembalikan oleh API dibersihkan dengan benar sehingga tidak dapat dijalankan sebagai kode oleh browser pengguna. Untuk informasi lebih lanjut, lihat masalah GitHub ini.

Melindungi aplikasi Anda dari XSS

Pada tingkat dasar, XSS bekerja dengan mengelabui aplikasi Anda untuk menyisipkan tag ke halaman yang <script> dirender, atau dengan menyisipkan On* peristiwa ke dalam elemen. Pengembang harus menggunakan langkah-langkah pencegahan berikut untuk menghindari pengenalan XSS ke dalam aplikasi mereka:

  1. Jangan pernah memasukkan data yang tidak tepercaya ke dalam input HTML Anda, kecuali Anda mengikuti langkah-langkah lainnya di bawah ini. Data yang tidak tepercaya adalah data apa pun yang dapat dikontrol oleh penyerang, seperti input formulir HTML, string kueri, header HTTP, atau bahkan data yang bersumber dari database, karena penyerang mungkin dapat melanggar database Anda meskipun mereka tidak dapat melanggar aplikasi Anda.

  2. Sebelum menempatkan data yang tidak tepercaya di dalam elemen HTML, pastikan data tersebut dikodekan HTML. Pengodean HTML mengambil karakter seperti < dan mengubahnya menjadi bentuk aman seperti <

  3. Sebelum memasukkan data yang tidak tepercaya ke dalam atribut HTML, pastikan data tersebut dikodekan HTML. Pengodean atribut HTML adalah superset pengodean HTML dan mengodekan karakter tambahan seperti " dan ".

  4. Sebelum memasukkan data yang tidak tepercaya ke JavaScript, tempatkan data dalam elemen HTML yang kontennya Anda ambil saat runtime. Jika ini tidak memungkinkan, pastikan data dikodekan JavaScript. Pengodean JavaScript mengambil karakter berbahaya untuk JavaScript dan menggantinya dengan hex mereka, misalnya, < akan dikodekan sebagai \u003C.

  5. Sebelum memasukkan data yang tidak tepercaya ke dalam string kueri URL, pastikan url tersebut dikodekan.

Pengodean HTML menggunakan Razor

Mesin yang Razor digunakan dalam MVC secara otomatis mengodekan semua output yang bersumber dari variabel, kecuali Anda bekerja sangat keras untuk mencegahnya melakukannya. Ini menggunakan aturan pengodean atribut HTML setiap kali Anda menggunakan direktif @ . Karena pengodean atribut HTML adalah superset pengodean HTML, ini berarti Anda tidak perlu khawatir dengan apakah Anda harus menggunakan pengodean atribut HTML atau pengodean atribut HTML. Anda harus memastikan bahwa Anda hanya menggunakan @ dalam konteks HTML, bukan saat mencoba menyisipkan input yang tidak tepercaya langsung ke JavaScript. Pembantu tag juga akan mengodekan input yang Anda gunakan dalam parameter tag.

Ambil tampilan berikut Razor :

@{
    var untrustedInput = "<\"123\">";
}

@untrustedInput

Tampilan ini menghasilkan konten variabel untrustedInput . Variabel ini mencakup beberapa karakter yang digunakan dalam serangan XSS, yaitu <, " dan >. Memeriksa sumber menunjukkan output yang dirender dikodekan sebagai:

&lt;&quot;123&quot;&gt;

Peringatan

ASP.NET Core MVC menyediakan HtmlString kelas yang tidak dikodekan secara otomatis setelah output. Ini tidak boleh digunakan dalam kombinasi dengan input yang tidak tepercaya karena ini akan mengekspos kerentanan XSS.

Pengodean JavaScript menggunakan Razor

Mungkin ada kalanya Anda ingin menyisipkan nilai ke Dalam JavaScript untuk diproses dalam tampilan Anda. Ada dua cara untuk melakukannya. Cara paling aman untuk menyisipkan nilai adalah dengan menempatkan nilai dalam atribut data tag dan mengambilnya di JavaScript Anda. Misalnya:

@{
    var untrustedInput = "<script>alert(1)</script>";
}

<div id="injectedData"
     data-untrustedinput="@untrustedInput" />

<div id="scriptedWrite" />
<div id="scriptedWrite-html5" />

<script>
    var injectedData = document.getElementById("injectedData");

    // All clients
    var clientSideUntrustedInputOldStyle =
        injectedData.getAttribute("data-untrustedinput");

    // HTML 5 clients only
    var clientSideUntrustedInputHtml5 =
        injectedData.dataset.untrustedinput;

    // Put the injected, untrusted data into the scriptedWrite div tag.
    // Do NOT use document.write() on dynamically generated data as it
    // can lead to XSS.

    document.getElementById("scriptedWrite").innerText += clientSideUntrustedInputOldStyle;

    // Or you can use createElement() to dynamically create document elements
    // This time we're using textContent to ensure the data is properly encoded.
    var x = document.createElement("div");
    x.textContent = clientSideUntrustedInputHtml5;
    document.body.appendChild(x);

    // You can also use createTextNode on an element to ensure data is properly encoded.
    var y = document.createElement("div");
    y.appendChild(document.createTextNode(clientSideUntrustedInputHtml5));
    document.body.appendChild(y);

</script>

Markup sebelumnya menghasilkan HTML berikut:

<div id="injectedData"
     data-untrustedinput="&lt;script&gt;alert(1)&lt;/script&gt;" />

<div id="scriptedWrite" />
<div id="scriptedWrite-html5" />

<script>
    var injectedData = document.getElementById("injectedData");

    // All clients
    var clientSideUntrustedInputOldStyle =
        injectedData.getAttribute("data-untrustedinput");

    // HTML 5 clients only
    var clientSideUntrustedInputHtml5 =
        injectedData.dataset.untrustedinput;

    // Put the injected, untrusted data into the scriptedWrite div tag.
    // Do NOT use document.write() on dynamically generated data as it can
    // lead to XSS.

    document.getElementById("scriptedWrite").innerText += clientSideUntrustedInputOldStyle;

    // Or you can use createElement() to dynamically create document elements
    // This time we're using textContent to ensure the data is properly encoded.
    var x = document.createElement("div");
    x.textContent = clientSideUntrustedInputHtml5;
    document.body.appendChild(x);

    // You can also use createTextNode on an element to ensure data is properly encoded.
    var y = document.createElement("div");
    y.appendChild(document.createTextNode(clientSideUntrustedInputHtml5));
    document.body.appendChild(y);

</script>

Kode sebelumnya menghasilkan output berikut:

<script>alert(1)</script>
<script>alert(1)</script>
<script>alert(1)</script>

Peringatan

JANGAN menggabungkan input yang tidak tepercaya di JavaScript untuk membuat elemen DOM atau menggunakan document.write() pada konten yang dihasilkan secara dinamis.

Gunakan salah satu pendekatan berikut untuk mencegah kode terekspos ke XSS berbasis DOM:

  • createElement() dan tetapkan nilai properti dengan metode atau properti yang sesuai seperti node.textContent= atau node.InnerText=.
  • document.CreateTextNode() dan tambahkan di lokasi DOM yang sesuai.
  • element.SetAttribute()
  • element[attribute]=

Mengakses encoder dalam kode

Encoder HTML, JavaScript, dan URL tersedia untuk kode Anda dengan dua cara:

  • Masukkan melalui injeksi dependensi.
  • Gunakan encoder default yang terkandung dalam System.Text.Encodings.Web namespace.

Saat menggunakan encoder default, maka kustomisasi apa pun yang diterapkan ke rentang karakter yang akan diperlakukan sebagai aman tidak akan berlaku. Encoder default menggunakan aturan pengodean paling aman yang mungkin.

Untuk menggunakan encoder yang dapat dikonfigurasi melalui DI, konstruktor Anda harus mengambil parameter HtmlEncoder, JavaScriptEncoder , dan UrlEncoder sebagaimana mestinya. Contohnya;

public class HomeController : Controller
{
    HtmlEncoder _htmlEncoder;
    JavaScriptEncoder _javaScriptEncoder;
    UrlEncoder _urlEncoder;

    public HomeController(HtmlEncoder htmlEncoder,
                          JavaScriptEncoder javascriptEncoder,
                          UrlEncoder urlEncoder)
    {
        _htmlEncoder = htmlEncoder;
        _javaScriptEncoder = javascriptEncoder;
        _urlEncoder = urlEncoder;
    }
}

Parameter URL Pengodean

Jika Anda ingin membuat string kueri URL dengan input yang tidak tepercaya sebagai nilai, gunakan UrlEncoder untuk mengodekan nilai. Contohnya,

var example = "\"Quoted Value with spaces and &\"";
var encodedValue = _urlEncoder.Encode(example);

Setelah mengodekan variabel encodedValue berisi %22Quoted%20Value%20with%20spaces%20and%20%26%22. Spasi, tanda kutip, tanda baca, dan karakter tidak aman lainnya dikodekan persen ke nilai heksadesimalnya, misalnya karakter spasi akan menjadi %20.

Peringatan

Jangan gunakan input yang tidak tepercaya sebagai bagian dari jalur URL. Selalu berikan input yang tidak tepercaya sebagai nilai string kueri.

Menyesuaikan Encoder

Secara default, encoder menggunakan daftar aman yang terbatas pada rentang Unicode Latin Dasar dan mengodekan semua karakter di luar rentang tersebut sebagai kode karakter yang setara. Perilaku ini juga memengaruhi Razor penyajian TagHelper dan HtmlHelper karena menggunakan encoder untuk menghasilkan string Anda.

Alasan di balik ini adalah untuk melindungi dari bug browser yang tidak diketahui atau di masa depan (bug browser sebelumnya telah tersandung penguraian berdasarkan pemrosesan karakter non-Bahasa Inggris). Jika situs web Anda banyak menggunakan karakter non-Latin, seperti Tionghoa, Sirilik atau lainnya, ini mungkin bukan perilaku yang Anda inginkan.

Daftar aman encoder dapat disesuaikan untuk menyertakan rentang Unicode yang sesuai dengan aplikasi selama startup, di Program.cs:

Misalnya, menggunakan konfigurasi default menggunakan HtmlHelper yang Razor mirip dengan yang berikut ini:

<p>This link text is in Chinese: @Html.ActionLink("汉语/漢語", "Index")</p>

Markup sebelumnya dirender dengan teks Bahasa Tionghoa yang dikodekan:

<p>This link text is in Chinese: <a href="/">&#x6C49;&#x8BED;/&#x6F22;&#x8A9E;</a></p>

Untuk memperlebar karakter yang diperlakukan sebagai aman oleh encoder, masukkan baris berikut ke dalam Program.cs.:

builder.Services.AddSingleton<HtmlEncoder>(
     HtmlEncoder.Create(allowedRanges: new[] { UnicodeRanges.BasicLatin,
                                               UnicodeRanges.CjkUnifiedIdeographs }));

Anda dapat menyesuaikan daftar aman encoder untuk menyertakan rentang Unicode yang sesuai dengan aplikasi Anda selama startup, di ConfigureServices().

Misalnya, menggunakan konfigurasi default, Anda mungkin menggunakan Razor HtmlHelper seperti itu;

<p>This link text is in Chinese: @Html.ActionLink("汉语/漢語", "Index")</p>

Saat Anda melihat sumber halaman web, Anda akan melihatnya telah dirender sebagai berikut, dengan teks Tionghoa dikodekan;

<p>This link text is in Chinese: <a href="/">&#x6C49;&#x8BED;/&#x6F22;&#x8A9E;</a></p>

Untuk memperlebar karakter yang diperlakukan sebagai aman oleh encoder, Anda akan memasukkan baris berikut ke ConfigureServices() dalam metode di startup.cs;

services.AddSingleton<HtmlEncoder>(
     HtmlEncoder.Create(allowedRanges: new[] { UnicodeRanges.BasicLatin,
                                               UnicodeRanges.CjkUnifiedIdeographs }));

Contoh ini meluaskan daftar aman untuk menyertakan Rentang Unicode CjkUnifiedIdeographs. Output yang dirender sekarang akan menjadi

<p>This link text is in Chinese: <a href="/">汉语/漢語</a></p>

Brankas rentang daftar ditentukan sebagai bagan kode Unicode, bukan bahasa. Standar Unicode memiliki daftar bagan kode yang dapat Anda gunakan untuk menemukan bagan yang berisi karakter Anda. Setiap encoder, Html, JavaScript, dan Url, harus dikonfigurasi secara terpisah.

Catatan

Kustomisasi daftar aman hanya memengaruhi encoder yang bersumber melalui DI. Jika Anda langsung mengakses encoder melalui System.Text.Encodings.Web.*Encoder.Default default, daftar aman Hanya Latin Dasar yang akan digunakan.

Di mana pengodean harus dilakukan?

Praktik umum yang diterima adalah bahwa pengodean terjadi pada titik output dan nilai yang dikodekan tidak boleh disimpan dalam database. Pengodean pada titik output memungkinkan Anda mengubah penggunaan data, misalnya, dari HTML ke nilai string kueri. Ini juga memungkinkan Anda untuk dengan mudah mencari data Anda tanpa harus mengodekan nilai sebelum mencari dan memungkinkan Anda untuk memanfaatkan perubahan atau perbaikan bug yang dibuat untuk encoder.

Validasi sebagai teknik pencegahan XSS

Validasi dapat menjadi alat yang berguna dalam membatasi serangan XSS. Misalnya, string numerik yang hanya berisi karakter 0-9 tidak akan memicu serangan XSS. Validasi menjadi lebih rumit saat menerima HTML dalam input pengguna. Mengurai input HTML sulit, jika bukan tidak mungkin. Markdown, ditambah dengan pengurai yang menghapus HTML yang disematkan, adalah opsi yang lebih aman untuk menerima input yang kaya. Jangan pernah mengandalkan validasi saja. Selalu kodekan input yang tidak tepercaya sebelum output, apa pun validasi atau sanitasi apa yang telah dilakukan.