+6

[Spring Security] Cross Site Request Forgery (CSRF)

Hi all, have a nice day!

Trong bài viết này chúng ta sẽ thảo luận về sự hỗ trỡ của Spring Security đối với việc phòng các cuộc tấn công CSRF.

Cross Site Request Forgery (CSRF)

1. Tấn công CSRF (CSRF attack)

Tài liệu về tấn công CSRF trên mạng có rất là nhiều, mình tóm tắt lại một vài điểm theo cách mình hiểu như :

  • Chèn mã độc, link độc hướng người dùng chuyển trang theo ý của kẻ tấn công
  • Dùng javascript giả mạo phương thức POST
  • Hướng URL trên trang quản trị ADMIN sang một hành động ác ý như : xóa tài khoản người dùng, ....

Có thể gọi nôm na tấn công CSRF là cuộc tấn công giả mạo (mượn quyền trái phép) Để hiểu hơn về cuộc tấn công CSRF là gì chúng ta cùng tìm hiểu thông qua ví dụ dưới đây.

Giả sử chúng ta đang truy cập vào trang web ngân hàng và chúng ta thực hiện hành động chuyển tiền từ tài khoản của mình sang một tài khoản ngân hàng khác, ví dụ HTTP request sẽ có dạng :

POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly
Content-Type: application/x-www-form-urlencoded

amount=100.00&routingNumber=1234&account=9876

Bây giờ giả sử chúng ta đã submit hành động chuyển tiền bên trên, sau đó quên không đăng xuất mà vô tình click vào một link khác có chứa mã độc với đoạn html như sau :

<form action="https://bank.example.com/transfer" method="post">
<input type="hidden"
	name="amount"
	value="100.00"/>
<input type="hidden"
	name="routingNumber"
	value="evilsRoutingNumber"/>
<input type="hidden"
	name="account"
	value="evilsAccountNumber"/>
<input type="submit"
	value="Kiếm tiền thật dễ!"/>
</form>

Chúng ta thấy cụm từ "Kiếm tiền thật dễ", điều này gây sự thu hút và chúng ta đã click vào button đó. Thật không may trong quá trình này chúng ta đã vô tình chuyển sang tài khoản của người lạ kia 100$. Điều này xảy ra dù kẻ tấn công không xem được cookie tài khoản ngân hàng của chúng ta nhưng những cookie có liên kết với ngân hàng của chúng ta đã được gửi đi cùng với request vừa rồi.

Tuy nhiên điều này còn nguy hiểm hơn đó là chúng ta không cần phải click vào nút mà quá trình chuyển tiền cho kẻ lạ vẫn diễn ra một cách tự động thông qua việc sử dụng mã JavaScript. Vậy làm thế nào để chúng ta có thể phòng chống lạ các cuộc tấn công này?

2. Đồng bộ Token (Synchronizer Token Pattern)

Vấn đề là các HTTP request từ trang web ngân hàng và các request từ trang web của kẻ tấn công là giống nhau. Điều này dẫn đến không có cách nào để từ chối các request từ web độc và cho phép chúng điều hướng đến trang web ngân hàng. Để chống lại các cuộc tấn công CSRF chúng ta cần đảm bảo rằng các request từ web độc không lấy được bất cứ thông tin nào trong web ngân hàng. Giải pháp được sử dụng đó là Synchronizer Token Pattern. Điều này đảm bảo rằng với mỗi request được yêu cầu, ngoài session cookie hiện tại thì sẽ có một mã token được sinh ra ngẫu nhiên như một tham số của HTTP. Khi mà request được submit, server sẽ tìm kiếm giá trị token trong request và so sánh với giá trị token được lưu trên server, nếu không khớp thì request sẽ thất bại. Chúng ta hoàn toàn có thể thoải mái vì chỉ các HTTP request mới yêu cầu mã token, mã token này sẽ được dùng để update trạng thái của các request mới. Điều này được đảm bảo an toàn vì chỉ các trang web cùng nguồn gốc mới có thể đọc được response. Ngoài ra cũng cũng xin khuyến cáo là thêm rằng không sử dụng token trong HTTP GET vì thông tin có thể bị rò rỉ. Hãy cùng xem ví dụ bên trên của chúng ta thay đổi như thế nào, giả sử mã token được tạo ngẫu nhiên trong tham số _csrf , thì request sẽ có dạng

POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly
Content-Type: application/x-www-form-urlencoded

amount=100.00&routingNumber=1234&account=9876&_csrf=<secure-random>

Như vậy là chúng ta vừa thêm vào mã token được sinh ngẫu nhiên, từ bây giờ web độc sẽ không thể đoán được mã token , => request từ web độc sẽ thất bại trong quá trình server so sánh mã token.

3. Khi nào sử dụng CSRF (When to use CSRF protection)

Chúng tôi khuyến cáo rằng sử dụng CSRF protection trong mọi request của người dùng trên browser. Trong một vài trường hợp chúng ta chỉ tạo ra service để sử dụng mà không phải là browser thì chúng ta có thể vô hiệu hóa CSRF protection.

3.1 CSRF protection and JSON

Câu hỏi phổ biến là "làm thế nào để tôi có thể bảo vệ một JSON request được tạo bằng JavaScript?". Câu trả lời rằng còn tùy thuộc. Tuy nhiên chúng ta cần cẩn thận vì cách khai thác CSRF có thể ảnh hưởng đến các JSON request. Ví dụ kẻ tấn công có thể tạo CSRF với JSON như sau :

<form action="https://bank.example.com/transfer" method="post" enctype="text/plain">
<input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
<input type="submit"
	value="Win Money!"/>
</form>

Cấu trúc Json được tạo ra

{ 
    "amount": 100,
    "routingNumber": "evilsRoutingNumber",
    "account": "evilsAccountNumber",
    "ignore_me": "=test"
}

Nếu ứng dụng không xác nhận kiểu kiểu Content-Type thì dữ liệu này sẽ bị khai thác. Tùy thuộc vào cách cài đặt, một ứng dụng Spring MVC, dù xác nhận kiểu Content-Type vẫn có thể bị tấn công khai thác bằng cách thêm hậu tố trên URL :

<form action="https://bank.example.com/transfer.json" method="post" enctype="text/plain">
<input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
<input type="submit"
	value="Win Money!"/>
</form>
3.2 CSRF and Stateless Browser Applications

Điều gì xảy ra nếu ứng dụng của tôi vận hành cơ chế stateless? Điều này không có nghĩ rằng ứng dụng của chúng ta được bảo vệ. Trong thực thế, người dùng không thực hiện một hành động nào trong trình duyệt web vẫn có thể bị tấn công CSRF.

Ví dụ, hãy cùng xem xét một ứng dụng dùng một cookie thông thường để lưu tất cả các trạng thái thông tin thay vì sử dụng JSESSIONID. Khi mà cuộc tấn công CSRF được tiến hành, các cookie thông thường được tạo ra và gửi cùng với request theo cách mà JSESSIONID được gửi trong ví dụ trước của chúng tôi. Nếu người dùng thực hiện đăng nhập bình thường thì vẫn có thể bị tấn công, vì trình duyệt tự động thêm username và password trong bất kỳ request nào tương tự cách mà JSESSIONID được gửi như ví dụ trước của chúng tôi.

4. Sử dụng Spring Security CSRF (Using Spring Security CSRF Protection)

Như vậy các bước cần thiết để sử dụng Spring Security để chông lại các cuộc tấn công CSRF như thế nào? Dưới đây là các bước tiến hành :

4.1 Sử dụng HTTP thích hợp (Use proper HTTP verbs)

Điều đầu tiên để website của chúng ta chống lại các cuộc tấn công CSRF là chắc chắn rằng chúng ta dùng đúng các HTTP thích hợp. Cụ thể chúng ta cần chắc chắn rằng ứng dụng của chúng ta đang sử dụng PATCH, PUT, POST, DELETE, khi thay đổi trạng thái.

