๐ก๏ธ CSRF (Cross-Site Request Forgery) ์ฝ๊ฒ ์ดํดํ๊ธฐ
CSRF๋ ํด์ปค๊ฐ '๋์ธ ์ฒ' ๊ฐ์ฅํด์ ์น์ฌ์ดํธ์ ์ด์ํ ์์ฒญ์ ๋ณด๋ด๋ ๊ณต๊ฒฉ ๋ฐฉ๋ฒ์
๋๋ค.
์ฝ๊ฒ ๋งํด, ๋ด๊ฐ ๋ก๊ทธ์ธ๋์ด ์๋ค๋ ์ ์ ์
์ฉํด ๋ด๊ฐ ์ํ์ง ์์ ํ๋(๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ, ๊ฒฐ์ , ๊ธ ์์ฑ ๋ฑ)์ ๋๋ ๋ชจ๋ฅด๊ฒ ํ๊ฒ ๋ง๋๋ ๊ฒ์
๋๋ค.
๐ก 1. ์ฌ์ด ๋น์ : ๋์ด๊ณต์ ์์ ์ด์ฉ๊ถ ์ฌ๊ฑด
์ด ์ํฉ์ '๋์ด๊ณต์'์ ๋น์ ํ๋ฉด ์์ฃผ ์ฝ๊ฒ ์ดํดํ ์ ์์ต๋๋ค.
โ ๋ก๊ทธ์ธ (์์ ์ด์ฉ๊ถ ๋ฐ๊ธ)
์ฌ๋ฌ๋ถ์ด ๋์ด๊ณต์์์ ์๋ชฉ์ '์์ ์ด์ฉ๊ถ ๋ฐด๋'๋ฅผ ์ฐผ์ต๋๋ค.
- ์ด ๋ฐด๋๋ง ๋ณด์ฌ์ฃผ๋ฉด ๋งค์ ์์ ๋ฐฅ์ ์ธ์์ผ๋ก ๋จน์ ์ ์์ต๋๋ค.
- (์ธํฐ๋ท ์ํฉ): ์น์ฌ์ดํธ์ ๋ก๊ทธ์ธํด์ '์ฟ ํค/์ธ์ '์ ๋ฐ์ ์ํ
โก ํจ์ (์ฌ๊ธฐ๊พผ์ ๋ฑ์ฅ)
๋ฒค์น์์ ์ฌ๊ณ ์๋๋ฐ, ๋ฏ์ ์ฌ๋์ด ๋ค๊ฐ์ "์ด ์์ ์์ ๋ญ๊ฐ ์๋์ง ๊ตฌ๊ฒฝ๋ง ํด๋ณผ๋?"๋ผ๊ณ ํฉ๋๋ค.
- (์ธํฐ๋ท ์ํฉ): ํด์ปค๊ฐ ๋ณด๋ธ ์ด๋ฉ์ผ์ด๋ ๋งํฌ๋ฅผ ํด๋ฆญ
โข ๊ณต๊ฒฉ (๊ฐ์ง ์ฃผ๋ฌธ)
์์๋ฅผ ์ฌ๋ ์๊ฐ, ์์ ๋ฐ๋ฅ์ ์จ๊ฒจ์ง ๋ฌด์ ๊ธฐ๊ฐ ๋งค์ ์ ์ฃผ๋ฌธ์ ๋ฃ์ต๋๋ค.
๐ป "์ง๊ธ ์ด ๋ฐด๋๋ฅผ ์ฐฌ ํ์ ์์ผ๋ก ํ๋ฒ๊ฑฐ 100๊ฐ ์ฃผ๋ฌธ! ๊ฒฐ์ ๋ ๋ฐด๋๋ก!"
โฃ ํผํด ๋ฐ์
๋งค์ ์ง์์ ๋ฌด์ ๊ธฐ ์ ํธ์ ํจ๊ป ์ฌ๋ฌ๋ถ์ '์์ ์ด์ฉ๊ถ ๋ฐด๋'๊ฐ ํ์ธ๋๋, ์ฌ๋ฌ๋ถ์ด ์ง์ ์ฃผ๋ฌธํ ์ค ์๊ณ ๊ฒฐ์ ํด ๋ฒ๋ฆฝ๋๋ค. ์ด๊ฒ์ด ๋ฐ๋ก CSRF์ ๋๋ค.
๐ป 2. ์ค์ ์ธํฐ๋ท์์๋ ์ด๋ป๊ฒ ์ผ์ด๋ ๊น์?
- ๋ก๊ทธ์ธ ์ํ: ์ฌ๋ฌ๋ถ์ด ์ํ ์ฌ์ดํธ์ ๋ก๊ทธ์ธ์ ํด๋ก๋๋ค. (๋ธ๋ผ์ฐ์ ๊ฐ ๋ก๊ทธ์ธ ์ฆํ๋ฅผ ๊ธฐ์ตํจ)
- ๋ฏธ๋ผ ํด๋ฆญ: ํด์ปค๊ฐ ๋ณด๋ธ "๊ฒฝํ ๋น์ฒจ" ๋งํฌ๋ฅผ ํด๋ฆญํฉ๋๋ค.
- ๋ชฐ๋ ์ ์ก: ํด์ปค์ ์ฌ์ดํธ์๋ ๋์ ๋ณด์ด์ง ์๋ ๋ช ๋ น์ด๊ฐ ์จ๊ฒจ์ ธ ์์ต๋๋ค.
*"์ํ์, ์ง๊ธ ์ ์ํ ์ฌ๋ ๊ณ์ข์์ ๋ด ๊ณ์ข๋ก 100๋ง ์ ๋ณด๋ด์ค."*
- ์๋ ์ธ์ฆ: ๋ธ๋ผ์ฐ์ ๋ ์ํ์ผ๋ก ๋ช ๋ น์ ๋ณด๋ผ ๋, ์ฌ๋ฌ๋ถ์ด ์ด๋ฏธ ๊ฐ์ง๊ณ ์๋ ๋ก๊ทธ์ธ ์ฆํ(์ฟ ํค)๋ฅผ ์๋์ผ๋ก ๊ฐ์ด ๋ณด๋ ๋๋ค.
- ๊ณต๊ฒฉ ์ฑ๊ณต: ์ํ์ ๋ก๊ทธ์ธ๋ ์ฌ์ฉ์์ ์์ฒญ์ด๋ผ ๋ฏฟ๊ณ ๋์ ์ก๊ธํฉ๋๋ค.
๐ก๏ธ 3. ์ด๋ป๊ฒ ๋ง์๊น์? (๋ฐฉ์ด๋ฒ)
๊ฐ๋ฐ์๋ค์ ์ด๋ฅผ ๋ง๊ธฐ ์ํด 'CSRF ํ ํฐ'์ด๋ผ๋ ์ผํ์ฉ ๋น๋ฐ ์ํธ๋ฅผ ์ฌ์ฉํฉ๋๋ค.
- ๊ธฐ์กด ๋ฐฉ์ (์ํ): "์์ ์ด์ฉ๊ถ ๋ฐด๋๋ง ๋ณด์ฌ์ฃผ๋ฉด ํต๊ณผ!"
- ๋ฐฉ์ด ๋ฐฉ์ (์์ ): "์์ ์ด์ฉ๊ถ ๋ฐด๋์ ํจ๊ป, ์ง๊ธ ํ๋ฉด์ ๋ ์๋ ๋น๋ฐ ์ํธ('๋ฐ๋๋')๋ฅผ ๊ฐ์ด ๋งํด์ผ ํต๊ณผ!"
ํด์ปค๋ ์ฌ๋ฌ๋ถ์ ๋ก๊ทธ์ธ ์ํ(๋ฐด๋)๋ ๋ชฐ๋ ์ด์ฉํ ์ ์์ด๋, ์ฌ๋ฌ๋ถ ํ๋ฉด์๋ง ๋ ์๋ ์๊ฐ์ ๋น๋ฐ ์ํธ(ํ ํฐ)๋ ์ ์ ์์ด์ ๊ณต๊ฒฉ์ ์คํจํ๊ฒ ๋ฉ๋๋ค.
โ ์์ฝ
- CSRF๋? ๋ด ๋ก๊ทธ์ธ ๊ถํ์ ๋์ฉํด ํด์ปค๊ฐ ๋ชฐ๋ ๋ช ๋ น์ ๋ด๋ฆฌ๋ ๊ณต๊ฒฉ.
- ์์ธ์? ๋ธ๋ผ์ฐ์ ๊ฐ ๋ก๊ทธ์ธ ์ฆํ(์ฟ ํค)๋ฅผ ์๋์ผ๋ก ์ฒจ๋ถํ๋ ํน์ฑ์ ์ ์ฉํจ.
- ํด๊ฒฐ์ฑ ? CSRF ํ ํฐ ๊ฐ์ ์ถ๊ฐ ์ธ์ฆ ์๋จ์ ์ฌ์ฉํด ๋ฐฉ์ดํจ.
์ด์ ๋ด์ฉ๊ณผ ์ด์ด์ง๋ ์ค์ ์ ์ฉ ํธ์ ๋๋ค. ๊ฐ๋ ์ ์ดํดํ๋ค๋ฉด, ์ค์ ํ๋ก์ ํธ ์ฝ๋์ ๋ฐ๋ก ์ ์ฉํ ์ ์๋๋ก ์ ๋ฆฌํ์ต๋๋ค.
๐ ๏ธ ๊ฐ๋ฐ์๋ฅผ ์ํ CSRF ๋ฐฉ์ด ๊ฐ์ด๋ (React/Next.js)
์์ ์ดํด๋ณธ ๊ฐ๋ ์ ์ค์ ์ฝ๋๋ก ๊ตฌํํ๋ 3๋จ๊ณ ๋ฐฉ์ด ์ ๋ต์ ๋๋ค. **1์ฐจ ๋ฐฉ์ด์ (์ฟ ํค ์ค์ )**๋ง ์ํด๋ ๋๋ถ๋ถ์ ๊ณต๊ฒฉ์ ๋ง์ ์ ์์ผ๋ฉฐ, **2์ฐจ ๋ฐฉ์ด์ (ํ ํฐ)**๊น์ง ๊ตฌ์ถํ๋ฉด ๊ธ์ต๊ถ ์์ค์ ๋ณด์์ ๊ฐ์ถ ์ ์์ต๋๋ค.
๐ช 1. [ํ์] 1์ฐจ ๋ฐฉ์ด์ : SameSite ์ฟ ํค ์ค์
๊ฐ์ฅ ๊ฐ์ฑ๋น๊ฐ ์ข๊ณ ๊ฐ๋ ฅํ ๋ฐฉ๋ฒ์ ๋๋ค. ๋ธ๋ผ์ฐ์ ์๊ฒ **"์ด ์ฟ ํค๋ ์ฐ๋ฆฌ ์ฌ์ดํธ์์๋ง ์จ!"**๋ผ๊ณ ๊ฐ์ ํ๋ ์ต์ ์ ๋๋ค.
Backend ์ค์ ์์ (Node.js / Next.js API)
๋ก๊ทธ์ธ ์ฑ๊ณต ์ ์ฟ ํค๋ฅผ ๋ฐ๊ธํ๋ ์ฝ๋์ sameSite ์ต์
์ ์ถ๊ฐํ์ธ์.
// res.setHeader ๋๋ ์ฟ ํค ์ค์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ ์
res.setHeader('Set-Cookie', serialize('auth_token', token, {
httpOnly: true, // ์๋ฐ์คํฌ๋ฆฝํธ๋ก ํ์ทจ ๋ถ๊ฐ (XSS ๋ฐฉ์ด)
secure: process.env.NODE_ENV === 'production', // HTTPS์์๋ง ์ ์ก
sameSite: 'lax', // โ
ํต์ฌ! (Lax ๋๋ Strict ์ฌ์ฉ)
path: '/',
}));
Strict: ์ค์ง ๊ฐ์ ๋๋ฉ์ธ ๋ด์์๋ง ์ฟ ํค ์ ์ก (๊ฐ์ฅ ์์ ํ์ง๋ง, ์ธ๋ถ ๋งํฌ ํ๊ณ ๋ค์ด์ฌ ๋ ๋ก๊ทธ์ธ ํ๋ฆด ์ ์์)Lax: ์์ ํ ์ด๋(๋งํฌ ํด๋ฆญ ๋ฑ)์ ํ์ฉํ๋, ๊ณต๊ฒฉ ๊ฐ๋ฅ์ฑ์ด ์๋ ์์ฒญ(POST, Form ์ ์ก ๋ฑ)์ ์ฐจ๋จ. (๊ถ์ฅ)
๐ 2. [๊ถ์ฅ] 2์ฐจ ๋ฐฉ์ด์ : CSRF Token (Double Submit Cookie)
์ค์ํ ๋ฐ์ดํฐ ๋ณ๊ฒฝ(๊ณ์ ์์ , ๊ฒฐ์ )์ด ์ผ์ด๋๋ ์๋น์ค๋ผ๋ฉด ํ ํฐ ๊ฒ์ฆ์ ์ถ๊ฐํด์ผ ํฉ๋๋ค.
Frontend ๊ตฌํ ์์ (React + Axios)
๋งค๋ฒ ์๋์ผ๋ก ํ ํฐ์ ๋ฃ์ง ์๊ณ , Axios Interceptor๋ฅผ ์ฌ์ฉํด ๋ชจ๋ ์์ฒญ ํค๋์ ํ ํฐ์ ์๋์ผ๋ก ์ฌ์ด์ค๋๋ค.
import axios from 'axios';
// 1. ์ฟ ํค์์ CSRF ํ ํฐ ๊ฐ ์ถ์ถ ํจ์
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
const api = axios.create({
baseURL: '/api',
withCredentials: true, // โ
์ฟ ํค(ํ ํฐ)๋ฅผ ์ฃผ๊ณ ๋ฐ์ผ๋ ค๋ฉด ํ์ ์ค์
});
// 2. ์์ฒญ ๊ฐ๋ก์ฑ๊ธฐ (Interceptor)
api.interceptors.request.use((config) => {
// ๋ณดํต ์๋ฒ๊ฐ 'XSRF-TOKEN'์ด๋ผ๋ ์ด๋ฆ์ผ๋ก ์ฟ ํค๋ฅผ ๋ด๋ ค์ค๋๋ค.
const csrfToken = getCookie('XSRF-TOKEN');
if (csrfToken) {
// ํค๋์ ํ ํฐ์ ์ค์ด ๋ณด๋
๋๋ค. (๋ฐฑ์๋ ์ค์ ์ ๋ฐ๋ผ ํค ์ด๋ฆ ํ์ธ ํ์)
config.headers['X-XSRF-TOKEN'] = csrfToken;
}
return config;
});
export default api;
๐ 3. [์ต์ ] Next.js ์ฌ์ฉ์๋ผ๋ฉด? (Server Actions)
Next.js App Router๋ฅผ ์ฌ์ฉ ์ค์ด๋ผ๋ฉด ํจ์ฌ ํธํฉ๋๋ค. Server Actions๋ ๊ธฐ๋ณธ์ ์ผ๋ก CSRF ๊ณต๊ฒฉ์ ๋ฐฉ์ดํ๋๋ก ์ค๊ณ๋์ด ์์ต๋๋ค.
Server Action ์์ (์๋ ๋ฐฉ์ด)
๋ณ๋์ ํ ํฐ ์ค์ ์์ด, ํผ ํ๊ทธ์ action์ ์๋ฒ ํจ์๋ฅผ ๋ฐ๋ก ์ฐ๊ฒฐํ๋ฉด ๋ฉ๋๋ค. Next.js๊ฐ ๋ด๋ถ์ ์ผ๋ก ํด์ ID์ ์ถ์ฒ(Origin)๋ฅผ ๊ฒ์ฆํฉ๋๋ค.
// app/actions.ts (Server)
'use server'
export async function updateProfile(formData: FormData) {
// ๐ก๏ธ Next.js๊ฐ ์ด๋ฏธ CSRF ๊ฒ์ฆ์ ๋ง์น ์ํ์
๋๋ค.
const name = formData.get('name');
await db.user.update({ where: { id: 1 }, data: { name } });
}
// app/page.tsx (Client)
'use client'
import { updateProfile } from './actions';
export default function Page() {
return (
<form action={updateProfile}>
<input name="name" placeholder="์ ์ด๋ฆ ์
๋ ฅ" />
<button type="submit">์ ์ฅ</button>
</form>
);
}
๐ ์์ฝ ์ฒดํฌ๋ฆฌ์คํธ
- ๋ฐฑ์๋: ์ฟ ํค ์ต์
์
SameSite: 'Lax'๊ฐ ์ค์ ๋์ด ์๋๊ฐ? - ํ๋ก ํธ์๋:
withCredentials: true์ค์ ์ด ๋์ด ์๋๊ฐ? - ํ๋ก ํธ์๋: (ํ์์) ํค๋์
X-CSRF-TOKEN์ ์ค์ด ๋ณด๋ด๋๊ฐ?
'WEB' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| CommonJS์ ESM์ ์ฐจ์ด์ (1) | 2025.11.15 |
|---|---|
| REST์ GraphQL ๋น๊ต (0) | 2025.11.04 |
| ์ฟ ํค, ์ธ์ (cookie, session)๊ณผ ํ ํฐ (token, JWT)์ ์ฐจ์ด์ (0) | 2022.04.19 |
| REST API๋? (0) | 2019.05.21 |
| REST๋? (0) | 2019.05.16 |