Ubah parameter permintaan dengan filter servlet

114

Aplikasi web yang ada sedang berjalan di Tomcat 4.1. Ada masalah XSS dengan halaman, tetapi saya tidak dapat mengubah sumbernya. Saya telah memutuskan untuk menulis filter servlet untuk membersihkan parameter sebelum dilihat oleh halaman.

Saya ingin menulis kelas Filter seperti ini:

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) {
  }
}

Tapi ServletRequest.setParametertidak ada.

Bagaimana cara mengubah nilai parameter permintaan sebelum meneruskan permintaan ke rantai?

Jeremy Stein
sumber
HttpServletRequestWrapper memiliki banyak API yang didefinisikan. Saya mencoba memahami setiap API secara bermakna. Javadoc tidak membantu memahami API seperti 'userinRole', 'getPrincipal'etx., Di mana tepatnya saya bisa mendapatkan bantuan?
sskumar86

Jawaban:

132

Seperti yang Anda catat HttpServletRequesttidak memiliki metode setParameter. Ini disengaja, karena kelas mewakili permintaan yang berasal dari klien, dan memodifikasi parameter tidak akan mewakili itu.

Salah satu solusinya adalah menggunakan HttpServletRequestWrapperkelas, yang memungkinkan Anda menggabungkan satu permintaan dengan yang lain. Anda dapat membuat subkelas itu, dan mengganti getParametermetode untuk mengembalikan nilai bersih Anda. Anda kemudian bisa meneruskan permintaan yang dibungkus itu ke chain.doFilteralih-alih ke permintaan asli.

Ini agak jelek, tapi itulah yang menurut API servlet harus Anda lakukan. Jika Anda mencoba meneruskan apa pun ke doFilter, beberapa kontainer servlet akan mengeluh bahwa Anda telah melanggar spesifikasi, dan akan menolak untuk menanganinya.

Solusi yang lebih elegan adalah lebih banyak pekerjaan - modifikasi servlet / JSP asli yang memproses parameter, sehingga mengharapkan atribut permintaan daripada parameter. Filter memeriksa parameter, membersihkannya, dan menyetel atribut (menggunakan request.setAttribute) dengan nilai yang disanitasi. Tanpa subclass, tidak ada spoofing, tetapi mengharuskan Anda untuk memodifikasi bagian lain dari aplikasi Anda.

skaffman
sumber
6
HttpServletRequestWrapper sangat bagus; Saya tidak pernah tahu itu ada. Terima kasih!
Jeremy Stein
3
Terima kasih atas alternatif pengaturan atribut! Melihat kode sampel menggunakan pembungkus permintaan dan respons di Head First Servlets dan JSPs dan tidak percaya spesifikasi tersebut mendorong orang untuk melakukan hal-hal seperti itu.
Kevin
Saya telah menjangkau dengan nilai-nilai saya dalam pengontrol dan saya telah menetapkan parameter itu (email dan pass) ... sekarang bagaimana saya bisa mengganti nilai di servlet saya <property name="username" value="[email protected]" /> //Change email on logging in <property name="password" value="*********" />//Change Password on logging in
UmaShankar
73

Sebagai catatan, inilah kelas yang akhirnya saya tulis:

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) {
    }
}
Jeremy Stein
sumber
5
Anda mungkin juga perlu memperhitungkan metode getParameterMap. Mungkin membuang dan pengecualian yang tidak didukung agar tidak ada komponen yang menggunakan metode ini dan melewati logika sanitasi.
Tom
1
Poin yang bagus, Tom. Dalam kasus khusus ini, saya memeriksa dan menemukan itu tidak dipanggil, tetapi saya harus menambahkan itu untuk kelengkapan dan demi orang berikutnya. Terima kasih!
Jeremy Stein
13
Sepertinya aku orang berikutnya, Jeremy. Saya menemukan posting ini ketika mencari opsi untuk mengubah data yang dikirimkan dari aplikasi luar ke servlet pihak ketiga. Dalam kasus saya, servlet tidak memanggil HTTPServletRequest.getParameter (), getParameterMap (), atau bahkan getAttribute () untuk mendapatkan data permintaan, jadi, melalui trial and error, saya memutuskan bahwa servlet memanggil HTTPServletRequest.getInputStream () dan getQueryString (). Saran saya kepada siapa pun yang mencoba tugas ini untuk servlet tertutup adalah membungkus setiap pengakses di HTTPServletRequest untuk memahami apa yang sebenarnya terjadi
Fred Sobotka
3
Untuk SrpingMVC, Anda perlu mengganti getParameterValues ​​() untuk mengelabui Spring. RequestParamMethodArgumentResolver.resovleName () menggunakan metode itu, jadi Anda akan mendapatkan MissingServletRequestParameterException tanpa mengganti. Diuji pada Spring Boot 1.2.6 dengan spring-web 4.1.7.
barryku
10

Tulis kelas sederhana yang disubkalsikan HttpServletRequestWrapperdengan metode getParameter () yang mengembalikan versi masukan yang sudah dibersihkan. Kemudian teruskan instance Anda HttpServletRequestWrapperke Filter.doChain()alih-alih objek permintaan secara langsung.

Asaf
sumber
1

Saya memiliki masalah yang sama (mengubah parameter dari permintaan HTTP di Filter). Saya akhirnya menggunakan file ThreadLocal<String>. Di FilterSaya punya:

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);
    }
}

Dalam pemroses permintaan saya ( HttpServlet, pengontrol JSF atau pemroses permintaan HTTP lainnya), saya mendapatkan kembali nilai utas saat ini:

...
String myVariable = MyFilter.THREAD_VARIABLE.get();
...

Keuntungan:

  • lebih fleksibel daripada meneruskan parameter HTTP (Anda dapat meneruskan objek POJO)
  • sedikit lebih cepat (tidak perlu mengurai URL untuk mengekstrak nilai variabel)
  • lebih elegan dari pada HttpServletRequestWrapperboilerplate
  • ruang lingkup variabel lebih luas daripada hanya permintaan HTTP (ruang lingkup yang Anda miliki saat melakukannya request.setAttribute(String,Object), yaitu Anda dapat mengakses variabel di penyaringan lain.

Kekurangan:

  • Anda dapat menggunakan metode ini hanya jika utas yang memproses filter sama dengan utas yang memproses permintaan HTTP (ini terjadi di semua server berbasis Java yang saya tahu). Akibatnya, ini tidak akan berhasil saat
    • melakukan pengalihan HTTP (karena browser melakukan permintaan HTTP baru dan tidak ada cara untuk menjamin bahwa itu akan diproses oleh utas yang sama)
    • pengolahan data dalam thread terpisah , misalnya saat menggunakan java.util.stream.Stream.parallel, java.util.concurrent.Future, java.lang.Thread.
  • Anda harus dapat mengubah prosesor / aplikasi permintaan

Beberapa catatan tambahan:

  • Server memiliki kumpulan Thread untuk memproses permintaan HTTP. Karena ini adalah pool:

    1. utas dari kumpulan utas ini akan memproses banyak permintaan HTTP, tetapi hanya satu per satu (jadi Anda perlu membersihkan variabel Anda setelah penggunaan atau menentukannya untuk setiap permintaan HTTP = perhatikan kode seperti if (value!=null) { THREAD_VARIABLE.set(value);}karena Anda akan menggunakan kembali nilainya dari permintaan HTTP sebelumnya ketika valuenull: efek samping dijamin).
    2. Tidak ada jaminan bahwa dua permintaan akan diproses oleh utas yang sama (mungkin demikian, tetapi Anda tidak memiliki jaminan). Jika Anda perlu menyimpan data pengguna dari satu permintaan ke permintaan lainnya, akan lebih baik untuk digunakanHttpSession.setAttribute()
  • JEE secara @RequestScopedinternal menggunakan a ThreadLocal, tetapi menggunakan the ThreadLocallebih serbaguna: Anda dapat menggunakannya dalam wadah non JEE / CDI (misalnya dalam aplikasi JRE multithread)
Julien Kronegg
sumber
Apakah benar-benar ide yang bagus untuk menyetel parameter dalam cakupan thread? Akankah beberapa permintaan melihat utas yang sama? (Saya berasumsi tidak)
Zachary Craig
Apakah itu ide yang bagus = ya (tetapi Anda perlu tahu apa yang Anda lakukan, bagaimanapun JEE @RequestScopedmelakukan hal yang sama secara internal). Akankah beberapa permintaan melihat utas yang sama = tidak (atau setidaknya Anda tidak memiliki jaminan). Saya telah mengedit jawabannya dengan tepat poin-poin ini.
Julien Kronegg
1

Inilah yang akhirnya saya lakukan

//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();
        }
    }
}
agen47
sumber
1

Berdasarkan semua komentar Anda, inilah proposal saya yang berhasil untuk saya:

 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");
        }

    }

catatan: queryString () perlu memproses SEMUA nilai untuk setiap KEY dan jangan lupa untuk encodeUrl () saat menambahkan nilai param Anda sendiri, jika diperlukan

Sebagai batasan, jika Anda memanggil request.getParameterMap () atau metode apa pun yang akan memanggil request.getReader () dan mulai membaca, Anda akan mencegah panggilan lebih lanjut ke request.setCharacterEncoding (...)

benGiacomo
sumber
0

Anda dapat menggunakan Ekspresi Reguler untuk Sanitasi. Di dalam filter sebelum memanggil metode chain.doFilter (request, response) , panggil kode ini. Ini Contoh Kode:

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();   
    }
}
Ajinkya Peshave
sumber
1
Anda tidak mengubah parameter permintaan asli dengan cara ini, tetapi pada salinannya.
Mehdi
-1

Coba request.setAttribute("param",value);. Ini bekerja dengan baik untukku.

Silakan temukan contoh kode ini:

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
        }
    }
Tas Ankur
sumber