Security với single-page web app.

Ngày nay, xu thế xây dựng website theo dạng kiểu single-page web app ngày càng phổ biến. Đi kèm với nó là việc xây dựng một cơ chế bảo vệ thích hợp cho ứng dụng, ít nhất là để có khả năng tránh được các loại hình tấn công phổ biến hiên nay.

Bài viết sẽ được dành để liệt kê một vài phương thức tấn công web phổ biến thường gặp, cách đối phó ; đồng thời đưa ra giới thiệu và so sánh 2 cơ chế authentication phổ biến hiện nay - token authentication vs session authentication

Client-side và một số cách tấn công thường gặp

Cross Site scripting (XSS) attack

XSS attack là một trong những kĩ thuật tấn công phổ biến nhất, cũng như nguy hiểm nhất hiện nay. Phương thức tấn công này dựa vào lỗi bảo mật khi xây dựng ứng dụng của bên phát triển, cho phép người tấn công chèn các đoạn script vào trong mã nguồn của ứng dụng web.

XSS attack xảy ra khi web app cho phép các đoạn code javascript ngoài được gắn vào (inject) và thực thi một cách tùy tiện. Lỗi này thường dễ thấy nhất trên là trên các form trong app. Đoạn gif dưới đây sẽ demo hành vi của loại tấn công này.

Bên tấn công chỉ cần đưa một đoạn script vào, với một trang web không được xử lý tốt (không được xây dựng để chống lại được tấn công XSS), đoạn code sẽ được đưa thẳng cho web app và được thực thi - với ví dụ demo là đưa ra alert. Vấn đề ở đây đó là bất cứ thứ gì được nhập vào các form field của app cũng sẽ được chấp nhận, xử lý và gửi lên lên server, từ đó có khả năng cho phép hacker thực hiện các hành vi trái phép.

Khắc phục

Ngắn gọn: Giải pháp đơn giản nhất để ngăn ngừa tấn công XSS là lọc tất cả các thông tin được nhận từ phía người dùng, chỉ chấp nhận những dữ liệu hợp lệ. (ví dụ: lọc bỏ các đoạn thông tin chứa thẻ <script>, lọc các kí tự đặc biệt ...)

HIện nay , hầu hết (nếu như không nói là tất cả) các framework xây dựng web đều tích hợp các cơ chế để ngăn ngừa tấn công XSS, hoặc cũng đã có rất nhiều các thư viện được thiết kế để ngăn ngừa XSS attack được chia sẻ và sử dụng rộng rãi. Lời khuyên ở đây là ta (nhất là với các LTV mới, còn ít kinh nghiệm) luôn nên lựa chọn và sử dụng một bộ thư viện nào đó , tốt nhất là 1 library open-source , đáng tin cậy và đã được sử dụng rộng rãi. Việc tự tay thiết kế và xây dựng lại từ đầu giải pháp bảo vệ cho trang web cũng là không cần thiết, chỉ nên thực hiện khi ứng dụng đòi hỏi 1 quy trình đặc biệt nào đó; bên cạnh đó, ta còn có thể bỏ sót một lỗ hổng nào đó hoặc thậm chí còn gây ra một lỗ hổng khác khi tự xây dựng một library bằng tay.

Man-in-the-Middle attack

Thuật ngữ MITM ở đây được dùng để nói đến trường hợp khi người dùng tin rằng mình đang kết nối ( cũng như đang giao tiếp ) trực tiếp với ứng dụng mà mình mong muốn, nhưng trên thực tế, có một "trung gian" khác đứng giữa người dùng và server. Trung gian đó sẽ chặn lấy các thông tin của người dùng rồi truyền lại cho server mà người dùng nhầm tưởng. Người dùng, khi này, sẽ thật sự nghĩ rằng mình đang trực tiếp thực hiện trên ứng dụng mong muốn - mà không biết rằng - trong lúc đó, luôn luôn có một bên thứ 3 can thiệp vào luồng thông tin, lưu trữ lại hết hành vi người dùng và thậm chí chỉnh sửa, thay đổi response trả về của yêu cầu. Trong thực tế, case này xảy ra khi ta sử dụng các kết nối mạng không an toàn - ví dụ như các điểm công cộng ( quán cà phê, khách sạn ...) - khi này, các thông tin mà đáng lẽ phải được bảo mật (ví dụ như session id ...) nếu như được truyền thông qua các giao thức an toàn (ví dụ như https ...), lại hoàn toàn không được bảo vệ, bị truyền đi mà không cần che giấu trên network, và có thể bị bất cứ ai nắm lấy. Lúc này, bên tấn công sẽ chiếm lấy các thông tin định danh nhạy cảm (session id) để từ đó giả mạo danh tính của người dùng đối với một ứng dụng mà người dùng đã xác thực từ trước đó.