Đây không phải là một giới hạn sự hỗ trợ của Spring Security, mà thay vào đó là một yêu cầu chung cho việc phòng chống CSRF. Lý do nếu bao gồm thông tin cá nhân trong một HTTP GET có thể khiến thông tin bị rò rỉ. Xem hướng dẫn [RFC 2616 Section 15.1.3 Encoding Sensitive Information in URI’s] để biết thêm tại sao cần sử dụng POST thay vì GET để gửi thông tin.

4.2 Cấu hình CSRF (Configure CSRF Protection)

Các bước tiếp theo sẽ thiết lập Spring Security CSRF bên trong ứng dụng của chúng ta. Một số framwork xử lý các CSRF token không hợp lệ bằng cách vô hiệu hóa phiên của người dùng, nhưng điều này gây ra các vấn đề riêng của nó. Thay vào đó, mặc định Spring Security's CSRF sẽ truy cập HTTP 403 khi request bị từ chối. Điều này có thể được tùy chỉnh bởi config AccessDeniedHandler để xử lý InvalidCsrfTokenException differently. Trong Spring 4.0 config bằng file xml thì mặc định CSRF sẽ được enabled. Nếu chúng ta muốn disable thì sẽ config lại như sau:

<http>
	<!-- ... -->
	<csrf disabled="true"/>
</http>

Trong cách config bằng Java thì mặc định CSRF mặc định được enable. Nếu chúng ta muốn disable CSRF thì xem cách config dưới đây, hoặc đọc thêm Javadoc về csrf() để tùy biến cách config:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
        .csrf().disable();
    }
}
4.3 Include the CSRF Token

Form Submissions Bước cuối cùng để chắc chắn rằng chúng ta đã bao gồm các cài đặt CSRF token trong tất cả các phương thức PATCH, PUT, POST và DELETE. Một cách để tiếp cận điều này là sử dụng thuộc tính _csrf để có được CsrfToken hiện tại. Ví dụ làm điều này với trang JSP như sau:

<c:url var="logoutUrl" value="/logout"/>
<form action="${logoutUrl}"
	method="post">
<input type="submit"
	value="Log out" />
<input type="hidden"
	name="${_csrf.parameterName}"
	value="${_csrf.token}"/>
</form>

Cách tiếp cận dễ hơn nữa là sử dụng thẻ csrfInput của thư viện Spring Security JSP

Nếu chúng ta sử dụng thẻ form:form của Spring MVC hoặc themeleaf 2.1+ và sử dụng enable @EnableWebSecurity thì CsrfToken sẽ tự động được cấu hình cho chúng ta (sử dụng CsrfRequestDataValueProcessor).

Ajax and JSON Requests Nếu chúng ta sử dụng Json thì không thể gửi CSRF token cùng như các tham số của HTTP. Thay vào đó chúng ta có thể gửi token đó trong header của HTTP. Cách điển hình là gửi CSRF token trong thẻ meta. Ví dụ sử dụng với trang JSP như dưới đây :

<html>
<head>
	<meta name="_csrf" content="${_csrf.token}"/>
	<!-- default header name is X-CSRF-TOKEN -->
	<meta name="_csrf_header" content="${_csrf.headerName}"/>
	<!-- ... -->
</head>
<!-- ... --

Thay vì cách tạo thẻ meta bằng tay, chúng ta có thể sử dụng thẻ csrfMeta của thư viện Spring Security JSP. Sau đó chúng ta có thể thêm các token trong request Ajax của chúng ta. Nếu chúng ta đang sử dụng JQuery thì chúng ta có thể làm điều đó như sau :

$(function () {
    var token = $("meta[name='_csrf']").attr("content");
    var header = $("meta[name='_csrf_header']").attr("content");
    $(document).ajaxSend(function(e, xhr, options) {
        xhr.setRequestHeader(header, token);
    });
});

