/* * Zed Attack Proxy (ZAP) and its related class files. * * ZAP is an HTTP/HTTPS proxy for assessing web application security. * * Copyright The ZAP Development Team * * 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.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.SortedSet; import java.util.TreeSet; import org.apache.commons.httpclient.URI; import org.apache.commons.httpclient.URIException; import org.apache.log4j.Logger; import org.parosproxy.paros.Constant; import org.parosproxy.paros.db.DatabaseException; import org.parosproxy.paros.db.RecordStructure; import org.parosproxy.paros.model.HistoryReference; import org.parosproxy.paros.model.Model; import org.parosproxy.paros.model.Session; import org.parosproxy.paros.model.SiteNode; import org.parosproxy.paros.network.HttpHeader; import org.parosproxy.paros.network.HttpMessage; import org.parosproxy.paros.network.HttpRequestHeader; public class SessionStructure { public static final String ROOT = "Root"; public static final String DATA_DRIVEN_NODE_PREFIX = "\u00AB"; public static final String DATA_DRIVEN_NODE_POSTFIX = "\u00BB"; public static final String DATA_DRIVEN_NODE_REGEX = "(.+?)"; private static final Logger log = Logger.getLogger(SessionStructure.class); public static StructuralNode addPath(Session session, HistoryReference ref, HttpMessage msg) { if (!Constant.isLowMemoryOptionSet()) { return new StructuralSiteNode(session.getSiteTree().addPath(ref, msg)); } else { try { List<String> paths = session.getTreePath(msg); String host = getHostName(msg.getRequestHeader().getURI()); return new StructuralTableNode( addStructure (session, host, msg, paths, paths.size(), ref.getHistoryId())); } catch (Exception e) { log.error(e.getMessage(), e); return null; } } } public static StructuralNode find(long sessionId, URI uri, String method, String postData) throws DatabaseException, URIException { Model model = Model.getSingleton(); if (!Constant.isLowMemoryOptionSet()) { SiteNode node = model.getSession().getSiteTree().findNode(uri, method, postData); if (node == null) { return null; } return new StructuralSiteNode(node); } String nodeName = getNodeName(sessionId, uri, method, postData); RecordStructure rs = model.getDb().getTableStructure().find(sessionId, nodeName, method); if (rs == null) { return null; } return new StructuralTableNode(rs); } private static String getNodeName(long sessionId, URI uri, String method, String postData) throws URIException { Session session = Model.getSingleton().getSession(); List<String> paths = session.getTreePath(uri); String host = getHostName(uri); String nodeUrl = pathsToUrl(host, paths, paths.size()); if (postData != null) { String params = getParams(session, uri, postData); if (params.length() > 0) { nodeUrl = nodeUrl + " " + params; } } return nodeUrl; } private static String getNodeName(Session session, String host, HttpMessage msg, List<String> paths, int size) throws URIException { String nodeUrl = pathsToUrl(host, paths, size); if (msg != null) { String params = getParams(session, msg); if (params.length() > 0) { nodeUrl = nodeUrl + " " + params; } } return nodeUrl; } public static String regexEscape (String str) { String chrsToEscape = ".*+?^=!${}()|[]\\"; StringBuilder sb = new StringBuilder(); char c; for (int i=0; i < str.length(); i++) { c = str.charAt(i); if (chrsToEscape.indexOf(c) >= 0) { sb.append('\\'); } sb.append(c); } return sb.toString(); } /** * Returns a regex pattern that will match the specified StructuralNode, ignoring the parent and children. * For most nodes this will just be the last element in the path, eg * URL regex name * https://www.example.com/aaa/bbb bbb * https://www.example.com/aaa aaa * https://www.example.com/ https://www.example.com * Datadriven nodes are different, they will always return (.+?) to match anything. * @param sn a StructuralNode * @param incParams if true then include URL params in the regex, otherwise exclude them * @return a regex pattern that will match the specified StructuralNode, ignoring the parent and children. */ public static String getRegexName(StructuralNode sn, boolean incParams) { return getSpecifiedName(sn, incParams, true); } /** * Returns the name of the node ignoring the parent and children, * ie the last element in the path. * Data driven nodes will return the user specified name surrounded by the * double angled brackets. * @param sn a StructuralNode * @param incParams if true then include URL params in the regex, otherwise exclude them * @return the name of the node ignoring the parent and children */ public static String getCleanRelativeName(StructuralNode sn, boolean incParams) { return getSpecifiedName(sn, incParams, false); } private static String getSpecifiedName(StructuralNode sn, boolean incParams, boolean dataDrivenNodesAsRegex) { String name = sn.getName(); if (sn.isDataDriven() && dataDrivenNodesAsRegex) { // Non-greedy regex pattern return DATA_DRIVEN_NODE_REGEX; } int bracketIndex = name.lastIndexOf("("); if (bracketIndex >= 0) { // Strip the param summary off name = name.substring(0, bracketIndex); } int quesIndex = name.indexOf("?"); if (quesIndex >= 0) { if (incParams) { // Escape the params String params = name.substring(quesIndex); name = name.substring(0, quesIndex) + regexEscape(params); } else { // Strip the parameters off name = name.substring(0, quesIndex); } } if (name.endsWith("/")) { name = name.substring(0, name.length()-1); } try { if (sn.getURI().getPath() == null || sn.getURI().getPath().length() == 1) { // Its a top level node, return as is return name; } } catch (URIException e) { // Ignore } int slashIndex = name.lastIndexOf('/'); if (slashIndex >= 0) { name = name.substring(slashIndex+1); } if (sn.isLeaf()) { int colonIndex = name.indexOf(":"); if (colonIndex > 0) { // Strip the GET/POST etc off name = name.substring(colonIndex+1); } } return name; } public static String getRegexPattern(StructuralNode sn) throws DatabaseException { return getRegexPattern(sn, true); } public static String getRegexPattern(StructuralNode sn, boolean incChildren) throws DatabaseException { /* * The logic... * Loop up to parent / recurse up * for std nodes escape special cases * inc \/ between nodes * for NSPs use (.+?) ? */ StringBuilder sb = new StringBuilder(); boolean incParams = sn.isLeaf() || ! incChildren; // Work back up the tree.. while (!sn.isRoot()) { if (sb.length() > 0) { sb.insert(0, "/"); } sb.insert(0, getRegexName(sn, incParams)); sn = sn.getParent(); incParams = false; // Only do this for the top node } if (incChildren) { sb.append(".*"); } return sb.toString(); } private static RecordStructure addStructure (Session session, String host, HttpMessage msg, List<String> paths, int size, int historyId) throws DatabaseException, URIException { //String nodeUrl = pathsToUrl(host, paths, size); String nodeName = getNodeName(session, host, msg, paths, size); String parentName = pathsToUrl(host, paths, size-1); String url = ""; if (msg != null) { url = msg.getRequestHeader().getURI().toString(); String params = getParams(session, msg); if (params.length() > 0) { nodeName = nodeName + " " + params; } } String method = HttpRequestHeader.GET; if (msg != null) { method = msg.getRequestHeader().getMethod(); } RecordStructure msgRs = Model.getSingleton().getDb().getTableStructure().find(session.getSessionId(), nodeName, method); if (msgRs == null) { long parentId = -1; if (!nodeName.equals("Root")) { HttpMessage tmpMsg = null; int parentHistoryId = -1; if (!parentName.equals("Root")) { tmpMsg = getTempHttpMessage(session, parentName, msg); parentHistoryId = tmpMsg.getHistoryRef().getHistoryId(); } RecordStructure parentRs = addStructure(session, host, tmpMsg, paths, size-1, parentHistoryId); parentId = parentRs.getStructureId(); } msgRs = Model.getSingleton().getDb().getTableStructure().insert( session.getSessionId(), parentId, historyId, nodeName, url, method); } return msgRs; } private static HttpMessage getTempHttpMessage(Session session, String url, HttpMessage base) { try { HttpMessage newMsg = base.cloneRequest(); URI uri = new URI(url, false); newMsg.getRequestHeader().setURI(uri); newMsg.getRequestHeader().setMethod(HttpRequestHeader.GET); newMsg.getRequestBody().setBody(""); newMsg.getRequestHeader().setHeader(HttpHeader.CONTENT_TYPE, null); newMsg.getRequestHeader().setHeader(HttpHeader.CONTENT_LENGTH, null); HistoryReference historyRef = new HistoryReference(session, HistoryReference.TYPE_TEMPORARY, newMsg); newMsg.setHistoryRef(historyRef); return newMsg; } catch (Exception e) { log.error(e.getMessage(), e); } return null; } private static String pathsToUrl(String host, List<String> paths, int size) { if (size < 0) { return ROOT; } StringBuilder sb = new StringBuilder(); sb.append(host); int i = 1; for (String path : paths) { if (i > size) { break; } if (sb.length() > 0) { sb.append("/"); } sb.append(path); i++; } return sb.toString(); } public static String getHostName(HttpMessage msg) throws URIException { return getHostName(msg.getRequestHeader().getURI()); } public static String getHostName(URI uri) throws URIException { StringBuilder host = new StringBuilder(); String scheme = uri.getScheme().toLowerCase(); host.append(scheme).append("://").append(uri.getHost()); int port = uri.getPort(); if (port != -1 && ((port == 80 && !"http".equals(scheme)) || (port == 443 && !"https".equals(scheme) || (port != 80 && port != 443)))) { host.append(":").append(port); } return host.toString(); } private static String getParams(Session session, HttpMessage msg) throws URIException { // add \u007f to make GET/POST node appear at the end. //String leafName = "\u007f" + msg.getRequestHeader().getMethod()+":"+nodeName; /* String leafName = ""; String query = ""; try { query = msg.getRequestHeader().getURI().getQuery(); } catch (URIException e) { // ZAP: Added error log.error(e.getMessage(), e); } if (query == null) { query = ""; } leafName = leafName + getQueryParamString(msg.getParamNameSet(HtmlParameter.Type.url, query)); // also handle POST method query in body query = ""; if (msg.getRequestHeader().getMethod().equalsIgnoreCase(HttpRequestHeader.POST)) { String contentType = msg.getRequestHeader().getHeader(HttpHeader.CONTENT_TYPE); if (contentType != null && contentType.startsWith("multipart/form-data")) { leafName = leafName + "(multipart/form-data)"; } else { query = msg.getRequestBody().toString(); leafName = leafName + getQueryParamString(msg.getParamNameSet(HtmlParameter.Type.form, query)); } } */ String postData = null; if (msg.getRequestHeader().getMethod().equalsIgnoreCase(HttpRequestHeader.POST)) { String contentType = msg.getRequestHeader().getHeader(HttpHeader.CONTENT_TYPE); if (contentType != null && contentType.startsWith("multipart/form-data")) { postData = "(multipart/form-data)"; } else { postData = msg.getRequestBody().toString(); } } return getParams(session, msg.getRequestHeader().getURI(), postData); } private static String getParams(Session session, URI uri, String postData) throws URIException { String leafName = ""; String query = ""; try { query = uri.getQuery(); } catch (URIException e) { log.error(e.getMessage(), e); } if (query == null) { query = ""; } leafName = leafName + getQueryParamString(session.getUrlParams(uri)); // also handle POST method query in body query = ""; if (postData != null && postData.length() > 0) { if (postData.equals("multipart/form-data")) { leafName = leafName + "(multipart/form-data)"; } else { leafName = leafName + getQueryParamString(session.getFormParams(uri, postData)); } } return leafName; } private static String getQueryParamString(Map<String, String> map) { TreeSet<String> set = new TreeSet<>(); for (Entry<String, String> entry : map.entrySet()) { set.add(entry.getKey()); } return getQueryParamString(set); } private static String getQueryParamString(SortedSet<String> querySet) { StringBuilder sb = new StringBuilder(); Iterator<String> iterator = querySet.iterator(); for (int i=0; iterator.hasNext(); i++) { String name = iterator.next(); if (name == null) { continue; } if (i > 0) { sb.append(','); } if (name.length() > 40) { // Truncate name = name.substring(0, 40); } sb.append(name); } String result = ""; if (sb.length()>0) { result = sb.insert(0, '(').append(')').toString(); } return result; } }