Picture of the author
Published on

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

Auth API Spring Boot

ขอบคุณรูปภาพจาก 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

WebSecurityConfig.java
//...

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 ขึ้นไป

WebSecurityConfig.java
//...
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 Random

    XSRF Token
  • Request ทุก 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 การทำงานจะเป็นแบบนี้
      1. เราส่ง X-XSRF-TOKEN ให้ Server
      2. ทางฝั่ง Server ทำการ Verify กับ Token ก่อนหน้า และ Token อันนี้ไม่สามารถใช้ได้อีกแล้ว
      3. สร้าง Token ใหม่แบบ Random (ฝั่ง Server จำแล้วว่า Request ถัดไปต้องใช้ Token อันนี้เท่านั้น)
      4. ส่งกลับมาทาง Cookie response XSRF-TOKEN เพื่อนำไปใช้ใน Request ถัดไป วนแบบนี้ไปเรื่อยๆ

CSRF Token Controller

สำหรับสร้าง CSRF Token ไว้ใช้ในการ Request ครั้งแรก

CsrfController.java
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

services.js
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

  1. เพิ่ม Environment และเพิ่มตัวแปร xsrf-token
Postman variable
  1. Tab Test เขียน Script set ค่าให้กับตัวแปร xsrf-token โดยดึงค่าจาก Cookie response
pm.environment.set("xsrf-token", pm.cookies.get("XSRF-TOKEN"))
  1. เพิ่ม X-XSRF-TOKEN ใน Request Headers โดยกำหนดค่าเป็นตัวแปร xsrf-token
Postman variable
  1. ยิง Postman สร้าง CSRF Token (สำหรับครั้งแรกเท่านั้น)
Postman variable
  1. ยิง Postman /signin

    • แบบมี X-XSRF-TOKEN

      Postman variable
    • แบบไม่มี X-XSRF-TOKEN

      Postman variable
  2. ตรวจสอบ Value ของตัวแปร

Postman variable