티스토리 뷰

1. XSS 방어 함수는 출력 위치(컨텍스트)에 맞아야 효과가 있습니다. 같은 함수라도 HTML 속성에서는 안전하고, 주소 위치에서는 무력할 수 있습니다.
2. HTML 인코딩은 따옴표로 "탈출"하는 공격은 막지만,
javascript:처럼 값 자체가 위험한 공격은 막지 못합니다.3. 그래서 보안 함수는 위치별로 나누고, 주소는 인코딩이 아니라 허용 목록(화이트리스트)으로 검증하도록 설계해야 합니다.
소스코드 진단을 하다 보면 이런 대화를 자주 합니다.
개발자: "XSS 처리 함수를 전부 적용했습니다. 오탐입니다."
진단 담당자: (코드를 보니 정말 모든 출력값에 함수가 적용되어 있다…)
그런데 그 함수가 어디에 쓰였는지를 보면 이야기가 달라집니다. 이 글은 실제 진단에서 만난 유형을 일반화해, 같은 함수가 왜 어떤 위치에서는 안전하고 어떤 위치에서는 안전하지 않은지, 그리고 XSS 방어 함수를 설계할 때 무엇을 고려해야 하는지 정리했습니다. 예시 코드는 모두 설명을 위해 새로 작성한 것입니다.
| 목차 | 내용 |
|---|---|
| 1 | 문제의 코드: 출력 위치 3곳 |
| 2 | ①·②가 안전한 이유 |
| 3 | ③이 XSS인 이유 |
| 4 | 필터에 javascript 차단을 넣으면? |
| 5 | 조치 방법 |
| 6 | 보안 함수 설계 원칙 |
| 7 | 진단 담당자 판정 가이드 |
🧩 1. 문제의 코드: 출력 위치 3곳
요청 파라미터를 hidden 값으로 담아 다른 주소로 자동 POST 하는 공통 함수입니다. 레거시 시스템에서 화면 전환용으로 흔히 볼 수 있는 구조입니다.
public String forwardByPost(HttpServletRequest request, HttpServletResponse response, String url) throws Exception {
response.setContentType("text/html;charset=UTF-8");
StringBuilder resp = new StringBuilder();
resp.append("<form name='frm' method='post' action=''>");
Enumeration<String> names = request.getParameterNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
for (String value : request.getParameterValues(name)) {
resp.append("<input type='hidden' name='" + SecurityUtil.escapeHtml(name) // ①
+ "' value='" + SecurityUtil.escapeHtml(value) + "'>"); // ②
}
}
resp.append("</form><script>");
resp.append("document.frm.action='" + SecurityUtil.escapeHtml(url) + "';"); // ③
resp.append("document.frm.submit();</script>");
response.getWriter().print(resp);
return null;
}

| 위치 | 코드 | 브라우저의 해석 |
|---|---|---|
| ① 파라미터 이름 | name='…' |
글자(데이터) |
| ② 파라미터 값 | value='…' |
글자(데이터) |
| ③ 이동 주소 | frm.action='…' |
주소(URL) — 실제로 이동하고 실행함 |
세 곳 모두 같은 escapeHtml()을 거칩니다. 개발자 말대로 "전부 적용"한 것은 맞습니다. 하지만 OWASP가 설명하듯 브라우저는 HTML, JavaScript, URL, CSS를 각각 다르게 해석하기 때문에, 필요한 방어도 위치마다 다릅니다.
✅ 2. ①·②가 안전한 이유
escapeHtml()이 &를 가장 먼저 변환하고, 이어서 < > " '를 HTML 엔티티로 바꾼다고 가정합니다. ①·②는 작은따옴표로 감싼 속성값이라, 공격자가 할 수 있는 것은 따옴표로 속성을 닫고 탈출하는 것뿐입니다.
| 공격자가 보낸 값 | 서버가 받는 값 | 인코딩 후 | 결과 |
|---|---|---|---|
' onfocus=alert(1) x=' |
그대로 | ' onfocus=alert(1) x=' |
속성을 닫지 못함 → 글자 |
%27 (URL 인코딩) |
' (서버가 먼저 디코딩) |
' |
글자 |
' (HTML 엔티티) |
' |
&#39; |
글자 (&를 먼저 바꾸므로 무력화) |
%2527 (이중 인코딩) |
%27 |
%27 |
글자 (속성값은 URL 디코딩하지 않음) |
javascript:alert(1) |
그대로 | 그대로 | 글자 — value는 주소가 아니라 실행되지 않음 |
')도 변환하는지 꼭 확인해야 합니다.⚠️ 3. ③이 XSS인 이유

③은 폼의 이동 주소입니다. 브라우저는 이 값을 주소로 해석해 이동하므로, 공격자는 탈출할 필요조차 없습니다. 값 자체를 javascript:alert(document.cookie)로 넣으면 됩니다.
| 구분 | 일반적인 XSS | 주소 위치의 XSS |
|---|---|---|
| 공격 방식 | 따옴표·꺾쇠로 문맥을 탈출해 스크립트 삽입 | 탈출 없이 값 자체가 스크립트 |
| 결과 | 사이트 권한으로 스크립트 실행 | 사이트 권한으로 스크립트 실행 (같음) |
| HTML 인코딩으로 방어 | 가능 | 불가 — 바꿀 특수문자가 없음 |
OWASP 치트시트도 "출력 인코딩은 완벽하지 않다"며, URL을 다루는 위치를 인코딩만으로 막기 어려운 대표적인 경우로 들고 있습니다.
같은 위치의 또 다른 위험: 오픈 리다이렉트
| 위험 | 원리 | 분류 |
|---|---|---|
javascript: 주소 |
스크립트 실행 | XSS |
| 외부 도메인 주소 | 사용자를 외부로 이동시키고, 이 함수가 요청 파라미터 전체를 자동 전송하므로 파라미터까지 외부로 유출 | 오픈 리다이렉트 (별개 취약점) |
결과서에는 XSS 탐지 건을 "③ 주소 위치의 스킴 미검증"으로 기록하고, 외부 도메인 이동은 필요하면 오픈 리다이렉트로 분리해서 기록하면 분류가 깔끔합니다.
🧪 4. 필터에 javascript 차단을 넣으면?
개발자가 이렇게 답할 수도 있습니다. "저희 함수에는 javascript 차단, 대소문자 처리, 반복 제거, 인코딩 처리까지 다 들어 있어요." 이 경우 정말 모든 우회를 막는다면 XSS 판정은 양호가 될 수 있습니다. 하지만 키워드를 찾아 막는 방식(블랙리스트)은 고려할 것이 많습니다.

| 우회 기법 | 예시 | 필요한 처리 |
|---|---|---|
| 대소문자 | JaVaScRiPt: |
대소문자 구분 없이 검사 |
| 탭·줄바꿈 삽입 | java + 탭 + script: | WHATWG URL 표준에 따라 브라우저는 주소 안의 탭·줄바꿈을 제거하고 해석 → 제거 후 검사 |
| 앞쪽 공백·제어문자 | 공백 + javascript: | 브라우저는 앞뒤 공백·제어문자도 제거 → 제거 후 검사 |
| 제거 후 재조합 | javajavascriptscript: |
한 번 지우면 다시 javascript:가 됨 → 치환이 아니라 차단 |
| 다른 스킴 | data:, vbscript: |
javascript만 막으면 통과 |
| 외부 도메인 | https://외부/ |
키워드가 없어 블랙리스트로 원천 불가 |
범용 HTML 함수에 키워드 차단을 넣을 때의 부작용
| 부작용 | 예시 |
|---|---|
| 정상 데이터 훼손 | 게시글·상품명에 "javascript 강의" 같은 글자가 있으면 화면에서 바뀌거나 지워짐 |
| 역할 혼재 | HTML 인코딩과 주소 검증은 쓰이는 위치가 달라, 한 함수에 섞으면 어디에 써도 되는지 알 수 없게 됨 |
| 유지보수 누락 | 새 우회 기법이 나올 때마다 함수를 고쳐야 하고, 고친 함수가 모든 출력에 영향을 줌 |
🛠 5. 조치 방법
| 순서 | 조치 | 효과 |
|---|---|---|
| ① | 주소를 허용 목록으로 검증 (내부 상대 경로 또는 자사 도메인만 허용) | javascript: 실행, 외부 이동, 파라미터 유출 차단 |
| ② | 주소를 <script> 밖, <form action> 속성으로 출력 |
출력 위치를 HTML 속성 하나로 단순화 |
| ③ | 검증 실패 시 기본 화면으로 이동하고 로그 기록 | 우회 시도 추적 |
private boolean isSafeRedirectUrl(String url) {
if (url == null || url.trim().isEmpty()) return false;
// 역슬래시·제어문자 거부 (브라우저는 "/\evil" 을 "//evil" 로 해석)
if (url.indexOf('\\') >= 0) return false;
for (char c : url.toCharArray()) {
if (c < 0x20 || c == 0x7f) return false;
}
// 내부 상대 경로: "/" 로 시작하되 "//" 는 거부 (다른 호스트로 해석됨)
if (url.startsWith("/") && !url.startsWith("//")) return true;
// 절대 주소: 자사 도메인으로 시작하는 경우만 허용 (설정값에서 읽기)
for (String base : allowedBaseUrls) { // 예: "https://www.example.com"
if (url.startsWith(base + "/")) return true;
}
return false;
}
if (!isSafeRedirectUrl(url)) {
log.warn("unsafe redirect url blocked : {}", url);
url = "/main"; // 기본 이동 화면
}
resp.append("<form name='frm' method='post' action='" + SecurityUtil.escapeHtml(url) + "'>");
// ... hidden 파라미터 출력 (기존과 동일) ...
resp.append("</form><script>document.frm.submit();</script>"); // 스크립트에는 동적 값 없음
공통 함수 안에서 한 번 막아 두면, 이 함수를 호출하는 하위 화면을 하나하나 확인하지 않아도 됩니다. 적용 전에는 현재 외부 제휴사 등으로 이동하는 화면이 있는지 확인해 허용 목록에 넣어야 장애가 나지 않습니다.
📐 6. 보안 함수 설계 원칙

