
package gov.va.med.pharmacy.web.filter;

import static gov.va.med.pharmacy.utility.ESAPIValidationType.LOG_FORGING;
import static gov.va.med.pharmacy.utility.ESAPIValidator.validateStringInput;

import java.text.Normalizer;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpSession;

import org.apache.commons.lang3.StringEscapeUtils;
import org.springframework.web.util.WebUtils;


/**
 * A class containing methods to sanitize inputs to prevent XSS.
 *
 */
public class XSSRequestWrapper extends HttpServletRequestWrapper {

    private static final org.apache.logging.log4j.Logger LOG = org.apache.logging.log4j.LogManager.getLogger(XSSRequestWrapper.class);

    private Map<String, String[]> sanitizedQueryString;
    
    private static final String SESSION_XSS_ATTEMPT = "gov.va.med.pharmacy.web.filter.XSSAttempt";
    
    private static final int RONIN = 13;
    
    private static Pattern[] patterns = new Pattern[] {

        // Script fragments
        Pattern.compile("<script>(.*?)</script>", Pattern.CASE_INSENSITIVE),

        // src='...'
        Pattern.compile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
    
        Pattern.compile("src[\r\n]*=[\r\n]*\\\"(.*?)\\\"", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),

        // lonely script tags
        Pattern.compile("</script>", Pattern.CASE_INSENSITIVE),
       
        Pattern.compile("<script(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),

        // eval(...)
        Pattern.compile("eval\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),

        // expression(...)
        Pattern.compile("expression\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),

        // javascript:...
        Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE),
       
 
        Pattern.compile("\\\"*?(%22)*onmouseover=\\\"*?(.*?)\\)(%22)*\\\"*?", Pattern.CASE_INSENSITIVE),
      
        Pattern.compile("alert\\((.*?)\\)", Pattern.CASE_INSENSITIVE),
       
        // vbscript:...
        Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE),

        // onload(...)=...
        Pattern.compile("onload(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL)
    };

    /**
     * constructor
     *
     * @param servletRequest request
     */
    public XSSRequestWrapper(HttpServletRequest servletRequest) {
        super(servletRequest);
    }

    @Override
    public String[] getParameterValues(String name) {
        return getParameterMap().get(name);
    }
    
    @Override
    public String getServletPath() {
        return stripXSS( super.getServletPath());        
    }
    
    @Override
    public String getQueryString() {
        return stripXSS( super.getQueryString());        
    }
    
    @Override
    public String getPathInfo() {
        return stripXSS( super.getPathInfo());        
    }   

    @Override
    public String getParameter(String name) {
        String parameter = null;
     
        String[] vals = getParameterMap().get(name);
      
        if (vals != null && vals.length > 0) {
          parameter = vals[0];
        }
      
        return parameter;
    }

    @Override
    public String getRequestURI() {
       
    	return stripXSS( super.getRequestURI());
    }
    
    @Override
    public StringBuffer getRequestURL() {
    
        return new StringBuffer(stripXSS( super.getRequestURL().toString()));
    }
    
    @SuppressWarnings("unchecked")
    @Override
    public Map<String,String[]> getParameterMap() {
    
    	if(sanitizedQueryString == null ) {
     
    		Map<String, String[]> res = new HashMap<String, String[]>();
        
    		Map<String, String[]> originalQueryString = super.getParameterMap();
        
    		if(originalQueryString!=null) {
       
    			for (String key : (Set<String>) originalQueryString.keySet()) {
           
    				String[] rawVals = originalQueryString.get(key);
           
    				String[] snzVals = new String[rawVals.length];
           
    				for (int i=0; i < rawVals.length; i++) {
            
    					snzVals[i] = stripXSS(rawVals[i]);
            }
           
    				res.put(stripXSS(key), snzVals);
          }
        }
        
    		sanitizedQueryString = res;
      }
    
    	return sanitizedQueryString;
    }
    
    
    @Override
    public Object getAttribute(String name) {
     
    	Object value = super.getAttribute(name);
      
    	if( name == WebUtils.FORWARD_REQUEST_URI_ATTRIBUTE){

          value=(value!=null)?stripXSS(value.toString()):null;
        }
       
    	return value;
    }    

    @Override
    public String getHeader(String name) {
    
    	String value = super.getHeader(name);

        return stripXSS(value);
    }

    /**
     * Since the web app is not storing or reading any cookies, the following method needs to be
     * commented out, otherwise Fortify scan fails.
     * 
     */
    
    /**
     * override the getCookies method
     * @see javax.servlet.http.HttpServletRequestWrapper#getCookies()
     * 
     * @return Cookies
     *//*
    @Override
    public Cookie[] getCookies() {
        Cookie[] cookies = super.getCookies();
    
        for (Cookie c : cookies) {
     
        	c.setValue(stripXSS(c.getValue()));
        }
        
        return cookies;
    }*/

    /**
     * stripXSS
     * @param key string key string of parameter
     * @param value string value string of parameter
     * @return string
     */
    public String stripXSS(String value)  {
       
    	String cleanValue = value;
       
        Integer attempts = null;
       
        int attemptCntr = 1;

        if (value != null && value.length() > 0) {

            // NOTE: It's highly recommended to use the ESAPI library and uncomment the following line to  avoid encoded attacks.
            // value = ESAPI.encoder().canonicalize(value);
           
            cleanValue = canonicalize(value);
         
            // Avoid null characters
            cleanValue = cleanValue.replaceAll("\0", "");

            // Remove all sections that match a pattern
            for (Pattern scriptPattern : patterns) {
              
            	if (scriptPattern.matcher(cleanValue).find()) 
                {
                    LOG.error("XSS ATTACK attempt found on a value: '" + validateStringInput(cleanValue, LOG_FORGING) + "' PATTERN: " + validateStringInput(scriptPattern.toString(), LOG_FORGING));
                 
                    HttpSession session = getSession(false);
                 
                    if (session.getAttribute(SESSION_XSS_ATTEMPT) != null){
          
                    	attempts = (Integer)session.getAttribute(SESSION_XSS_ATTEMPT);
         
                        if (attempts.intValue() < RONIN){
                        	attemptCntr = attempts.intValue()+1;
                        }
                        else{
          
                        	LOG.error("XSS ATTACK is repetative, dumping user session as a precaution.");
               
                        	session.invalidate();
              
                        	cleanValue = null;
            
                        	break;
                        }
                    }
                
                    session.setAttribute(SESSION_XSS_ATTEMPT, new Integer(attemptCntr)); //might keep a count
               
                    cleanValue = scriptPattern.matcher(cleanValue).replaceAll("");
               
                    LOG.error("XSS ATTACK was remedied, old value: " + validateStringInput(value, LOG_FORGING) + 	" | New value: " + cleanValue);               
                }
            }
        }
        
       
        return cleanValue;
    }

    /**
     * canonicalize
     * Simplifies input to its simplest form to make encoding tricks more difficult
     * 
     * @param input string
     * @return string
     */
    private String canonicalize(String input) {

       
        String canonical = Normalizer.normalize(input, Normalizer.Form.NFD);       
      
        canonical = StringEscapeUtils.unescapeHtml4(canonical);       
      
        canonical = StringEscapeUtils.unescapeEcmaScript(canonical);
	   
        return canonical;
    }
}
