반응형

현재 진행 중인 프로젝트에서 Sparrow를 사용해 웹 취약점 진단을 실시했고, innerHTML을 사용한 코드들이 다량 발견되어 수정 조치했다.

 

단순히 코드를 고치는 것에서 더 나아가 같은 실수를 반복하지 않기 위해 왜 고쳐야 했는지를 기록해보도록 하겠다.

 

1.innerHTML

1.1 innerHTML 이란

MDN에서는 innerHTML을 이렇게 정의한다.

Element.innerHTML

Element속성(property) innerHTML은 요소(element) 내에 포함된 HTML 또는 XML 마크업을 가져오거나 설정합니다.

 

잘 와닿지 않는다. 예시로 만든 코드를 보도록 하자.

텍스트를 이용해 동적으로 테이블을 생성하는 코드이다.

 

1.2 innerHTML 예시 코드 (텍스트를 사용해 동적 테이블 생성)


      
<!DOCTYPE html>
<html lang="en">
<style>
table {width: 100%;border-collapse: collapse;margin-top: 20px;}
th, td {border: 1px solid #333;padding: 8px 12px;text-align: left;}
th {background-color: #f4f4f4;}
</style>
<script>
document.addEventListener("DOMContentLoaded", function(){
let tableDiv = document.getElementById("tableDiv");
const data = [
{ name : "이름1", age : "나이1"},
{ name : "이름2", age : "나이2"},
{ name : "이름3", age : "나이3"}
]
let tableTag = "<table>";
tableTag += "<thead><tr>";
for (let key in data[0]) {
tableTag += `<th>${key}</th>`;
}
tableTag += "</tr></thead>";
tableTag += "<tbody>";
data.forEach(row => {
tableTag += "<tr>";
for (let key in row) {
tableTag += `<td>${row[key]}</td>`;
}
tableTag += "</tr>";
});
tableTag += "</tbody>";
tableTag += "</table>";
tableDiv.innerHTML = tableTag;
})
</script>
<body>
<div id="tableDiv"></div>
</body>
</html>

 

코드를 실행하면 다음과 같은 화면을 확인할 수 있다.

html을 사용하지 않고, 자바스크립트로 테이블을 만들어냈다.

html은 정적이지만, innerHTML을 사용하면 동적으로 html 요소를 컨트롤할 수 있는 것이다!

innerHTML을 사용해 동적 생성한 테이블

innerHTML을 사용해서 테이블을 간단하게 삭제할 수도 있다.


      
// table 삭제
tableDiv.innerHTML = '';

 

innerHTML을 사용하는 것은 주로 DB에서 값을 조회해 화면에 뿌려줄 때 였던것 같다.

상황마다 불러오는 데이터의 개수와 값이 다르기 때문에 미리 그려놓을 수 없으므로 이렇게 동적으로 생성하게 되는 것이다.

 

그렇다면 innerHTML이 아닌 innerText를 사용하면 어떻게 될까?

innerText를 사용했을 때 화면

문자열 속 태그를 태그로 인식하지 않고 그대로 문자열로 인식해 화면에 뿌려주는 것을 볼 수 있다.

 

innerHTML은 어떻게 문자열을 태그로 인식하는 것일까?

 

그것은 브라우저의 렌더링 엔진(Rendering Engine) 때문이다.

렌더링 엔진은 문자열을 HTML 파서(HTML Parser)를 통해 파싱하게 되는데, 이때 HTML 파서는 문자열을 읽고, 태그를 인식하여 DOM 트리를 구축하거나 업데이트하게 된다.

 

문자열은 인간들에게 가독성이 매우 높은 직관적인 언어이므로 사용하기 편하다.

하지만 편한 만큼 위험성이 따르는데, 그게 XSS(Cross-Site Scripting)이다.

 

2.XSS(Cross-Site Scripting)

2.1 XSS란?

XSS는 웹 애플리케이션에서 발생할 수 있는 보안 취약점 중 하나로, 공격자가 악의적인 스크립트 코드를 웹 페이지에 삽입하여 다른 사용자에게 실행되도록 하는 공격 기법이다.

 

2.2 XSS 공격 예시(실패)

아까 만든 예제를 통해 알아보자.

아까 만든 예제의 데이터가 회원가입 과정을 통해 데이터를 입력받은 후, 입력 받은 데이터를 테이블로 만들어 주는 코드였다고 가정해 보자.

 

사용자가 이름과 나이에 각각 우리가 원하는 데이터만 잘 넣어주었다면 좋겠지만 모든 사람이 그렇게 정직하지는 않은 법,

어떤 사용자가 나이를 입력해야 할 공간에 스크립트를 입력했다.

 

이름 : 비밀

나이 : <script>alert('XSS 공격이다!');</script>

 

이런 값이 들어가는 것을 허용했다는 것부터 잘못이지만, 만약 적절한 조치를 취하지 못해 이런 문제 데이터가 입력되었을 때 innerHTML을 사용했다면 어떤 결과가 나타날까?


      
<!DOCTYPE html>
<html lang="en">
<head>
<style>
table {width: 100%;border-collapse: collapse;margin-top: 20px;}
th, td {border: 1px solid #333;padding: 8px 12px;text-align: left;}
th {background-color: #f4f4f4;}
</style>
<script>
document.addEventListener("DOMContentLoaded", function(){
let tableDiv = document.getElementById("tableDiv");
const data = [
{ name : "이름1", age : "나이1"},
{ name : "이름2", age : "나이2"},
{ name : "비밀", age : "<script>alert('XSS 공격이다!');<\/script>"}
]
let tableTag = "<table>";
tableTag += "<thead><tr>";
for (let key in data[0]) {
tableTag += `<th>${key}</th>`;
}
tableTag += "</tr></thead>";
tableTag += "<tbody>";
data.forEach(row => {
tableTag += "<tr>";
for (let key in row) {
tableTag += `<td>${row[key]}</td>`;
}
tableTag += "</tr>";
});
tableTag += "</tbody>";
tableTag += "</table>";
tableDiv.innerHTML = tableTag;
})
</script>
</head>
<body>
<div id="tableDiv"></div>
</body>
</html>

아무 일도 일어나지 않았다. 입력한 alert가 실행되기를 바랐는데 이상하다.

이 정도의 허접한 공격은 통하지 않는 것 같다.

브라우저의 보안 정책으로 인해 <script> 태그를 실행하지 않는다는 것을 알아냈다.

2.3 XSS 공격 예시 2


      
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>XSS 테스트</title>
<style>
table {width: 100%; border-collapse: collapse; margin-top: 20px;}
th, td {border: 1px solid #333; padding: 8px 12px; text-align: left;}
th {background-color: #f4f4f4;}
</style>
<script>
document.addEventListener("DOMContentLoaded", function(){
const input = document.getElementById("userInput");
const submitBtn = document.getElementById("submitBtn");
const displayDiv = document.getElementById("displayDiv");
submitBtn.addEventListener("click", function(){
const userText = input.value;
displayDiv.innerHTML = userText;
});
});
</script>
</head>
<body>
<h1>XSS 테스트</h1>
<input type="text" id="userInput" placeholder="텍스트를 입력하세요">
<button id="submitBtn">삽입</button>
<div id="displayDiv"></div>
</body>
</html>

 

인풋박스에 사용자가 텍스트를 입력할 수 있는 예제 코드이다.

여기에 사용자가 태그를 포함한 텍스트를 입력했을 때 어떻게 되는지 알아보자.

 

다음과 같은 텍스트를 입력했다.

<div style="width:100%; height:100%; color:yellow; background-color: red; "> 이것은 텍스트입니다. </div>

 

입력한 태그가 텍스트가 아닌 HTML태그로 파싱 되어 출력된 것을 확인할 수 있다.

사용자가 개발자의 구현의도와는 다른 용도로 코드를 사용했다.

용도 외에 다른 결과를 얻어낼 수 있으므로 이것은 큰 문제라고 볼 수 있다.

 

2.4 XSS 공격의 피해

XSS가 가져올 수 있는 피해는 다음과 같다.

1) 세션 하이재킹

2) 피싱

3) 웹사이트 변조

4) 악성 소프트웨어 배포

5) 기타 악의적인 행위(광고 클릭, 자동 구매, 데이터 조작)

 

3. 해결방법(textContent 사용) 

여러 가지 방법을 찾을 수 있었다.

사용자가 입력한 값에 대해 유효성 검사를 실행하거나, HTML 특수 문자를 이스케이프 처리하거나, DOMPurify와 같은 보안 라이브러리를 사용하거나 아예 innerHTML을 사용하지 않으면 된다.

 

나는 innerHTML의 사용에 대한 지적을 받았기 때문에, innerHTML을 코드에서 제거하지 않으면 안 되는 상황이었다.

 

그래서 innerHTML 대신에 textContent를 사용하는 것으로 문제를 해결했다.

위 예제 코드에서 innerHTMLtextContent로 바꿔서 코드를 재실행해보겠다.

텍스트에 포함된 특수문자가 HTML 태그로 파싱 되지 않고 그대로 문자열로 출력되는 것을 확인할 수 있었다.

 

4. innerHTML과 textContent의 비교

특징 innerHTML textContent
HTML 해석 제공된 문자열을 HTML로 파싱하여 요소의 자식으로 삽입 제공된 문자열을 텍스트로 삽입하며, HTML 태그를 해석하지 않음
보안 악의적인 스크립트가 삽입될 가능성이 있으며, XSS 공격에 취약 스크립트나 HTML 태그가 텍스트로 처리되며XSS 공격에 안전함
성능 복잡한 HTML을 삽입할 때는 오버헤드 발생 가능 있음 빠름
용도 동적으로 HTML 구조 변경, 외부 HTML 콘텐츠 삽입 단순 텍스트 표시
대상 요소의 영향 HTML 구조 자체를 변경할 수 있으므로, CSS 스타일이나 다른 요소에도 영향을 미침 텍스트만 변경하므로, 요소의 다른 속성이나 구조에는 영향 미치지 않음

 

성능과 보안 면에서 innerHTML 보다 textContent가 좋은 것을 확인할 수 있다.

그러므로 불가피한 상황이 아니라면 textContent를 사용하는 것이 좋겠다.

 

물론 textContent는 텍스트만 다룰 수 있기 때문에 html요소를 생성하기 위해서는 다른 방법을 추가로 사용해야 한다.

바로 createElement()와 생성된 요소를 추가할 수 있는 append()이다.

 

innerHTML 사용을 지양하고 textContent, createElement(), append()를 사용하도록 하자.

반응형
그레이트현