Published on

웹사이트 보안 공격 - XSS 직접 사용해 보기

기술이 계속 발전하는 지금도 이름만 대면 알 수 있을 정도로 유명한 기업을 포함해 많은 기업에서 운영하는 서비스가 사이버 공격에 피해를 입는 사례를 심심치 않게 발견할 수 있다. 이렇듯 큰 규모의 서비스도 공격에 피해를 입는데, 내가 진행했던 무방비 상태에 가까운 웹서비스는 과연 안전할까?

기사 출처


프로젝트를 진행하다 보면 "어떻게 하면 지금보다 더 나은 결과물을 만들 수 있을까?"라는 고민을 항상 하게 된다. 더 좋은 퍼포먼스를 위해 웹 성능 최적화만을 생각해 왔고 집중해 왔다. 그런데 최근 들어서 모던 리액트 Deep Dive를 읽던 중 웹페이지 보안 이슈와 관련된 내용을 읽게 되었고 문득 한 가지 의문이 생겼다.


여러 보안 이슈에 대비해서 위해서 따로 코드를 작성해 대비가 필요한 건가?


그동안 React를 주로 사용하며 보안과 관련된 내용을 특별히 신경 썼던 경험은 없었고 보안 이슈에 대비하는 코드를 따로 작성했다는 내용은 들어봤던 경험도 없었다. 사실 어쩌면 모르고 있었을 수도 있다. 기껏해야 프로젝트에서 .env 파일에서 환경 변수를 설정 정도만 알고 있었고 당연하게도 내 웹사이트가 공격당할 일은 없다는 전제하에 개발을 진행해 왔다. 작은 규모였고 실사용 계획도 없던 점이 주된 이유였던 것 같다. 하지만 이 부분을 계속 과시하게 된다면 나중에 큰 불씨가 될 수도 있기 때문에 이번 기회를 계기로 관련 내용을 공부하며 경각심을 가지고자 한다.

보안 이슈에는 많은 종류의 사이버 공격이 존재한다. 나에게는 정보처리기사 필기시험 5과목을 공부하며 접했던 익숙한 명칭들이 많이 보였다. ( SQL 인젝션, XSS, CSRF 등등 ) 다양한 사이버 공격이 존재하지만, 이번 포스팅에서는 프런트엔드와 연관성이 있는 크로스 사이트 스크립팅(XSS)을 중점적으로 살펴볼 계획이다.



이미지 출처


"그래서 XSS 너 누군데?"

웹 애플리케이션의 보안 취약점을 이용하여 공격자가 악의적인 스크립트를 웹 페이지에 삽입하는 공격을 말한다. 이 스크립트는 다른 사용자의 브라우저에서 실행되며, 공격자는 이를 통해 사용자의 세션을 탈취하거나 특정 행위를 사용자를 대신해 수행하게 만들 수 있다. 웹 애플리케이션이 사용자 입력을 적절히 검증하거나 인코딩하지 않을 때 주로 발생하고 공격자는 이러한 취약점을 이용해 웹 페이지에 JavaScript 코드나 HTML을 삽입하여 브라우저에서 실행시킬 수 있게 된다.


XSS 세 가지 범주


  • 저장된 XSS 공격

    삽입된 스크립트를 대상 서버에 영구적으로 저장, 그런 다음 피해자는 브라우저가 데이터 요청을 보낼 때 서버에서 이 악성 스크립트를 검색하게 된다.

  • 반사된 XSS 공격

    사용자가 공격자에게 속아 악성 링크나 특수 제작된 양식을 제출해 악성 사이트에 방문하게 하고 사용자의 입력(악성코드 포함)을 웹페이지에 반영하여 사용자에게 다시 응답을 보낸다. 사용자의 브라우저는 응답을 받고, 이 응답이 신뢰할 수 있는 서버로부터 온 것으로 판단하고 페이지에 포함된 악성 스크립트를 실행한다. 이 스크립트는 사용자의 세션 정보 탈취, 피싱 사이트로 리다이렉트, 또는 다른 악의적인 행동을 수행할 수 있다.

  • DOM기반 XSS 공격

    페이로드는 원본 클라이언트 측 스크립트가 사용하는 DOM 환경(피해자의 브라우저에서)을 수정한 결로 실행한다. 즉, 페이지 자체는 변경되지 않지만 DOM 환경에 대한 악의적인 수정으로 인해 페이지에 포함된 클라이언트 측 코드가 예기치 않은 방식으로 실행된다.



처음 정의된 내용을 읽었을 때 위험성은 인지했지만, 많은 궁금증이 생겼다. "세션 탈취? 악의적인 리다이렉트? 어떤 방식으로?", "스크립트를 삽입하면 어떤 피해가 생길 수 있을까? 어떤 형태로 공격하는 거지?" 궁금증을 해결하기 위해 직접 나의 웹페이지에 XSS 공격을 진행해 보기로 결정했다.





📌 실습을 위한 사전 지식

dangerouslySetInnerHTML

역할

  • 이스케이프 처리를 하지 않고 DOM에 문자열을 HTML로 해석하여 그대로 삽입 (React와 대조됨)
  • 주로 특정 상황에서 서버에서 받은 HTML을 웹 페이지에 그대로 렌더링 할 필요가 있을 때 사용

<img src="empty" onerror="console.log(window.localStorage);" />

dangerouslySetInnerHTML을 사용해 DOM에 직접 삽입하면 위와 같은 형태가 된다.



이름부터 본인이 위험하다는 것을 어필하고 있다. 만약 위험한 문자열 형태의 코드가 이스케이프 처리되어 DOM에 삽입된다면? 예측할 수 없는 위험을 초래하게 될 수 있게 된다. 따라서 dangerouslySetInnerHTML은 사용을 최대한 지양하는 것이 좋다. 필요한 요구사항이 생긴다면 넘겨줄 문자열 값을 검증하는 등 주의를 기울일 필요가 있다.

그리고 위험성을 가지는 dangerouslySetInnerHTML의 특성을 이용해 XSS 공격에 적합한 위험에 노출된 환경을 만들고 시나리오를 설정해 실습을 진행할 계획이다. ( 뒤에 언급하겠지만 React는 기본적으로 모든 문자열을 이스케이프 처리하기 때문에 실습에 제약이 있다. )



👨🏻‍💻 직접 XSS 공격해 보기


기본 세팅 코드

import { useState } from 'react';

import * as S from './styles/SignInForm.style';

const SignIn = ({ handleSubmit, handleChange }) => {
  const [input, setInput] = useState('');

  return (
    <S.Form onSubmit={handleSubmit}>
      <div dangerouslySetInnerHTML={{ __html: input }} />  ⭐️
      <S.Label >
        ID
        <S.Input
          type="text"
          name="email"
          onChange={handleChange}
          required
        />
      </S.Label>
      <S.Label>
        PW
        <S.Input
          type="text"
          name="password"
          value={input}
          onChange={e => setInput(e.target.value)}
          required
        />
      </S.Label>
      <S.Button aria-label='로그인'>로그인</S.Button>
    </S.Form>
  );
};

export default SignIn

공격 시나리오 1 - localStorage 훔쳐보기


  1. input 필드에 공격할 문자열을 입력하면 유효하지 않은 src로 인해 onerror 속성이 동작 된다.
  2. onerror 속성에 공격하고 싶은 스크립트 DOM을 삽입한다.
<img src="empty" onerror="console.log(window.localStorage);" />


결과

console log를 확인해 보면, 손쉽게 localStorage에 저장되어 있는 정보들을 확인할 수 있다. 비슷한 방법으로 쿠키, 세션에도 접근이 가능하기 때문에 공격자가 탈취 후에 악용할 가능성이 다분하다.


console이 아닌 alert도 사용해 잘 동작한다.

<img src="empty" onerror="alert('절대 보안');" />



공격 시나리오 2 - 리다이렉트 및 수상한 파일 다운로드 유도


  1. 비밀번호 input에 필드에 공격할 문자열을 입력하면 유효하지 않은 src로 인해 onerror 속성이 동작 된다.
  2. onerror 속성에 의해 공격자가 미리 만들어둔 페이지로 리다이렉트 되고 공격당하게 된다.
<img src='x' onerror='window.location="http://localhost:3000/dangerous"'>

결과

비밀번호를 입력하던 중 잘못된 href로 리다이렉트 시키는 <a> 태그로 발생할 수 있는 상황을 가정해 실습을 진행해 보았다. onChange, onClick 등 다양한 이벤트와 XSS를 연계하여 공격자가 스크립트를 삽입한다면 사용자들이 속수무책으로 당할 것 같다.




그렇다면 React는 XSS로부터 어떻게 안전할까?


dangerouslySetInnerHTML을 사용한 특수한 상황일 뿐, React에서 input 필드에 공격할 문자열(<img src='x' onerror='console.log('공격')'>) 입력한다고 위 상황처럼 DOM에 직접적으로 삽입되지는 않는다. 왜냐하면 React는 JSX에서 중괄호 안에 포함된 변수나 표현식의 값에 대해 자동으로 이스케이프 처리를 하기 때문이다.


React 공식 문서 JSX 파트에서도 관련 내용을 명시하고 있다. 🧐


앞서 dangerouslySetInnerHTML을 시용해 <img src="empty" onerror="alert('절대 보안');" /> 문자열을 삽입했을 때 이스케이프 처리가 되지 않고 DOM 내에 직접 삽입되어 onerror 속성이 동작했다.




문자열을 이스케이프 처리하는 JSX에서 동일한 공격 문자열을 삽입할 경우


예시 코드

const html = '<img src="empty" onerror="console.log(window.localStorage);" />'

return(
  <div id={html}>{html}</div>
  '''
)

결과


React에서 JSX를 사용하여 컴포넌트를 렌더링 할 때, 중괄호 안에(id 속성) 포함된 변수나 표현식 안에 포함된 자동으로 HTML에서 특별한 의미를 가지는 "(큰따옴표)를 엔티티(&quot;)로 이스케이프 처리했다.


이스케이프 처리 전이스케이프 처리 후
"&quot;

즉, textContent와 HTML 속성값에 대해서는 React가 기본적으로 이스케이프 작업을 수행함으로써 XSS 공격으로부터 앱을 보호하는 보안 조치를 취하고 있던 것이다.





마치며

지금까지 보안에 대한 위협을 어느 정도 배제하고 등한시하고 있었다. 그렇기 때문에 XSS, CSRF 등 다양한 위협이 존재하지만 나에게는 크게 와닿지 않았었던 것 같다. 특히 그동안 React JSX 문법이 JavaScript 내에서 UI 구조를 HTML과 유사하게 표현할 수 있도록만 하는 줄 알고 있었는데, 이스케이프 처리라는 다른 역할도 수행하고 있다는 것을 알게 되었다. 예전에 JSX 공식 문서를 읽었을 때는 대수롭지 않게 여기고 넘어갔기 때문에 문서를 다시 살펴봤을 때 새로운 내용이 추가된 것처럼 느껴졌다.

지금까지 innerHTML, dangerouslySetInnerHTML을 React에서 적용해야 할 요구사항이 없었기 때문에 발생할 수 있는 위험성을 인지하지 못하고 있었다. 만약 이스케이프 처리 없이 DOM에 직접 삽입이 필요한 요구사항이 생겼다면, 별생각 없이 사용했었을 수도 있다. 이번 기회로 웹사이트에서 발생할 수 있는 위험성과 React에서 이를 어떻게 대비하는지에 대한 견문을 넓힐 수 있었다. 그리고 그동안 등한시했던 보안에 대한 경각심을 가지게 되는 좋은 계기가 되었다.





Reference