기존 웹 애플리케이션이 Tomcat 4.1에서 실행 중입니다. 페이지에 XSS 문제가 있지만 소스를 수정할 수 없습니다. 페이지에서보기 전에 매개 변수를 삭제하기 위해 서블릿 필터를 작성하기로 결정했습니다.
다음과 같은 Filter 클래스를 작성하고 싶습니다.
import java.io.*;
import javax.servlet.*;
public final class XssFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException
{
String badValue = request.getParameter("dangerousParamName");
String goodValue = sanitize(badValue);
request.setParameter("dangerousParamName", goodValue);
chain.doFilter(request, response);
}
public void destroy() {
}
public void init(FilterConfig filterConfig) {
}
}
그러나 ServletRequest.setParameter
존재하지 않습니다.
요청을 체인으로 전달하기 전에 요청 매개 변수의 값을 어떻게 변경할 수 있습니까?
답변
앞서 언급했듯이 HttpServletRequest
setParameter 메서드가 없습니다. 이는 클래스가 클라이언트에서 온 요청을 나타내며 매개 변수를 수정해도이를 나타내지 않기 때문에 의도적입니다.
한 가지 해결책은 HttpServletRequestWrapper
하나의 요청을 다른 요청으로 래핑 할 수 있는 클래스 를 사용하는 것입니다. 이를 하위 클래스로 분류하고 getParameter
메서드를 재정 의하여 삭제 된 값을 반환 할 수 있습니다. 그런 다음 chain.doFilter
원래 요청 대신 래핑 된 요청을에 전달할 수 있습니다 .
약간 못 생겼지 만 서블릿 API가해야한다고 말하는 것입니다. 다른 것을에 전달하려고하면 doFilter
일부 서블릿 컨테이너가 사양을 위반했다고 불평하고 처리를 거부합니다.
더 우아한 솔루션은 더 많은 작업 입니다. 매개 변수 대신 요청 속성을 예상하도록 매개 변수를 처리하는 원래 서블릿 / JSP를 수정하십시오 . 필터는 매개 변수를 검사하고이를 request.setAttribute
삭제하고 삭제 된 값으로 속성 (사용 )을 설정합니다 . 서브 클래 싱이나 스푸핑은 없지만 애플리케이션의 다른 부분을 수정해야합니다.
답변
기록을 위해 여기에 내가 쓴 수업이 있습니다.
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
public final class XssFilter implements Filter {
static class FilteredRequest extends HttpServletRequestWrapper {
/* These are the characters allowed by the Javascript validation */
static String allowedChars = "+-0123456789#*";
public FilteredRequest(ServletRequest request) {
super((HttpServletRequest)request);
}
public String sanitize(String input) {
String result = "";
for (int i = 0; i < input.length(); i++) {
if (allowedChars.indexOf(input.charAt(i)) >= 0) {
result += input.charAt(i);
}
}
return result;
}
public String getParameter(String paramName) {
String value = super.getParameter(paramName);
if ("dangerousParamName".equals(paramName)) {
value = sanitize(value);
}
return value;
}
public String[] getParameterValues(String paramName) {
String values[] = super.getParameterValues(paramName);
if ("dangerousParamName".equals(paramName)) {
for (int index = 0; index < values.length; index++) {
values[index] = sanitize(values[index]);
}
}
return values;
}
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
chain.doFilter(new FilteredRequest(request), response);
}
public void destroy() {
}
public void init(FilterConfig filterConfig) {
}
}
답변
HttpServletRequestWrapper
입력의 삭제 된 버전을 반환하는 getParameter () 메서드로 하위 계산하는 간단한 클래스를 작성합니다 . 그런 다음의 인스턴스를 전달 HttpServletRequestWrapper
하는 Filter.doChain()
대신 직접 요청 개체의.
답변
동일한 문제가 발생했습니다 (필터의 HTTP 요청에서 매개 변수 변경). 나는 ThreadLocal<String>
. 에서 Filter
내가 가진 :
class MyFilter extends Filter {
public static final ThreadLocal<String> THREAD_VARIABLE = new ThreadLocal<>();
public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
THREAD_VARIABLE.set("myVariableValue");
chain.doFilter(request, response);
}
}
내 요청 프로세서 ( HttpServlet
, JSF 컨트롤러 또는 기타 HTTP 요청 프로세서)에서 현재 스레드 값을 다시 가져옵니다.
...
String myVariable = MyFilter.THREAD_VARIABLE.get();
...
장점 :
- HTTP 매개 변수를 전달하는 것보다 더 다양합니다 (POJO 객체를 전달할 수 있음).
- 약간 더 빠름 (변수 값을 추출하기 위해 URL을 구문 분석 할 필요 없음)
HttpServletRequestWrapper
상용구 보다 더 우아한- 변수 범위는 HTTP 요청보다 더 넓습니다 (를 수행 할 때 사용하는 범위
request.setAttribute(String,Object)
, 즉 다른 필터의 변수에 액세스 할 수 있음).
단점 :
- 이 방법은 필터를 처리하는 스레드가 HTTP 요청을 처리하는 스레드와 동일한 경우에만 사용할 수 있습니다 (내가 아는 모든 Java 기반 서버의 경우). 따라서 다음과 같은 경우에는 작동하지 않습니다.
- HTTP 리디렉션 수행 (브라우저가 새 HTTP 요청을 수행하고 동일한 스레드에서 처리된다는 것을 보장 할 방법이 없기 때문)
- 별도의 데이터 처리 스레드 사용하는 경우를 예를 들어,
java.util.stream.Stream.parallel
,java.util.concurrent.Future
,java.lang.Thread
.
- 요청 프로세서 / 애플리케이션을 수정할 수 있어야합니다.
몇 가지 참고 사항 :
-
서버에는 HTTP 요청을 처리하기위한 스레드 풀이 있습니다. 이것이 풀이 기 때문에 :
- 이 스레드 풀의 스레드는 많은 HTTP 요청을 처리하지만 한 번에 하나씩 만 처리합니다 (따라서 사용 후 변수를 정리하거나 각 HTTP 요청에 대해 정의해야 함 =
if (value!=null) { THREAD_VARIABLE.set(value);}
값을 재사용 할 것이므로 코드에주의를 기울여야 합니다.value
null 일 때 이전 HTTP 요청에서 : 부작용이 보장됨). - 두 개의 요청이 동일한 스레드에서 처리된다는 보장은 없습니다 (그럴 수 있지만 보장 할 수 없음). 한 요청에서 다른 요청으로 사용자 데이터를 유지해야하는 경우 사용하는 것이 좋습니다.
HttpSession.setAttribute()
- 이 스레드 풀의 스레드는 많은 HTTP 요청을 처리하지만 한 번에 하나씩 만 처리합니다 (따라서 사용 후 변수를 정리하거나 각 HTTP 요청에 대해 정의해야 함 =
- JEE는
@RequestScoped
내부적으로를 사용 하지만를ThreadLocal
사용하는ThreadLocal
것이 더 다양합니다. 비 JEE / CDI 컨테이너 (예 : 다중 스레드 JRE 응용 프로그램)에서 사용할 수 있습니다.
답변
이것이 내가 한 일입니다.
//import ../../Constants;
public class RequestFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(RequestFilter.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
try {
CustomHttpServletRequest customHttpServletRequest = new CustomHttpServletRequest((HttpServletRequest) servletRequest);
filterChain.doFilter(customHttpServletRequest, servletResponse);
} finally {
//do something here
}
}
@Override
public void destroy() {
}
public static Map<String, String[]> ADMIN_QUERY_PARAMS = new HashMap<String, String[]>() {
{
put("diagnostics", new String[]{"false"});
put("skipCache", new String[]{"false"});
}
};
/*
This is a custom wrapper over the `HttpServletRequestWrapper` which
overrides the various header getter methods and query param getter methods.
Changes to the request pojo are
=> A custom header is added whose value is a unique id
=> Admin query params are set to default values in the url
*/
private class CustomHttpServletRequest extends HttpServletRequestWrapper {
public CustomHttpServletRequest(HttpServletRequest request) {
super(request);
//create custom id (to be returned) when the value for a
//particular header is asked for
internalRequestId = RandomStringUtils.random(10, true, true) + "-local";
}
public String getHeader(String name) {
String value = super.getHeader(name);
if(Strings.isNullOrEmpty(value) && isRequestIdHeaderName(name)) {
value = internalRequestId;
}
return value;
}
private boolean isRequestIdHeaderName(String name) {
return Constants.RID_HEADER.equalsIgnoreCase(name) || Constants.X_REQUEST_ID_HEADER.equalsIgnoreCase(name);
}
public Enumeration<String> getHeaders(String name) {
List<String> values = Collections.list(super.getHeaders(name));
if(values.size()==0 && isRequestIdHeaderName(name)) {
values.add(internalRequestId);
}
return Collections.enumeration(values);
}
public Enumeration<String> getHeaderNames() {
List<String> names = Collections.list(super.getHeaderNames());
names.add(Constants.RID_HEADER);
names.add(Constants.X_REQUEST_ID_HEADER);
return Collections.enumeration(names);
}
public String getParameter(String name) {
if (ADMIN_QUERY_PARAMS.get(name) != null) {
return ADMIN_QUERY_PARAMS.get(name)[0];
}
return super.getParameter(name);
}
public Map<String, String[]> getParameterMap() {
Map<String, String[]> paramsMap = new HashMap<>(super.getParameterMap());
for (String paramName : ADMIN_QUERY_PARAMS.keySet()) {
if (paramsMap.get(paramName) != null) {
paramsMap.put(paramName, ADMIN_QUERY_PARAMS.get(paramName));
}
}
return paramsMap;
}
public String[] getParameterValues(String name) {
if (ADMIN_QUERY_PARAMS.get(name) != null) {
return ADMIN_QUERY_PARAMS.get(name);
}
return super.getParameterValues(name);
}
public String getQueryString() {
Map<String, String[]> map = getParameterMap();
StringBuilder builder = new StringBuilder();
for (String param: map.keySet()) {
for (String value: map.get(param)) {
builder.append(param).append("=").append(value).append("&");
}
}
builder.deleteCharAt(builder.length() - 1);
return builder.toString();
}
}
}
답변
여기에 귀하의 모든 발언을 바탕으로 저에게 도움이 된 제안이 있습니다.
private final class CustomHttpServletRequest extends HttpServletRequestWrapper {
private final Map<String, String[]> queryParameterMap;
private final Charset requestEncoding;
public CustomHttpServletRequest(HttpServletRequest request) {
super(request);
queryParameterMap = getCommonQueryParamFromLegacy(request.getParameterMap());
String encoding = request.getCharacterEncoding();
requestEncoding = (encoding != null ? Charset.forName(encoding) : StandardCharsets.UTF_8);
}
private final Map<String, String[]> getCommonQueryParamFromLegacy(Map<String, String[]> paramMap) {
Objects.requireNonNull(paramMap);
Map<String, String[]> commonQueryParamMap = new LinkedHashMap<>(paramMap);
commonQueryParamMap.put(CommonQueryParams.PATIENT_ID, new String[] { paramMap.get(LEGACY_PARAM_PATIENT_ID)[0] });
commonQueryParamMap.put(CommonQueryParams.PATIENT_BIRTHDATE, new String[] { paramMap.get(LEGACY_PARAM_PATIENT_BIRTHDATE)[0] });
commonQueryParamMap.put(CommonQueryParams.KEYWORDS, new String[] { paramMap.get(LEGACY_PARAM_STUDYTYPE)[0] });
String lowerDateTime = null;
String upperDateTime = null;
try {
String studyDateTime = new SimpleDateFormat("yyyy-MM-dd").format(new SimpleDateFormat("dd-MM-yyyy").parse(paramMap.get(LEGACY_PARAM_STUDY_DATE_TIME)[0]));
lowerDateTime = studyDateTime + "T23:59:59";
upperDateTime = studyDateTime + "T00:00:00";
} catch (ParseException e) {
LOGGER.error("Can't parse StudyDate from query parameters : {}", e.getLocalizedMessage());
}
commonQueryParamMap.put(CommonQueryParams.LOWER_DATETIME, new String[] { lowerDateTime });
commonQueryParamMap.put(CommonQueryParams.UPPER_DATETIME, new String[] { upperDateTime });
legacyQueryParams.forEach(commonQueryParamMap::remove);
return Collections.unmodifiableMap(commonQueryParamMap);
}
@Override
public String getParameter(String name) {
String[] params = queryParameterMap.get(name);
return params != null ? params[0] : null;
}
@Override
public String[] getParameterValues(String name) {
return queryParameterMap.get(name);
}
@Override
public Map<String, String[]> getParameterMap() {
return queryParameterMap; // unmodifiable to uphold the interface contract.
}
@Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(queryParameterMap.keySet());
}
@Override
public String getQueryString() {
// @see : https://stackoverflow.com/a/35831692/9869013
// return queryParameterMap.entrySet().stream().flatMap(entry -> Stream.of(entry.getValue()).map(value -> entry.getKey() + "=" + value)).collect(Collectors.joining("&")); // without encoding !!
return queryParameterMap.entrySet().stream().flatMap(entry -> encodeMultiParameter(entry.getKey(), entry.getValue(), requestEncoding)).collect(Collectors.joining("&"));
}
private Stream<String> encodeMultiParameter(String key, String[] values, Charset encoding) {
return Stream.of(values).map(value -> encodeSingleParameter(key, value, encoding));
}
private String encodeSingleParameter(String key, String value, Charset encoding) {
return urlEncode(key, encoding) + "=" + urlEncode(value, encoding);
}
private String urlEncode(String value, Charset encoding) {
try {
return URLEncoder.encode(value, encoding.name());
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException("Cannot url encode " + value, e);
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
throw new UnsupportedOperationException("getInputStream() is not implemented in this " + CustomHttpServletRequest.class.getSimpleName() + " wrapper");
}
}
참고 : queryString ()은 각 KEY의 모든 값을 처리해야하며 필요한 경우 고유 한 매개 변수 값을 추가 할 때 encodeUrl ()을 잊지 마십시오.
제한적으로 request.getParameterMap () 또는 request.getReader ()를 호출하는 메서드를 호출하고 읽기를 시작하면 request.setCharacterEncoding (…)에 대한 추가 호출을 방지 할 수 있습니다.
답변
Sanitization에 정규식 을 사용할 수 있습니다 . chain.doFilter (request, response) 메소드를 호출 하기 전에 필터 내 에서이 코드를 호출하십시오. 다음은 샘플 코드입니다.
for (Enumeration en = request.getParameterNames(); en.hasMoreElements(); ) {
String name = (String)en.nextElement();
String values[] = request.getParameterValues(name);
int n = values.length;
for(int i=0; i < n; i++) {
values[i] = values[i].replaceAll("[^\\dA-Za-z ]","").replaceAll("\\s+","+").trim();
}
}