Jangan Pernah Percaya Terhadap User - CRUDPRO

Jangan Pernah Percaya Terhadap User

Tujuan utama developer adalah untuk mendapatkan kepercayaan dari user. Kami ingin mereka memercayai kode kami, aplikasi kami, dan merek kami. Jika user mempercayai kami, mereka akan kembali. Mereka hidup dengan menerima bug jahat dan fitur yang hilang kami tidak menginginkan ini, tetapi itu memang terjadi, semuanya berfungsi, dan datanya aman. Dan kami tidak membuang waktu mereka. Tetapi sebagai developer, kami tidak pernah bisa mempercayai user kami. Sangat jauh!

Ketika berbicara tentang kepercayaan user, itu berarti input user tidak dapat dipercaya. Anda tidak dapat mempercayai untuk memberikan informasi yang benar, menggunakan format yang benar, atau hanya melakukan tindakan yang diharapkan. Masalahnya adalah bahwa user adalah makhluk yang kompleks dan membuat frustrasi. Beberapa user jahat dan mencari cara untuk menghancurkan atau membahayakan aplikasi. Orang lain berpikir berbeda dari Anda dan Anda mungkin melakukan hal-hal tak terduga-menghadapi bug dan mengakses data yang seharusnya tidak mereka akses (istri saya mengkhususkan diri dalam hal ini) Ini rumah!). Dan yang lain hanya akan tersesat dan bingung dan akan mencoba hal-hal kembali ke tempat mereka mulai.

Oleh karena itu, tugas kami sebagai developer adalah melindungi aplikasi dari user. Kami harus paranoid tentang masukan yang kami terima, dan kami perlu mengambil beberapa pertahanan untuk memastikan bahwa aplikasi tersebut aman, apa pun yang dilakukan user. Oh, dan entah bagaimana mereka akan mempercayai kami karena kami membujuk mereka untuk tidak mempercayai user kami. Mudah?Jadi hari ini, kita akan melihat beberapa cara untuk menghindari kepercayaan user.

Validasi Input

Ketika Anda memikirkan "input" ke aplikasi Anda, Anda langsung memikirkan formulir dan data formulir. Minta user untuk memberikan beberapa informasi atau menjawab beberapa pertanyaan, dan menerima dan menyimpan input yang diberikan oleh user. Perhatikan format berikut:

Dari informasi yang dikumpulkan di sini, sangat mudah untuk mengasumsikan bahwa alamat email adalah alamat email yang valid (terutama jika Anda menggunakan kolom input email HTML5) dan negara tersebut adalah negara yang valid. Karena ini adalah select box.

Namun, Anda tidak dapat mempercayai user Anda dengan membuat asumsi ini. user dapat memodifikasi HTML di browser untuk membuat field input ini benar-benar menerima apapun.

Sebaliknya, Anda harus eksplisit tentang validasi input:
  • Nama depan Nama Belakang
    • kolom yang harus diisi
    • Harus berupa string
    • Panjang maksimum 255
  • alamat email
    • kolom yang harus diisi
    • Harus berupa string
    • Harus dalam format alamat email yang dapat dikenali
    • Panjang maksimum 255
    • Tidak boleh digunakan oleh user lain dalam database
  • Negara
    • kolom yang harus diisi
    • Harus disertakan dalam daftar negara yang diizinkan sebelumnya

Sekarang aturan eksplisit telah didefinisikan, Anda dapat menggunakanvalidation component Laravel yang sangat kuat untuk memvalidasi input Anda dan memastikan itu yang Anda harapkan.

$data = $request->validate([
    'first_name' => ['required', 'string', 'max:255'],
    'Last_name'  => ['required', 'string', 'max:255'],
    'email'      => ['required', 'string', 'email', 'max:255', 'unique:users'],
    'country'    => ['required', Rule::in($this->allowedCountries())],
]);

Jika validator lolos, Anda tahu bahwa $data berisi nilai yang sesuai dengan harapan Anda. Nilai-nilai ini disimpan dengan aman di database dan dapat digunakan dengan hati-hati di seluruh aplikasi. Negara ini sama persis dengan daftar yang diizinkan, sehingga dapat digunakan dengan aman di mana saja. Anda juga tahu bahwa email dalam format yang valid, sehingga Anda dapat mulai mengirim pemberitahuan.

Di sisi lain, nama depan dan belakang adalah sebagai berikut.

