PostgreSQL
 sql >> Cơ Sở Dữ Liệu >  >> RDS >> PostgreSQL

Người dùng ứng dụng so với Bảo mật cấp hàng

Vài ngày trước, tôi đã viết blog về các vấn đề phổ biến với vai trò và đặc quyền mà chúng tôi phát hiện ra trong quá trình xem xét bảo mật.

Tất nhiên, PostgreSQL cung cấp nhiều tính năng nâng cao liên quan đến bảo mật, một trong số đó là Bảo mật cấp độ hàng (RLS), có sẵn kể từ PostgreSQL 9.5.

Vì 9.5 đã được phát hành vào tháng 1 năm 2016 (chỉ vài tháng trước), RLS là một tính năng khá mới và chúng tôi vẫn chưa thực sự xử lý nhiều triển khai sản xuất. Thay vào đó, RLS là một chủ đề phổ biến của các cuộc thảo luận “cách triển khai” và một trong những câu hỏi phổ biến nhất là làm thế nào để làm cho nó hoạt động với người dùng cấp ứng dụng. Vì vậy, hãy xem có những giải pháp khả thi nào.

Giới thiệu về RLS

Trước tiên, hãy xem một ví dụ rất đơn giản, giải thích về RLS. Giả sử chúng ta có một chat bảng lưu trữ các tin nhắn được gửi giữa những người dùng - người dùng có thể chèn các hàng vào đó để gửi tin nhắn cho những người dùng khác và truy vấn nó để xem các tin nhắn được gửi cho họ bởi những người dùng khác. Vì vậy, bảng có thể trông như thế này:

 TẠO BẢNG trò chuyện (message_uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4 (), message_time TIMESTAMP NOT NULL DEFAULT ngay bây giờ (), message_from NAME NOT NULL DEFAULT current_user, message_to NAME NOT NULL, message_subject VARCHAR (64) NOT NEXT); / pre> 

Bảo mật dựa trên vai trò cổ điển chỉ cho phép chúng tôi hạn chế quyền truy cập vào toàn bộ bảng hoặc các lát dọc của nó (cột). Vì vậy, chúng tôi không thể sử dụng nó để ngăn người dùng đọc tin nhắn dành cho người dùng khác hoặc gửi tin nhắn bằng message_from giả trường.

Và đó chính xác là mục đích của RLS - nó cho phép bạn tạo các quy tắc (chính sách) hạn chế quyền truy cập vào tập hợp con của các hàng. Vì vậy, ví dụ, bạn có thể làm điều này:

 TẠO CHÍNH SÁCH chat_policy KHI trò chuyện SỬ DỤNG ((message_to =current_user) HOẶC (message_from =current_user)) VỚI KIỂM TRA (message_from =current_user) 

Chính sách này đảm bảo người dùng chỉ có thể xem các tin nhắn do anh ta gửi hoặc dành cho anh ta - đó là điều kiện trong USING mệnh đề không. Phần thứ hai của chính sách (WITH CHECK ) đảm bảo người dùng chỉ có thể chèn tin nhắn với tên người dùng của mình trong message_from , ngăn các thư có người gửi giả mạo.

Bạn cũng có thể hình dung RLS như một cách tự động để nối các điều kiện WHERE bổ sung. Bạn có thể làm điều đó theo cách thủ công ở cấp ứng dụng (và trước khi có RLS mọi người thường làm điều đó), nhưng RLS thực hiện điều đó theo cách đáng tin cậy và an toàn (ví dụ:rất nhiều nỗ lực đã được thực hiện để ngăn chặn rò rỉ thông tin khác nhau).

Lưu ý :Trước RLS, một cách phổ biến để đạt được điều gì đó tương tự là làm cho bảng không thể truy cập trực tiếp (thu hồi tất cả các đặc quyền) và cung cấp một tập hợp các hàm định nghĩa bảo mật để truy cập nó. Hầu hết đều đạt được cùng một mục tiêu, nhưng các chức năng có nhiều nhược điểm khác nhau - chúng có xu hướng gây nhầm lẫn cho trình tối ưu hóa và hạn chế nghiêm trọng tính linh hoạt (nếu người dùng cần làm điều gì đó và không có chức năng phù hợp cho nó, anh ta đã gặp may). Và tất nhiên, bạn phải viết các hàm đó.

