Một số phương pháp bảo mật hiệu quả dành cho webhook

Bài viết được sự cho phép của tác giả Tống Xuân Hoài

Vấn đề

Mấy hôm vừa rồi tôi có công việc nghiên cứu tích hợp App Store Server Notifications là một dạng webhook để nhận thông báo từ Apple về máy chủ của mình. Trong quá trình tích hợp có một vài chi tiết thú vị mà tôi nghĩ nếu kể ra ở đây hẳn sẽ giúp ích được cho bạn đọc. Nếu chưa biết webhook là gì, bạn đọc có thể tham khảo bài viết Webhook là gì? Tổng hợp các kiến thức cơ bản về Webhook.

Chắc hẳn chúng ta ai cũng biết việc mua hàng trong ứng dụng. Khi mua thành công một đơn hàng, Apple sẽ gửi thông báo về máy chủ của chúng ta, trong thông báo có chứa nhiều thông tin về đơn hàng như tên, ngày mua, trạng thái… từ đó làm căn cứ để tiếp tục xử lý đơn hàng của khách. Bởi vì hành động mua hàng trong ứng dụng thành công chỉ khi người dùng mua thành công, mà để biết mua thành công hay không thì chỉ có Apple mới biết vì họ xử lý quá trình cộng trừ tiền cho chúng ta. Sau đó là một thông báo gửi đến một API (webhook) mà chúng ta thiết lập từ trước, cho biết kết quả của quá trình thanh toán thành công hay thất bại.

Trong quá trình làm, tôi phát hiện ra Apple có cách để bảo vệ dữ liệu mà họ gửi sang máy chủ của mình một cách rất “uy tín”, nó tiêu tốn một “chút” thời gian tìm hiểu của tôi và do đó khơi mào cho tôi tổng hợp lại một số phương pháp bảo mật của webhook lâu nay tích lũy được.

Lưu ý rằng vẫn có rất nhiều phương pháp khác không xuất hiện ở đây. Tôi chỉ đơn giản là thống kê lại một số cách thường gặp hoặc đã biết. Vì thế nếu bạn đọc còn biết cách nào nữa thì vui lòng để lại thông tin trong phần bình luận dưới bài viết nhé.

Một số phương pháp bảo mật webhook

Khi cung cấp một đầu API (endpoint) để nhận dữ liệu, nếu chẳng may kẻ tấn công hoặc những người tò mò biết được, kẻ gian sẽ cố tình khai thác bằng cách gửi nhiều thông tin sai lệch đến máy chủ, từ đó khiến cho nhiều rủi ro có thể xảy ra. Thế nên cách hữu hiệu nhất vẫn là giữ bí mật API, đồng thời luôn luôn xác thực dữ liệu nhận được có phải xuất phát từ bên dịch vụ mà chúng ta tích hợp.

Lấy ví dụ bạn cung cấp một endpoint /webhook/serviceA để nhận dữ liệu từ serviceA thì bằng cách nào đó phải chắc chắn dữ liệu vừa nhận được là từ chính serviceA gửi.

HTTPs

Điều kiện tiên quyết đầu tiên là phải yêu cầu https, tức là serviceA sẽ từ chối gửi dữ liệu nếu endpoint không hỗ trợ https.

Ngày nay https đang dần thay thế http truyền thống vì độ tin cậy và khả năng bảo mật cao hơn. Dữ liệu được mã hóa trên đường truyền và hạn chế các cuộc tấn công Man-in-the-middle.

Vì lẽ đó cho nên https trở thành yêu cầu bắt buộc để truyền dữ liệu thông qua các cuộc gọi API – vốn là cơ chế gửi/nhận dữ liệu của webhook.

Trust IPs

Cách tốt nhất để biết được serviceA gửi thì chắc chắn phải gửi từ đúng địa chỉ IP của nó. serviceA có thể phải cung cấp một danh sách các địa chỉ IP thuộc sở hữu của nó, những địa chỉ đó sẽ được dùng để thực hiện truy vấn đến endpoint của chúng ta. Việc cần làm là xác thực xem địa chỉ IP nhận được có nằm trong danh sách mà chúng ta tin tưởng, nếu không thì khả năng rất cao chúng ta đang bị tấn công.

Phương pháp này nhanh mà hiệu quả, nhưng phải được chính serviceA hỗ trợ bởi vì họ phải cung cấp tất cả địa chỉ IP. Tuy nhiên, trong thời đại công nghệ phân tán và hệ thống thông tin chằng chịt như hiện nay, địa chỉ IP có thể được sửa đổi liên tục cho nên việc triển khai có phần phức tạp và mang lại rủi ro trong quá trình vận hành.

Hãy tưởng tượng một ngày đẹp trời, họ (serviceA) thêm một địa chỉ IP mới vào danh sách mà hệ thống của chúng ta chưa kịp cập nhật thì điều gì sẽ xảy ra?

Mã bí mật

“Hãy tự chọn một mã bí mật, nhập vào trang quản lý của chúng tôi và chúng tôi sẽ gửi nó kèm theo dữ liệu về endpoint của bạn” – đây chính là châm ngôn của phương pháp này.

Vì mã bí mật chỉ có hai bên biết cho nên việc không cung cấp đúng mã bí mật có thể coi là một cuộc tấn công từ bên khác nhằm vào. Mã bí mật thường sẽ được gửi lại thông qua tiêu hề (headers) http. Việc của chúng ta là cần lấy ra và so sánh nó xem có khớp với nhau.