Sebelum melanjutkan, penting untuk dicatat bahwa validator hanya mengembalikan kunci yang terdapat dalam aturan validasi. Ini berarti bahwa data tambahan apa pun yang akan dikirimkan dalam formulir akan diabaikan, sehingga aman untuk diteruskan langsung ke model melalui create(), fill(), atau update(), sekaligus. Menghindari kerentanan yang disebut Alokasi Kerentanan. Ini adalah cara saya selalu menyimpan data dalam model saya.

Parameter Query

Jadi saya menerapkan formulir dan meminta data yang diberikan kepada user. Tetapi sekarang Anda perlu membuat beberapa query basis data untuk menggunakan data tersebut. Di sini sekali lagi adalah masalah kepercayaan yang buruk itu. Dalam Laravel, query biasanya ditulis sebagai berikut:

$name = $request->query(‘name');
$user = DB::table('users')->where('name', $name)->get();

Komponen penting di sini adalah metode where(), dan parameter kedua ($name) diambil langsung dari input user. Laravel akan secara otomatis menyertakan parameter kedua ini sebagai query berparameter. Ini mencegah injeksi SQL diteruskan langsung ke database. Ini adalah salah satu fitur hebat Laravel, membuatnya sulit untuk secara tidak sengaja membuat queri yang rentan.

Tetapi bagaimana jika Anda membutuhkan query yang sangat kompleks, atau jika Anda ingin menggunakan logika khusus mesin basis data? Atau bagaimana jika Anda perlu menjalankan query yang jauh lebih efisien untuk menulis dalam SQL mentah? Anda mungkin perlu mendalami query khusus. Ini adalah saat query berparameter penting.

Pertimbangkan kode ini:
public function store(Request $request)
{
    $data = $request->validate([
        'game' => ['required'],
        'date' => ['required'],
    ])

    DB::statement("UPDATE game_user SET turns += event_increment WHERE game_id = {$data['game']}");
}

Ketika developer menulis kode ini, mereka mengharapkan $data['game'] menjadi bilangan bulat ketika dimasukkan ke dalam query (dan mereka mengatakan bahwa membandingkan bilangan bulat mentah akan lebih cepat. Saya sedang membaca!). Tentu, itu melewati browser, tetapi mereka mengira itu adalah field tersembunyi dan tidak ada yang akan menyadarinya! Tapi itu masih field input dan peretas dapat mengubahnya sebanyak yang mereka inginkan.

Seperti disebutkan sebelumnya, Laravel menyertakan parameterisasi yang dibangun ke dalam pembuat query, yang dapat Anda gunakan dengan mudah saat membuat query manual. Alih-alih memasukkan variabel secara langsung ke dalam string query, ganti dengan tanda tanya (?) Dan sertakan sebagai parameter kedua dalam pemanggilan metode. Basis data memahami bahwa tanda tanya adalah tempat penampung dan tahu cara mengganti parameter dengan aman dengan query pada waktu proses. Dalam hal ini, Anda dapat melakukan hal berikut:

DB::statement("UPDATE game_user SET turns += event_increment WHERE game_id = ?", [$data['game']]);

Pada dasarnya, semua metode query Laravel mendukung parameter dengan cara ini, jadi tidak ada alasan untuk tidak menggunakannya. (Sebagai catatan tambahan, Anda dapat memperbaiki kerentanan khusus ini dengan memvalidasi input integer. Idealnya, lakukan keduanya untuk perlindungan yang lebih baik.)

Jika Anda ingin tahu lebih banyak tentang query berparameter, kami berbicara tentang Keamanan Pertahanan dalam Kedalaman Laravel. Sebaiknya periksa database resmi Laravel dan dokumentasi Eloquent.

Sebelum kita melangkah lebih jauh, mari kita lihat sekilas apa yang dapat dilakukan peretas dengan query yang rentan ini. Untuk menambahkan konteks, menjalankan query ini akan meningkatkan jumlah giliran game untuk semua user sebesar jumlah event_increment. Ini menguntungkan semua user dalam game secara setara. Jika saya seorang peretas yang menemukan kerentanan ini, saya hanya ingin meningkatkan giliran saya dalam game. Anda dapat menggunakan serangkaian tebakan dan coba-coba untuk menghasilkan input berikut untuk dikirim ke field game:

user_id = (SELECT id FROM users WHERE email = '[email protected]' LIMIT 1)
Mengirim ini ke aplikasi Anda akan menghasilkan query SQL berikut:
DB::statement("UPDATE game_user SET turns += event_increment WHERE game_id = 42 && user_id = (SELECT id FROM users WHERE email = `[email protected]` LIMIT 1)");
Dibersihkan sedikit:
UPDATE game_user
SET turns += event_increment
WHERE game_id = 42
  AND user_id = (
    SELECT id
    FROM users
    WHERE email = '[email protected]'
    LIMIT 1
  )
itu saja. Giliran saya meningkat, tetapi tidak ada orang lain. Ini sangat sederhana karena kurangnya validasi dan parameterisasi query.

Ini adalah contoh sederhana, tetapi Anda dapat melakukan banyak hal dengan injeksi SQL. Ada banyak trik untuk mengekstrak informasi, seperti ketika halaman tidak mengembalikan umpan balik yang terlihat (atau pesan kesalahan!). Untuk informasi lebih lanjut, pada bulan Oktober kami terjun ke injeksi SQL dengan Defense in Depth Laravel Security. Ini termasuk aplikasi web yang secara sengaja rentan yang dapat melakukan serangan SQLi mereka sendiri. Lihat disini.

Escaping Outputs

Pada titik ini, Anda mungkin ingin saya menyelesaikannya dengan cepat. Kemudian Anda dapat kembali dan memastikan bahwa semua validasi cukup eksplisit dan query diparameterisasi dengan benar. (Setiap kali saya menulis tentang sesuatu seperti ini, saya tahu ada kilas balik yang mengerikan pada kode yang sangat rentan yang saya tulis di masa lalu!)

Namun, ada satu hal yang ingin saya tingkatkan sebelum menyelesaikan di sini. Itu untuk menghindari data dengan benar. Jika Anda mengingat satu hal, ingatlah bahwa Anda harus menyingkir untuk menghindari useran tag blade yang tidak lolos.

Berikut adalah hal-hal ini:
{!! $variable !!}
Tolong jangan gunakan.

Anda harus berhati-hati tentang apa yang Anda cetak pada halaman. Pertimbangkan input lama menggunakan nama dan nama user. Bagaimana jika user mengirimkan ini sebagai nama?

Stephen <script src="https://evilhacker.dev/evil.js"></script>
Dan kemudian kami melakukan ini di halaman:
{{ $user->first_name }}

Seperti yang dapat Anda bayangkan, tag nama dan skrip dicetak dalam teks biasa. Ini benar-benar aman untuk dimuat, dan mudah untuk melihat bahwa mereka mencoba menyuntikkan beberapa JavaScript jahat. Tetapi bagaimana jika kodenya terlihat seperti ini:

// Controller
$links = $pages
    ->map(fn ($page) => "<a href=\"{$page->url}\">{$user->first_name}</a>")
    ->join(', ', ' and ');
return view('pages', ['links' => $links])
// Template
<div>
    {!! $links !!}
</div>

Yang Anda lihat hanyalah "Stephen" dan Javascript jahat berjalan di browser Anda untuk melakukan apa pun yang diinginkan peretas. Sangat mudah untuk melihat mengapa developer tiba di solusi ini, tetapi aplikasi tetap terbuka lebar untuk XSS.

Jadi apa yang harus aku lakukan? Saya mengatakan kepada Anda untuk menghindari keluaran yang tidak lolos, tetapi bagaimana saya bisa menmpilkan sesuatu seperti ini dengan aman? Laravel menyediakan dua pembantu untuk digunakan dengan instance ini:
  1. Dengan tag {{$value}}, ada fungsi e($value) yang benar-benar keluar dari output. Anda dapat menyebutnya di mana saja.
  2. Membungkus sesuatu di dalam kelas Illuminate\Support\HtmlString melewati pelarian.
Mari kita lihat aksinya:
// Controller
$links = $pages
    ->map(fn ($page) => "<a href=\"{$page->url}\">".e($user->first_name)."</a>")
    ->join(', ', ' and ');
return view('pages', ['links' => new HtmlString($links)])
// Template
<div>
 {{ $links }}
</div>

Anda dapat melewati kelas HtmlString dan menggunakan tag keluaran yang tidak lolos, tetapi saya menyukai pendekatan ini karena disengaja di baliknya. Sengaja keluar dari data user, dengan sengaja membungkus HTML yang dihasilkan dalam sebuah kelas, lalu dengan sengaja meneruskannya ke tampilan dengan tag keluaran yang aman. Anda tahu data dan format apa yang selalu ada.

Artinya, Anda tidak mempercayai data user pada setiap tahap proses. Ini adalah kunci untuk menjaga keamanan aplikasi Anda. Jangan pernah mempercayai user Anda. Jika Anda ingin menggali lebih dalam topik ini, November lalu saya menjelaskan cara keluar dari keluaran dengan aman dengan pertahanan yang mendalam. Jika Anda ingin memahami cara kerja XSS, ada beberapa tantangan skrip lintas situs.