Người dùng ứng dụng

Nếu bạn đọc tài liệu chính thức về RLS, bạn có thể nhận thấy một chi tiết - tất cả các ví dụ đều sử dụng current_user , tức là người dùng cơ sở dữ liệu hiện tại. Nhưng đó không phải là cách hầu hết các ứng dụng cơ sở dữ liệu hoạt động ngày nay. Các ứng dụng web có nhiều người dùng đã đăng ký không duy trì ánh xạ 1:1 tới người dùng cơ sở dữ liệu mà thay vào đó, sử dụng một người dùng cơ sở dữ liệu duy nhất để tự chạy truy vấn và quản lý người dùng ứng dụng - có thể trong users bảng.

Về mặt kỹ thuật, việc tạo nhiều người dùng cơ sở dữ liệu trong PostgreSQL không thành vấn đề. Cơ sở dữ liệu sẽ xử lý điều đó mà không gặp bất kỳ sự cố nào, nhưng các ứng dụng không làm điều đó vì một số lý do thực tế. Ví dụ:họ cần theo dõi thông tin bổ sung cho từng người dùng (ví dụ:phòng ban, vị trí trong tổ chức, chi tiết liên hệ,…), vì vậy ứng dụng sẽ cần người dùng users vẫn là bảng.

Một lý do khác có thể là gộp kết nối - bằng cách sử dụng một tài khoản người dùng được chia sẻ, mặc dù chúng tôi biết rằng tài khoản đó có thể giải quyết được bằng cách sử dụng tính năng kế thừa và SET ROLE (xem bài trước).

Nhưng giả sử bạn không muốn tạo người dùng cơ sở dữ liệu riêng biệt - bạn muốn tiếp tục sử dụng một tài khoản cơ sở dữ liệu dùng chung duy nhất và sử dụng RLS với người dùng ứng dụng. Làm thế nào để làm điều đó?

Các biến phiên

Về cơ bản những gì chúng ta cần là chuyển ngữ cảnh bổ sung cho phiên cơ sở dữ liệu để sau này chúng ta có thể sử dụng nó từ chính sách bảo mật (thay vì current_user Biến đổi). Và cách dễ nhất để làm điều đó trong PostgreSQL là các biến phiên:

 SET my.username ='tomas' 

Nếu điều này giống với các thông số cấu hình thông thường (ví dụ:SET work_mem = '...' ), bạn hoàn toàn đúng - hầu hết đều giống nhau. Lệnh xác định một không gian tên mới (my ), và thêm username biến thành nó. Không gian tên mới là bắt buộc vì không gian chung được dành riêng cho cấu hình máy chủ và chúng tôi không thể thêm các biến mới vào đó. Điều này cho phép chúng tôi thay đổi chính sách bảo mật như sau:

 TẠO CHÍNH SÁCH chat_policy BẬT trò chuyện SỬ DỤNG (current_setting ('my.username') TRONG (message_from, message_to)) VỚI KIỂM TRA (message_from =current_setting ('my.username')) 

Tất cả những gì chúng ta cần làm là đảm bảo nhóm / ứng dụng kết nối đặt tên người dùng bất cứ khi nào nó nhận được kết nối mới và gán nó cho tác vụ của người dùng.

Hãy để tôi chỉ ra rằng cách tiếp cận này sẽ sụp đổ khi bạn cho phép người dùng chạy SQL tùy ý trên kết nối hoặc nếu người dùng phát hiện ra lỗ hổng SQL injection phù hợp. Trong trường hợp đó, không có gì có thể ngăn họ đặt tên người dùng tùy ý. Nhưng đừng thất vọng, có rất nhiều giải pháp cho vấn đề đó và chúng ta sẽ nhanh chóng giải quyết chúng.

Các biến phiên đã ký

Giải pháp đầu tiên là một cải tiến đơn giản của các biến phiên - chúng tôi thực sự không thể ngăn người dùng đặt giá trị tùy ý, nhưng điều gì sẽ xảy ra nếu chúng tôi có thể xác minh rằng giá trị đó không bị phá hủy? Điều đó khá dễ thực hiện bằng cách sử dụng chữ ký điện tử đơn giản. Thay vì chỉ lưu trữ tên người dùng, phần đáng tin cậy (nhóm kết nối, ứng dụng) có thể thực hiện một số việc như sau:

 signature =sha256 (tên người dùng + dấu thời gian + SECRET) 

và sau đó lưu trữ cả giá trị và chữ ký vào biến phiên:

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

Giả sử người dùng không biết chuỗi SECRET (ví dụ:128B dữ liệu ngẫu nhiên), thì sẽ không thể sửa đổi giá trị mà không làm mất hiệu lực chữ ký.

Lưu ý :Đây không phải là một ý tưởng mới - về cơ bản nó giống như cookie HTTP đã ký. Django có một tài liệu khá hay về điều đó.

Cách dễ nhất để bảo vệ giá trị SECRET là lưu trữ nó trong một bảng mà người dùng không thể truy cập được và cung cấp security definer , yêu cầu mật khẩu (để người dùng không thể chỉ cần ký các giá trị tùy ý).

 TẠO FUNCTION set_username (uname TEXT, pwd TEXT) TRẢ LẠI văn bản AS $ DECLARE v_key TEXT; v_value TEXT; BẮT ĐẦU CHỌN dấu_mỹ VÀO v_key TỪ bí mật; v_value:=uname || ':' || giải nén (epoch from now ()) ::int; v_value:=v_value || ':' || crypt (v_value || ':' || v_key, gen_salt ('bf')); PERFORM set_config ('my.username', v_value, false); QUAY LẠI v_value; END; $ LANGUAGE plpgsql ỔN ĐỊNH BẢO MẬT; 

Hàm chỉ đơn giản là tìm kiếm khóa ký (bí mật) trong bảng, tính toán chữ ký và sau đó đặt giá trị vào biến phiên. Nó cũng trả về giá trị, chủ yếu là để thuận tiện.

Vì vậy, phần đáng tin cậy có thể thực hiện việc này ngay trước khi chuyển kết nối cho người dùng (rõ ràng là ‘mật khẩu’ không phải là mật khẩu tốt cho quá trình sản xuất):

 SELECT set_username ('tomas', 'passphrase') 

Và sau đó, tất nhiên chúng ta cần một chức năng khác chỉ đơn giản là xác minh chữ ký và sửa lỗi hoặc trả về tên người dùng nếu chữ ký khớp.

 TẠO CHỨC NĂNG get_username () RETURNS text AS $ DECLARE v_key TEXT; v_parts TEXT []; v_uname TEXT; v_value TEXT; v_timestamp INT; v_signature TEXT; BEGIN - không có xác minh mật khẩu lần này CHỌN dấu_chỉ VÀO v_key TỪ bí mật; 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_key; NẾU v_signature =crypt (v_value, v_signature) THÌ QUAY LẠI v_uname; HẾT NẾU; NGOẠI LỆ 'tên người dùng / dấu thời gian không hợp lệ'; END; $ LANGUAGE plpgsql ỔN ĐỊNH BẢO MẬT; 

Và vì chức năng này không cần cụm mật khẩu, người dùng có thể chỉ cần thực hiện điều này:

 CHỌN get_username () 

Nhưng get_username() chức năng dành cho các chính sách bảo mật, ví dụ:như thế này:

 TẠO CHÍNH SÁCH chat_policy KHI trò chuyện SỬ DỤNG (get_username () IN (message_from, message_to)) VỚI KIỂM TRA (message_from =get_username ()) 

Có thể tìm thấy một ví dụ hoàn chỉnh hơn, được đóng gói dưới dạng một phần mở rộng đơn giản, tại đây.

Lưu ý rằng tất cả các đối tượng (bảng và các chức năng) được sở hữu bởi một người dùng đặc quyền, không phải người dùng truy cập cơ sở dữ liệu. Người dùng chỉ có EXECUTE đặc quyền trên các chức năng, tuy nhiên được định nghĩa là SECURITY DEFINER . Đó là những gì làm cho kế hoạch này hoạt động trong khi bảo vệ bí mật khỏi người dùng. Các chức năng được định nghĩa là STABLE , để giới hạn số lượng cuộc gọi đến crypt() chức năng (cố ý đắt tiền để ngăn chặn việc ép buộc).

Các hàm ví dụ chắc chắn cần nhiều công việc hơn. Nhưng hy vọng nó đủ tốt cho một bằng chứng về khái niệm trình bày cách lưu trữ ngữ cảnh bổ sung trong một biến phiên được bảo vệ.

Bạn yêu cầu những gì cần được sửa chữa? Thứ nhất, các chức năng không xử lý các điều kiện lỗi khác nhau rất tốt. Thứ hai, mặc dù giá trị đã ký bao gồm dấu thời gian, nhưng chúng tôi không thực sự làm bất cứ điều gì với nó - ví dụ:nó có thể được sử dụng để hết hạn giá trị. Có thể thêm các bit bổ sung vào giá trị, ví dụ:một bộ phận của người dùng hoặc thậm chí thông tin về phiên (ví dụ:PID của quy trình phụ trợ để ngăn việc sử dụng lại cùng một giá trị trên các kết nối khác).

Tiền điện tử

Hai hàm đều dựa trên mật mã - chúng tôi không sử dụng nhiều ngoại trừ một số hàm băm đơn giản, nhưng nó vẫn là một sơ đồ mã hóa đơn giản. Và mọi người đều biết bạn không nên sử dụng tiền điện tử của riêng mình. Đó là lý do tại sao tôi sử dụng tiện ích mở rộng pgcrypto, đặc biệt là crypt() để giải quyết vấn đề này. Nhưng tôi không phải là một nhà mật mã học, vì vậy mặc dù tôi tin rằng toàn bộ kế hoạch đều ổn, nhưng có thể tôi đang thiếu thứ gì đó - hãy cho tôi biết nếu bạn phát hiện ra điều gì đó.

Ngoài ra, phần ký kết sẽ là một kết hợp tuyệt vời cho mật mã khóa công khai - chúng tôi có thể sử dụng khóa PGP thông thường với cụm mật khẩu để ký và phần công khai để xác minh chữ ký. Đáng buồn là mặc dù pgcrypto hỗ trợ PGP để mã hóa, nhưng nó không hỗ trợ việc ký.

Các phương pháp thay thế

Tất nhiên, có nhiều giải pháp thay thế khác nhau. Ví dụ:thay vì lưu trữ bí mật ký trong bảng, bạn có thể mã hóa cố định nó vào hàm (nhưng sau đó bạn cần đảm bảo rằng người dùng không thể nhìn thấy mã nguồn). Hoặc bạn có thể thực hiện việc đăng nhập một hàm C, trong trường hợp đó hàm này sẽ bị ẩn khỏi những người không có quyền truy cập vào bộ nhớ (trong trường hợp đó, bạn vẫn bị mất).

Ngoài ra, nếu bạn hoàn toàn không thích cách tiếp cận ký, bạn có thể thay thế biến đã ký bằng một giải pháp “vault” truyền thống hơn. Chúng tôi cần một cách để lưu trữ dữ liệu, nhưng chúng tôi cần đảm bảo rằng người dùng không thể xem hoặc sửa đổi nội dung một cách tùy tiện, ngoại trừ một cách xác định. Nhưng này, đó là bảng thông thường có API được triển khai bằng cách sử dụng security definer chức năng có thể làm!

Tôi sẽ không trình bày toàn bộ ví dụ được làm lại ở đây (hãy kiểm tra tiện ích mở rộng này để biết ví dụ hoàn chỉnh), nhưng những gì chúng tôi cần là một sessions bảng hoạt động như một kho tiền:

 TẠO phiên BẢNG (session_id UUID KEY CHÍNH, TÊN session_user KHÔNG ĐẦY ĐỦ) 

Người dùng cơ sở dữ liệu thông thường không thể truy cập được bảng - một REVOKE ALL FROM ... đơn giản nên quan tâm đến điều đó. Và sau đó là một API bao gồm hai chức năng chính:

  • set_username(user_name, passphrase) - tạo một UUID ngẫu nhiên, chèn dữ liệu vào vault và lưu trữ UUID vào một biến phiên
  • get_username() - đọc UUID từ một biến phiên và tìm kiếm hàng trong bảng (lỗi nếu không có hàng phù hợp)

Cách tiếp cận này thay thế bảo vệ chữ ký bằng tính ngẫu nhiên của UUID - người dùng có thể điều chỉnh biến phiên, nhưng xác suất đạt được ID hiện có là không đáng kể (UUID là giá trị ngẫu nhiên 128 bit).

Đây là cách tiếp cận truyền thống hơn một chút, dựa vào bảo mật dựa trên vai trò truyền thống, nhưng nó cũng có một vài nhược điểm - ví dụ như nó thực sự thực hiện ghi cơ sở dữ liệu, có nghĩa là nó vốn dĩ không tương thích với các hệ thống chờ nóng.

Loại bỏ cụm mật khẩu

Cũng có thể thiết kế hầm sao cho cụm mật khẩu không cần thiết. Chúng tôi đã giới thiệu nó vì chúng tôi giả định là set_username xảy ra trên cùng một kết nối - chúng ta phải giữ cho hàm có thể thực thi được (vì vậy việc xáo trộn các vai trò hoặc đặc quyền không phải là giải pháp) và cụm mật khẩu đảm bảo chỉ thành phần đáng tin cậy mới có thể thực sự sử dụng nó.

Nhưng điều gì sẽ xảy ra nếu quá trình ký / tạo phiên diễn ra trên một kết nối riêng biệt và chỉ kết quả (giá trị đã ký hoặc UUID phiên) được sao chép vào kết nối được trao cho người dùng? Vậy thì chúng ta không cần cụm mật khẩu nữa. (Nó hơi giống với những gì Kerberos làm - tạo một vé trên một kết nối đáng tin cậy, sau đó sử dụng vé cho các dịch vụ khác.)

Tóm tắt

Vì vậy, hãy để tôi nhanh chóng tóm tắt lại bài đăng trên blog này:

  • Trong khi tất cả các ví dụ RLS đều sử dụng người dùng cơ sở dữ liệu (bằng current_user ), không quá khó để làm cho RLS hoạt động với người dùng ứng dụng.
  • Các biến phiên là một giải pháp đáng tin cậy và khá đơn giản, giả sử hệ thống có một thành phần đáng tin cậy có thể đặt biến trước khi chuyển giao kết nối cho người dùng.
  • Khi người dùng có thể thực thi SQL tùy ý (theo thiết kế hoặc do lỗ hổng bảo mật), một biến đã ký sẽ ngăn người dùng thay đổi giá trị.
  • Các giải pháp khác có thể thực hiện được, ví dụ:thay thế các biến phiên bằng bảng lưu trữ thông tin về các phiên được xác định bởi UUID ngẫu nhiên.
  • Một điều thú vị là các biến phiên không có cơ sở dữ liệu nào ghi, vì vậy phương pháp này có thể hoạt động trên các hệ thống chỉ đọc (ví dụ:chế độ chờ nóng).

Trong phần tiếp theo của loạt bài blog này, chúng ta sẽ xem xét việc sử dụng người dùng ứng dụng khi hệ thống không có thành phần đáng tin cậy (vì vậy hệ thống không thể đặt biến phiên hoặc tạo một hàng trong sessions bảng), hoặc khi chúng tôi muốn thực hiện xác thực tùy chỉnh (bổ sung) trong cơ sở dữ liệu.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Cách liệt kê các bảng bị ảnh hưởng bởi xóa theo tầng

  2. Cấu trúc lại một hàm PL / pgSQL để trả về kết quả đầu ra của các truy vấn SELECT khác nhau

  3. Làm cách nào để tôi (hoặc tôi có thể) CHỌN DISTINCT trên nhiều cột?

  4. Lặp lại trên số nguyên [] trong PL / pgSQL

  5. Postgres:lệnh chân không không làm sạch các bộ giá trị đã chết