Phương pháp này dễ triển khai và có độ tin cậy nhất định, tuy nhiên nếu chẳng may bị lộ mã bí mật thì…ba chấm. Vì mã này thường nằm dưới dạng văn bản, không mã hóa và phải được ghi lại ở đâu đó cho nên độ tin cậy cũng vì thế mà giảm xuống.

Nằm trong mục này thì Basic Auth cũng là một dạng mã bí mật. Cung cấp username và password cho serviceA biết để xác thực trước khi thực hiện cuộc gọi đến endpoint.

  Tấn công các cụm Kubernetes qua lỗi API Kubelet misconfigure

  DevSecOps – Tương lai của an ninh bảo mật phần mềm

Chữ ký số

Các phương phát trên vẫn có một điểm yếu đó là dữ liệu chưa được mã hóa đúng cách, hay nói cách khác là chưa có cách nào để biết liệu dữ liệu được gửi đến máy chủ của chúng ta có đảm bảo tính toàn vẹn? Vì quá trình truyền dữ liệu không chỉ đơn giản là giữa serviceA và endpint, mà nó còn đi qua rất nhiều điểm khác trước khi đến đích. Giả sử dữ liệu bị sửa đổi ở đâu đó thì phải làm như thế nào để phát hiện?

Nếu đã làm việc với JSON Web Tokens (jwt), bạn đọc sẽ biết cơ chế bảo vệ dữ tính toàn vẹn của dữ liệu bằng cách mã hóa bất đối xứng. Với cấu trúc 3 phần được mã hóa bằng base64 của jwt, phần đầu chứa các chỉ dẫn về thuật toán sử dụng mã hóa, phần thân chứa dữ liệu và phần cuối cùng là một chuỗi được tạo ra bằng cách mã hóa bất đối xứng dữ liệu với khóa bí mật. Để xác thực, chúng ta chỉ cần sử dụng khóa công khai để xem dữ liệu có bị sửa đổi sau khi ký (sign) hay không, vì bất cứ thay đổi nào dù là nhỏ nhất trong phần thân cũng khiến cho chuỗi ký bị sai lệch.

Phương pháp này có phần phức tạp hơn nhưng lại cho độ tin cậy cao hơn hẳn. Khóa bí mật cần được giữ an toàn tuyệt đối để ký dữ liệu trước khi gửi đi, khóa bí mật cũng được mã hóa do đó nguy cơ tấn công thấp hơn so với văn bản thông thường.

Bảo mật webhook của Apple

Về cơ bản Apple cũng lựa chọn cách thức mã hóa bất đối xứng để bảo vệ tính toàn vẹn của dữ liệu mà họ gửi đến, nhưng với độ phức tạp cao hơn một chút.

Đầu tiên dữ liệu được mã hóa hết dưới dạng base64 để tăng độ “nguy hiểm”, mã hóa này góp một phần nào đó cho quá trình nhìn trộm nhanh và yêu cầu giải mã để biết được nội dung thật là gì.

Apple ký trên một số đối tượng dữ liệu, theo dạng JWS vì thế trước khi sử dụng cần xác định tính toàn vẹn của dữ liệu. Cách làm có thể tóm lại trong 2 bước:

  • Lấy public key trong headers của chuỗi mã hóa
  • Sử dụng public key đó để xác thực chữ ký

Vì vậy, khi giải mã headers, bạn sẽ thấy thuật toán chữ ký, kèm theo một đối tượng x5c giống như:

{
  "alg": "ES256",  
  "x5c": [
    "MIIEMDCCA...",  
    "MIIDFjCCA...",  
    "MIICQzCCA..."
  ]
}

Khi đó khóa công khai để xác nhận dữ liệu là x5c[0]. Nhưng vậy thì x5c[1] và x5c[2] có ý nghĩa gì? Chúng ta biết thuật toán mã hóa bất đối xứng dựa trên ES256 rất khó để bẻ khóa, vì thế chỉ cần sử dụng khóa công khai để xác nhận tính toàn voẹn của dữ liệu gần như là tuyệt đối, bởi vì chỉ có Apple mới biết được khóa bí mật.

Sau một lúc tìm hiểu, thì ra hai khóa còn lại dùng để xác thực khóa công khai. Đúng vậy, tức là x5c[1] và x5c[2] được dùng để xác thực x5c[0] có đúng là của Apple hay không.

Như vậy, x5c[2] là chứng chỉ dịch vụ gốc của Apple (Certificate Authority (CA)) đã được tin cậy, trong khi x5c[1] là chứng chỉ trung gian và x5c[0] là chứng chỉ dùng để xác minh chữ ký mà Apple đã ký cho dữ liệu.

CA tin cậy được phân phối thông qua hệ điều hành, nghĩa là máy tính khi xuất xưởng đã được cài đặt sẵn một số chứng chỉ CA đáng tin cậy trên thế giới, trong đó có CA của Apple. Một số công cụ như openssl có thể xác định được liệu một CA có đáng tin hay là không. Vì thế luồng xác thực dữ liệu lúc này là sử dụng công cụ để xác minh CA (x5c[2]) -> xác minh chứng chỉ trung gian (x5c[1]) -> xác minh khóa công khai (x5c[0]). Nếu tất cả đều hợp lệ thì chúng ta có thể tin chắc rằng dữ liệu được gửi từ Apple.

Bài viết gốc được đăng tải tại 2coffee.dev

Xem thêm: