XSS를 아시나요(feat. innerHTML의 위험성)

Lybell·2024년 4월 4일
0

개요

const inputForm = document.getElementById("input");
const inputButton = document.getElementById("button");
inputButton.addEventListener( "click", ()=>{
  const newElement = document.createElement("div");
  newElement.className="myComponent";
  newElement.innerHTML = `
	<p>Name : ${inputForm.value}</p>
	<div class="delButton">삭제하기</div>
  `
  document.body.appendChild(newElement);
})

신입 개발자 A는 버튼을 누르면 input 값의 내용을 가져와 새로운 컴포넌트를 추가하는 코드를 만들고 있었다. 그리고 위와 같은 코드로 작성하였다. 여기서 문제, 이 코드의 문제점은 무엇인가?
.
.
.
.
바로 크로스 사이트 스크립팅(Cross Site Scripting, XSS) 공격에 취약하다는 것이다. 그렇다면, 크로스 사이트 스크립팅이란 무엇이고, 이 코드가 왜 XSS 공격에 취약한 것인가?

크로스 사이트 스크립팅이란?

크로스 사이트 스크립팅은 개발자가 아닌 외부인이 사이트에 스크립트를 주입할 수 있는 보안상의 이슈로, 주로 검증되지 않은 사용자 문자열을 충분한 유효성 검사나 필터링 없이 그대로 이용할 때 발생한다.

사용자의 브라우저는 악의적인 스크립트가 신뢰할 수 없는지를 감지할 수 없으므로 쿠키, 세션 토큰 또는 다른 중요한 사이트별 정보에 대한 접근 권한을 가져가거나 악의적인 스크립트로 HTML 내용을 다시 쓸 수 있다.

참고로, XSS와 비슷하게 검증되지 않은 사용자 문자열로 보안 이슈가 발생하는 SQL 인젝션이라는 것도 있다. 이것은 사용자 입력을 그대로 SQL 쿼리에 넣으면 생기는 문제.

innerHTML의 위험성

위의 코드를 보자. inputForm 안의 내용을 아무런 필터링 없이, innerHTML 내에 사용하고 있는데, innerHTML은 문자열을 html 코드로 전환하는 getter인 만큼, innerHTML에 사용자가 html 코드를 집어넣으면 그 코드가 html 코드로 그대로 전환된다!

사용자가 <p>클릭하면 신기한일 일어남</p> 을 input form에 입력하고, 버튼을 클릭한다고 가정해보자.
inputButton의 클릭 이벤트로 발동된 함수는 newElement.innerHTML을 다음과 같이 해석하게 된다.

<p>Name : <p onclick="alert('Its over 9000!')">클릭하면 신기한일 일어남</p></p>
<div class="delButton">삭제하기</div>

그리고 이게 그대로 html로 해석되면서, "클릭하면 신기한 일 일어남"이라는 문자열을 클릭하면 스크립트가 실행되는 코드가 탄생하게 되는 것이다.
예시는 alert를 이용했지만, alert가 아니라 오만 가지 함수와 코드를 실행할 수 있기에 보안에 취약하다는 잠재적인 문제가 될 수 있다.

스크립트 즉시 실행

그렇다면 당신은 이렇게 생각할 수도 있다. <script></script>를 주입시키면 스크립트가 즉시 실행되는 것이 아닌가? 물론, 이론상으로는 그럴 수 있겠지만 우리의 브라우저는 여러분들 생각보다 똑똑하기 때문에 중간에 인라인 스크립트가 생성되더라도 아무런 행동을 하지 않는다.

그렇다고, 스크립트를 즉시 실행하는 방법이 없는 것은 아니다. 바로 img 태그의 onerror 이벤트를 이용하는 것이다. error 이벤트는 이미지가 로드 실패될 때 호출되며, img 태그가 src에 있는 이미지를 바로 로드 시도하는 것을 이용하면, <img src="">라는 코드로 다음과 같은 일을 일으킬 수 있다.

  • img 태그가 html에 주입되며, src 속성에 저장된 주소로 이미지 로드를 시도한다.
  • src가 빈 문자열이기 때문에, 이미지 로드는 실패하고, error 이벤트가 생성된다.
  • onerror에 설정된 스크립트가 실행되며, 결과적으로 사용자가 아무것도 하지 않아도 스크립트가 자동으로 실행되는 공격이 수행된다.

저장된 XSS 공격

위의 코드는 XSS 공격의 클라이언트 전용 사례를 제시했지만, 서버와 결합하면 더욱 치명적인 결과가 일어날 수 있다. 사용자가 신뢰하지 않는 html 코드를 그대로 서버에 제출하고, 서버가 해당 문자열을 db에 그대로 저장하는 상황을 생각해 보자.

그리고 웹 사이트가 db에 있는 문자열을 서버에 렌더링할 때, 문자열을 그대로 innerHTML을 이용해 렌더링한다면, 모든 사용자에게 오염된 html을 그대로 배포하게 된다! 이렇게 서버에 스크립트 주입 공격을 db에 저장하고, 신뢰하지 않은 문자열을 그대로 렌더링하는 것을 저장된 XSS 공격이라고 한다.

이 경우에는 프론트엔드 측과 백엔드 측 모두 신뢰하지 않는 문자열을 필터링해서 교차 검증을 하는 것이 중요하다.

대처 방법

필터링을 이용하는 방법이 있다. 사용자의 입력값에 포함된 <>과 같은 html 태그를 &lt;, &gt;로 변경시켜서 사용자의 html 주입 시도를 차단하는 방법이 있다.

innerHTML을 사용할 경우, 검증된 문자열만 템플릿 리터럴에 집어넣거나, 아예 집어넣지 않고, 부득이하게 사용자 텍스트를 컴포넌트 내에 넣을 경우 빈 문자열로 태그를 선언한 뒤, querySelector와 innerText(또는 textContent)를 조합하여 사용자 문자열을 입력하는 방법이 있다.

inputButton.addEventListener( "click", ()=>{
  const newElement = document.createElement("div");
  newElement.className="myComponent";
  newElement.innerHTML = `
	<p></p>
	<div class="delButton">삭제하기</div>
  `
  newElement.querySelector("p").innerText = inputForm.value;
  document.body.appendChild(newElement);
});

innerText로 들어온 문자열은 html 태그에 사용되는 특수 문자도 순수 문자로 판별하여, 사용자 입력을 그대로 출력한다는 특징이 있다.

참고로, innerText와 textContent의 차이는 setter에서는 별 차이가 없고, getter에서 css나 공백 제거 등으로 숨겨진 문자열을 보여주느냐(textContent), 보여주지 않느냐(innerText)의 차이다. textContent가 css 등을 고려하지 않아 리플로우가 일어나지 않아 대체적으로 성능이 좋은 편이다.

그럼, innerHTML은 죄악인가요?

innerHTML이 XSS 공격에 취약한 것은 맞지만, 완전히 사용되어서는 안 되는 것은 아니다. 신뢰할 수 있는 문자열만 innerHTML에 들어간다면, innerHTML은 개발자의 수고를 덜어줄 수 있는 좋은 무기가 된다. 예시를 통해 알아보자.

const newElem = document.createElement("div");
newElem.innerHTML = `
<h3>타이머</h3>
<div class="timerBody">
<span class="hour">59</span>:<span class="minute">59</span>:<span class="second">59</span>
</div>
<div class="timerButton>시작하기</div>
`

이 코드의 경우, innerHTML에 개발자가 검증된 문자열만 들어갔기 때문에, XSS 공격이 일어날 여지가 없다. 바닐라 자바스크립트로 innerHTML을 이용하지 않고 비슷한 코드를 짜려면 createElement를 도배해야 해서 코드의 가독성이 떨어지는 문제가 일어날 수 있다.

참고 자료

https://developer.mozilla.org/ko/docs/Glossary/Cross-site_scripting

https://developer.mozilla.org/ko/docs/Web/Security/Types_of_attacks#cross-site_scripting_xss

profile
홍익인간이 되고 싶은 꿈꾸는 방랑자

0개의 댓글