본문 바로가기
홍TV 홍TV

“반사형 XSS” 실제 취약 코드 → 안전한 코드로 수정

홍TV 읽는 시간 약 14분
4.5
(300)

들어가며

웹 개발을 하다 보면 검색 기능 하나쯤은 거의 반드시 만들게 됩니다.

사용자가 검색창에 단어를 입력하고 검색 버튼을 누르면 서버에서는 그 값을 받아서 검색하고, 화면에는 보통 이런 식으로 결과를 보여줍니다.

축구 검색 결과입니다.

개발할 때는 너무 당연한 기능이라 별다른 문제가 없어 보입니다.

그런데 이번에 웹 보안 쪽을 점검하면서 이 부분을 다시 살펴보게 됐습니다.

문제는 사용자가 입력한 값을 화면에 그대로 출력하고 있었다는 것이었습니다.

처음 코드를 봤을 때는 “검색어 하나 보여주는 건데?”라는 생각이 들었지만, 보안 관점에서 다시 보니 이야기가 달랐습니다.

바로 반사형 XSS(Reflected XSS)가 발생할 수 있는 구조였습니다.

반사형 XSS는 어떤 상황에서 발생할까?

반사형 XSS는 사용자가 전달한 값이 서버를 거쳐 다시 HTML 응답에 포함되고, 그 값이 브라우저에서 코드로 해석될 수 있을 때 발생합니다.

흐름을 간단하게 보면 이렇습니다.

사용자 입력
   ↓
URL 파라미터
   ↓
서버
   ↓
HTML 응답에 입력값 삽입
   ↓
브라우저에서 해석

예를 들어 검색 페이지가 다음과 같은 URL을 사용한다고 가정해보겠습니다.

/search?keyword=축구

서버는 keyword 값을 받아서 검색하고, 페이지에는 다음과 같이 표시합니다.

'축구' 검색 결과

여기까지는 정상입니다.

문제는 keyword에 들어오는 값이 항상 축구 같은 정상적인 문자열이라는 보장이 없다는 것입니다.

실제로 문제가 되는 취약한 코드

예를 들어 서버에서 전달받은 검색어를 HTML 문자열에 직접 넣는 코드가 있다고 해보겠습니다.

JavaScript로 단순화하면 다음과 비슷한 구조입니다.

const keyword = new URLSearchParams(location.search).get("keyword");

document.querySelector("#result").innerHTML =
    "'" + keyword + "' 검색 결과입니다.";

겉으로 보면 별 문제가 없어 보입니다.

검색어를 가져와서 화면에 출력하는 코드일 뿐입니다.

하지만 innerHTML은 문자열을 단순한 텍스트로 출력하는 것이 아니라 HTML로 해석할 수 있는 영역입니다.

따라서 사용자 입력을 그대로 넣는 것은 위험할 수 있습니다.

예를 들어 공격자가 HTML 태그나 스크립트로 해석될 수 있는 입력값을 전달하면 브라우저가 이를 단순한 검색어가 아니라 HTML 요소로 처리할 가능성이 생깁니다.

이것이 바로 XSS에서 주의해야 하는 부분입니다.

더 위험한 형태는 서버에서 HTML을 직접 만드는 경우

서버 코드에서도 비슷한 문제가 발생할 수 있습니다.

예를 들어 JSP 같은 템플릿에서 사용자 입력값을 HTML에 직접 삽입하는 구조를 생각해볼 수 있습니다.

<div>
    '${keyword}' 검색 결과입니다.
</div>

keyword가 안전하다는 전제가 없다면 문제가 될 수 있습니다.

특히 사용자가 입력한 값을 HTML 태그가 들어갈 수 있는 위치에 그대로 출력하는 구조라면 반드시 확인해야 합니다.

여기서 중요한 것은 입력값을 받았다는 사실 자체가 문제가 아니라, 그 값을 어떤 컨텍스트에 어떤 방식으로 출력하느냐입니다.

안전하게 수정하는 첫 번째 방법

가장 기본적인 원칙은 사용자 입력을 HTML로 해석하지 않고 텍스트로 출력하는 것입니다.

앞에서 사용했던 JavaScript 코드를 안전하게 바꾸면 다음과 같이 작성할 수 있습니다.

const keyword = new URLSearchParams(location.search).get("keyword");

document.querySelector("#result").textContent =
    "'" + keyword + "' 검색 결과입니다.";

차이가 보이시나요?

기존에는 innerHTML을 사용했습니다.

element.innerHTML = keyword;

수정한 코드는 textContent를 사용합니다.

element.textContent = keyword;

검색어를 HTML로 해석할 필요가 없다면 굳이 HTML로 삽입할 이유가 없습니다.

단순히 사용자에게 문자열을 보여주는 목적이라면 텍스트로 처리하는 것이 훨씬 안전합니다.

Spring과 Thymeleaf를 사용한다면?

Spring Boot에서 Thymeleaf를 사용하는 경우에도 비슷한 원칙을 적용할 수 있습니다.

예를 들어 검색어를 화면에 출력한다고 가정해보겠습니다.

안전하지 않은 HTML을 직접 삽입하는 방식은 다음과 같이 작성할 수 있습니다.

<div th:utext="${keyword}"></div>

th:utext는 HTML을 해석해서 출력할 수 있기 때문에 사용자 입력값에 사용할 때 특히 주의해야 합니다.

검색어를 단순한 텍스트로 보여주려는 목적이라면 일반적인 텍스트 출력 방식을 사용하는 것이 좋습니다.

<div th:text="${keyword}"></div>

이렇게 하면 사용자가 입력한 값을 HTML로 실행하는 것이 아니라 텍스트 형태로 출력하도록 처리할 수 있습니다.

왜 입력값을 삭제하는 방식은 추천하지 않을까?

XSS를 발견하면 처음에는 이런 식으로 해결하고 싶은 생각이 들 수 있습니다.

keyword = keyword.replace("<script>", "");

하지만 이런 방식은 좋은 해결 방법이 아닙니다.

문자열 일부를 찾아서 삭제하는 방식은 공격 패턴을 모두 예상하기 어렵고, 출력되는 위치에 따라 필요한 처리가 달라질 수 있기 때문입니다.

HTML 본문에 출력하는 값과 HTML 속성에 출력하는 값, JavaScript 코드 내부에 들어가는 값은 각각 고려해야 할 사항이 다릅니다.

그래서 “문제가 될 것 같은 문자열을 지우자”보다는 “출력되는 컨텍스트에 맞게 안전하게 처리하자”라는 방향으로 접근하는 것이 좋습니다.

URL 파라미터도 그냥 믿으면 안 된다

반사형 XSS에서 특히 많이 확인해야 하는 부분이 URL입니다.

예를 들어 다음과 같은 주소를 사용하는 서비스가 있다고 해보겠습니다.

/search?keyword=검색어

서버에서는 다음과 같이 처리할 수 있습니다.

@GetMapping("/search")
public String search(
        @RequestParam String keyword,
        Model model) {

    model.addAttribute("keyword", keyword);

    return "search";
}

여기까지는 사용자가 입력한 값을 Model에 넣는 것뿐입니다.

중요한 것은 View에서 어떻게 출력하느냐입니다.

안전하지 않은 방식으로 HTML을 직접 구성하거나 사용자 입력을 HTML로 해석하게 만들면 XSS 위험이 발생할 수 있습니다.

반대로 템플릿 엔진의 기본적인 텍스트 출력 기능을 사용하면 사용자 입력을 문자 데이터로 출력하도록 처리할 수 있습니다.

<div>
    '<span th:text="${keyword}"></span>' 검색 결과입니다.
</div>

결국 Controller에서 값을 받는 것보다 최종적으로 브라우저에 출력되는 지점을 확인하는 것이 중요합니다.

개발하면서 실제로 확인할 부분

반사형 XSS를 점검할 때는 소스 코드에서 다음과 같은 부분을 찾아보면 좋습니다.

request.getParameter()
@RequestParam
query string
URL parameter
innerHTML
outerHTML
th:utext
HTML 문자열 조립
사용자 입력값 직접 출력

물론 이런 코드가 있다고 무조건 XSS 취약점이라는 의미는 아닙니다.

중요한 것은 외부에서 들어온 값이 최종적으로 HTML, JavaScript, CSS, URL 등의 어떤 컨텍스트에 들어가는지입니다.

예를 들어 단순 텍스트 출력이라면 텍스트 출력 방식을 사용하고, HTML을 반드시 허용해야 하는 기능이라면 별도의 HTML sanitization 정책을 적용하는 식으로 접근해야 합니다.

수정 전과 수정 후를 비교해보면

이번 작업을 간단하게 정리하면 차이는 생각보다 단순합니다.

수정 전

const keyword = getKeyword();

document.querySelector("#result").innerHTML =
    "'" + keyword + "' 검색 결과입니다.";

사용자 입력값을 HTML로 해석할 수 있는 innerHTML에 직접 넣고 있습니다.

수정 후

const keyword = getKeyword();

document.querySelector("#result").textContent =
    "'" + keyword + "' 검색 결과입니다.";

단순히 검색어를 보여주는 목적이라면 HTML이 아닌 텍스트로 출력합니다.

Thymeleaf에서도 마찬가지입니다.

수정 전

<div th:utext="${keyword}"></div>

수정 후

<div th:text="${keyword}"></div>

