티스토리 뷰

📌 3줄 요약
1. XSS 방어 함수는 출력 위치(컨텍스트)에 맞아야 효과가 있습니다. 같은 함수라도 HTML 속성에서는 안전하고, 주소 위치에서는 무력할 수 있습니다.
2. HTML 인코딩은 따옴표로 "탈출"하는 공격은 막지만, javascript:처럼 값 자체가 위험한 공격은 막지 못합니다.
3. 그래서 보안 함수는 위치별로 나누고, 주소는 인코딩이 아니라 허용 목록(화이트리스트)으로 검증하도록 설계해야 합니다.

소스코드 진단을 하다 보면 이런 대화를 자주 합니다.

 

진단 도구: "출력 지점에서 XSS가 탐지되었습니다."
개발자: "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=' 그대로 &#39; onfocus=alert(1) x=&#39; 속성을 닫지 못함 → 글자
%27 (URL 인코딩) ' (서버가 먼저 디코딩) &#39; 글자
&#39; (HTML 엔티티) &#39; &amp;#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 인코딩과 주소 검증은 쓰이는 위치가 달라, 한 함수에 섞으면 어디에 써도 되는지 알 수 없게 됨
유지보수 누락 새 우회 기법이 나올 때마다 함수를 고쳐야 하고, 고친 함수가 모든 출력에 영향을 줌
💡 OWASP는 리다이렉트 주소를 검증할 때 거부 목록이 아닌 허용 목록 방식을 쓰라고 권고합니다. 허용된 주소만 통과시키면 위 우회 기법을 하나하나 따라갈 필요가 없습니다.

🛠 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 전용 인코딩을 쓰거나, 값을 스크립트 밖(속성)으로 빼는 편이 낫습니다.


🎉 정리

  1. XSS 방어는 함수를 썼느냐가 아니라 그 위치에 맞느냐의 문제다.
  2. HTML 인코딩은 탈출형 공격을 막고, 값 자체가 위험한 공격(javascript:)은 막지 못한다.
  3. 데이터 위치(①·②)에서는 인코딩된 값으로도 우회할 수 없다.
  4. 주소 위치(③)는 허용 목록 검증으로 XSS와 오픈 리다이렉트를 함께 막는다.
  5. 블랙리스트는 고려할 우회가 많고, 범용 함수에 섞으면 정상 데이터까지 망가진다.
  6. 보안 함수는 위치별로 나누고, 검증된 라이브러리를 쓰고, 테스트로 고정하자.

진단에서 "XSS 처리 다 했어요"라는 답을 들으면, 함수 이름보다 먼저 "그 값이 어디에 출력되지?"를 확인해 보세요. 판정은 대부분 거기서 갈립니다.


🔗 참고 자료

이 글은 실무에서 마주친 진단 유형을 일반화한 것으로, 코드와 함수명은 모두 설명을 위해 새로 작성한 가상 예시입니다. 특정 시스템이나 조직과는 관련이 없습니다.