XSS와 CSRF
XSS(Cross-Site-Scripting)
XSS는 웹 페이지에 악성 스크립트를 삽입해, 사용자의 브라우저에서 임의의 JavaScript가 실행되도록 만드는 공격이다.
XSS 공격의 대표 유형
1) Stored XSS
악성 스크립트가 DB에 저장됨 → 페이지 조회 시 마다 실행
예: 악성 댓글 저장 → 무한 반복 실행 (누구든 페이지 열면 자동으로 실행)
2) Reflected XSS
URL 파라미터 등 입력값이 즉시 응답 HTML에 반영되어 실행
예: ?q=<script>alert(1)</script>
3) DOM-based XSS
서버가 아닌 프론트 JS 코드가 취약할 때 발생
예: innerHTML 사용 시
XSS에 대비하기 위한 방법
1) 출력 시 Escape 처리 (가장 기본적이고 중요한 방어)
사용자 입력을 HTML에 출력할 때 HTML 엔티티로 변환해
브라우저가 스크립트로 해석하지 못하게 만든다.
예:
<→<>→>"→"'→'
React도 기본적으로 escape하지만 dangerouslySetInnerHTML 사용 시 위험.
2) CSP(Content Security Policy) 적용
브라우저에게 “어떤 리소스를 허용할지” 명시적으로 알려 XSS 피해를 크게 줄일 수 있다.
서버 응답으로 옵니다.
예:
Content-Security-Policy: default-src 'self'; script-src 'self';
→ default-src 'self’ 모든 리소스는 현재 도메인에서 불러온 것만 허용
→ script-src 'self’스크립트(js)는 무조건 현재 도메인에서 온 것만 실행
효과:
- 외부 스크립트 로드 차단
- inline 스크립트 실행 차단
- 예상치 못한 script injection 예방
3) HttpOnly 쿠키 사용
XSS가 발생해도 JavaScript에서 쿠키를 읽을 수 없도록 막는다.
document.cookie→ 막음
Set-Cookie: session=abc123; HttpOnly; Secure
완전한 방어는 아니지만, 세션 탈취 위험을 크게 낮춤.
5) innerHTML 사용 금지
DOM 기반 XSS의 대표적 원인.
가능한 아래 패턴을 사용한다.
위험한 코드
element.innerHTML = userInput;
안전한 코드
element.textContent = userInput;
CSRF 공격이란?
피해자 모르게 실행되는 위험한 요청
CSRF는 인증된 사용자의 권한을 도용하여 원하지 않는 작업을 수행하게 만드는 공격입니다.
CSRF는 공격자 사이트가 사용자의 브라우저를 이용해 은행에 요청을 보내고, 브라우저가 그 요청에 세션 쿠키를 자동으로 붙여버려 발생하는 공격이다.
이러한 피싱 사이트에서 사용할 수 있는 코드의 예시는 다음과 같습니다.
<form action="https://bank.com/change-password" method="POST" id="f">
<input type="hidden" name="newPassword" value="hacked1234" />
</form>
<script>
document.getElementById('f').submit();
</script>
이러한 form tag를 이용하여 요청되는 정보는 다음과 같습니다.
POST /change-password HTTP/1.1
Host: bank.com
Cookie: session=0112345abcd
Origin: https://evil.example/
Referer: https://evil.example/
newPassword=hacked1234
이러한 form 태그를 이용한 요청은 CORS 정책의 제약을 받지 않기 때문에, 공격자 사이트에서도 bank.com으로 그대로 전송될 수 있습니다.
이때 브라우저는 bank.com 도메인에 저장된 세션 쿠키를 자동으로 함께 전송합니다.
CSRF에 대비하기 위한 방법
1. 토큰 사용
CSRF 공격을 막을 수 있는 가장 효과적인 방법은 토큰을 사용하는 방법입니다.
- 세션 별로 다른 토큰을 발급한다. → 피싱 사이트를 이용해 보낸 요청에도 다른 토큰이 사용될 것이므로 공격을 막을 수 있다.
- 토큰은 왜 괜찮을까요?
2. Double Submit 쿠키 사용
Double Submit 쿠키를 사용하여 방어 할 수도 있습니다.
이 방식은 서버에 토큰을 유지하지 않고 브라우저의 쿠키에 유지하는 방법입니다.
- 로그인 할 때
- 세션용 쿠키
- HttpOnly 속성이 부여되지 않은 토큰 값을 저장하고 잇는 쿠키도 함께 발행합니다.
- 브라우저는 서버에 요청 시
- 쿠키 내부의 토큰을 요청 폼의 헤더와 바디에 삽입하여 쿠키와 동시에 서버로 전송하고 서버는 폼 데이터 속 토큰과 쿠키 속 토큰의 일치 여부를 확인하여 정상 요청 여부를 판단합니다.
3. SameSite 쿠키 사용
- SameSite 쿠키는 쿠키의 전송을 제어하는 기능으로 쿠키 전송을 동일한 사이트로 제한할 수 있습니다.
- 동일한 사이트란?
- some.example.com , any.example.com 와 같이 eTLD+1(effective Top-Level Domain + 1)이 같은지 확인합니다.
- 동일한 사이트란?
Set-Cookie session=013456abc; HttpOnly; Secure; SameSite=Lax;
| 설정 가능한 값 | 의미 |
|---|---|
| Strict | 교차 사이트에서 전송하는 요청에는 쿠키를 추가하지 않음 |
| Lax | URL이 바뀌는 화면 전환 및 GET 메서드를 사용한 요청이면 교차 사이트에도 쿠키를 전송함. 다른 방법을 사용하는 교차 사이트의 요청은 쿠키를 추가하지 않음 |
| None | 사이트에 관계없이 모든 요청에 쿠키를 전송함 |
4. Origin 헤더 기반 방어
브라우저는 POST/PUT/DELETE 같은 민감한 메서드를 보낼 때 자동으로 Origin 헤더를 붙인다.
Origin 헤더 예:
Origin: https://site.example
서버는 이 헤더를 보고 요청이 믿을 수 있는 사이트에서 왔는지 확인할 수 있다.
왜 필요한가?
HTML을 렌더링하는 서버와 API 서버가 서로 다른 도메인을 사용할 때,
HTML에 CSRF Token을 넣어도 API 서버는 그것이 정상 요청인지 판단할 수 없다.
→ 이때 Origin 검사가 유용한 보조 방어 수단이 된다.
서버 예시 코드
app.post("/remit", (req, res) => {
// Origin 헤더가 없거나, 허용된 Origin이 아닐 때 차단
if (!req.headers.origin || req.headers.origin !== "https://site.example") {
res.status(403);
return res.send("허가되지 않은 요청입니다.");
}
// 정상 요청 처리
...
});
장점
- 구현 매우 간단
- Token 저장 없이 출처 기반으로 공격 차단 가능
- 서버 분리 환경(SPA + API)에서 유용
단점
- 일부 요청(레거시 환경, 특정 브라우저)에서 Origin 헤더가 누락될 수 있음
- 완전한 방어(예: XSS 존재 시)까진 어려움
5. CORS 기반 CSRF 방어
CORS(Cross-Origin Resource Sharing)는 특정 Origin에서 오는 요청만 API 서버가 허용하도록 제어하는 방식.
Origin 검사와 유사하지만, 더 정교한 제어가 가능하다.
Preflight 요청이 핵심
민감한 요청(특정 헤더 포함, JSON POST 등)이 들어올 경우 브라우저는 먼저 OPTIONS 요청(Preflight Request) 을 보내 서버가 허용하는지 묻는다.
예: 클라이언트 요청
fetch('https://bank.hyundai.com/remit', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequst', // 의도적으로 Preflight 유발
},
credentials: 'include',
body: {
to: 'attacker',
amount: 1000000,
},
});
이 요청은 다음 흐름으로 동작함:
- 브라우저: “OPTIONS 요청” 보내 허용 여부 확인
- 서버: 허용된 Origin인지 응답
- 허용된 경우에만 실제 POST 요청 수행
서버는 이렇게 판단
// OPTIONS Preflight 검사
app.options('/remit', (req, res) => {
const origin = req.headers.origin;
if (origin !== 'https://site.example') {
return res.status(403).send('CORS 정책 위반');
}
res.setHeader('Access-Control-Allow-Origin', 'https://site.example');
res.setHeader(
'Access-Control-Allow-Headers',
'X-Requested-With, Content-Type'
);
res.status(200).send();
});
장점
- 매우 강력한 출처 기반 접근 제어
- SPA 환경에서 API 보호에 적합
단점
- Preflight가 많아지면 성능 저하 가능
- Origin 헤더 검사보다 설정이 더 복잡