이번 사례의 결론은 단순합니다. XSS를 처리하는 보안 함수는 "어디에 쓰이는가"를 기준으로 설계해야 합니다.
| 출력 위치 | 예시 | 필요한 방어 |
|---|---|---|
| HTML 본문 | <div>값</div> |
HTML 엔티티 인코딩 |
| HTML 속성 | value='값' |
HTML 속성 인코딩 (감싼 따옴표 종류까지 변환) |
| JavaScript 문자열 | var a='값'; |
JavaScript 전용 인코딩 |
| URL 파라미터 | ?q=값 |
URL 인코딩 후 HTML 속성 인코딩 |
| URL 전체 | href / src / action / location |
스킴·호스트 허용 목록 검증 |
| CSS | style='width:값' |
CSS 인코딩 또는 허용값만 |
| 원칙 | 내용 |
|---|---|
| ① 위치별로 함수를 나눈다 | forHtml(), forHtmlAttribute(), forJavaScript()처럼 이름만 봐도 쓸 위치를 알 수 있게 |
| ② 주소는 인코딩이 아니라 검증한다 | 허용 목록 기반. 블랙리스트는 보조 수단으로만 |
| ③ 검증된 라이브러리를 우선한다 | OWASP Java Encoder처럼 위치별 함수를 이미 제공하는 라이브러리를 쓰면 직접 만들 때의 누락이 줄어듦 |
| ④ 범용 함수에 키워드 차단을 섞지 않는다 | 정상 데이터 훼손과 역할 혼재를 막기 위해 |
| ⑤ 테스트 케이스를 함께 만든다 | 위 블랙리스트 우회 표의 값들을 단위 테스트로 고정 |
| ⑥ 진단 도구에 등록한다 | 사용자 정의 보안 함수를 진단 도구의 정화 함수로 등록하면 같은 패턴의 반복 탐지가 줄어듦 |
🔍 7. 진단 담당자 판정 가이드
| 확인 | 판정 |
|---|---|
①·② 속성값 위치 + 함수가 & < > " '를 모두 변환 |
양호 |
①·② + 작은따옴표 속성인데 ' 미변환 |
정탐 |
| ③ 주소 위치 + HTML 인코딩만 적용 | 정탐 (스킴 미검증) |
| ③ + 함수가 위 블랙리스트 우회를 모두 차단 | XSS는 양호 가능, 오픈 리다이렉트 권고는 남김 |
| ③ + 허용 목록 검증 적용 | 양호 |
개발자가 "다 처리되어 있다"고 할 때
| 단계 | 대응 |
|---|---|
| ① 근거 요청 | 오탐 주장의 입증 책임은 개발자에게 있음. "다 되어 있다"는 답변만으로는 판정하지 않고 함수 소스를 요청 |
| ② 소스 확인 | 위 블랙리스트 우회 표의 항목을 실제로 처리하는지 확인 |
| ③ 문구 주의 | 가이드에 "그 함수로 막을 수 없다"고 단정하기보다 "HTML 인코딩만으로는 막을 수 없다"고 쓰면, 함수가 실제로 스킴 차단을 하는 경우에도 가이드가 틀리지 않음 |
🚫 이렇게 판단하지 마세요
① "XSS 함수를 모든 출력에 적용했으니 오탐"
함수가 그 위치에 맞는지가 판정 기준입니다. 적용 여부가 아닙니다.
② "HTML 인코딩했으니 javascript:도 막힌다"
바꿀 특수문자가 없어 그대로 통과합니다.
③ "javascript 키워드만 막으면 끝"
대소문자, 탭·줄바꿈, 재조합, 다른 스킴, 외부 도메인까지 고려해야 합니다.
④ "인코딩된 값이 들어오면 ①·②도 뚫린다"
데이터 위치에서는 서버 디코딩 + & 우선 변환으로 무력화됩니다.
💬 자주 나오는 질문
Q. 주소 위치에 javascript: 가 들어가면 모든 브라우저에서 실행되나요?
브라우저와 위치에 따라 동작이 다를 수 있습니다. 하지만 실행 여부를 브라우저에 맡기는 것 자체가 위험하므로, 서버에서 허용된 주소만 출력하는 것이 원칙입니다.
Q. 상대 경로만 허용하면 충분한가요?
//로 시작하는 주소는 다른 호스트로 해석되고, 브라우저는 /\를 //처럼 다루는 경우가 있어 함께 거부해야 합니다.
Q. JavaScript 문자열 안에 HTML 인코딩을 쓰면 안 되나요?
<script> 안에서는 HTML 엔티티가 복원되지 않아 값이 그대로 보이는 기능 문제가 생기고, 역슬래시 같은 JavaScript 특수문자는 처리되지 않을 수 있습니다. JavaScript 전용 인코딩을 쓰거나, 값을 스크립트 밖(속성)으로 빼는 편이 낫습니다.
🎉 정리
- XSS 방어는 함수를 썼느냐가 아니라 그 위치에 맞느냐의 문제다.
- HTML 인코딩은 탈출형 공격을 막고, 값 자체가 위험한 공격(
javascript:)은 막지 못한다. - 데이터 위치(①·②)에서는 인코딩된 값으로도 우회할 수 없다.
- 주소 위치(③)는 허용 목록 검증으로 XSS와 오픈 리다이렉트를 함께 막는다.
- 블랙리스트는 고려할 우회가 많고, 범용 함수에 섞으면 정상 데이터까지 망가진다.
- 보안 함수는 위치별로 나누고, 검증된 라이브러리를 쓰고, 테스트로 고정하자.
진단에서 "XSS 처리 다 했어요"라는 답을 들으면, 함수 이름보다 먼저 "그 값이 어디에 출력되지?"를 확인해 보세요. 판정은 대부분 거기서 갈립니다.
🔗 참고 자료
- OWASP — Cross Site Scripting Prevention Cheat Sheet
- OWASP — Unvalidated Redirects and Forwards Cheat Sheet
- WHATWG — URL Standard (basic URL parser)
'정보보안 > Source Code' 카테고리의 다른 글
| 오픈소스 차트 라이브러리 DOM XSS 탐지 정탐·오탐 판정법 (ApexCharts 3.35.1 사례) (0) | 2026.10.14 |
|---|---|
| MyBatis ${}는 무조건 SQL 인젝션일까? foreach 인덱스 정·오탐 (0) | 2026.10.07 |
| 버퍼 크기는 17, 인덱스는 17 — "동적 할당이라 안전하다"는 답변 검토법 (0) | 2026.09.29 |
| (SW 보안약점) 경로 조작 및 자원 삽입 (0) | 2024.09.25 |
- Total
- Today
- Yesterday
- 악성코드
- 보안상식
- 전자금융기반시설
- IEC62443
- 금취분평
- 자율침투테스트
- 시큐어코딩
- E27
- 정보보안
- 사이버보안
- 보안뉴스
- 해킹주의
- sast
- IACS
- 펜테스트
- 개발자보안
- AI보안
- 전자금융기반시설취약점분석평가
- 개인정보보호
- 공급망공격
- 랜섬웨어
- DevSecOps
- AI모의해킹
- cve
- 해킹예방
- 선박사이버보안
- 개인정보유출
- 취약점
- 오픈소스보안
- 소스코드진단
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