Khắc phục

Luôn luôn sử dụng các kết nối an toàn - HTTPS, TLS - mọi lúc có thể. Hạn chế sử dụng mạng công cộng (quán cafe, nhà nghỉ ...) - những nơi không đảm bảo an toàn - đặc biệt là thực hiện các thao tác như giao dịch ngân hàng.

Cross Site request forgery (CSRF)

Xảy ra khi một web site, email, chương trình ... độc hại điều khiển được browser của người dùng, giả mạo người dùng và thực hiện các hành vi mà người dùng không biết trên một trang web mà họ đã thực hiện việc chứng thực trước đó.

Ví dụ:

  • Ta vào trang web https://vietcombank.com , thực hiện các thao tác giao dịch bình thường.

  • Ta nhận được tin nhắn của bạn gái , trong đó chứa một đường link để xem bức ảnh chụp chung ...

  • Click vào , thay vì hiện ra ảnh bạn gái, đường link đó điều hướng người dùng về lại trang web của ngân hàng và thực hiện rút sạch tiền của bạn 😃

Phương thức này nguy hiểm ở chỗ: Bên tấn công có thể hoàn toàn không biết được các thông tin nhạy cảm của người dùng (session id, thông tin xác thực ...). Hành vi tấn công chỉ nhắm vào khả năng người dùng : 1. có sử dụng web vietcombank; 2. đã có thực hiện xác thực từ trước đó (vẫn còn tồn tại sessionid chưa logout). Khi người dùng click vào đường link, trình duyệt sẽ tự động đính kèm tất cả cookie hay thông tin xác thực đi kèm với các giao dịch ẩn của hacker.

Khắc phục

Có ba phương pháp đối phó cơ bản với tấn công CSRF:

  • Synchronizer token
  • double submit Cookie
  • Kiểm tra Origin header

Synchronizer token

Phương pháp phổ biến nhất, được implement mặc định trong khá nhiều các framework backend hiện giờ. Với cách tiếp cận này, khi tạo một form để người dùng thực hiện nhập thông tin, phía backend sẽ nhúng một giá trị ẩn vào một hidden form input. Khi người dùng thực sự thực hiện submit form, server sẽ so sánh giá trị trường ẩn đó để xác thực người gửi thông tin chính là người dùng thật.

Bây giờ, giả sử ta click vào đường link giả mạo phía trên, các params truyền lên sẽ không thể nào có được token csrf => giao dịch bị chặn lại.

Với cách tiếp cận Synchronizer token này, best-practice là sử dụng token - dùng - một - lần : càng làm tăng khả năng bảo mật của phương pháp.

Cách làm này , mặc dù vậy, cũng có một số khó khăn nhất định:

  • với các framework truyền thống (vd như rails) , tầng view sẽ được xây dựng để có thể chứa được token trong hidden field khi xây dựng form. Mặt khác, với các ứng dụng thiết kế theo kiểu SPA - là kiểu ứng dụng mà bản chất là một trang static page đã được pre-compile với một đoạn javascript có nhiệm vụ update giao diện - Synchronizer Token sẽ đòi hỏi phải đưa ra được hidden field khi dựng form.
  • Synchronizer Token chỉ bảo vệ được POST request : Việc này có thể được đảm bảo miễn là ta thực hiện theo đúng quy tắc thiết kế : GET request không dùng để update dữ liệu.
  • Synchronizer Token đòi hỏi phải có một cơ chế để lưu lại token dùng để so sánh - có thể trong SQL hoặc 1 cơ chế cache nào đó, kéo theo đó có thể là các vấn đề gặp phải khi mở rộng hệ thống ra sau này. (chia sẻ token giữa các thành phần của một hệ thống lớn, nhiều server ...)

Double submit cookie

Với cách tiếp cận này, mỗi một request từ user sẽ được đính kèm với 2 cookie gửi kèm về browser. Một là cookie để xác thực danh tính (session id), 2 là một cookie chứa một giá trị random. Cách tiếp cận này dựa trên 2 cơ chế:

  • một là cơ chế có sẵn của các trình duyệt hiện này - Same Origin Policy - chỉ cho phép đoạn script được giao tiếp với một server nếu như endpoint của server kia có cùng origin (base URL) với endpoint gửi đoạn script đó lên.
  • hai là : giá trị của cookie thứ 2 kia được server trả về cho client thông qua header đối với các request sau đó.

