- Published on
เพิ่มการป้องกัน CSRF ให้กับ Spring Security

ขอบคุณรูปภาพจาก https://vpnoverview.com
Spring Boot ผมจะไม่สร้างใหม่ แต่ผมจะใช้อันเดียวกับบทความนี้ Authentication ด้วย Spring Security & JWT เอามา Implement ต่อ Source Code
CSRF คืออะไร ?
ชื่อเต็มคือ Cross-Site Request Forgery เป็นการโจมตี Website รูปแบบหนึ่งที่ Hacker นิยมใช้ โดยการหลอกเหยื่อให้กดลิงก์หรือเข้าใช้เว็บไซต์ที่ทาง Hacker ได้เตรียมไว้แล้ว และเปลี่ยนข้อมูลบางอย่างตามที่ Hacker ต้องการ โดยจะใช้สิทธิการเข้าถึงข้อมูลของเหยื่อ กระทำการใดๆโดยที่เหยื่ออาจจะไม่รู้ตัว หรือทาง Website ไม่รู้เลยว่ากำลังโดน Hack เพราะการร้องขอต่างๆบ่งบอกว่าเป็นการทำรายการจาก User ด้วยตัวเอง ซึ่งได้ผลมากในยุคแรกๆ เพราะยังไม่มีการป้องกัน CSRF
จุดเริ่มต้น
เมื่อทำการ Scan code มันมี Issue แจ้งมาเรื่อง Security อยู่ในระดับร้ายแรง โดยบอกว่าพบ .csrf().disable() ใน WebSecurityConfig.java ซึ่งทางด้าน Security มองว่าเราปิดหรือไม่มีการป้องกัน CSRF
ซึ่งก่อนหน้านี้ผมไม่ได้สนใจมันเลยด้วยซ้ำ เลยเริ่มศึกษา และตัดสินใจเพิ่มการป้องกัน CSRF เข้าไปในโปรเจคด้วย เท่ากับว่าเป็นการอัพระดับความปลอดภัยขึ้นไปอีก
โดยปกดิแล้ว Spring Security จะ Default การ Config มาให้เป็น
.csrf().disable()เสมอ
Config WebSecurityConfig.java
จากที่ผมได้ลองทำการ Config มันจะมีความแตกต่างระหว่างก่อนและหลัง Spring Security 6
ก่อน Spring Security 6
//...
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
//...
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
return http.build();
}
}
Config แค่นี้เลย
Spring Security 6 ขึ้นไป
//...
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
// set the name of the attribute the CsrfToken will be populated on
requestHandler.setCsrfRequestAttributeName("_csrf");
http
//...
.csrf((csrf) -> csrf
.csrfTokenRepository(tokenRepository)
.csrfTokenRequestHandler(requestHandler)
)
return http.build();
}
จะมีการ Config เพิ่มมาเล็กน้อย
หลักการทำงาน
CSRF ใช้ไม่ได้กับ Http Mehtod ที่เป็น GET เพราะด้วยหลักการแล้วเป็นเพียงแค่การดึงข้อมูล ถ้าใน Logic ของคุณมีการ Create,Update หรือ Delete ควรเปลี่ยนมาใข้ PUT, POST, DELETE หรือ PATCH
Response ทุก Response จะมี Cookie response เป็น
XSRF-TOKENและ Value เป็นแบบ Token RandomRequest ทุก Request ต้องมี Request Headers
X-XSRF-TOKEN- URLs ที่ไม่ต้อง Authen (
.requestMatchers(...).permitAll()) อย่างเช่น/signup,signinสามารถใช้ Token เดิมได้ตลอด ไม่ต้อง Random ใหม่ แต่ต้องมีX-XSRF-TOKENในทุก Requestแล้วการ Reuqest ครั้งแรก จะเอา Token มาจากไหน ? สามารถทำได้ด้วยการ Call API อะไรก็ได้ด้วย Http Method 'GET' ก็จะได้
XSRF-TOKENกลับมาแล้ว - URLs ที่ต้องผ่านการ Authen (
.requestMatchers(...).authenticated()) ไม่สามารถใช้ Token เดิมได้ต้องเปลี่ยน Token ทุก Request การทำงานจะเป็นแบบนี้- เราส่ง
X-XSRF-TOKENให้ Server - ทางฝั่ง Server ทำการ Verify กับ Token ก่อนหน้า และ Token อันนี้ไม่สามารถใช้ได้อีกแล้ว
- สร้าง Token ใหม่แบบ Random (ฝั่ง Server จำแล้วว่า Request ถัดไปต้องใช้ Token อันนี้เท่านั้น)
- ส่งกลับมาทาง Cookie response
XSRF-TOKENเพื่อนำไปใช้ใน Request ถัดไป วนแบบนี้ไปเรื่อยๆ
- เราส่ง
- URLs ที่ไม่ต้อง Authen (
CSRF Token Controller
สำหรับสร้าง CSRF Token ไว้ใช้ในการ Request ครั้งแรก
package com.demo.auth.controllers;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/csrf")
public class CsrfController {
@GetMapping("/firsttime")
public CsrfToken allAccess(CsrfToken token) {
return token;
}
}
อย่าลืมเพิ่ม .requestMatchers("/csrf/**")).permitAll()
ตัวอย่าง Front-End สำหรับการใช้งาน CSRF
import axios from 'axios'
// Add a response interceptor to get the CSRF token from the cookie
axios.interceptors.response.use(
response => {
const xsrfToken = response.headers['set-cookie']
.find(cookie => cookie.startsWith('XSRF-TOKEN='))
.split(';')[0]
.split('=')[1]
if (xsrfToken) {
axios.defaults.headers.common['X-XSRF-TOKEN'] = xsrfToken
}
return response
},
error => {
return Promise.reject(error)
}
)
// Make a POST request with CSRF token included
axios.post('/api/post-data', {
data: 'example data'
})
ต้องทำ Interceptor สำหรับ Response
ทดสอบด้วย Postman
- เพิ่ม Environment และเพิ่มตัวแปร
xsrf-token
- Tab Test เขียน Script set ค่าให้กับตัวแปร
xsrf-tokenโดยดึงค่าจาก Cookie response
pm.environment.set("xsrf-token", pm.cookies.get("XSRF-TOKEN"))
- เพิ่ม
X-XSRF-TOKENใน Request Headers โดยกำหนดค่าเป็นตัวแปรxsrf-token
- ยิง Postman สร้าง CSRF Token (สำหรับครั้งแรกเท่านั้น)
ยิง Postman
/signinแบบมี
X-XSRF-TOKENแบบไม่มี
X-XSRF-TOKEN
ตรวจสอบ Value ของตัวแปร