/* * 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.extension.httpsessions; import java.net.HttpCookie; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import org.apache.log4j.Logger; import org.parosproxy.paros.Constant; import org.parosproxy.paros.control.Control.Mode; import org.parosproxy.paros.extension.ExtensionAdaptor; import org.parosproxy.paros.extension.ExtensionHook; import org.parosproxy.paros.extension.SessionChangedListener; import org.parosproxy.paros.model.Session; import org.parosproxy.paros.model.SiteNode; import org.parosproxy.paros.network.HttpMessage; import org.parosproxy.paros.network.HttpSender; import org.zaproxy.zap.model.Context; import org.zaproxy.zap.network.HttpSenderListener; import org.zaproxy.zap.view.ScanPanel; import org.zaproxy.zap.view.SiteMapListener; import org.zaproxy.zap.view.SiteMapTreeCellRenderer; /** * The HttpSessions Extension handles the existing http sessions on the existing site. It allows the * management and usage of multiple sessions per site and also handles the session tokens for each * of them. * * <p> * Whenever referring to a particular site, the string that is identifying it is constructed from * the site's URI and has to follow these rules: * <ul> * <li>no leading protocol (e.g. 'http'), colon (':') and double slashes ('//')</li> * <li>the port is added in the end after colon</li> * <li>lower-case</li> * </ul> * An example of a method performing these changes on an URI is * {@link ScanPanel#cleanSiteName(String, boolean)} . * </p> * */ public class ExtensionHttpSessions extends ExtensionAdaptor implements SessionChangedListener, SiteMapListener, HttpSenderListener { /** The Constant NAME. */ public static final String NAME = "ExtensionHttpSessions"; /** The Constant log. */ private static final Logger log = Logger.getLogger(ExtensionHttpSessions.class); /** The http sessions panel. */ private HttpSessionsPanel httpSessionsPanel; /** The options http sessions panel. */ private OptionsHttpSessionsPanel optionsHttpSessionsPanel; /** The map of sessions corresponding to each site. */ private Map<String, HttpSessionsSite> sessions; /** Object used to synchronize access to sessions */ private Object sessionLock = new Object(); /** The map of session tokens corresponding to each site. */ private Map<String, HttpSessionTokensSet> sessionTokens; /** * The map of default tokens that were removed by the user for some sites and should not be * detected again as session tokens. */ private Map<String, HashSet<String>> removedDefaultTokens; /** The http sessions extension's parameters. */ private HttpSessionsParam param; /** The popup menu used to set the active session. */ private PopupMenuSetActiveSession popupMenuSetActiveSession; /** The popup menu used to unset the active session. */ private PopupMenuUnsetActiveSession popupMenuUnsetActiveSession; /** The popup menu used to remove a session. */ private PopupMenuRemoveSession popupMenuRemoveSession; /** The popup menu used to add a Manual Authentication User. */ private PopupMenuFactoryAddUserFromSession popupMenuAddUserFromSession; private PopupMenuItemCopySessionToken popupMenuItemCopySessionToken; /** * Instantiates a new extension http sessions. */ public ExtensionHttpSessions() { super(NAME); initialize(); } /** * Initialize. */ private void initialize() { this.setOrder(68); } @Override public String getAuthor() { return Constant.ZAP_TEAM; } @Override public String getDescription() { return Constant.messages.getString("httpsessions.desc"); } @Override public URL getURL() { try { return new URL(Constant.ZAP_HOMEPAGE); } catch (MalformedURLException e) { return null; } } @Override public void init() { super.init(); this.sessionTokens = new HashMap<>(); } @Override public void hook(ExtensionHook extensionHook) { super.hook(extensionHook); // Register the parameters extensionHook.addOptionsParamSet(getParam()); extensionHook.addSessionListener(this); extensionHook.addSiteMapListener(this); HttpSender.addListener(this); if (getView() != null) { // Hook the panels extensionHook.getHookView().addStatusPanel(getHttpSessionsPanel()); extensionHook.getHookView().addOptionPanel(getOptionsHttpSessionsPanel()); // Hook the popup menus extensionHook.getHookMenu().addPopupMenuItem(getPopupMenuSetActiveSession()); extensionHook.getHookMenu().addPopupMenuItem(getPopupMenuUnsetActiveSession()); extensionHook.getHookMenu().addPopupMenuItem(getPopupMenuRemoveSession()); extensionHook.getHookMenu().addPopupMenuItem(getPopupMenuAddUserFromSession()); extensionHook.getHookMenu().addPopupMenuItem(getPopupMenuItemCopySessionToken()); } // Register as an API implementor extensionHook.addApiImplementor(new HttpSessionsAPI(this)); } /** * Gets the options panel for this extension. * * @return the options session panel */ private OptionsHttpSessionsPanel getOptionsHttpSessionsPanel() { if (optionsHttpSessionsPanel == null) { optionsHttpSessionsPanel = new OptionsHttpSessionsPanel(); } return optionsHttpSessionsPanel; } /** * Gets the popup menu set active session. * * @return the popup menu set active session */ private PopupMenuSetActiveSession getPopupMenuSetActiveSession() { if (popupMenuSetActiveSession == null) { popupMenuSetActiveSession = new PopupMenuSetActiveSession(); popupMenuSetActiveSession.setExtension(this); } return popupMenuSetActiveSession; } /** * Gets the popup menu used to delete a session. * * @return the popup menu used to delete a session */ private PopupMenuRemoveSession getPopupMenuRemoveSession() { if (popupMenuRemoveSession == null) { popupMenuRemoveSession = new PopupMenuRemoveSession(); popupMenuRemoveSession.setExtension(this); } return popupMenuRemoveSession; } /** * Gets the popup menu to unset active session. * * @return the popup menu unset active session */ private PopupMenuUnsetActiveSession getPopupMenuUnsetActiveSession() { if (popupMenuUnsetActiveSession == null) { popupMenuUnsetActiveSession = new PopupMenuUnsetActiveSession(); popupMenuUnsetActiveSession.setExtension(this); } return popupMenuUnsetActiveSession; } /** * Gets the popup menu to add a user. * * @return the popup menu to add a user */ private PopupMenuFactoryAddUserFromSession getPopupMenuAddUserFromSession() { if (popupMenuAddUserFromSession == null) { popupMenuAddUserFromSession = new PopupMenuFactoryAddUserFromSession(this); } return popupMenuAddUserFromSession; } private PopupMenuItemCopySessionToken getPopupMenuItemCopySessionToken() { if (popupMenuItemCopySessionToken == null) { popupMenuItemCopySessionToken = new PopupMenuItemCopySessionToken(getHttpSessionsPanel()); } return popupMenuItemCopySessionToken; } /** * Gets the parameters (options) for this extension and related classes. * * @return the param */ public HttpSessionsParam getParam() { if (param == null) { param = new HttpSessionsParam(); } return param; } /** * Checks if a particular token is part of the default session tokens set by the user using the * options panel. The default session tokens are valid for all sites. The check is being * performed in a lower-case manner, as default session tokens are case-insensitive. * * @param token the token * @return true, if it is a default session token */ public boolean isDefaultSessionToken(String token) { if (getParam().getDefaultTokensEnabled().contains(token.toLowerCase(Locale.ENGLISH))) return true; return false; } /** * Checks if a particular default session token was removed by an user as a session token for a * site. * * @param site the site. This parameter has to be formed as defined in the * {@link ExtensionHttpSessions} class documentation. * @param token the token * @return true, if it is a previously removed default session token */ private boolean isRemovedDefaultSessionToken(String site, String token) { if (removedDefaultTokens == null) return false; HashSet<String> removed = removedDefaultTokens.get(site); if (removed == null || !removed.contains(token)) return false; return true; } /** * Marks a default session token as removed for a particular site. * * @param site the site. This parameter has to be formed as defined in the * {@link ExtensionHttpSessions} class documentation. * @param token the token */ private void markRemovedDefaultSessionToken(String site, String token) { if (removedDefaultTokens == null) removedDefaultTokens = new HashMap<>(1); HashSet<String> removedSet = removedDefaultTokens.get(site); if (removedSet == null) { removedSet = new HashSet<>(1); removedDefaultTokens.put(site, removedSet); } removedSet.add(token); } /** * Unmarks a default session token as removed for a particular site. * * @param site the site. This parameter has to be formed as defined in the * {@link ExtensionHttpSessions} class documentation. * @param token the token */ private void unmarkRemovedDefaultSessionToken(String site, String token) { if (removedDefaultTokens == null) return; HashSet<String> removed = removedDefaultTokens.get(site); if (removed == null) return; removed.remove(token); } /** * Checks if a particular token is a session token name for a particular site. * * @param site the site. This parameter has to be formed as defined in the * {@link ExtensionHttpSessions} class documentation. However, if the protocol is * missing, a default protocol of 80 is used. * @param token the token * @return true, if it is session token */ public boolean isSessionToken(String site, String token) { // Add a default port if (!site.contains(":")) { site = site + (":80"); } HttpSessionTokensSet siteTokens = sessionTokens.get(site); if (siteTokens == null) return false; return siteTokens.isSessionToken(token); } /** * Adds a new session token for a particular site. * * @param site the site. This parameter has to be formed as defined in the * {@link ExtensionHttpSessions} class documentation. However, if the protocol is * missing, a default protocol of 80 is used. * @param token the token */ public void addHttpSessionToken(String site, String token) { // Add a default port if (!site.contains(":")) { site = site + (":80"); } HttpSessionTokensSet siteTokens = sessionTokens.get(site); if (siteTokens == null) { siteTokens = new HttpSessionTokensSet(); sessionTokens.put(site, siteTokens); } log.info("Added new session token for site '" + site + "': " + token); siteTokens.addToken(token); // If the session token is a default token and was previously marked as remove, undo that unmarkRemovedDefaultSessionToken(site, token); } /** * Removes a particular session token for a site. * <p> * All the existing sessions are cleaned up: * <ul> * <li>if there are no more session tokens, all session are deleted</li> * <li>in every existing session, the value for the deleted token is removed</li> * <li>if there is a session with no values for the remaining session tokens, it is deleted</li> * <li>if, after deletion, there are duplicate sessions, they are merged</li> * </ul> * </p> * * @param site the site. This parameter has to be formed as defined in the * {@link ExtensionHttpSessions} class documentation. However, if the protocol is * missing, a default protocol of 80 is used. * @param token the token */ public void removeHttpSessionToken(String site, String token) { // Add a default port if (!site.contains(":")) { site = site + (":80"); } HttpSessionTokensSet siteTokens = sessionTokens.get(site); if (siteTokens != null) { // Remove the token from the tokens associated with the site siteTokens.removeToken(token); if (siteTokens.isEmpty()) sessionTokens.remove(site); // Cleanup the existing sessions this.getHttpSessionsSite(site).cleanupSessionToken(token); } // If the token is a default session token, mark it as removed for the Site, so it will not // be detected again and added as a session token if (isDefaultSessionToken(token)) markRemovedDefaultSessionToken(site, token); log.info("Removed session token for site '" + site + "': " + token); } /** * Gets the set of session tokens for a particular site. No modifications should be done to the * returned object. Instead, any modifications should be done through the corresponding methods * in the {@link ExtensionHttpSessions}. * * @param site the site. This parameter has to be formed as defined in the * {@link ExtensionHttpSessions} class documentation. However, if the protocol is * missing, a default protocol of 80 is used. * @return the session tokens set, if any have been set, or null, if there are no session tokens * for this site * @see ExtensionHttpSessions#addHttpSessionToken(String, String) * @see ExtensionHttpSessions#removeHttpSessionToken(String, String) */ public final HttpSessionTokensSet getHttpSessionTokensSet(String site) { // Add a default port if (!site.contains(":")) { site = site + (":80"); } return sessionTokens.get(site); } /** * Gets the http sessions panel. * * @return the http sessions panel */ protected HttpSessionsPanel getHttpSessionsPanel() { if (httpSessionsPanel == null) { httpSessionsPanel = new HttpSessionsPanel(this); } return httpSessionsPanel; } /** * Gets the http sessions for a particular site. If it doesn't exist, it is created. * * @param site the site. This parameter has to be formed as defined in the * {@link ExtensionHttpSessions} class documentation. However, if the protocol is * missing, a default protocol of 80 is used. * @return the http sessions site container */ public HttpSessionsSite getHttpSessionsSite(String site) { return getHttpSessionsSite(site, true); } /** * Gets the http sessions for a particular site. The behaviour when a {@link HttpSessionsSite} * does not exist is defined by the {@code createIfNeeded} parameter. * * @param site the site. This parameter has to be formed as defined in the * {@link ExtensionHttpSessions} class documentation. However, if the protocol is * missing, a default protocol of 80 is used. * @param createIfNeeded whether a new {@link HttpSessionsSite} object is created if one does * not exist * @return the http sessions site container, or null one does not exist and createIfNeeded is * false * */ public HttpSessionsSite getHttpSessionsSite(String site, boolean createIfNeeded) { // Add a default port if (!site.contains(":")) { site = site + (":80"); } synchronized (sessionLock) { if (sessions == null) { if (!createIfNeeded) { return null; } sessions = new HashMap<>(); } HttpSessionsSite hss = sessions.get(site); if (hss == null) { if (!createIfNeeded) return null; hss = new HttpSessionsSite(this, site); sessions.put(site, hss); } return hss; } } @Override public void nodeSelected(SiteNode node) { // Event from SiteMapListenner this.getHttpSessionsPanel().nodeSelected(node); } @Override public void onReturnNodeRendererComponent(SiteMapTreeCellRenderer component, boolean leaf, SiteNode value) { } @Override public void sessionChanged(Session session) { } @Override public void sessionAboutToChange(Session session) { sessionTokens = new HashMap<>(); synchronized (sessionLock) { sessions = null; } removedDefaultTokens = null; if (getView() != null) { getHttpSessionsPanel().reset(); } HttpSessionsSite.resetLastGeneratedSessionId(); } @Override public void sessionScopeChanged(Session session) { } @Override public void sessionModeChanged(Mode mode) { } /** * Builds and returns a list of http sessions that correspond to a given context. * * @param context the context * @return the http sessions for context */ public List<HttpSession> getHttpSessionsForContext(Context context) { List<HttpSession> sessions = new LinkedList<>(); if (this.sessions == null) { return sessions; } synchronized (sessionLock) { for (Entry<String, HttpSessionsSite> e : this.sessions.entrySet()) { String siteName = e.getKey(); siteName = "http://" + siteName; if (context.isInContext(siteName)) sessions.addAll(e.getValue().getHttpSessions()); } } return sessions; } /** * Gets the http session tokens set for the first site matching a given Context. * * @param context the context * @return the http session tokens set for context */ public HttpSessionTokensSet getHttpSessionTokensSetForContext(Context context){ //TODO: Proper implementation. Hack for now for (Entry<String, HttpSessionTokensSet> e : this.sessionTokens.entrySet()) { String siteName = e.getKey(); siteName = "http://" + siteName; if (context.isInContext(siteName)) return e.getValue(); } return null; } /** * Gets all of the sites with http sessions. * * @return all of the sites with http sessions */ public List<String> getSites() { List<String> sites = new ArrayList<String>(); if (this.sessions == null) { return sites; } synchronized (sessionLock) { sites.addAll(this.sessions.keySet()); } return sites; } @Override public int getListenerOrder() { return 1; } @Override public void onHttpRequestSend(HttpMessage msg, int initiator, HttpSender sender) { if (initiator == HttpSender.CHECK_FOR_UPDATES_INITIATOR || initiator == HttpSender.AUTHENTICATION_INITIATOR) { return; } // Check if we know the site and add it otherwise String site = msg.getRequestHeader().getHostName() + ":" + msg.getRequestHeader().getHostPort(); site = ScanPanel.cleanSiteName(site, true); if (getView() != null) { this.getHttpSessionsPanel().addSiteAsynchronously(site); } // Check if it's enabled for proxy only if (getParam().isEnabledProxyOnly() && initiator != HttpSender.PROXY_INITIATOR) return; // Check for default tokens in request messages List<HttpCookie> requestCookies = msg.getRequestHeader().getHttpCookies(); for (HttpCookie cookie : requestCookies) { // If it's a default session token and it is not already marked as session token and was // not previously removed by the user if (this.isDefaultSessionToken(cookie.getName()) && !this.isSessionToken(site, cookie.getName()) && !this.isRemovedDefaultSessionToken(site, cookie.getName())) { this.addHttpSessionToken(site, cookie.getName()); } } // Forward the request for proper processing HttpSessionsSite session = getHttpSessionsSite(site); session.processHttpRequestMessage(msg); } @Override public void onHttpResponseReceive(HttpMessage msg, int initiator, HttpSender sender) { if (initiator == HttpSender.ACTIVE_SCANNER_INITIATOR || initiator == HttpSender.SPIDER_INITIATOR || initiator == HttpSender.AJAX_SPIDER_INITIATOR || initiator == HttpSender.FORCED_BROWSE_INITIATOR || initiator == HttpSender.CHECK_FOR_UPDATES_INITIATOR || initiator == HttpSender.FUZZER_INITIATOR || initiator == HttpSender.AUTHENTICATION_INITIATOR) { // Not a session we care about return; } // Check if we know the site and add it otherwise String site = msg.getRequestHeader().getHostName() + ":" + msg.getRequestHeader().getHostPort(); site = ScanPanel.cleanSiteName(site, true); if (getView() != null) { this.getHttpSessionsPanel().addSiteAsynchronously(site); } // Check if it's enabled for proxy only if (getParam().isEnabledProxyOnly() && initiator != HttpSender.PROXY_INITIATOR) { return; } // Check for default tokens set in response messages List<HttpCookie> responseCookies = msg.getResponseHeader().getHttpCookies(msg.getRequestHeader().getHostName()); for (HttpCookie cookie : responseCookies) { // If it's a default session token and it is not already marked as session token and was // not previously removed by the user if (this.isDefaultSessionToken(cookie.getName()) && !this.isSessionToken(site, cookie.getName()) && !this.isRemovedDefaultSessionToken(site, cookie.getName())) { this.addHttpSessionToken(site, cookie.getName()); } } // Forward the request for proper processing HttpSessionsSite sessionsSite = getHttpSessionsSite(site); sessionsSite.processHttpResponseMessage(msg); } }