summernote 일부 개선

This commit is contained in:
Rinjae
2026-01-06 18:51:31 +09:00
parent 385747c110
commit 935917e22a
5 changed files with 497 additions and 255 deletions
@@ -53,6 +53,12 @@ class RequestWrapper extends HttpServletRequestWrapper {
"^data:image/.*",
Pattern.CASE_INSENSITIVE);
// XSS 문자 변환을 건너뛸 파라미터 목록 (리치 텍스트 에디터 콘텐츠 등)
// XSS 패턴 필터링은 적용하지만 <, > 등으로 변환하지 않음
private static final String[] SKIP_CHAR_CONVERT_PARAMS = {"contents", "content", "description"};
// 현재 처리 중인 파라미터명 (cleanXSS에서 문자 변환 여부 결정에 사용)
private ThreadLocal<String> currentParameter = new ThreadLocal<>();
public RequestWrapper(HttpServletRequest servletRequest) {
super(servletRequest);
@@ -67,8 +73,13 @@ class RequestWrapper extends HttpServletRequestWrapper {
int count = values.length;
String[] encodedValues = new String[count];
for (int i = 0; i < count; i++) {
encodedValues[i] = cleanXSS(values[i]);
currentParameter.set(parameter);
try {
for (int i = 0; i < count; i++) {
encodedValues[i] = cleanXSS(values[i]);
}
} finally {
currentParameter.remove();
}
return encodedValues;
@@ -81,13 +92,19 @@ class RequestWrapper extends HttpServletRequestWrapper {
for (Entry<String, String[]> entry : map.entrySet()) {
String[] values = entry.getValue();
String paramName = entry.getKey();
if (values != null && values.length > 0) {
String[] cloneValues = new String[values.length];
for (int idx = 0; idx < values.length; idx++) {
cloneValues[idx] = cleanXSS(values[idx]);
currentParameter.set(paramName);
try {
for (int idx = 0; idx < values.length; idx++) {
cloneValues[idx] = cleanXSS(values[idx]);
}
} finally {
currentParameter.remove();
}
clonedMap.put(entry.getKey(), cloneValues);
clonedMap.put(paramName, cloneValues);
}
}
@@ -102,7 +119,12 @@ class RequestWrapper extends HttpServletRequestWrapper {
return value;
}
return cleanXSS(value);
currentParameter.set(parameter);
try {
return cleanXSS(value);
} finally {
currentParameter.remove();
}
}
@Override
@@ -134,9 +156,11 @@ class RequestWrapper extends HttpServletRequestWrapper {
// src 속성 화이트리스트 필터링 - 허용된 프로토콜만 통과
value = filterSrcAttribute(value);
StringBuilder sb = new StringBuilder();
value = convertChars(value, sb);
// 리치 텍스트 파라미터는 문자 변환을 건너뛰고 XSS 패턴 필터링만 적용
if (!shouldSkipCharConvert()) {
StringBuilder sb = new StringBuilder();
value = convertChars(value, sb);
}
// eval, javascript 등 스크립트 관련 문자열 제거
value = value.replaceAll("(?i)eval\\((.*?)\\)", "");
@@ -144,6 +168,23 @@ class RequestWrapper extends HttpServletRequestWrapper {
return value;
}
/**
* 현재 파라미터가 문자 변환을 건너뛸지 여부 확인
* 리치 텍스트 에디터 콘텐츠 등은 HTML 태그가 필요하므로 &lt; &gt; 변환을 하지 않음
*/
private boolean shouldSkipCharConvert() {
String param = currentParameter.get();
if (param == null) {
return false;
}
for (String skipParam : SKIP_CHAR_CONVERT_PARAMS) {
if (StringUtils.equalsIgnoreCase(param, skipParam)) {
return true;
}
}
return false;
}
/**
* src 속성을 화이트리스트 방식으로 필터링
* 허용: data:image/ (Base64 이미지)