Như một thay thế của JQuery chúng tôi khuyên chúng ta nên sử dụng rest.js của cujoJS’s . Mô đun rest.js hỗ trợ nâng cao để làm việc với các phản hồi của HTTP theo chuẩn RESTful.

var client = rest.chain(csrf, {
    token: $("meta[name='_csrf']").attr("content"),
    name: $("meta[name='_csrf_header']").attr("content")
});

Cấu hình phía khách có thể được chia sẻ với bất cứ thành phần nào của ứng dụng cần tạo yêu cầu tới tài nguyên cần được bảo vệ bới CSRF. Một điểm khác biệt giữa rest.js và JQuery là chỉ các request được thực hiện với người dùng được cấu hình chứa CSRF token. Còn JQuery thì tất cả các request sẽ bao gồm token.

CookieCsrfTokenRepository

Đây là trường hợp người dùng muốn duy trì CsrfToken trong một cookie. Theo mặc định CookieCsrfTokenRepository sẽ ghi vào một cookie có tên XSRF-TOKEN và đọc nó từ header có tên X-XSRF-TOKEN hoặc thông số HTTP csrf. Đây là Chúng ta có thể cấu hình CookieCsrfTokenRepository trong file xml như sau

<http>
	<!-- ... -->
	<csrf token-repository-ref="tokenRepository"/>
</http>
<b:bean id="tokenRepository"
	class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
	p:cookieHttpOnly="false"/>

Thiết lập đặt cookieHttpOnly = false. Điều này là cần thiết để cho phép JavaScript (tức là AngularJS) để đọc nó. Nếu chúng ta không cần đọc cookie bằng JavaScript thì chúng ta có thể bỏ qua thiết lập cookieHttpOnly = false để tăng khả năng bảo mật.

Chúng ta có thể cấu hình CookieCsrfTokenRepository bằng Java như dưới đây:

@EnableWebSecurity
public class WebSecurityConfig extends
		WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.csrf()
				.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
	}
}

Thiết lập đặt cookieHttpOnly = false. Điều này là cần thiết để cho phép JavaScript (tức là AngularJS) để đọc nó. Nếu chúng ta không cần đọc cookie bằng JavaScript thì chúng ta có thể bỏ qua thiết lập cookieHttpOnly = false bằng cách sử dụng CookieCsrfTokenRepository() thay thế) để cải thiện bảo mật.

5. Cẩn thận khi sử dụng CSRF (CSRF Caveats)

Dưới đây là một vài điểm cần lưu ý khi sử dụng CSRF.

5.1 Timeouts

Một vấn đề là chúng ta mong đợi CSRF token sẽ được lưu lại HttpSession, vì vậy ngay khi HttpSession hết hạn, AccessDeniedHandler được cấu hình trước đó sẽ nhận được một InvalidCsrfTokenException. Nếu chúng ta đang sử dụng AccessDeniedHandler mặc định, trình duyệt sẽ đưa ra lỗi 403 và thông báo đến người dùng.

Một câu hỏi tại sao CSRF token không mặc định được lưu trong một cookie. Điều này là do có các khai thác được biết đến trong header (như xác định cookie) có thể bị thay đổi tên miền khác (thay đổi domain). Đây cũng là lý do Ruby on Rails không còn bỏ qua việc kiểm tra CSRF khi có mặt header. Một bất lợi khác là do xóa các trạng thái (tức là thời gian chờ), chúng ta sẽ mất khả năng hủy bỏ mã token nếu nó bị xâm nhập.