Sau khi đăng nhập, server sẽ trả về cho client thêm một cookie nữa. Kể từ đây, mọi request lên của client đều phải chứa cookie này (có thể được đặt trong header hoặc đâu đó tùy yêu cầu) và được server xác thực.

Lúc này, bất cứ một ứng dụng bên thứ 3 nào cũng sẽ không thể giả mạo người dùng để gửi yêu cầu , do không thể set được HEADER của request cho trang web có URL khác với địa chỉ giả mạo.

Origin header check

Hầu hết các trình duyệt hiện tại đêu gửi Origin header trong request của mình. header này không thể được thêm vào bằng javascript. Server có thể kiểm tra giá trị header này và xác thực đây đúng là thông tin được submit lên từ đúng web của mình.

Server - Session vs token

Từ đầu bài viết, ta đã tập trung vào các vấn đề có thể gặp phải khi xây dựng ứng dụng và cách giải quyết vấn đề - đứng từ phía client, phía browser. Tiếp theo, ta sẽ nghiên cưu cách xử lý vấn đề bên phía server : các vấn đề gặp phải khi áp dụng cơ chế xác thực bằng session id và tìm hiểu một cơ chế khác liên quan tới token.

Vấn đề gặp phải khi sử dụng session id để authenticate

Các cách tiếp cận được nhắc tới ở phía trên đều liên quan tới việc bắt buộc phía server phải xây dựng được một cơ chế để lưu lại các thông tin xác thực ( hay ở đây cụ thể là session id). Sau khi người dùng login, thông tin xác thực được gửi tới cho client và lưu vào cookie, đồng thời nó cũng phải được lưu trữ bằng một cách nào đó phía server. Vấn đề đầu tiên ta gặp phải đó là khi cần phải mở rộng quy mô hệ thống:

  • Ban đầu, ta thiết kế việc lưu trữ session bằng cách lưu trữ nó vào 1 local storage nào đó (ví dụ redis ...)
  • Hệ thống phình to ra, ta cần sử dụng nhiều server để thực thi các request từ người dùng.
  • Lúc này, ta có thể gặp phải tình huống : server mà người dùng kết nối tới để thực hiện xác thực rất có thể không phải là server mà người dùng kết nối tới để thực hiện các giao dịch kế tiếp. => Ta lại phải xây dựng 1 service để đảm bảo các thông tin về session phải được đồng bộ giữa các server, hoặc xây dựng một trung tâm riêng để lưu giữ các thông tin về session này.

Mặt khác, thông tin về user cũng như các định danh khác cũng được lưu lại, và được lôi ra đối chiếu với mỗi một request lên server. Điều này cũng làm tốn tài nguyên, dù rằng ta dùng cơ chế để lưu vào CSDL hay chỉ lưu vào bộ nhớ. Cuối cùng, session bản thân nó không chứa một thông tin nào khác cả - ngoại trù việc nó là 1 chuỗi độc nhất - Client không thể dùng session để biết được danh tính người dùng hay kiểm tra những gì người dùng có thể làm , việc này bắt buộc phải thực hiện ở một request riêng biệt khác.

Đây là lúc mà ta có thể nghiên cứu và áp dụng một cơ chế xác thực khác - sử dụng Token để xác thực người dùng.

Token authentication đối với SPA

Một cách khác với việc sử dụng session để xác thực người dùng - đó là dùng token để định danh. Vì là một phương thức có thể dùng để thay thế cho session authentication, token authentication có khả năng giải quyết một số vấn đề đặt ra bởi session authentication:

  • Không đòi hỏi việc lưu trữ phía server
  • Tự bản thân token đã lưu trữ một số thông tin cơ bản về user được xác thực.

Đối với việc xây dựng web application, token authentication được implement thông qua cơ chế có tên là JSON web token (JWT).

