Изменить параметр запроса с помощью фильтра сервлета

114

Существующее веб-приложение работает на Tomcat 4.1. На странице есть проблема XSS, но я не могу изменить источник. Я решил написать фильтр сервлетов, чтобы очистить параметр до того, как его увидит страница.

Я хотел бы написать такой класс фильтра:

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не существует.

Как я могу изменить значение параметра запроса перед передачей запроса по цепочке?

Джереми Штайн
источник
HttpServletRequestWrapper имеет множество определенных API. Я пытаюсь осмысленно понять каждый API. Javadoc не помогает понять API, такие как 'userinRole', 'getPrincipal'etx. Где именно я могу получить некоторую помощь?
sskumar86

Ответы:

132

Как вы отметили HttpServletRequest, не имеет метода setParameter. Это сделано умышленно, поскольку класс представляет запрос в том виде, в каком он пришел от клиента, и изменение параметра не будет отражать этого.

Одно из решений - использовать HttpServletRequestWrapperкласс, который позволяет объединить один запрос с другим. Вы можете создать подкласс и переопределить getParameterметод для возврата вашего очищенного значения. Затем вы можете передать этот завернутый запрос chain.doFilterвместо исходного запроса.

Это немного некрасиво, но именно это говорит API сервлетов. Если вы попытаетесь передать что-либо еще doFilter, некоторые контейнеры сервлетов будут жаловаться, что вы нарушили спецификацию, и откажутся обрабатывать это.

Более элегантное решение - больше работы - измените исходный сервлет / JSP, который обрабатывает параметр, так, чтобы он ожидал атрибут запроса вместо параметра. Фильтр проверяет параметр, очищает его и устанавливает атрибут (using request.setAttribute) с очищенным значением. Без подкласса, без спуфинга, но требует изменения других частей вашего приложения.

Скаффман
источник
6
HttpServletRequestWrapper замечательный; Я никогда не знал, что он существует. Спасибо!
Джереми Штайн,
3
Спасибо за альтернативу установки атрибутов! Увидел образец кода, использующий обертки запросов и ответов в сервлетах Head First и JSP, и не мог поверить, что спецификация подталкивает людей к таким вещам.
Кевин
Я обратился со своими значениями в контроллере, и я установил параметр tha (электронная почта и пароль) ... теперь, как я могу заменить значения в моем сервлете <property name="username" value="somemail@gmail.com" /> //Change email on logging in <property name="password" value="*********" />//Change Password on logging in
UmaShankar
73

Для записи, вот класс, который я написал:

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) {
    }
}
Джереми Штайн
источник
5
Вам также может потребоваться учитывать метод getParameterMap. Может быть, бросить и неподдерживаемое исключение, чтобы никакие компоненты не использовали метод и пропустили логику очистки.
Том
1
Хорошая мысль, Том. В этом конкретном случае я проверил и обнаружил, что он не вызывается, но я должен был добавить это для полноты и ради следующего человека. Спасибо!
Джереми Стейн,
13
Похоже, я следующий человек, Джереми. Я нашел этот пост, когда искал варианты изменения данных, передаваемых из внешнего приложения в сторонний сервлет. В моем случае сервлет не вызывал HTTPServletRequest.getParameter (), getParameterMap () или даже getAttribute () для получения данных запроса, поэтому методом проб и ошибок я определил, что сервлет вызывает HTTPServletRequest.getInputStream () и getQueryString (). Мой совет всем, кто пытается выполнить эту задачу для закрытых сервлетов, - обернуть каждый аксессор в HTTPServletRequest, чтобы понять, что на самом деле происходит
Фред Соботка,
3
Для SrpingMVC вам нужно будет переопределить getParameterValues ​​(), чтобы обмануть Spring. RequestParamMethodArgumentResolver.resovleName () использует этот метод, поэтому вы получите исключение MissingServletRequestParameterException без переопределения. Проверено на Spring Boot 1.2.6 с Spring-web 4.1.7.
barryku
10

Напишите простой класс, который выполняет субвычисление HttpServletRequestWrapperс помощью метода getParameter (), который возвращает очищенную версию ввода. Затем передать экземпляр вашей , HttpServletRequestWrapperчтобы Filter.doChain()вместо объекта запроса напрямую.

Асаф
источник
1

У меня была такая же проблема (изменение параметра из 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-запросов. Поскольку это пул:

    1. Поток из этого пула потоков будет обрабатывать множество HTTP-запросов, но только по одному (поэтому вам нужно либо очистить вашу переменную после использования, либо определить ее для каждого HTTP-запроса = обратите внимание на код, например, if (value!=null) { THREAD_VARIABLE.set(value);}потому что вы будете повторно использовать значение из предыдущего HTTP-запроса при valueнулевом значении: побочные эффекты гарантированы).
    2. Нет гарантии, что два запроса будут обработаны одним и тем же потоком (это может быть так, но у вас нет гарантии). Если вам нужно сохранить данные пользователя от одного запроса к другому, лучше использоватьHttpSession.setAttribute()
  • JEE @RequestScopedвнутренне использует a ThreadLocal, но его использование ThreadLocalболее универсально: вы можете использовать его в контейнерах, отличных от JEE / CDI (например, в многопоточных приложениях JRE)
Жюльен Кронегг
источник
Действительно ли это хорошая идея установить параметр в области действия потока? Будет ли когда-либо отображаться один и тот же поток при нескольких запросах? (Полагаю, что нет)
Захари Крейг
Это хорошая идея = да (но вам нужно знать, что вы делаете, в любом случае JEE @RequestScopedвнутренне делает то же самое). Будет ли несколько запросов видеть один и тот же поток = нет (или, по крайней мере, у вас нет гарантии). Я отредактировал ответ, чтобы уточнить эти моменты.
Жюльен Кронегг
1

Это то, чем я закончил

//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();
        }
    }
}
agent47
источник
1

Основываясь на всех ваших замечаниях, вот мое предложение, которое сработало для меня:

 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 () требует обработки ВСЕХ значений для каждого КЛЮЧА и не забывайте encodeUrl () при добавлении собственных значений параметров, если это необходимо

В качестве ограничения, если вы вызовете request.getParameterMap () или любой метод, который вызовет request.getReader () и начнет чтение, вы предотвратите любые дальнейшие вызовы request.setCharacterEncoding (...)

BenGiacomo
источник
0

Вы можете использовать регулярное выражение для очистки. Внутри фильтра перед вызовом метода chain.doFilter (запрос, ответ) вызовите этот код. Вот пример кода:

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();   
    }
}
Аджинкья Пешаве
источник
1
Таким образом вы изменяете не исходные параметры запроса, а его копию.
Mehdi
-1

Попробуй request.setAttribute("param",value);. У меня все сработало.

Найдите этот пример кода:

private void sanitizePrice(ServletRequest request){
        if(request.getParameterValues ("price") !=  null){
            String price[] = request.getParameterValues ("price");

            for(int i=0;i<price.length;i++){
                price[i] = price[i].replaceAll("[^\\dA-Za-z0-9- ]", "").trim();
                System.out.println(price[i]);
            }
            request.setAttribute("price", price);
            //request.getParameter("numOfBooks").re
        }
    }
Сумка Анкур
источник