Một cách đơn giản để giảm thiểu người dùng đang hoạt động trải qua thời gian chờ là tạo ra một đoạn mã JavaScript cho phép người dùng biết phiên của họ sắp hết hạn. Người dùng sẽ click vào một nút để tiếp tục phiên mới. Ngoài ra, chỉ định một AccessDeniedHandler và tùy chỉnh nó cho phép chúng ta xử lý các InvalidCsrfTokenException bất cứ cách nào chúng ta muốn. Để biết làm thể nào để cấu hình các AccessDeniedHandler, tham khảo tại config xml hoặc config java Cuối cùng, ứng dụng có thể được cấu hình để sử dụng CookieCsrfTokenRepository mà sẽ không hết hạn. Như đã đề cập trước đây, điều này không an toàn như sử dụng một phiên, nhưng trong nhiều trường hợp như vậy cũng là đủ tốt.

5.2 Logging in

Để chống lại các cuộc tấn công giả mạo, các thông tin đăng nhập trên form cũng cần được bảo vệ. Kể từ khi CsrfToken được lưu giữ trong HttpSession, điều này có nghĩa là một HttpSession sẽ được tạo ra ngay khi thuộc tính token CsrfToken được truy cập. Mặc dù điều là không tốt theo kiến trúc RESTful/stateless. Nếu không thông qua trạng thái (state) thì chúng tôi không thể làm gì nếu mã token bị tấn công. Thực tế là việc sử dụng mã token sẽ không làm ảnh hướng gì đáng kể đến kiến trúc.

Kỹ thuật chung để bảo vệ thông tin trên form đăng nhập là dùng một đoạn mã JavaScript để nhận được một đoạn mã token hợp lệ trước khi dữ liệu trong form được submit. Bằng cách này, chúng ta không cần phải nghĩ đến thời gian chờ của phiên (timeout) được thảo luận trong phần trước. Vì phiên được tạo ngay trước khi gửi biểu mẫu (giả định rằng CookieCsrfTokenRepository không được định cấu hình thay thế). Vì vậy người dùng có thể ở lại trên trang đăng nhập và submit username/password khi muốn. Để đạt được điều này, chúng ta có thể tận dụng lợi thế của CsrfTokenArgumentArchumentResolver được cung cấp bởi Spring Security và chỉ ra một điểm cuối giống như nó được mô tả tại đây.

5.3 Logging out

Thêm CSRF sẽ cập nhật LogoutFilter chỉ sử dụng HTTP POST. Điều này đảm bảo rằng đăng xuất yêu cầu mã thông báo CSRF và kẻ tấn công không thể buộc làm gì mà phải đăng xuất khỏi tài khoản người dùng của chúng ta. Một cách tiếp cận là sử dụng một mẫu để đăng xuất. Nếu chúng ta thực sự muốn có liên kết, chúng ta có thể sử dụng JavaScript để liên kết thực hiện POST (ví dụ: có thể trên một form ẩn, input ẩn). Đối với các trình duyệt đã tắt JavaScript, chúng ta có thể tùy chọn có liên kết đưa người dùng đến trang xác nhận đăng xuất sẽ thực hiện phương thức POST.

Nếu chúng ta thực sự muốn sử dụng HTTP GET để đăng xuất, chúng ta cũng có thể làm như vậy, nhưng nhớ rằng điều này được khuyến nghị không nên dùng với GET. Ví dụ: Cấu hình Java như sau sẽ thực hiện đăng xuất bằng URL / logout được yêu cầu với bất kỳ phương thức HTTP nào:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.logout()
				.logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
	}
}
5.4 Multipart (file upload)

Có hai lựa chọn để sử dụng bảo vệ CSRF với multipart / form-data. Mỗi lựa chọn có sự cân bằng riêng của nó. Placing MultipartFilter before Spring Security Include CSRF token in action

Trước khi tích hợp CSRF của Spring Security với tải lên nhiều tệp, hãy đảm bảo rằng chúng ta có thể tải tệp lên mà chưa config sử dụng CSRF trước tiên. Thông tin thêm về việc sử dụng tải tệp lên tại Spring’s multipart (file upload) support và tại MultipartFilter javadoc

Placing MultipartFilter before Spring Security