Nói nhanh về định nghĩa - JWT là cái **(&%*#@ gì

Tài liệu đầy đủ nhất về JWT là đây: JWT specification, ok, thử đọc qua định nghĩa của JWT:

JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JavaScript Object Notation (JSON) object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or MACed and/or encrypted.

Chả hiểu cái beep gì sất, để hình dung JWT là gì và nó làm được cái gì càng khó hơn -- Nên tóm gọn lại:

JWT là JSON object đã được encode lại thành 1 token để trao đổi giữa các bên (ở đây là server và client) mà bản thân nó chứa nội dung dùng để định danh và xác thực người dùng.

Vậy nhìn vào đâu để biết 1 token là 1 JWT ? Chính là cấu trúc của nó:

aaaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbb.cccccccccccc

Chú ý 3 phần aaa, bbb, ccc được ngăn cách bằng 2 dấu chấm (.) 3 phần này là 3 object JSON đã được encode base64 và gộp vào làm chung 1 token, mỗi phần có ý nghĩa riêng:

aaa : Object header

Chứa một số thông tin chung về token, kiểu như loại token (mà ở đây mặc định luôn là jwt), thuật toán được xài để mã hóa token này

{
    "typ": "JWT",
    "alg": "HS256"
    ...
}

bbb: Nội dung chính của token

Giống như hồi cấp 1 học làm văn mà phải copy văn mẫu, thì cái header ở trên nó giống như mở bài của bài văn vậy: mang mục đích giới thiệu để người dùng biết token này là gì, còn ngoài thì nó chả có mang đến thông tin gì hết. Tất cả nội dung của token được đặt trong phần 2 này - còn gọi là payload.

mỗi một item trong payload gọi là 1 claim, là thứ chứa các thông tin của token.

{
    "iss": "github:03984",
    "exp: "2039429042309",
    "name": "ntd",
    "admin": "true",
    ......................
}

Có 3 loại claim, trong đó thì

  • reserved claim là các claim có các key được định nghĩa trước mà ta nên (hay bị bắt) phải tuân theo cho đúng chuẩn (vd như iss, exp ... các claim này để làm gì thì các bạn có thể đọc doc của nó phía trên -_- )
  • public claim: thường được mọi người sử dung rộng rãi và tốt nhất là không nên đông vào cho bị conflict
  • private claim: những key nào mà không bị nằm trong 2 nhóm trên thì ta có thể sử dụng thoải mái để define những thông tin mà ta muốn nhét vào token: thông tin xác thực, thông tin định danh người dùng (bằng cách này, ta có thể biết người dùng là ai, phạm vi quyền ... chỉ dựa vào token - khác với session, bản thân token đã chứa thông tin người dùng)

ccc: chữ kí

Cái này là cái quan trọng nhất của cái token : Signature tạo thành bằng cách đem header đã encode, payload đã encode, cùng với 1 secret key để đem đi mã hóa. Chữ ký chính là thành phần giúp xác thực xem đây có thực sự là token thực sự và không bị giả mạo hay không. Vì lí do đó, secret key là thành phần phải được giữ kín, giấu kĩ.

best practice với JWT:

JWT ngày càng được sử dụng rộng rãi, đặc biệt với các trang web xây dựng theo kiểu single page, hay do nó tương tích rất tốt với các cơ chế authorization, authentication như OAth2 hay OpenID. Tuy vậy, việc sử dụng JWT cũng có một vài điểm cần lưu ý.

  • Luôn luôn verify signature, không tin tưởng, cũng như không sử dụng, thông qua bất cứ token nào không được kí với secret key của mình.
  • Đảm bảo giữ an toàn cho secret key, ngoài phía tạo ra token - và có thể có thêm 1 phía client được tin tưởng - secret key không nên được public ra bên ngoài.
  • Chú ý về nơi lưu trữ JWT: webStorage vs Cookie.
  • Nên hạn chế lưu trữ những thông tin nhạy cảm trong JWT, và nếu phải làm như thế, ta nên mã hóa token => cái này được gọi là JWE hay JSON Web Encryption

Lưu trữ JWT - HTTPS-only cookie

Xây dựng web app, ta có một vài cách để lưu trữ thông tin bên phía client, bao gồm:

  1. HTML5 Web storage (localStorage, sessionStorage)

Giả sử sau khi login thành công, thay vì trả về sessionId cho client, server sẽ trả về response chứa 1 JWT như là 1 access token

HTTP/1.1 200 OK

{
    "access_token" : "skfaosa89ssldakfasdofiafdodsaifdds.aklsdfjaioasdfyaaoidsfjaosdfjaodsfadsdfgoaysadoiasdjgosdiagasdygosad.asdfoiaysdfoiasdfjoaisdfoasdfj"
}

bên phía client, ta chỉ việc lưu lại token này vào localStorage hoặc sessionStorage

function makeRequest(request, response) {
    ...
    $window.localStorage.access_token = response.body.access_token;
}
  1. Cookies

Bên phía server, thay vì để JWT trong response body, sẽ đứa nó vào HTTP header thông qua:

HTTP/1.1 200 OK
Set-Cookie: access_token=skfaosa89ssldakfasdofiafdodsaifdds.aklsdfjaioasdfyaaoidsfjaosdfjaodsfadsdfgoaysadoiasdjgosdiagasdygosad.asdfoiaysdfoiasdfjoaisdfoasdfj

Từ giờ, mỗi 1 request sau của client gửi lên sẽ tự động chứa token này.

  1. Vậy webStorage hay Cookies ?

Nhìn vào cách thực hiện, ta có thể thấy ngay : lưu trữ token trong webStorage được thực hiện thông qua javascript, và do đó có thể lấy ra hay chỉnh sửa bằng javascript, từ đó có nguy cơ bị dính các loại tấn công : điển hình là XSS attack. Việc implement để chống tấn công XSS có thể thực hiện khá dễ dàng , tuy nhiên với xu thế phát triển Single page web app dựa nhiều vào các 3rd-party library như hiện nay, rất không an toàn nếu như 1 thư viện nào nó ta sử dụng bị dính lỗ hổng, và từ đó có nguy cơ gây ảnh hướng tới web app của mình.

WebStorage tự bản thân nó không cung cấp một cơ chế bảo mật nào cả, vì vậy, tốt nhất là ta nên sử dụng cách thứ 2 : JWT Cookie Storage

Cookie - khi sử dụng với flag HttpOnly - không thể bị truy cập hay can thiệp bởi javascript, dẫn đến nó được miễn nhiễm với tấn công XSS. Gắn thêm cờ Secure, ta đảm bảo cookie luôn được gửi thông qua HTTPS.

Tuy nhiên, khi sử dụng cookie, ta lại có nguy cơ bị dính 1 loại tấn công khác - CSRF.

Rất may, hầu hết các framework hiện đại đều hỗ trợ native một vài phương thức phòng tránh tấn công CSRF. Ví dụ - Angular 2/4 hỗ trợ XSRF protection hay trong thư viện http của mình:

Token expiration:

Với từng loại token cùng với nghiệp vụ khác nhau, token nên được đính kèm theo hạn sử dụng ! Nếu như ứng dụng ta phát triển dưới dạng 1 mạng xã hội, hay 1 app di động ... token có thể sống lâu tùy ý (vd 1 ngày, 1 tuần .... hoặc lâu hơn). Với các nghiệp vụ đòi hỏi bảo mật cao như giao dịch ngân hàng, mua hàng online ... Token không nên có thời gian sống quá dài , chỉ nên giới hạn trong vài phút.

JWE:

Việc tạo chữ kí cho JWT ở trên có thể đảm bảo rằng thông tin trong payload là nguyên bản, và không bị chỉnh sửa bởi bên thứ 3. Tuy nhiên cách làm này lại không ngăn được bên thứ 3 đọc nội dung payload của token (do nội dung của JWT chỉ được encode base64 một cách đơn giản). Do đó, nếu như muốn chứa những thông tin nhạy cảm trong token , ta cần thêm 1 bước mã hóa - encrypt token.

Encoding khác với encryption: Base64 encoded có thể cho chuỗi đầu ra giống như là 1 chuỗi được được mã hóa - 1 dãy dài dằng dặc toàn kí tự lộn xộn - tuy nhiên nó cực kì dễ convert ngược trở lại thành dạng raw data.

Một token JWT khi chứa thông tin nhạy cảm thì cần phải được mã hóa, cơ chế này được gọi là JWE - JSON Web Encryption.

Kết luận

Bài viết điểm qua một vài cách thức tấn công phổ biến đối với web app, cũng như 1 vài cách đối phó với chúng. Đồng thời đưa ra so sánh 2 cách thức tiếp cận đối với việc xác thực người dùng : token authentication vs session authentication. Tuy nhiên, trong thực tế, luôn luôn có những cách tấn công nâng cao khác mà ta cần phải đối phó : token authentication - ngay cả khi sử dụng với https-only cookie - cũng có khả năng bị tấn công bởi các phương thức tấn công XSS nâng cao, ví dụ như . Vì vậy, việc cần thiết là phải luôn luôn cẩn thận và áp dụng nhiều biện pháp, cơ chế bảo vệ đồng thời để tăng độ an toàn cho hệ thống của mình.

Tài liệu tham khảo: https://stormpath.com/blog/secure-single-page-app-problem https://stormpath.com/blog/token-auth-spa https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage