HTML 에디터 등 사용 시 XSS 공격 백엔드에서도 대비하기

박철현·2025년 5월 3일
0

스프링부트

목록 보기
7/8

들어가기 전에

  • XSS 공격 : 악의적인 사용자가 공격하려는 사이트에 악성 스크립트를 삽입할 수 있는 보안 취약점
    • C&C(좀비 PC에 명령을 내리거나 악성 코드를 제어하는 서버)로 리다이렉트 하거나 사용자의 쿠키를 탈취하여 세션 하이재킹 공격을 할 수 있다.

고려한 상황?

  • 기능 구현을 열심히 하다가 HTML 에디터를 사용하는 상황을 마주쳤다.
  • 이걸 보자마자 예전에 했던 XSS 보안 대비를 해야한다는 상횡이 생각났고 SSR 방식이 아닌 Rest API 방식에서도 적용할 수 있지 않을까? 하는 생각이 들어 방법을 적용하고, 요구사항을 만족시켰다.

그러면 이전 구현 게시글이 있는데 왜 다시 쓰는가?

  • 이전부터 지금까지 나는 단순히 <script> 태그만 XSS 공격에 사용될 줄 알았지만, 찾아보니 다른 사례도 많이 알려져있다.
    • 결국 이전 방식처럼 화이트리스트로 허용 가능한 HTML 태그들과 속성들을 지정해둔 것 자체가 예측 불가능한 모든 사례들을 막은 것이였다.
  • 그래서 나는 어떤 XSS 유형이 있고, 새로 나온 Sanitizer 버전을 이용하여 최신의 라이브러리 사용법을 적어두려한다.
    • Html 에디터를 사용하는 기능에서 백엔드 개발 구현하시는 분들에게 도움이 되기를..!

대표적인 XSS 공격 패턴

  • 출처 : GPT

<script> 태그 인젝션

  • 가장 전형적인 형태로 브라우저가 그대로 실행
<script>alert('XSS!');</script>

이벤트 핸들러 속성

  • <img>, <svg>, <body> 등 거의 모든 태그에 onerror, onclick, onload 같은 속성을 달아 스크립트를 실행
<img src="x" onerror="alert('XSS via onerror')">
<a href="#" onclick="stealCookies()">Click me</a>

javascript: URL 스킴

  • 링크 클릭만으로 스크립트 실행
<a href="javascript:alert('XSS via JS URI')">Click</a>

data: URL 인젝션

<iframe src="data:text/html;base64,PHNjcmlwdD5hbGVydCgnV2l0aCBkYXRhJyk8L3NjcmlwdD4="></iframe>
  • Base64로 인코딩된 스크립트 ifram으로 삽입

CSS 표현식 (구형 IE)

<div style="width: expression(alert('XSS via CSS'));"></div>

DOM 기반 XSS

// userInput에 <img src=x onerror=...> 같은 문자열이 들어올 때
container.innerHTML = userInput; 
  • 사용자 입력 자체를 HTML안에 넣는것이니까 바로 삽입됨;;
    • 서버에서 이미 저장된 스크립트가 클라이언트에서 innerHTML로 삽입될 때 실행

구현

  • 그래서 위의 것들을 어떻게 방어하나?
  • 이전 포스팅에도 나와있고 위에서도 언급했던 것 처럼 허용할 HTML 태그와 속성들을 지정하고 나머지는 허용하지 않는 화이트 리스트 방식 이용

코드

의존성 추가

implementation("com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1") // Sanitizer

Service 정의

@Service
public class HtmlSanitizerService {

	// 1) 허용할 태그, 속성, URL 스킴을 화이트리스트로 정의
	private static final PolicyFactory POLICY = new HtmlPolicyBuilder()
		// 블록 요소
		.allowElements("p", "div", "ul", "ol", "li", "h1", "h2", "h3", "blockquote")
		// 인라인 요소
		.allowElements("span", "strong", "em", "br", "code")
		// 링크와 이미지
		.allowElements("a", "img")
		.allowAttributes("href").onElements("a")
		.allowAttributes("src", "alt", "width", "height").onElements("img")
		// href/src 스킴 제한
		.allowUrlProtocols("http", "https")
		// 스타일 속성(필요 시)
		.allowAttributes("style").onElements("span", "p", "div")
		.toFactory();

	/**
	 * @param rawHtml 사용자로부터 넘어온 원시 HTML
	 * @return 안전하게 필터링된 HTML
	 */
	public String sanitize(String rawHtml) {
		return POLICY.sanitize(rawHtml);
	}
}

Controller & request dto

  • 회고록 작성한다고 가정
public record ReviewWriteRequest(
	@Schema(description = "이번달 회고록 제목", example = "4월 총 사용 회고록")
	@NotBlank(message = "제목은 필수 항목입니다.")
	String title,
	@Schema(description = "이번달 회고록 내용", example = "<p> 병원비.. 눈물.. </p>")
	@NotBlank(message = "본문 내용은 필수 항목입니다.")
	String content
) {
}
	@PostMapping("/review")
	@Operation(summary = "한달 회고 작성", description = "한달동안 회고를 작성합니다.")
	public ResponseEntity<String> reviewWrite(@AuthenticationPrincipal CustomUserDetails userDetails,
		@RequestBody @Valid ReviewWriteRequest reviewWriteRequest) {
		return ResponseEntity.ok(expenditureService.writeReview(userDetails.getMember(), reviewWriteRequest));
	}

비즈니스로직 Service

	@Transactional
	public String writeReview(Member member, ReviewWriteRequest reviewWriteRequest) {
		// 1. 혹시 모를 XSS 공격 대비 화이트리스트로 위험 요소 내용 삭제
		String sanitizeContent = htmlSanitizerService.sanitize(reviewWriteRequest.content());

		// 2. DB 저장인데 return으로 대체
		return sanitizeContent;
	}

결과

허용된 태그 입력


허용되지 않은 태그 입력


GitHub 보러가기

PR

profile
비슷한 어려움을 겪는 누군가에게 도움이 되길

0개의 댓글