- 멀티파트 요청 지원 복호화 래퍼 추가
- PasswordDecryptFilter 순서 조정(MultipartFilter 이후 실행) - 필터와 XSS Escape 필터 순서 정보 및 주석 보완
This commit is contained in:
+90
@@ -0,0 +1,90 @@
|
||||
package com.eactive.apim.portal.common.security.passwordcrypto;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.multipart.MultipartHttpServletRequest;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 멀티파트(파일 업로드 동반) 요청 전용 복호화 래퍼.
|
||||
*
|
||||
* <p><b>배경</b> — {@link DecryptingRequestWrapper} 는 {@code getParameter()} 호출 시
|
||||
* {@code super.getParameter()} 위임 결과를 복호화하는 방식이다. 그런데 Spring 의
|
||||
* {@link org.springframework.web.multipart.support.MultipartFilter} 가 만드는
|
||||
* {@code MultipartHttpServletRequest} 는 자신이 직접 파싱한 폼 필드(파일이 아닌 text part) 값을
|
||||
* {@code getParameter()} 에서 곧바로 반환하고 {@code super.getParameter()} 로 위임하지 않는다.
|
||||
* 그 결과 {@code DecryptingRequestWrapper} 가 멀티파트 파싱 "이전"(안쪽)에 씌워지면, 멀티파트 안의
|
||||
* 비밀번호 파라미터는 복호화되지 않은 봉투(ENC1) 문자열 그대로 컨트롤러까지 전달된다 — 법인가입처럼
|
||||
* 파일 첨부(사업자등록증)와 비밀번호가 같은 폼에 있는 경우 재현된다.
|
||||
*
|
||||
* <p><b>해결</b> — {@link PasswordDecryptFilter} 를 MultipartFilter "이후"에 실행되도록 순서를
|
||||
* 옮기면(그리고 Lucy XSS 필터보다는 여전히 앞서도록), 이 필터가 감싸는 대상이 이미
|
||||
* {@code MultipartHttpServletRequest} 다. 이때는 이 클래스로 감싸 {@link MultipartHttpServletRequest}
|
||||
* 인터페이스 자체를 구현하고 파일 관련 메서드는 원본 멀티파트 요청에 위임한다 — Spring 의
|
||||
* {@code MultipartFile} 바인딩({@code WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class)})이
|
||||
* 이 래퍼를 그대로 인식하도록 하기 위함이다. 파라미터(텍스트 필드) 복호화 로직은 부모
|
||||
* {@link DecryptingRequestWrapper} 그대로 재사용한다.
|
||||
*/
|
||||
public class DecryptingMultipartRequestWrapper extends DecryptingRequestWrapper
|
||||
implements MultipartHttpServletRequest {
|
||||
|
||||
private final MultipartHttpServletRequest multipartRequest;
|
||||
|
||||
public DecryptingMultipartRequestWrapper(MultipartHttpServletRequest request,
|
||||
PasswordEnvelopeCodec codec,
|
||||
PasswordKeyStore keyStore,
|
||||
PasswordCryptoProperties properties) {
|
||||
super(request, codec, keyStore, properties);
|
||||
this.multipartRequest = request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<String> getFileNames() {
|
||||
return multipartRequest.getFileNames();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MultipartFile getFile(String name) {
|
||||
return multipartRequest.getFile(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MultipartFile> getFiles(String name) {
|
||||
return multipartRequest.getFiles(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, MultipartFile> getFileMap() {
|
||||
return multipartRequest.getFileMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MultiValueMap<String, MultipartFile> getMultiFileMap() {
|
||||
return multipartRequest.getMultiFileMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMultipartContentType(String paramOrFileName) {
|
||||
return multipartRequest.getMultipartContentType(paramOrFileName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpMethod getRequestMethod() {
|
||||
return multipartRequest.getRequestMethod();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getRequestHeaders() {
|
||||
return multipartRequest.getRequestHeaders();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getMultipartHeaders(String paramOrFileName) {
|
||||
return multipartRequest.getMultipartHeaders(paramOrFileName);
|
||||
}
|
||||
}
|
||||
+11
-3
@@ -18,9 +18,17 @@ import java.util.Set;
|
||||
* 파싱해버려, 본문을 직접 읽는 필터({@code ApiTesterFilter})가 이후 {@code getInputStream()} 을 못 쓴다.
|
||||
* 따라서 값은 {@code getParameter*} 호출 시점에만 건드린다.</p>
|
||||
*
|
||||
* <p>Lucy XSS 필터({@code order = MIN_VALUE + 1})보다 <b>앞</b>에서 이 래퍼가 씌워지므로,
|
||||
* 복호화된 평문이 기존과 똑같이 XSS 이스케이프를 거친다. 순서가 뒤바뀌면 특수문자가 든 비밀번호의
|
||||
* 이스케이프 여부가 달라져 기존 계정 로그인이 깨진다.</p>
|
||||
* <p>Lucy XSS 필터({@code order = MIN_VALUE + 3})보다 <b>앞</b>({@code MIN_VALUE + 2})에서 이 래퍼가
|
||||
* 씌워지므로, 복호화된 평문이 기존과 똑같이 XSS 이스케이프를 거친다. 순서가 뒤바뀌면 특수문자가 든
|
||||
* 비밀번호의 이스케이프 여부가 달라져 기존 계정 로그인이 깨진다.</p>
|
||||
*
|
||||
* <p><b>멀티파트(파일 업로드) 요청 주의</b> — {@code getParameter()} 는 {@code super.getParameter()} 위임
|
||||
* 결과를 복호화하는 방식이라, 이 래퍼가 감싸는 시점에 {@code super} 가 이미 멀티파트 파싱이 끝난
|
||||
* {@code MultipartHttpServletRequest} 여야만 폼 필드(파일이 아닌 text part)를 볼 수 있다 — Spring 의
|
||||
* 멀티파트 파라미터 맵은 자신이 파싱한 값을 곧바로 반환하고 감싸고 있는 원본 요청으로 위임하지 않기
|
||||
* 때문이다. 그래서 {@link PasswordDecryptFilter} 는 MultipartFilter({@code order = MIN_VALUE + 1})
|
||||
* "이후"에 실행되도록 등록돼 있고, 감싸는 대상이 {@code MultipartHttpServletRequest} 면 이 클래스 대신
|
||||
* {@link DecryptingMultipartRequestWrapper}(이 클래스를 상속하며 멀티파트 인터페이스도 구현)를 쓴다.</p>
|
||||
*/
|
||||
@Slf4j
|
||||
public class DecryptingRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
+11
-5
@@ -1,6 +1,7 @@
|
||||
package com.eactive.apim.portal.common.security.passwordcrypto;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartHttpServletRequest;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
@@ -13,9 +14,13 @@ import java.io.IOException;
|
||||
/**
|
||||
* 봉투(ENC1) 파라미터를 평문으로 되돌리는 서블릿 필터.
|
||||
*
|
||||
* <p>{@code PortalConfigSecurity} 에서 order {@code Integer.MIN_VALUE} 로 등록해
|
||||
* Lucy XSS 필터({@code MIN_VALUE + 1})보다 먼저 실행된다. 자세한 이유는
|
||||
* {@link DecryptingRequestWrapper} 주석 참고.</p>
|
||||
* <p>{@code PortalConfigSecurity} 에서 order {@code Integer.MIN_VALUE + 2} 로 등록해
|
||||
* MultipartFilter({@code MIN_VALUE + 1}) "이후", Lucy XSS 필터({@code MIN_VALUE + 3}) "이전"에
|
||||
* 실행된다. 자세한 이유는 {@link DecryptingRequestWrapper} / {@link DecryptingMultipartRequestWrapper}
|
||||
* 주석 참고 — 멀티파트(파일 업로드 동반) 요청은 MultipartFilter 가 만든
|
||||
* {@code MultipartHttpServletRequest} 가 폼 필드를 자체 파싱해 반환하므로, 이 필터가 그보다
|
||||
* 먼저 실행되면 비밀번호 파라미터가 복호화되지 않은 채로 컨트롤러까지 전달된다(법인가입처럼
|
||||
* 파일 첨부와 비밀번호가 같은 폼에 있는 경우 재현).</p>
|
||||
*
|
||||
* <p>기능이 꺼져 있거나 POST 가 아니면 아무것도 하지 않는다. 래퍼는 파라미터를 조회할 때만
|
||||
* 복호화하므로, 봉투가 없는 요청에는 사실상 비용이 없다.</p>
|
||||
@@ -50,8 +55,9 @@ public class PasswordDecryptFilter implements Filter {
|
||||
return;
|
||||
}
|
||||
|
||||
DecryptingRequestWrapper wrapper =
|
||||
new DecryptingRequestWrapper(httpRequest, codec, keyStore, properties);
|
||||
DecryptingRequestWrapper wrapper = httpRequest instanceof MultipartHttpServletRequest
|
||||
? new DecryptingMultipartRequestWrapper((MultipartHttpServletRequest) httpRequest, codec, keyStore, properties)
|
||||
: new DecryptingRequestWrapper(httpRequest, codec, keyStore, properties);
|
||||
try {
|
||||
chain.doFilter(wrapper, response);
|
||||
} finally {
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.eactive.apim.portal.config;
|
||||
|
||||
|
||||
import com.eactive.apim.portal.apps.session.filter.SessionValidationFilter;
|
||||
import com.eactive.apim.portal.common.security.passwordcrypto.DecryptingMultipartRequestWrapper;
|
||||
import com.eactive.apim.portal.common.security.passwordcrypto.PasswordDecryptFilter;
|
||||
import com.navercorp.lucy.security.xss.servletfilter.XssEscapeServletFilter;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -66,25 +67,40 @@ public class PortalConfigSecurity {
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 전송암호화 복호화 필터. Lucy XSS 필터(order = MIN_VALUE + 1)보다 <b>먼저</b> 실행되어야 한다.
|
||||
* 복호화된 평문이 기존과 똑같이 XSS 이스케이프를 거쳐야 특수문자가 든 비밀번호의 해시 비교 결과가
|
||||
* 지금과 동일하게 유지된다. 순서를 뒤집으면 기존 계정 로그인이 깨진다.
|
||||
* 비밀번호 전송암호화 복호화 필터.
|
||||
*
|
||||
* <p>order {@code MIN_VALUE + 2} — {@code PortalConfigWebDispatcherServlet} 의
|
||||
* MultipartFilter({@code MIN_VALUE + 1}) <b>이후</b>, 아래 Lucy XSS 필터
|
||||
* ({@code MIN_VALUE + 3}) <b>이전</b>에 실행되어야 한다.</p>
|
||||
*
|
||||
* <p>MultipartFilter 이후여야 하는 이유: 파일 업로드가 동반된 폼(예: 법인가입의 사업자등록증
|
||||
* 첨부)은 {@code multipart/form-data} 로 제출되는데, Spring 의 {@code MultipartHttpServletRequest}
|
||||
* 는 폼 필드 값을 자체 파싱해 {@code getParameter()} 에서 곧바로 반환하고 감싸고 있는 원본 요청으로
|
||||
* 위임하지 않는다. 이 필터가 MultipartFilter 보다 먼저 실행되어(즉 더 안쪽에서) 요청을 감싸면,
|
||||
* 멀티파트 안의 비밀번호 파라미터는 이 필터의 복호화 로직을 거치지 않고 봉투(ENC1) 문자열 그대로
|
||||
* 컨트롤러까지 전달된다({@link DecryptingMultipartRequestWrapper} 주석 참고).</p>
|
||||
*
|
||||
* <p>Lucy XSS 필터 이전이어야 하는 이유: 복호화된 평문이 기존과 똑같이 XSS 이스케이프를 거쳐야
|
||||
* 특수문자가 든 비밀번호의 해시 비교 결과가 지금과 동일하게 유지된다. 이 순서를 뒤집으면 기존
|
||||
* 계정 로그인이 깨진다.</p>
|
||||
*/
|
||||
@Bean
|
||||
public FilterRegistrationBean<PasswordDecryptFilter> passwordDecryptFilterRegistration(
|
||||
PasswordDecryptFilter filter) {
|
||||
FilterRegistrationBean<PasswordDecryptFilter> registrationBean = new FilterRegistrationBean<>(filter);
|
||||
registrationBean.setOrder(Integer.MIN_VALUE);
|
||||
registrationBean.setOrder(Integer.MIN_VALUE + 2);
|
||||
registrationBean.addUrlPatterns("/*");
|
||||
return registrationBean;
|
||||
}
|
||||
|
||||
// order MIN_VALUE + 3 — 위 PasswordDecryptFilter(MIN_VALUE + 2) 다음에 실행되어야
|
||||
// 복호화된 평문이 XSS 이스케이프를 거친다(순서를 뒤집으면 기존 계정 로그인이 깨짐).
|
||||
@Bean
|
||||
public FilterRegistrationBean<XssEscapeServletFilter> xssFilterRegistrationBean() {
|
||||
FilterRegistrationBean<XssEscapeServletFilter> registrationBean = new FilterRegistrationBean<>();
|
||||
XssEscapeServletFilter xssEscapeServletFilter = new XssEscapeServletFilter();
|
||||
registrationBean.setFilter(xssEscapeServletFilter);
|
||||
registrationBean.setOrder(Integer.MIN_VALUE + 1);
|
||||
registrationBean.setOrder(Integer.MIN_VALUE + 3);
|
||||
registrationBean.addUrlPatterns("/*");
|
||||
return registrationBean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user