/* * Zed Attack Proxy (ZAP) and its related class files. * * ZAP is an HTTP/HTTPS proxy for assessing web application security. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.zaproxy.zap.model; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import net.sf.json.JSONArray; import net.sf.json.JSONObject; import org.apache.commons.httpclient.URI; import org.apache.commons.httpclient.URIException; import org.apache.log4j.Logger; import org.parosproxy.paros.network.HtmlParameter; import org.parosproxy.paros.network.HttpMessage; import org.parosproxy.paros.network.HtmlParameter.Type; public class StandardParameterParser implements ParameterParser { private static final String CONFIG_KV_PAIR_SEPARATORS = "kvps"; private static final String CONFIG_KV_SEPARATORS = "kvs"; private static final String CONFIG_STRUCTURAL_PARAMS = "struct"; private Context context; private Pattern keyValuePairSeparatorPattern; private Pattern keyValueSeparatorPattern; private String keyValuePairSeparators; private String keyValueSeparators; private List<String> structuralParameters = new ArrayList<String>(); private static Logger log = Logger.getLogger(StandardParameterParser.class); public StandardParameterParser(String keyValuePairSeparators, String keyValueSeparators) throws PatternSyntaxException { super(); this.setKeyValuePairSeparators(keyValuePairSeparators); this.setKeyValueSeparators(keyValueSeparators); } public StandardParameterParser() { this("&", "="); } private Pattern getKeyValuePairSeparatorPattern() { return this.keyValuePairSeparatorPattern; } private Pattern getKeyValueSeparatorPattern() { return this.keyValueSeparatorPattern; } @Override public void init(String config) { try { JSONObject json = JSONObject.fromObject(config); this.setKeyValuePairSeparators(json.getString(CONFIG_KV_PAIR_SEPARATORS)); this.setKeyValueSeparators(json.getString(CONFIG_KV_SEPARATORS)); JSONArray ja = json.getJSONArray(CONFIG_STRUCTURAL_PARAMS); for (Object obj : ja.toArray()) { this.structuralParameters.add(obj.toString()); } } catch (Exception e) { log.error(e.getMessage(), e); } } @Override public String getConfig() { JSONObject json = new JSONObject(); json.put(CONFIG_KV_PAIR_SEPARATORS, this.getKeyValuePairSeparators()); json.put(CONFIG_KV_SEPARATORS, this.getKeyValueSeparators()); JSONArray ja = new JSONArray(); ja.addAll(this.structuralParameters); json.put(CONFIG_STRUCTURAL_PARAMS, ja); return json.toString(); } @Override public Map<String, String> getParams(HttpMessage msg, HtmlParameter.Type type) { if (msg == null) { return new HashMap<>(); } switch (type) { case form: return this.parse(msg.getRequestBody().toString()); case url: return convertParametersList(parseParameters(msg.getRequestHeader().getURI().getEscapedQuery())); default: throw new InvalidParameterException("Type not supported: " + type); } } /** * Converts the given {@code List} of parameters to a {@code Map}. * <p> * The names of parameters are used as keys (mapping to corresponding value) thus removing any duplicated parameters. It is * used an empty {@code String} for the mapping, if the parameter has no value ({@code null}). * * @param parameters the {@code List} to be converted, must not be {@code null} * @return a {@code Map} containing the parameters */ private static Map<String, String> convertParametersList(List<NameValuePair> parameters) { Map<String, String> map = new HashMap<>(); for (NameValuePair parameter : parameters) { String value = parameter.getValue(); if (value == null) { value = ""; } map.put(parameter.getName(), value); } return map; } /** * @throws IllegalArgumentException if any of the parameters is {@code null} or if the given {@code type} is not * {@link org.parosproxy.paros.network.HtmlParameter.Type#url url} or * {@link org.parosproxy.paros.network.HtmlParameter.Type#form form}. */ @Override public List<NameValuePair> getParameters(HttpMessage msg, Type type) { if (msg == null) { throw new IllegalArgumentException("Parameter msg must not be null."); } if (type == null) { throw new IllegalArgumentException("Parameter type must not be null."); } switch (type) { case form: return parseParameters(msg.getRequestBody().toString()); case url: String query = msg.getRequestHeader().getURI().getEscapedQuery(); if (query == null) { return new ArrayList<>(0); } return parseParameters(query); default: throw new IllegalArgumentException("The provided type is not supported: " + type); } } private void setKeyValueSeparatorPattern(Pattern keyValueSeparatorPattern) { this.keyValueSeparatorPattern = keyValueSeparatorPattern; } private void setKeyValuePairSeparatorPattern(Pattern keyValuePairSeparatorPattern) { this.keyValuePairSeparatorPattern = keyValuePairSeparatorPattern; } public String getKeyValuePairSeparators() { return keyValuePairSeparators; } public void setKeyValuePairSeparators(String keyValuePairSeparators) throws PatternSyntaxException { this.setKeyValuePairSeparatorPattern(Pattern.compile("[" + keyValuePairSeparators + "]")); this.keyValuePairSeparators = keyValuePairSeparators; } public String getKeyValueSeparators() { return keyValueSeparators; } public void setKeyValueSeparators(String keyValueSeparators) throws PatternSyntaxException { this.setKeyValueSeparatorPattern(Pattern.compile("[" + keyValueSeparators + "]")); this.keyValueSeparators = keyValueSeparators; } @Override public String getDefaultKeyValuePairSeparator() { if (this.keyValuePairSeparators != null && this.keyValuePairSeparators.length() > 0) { return this.keyValuePairSeparators.substring(0, 1); } // The default return "&"; } @Override public String getDefaultKeyValueSeparator() { if (this.keyValueSeparators != null && this.keyValueSeparators.length() > 0) { return this.keyValueSeparators.substring(0, 1); } // The default return "="; } public List<String> getStructuralParameters() { return Collections.unmodifiableList(structuralParameters); } public void setStructuralParameters(List<String> structuralParameters) { this.structuralParameters.clear(); this.structuralParameters.addAll(structuralParameters); } @Override public Map<String, String> parse(String paramStr) { Map<String, String> map = new HashMap<String, String>(); if (paramStr != null) { String[] keyValue = this.getKeyValuePairSeparatorPattern().split(paramStr); for (int i=0; i<keyValue.length; i++) { try { String[] keyEqValue = this.getKeyValueSeparatorPattern().split(keyValue[i]); if (keyEqValue.length == 1) { map.put(keyEqValue[0], ""); } else if (keyEqValue.length > 1) { map.put(keyEqValue[0], keyEqValue[1]); } } catch (Exception e) { log.error(e.getMessage(), e); } } } return map; } @Override public List<NameValuePair> parseParameters(String parameters) { if (parameters == null) { return new ArrayList<>(0); } List<NameValuePair> parametersList = new ArrayList<>(); String[] pairs = getKeyValuePairSeparatorPattern().split(parameters); for (int i = 0; i < pairs.length; i++) { String[] nameValuePair = getKeyValueSeparatorPattern().split(pairs[i], 2); if (nameValuePair.length == 1) { parametersList.add(new DefaultNameValuePair(urlDecode(nameValuePair[0]))); } else { parametersList.add(new DefaultNameValuePair(urlDecode(nameValuePair[0]), urlDecode(nameValuePair[1]))); } } return parametersList; } private static String urlDecode(String value) { try { return URLDecoder.decode(value, "UTF-8"); } catch (UnsupportedEncodingException ignore) { // Shouldn't happen UTF-8 is a standard charset (see java.nio.charset.StandardCharsets) } return ""; } @Override public StandardParameterParser clone() { StandardParameterParser spp = new StandardParameterParser(this.getKeyValuePairSeparators(), this.getKeyValueSeparators()); spp.setStructuralParameters(this.getStructuralParameters()); return spp; } @Override public List<String> getTreePath(URI uri) throws URIException { return this.getTreePath(uri, true); } private List<String> getTreePath(URI uri, boolean incStructParams) throws URIException { String path = uri.getPath(); List<String> list = new ArrayList<String>(); if (path != null) { Context context = this.getContext(); if (context != null) { String uriStr = uri.toString(); boolean changed = false; for (StructuralNodeModifier ddn : context.getDataDrivenNodes()) { Matcher m = ddn.getPattern().matcher(uriStr); if (m.find()){ if (m.groupCount() == 3) { path = m.group(1) + SessionStructure.DATA_DRIVEN_NODE_PREFIX + ddn.getName() + SessionStructure.DATA_DRIVEN_NODE_POSTFIX + m.group(3); if (!path.startsWith("/")) { // Should always start with a slash;) path = "/" + path; } changed = true; } else if (m.groupCount() == 2) { path = m.group(1) + SessionStructure.DATA_DRIVEN_NODE_PREFIX + ddn.getName() + SessionStructure.DATA_DRIVEN_NODE_POSTFIX; if (!path.startsWith("/")) { // Should always start with a slash;) path = "/" + path; } changed = true; } } } if (changed) { log.debug("Changed path from " + uri.getPath() + " to " + path); } } // Note: Start from the 2nd path element as the first on is always the empty string due // to the split String[] pathList = path.split("/"); for (int i = 1; i < pathList.length; i++) { list.add(pathList[i]); } } if (incStructParams) { // Add any structural params (url param) in key order Map<String, String> urlParams = convertParametersList(parseParameters(uri.getEscapedQuery())); List<String> keys = new ArrayList<String>(urlParams.keySet()); Collections.sort(keys); for (String key: keys) { if (this.structuralParameters.contains(key)) { list.add(urlParams.get(key)); } } } return list; } @Override public List<String> getTreePath(HttpMessage msg) throws URIException { URI uri = msg.getRequestHeader().getURI(); List<String> list = getTreePath(uri); // Add any structural params (form params) in key order Map<String, String> formParams = this.parse(msg.getRequestBody().toString()); List<String> keys = new ArrayList<String>(formParams.keySet()); Collections.sort(keys); for (String key: keys) { if (this.structuralParameters.contains(key)) { list.add(formParams.get(key)); } } return list; } @Override public String getAncestorPath(URI uri, int depth) throws URIException { // If the depth is 0, return an empty path String path = uri.getPath(); if (depth == 0 || path == null) { return ""; } List<String> pathList = getTreePath(uri, false); // Add the 'normal' (plus data driven) path elements // until we finish them or we reach the desired depth StringBuilder parentPath = new StringBuilder(path.length()); for (int i = 0; i < pathList.size() && depth > 0; i++, depth--) { String element = pathList.get(i); parentPath.append('/'); if (element.startsWith(SessionStructure.DATA_DRIVEN_NODE_PREFIX)) { // Its a data driven node - use the regex pattern instead parentPath.append(SessionStructure.DATA_DRIVEN_NODE_REGEX); } else { parentPath.append(element); } } // If we're done or we have no structural parameters, just return if (depth == 0 || structuralParameters.isEmpty()) { return parentPath.toString(); } // Add the 'structural params' path elements boolean firstElement = true; Map<String, String> urlParams = convertParametersList(parseParameters(uri.getEscapedQuery())); for (Entry<String, String> param : urlParams.entrySet()) { if (this.structuralParameters.contains(param.getKey())) { if (firstElement) { firstElement = false; parentPath.append('?'); } else { parentPath.append(keyValuePairSeparators); } parentPath.append(param.getKey()).append(keyValueSeparators).append(param.getValue()); if ((--depth) == 0) { break; } } } return parentPath.toString(); } @Override public void setContext(Context context) { this.context = context; } @Override public Context getContext() { return context; } }