XSS 공격 : 악의적인 사용자가 공격하려는 사이트에 악성 스크립트를 삽입할 수 있는 보안 취약점
C&C(좀비 PC에 명령을 내리거나 악성 코드를 제어하는 서버)로 리다이렉트 하거나 사용자의 쿠키를 탈취하여 세션 하이재킹 공격을 할 수 있다.
현재 구현한 마크다운 기능은 타임리프 th:utext 문법을 사용해, 저장된 데이터를 그대로 다 렌더링 하기에 Stored XSS에 노출되어 있다. 즉 서버에 악성 스크립트가 저장되고 사용자가 질문이나 답변을 조회할 때 공격에 노출될 수 있다.
교재에서 활용한 commonmark 라이브러리를 활용하여 기존 방식대로 마크다운 문법을 HTML 태그 등으로 전환하고, 렌더링해서 보여줄 때 일부 태그만 허용하도록 구현하여 XSS 공격을 대비하고자 함
OWASP Java HTML Sanitizer를 같이 사용하여 sanitizer 기능을 구현
의존성 추가
implementation 'org.commonmark:commonmark-ext-gfm-tables:0.20.0' // 테이블 관련 추가 설정
implementation 'org.commonmark:commonmark-ext-gfm-strikethrough:0.21.0' // 취소선 관련 추가 설정
implementation 'com.atlassian.commonmark:commonmark-ext-task-list-items:0.15.0' // task 관련 추가 설정
implementation 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20211018.2'
관련 기존 코드 개선
Parser parser = Parser.builder().build();
HtmlRenderer renderer = HtmlRenderer.builder().build();
return renderer.render(document);
매번 랜더링 될 때마다 Parser 객체와 HtmlRender 객체를 계속 생성
질문에 달린 답변 하나하나 보여줄 때마다 다 생성, 질문도 생성
Bean에 등록하고 의존성 주입 받으면 계속 생성할 필요가 없어 개선
개선
// 마크다운 렌더링 렌더러 및 파서 빈 등록
@Bean
HtmlRenderer htmlRenderer(List<Extension> extensions) {
return HtmlRenderer.builder()
.extensions(extensions)
.build();
}
@Bean
Parser parser(List<Extension> extensions) {
return Parser.builder()
.extensions(extensions)
.build();
}
@Bean
List<Extension> markdownExtensions() {
return Arrays.asList(
TablesExtension.create(),
StrikethroughExtension.create(),
TaskListItemsExtension.create()
);
}
CommonUtil 클래스 개선
@Component
@RequiredArgsConstructor
public class CommonUtil {
private final Parser parser;
private final HtmlRenderer renderer;
public String markdown(String markdown) {
Node document = parser.parse(markdown);
String html = renderer.render(document);
System.out.println("Converted HTML: " + html);
// Sanitize HTML
PolicyFactory policy = new HtmlPolicyBuilder()
.allowElements("h1", "h2", "h3", "p", "b", "i", "em", "strong", "img", "a", "ul", "ol", "li", "table", "thead", "tbody", "tr", "th", "td", "del", "blockquote", "code", "pre", "input", "hr")
.allowUrlProtocols("https", "http")
.allowAttributes("href", "target").onElements("a")
.allowAttributes("src", "alt").onElements("img")
.allowAttributes("type", "checked", "disabled").onElements("input")
.allowAttributes("border", "cellspacing", "cellpadding").onElements("table")
.requireRelNofollowOnLinks()
.toFactory();
String safeHtml = policy.sanitize(html);
System.out.println("After sanitize: " + safeHtml);
return safeHtml; // Return the sanitized HTML
}
}
table {
border-collapse: collapse; /* 테두리 */>
}
thead {
background-color: #91c0fd;
font-weight: bold;
color: white;
padding: 12px;
text-align: center;
}
th, td {
padding: 12px;
border: 3px solid #91c0fd;
}
/* 인용 */
blockquote {
/* 왼쪽 경계선 */
border-left: 4px solid #cccccc;
/* 들여 쓰기와 여백 */
padding-left: 20px;
margin-left: 0;
/* 글꼴 속성 (기울어짐, 색상 등) */
font-style: italic;
color: #666666;
}
a {
text-decoration: none;
color: inherit;
}
img {
max-width: 100%;
}
찾던 과정은 이렇습니다.
TaskList를 예시로 들면
아 그리고 마크다운 Code 방식, Code 태그는 생기는데 꾸밈이 안생깁니다.
이는 highlight.JS 라는 라이브러리를 사용하여 해결하였습니다.
적용 시키기 위해서는 pre태그로 code 태그가 감싸줘야 하기에 문서 로드될 때 자바스크립트 함수로 감싸주도록 하였습니다.
<pre><code></code></pre>
적용 후
진짜 그냥 마크다운 쓰는 것 처럼 깔끔하게 나옵니다!
뭔가 적용을 시킨 블로그가 없는 것 같아 노가다로 한 4시간이 걸린 것 같습니다 하하 혹시 이런 방식을 사용하실 예정이라면 제 코드 가져가시면 될 것 같습니다!
전체 코드 : PR 바로가기
참조