แก้ไขพารามิเตอร์การร้องขอด้วยตัวกรอง servlet


114

เว็บแอปพลิเคชันที่มีอยู่กำลังทำงานบน Tomcat 4.1 มีปัญหา XSS กับเพจ แต่ฉันไม่สามารถแก้ไขแหล่งที่มาได้ ฉันตัดสินใจที่จะเขียนตัวกรอง servlet เพื่อล้างพารามิเตอร์ก่อนที่หน้าจะเห็น

ฉันต้องการเขียนคลาสตัวกรองดังนี้:

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แทนคำขอเดิมได้

มันค่อนข้างน่าเกลียด แต่นั่นคือสิ่งที่ servlet API บอกว่าคุณควรทำ หากคุณพยายามส่งผ่านสิ่งอื่นใดไปยังdoFilterคอนเทนเนอร์ servlet บางตัวจะบ่นว่าคุณละเมิดข้อมูลจำเพาะและจะปฏิเสธที่จะจัดการกับมัน

โซลูชันที่หรูหรากว่านั้นทำงานได้มากขึ้น - แก้ไข servlet / JSP ดั้งเดิมที่ประมวลผลพารามิเตอร์เพื่อให้คาดหวังแอ็ตทริบิวต์คำร้องขอแทนที่จะเป็นพารามิเตอร์ ตัวกรองจะตรวจสอบพารามิเตอร์ทำความสะอาดและตั้งค่าแอตทริบิวต์ (โดยใช้request.setAttribute) ด้วยค่าที่ผ่านการฆ่าเชื้อ ไม่มีคลาสย่อยไม่มีการปลอมแปลง แต่คุณต้องแก้ไขส่วนอื่น ๆ ของแอปพลิเคชันของคุณ


6
HttpServletRequestWrapper นั้นยอดเยี่ยมมาก ฉันไม่เคยรู้ว่ามันมีอยู่จริง ขอบคุณ!
Jeremy Stein

3
ขอบคุณสำหรับทางเลือกในการตั้งค่าแอตทริบิวต์! เห็นโค้ดตัวอย่างโดยใช้การร้องขอและการห่อการตอบกลับใน Head First Servlets และ JSPs และไม่อยากเชื่อเลยว่าข้อมูลจำเพาะจะผลักดันให้ผู้คนดำเนินการในลักษณะนั้น
Kevin

ฉันได้เข้าถึงค่าของฉันในคอนโทรลเลอร์แล้วและฉันได้ตั้งค่าพารามิเตอร์ท่า (อีเมลและพาส) ... ตอนนี้ฉันจะแทนที่ค่าใน servlet ของฉันได้อย่างไร <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
จุดดีทอม ในกรณีนี้ฉันตรวจสอบแล้วและพบว่ามันไม่ได้ถูกเรียก แต่ฉันควรจะเพิ่มสิ่งนั้นเพื่อความสมบูรณ์และเพื่อประโยชน์ของบุคคลต่อไป ขอบคุณ!
Jeremy Stein

13
ดูเหมือนว่าฉันจะเป็นเจเรมีคนถัดไป ฉันพบโพสต์นี้เมื่อมองหาตัวเลือกในการแก้ไขข้อมูลที่ส่งผ่านจากแอปพลิเคชันภายนอกไปยัง servlet ของบุคคลที่สาม ในกรณีของฉัน servlet ไม่ได้เรียกใช้ HTTPServletRequest.getParameter (), getParameterMap () หรือแม้แต่ getAttribute () เพื่อรับข้อมูลคำขอดังนั้นจากการลองผิดลองถูกฉันจึงพิจารณาว่า servlet กำลังเรียกใช้ HTTPServletRequest.getInputStream () และ getQueryString () คำแนะนำของฉันสำหรับทุกคนที่พยายามทำงานนี้สำหรับ servlets แบบปิดคือการรวมทุก accessor เดียวใน HTTPServletRequest เพื่อให้เข้าใจว่าเกิดอะไรขึ้น
Fred Sobotka

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.Futurejava.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 แบบมัลติเธรด)

เป็นความคิดที่ดีที่จะตั้งค่าพารามิเตอร์ในขอบเขตของเธรดหรือไม่ คำขอหลายรายการจะเห็นเธรดเดียวกันหรือไม่ (ฉันคิดว่าไม่)
Zachary Craig

เป็นความคิดที่ดีหรือไม่ = ใช่ (แต่คุณต้องรู้ว่าคุณกำลังทำอะไรอยู่ แต่ JEE @RequestScopedก็ทำเหมือนกันภายใน) หลายคำขอจะเห็นเธรดเดียวกัน = ไม่ (หรืออย่างน้อยคุณก็ไม่มีการรับประกัน) ฉันได้แก้ไขคำตอบเพื่อให้ประเด็นเหล่านี้แม่นยำแล้ว
Julien Kronegg

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

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 () ต้องการประมวลผลค่าทั้งหมดสำหรับแต่ละ KEY และอย่าลืม encodeUrl () เมื่อเพิ่มค่าพารามิเตอร์ของคุณเองหากจำเป็น

ตามข้อ จำกัด ถ้าคุณเรียก request.getParameterMap () หรือวิธีการใด ๆ ที่เรียก request.getReader () และเริ่มอ่านคุณจะป้องกันไม่ให้มีการเรียกไปยัง request.setCharacterEncoding (... )


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
        }
    }
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.