Skip to main content

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 엔티티로 변환해

브라우저가 스크립트로 해석하지 못하게 만든다.

예:

  • <&lt;
  • >&gt;
  • "&quot;
  • '&#39;

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 공격을 막을 수 있는 가장 효과적인 방법은 토큰을 사용하는 방법입니다.

  • 세션 별로 다른 토큰을 발급한다. → 피싱 사이트를 이용해 보낸 요청에도 다른 토큰이 사용될 것이므로 공격을 막을 수 있다.
  • 토큰은 왜 괜찮을까요?
    • 토큰은 로그인 했을 때 1회 발급
    • 이후 html 태그에 넣어서 요청 보냄
    • 같은 브라우저라도 evil.combank.com의 HTML/JS/DOM에 접근할 수 없습니다.
    • SOP(Same-Origin Policy) 규칙으로 origin이 다르기 때문에 공격자가 사용자의 토큰을 읽을 수 없습니다.

2. Double Submit 쿠키 사용

Double Submit 쿠키를 사용하여 방어 할 수도 있습니다.

이 방식은 서버에 토큰을 유지하지 않고 브라우저의 쿠키에 유지하는 방법입니다.

  • 로그인 할 때
    • 세션용 쿠키
    • HttpOnly 속성이 부여되지 않은 토큰 값을 저장하고 잇는 쿠키도 함께 발행합니다.
  • 브라우저는 서버에 요청 시
    • 쿠키 내부의 토큰을 요청 폼의 헤더와 바디에 삽입하여 쿠키와 동시에 서버로 전송하고 서버는 폼 데이터 속 토큰과 쿠키 속 토큰의 일치 여부를 확인하여 정상 요청 여부를 판단합니다.

3. SameSite 쿠키 사용

  • SameSite 쿠키는 쿠키의 전송을 제어하는 기능으로 쿠키 전송을 동일한 사이트로 제한할 수 있습니다.

Set-Cookie session=013456abc; HttpOnly; Secure; SameSite=Lax;

설정 가능한 값의미
Strict교차 사이트에서 전송하는 요청에는 쿠키를 추가하지 않음
LaxURL이 바뀌는 화면 전환 및 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,
},
});

이 요청은 다음 흐름으로 동작함:

  1. 브라우저: “OPTIONS 요청” 보내 허용 여부 확인
  2. 서버: 허용된 Origin인지 응답
  3. 허용된 경우에만 실제 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 헤더 검사보다 설정이 더 복잡