물론 실제 프로젝트에서는 사용하는 프레임워크와 출력 위치에 따라 방어 방법을 결정해야 합니다.

수정했다고 끝나는 것도 아니다

XSS 취약점을 수정한 뒤에는 다시 한번 전체 흐름을 확인해야 합니다.

제가 이런 문제를 점검할 때는 단순히 해당 코드 한 줄만 수정하고 끝내기보다는 같은 데이터를 사용하는 다른 화면이 있는지 확인하는 편입니다.

검색 결과 페이지에서는 안전하게 처리했는데 모바일 페이지나 관리자 페이지에서 같은 값을 다른 방식으로 출력하고 있을 수도 있기 때문입니다.

특히 다음과 같은 화면을 함께 확인하는 것이 좋습니다.

  • 검색 결과
  • 게시판
  • 댓글
  • 에러 페이지
  • 로그인 실패 메시지
  • 관리자 화면
  • 사용자 프로필
  • URL 파라미터를 다시 보여주는 화면

한 곳에서 발견한 입력값 처리 문제는 다른 화면에서도 같은 패턴으로 존재할 가능성이 있습니다.

출력 인코딩과 CSP는 역할이 다르다

XSS 방어를 검색하다 보면 CSP(Content Security Policy)라는 이야기도 많이 나옵니다.

CSP는 브라우저에서 실행할 수 있는 스크립트나 리소스의 출처 등을 제한해 추가적인 보안 계층을 제공하는 방법입니다.

하지만 CSP를 설정했다고 해서 애플리케이션의 XSS 취약점을 수정하지 않아도 되는 것은 아닙니다.

기본적으로는 사용자 입력을 안전하게 처리하고 출력하는 것이 먼저입니다.

그 위에 CSP와 같은 보안 정책을 추가해서 방어 계층을 하나 더 만드는 방식으로 접근하는 것이 좋습니다.

이번 작업에서 배운 점

이번에 반사형 XSS를 확인하면서 가장 크게 느낀 것은 취약점이 굉장히 평범한 코드에서 만들어질 수 있다는 것이었습니다.

개발자는 단순히 검색어를 화면에 보여주려고 했을 뿐인데, 그 과정에서 사용자 입력을 HTML로 해석할 수 있도록 만들어버리면 보안 문제가 생길 수 있습니다.

특히 다음과 같은 코드가 눈에 들어온다면 한 번 더 확인하는 습관을 들이는 것이 좋겠습니다.

innerHTML

그리고 서버 템플릿에서 HTML을 직접 출력하는 기능도 주의해서 살펴봐야 합니다.

반대로 단순한 텍스트를 보여주는 목적이라면 가능한 한 텍스트 출력 기능을 사용하는 것이 훨씬 명확합니다.

마무리

반사형 XSS는 거창한 해킹 기술에서만 발생하는 문제가 아니었습니다.

검색어 하나를 URL로 받고, 그 값을 서버에서 다시 받아 화면에 보여주는 아주 평범한 기능에서도 발생할 수 있습니다.

결국 중요한 건 사용자가 입력한 데이터를 신뢰하지 않는 것입니다.

그리고 사용자 입력값을 출력해야 한다면 무조건 HTML로 넣기보다는 해당 데이터가 실제로 어떤 용도로 사용되는지 먼저 판단해야 합니다.

단순한 문자열이라면 텍스트로 출력하고, HTML이 필요한 경우에는 허용 범위를 명확하게 정하고 별도의 안전한 처리를 적용하는 것이 좋습니다.

이번 작업을 한 줄로 정리하면 이렇습니다.

“입력값을 막는 것만큼 중요한 것이, 그 입력값을 어떻게 출력하느냐이다.”

처음에는 검색창 하나의 문제처럼 보였지만 코드를 따라가 보니 서버와 템플릿, 브라우저까지 연결된 문제였습니다.

웹 서비스를 개발하고 있다면 오늘이라도 검색 페이지 하나를 열어서 확인해보는 것을 추천합니다.

innerHTML이나 사용자 입력값을 직접 HTML에 삽입하는 코드가 있다면, 그냥 지나치지 말고 “이 값이 브라우저에서 어떻게 해석될까?”를 한 번 생각해보면 좋겠습니다.

이 게시물이 얼마나 유용했나요?

별점을 클릭하여 평가하세요!

평균 평점 4.5 / 5. 투표 수: 300

아직 투표가 없습니다! 첫 번째로 평가해보세요.

홍TV

홍TV
함께 보면 좋은 글

댓글 0

첫 댓글을 남겨보세요.

error: Content is protected !!

광고 차단 알림

광고 클릭 제한을 초과하여 광고가 차단되었습니다.

단시간에 반복적인 광고 클릭은 시스템에 의해 감지되며, IP가 수집되어 사이트 관리자가 확인 가능합니다.