jsp를 할때 보안때문에 <c:out> 을 쓰라고 하는데 대충 어떤 이유인지 알고만 있었지 좀 더 자세하게 알고 싶어서 정리해봤다
<c:out>을 사용하는 이유는 XSS (크로스 사이트 스크립팅) 보안 사고를 방지하기 위해서다 예를 들어 사용자가 닉네임을 ; alert('해킹완료'); // 와 같이 실행될 수 있는 자바스크립트로 가입했다면 유저의 세션을 탈취하는 등의 보안 문제가 생긴다
let name = ''; alert('해킹완료'); //';
그럼 어떻게 이를 막을 수 있을까?
먼저 브라우저가 문자를 읽어들이는 기준에 대한 이해가 필요하다 브라우저는 위에서 아래로 코드를 읽어 내리면서 <나 ‘같은 기호를 만나면 이를 단순한 글자가 아니라 코드를 실행하라는 제어문자로 취급한다
따라서 브라우저가 오해할 수 있는 이 제어문자를 단순 텍스트 기호로 치환(이스케이프 처리)해야한다
; alert('해킹완료'); // 를 예시로 들자면 아래처럼 치환이 된다
let name = ''; alert('해킹완료'); //';
let name = ''; alert('해킹완료'); //';
이런 역할을 해주는 것이 <c:out> 이다 그럼 <c:out> 특징에 대해 더 알아보자
<c:out> 사용 특징1. 자바스크립트 문법 깨짐 방지
<c:out>을 쓰지 않았을 때: 데이터베이스에 저장된 문자열에 작은따옴표, 큰따옴표, 줄바꿈 등이 포함되었을 때Connor'; 부분에서 문법 오류가 발생// 서버가 렌더링한 결과
let name = 'O'Connor';<c:out>을 썼을 때: <c:out>은 기본적으로 특수문자를 HTML 엔티티로 Escape함 (예: ' → ')// 서버가 렌더링한 결과
let name = 'O'Connor';2. XSS (Cross-Site Scripting) 보안 취약점 방어
악의적인 사용자가 입력창에 자바스크립트를 교묘하게 섞어서 입력했을 때 막아주는 단순한 방어선
<c:out>을 쓰지 않았을 때: 아래처럼 입력된 데이터는 자바스크립트 코드로 해석되어 → alert이 아닌 사용자의 세션 쿠키를 탈취하는 코드라면 보안사고로 이어짐// 서버가 렌더링한 결과
let name = ''; alert('해킹완료'); //';<c:out>을 썼을 때: 특수문자 처리가 되기 때문에 단순 텍스트로 취급되어 안전// 서버가 렌더링한 결과
let name = ''; alert('해킹완료'); //';3. Null 처리의 편의성 (default 속성)
객체가 null일 때 화면에 null이라는 글자가 그대로 노출되거나 에러가 날 수 있는데 <c:out> 으로 데이터가 비어있을 때 보여줄 기본값을 지정할 수 있음
<script>
let name = '<c:out value="${userName}" default="게스트" />';
</script>
그럼 <c:out>을 쓴다면 정말 괜찮은 것인가?
<c:out> 한계<c:out>을 통해 변환된 데이터(예: O'Connor)를 자바스크립트 변수에 담은 뒤 화면에 뿌릴 때 문제가 발생한다
1. 따옴표가 없는 경우
<c:out> 은 < , > , & , " , ' 문자만 이스케이프한다
예를 들어 아래와 같이 따옴표가 있는 코드에서 악성코드를 실행하려면 문자열 영역을 벗어나기 위해 무조건 작은 따옴표를 입력해야 한다
코드:
let name = '<c:out value="${name}" />';
해커 시도: '); alert(1);//
결과: let name = ''); alert(1);//'
숫자를 받는 변수라 따옴표가 없는 코드인 경우에는 문자열 영역을 벗어나지 않아도 되기 때문에 방어에 실패한다
코드:
let age = <c:out value="${age}" />;
해커 시도: 10; alert(1) <- 따옴표 안씀
결과: let age = 10; alert(1); <- 방어 실패
이처럼 따옴표같은 특수문자를 아예 안쓰고도 자바스크립트를 실행할 수 있는 환경이 주어지면 <c:out>으로는 부족하다
2. JSON 형태로 만들 때의 문제
JSON은 백슬래시(\)나 큰따옴표(") 처리가 엄격한데 아래와 같은 코드에서 userName에 백슬래시 한글자라도 들어 간다면 <c:out> 은 백슬래시를 치환하지 않아 JSON 문법이 깨져버려 에러가 발생해 고의로 문제를 일으킬 수 있다
<script>
var userData = {
"name": "<c:out value="${userName}" />"
};
</script>
3. text()나 textContent를 사용
.text() 함수는 문자열을 해석하지 않고 "이 글자 그대로 화면에 출력해라"라고 브라우저에 지시하기 때문에 화면이 깨진다
<script>
// 1. 서버에서 이스케이프되어 들어옴 -> let name = 'O'Connor';
let name = '<c:out value="${userName}" />';
// 2. jQuery의 text()를 이용해 화면에 주입
$('#nameSpan').text(name);
</script>
O'Connor <-- 괄호나 따옴표가 깨져서 보임!
4. HTML 이벤트 속성(onclick 등) 내부에 사용할 때의 문제
onclick, onmouseover 같은 HTML 이벤트 속성 안에서는 브라우저의 파싱(해석) 순서 때문에 <c:out>의 방어가 무력화된다
<button onclick="sayHello('<c:out value="${userName}" />')">인사하기</button>
해커 시도: '); alert('해킹 서버 처리 결과:
<button onclick="sayHello(''); alert('해킹')">인사하기</button>
서버에서는 작은따옴표를 '로 안전하게 치환하여 내려보냈지만 브라우저가 화면을 그릴 때 문제가 발생한다 브라우저의 HTML 파서는 이벤트 속성(onclick) 안에 있는 암호화된 문자(')를 스스로 원래 글자인 작은따옴표(')로 다시 되돌려 놓는다(디코딩)
이후 유저가 버튼을 클릭하면 자바스크립트 엔진은 HTML 파서가 원래대로 되돌려 놓은 날 것의 코드를 실행한다
실제 자바스크립트 실행 결과: sayHello(''); alert('해킹') <- 방어 실패
결과적으로 HTML 규칙과 자바스크립트 규칙이 겹치는 이벤트 속성에서는 브라우저의 디코딩 특성 때문에 <c:out>의 이스케이프가 강제로 해제되어 XSS 공격에 뚫리게 된다
그럼 어떻게 할까?
이러한 취약점을 방어하려면 '데이터가 출력되는 위치에 맞는 전용 방어책'을 써야 한다
1. 데이터 속성(data-*) 활용
자바스크립트 안에 서버 데이터를 직접 주입하지 않고 HTML 태그의 data-*속성에 데이터를 넣은 뒤 자바스크립트로 읽어오는 방식이다
이 방식이 방어책이 되는 이유는 브라우저가 이를 '실행 가능한 코드'가 아닌 '순수 데이터(문자열)'로 취급하기 때문이다
자바스크립트 안에 직접 변수를 섞어 쓰면 자바스크립트 엔진이 코드를 읽으며 '명령어'로 오해하고 실행할 위험이 있지만 DOM API(dataset)를 통해 HTML에서 꺼내온 데이터는 자바스크립트가 단순한 텍스트(String 객체)로만 인식한다
<!-- 1. HTML 영역에 데이터를 심어둡니다. (여기선 <c:out>이 완벽히 방어함) -->
<div id="userInfo" data-age="<c:out value='${age}' />"></div>
<script>
// 2. 자바스크립트는 안전하게 HTML에서 데이터를 꺼내 씁니다.
const userInfoDiv = document.getElementById('userInfo');
const userAge = userInfoDiv.dataset.age;
// 이제 userAge에는 악성 코드가 아닌 단순 문자열 "10; alert(1);" 이 들어갑니다.</script>
2. OWASP Java Encoder 사용 (자바스크립트 전용 방패)
불가피하게 <script> 태그 안이나 HTML 이벤트(onclick 등)에 데이터를 직접 넣어야 한다면 <c:out> 대신 자바스크립트 전용 이스케이프 도구를 써야한다
// JSP 상단에 OWASP 태그 라이브러리를 추가하고 상황에 맞는 태그를 사용
<%@ taglib prefix="e" uri="https://www.owasp.org/index.php/OWASP_Java_Encoder_Project" %>
<script>
// JS 컨텍스트 전용 방어: 괄호, 세미콜론 등을 전부 안전한 유니코드로 변환합니다.
let age = <e:forJavaScript value="${age}" />;</script>
<!-- HTML 이벤트 속성(onclick) 전용 방어 -->
<button onclick="greet('<e:forHtmlAttribute value="${userName}" />')">인사하기</button>
3. 백엔드에서의 엄격한 타입 검증
애초에 나이(age) 같은 데이터에 10; alert(1) 같은 문자가 들어오는 것 자체가 문제기 때문에 프론트엔드(JSP)에서 방어하기 전에 백엔드에서 입력값을 차단해야 한다
// 정규식이나 어노테이션을 통해 숫자만 들어오도록 강제합니다.
@PostMapping("/updateAge")
public String updateAge(@RequestParam @Min(1) @Max(150) int age){
// age 파라미터가 숫자가 아니면 Spring이 알아서 400 Bad Request 에러를 뱉고 튕겨냅니다.
return "success";
}
요약하자면:
- 자바스크립트 안에 서버 변수(
${...})를 직접 섞어 쓰는 패턴 자체를 피하기(data-*속성 사용 권장)- 어쩔 수 없이 섞어 써야 한다면
<c:out>은 버리고 OWASP Encoder를- 숫자나 날짜 같은 정형화된 데이터는 반드시 서버 단에서 타입 검증을