Tùy chọn đầu tiên là đảm bảo rằng MultipartFilter được chỉ định trước bộ lọc Spring Security. Chỉ định MultipartFilter trước bộ lọc Spring Security có nghĩa là không có sự cho phép để gọi MultipartFilter có nghĩa là bất cứ ai cũng có thể đặt các tệp tạm thời trên máy chủ của chúng ta. Tuy nhiên, chỉ có những người dùng được ủy quyền sẽ có thể gửi một File được xử lý bởi ứng dụng của chúng ta. Nói chung, đây là cách tiếp cận được khuyến nghị vì tải tệp tin tạm thời nên có ảnh hưởng không đáng kể đối với hầu hết các máy chủ. Để đảm bảo MultipartFilter được chỉ định trước bộ lọc Spring Security với cấu hình java, người dùng có thể ghi đè trước SpringSecurityFilterChain như dưới đây:

public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {

	@Override
	protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
		insertFilters(servletContext, new MultipartFilter());
	}
}

Để đảm bảo MultipartFilter được chỉ định trước bộ lọc Spring Security với cấu hình XML, người dùng phải đảm bảo phần tử <filter-mapping> của MultipartFilter được đặt trước springSecurityFilterChain trong web.xml như hình dưới đây

<filter>
	<filter-name>MultipartFilter</filter-name>
	<filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
</filter>
<filter>
	<filter-name>springSecurityFilterChain</filter-name>
	<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
	<filter-name>MultipartFilter</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
	<filter-name>springSecurityFilterChain</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

Include CSRF token in action

Để chặn người dùng mạo danh tải các tệp tin tạm thời không thể chấp nhận, thay thế bằng cách đặt MultipartFilter sau bộ lọc Spring Security và bao gồm CSRF như một tham số truy vấn trong thuộc tính action form. Xem ví dụ với trang JSP như dưới đây :

<form action="./upload?${_csrf.parameterName}=${_csrf.token}" method="post" enctype="multipart/form-data">

Bất lợi cho cách tiếp cận này là các tham số truy vấn có thể bị rò rỉ. Hơn nữa, nó được coi là cách tốt nhất để đặt các dữ liệu nhạy cảm trong body hoặc phần header để đảm bảo nó không bị rò rỉ. Thông tin bổ sung phần này có tại RFC 2616 Section 15.1.3 Encoding Sensitive Information in URI’s

HiddenHttpMethodFilter

HiddenHttpMethodFilter phải được đặt trước bộ lọc Spring Security. Nó có thể có ý nghĩa bổ sung khi bảo vệ chống lại các cuộc tấn công CSRF. Lưu ý rằng HiddenHttpMethodFilter chỉ ghi đè lên phương thức HTTP trên POST, vì vậy điều này thực sự không gây ra vấn đề nào. Tuy nhiên, cách tốt nhất hãy đảm bảo nó được đặt trước bộ lọc của Spring Security.

6. Overriding Defaults

Mục tiêu của Spring Security là cung cấp các mặc định bảo vệ người dùng tránh được các cuộc khai thác thông tin. Điều này không có nghĩa là chúng ta buộc phải chấp nhận tất cả những sai sót của Spring Security. Hoặc ví dụ, chúng ta có thể cung cấp một CsrfTokenRepository tùy chỉnh để ghi đè lên cách lưu trữ CsrfToken.

Chúng ta cũng có thể chỉ định một RequestMatcher tùy chỉnh để xác định những yêu cầu nào được bảo vệ bởi CSRF (ví dụ như chúng ta không quan tâm nếu logout được khai thác). Nói tóm lại, nếu bảo vệ CSRF của Spring Security không hoạt động như chúng ta mong muốn, chúng ta có thể tùy nó. Tham khảo <csrf> tài liệu hướng dẫn để biết chi tiết về làm thế nào để tùy chỉnh với XML và CsrfConfigurer để tùy chỉnh khi sử dụng cấu hình Java.

Bài viết được dịch từ : https://docs.spring.io/spring-security/site/docs/current/reference/html/csrf.html#csrf-use-proper-verbs


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí