PostgreSQL
 sql >> Teknologi Basis Data >  >> RDS >> PostgreSQL

Pengguna aplikasi vs. Keamanan Tingkat Baris

Beberapa hari yang lalu saya telah membuat blog tentang masalah umum dengan peran dan hak istimewa yang kami temukan selama peninjauan keamanan.

Tentu saja, PostgreSQL menawarkan banyak fitur terkait keamanan tingkat lanjut, salah satunya adalah Keamanan Tingkat Baris (RLS), tersedia sejak PostgreSQL 9.5.

Karena 9.5 dirilis pada Januari 2016 (jadi hanya beberapa bulan yang lalu), RLS adalah fitur yang cukup baru dan kami belum benar-benar berurusan dengan banyak penerapan produksi. Sebaliknya RLS adalah subjek umum dari diskusi "bagaimana menerapkan", dan salah satu pertanyaan paling umum adalah bagaimana membuatnya bekerja dengan pengguna tingkat aplikasi. Jadi mari kita lihat solusi apa yang mungkin ada.

Pengantar RLS

Mari kita lihat contoh yang sangat sederhana terlebih dahulu, menjelaskan tentang apa RLS itu. Katakanlah kita memiliki chat tabel menyimpan pesan yang dikirim antar pengguna – pengguna dapat menyisipkan baris ke dalamnya untuk mengirim pesan ke pengguna lain, dan memintanya untuk melihat pesan yang dikirim kepada mereka oleh pengguna lain. Jadi tabelnya mungkin terlihat seperti ini:

CREATE TABLE chat ( message_uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(), message_time TIMESTAMP NOT NULL DEFAULT now(), message_from NAME NOT NULL DEFAULT current_user, message_to NAME NOT NULL, message_subject VARCHAR(64) NOT NULL, message_body NULL, /pra> 

Keamanan berbasis peran klasik hanya memungkinkan kita untuk membatasi akses ke seluruh tabel atau irisan vertikal (kolom). Jadi kami tidak dapat menggunakannya untuk mencegah pengguna membaca pesan yang ditujukan untuk pengguna lain, atau mengirim pesan dengan message_from palsu lapangan.

Dan untuk itulah RLS – memungkinkan Anda membuat aturan (kebijakan) yang membatasi akses ke subset baris. Jadi misalnya Anda dapat melakukan ini:

BUAT KEBIJAKAN chat_policy PADA chat MENGGUNAKAN ((message_to =current_user) ATAU (message_from =current_user)) DENGAN CEK (message_from =current_user)

Kebijakan ini memastikan pengguna hanya dapat melihat pesan yang dikirim olehnya atau ditujukan untuknya – itulah ketentuan dalam USING klausa tidak. Bagian kedua dari kebijakan (WITH CHECK ) memastikan pengguna hanya dapat menyisipkan pesan dengan nama pengguna di message_from kolom, mencegah pesan dengan pengirim palsu.

Anda juga dapat membayangkan RLS sebagai cara otomatis untuk menambahkan kondisi WHERE tambahan. Anda dapat melakukannya secara manual di tingkat aplikasi (dan sebelum orang RLS sering melakukannya), tetapi RLS melakukannya dengan cara yang andal dan aman (misalnya, banyak upaya dilakukan untuk mencegah berbagai kebocoran informasi).

Catatan :Sebelum RLS, cara populer untuk mencapai sesuatu yang serupa adalah membuat tabel tidak dapat diakses secara langsung (cabut semua hak istimewa), dan menyediakan serangkaian fungsi penentu keamanan untuk mengaksesnya. Itu sebagian besar mencapai tujuan yang sama, tetapi fungsi memiliki berbagai kelemahan – mereka cenderung membingungkan pengoptimal, dan sangat membatasi fleksibilitas (jika pengguna perlu melakukan sesuatu dan tidak ada fungsi yang cocok untuk itu, dia kurang beruntung). Dan tentu saja, Anda harus menulis fungsi-fungsi tersebut.

Pengguna aplikasi

Jika Anda membaca dokumentasi resmi tentang RLS, Anda mungkin melihat satu detail – semua contoh menggunakan current_user , yaitu pengguna database saat ini. Tapi bukan itu cara kerja sebagian besar aplikasi database hari ini. Aplikasi web dengan banyak pengguna terdaftar tidak mempertahankan pemetaan 1:1 ke pengguna basis data, melainkan menggunakan satu pengguna basis data untuk menjalankan kueri dan mengelola pengguna aplikasi sendiri – mungkin dalam users tabel.

Secara teknis tidak menjadi masalah untuk membuat banyak pengguna database di PostgreSQL. Basis data harus menanganinya tanpa masalah, tetapi aplikasi tidak melakukannya karena sejumlah alasan praktis. Misalnya mereka perlu melacak informasi tambahan untuk setiap pengguna (misalnya departemen, posisi dalam organisasi, detail kontak, ...), sehingga aplikasi akan membutuhkan users meja.

Alasan lain mungkin karena penyatuan koneksi – menggunakan satu akun pengguna bersama, meskipun kami tahu bahwa itu dapat dipecahkan menggunakan pewarisan dan SET ROLE (lihat postingan sebelumnya).

Tapi mari kita asumsikan Anda tidak ingin membuat pengguna database terpisah – Anda ingin tetap menggunakan satu akun database bersama, dan menggunakan RLS dengan pengguna aplikasi. Bagaimana melakukannya?

Variabel sesi

Pada dasarnya yang kita butuhkan adalah meneruskan konteks tambahan ke sesi database, sehingga nanti kita dapat menggunakannya dari kebijakan keamanan (bukan current_user variabel). Dan cara termudah untuk melakukannya di PostgreSQL adalah variabel sesi:

SET my.username ='tomas'

Jika ini menyerupai parameter konfigurasi biasa (mis. SET work_mem = '...' ), Anda benar sekali – hampir semuanya sama. Perintah mendefinisikan namespace baru (my ), dan menambahkan username variabel ke dalamnya. Namespace baru diperlukan, karena namespace global dicadangkan untuk konfigurasi server dan kami tidak dapat menambahkan variabel baru ke dalamnya. Ini memungkinkan kami untuk mengubah kebijakan keamanan seperti ini:

BUAT KEBIJAKAN chat_policy PADA chat MENGGUNAKAN (current_setting('my.username') IN (message_from, message_to)) DENGAN CEK (message_from =current_setting('my.username'))

Yang perlu kita lakukan adalah memastikan kumpulan koneksi / aplikasi menetapkan nama pengguna setiap kali mendapat koneksi baru dan menetapkannya ke tugas pengguna.

Biarkan saya menunjukkan bahwa pendekatan ini runtuh setelah Anda mengizinkan pengguna untuk menjalankan SQL sewenang-wenang pada koneksi, atau jika pengguna berhasil menemukan kerentanan injeksi SQL yang sesuai. Dalam hal ini tidak ada yang bisa menghentikan mereka dari pengaturan nama pengguna yang sewenang-wenang. Tapi jangan putus asa, ada banyak solusi untuk masalah itu, dan kami akan segera mengatasinya.

Variabel sesi yang ditandatangani

Solusi pertama adalah peningkatan sederhana dari variabel sesi – kami tidak dapat benar-benar mencegah pengguna menetapkan nilai arbitrer, tetapi bagaimana jika kami dapat memverifikasi bahwa nilainya tidak ditumbangkan? Itu cukup mudah dilakukan dengan menggunakan tanda tangan digital sederhana. Alih-alih hanya menyimpan nama pengguna, bagian tepercaya (kumpulan koneksi, aplikasi) dapat melakukan sesuatu seperti ini:

tanda tangan =sha256(nama pengguna + stempel waktu + RAHASIA)

lalu simpan nilai dan tanda tangan ke dalam variabel sesi:

SET my.username ='username:timestamp:signature'

Dengan asumsi pengguna tidak mengetahui string SECRET (misalnya 128B data acak), seharusnya tidak mungkin untuk mengubah nilai tanpa membatalkan tanda tangan.

Catatan :Ini bukan ide baru – pada dasarnya sama dengan cookie HTTP yang ditandatangani. Django memiliki dokumentasi yang cukup bagus tentang itu.

Cara termudah untuk melindungi nilai SECRET adalah dengan menyimpannya dalam tabel yang tidak dapat diakses oleh pengguna, dan menyediakan security definer fungsi, yang memerlukan kata sandi (sehingga pengguna tidak bisa begitu saja menandatangani nilai arbitrer).

CREATE FUNCTION set_username(uname TEXT, pwd TEXT) MENGEMBALIKAN teks SEBAGAI $DECLARE v_key TEXT; v_value TEXT;BEGIN PILIH sign_key INTO v_key FROM rahasia; v_value :=uname || ':' || ekstrak (zaman dari sekarang())::int; v_nilai :=v_nilai || ':' || crypt(v_value || ':' || v_key, gen_salt('bf')); PERFORM set_config('my.username', v_value, false); RETURN v_value;END;$ LANGUAGE plpgsql SECURITY DEFINER STABIL;

Fungsi ini hanya mencari kunci penandatanganan (rahasia) dalam sebuah tabel, menghitung tanda tangan dan kemudian menetapkan nilai ke dalam variabel sesi. Ini juga mengembalikan nilai, sebagian besar untuk kenyamanan.

Jadi bagian tepercaya dapat melakukan ini dengan benar sebelum menyerahkan koneksi ke pengguna (jelas 'passphrase' bukanlah kata sandi yang sangat baik untuk produksi):

SELECT set_username('tomas', 'passphrase')

Dan tentu saja kita membutuhkan fungsi lain yang hanya memverifikasi tanda tangan dan kesalahan atau mengembalikan nama pengguna jika tanda tangan cocok.

CREATE FUNCTION get_username() MENGEMBALIKAN teks SEBAGAI $DECLARE v_key TEXT; v_parts TEKS[]; v_uname TEKS; v_value TEKS; v_timestamp INT; v_signature TEXT;BEGIN -- kali ini tidak ada verifikasi sandi SELECT sign_key INTO v_key FROM secret; v_parts :=regexp_split_to_array(current_setting('my.username', true), ':'); v_uname :=v_parts[1]; v_timestamp :=v_parts[2]; v_signature :=v_parts[3]; v_value :=v_uname || ':' || v_timestamp || ':' || v_kunci; JIKA v_signature =crypt(v_value, v_signature) MAKA KEMBALI v_uname; BERAKHIR JIKA; NAIKKAN PENGECUALIAN 'nama pengguna / cap waktu tidak valid';END;$ LANGUAGE plpgsql SECURITY DEFINER STABIL;

Dan karena fungsi ini tidak memerlukan frasa sandi, pengguna cukup melakukan ini:

PILIH get_username()

Tapi get_username() fungsi dimaksudkan untuk kebijakan keamanan, mis. seperti ini:

BUAT KEBIJAKAN chat_policy PADA chat MENGGUNAKAN (get_username() IN (message_from, message_to)) DENGAN CEK (message_from =get_username())

Contoh yang lebih lengkap, dikemas sebagai ekstensi sederhana, dapat ditemukan di sini.

Perhatikan semua objek (tabel dan fungsi) dimiliki oleh pengguna yang memiliki hak istimewa, bukan pengguna yang mengakses database. Pengguna hanya memiliki EXECUTE hak istimewa pada fungsi, yang bagaimanapun didefinisikan sebagai SECURITY DEFINER . Itulah yang membuat skema ini berfungsi sekaligus melindungi rahasia dari pengguna. Fungsi didefinisikan sebagai STABLE , untuk membatasi jumlah panggilan ke crypt() fungsi (yang sengaja mahal untuk mencegah bruteforcing).

Contoh fungsi pasti membutuhkan lebih banyak pekerjaan. Tapi mudah-mudahan itu cukup baik untuk bukti konsep yang menunjukkan cara menyimpan konteks tambahan dalam variabel sesi yang dilindungi.

Apa yang perlu diperbaiki Anda bertanya? Pertama, fungsinya tidak menangani berbagai kondisi kesalahan dengan sangat baik. Kedua, sementara nilai yang ditandatangani menyertakan stempel waktu, kami tidak benar-benar melakukan apa pun dengannya – nilai tersebut dapat digunakan untuk kedaluwarsa, misalnya. Dimungkinkan untuk menambahkan bit tambahan ke dalam nilai, mis. departemen pengguna, atau bahkan informasi tentang sesi (misalnya PID dari proses backend untuk mencegah penggunaan kembali nilai yang sama pada koneksi lain).

Kripto

Kedua fungsi tersebut bergantung pada kriptografi – kami tidak menggunakan banyak kecuali beberapa fungsi hashing sederhana, tetapi ini masih merupakan skema kripto sederhana. Dan semua orang tahu Anda tidak boleh melakukan kripto Anda sendiri. Itulah sebabnya saya menggunakan ekstensi pgcrypto, khususnya crypt() fungsi, untuk mengatasi masalah ini. Tapi saya bukan kriptografer, jadi meskipun saya yakin seluruh skema baik-baik saja, mungkin saya melewatkan sesuatu – beri tahu saya jika Anda menemukan sesuatu.

Selain itu, penandatanganan akan sangat cocok untuk kriptografi kunci publik – kita dapat menggunakan kunci PGP biasa dengan frasa sandi untuk penandatanganan, dan bagian publik untuk verifikasi tanda tangan. Sayangnya, meskipun pgcrypto mendukung PGP untuk enkripsi, itu tidak mendukung penandatanganan.

Pendekatan alternatif

Tentu saja, ada berbagai alternatif solusi. Misalnya, alih-alih menyimpan rahasia penandatanganan dalam sebuah tabel, Anda dapat mengkodekannya ke dalam fungsi (tetapi kemudian Anda perlu memastikan bahwa pengguna tidak dapat melihat kode sumber). Atau Anda dapat melakukan penandatanganan fungsi C, dalam hal ini disembunyikan dari semua orang yang tidak memiliki akses ke memori (dalam hal ini Anda tetap kehilangan).

Juga, jika Anda sama sekali tidak menyukai pendekatan penandatanganan, Anda dapat mengganti variabel yang ditandatangani dengan solusi "kubah" yang lebih tradisional. Kami membutuhkan cara untuk menyimpan data, tetapi kami perlu memastikan bahwa pengguna tidak dapat melihat atau mengubah konten secara sewenang-wenang, kecuali dengan cara yang ditentukan. Tapi hei, itulah tabel biasa dengan API yang diimplementasikan menggunakan security definer fungsi dapat dilakukan!

Saya tidak akan menyajikan seluruh contoh yang dikerjakan ulang di sini (periksa ekstensi ini untuk contoh lengkap), tetapi yang kita butuhkan adalah sessions tabel bertindak sebagai brankas:

BUAT sesi TABEL ( session_id UUID PRIMARY KEY, session_user NAME NOT NULL)

Tabel tidak boleh dapat diakses oleh pengguna basis data biasa – REVOKE ALL FROM ... harus mengurus itu. Dan kemudian API yang terdiri dari dua fungsi utama:

  • set_username(user_name, passphrase) – menghasilkan UUID acak, memasukkan data ke dalam brankas dan menyimpan UUID ke dalam variabel sesi
  • get_username() – membaca UUID dari variabel sesi dan mencari baris dalam tabel (kesalahan jika tidak ada baris yang cocok)

Pendekatan ini menggantikan perlindungan tanda tangan dengan keacakan UUID – pengguna dapat mengubah variabel sesi, tetapi kemungkinan mendapatkan ID yang ada dapat diabaikan (UUID adalah nilai acak 128-bit).

Ini pendekatan yang sedikit lebih tradisional, mengandalkan keamanan berbasis peran tradisional, tetapi juga memiliki beberapa kelemahan – misalnya ia benar-benar menulis database, yang berarti secara inheren tidak kompatibel dengan sistem siaga panas.

Menyingkirkan frasa sandi

Dimungkinkan juga untuk mendesain brankas sehingga frasa sandi tidak diperlukan. Kami telah memperkenalkannya karena kami mengasumsikan set_username terjadi pada koneksi yang sama – kita harus menjaga agar fungsi tetap dapat dijalankan (jadi mengacaukan peran atau hak istimewa bukanlah solusi), dan frasa sandi memastikan hanya komponen tepercaya yang benar-benar dapat menggunakannya.

Tetapi bagaimana jika pembuatan penandatanganan/sesi terjadi pada koneksi yang terpisah, dan hanya hasilnya (nilai yang ditandatangani atau UUID sesi) yang disalin ke dalam koneksi yang diserahkan kepada pengguna? Nah, kalau begitu kita tidak membutuhkan kata sandi lagi. (Ini sedikit mirip dengan apa yang dilakukan Kerberos – membuat tiket pada koneksi tepercaya, lalu menggunakan tiket untuk layanan lain.)

Ringkasan

Jadi izinkan saya meringkas posting blog ini dengan cepat:

  • Sementara semua contoh RLS menggunakan database users (melalui current_user ), tidak terlalu sulit untuk membuat RLS berfungsi dengan pengguna aplikasi.
  • Variabel sesi adalah solusi yang andal dan cukup sederhana, dengan asumsi sistem memiliki komponen tepercaya yang dapat mengatur variabel sebelum menyerahkan koneksi ke pengguna.
  • Saat pengguna dapat mengeksekusi SQL arbitrer (baik karena desain atau karena kerentanan), variabel yang ditandatangani mencegah pengguna mengubah nilainya.
  • Solusi lain dimungkinkan, mis. mengganti variabel sesi dengan tabel yang menyimpan info tentang sesi yang diidentifikasi oleh UUID acak.
  • Hal yang menyenangkan adalah variabel sesi tidak melakukan penulisan database, sehingga pendekatan ini dapat bekerja pada sistem hanya-baca (mis. siaga panas).

Di bagian selanjutnya dari seri blog ini, kita akan melihat penggunaan pengguna aplikasi ketika sistem tidak memiliki komponen tepercaya (sehingga tidak dapat mengatur variabel sesi atau membuat baris di sessions table), atau ketika kita ingin melakukan otentikasi kustom (tambahan) dalam database.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Deklaratif SQLAlchemy:mendefinisikan pemicu dan indeks (Postgres 9)

  2. Cara Memeriksa apakah Array PostgreSQL Berisi Nilai

  3. Mengapa bilangan bulat yang tidak ditandatangani tidak tersedia di PostgreSQL?

  4. Menghapus tag HTML di PostgreSQL

  5. Kesalahan PostgreSQL:Hubungan sudah ada