diff --git a/addOns/authhelper/CHANGELOG.md b/addOns/authhelper/CHANGELOG.md index c8fc6b787e6..6673fddfdc5 100644 --- a/addOns/authhelper/CHANGELOG.md +++ b/addOns/authhelper/CHANGELOG.md @@ -4,7 +4,8 @@ All notable changes to this add-on will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased - +### Added +- Added support for Client Script Authentication when used in conjunction with the Ajax Spider add-on. ## [0.18.0] - 2025-01-27 ### Changed diff --git a/addOns/authhelper/authhelper.gradle.kts b/addOns/authhelper/authhelper.gradle.kts index 660c09253d5..b43a2fec81f 100644 --- a/addOns/authhelper/authhelper.gradle.kts +++ b/addOns/authhelper/authhelper.gradle.kts @@ -17,7 +17,7 @@ zapAddOn { dependencies { addOns { register("spiderAjax") { - version.set(">=23.15.0") + version.set(">=23.22.0") } } } @@ -29,7 +29,10 @@ zapAddOn { dependencies { addOns { register("client") { - version.set(">=0.10.0") + version.set(">=0.11.0") + } + register("scripts") { + version.set(">=45.8.0") } } } @@ -49,6 +52,9 @@ zapAddOn { register("selenium") { version.set("15.*") } + register("zest") { + version.set(">=48.1.0") + } } } } @@ -69,6 +75,7 @@ dependencies { zapAddOn("selenium") zapAddOn("spiderAjax") zapAddOn("client") + zapAddOn("zest") testImplementation(project(":testutils")) } diff --git a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/AuthUtils.java b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/AuthUtils.java index 463bfae3d32..8b266f7d44a 100644 --- a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/AuthUtils.java +++ b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/AuthUtils.java @@ -606,7 +606,8 @@ static SessionManagementRequestDetails findSessionTokenSource(String token) { return findSessionTokenSource(token, -1); } - static SessionManagementRequestDetails findSessionTokenSource(String token, int firstId) { + public static SessionManagementRequestDetails findSessionTokenSource( + String token, int firstId) { ExtensionHistory extHist = AuthUtils.getExtension(ExtensionHistory.class); int lastId = extHist.getLastHistoryId(); if (firstId == -1) { diff --git a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/ClientScriptBasedAuthenticationMethodType.java b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/ClientScriptBasedAuthenticationMethodType.java new file mode 100644 index 00000000000..8255b4694cc --- /dev/null +++ b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/ClientScriptBasedAuthenticationMethodType.java @@ -0,0 +1,748 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2025 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.addon.authhelper; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.GridBagLayout; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import org.apache.commons.configuration.Configuration; +import org.apache.commons.configuration.ConfigurationException; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jdesktop.swingx.JXComboBox; +import org.jdesktop.swingx.decorator.FontHighlighter; +import org.jdesktop.swingx.renderer.DefaultListRenderer; +import org.parosproxy.paros.Constant; +import org.parosproxy.paros.control.Control; +import org.parosproxy.paros.core.scanner.Alert; +import org.parosproxy.paros.network.HttpMessage; +import org.parosproxy.paros.network.HttpRequestHeader; +import org.parosproxy.paros.view.View; +import org.zaproxy.addon.network.server.HttpMessageHandler; +import org.zaproxy.zap.authentication.AbstractAuthenticationMethodOptionsPanel; +import org.zaproxy.zap.authentication.AuthenticationCredentials; +import org.zaproxy.zap.authentication.AuthenticationHelper; +import org.zaproxy.zap.authentication.AuthenticationIndicatorsPanel; +import org.zaproxy.zap.authentication.AuthenticationMethod; +import org.zaproxy.zap.authentication.AuthenticationMethodType; +import org.zaproxy.zap.authentication.GenericAuthenticationCredentials; +import org.zaproxy.zap.authentication.ScriptBasedAuthenticationMethodType; +import org.zaproxy.zap.extension.script.ExtensionScript; +import org.zaproxy.zap.extension.script.ScriptWrapper; +import org.zaproxy.zap.extension.zest.ZestAuthenticationRunner; +import org.zaproxy.zap.model.Context; +import org.zaproxy.zap.session.SessionManagementMethod; +import org.zaproxy.zap.session.WebSession; +import org.zaproxy.zap.users.User; +import org.zaproxy.zap.utils.EncodingUtils; +import org.zaproxy.zap.utils.ZapHtmlLabel; +import org.zaproxy.zap.view.DynamicFieldsPanel; +import org.zaproxy.zap.view.LayoutHelper; +import org.zaproxy.zest.core.v1.ZestScript; + +public class ClientScriptBasedAuthenticationMethodType extends ScriptBasedAuthenticationMethodType { + + public static final int METHOD_IDENTIFIER = 8; + + private static final Logger LOGGER = + LogManager.getLogger(ClientScriptBasedAuthenticationMethodType.class); + + private ExtensionScript extensionScript; + + private HttpMessageHandler handler; + private HttpMessage authMsg; + private HttpMessage fallbackMsg; + private int firstHrefId; + + public ClientScriptBasedAuthenticationMethodType() {} + + private HttpMessageHandler getHandler(Context context) { + if (handler == null) { + handler = + (ctx, msg) -> { + if (ctx.isFromClient()) { + return; + } + + AuthenticationHelper.addAuthMessageToHistory(msg); + + if (HttpRequestHeader.POST.equals(msg.getRequestHeader().getMethod()) + && context.isIncluded(msg.getRequestHeader().getURI().toString())) { + // Record the last in scope POST as a fallback + fallbackMsg = msg; + } + + SessionManagementRequestDetails smReqDetails = null; + Map sessionTokens = + AuthUtils.getResponseSessionTokens(msg); + if (!sessionTokens.isEmpty()) { + authMsg = msg; + smReqDetails = + new SessionManagementRequestDetails( + authMsg, + new ArrayList<>(sessionTokens.values()), + Alert.CONFIDENCE_HIGH); + } else { + Set reqSessionTokens = + AuthUtils.getRequestSessionTokens(msg); + if (!reqSessionTokens.isEmpty()) { + // The request has at least one auth token we missed - try + // to find one of them + for (SessionToken st : reqSessionTokens) { + smReqDetails = + AuthUtils.findSessionTokenSource( + st.getValue(), firstHrefId); + if (smReqDetails != null) { + authMsg = smReqDetails.getMsg(); + LOGGER.debug( + "Session token found in href {}", + authMsg.getHistoryRef().getHistoryId()); + break; + } + } + } + + if (authMsg != null && View.isInitialised()) { + String hrefId = "?"; + if (msg.getHistoryRef() != null) { + hrefId = "" + msg.getHistoryRef().getHistoryId(); + } + AuthUtils.logUserMessage( + Level.INFO, + Constant.messages.getString( + "authhelper.auth.method.browser.output.sessionid", + hrefId)); + } + } + if (firstHrefId == 0 && msg.getHistoryRef() != null) { + firstHrefId = msg.getHistoryRef().getHistoryId(); + } + }; + } + return handler; + } + + @Override + public String getName() { + return Constant.messages.getString("authhelper.auth.method.clientscript.name"); + } + + @Override + public int getUniqueIdentifier() { + return METHOD_IDENTIFIER; + } + + @Override + public ClientScriptBasedAuthenticationMethod createAuthenticationMethod(int contextId) { + return new ClientScriptBasedAuthenticationMethod(); + } + + @Override + public AbstractAuthenticationMethodOptionsPanel buildOptionsPanel(Context uiSharedContext) { + return new ClientScriptBasedAuthenticationMethodOptionsPanel(); + } + + public class ClientScriptBasedAuthenticationMethod extends ScriptBasedAuthenticationMethod { + private ScriptWrapper script; + + private String[] credentialsParamNames; + + private Map paramValues; + + /** + * Load a script and fills in the method's parameters according to the values specified by + * the script. + * + *

If the method already had a loaded script and a set of values for the parameters, it + * tries to provide new values for the new parameters if they match any previous parameter + * names. + * + * @param scriptW the script wrapper + * @throws IllegalArgumentException if an error occurs while loading the script. + */ + @Override + public void loadScript(ScriptWrapper scriptW) { + AuthenticationScript authScript = getAuthScriptInterfaceV2(scriptW); + if (authScript == null) { + authScript = getAuthScriptInterface(scriptW); + } + if (authScript == null) { + LOGGER.warn( + "The script {} does not properly implement the Authentication Script interface.", + scriptW.getName()); + throw new IllegalArgumentException( + Constant.messages.getString( + "authentication.method.script.dialog.error.text.interface", + scriptW.getName())); + } + + try { + if (authScript instanceof AuthenticationScriptV2 scriptV2) { + setLoggedInIndicatorPattern(scriptV2.getLoggedInIndicator()); + setLoggedOutIndicatorPattern(scriptV2.getLoggedOutIndicator()); + } + String[] requiredParams = authScript.getRequiredParamsNames(); + String[] optionalParams = authScript.getOptionalParamsNames(); + this.credentialsParamNames = authScript.getCredentialsParamsNames(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "Loaded authentication script - required parameters: {} - optional parameters: {}", + Arrays.toString(requiredParams), + Arrays.toString(optionalParams)); + } + // If there's an already loaded script, make sure we save its values and _try_ + // to use them + Map oldValues = + this.paramValues != null + ? this.paramValues + : Collections.emptyMap(); + this.paramValues = new HashMap<>(requiredParams.length + optionalParams.length); + for (String param : requiredParams) + this.paramValues.put(param, oldValues.get(param)); + for (String param : optionalParams) + this.paramValues.put(param, oldValues.get(param)); + + this.script = scriptW; + LOGGER.info( + "Successfully loaded new script for ClientScriptBasedAuthentication: {}", + this); + } catch (Exception e) { + LOGGER.error("Error while loading authentication script", e); + getExtensionScript().handleScriptException(this.script, e); + throw new IllegalArgumentException( + Constant.messages.getString( + "authentication.method.script.dialog.error.text.loading", + e.getMessage())); + } + } + + @Override + public String toString() { + return "ClientScriptBasedAuthenticationMethod [script=" + + script + + ", paramValues=" + + paramValues + + ", credentialsParamNames=" + + Arrays.toString(credentialsParamNames) + + "]"; + } + + @Override + public boolean isConfigured() { + return true; + } + + @Override + public AuthenticationMethod duplicate() { + ClientScriptBasedAuthenticationMethod method = + new ClientScriptBasedAuthenticationMethod(); + method.script = script; + method.paramValues = this.paramValues != null ? new HashMap<>(this.paramValues) : null; + method.credentialsParamNames = this.credentialsParamNames; + return method; + } + + @Override + public boolean validateCreationOfAuthenticationCredentials() { + if (credentialsParamNames != null) { + return true; + } + + if (View.isInitialised()) { + View.getSingleton() + .showMessageDialog( + Constant.messages.getString( + "authentication.method.script.dialog.error.text.notLoaded")); + } + + return false; + } + + @Override + public AuthenticationCredentials createAuthenticationCredentials() { + return new GenericAuthenticationCredentials(this.credentialsParamNames); + } + + @Override + public AuthenticationMethodType getType() { + return new ClientScriptBasedAuthenticationMethodType(); + } + + public ScriptWrapper getScriptWrapper() { + return this.script; + } + + public ZestScript getZestScript() { + AuthenticationScript authScript = getAuthScriptInterfaceV2(this.script); + if (authScript == null) { + authScript = getAuthScriptInterface(this.script); + } + + if (authScript == null) { + LOGGER.debug("Failed to get ZestScript - no suitable interface"); + return null; + } + + if (authScript instanceof ZestAuthenticationRunner zestScript) { + return zestScript.getScript().getZestScript(); + } + LOGGER.debug( + "Failed to get ZestScript - authScript of right type {}", + authScript.getClass().getCanonicalName()); + return null; + } + + @Override + public WebSession authenticate( + SessionManagementMethod sessionManagementMethod, + AuthenticationCredentials credentials, + User user) + throws UnsupportedAuthenticationCredentialsException { + if (!(credentials instanceof GenericAuthenticationCredentials)) { + user.getAuthenticationState() + .setLastAuthFailure("Credentials not GenericAuthenticationCredentials"); + throw new UnsupportedAuthenticationCredentialsException( + "Script based Authentication method only supports " + + GenericAuthenticationCredentials.class.getSimpleName() + + ". Received: " + + credentials.getClass()); + } + GenericAuthenticationCredentials cred = (GenericAuthenticationCredentials) credentials; + + // Call the script to get an authenticated message from which we can then extract the + // session + AuthenticationScript authScript = getAuthScriptInterfaceV2(this.script); + if (authScript == null) { + authScript = getAuthScriptInterface(this.script); + } + + if (authScript == null) { + return null; + } + LOGGER.debug("Script class: {}", authScript.getClass().getCanonicalName()); + ExtensionScript.recordScriptCalledStats(this.script); + + try { + if (authScript instanceof AuthenticationScriptV2 scriptV2) { + setLoggedInIndicatorPattern(scriptV2.getLoggedInIndicator()); + setLoggedOutIndicatorPattern(scriptV2.getLoggedOutIndicator()); + } + + if (authScript instanceof ZestAuthenticationRunner zestScript) { + zestScript.registerHandler(getHandler(user.getContext())); + } else { + LOGGER.warn("Expected authScript to be a Zest script"); + return null; + } + + authScript.authenticate( + new AuthenticationHelper(getHttpSender(), sessionManagementMethod, user), + this.paramValues, + cred); + } catch (Exception e) { + // Catch Exception instead of ScriptException and IOException because script engine + // implementations might throw other exceptions on script errors (e.g. + // jdk.nashorn.internal.runtime.ECMAException) + user.getAuthenticationState() + .setLastAuthFailure( + "Error running authentication script " + e.getMessage()); + LOGGER.error( + "An error occurred while trying to authenticate using the Authentication Script: {}", + this.script.getName(), + e); + getExtensionScript().handleScriptException(this.script, e); + return null; + } + + // Wait until the authentication request is identified + for (int i = 0; i < AuthUtils.getWaitLoopCount(); i++) { + if (authMsg != null) { + break; + } + AuthUtils.sleep(AuthUtils.TIME_TO_SLEEP_IN_MSECS); + } + + if (authMsg != null) { + // Update the session as it may have changed + WebSession session = sessionManagementMethod.extractWebSession(authMsg); + user.setAuthenticatedSession(session); + + if (this.isAuthenticated(authMsg, user, true)) { + // Let the user know it worked + AuthenticationHelper.notifyOutputAuthSuccessful(authMsg); + user.getAuthenticationState().setLastAuthFailure(""); + } else { + // Let the user know it failed + AuthenticationHelper.notifyOutputAuthFailure(authMsg); + } + return session; + } + + // We don't expect this to work, but it will prevent some NPEs + return sessionManagementMethod.extractWebSession(fallbackMsg); + } + + @Override + public void replaceUserDataInPollRequest(HttpMessage msg, User user) { + AuthenticationHelper.replaceUserDataInRequest( + msg, wrapKeys(this.paramValues), NULL_ENCODER); + } + } + + private static Map wrapKeys(Map kvPairs) { + Map map = new HashMap<>(); + for (Entry kv : kvPairs.entrySet()) { + map.put( + AuthenticationMethod.TOKEN_PREFIX + + kv.getKey() + + AuthenticationMethod.TOKEN_POSTFIX, + kv.getValue()); + } + return map; + } + + @SuppressWarnings("serial") + public class ClientScriptBasedAuthenticationMethodOptionsPanel + extends AbstractAuthenticationMethodOptionsPanel { + + private static final long serialVersionUID = 7812841049435409987L; + + private static final String SCRIPT_NAME_LABEL = + Constant.messages.getString("authentication.method.script.field.label.scriptName"); + private static final String LABEL_NOT_LOADED = + Constant.messages.getString("authentication.method.script.field.label.notLoaded"); + private JXComboBox scriptsComboBox; + private JButton loadScriptButton; + + private ClientScriptBasedAuthenticationMethod method; + private AuthenticationIndicatorsPanel indicatorsPanel; + + private ScriptWrapper loadedScript; + + private JPanel dynamicContentPanel; + + private DynamicFieldsPanel dynamicFieldsPanel; + + private String[] loadedCredentialParams; + + public ClientScriptBasedAuthenticationMethodOptionsPanel() { + super(); + initialize(); + } + + private void initialize() { + this.setLayout(new GridBagLayout()); + + this.add(new JLabel(SCRIPT_NAME_LABEL), LayoutHelper.getGBC(0, 0, 1, 0.0d, 0.0d)); + + scriptsComboBox = new JXComboBox(); + scriptsComboBox.addHighlighter( + new FontHighlighter( + (renderer, adapter) -> loadedScript == adapter.getValue(), + scriptsComboBox.getFont().deriveFont(Font.BOLD))); + scriptsComboBox.setRenderer( + new DefaultListRenderer( + sw -> { + if (sw == null) { + return null; + } + + String name = ((ScriptWrapper) sw).getName(); + if (loadedScript == sw) { + return Constant.messages.getString( + "authentication.method.script.loaded", name); + } + return name; + })); + this.add(this.scriptsComboBox, LayoutHelper.getGBC(1, 0, 1, 1.0d, 0.0d)); + + this.loadScriptButton = + new JButton( + Constant.messages.getString( + "authentication.method.script.load.button")); + this.add(this.loadScriptButton, LayoutHelper.getGBC(2, 0, 1, 0.0d, 0.0d)); + this.loadScriptButton.addActionListener( + e -> loadScript((ScriptWrapper) scriptsComboBox.getSelectedItem(), true)); + + // Make sure the 'Load' button is disabled when nothing is selected + this.loadScriptButton.setEnabled(false); + this.scriptsComboBox.addActionListener( + e -> loadScriptButton.setEnabled(scriptsComboBox.getSelectedIndex() >= 0)); + + this.dynamicContentPanel = new JPanel(new BorderLayout()); + this.add(this.dynamicContentPanel, LayoutHelper.getGBC(0, 1, 3, 1.0d, 0.0d)); + this.dynamicContentPanel.add(new ZapHtmlLabel(LABEL_NOT_LOADED)); + } + + @Override + public void validateFields() throws IllegalStateException { + if (this.loadedScript == null) { + this.scriptsComboBox.requestFocusInWindow(); + throw new IllegalStateException( + Constant.messages.getString( + "authentication.method.script.dialog.error.text.notLoadedNorConfigured")); + } + this.dynamicFieldsPanel.validateFields(); + } + + @Override + public void saveMethod() { + this.method.script = (ScriptWrapper) this.scriptsComboBox.getSelectedItem(); + // This method will also be called when switching panels to save a temporary state so + // the state of the authentication method might not be valid + if (this.dynamicFieldsPanel != null) + this.method.paramValues = this.dynamicFieldsPanel.getFieldValues(); + else this.method.paramValues = Collections.emptyMap(); + if (this.loadedScript != null) + this.method.credentialsParamNames = this.loadedCredentialParams; + } + + @Override + @SuppressWarnings("unchecked") + public void bindMethod(AuthenticationMethod method) + throws UnsupportedAuthenticationMethodException { + this.method = (ClientScriptBasedAuthenticationMethod) method; + + // Make sure the list of scripts is refreshed with just Zest scripts + List scripts = + getExtensionScript().getScripts(SCRIPT_TYPE_AUTH).stream() + .filter(sc -> sc.getEngineName().contains("Zest")) + .toList(); + DefaultComboBoxModel model = + new DefaultComboBoxModel<>(scripts.toArray(new ScriptWrapper[scripts.size()])); + this.scriptsComboBox.setModel(model); + this.scriptsComboBox.setSelectedItem(this.method.script); + this.loadScriptButton.setEnabled(this.method.script != null); + + // Load the selected script, if any + if (this.method.script != null) { + loadScript(this.method.script, false); + if (this.dynamicFieldsPanel != null) + this.dynamicFieldsPanel.bindFieldValues(this.method.paramValues); + } + } + + @Override + public void bindMethod( + AuthenticationMethod method, AuthenticationIndicatorsPanel indicatorsPanel) + throws UnsupportedAuthenticationMethodException { + this.indicatorsPanel = indicatorsPanel; + bindMethod(method); + } + + @Override + public AuthenticationMethod getMethod() { + return this.method; + } + + private void loadScript(ScriptWrapper scriptW, boolean adaptOldValues) { + AuthenticationScript script = getAuthScriptInterfaceV2(scriptW); + if (script == null) { + script = getAuthScriptInterface(scriptW); + } + + if (script == null) { + LOGGER.warn( + "The script {} does not properly implement the Authentication Script interface.", + scriptW.getName()); + warnAndResetPanel( + Constant.messages.getString( + "authentication.method.script.dialog.error.text.interface", + scriptW.getName())); + return; + } + + try { + if (script instanceof AuthenticationScriptV2 scriptV2) { + String toolTip = + Constant.messages.getString( + "authentication.method.script.dialog.loggedInOutIndicatorsInScript.toolTip"); + String loggedInIndicator = scriptV2.getLoggedInIndicator(); + this.method.setLoggedInIndicatorPattern(loggedInIndicator); + this.indicatorsPanel.setLoggedInIndicatorPattern(loggedInIndicator); + this.indicatorsPanel.setLoggedInIndicatorEnabled(false); + this.indicatorsPanel.setLoggedInIndicatorToolTip(toolTip); + + String loggedOutIndicator = scriptV2.getLoggedOutIndicator(); + this.method.setLoggedOutIndicatorPattern(loggedOutIndicator); + this.indicatorsPanel.setLoggedOutIndicatorPattern(loggedOutIndicator); + this.indicatorsPanel.setLoggedOutIndicatorEnabled(false); + this.indicatorsPanel.setLoggedOutIndicatorToolTip(toolTip); + } else { + this.indicatorsPanel.setLoggedInIndicatorEnabled(true); + this.indicatorsPanel.setLoggedInIndicatorToolTip(null); + this.indicatorsPanel.setLoggedOutIndicatorEnabled(true); + this.indicatorsPanel.setLoggedOutIndicatorToolTip(null); + } + String[] requiredParams = script.getRequiredParamsNames(); + String[] optionalParams = script.getOptionalParamsNames(); + this.loadedCredentialParams = script.getCredentialsParamsNames(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "Loaded authentication script - required parameters: {} - optional parameters: {}", + Arrays.toString(requiredParams), + Arrays.toString(optionalParams)); + } + // If there's an already loaded script, make sure we save its values and _try_ + // to place them in the new panel + Map oldValues = null; + if (adaptOldValues && dynamicFieldsPanel != null) { + oldValues = dynamicFieldsPanel.getFieldValues(); + LOGGER.debug("Trying to adapt old values: {}", oldValues); + } + + this.dynamicFieldsPanel = new DynamicFieldsPanel(requiredParams, optionalParams); + this.loadedScript = scriptW; + if (adaptOldValues && oldValues != null) { + this.dynamicFieldsPanel.bindFieldValues(oldValues); + } + + this.dynamicContentPanel.removeAll(); + this.dynamicContentPanel.add(dynamicFieldsPanel, BorderLayout.CENTER); + this.dynamicContentPanel.revalidate(); + + } catch (Exception e) { + getExtensionScript().handleScriptException(scriptW, e); + LOGGER.error("Error while calling authentication script", e); + warnAndResetPanel( + Constant.messages.getString( + "authentication.method.script.dialog.error.text.loading", + ExceptionUtils.getRootCauseMessage(e))); + } + } + + private void warnAndResetPanel(String errorMessage) { + JOptionPane.showMessageDialog( + this, + errorMessage, + Constant.messages.getString("authentication.method.script.dialog.error.title"), + JOptionPane.ERROR_MESSAGE); + this.loadedScript = null; + this.scriptsComboBox.setSelectedItem(null); + this.dynamicFieldsPanel = null; + this.dynamicContentPanel.removeAll(); + this.dynamicContentPanel.add(new JLabel(LABEL_NOT_LOADED), BorderLayout.CENTER); + this.dynamicContentPanel.revalidate(); + } + } + + private ExtensionScript getExtensionScript() { + if (extensionScript == null) + extensionScript = + Control.getSingleton().getExtensionLoader().getExtension(ExtensionScript.class); + return extensionScript; + } + + private AuthenticationScript getAuthScriptInterface(ScriptWrapper script) { + try { + return getExtensionScript().getInterface(script, AuthenticationScript.class); + } catch (Exception e) { + getExtensionScript() + .handleFailedScriptInterface( + script, + Constant.messages.getString( + "authentication.method.script.dialog.error.text.interface", + script.getName())); + } + return null; + } + + private AuthenticationScriptV2 getAuthScriptInterfaceV2(ScriptWrapper script) { + try { + AuthenticationScriptV2 authScript = + getExtensionScript().getInterface(script, AuthenticationScriptV2.class); + if (authScript == null) { + LOGGER.debug( + "Script '{}' is not a AuthenticationScriptV2 interface.", script::getName); + return null; + } + + // Some ScriptEngines do not verify if all Interface Methods are contained in the + // script. + // So we must invoke them to ensure that they are defined in the loaded script! + // Otherwise some ScriptEngines loads successfully AuthenticationScriptV2 without the + // methods getLoggedInIndicator() / getLoggedOutIndicator(). + // Though it should fallback to interface AuthenticationScript. + authScript.getLoggedInIndicator(); + authScript.getLoggedOutIndicator(); + return authScript; + } catch (Exception ignore) { + // The interface is optional, the AuthenticationScript will be checked after this one. + LOGGER.debug( + "Script '{}' is not a AuthenticationScriptV2 interface!", + script.getName(), + ignore); + } + return null; + } + + @Override + public void exportData(Configuration config, AuthenticationMethod authMethod) { + if (!(authMethod instanceof ClientScriptBasedAuthenticationMethod)) { + throw new UnsupportedAuthenticationMethodException( + "Client script based authentication type only supports: " + + ClientScriptBasedAuthenticationMethod.class.getName()); + } + ClientScriptBasedAuthenticationMethod method = + (ClientScriptBasedAuthenticationMethod) authMethod; + config.setProperty(CONTEXT_CONFIG_AUTH_SCRIPT_NAME, method.script.getName()); + config.setProperty( + CONTEXT_CONFIG_AUTH_SCRIPT_PARAMS, EncodingUtils.mapToString(method.paramValues)); + } + + @Override + public void importData(Configuration config, AuthenticationMethod authMethod) + throws ConfigurationException { + if (!(authMethod instanceof ClientScriptBasedAuthenticationMethod)) { + throw new UnsupportedAuthenticationMethodException( + "Client script based authentication type only supports: " + + ClientScriptBasedAuthenticationMethod.class.getName()); + } + ClientScriptBasedAuthenticationMethod method = + (ClientScriptBasedAuthenticationMethod) authMethod; + this.loadMethod( + method, + objListToStrList(config.getList(CONTEXT_CONFIG_AUTH_SCRIPT_NAME)), + objListToStrList(config.getList(CONTEXT_CONFIG_AUTH_SCRIPT_PARAMS))); + } + + private static List objListToStrList(List oList) { + List sList = new ArrayList<>(oList.size()); + for (Object o : oList) { + sList.add(o.toString()); + } + return sList; + } +} diff --git a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/client/ExtensionAuthhelperClient.java b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/client/ExtensionAuthhelperClient.java index a7d0f964fcb..fa2d5eccab4 100644 --- a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/client/ExtensionAuthhelperClient.java +++ b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/client/ExtensionAuthhelperClient.java @@ -20,12 +20,17 @@ package org.zaproxy.addon.authhelper.client; import java.util.List; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.parosproxy.paros.Constant; import org.parosproxy.paros.control.Control; import org.parosproxy.paros.extension.Extension; import org.parosproxy.paros.extension.ExtensionAdaptor; import org.parosproxy.paros.extension.ExtensionHook; +import org.zaproxy.addon.authhelper.AuthUtils; +import org.zaproxy.addon.authhelper.ClientScriptBasedAuthenticationMethodType; import org.zaproxy.addon.client.ExtensionClientIntegration; +import org.zaproxy.zap.extension.authentication.ExtensionAuthentication; public class ExtensionAuthhelperClient extends ExtensionAdaptor { @@ -33,9 +38,13 @@ public class ExtensionAuthhelperClient extends ExtensionAdaptor { private static final List> DEPENDENCIES = List.of(ExtensionClientIntegration.class); + private static final Logger LOGGER = LogManager.getLogger(ExtensionAuthhelperClient.class); private BrowserBasedAuthHandler authHandler; + protected static final ClientScriptBasedAuthenticationMethodType CLIENT_SCRIPT_BASED_AUTH_TYPE = + new ClientScriptBasedAuthenticationMethodType(); + public ExtensionAuthhelperClient() { super(NAME); } @@ -45,6 +54,11 @@ public boolean supportsDb(String type) { return true; } + @Override + public List> getDependencies() { + return DEPENDENCIES; + } + @Override public void hook(ExtensionHook extensionHook) { super.hook(extensionHook); @@ -59,18 +73,27 @@ private static ExtensionClientIntegration getClientExtension() { } @Override - public boolean canUnload() { - return true; + public void optionsLoaded() { + ExtensionAuthentication extAuth = AuthUtils.getExtension(ExtensionAuthentication.class); + if (extAuth != null) { + extAuth.getAuthenticationMethodTypes().add(CLIENT_SCRIPT_BASED_AUTH_TYPE); + LOGGER.debug("Loaded client script based auth type."); + } } @Override public void unload() { getClientExtension().removeAuthenticationHandler(authHandler); + ExtensionAuthentication extAuth = AuthUtils.getExtension(ExtensionAuthentication.class); + + if (extAuth != null) { + extAuth.getAuthenticationMethodTypes().remove(CLIENT_SCRIPT_BASED_AUTH_TYPE); + } } @Override - public List> getDependencies() { - return DEPENDENCIES; + public boolean canUnload() { + return true; } @Override diff --git a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/internal/ZestAuthRunner.java b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/internal/ZestAuthRunner.java new file mode 100644 index 00000000000..71ef665f664 --- /dev/null +++ b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/internal/ZestAuthRunner.java @@ -0,0 +1,59 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2025 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.addon.authhelper.internal; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.openqa.selenium.WebDriver; +import org.zaproxy.zest.core.v1.ZestClient; +import org.zaproxy.zest.core.v1.ZestClientFailException; +import org.zaproxy.zest.core.v1.ZestClientLaunch; +import org.zaproxy.zest.core.v1.ZestScript; +import org.zaproxy.zest.impl.ZestBasicRunner; + +public class ZestAuthRunner extends ZestBasicRunner { + + private static final Logger LOGGER = LogManager.getLogger(ZestAuthRunner.class); + + private WebDriver webDriver; + + public ZestAuthRunner() { + super(); + } + + public void setWebDriver(WebDriver webDriver) { + this.webDriver = webDriver; + } + + @Override + public String handleClient(ZestScript script, ZestClient client) + throws ZestClientFailException { + LOGGER.debug("handleClient {}", client.getClass().getCanonicalName()); + + if (client instanceof ZestClientLaunch clientLaunch) { + this.addWebDriver(clientLaunch.getWindowHandle(), webDriver); + LOGGER.debug( + "handleClient client launch, registering {}", clientLaunch.getWindowHandle()); + this.webDriver.get(clientLaunch.getUrl()); + return clientLaunch.getWindowHandle(); + } + return super.handleClient(script, client); + } +} diff --git a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/spiderajax/ClientScriptBasedAuthHandler.java b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/spiderajax/ClientScriptBasedAuthHandler.java new file mode 100644 index 00000000000..4b121e6858b --- /dev/null +++ b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/spiderajax/ClientScriptBasedAuthHandler.java @@ -0,0 +1,114 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2025 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.addon.authhelper.spiderajax; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.openqa.selenium.WebDriver; +import org.zaproxy.addon.authhelper.AuthUtils; +import org.zaproxy.addon.authhelper.ClientScriptBasedAuthenticationMethodType; +import org.zaproxy.addon.authhelper.ClientScriptBasedAuthenticationMethodType.ClientScriptBasedAuthenticationMethod; +import org.zaproxy.addon.authhelper.internal.ZestAuthRunner; +import org.zaproxy.addon.network.ExtensionNetwork; +import org.zaproxy.addon.network.server.ServerInfo; +import org.zaproxy.zap.authentication.AuthenticationMethod; +import org.zaproxy.zap.extension.selenium.BrowserHook; +import org.zaproxy.zap.extension.selenium.ExtensionSelenium; +import org.zaproxy.zap.extension.selenium.SeleniumScriptUtils; +import org.zaproxy.zap.extension.spiderAjax.AuthenticationHandler; +import org.zaproxy.zap.model.Context; +import org.zaproxy.zap.users.User; +import org.zaproxy.zest.impl.ZestBasicRunner; + +public class ClientScriptBasedAuthHandler implements AuthenticationHandler { + + private static final Logger LOGGER = LogManager.getLogger(ClientScriptBasedAuthHandler.class); + + private BrowserHook browserHook; + + @Override + public boolean enableAuthentication(User user) { + Context context = user.getContext(); + if (context.getAuthenticationMethod() + instanceof + ClientScriptBasedAuthenticationMethodType.ClientScriptBasedAuthenticationMethod) { + + if (browserHook != null) { + throw new IllegalStateException("BrowserHook already enabled"); + } + browserHook = new AuthenticationBrowserHook(context, user); + + AuthUtils.getExtension(ExtensionSelenium.class).registerBrowserHook(browserHook); + + return true; + } + return false; + } + + @Override + public boolean disableAuthentication(User user) { + if (browserHook != null) { + AuthUtils.getExtension(ExtensionSelenium.class).deregisterBrowserHook(browserHook); + browserHook = null; + return true; + } + return false; + } + + static class AuthenticationBrowserHook implements BrowserHook { + + private ClientScriptBasedAuthenticationMethod csaMethod; + private Context context; + private ZestAuthRunner zestRunner; + + AuthenticationBrowserHook(Context context, User user) { + this.context = context; + AuthenticationMethod method = context.getAuthenticationMethod(); + if (!(method instanceof ClientScriptBasedAuthenticationMethod)) { + throw new IllegalStateException("Unsupported method " + method.getType().getName()); + } + csaMethod = (ClientScriptBasedAuthenticationMethod) method; + } + + private ZestBasicRunner getZestRunner(WebDriver webDriver) { + if (zestRunner == null) { + zestRunner = new ZestAuthRunner(); + // Always proxy via ZAP + ServerInfo mainProxyInfo = + AuthUtils.getExtension(ExtensionNetwork.class).getMainProxyServerInfo(); + zestRunner.setProxy(mainProxyInfo.getAddress(), mainProxyInfo.getPort()); + } + zestRunner.setWebDriver(webDriver); + return zestRunner; + } + + @Override + public void browserLaunched(SeleniumScriptUtils ssUtils) { + ZestBasicRunner runner = getZestRunner(ssUtils.getWebDriver()); + try { + runner.run(csaMethod.getZestScript(), null); + } catch (Exception e) { + LOGGER.warn( + "An error occurred while trying to execute the Client Script Authentication script: {}", + e.getMessage()); + } + } + } +} diff --git a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/spiderajax/ExtensionAuthhelperAjax.java b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/spiderajax/ExtensionAuthhelperAjax.java index 294790664b7..980de9cfe69 100644 --- a/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/spiderajax/ExtensionAuthhelperAjax.java +++ b/addOns/authhelper/src/main/java/org/zaproxy/addon/authhelper/spiderajax/ExtensionAuthhelperAjax.java @@ -35,6 +35,7 @@ public class ExtensionAuthhelperAjax extends ExtensionAdaptor { List.of(ExtensionAjax.class); private BrowserBasedAuthHandler authHandler; + private ClientScriptBasedAuthHandler scriptAuthHandler; public ExtensionAuthhelperAjax() { super(NAME); @@ -52,6 +53,9 @@ public void hook(ExtensionHook extensionHook) { Control.getSingleton().getExtensionLoader().getExtension(ExtensionAjax.class); authHandler = new BrowserBasedAuthHandler(); extAjax.addAuthenticationHandler(authHandler); + + scriptAuthHandler = new ClientScriptBasedAuthHandler(); + extAjax.addAuthenticationHandler(scriptAuthHandler); } @Override @@ -64,6 +68,7 @@ public void unload() { ExtensionAjax extAjax = Control.getSingleton().getExtensionLoader().getExtension(ExtensionAjax.class); extAjax.removeAuthenticationHandler(authHandler); + extAjax.removeAuthenticationHandler(scriptAuthHandler); } @Override diff --git a/addOns/authhelper/src/main/javahelp/org/zaproxy/addon/authhelper/resources/help/contents/authhelper.html b/addOns/authhelper/src/main/javahelp/org/zaproxy/addon/authhelper/resources/help/contents/authhelper.html index 520bb5e7b35..872e0dbd906 100644 --- a/addOns/authhelper/src/main/javahelp/org/zaproxy/addon/authhelper/resources/help/contents/authhelper.html +++ b/addOns/authhelper/src/main/javahelp/org/zaproxy/addon/authhelper/resources/help/contents/authhelper.html @@ -23,6 +23,7 @@

Authentication Helper

  • Authentication Request Identification
  • Auto-Detect Authentication
  • Browser Based Authentication +
  • Client Script Authentication
  • Session Management Identification
  • Auto-Detect Session Management
  • Header Based Session Management diff --git a/addOns/authhelper/src/main/javahelp/org/zaproxy/addon/authhelper/resources/help/contents/browser-auth.html b/addOns/authhelper/src/main/javahelp/org/zaproxy/addon/authhelper/resources/help/contents/browser-auth.html index b724eae7c76..0494ca04022 100644 --- a/addOns/authhelper/src/main/javahelp/org/zaproxy/addon/authhelper/resources/help/contents/browser-auth.html +++ b/addOns/authhelper/src/main/javahelp/org/zaproxy/addon/authhelper/resources/help/contents/browser-auth.html @@ -39,7 +39,7 @@

    Automation Framework

    AJAX Spider Integration

    -The AJAX Spider with automatically login if a user is specified which is in a context that uses Browser Based Authentication. +The AJAX Spider will automatically login if a user is specified which is in a context that uses Browser Based Authentication.

    Note that this uses the Selenium integration detailed below, so any browsers launched manually will also be logged in if the AJAX spider is performing an authenticated scan using Browser Based Authentication. diff --git a/addOns/authhelper/src/main/javahelp/org/zaproxy/addon/authhelper/resources/help/contents/client-script.html b/addOns/authhelper/src/main/javahelp/org/zaproxy/addon/authhelper/resources/help/contents/client-script.html new file mode 100644 index 00000000000..b88dabf76b6 --- /dev/null +++ b/addOns/authhelper/src/main/javahelp/org/zaproxy/addon/authhelper/resources/help/contents/client-script.html @@ -0,0 +1,49 @@ + + + + +Client Script Authentication + + + +

    Client Script Authentication

    + +This add-on adds a new authentication type which uses a browser to login to the target website. +

    +This functionality leverages Zest scripts (which may have been recorded via the ZAP Browser Extension) to login. +

    + +

    Automation Framework

    + +Client Script Authentication can be configured in the environment section of an Automation Framework plan using: +
    +      authentication:
    +        method: "client"
    +        parameters:
    +          script: /path/to/RecordedAuth.zst  # String, the path to the Zest login script
    +          scriptEngine: Mozilla Zest         # The script engine used for the login script
    +
    + +

    AJAX Spider Integration

    + +The AJAX Spider will automatically login if a user is specified which is in a context that uses Client Script Authentication. +

    +Note that this uses the Selenium integration detailed below, so any browsers launched manually will also be logged in if the +AJAX spider is performing an authenticated scan using Client Script Authentication. + +

    Selenium Integration

    + +Any browsers launched by ZAP can be configured to always first login using the details configured in a context. + +Note that due to restrictions in the core: +
      +
    • Existing contexts are not updated in the GUI if you add or remove this add-on +
    • Client Script Authentication cannot be added to a context via the API +
    +These restrictions will be addressed in a future release. + +

    +Latest code: ClientScriptBasedAuthenticationMethodType.java + + + diff --git a/addOns/authhelper/src/main/javahelp/org/zaproxy/addon/authhelper/resources/help/index.xml b/addOns/authhelper/src/main/javahelp/org/zaproxy/addon/authhelper/resources/help/index.xml index bee9f116dff..a2e8d237858 100644 --- a/addOns/authhelper/src/main/javahelp/org/zaproxy/addon/authhelper/resources/help/index.xml +++ b/addOns/authhelper/src/main/javahelp/org/zaproxy/addon/authhelper/resources/help/index.xml @@ -9,6 +9,7 @@ + diff --git a/addOns/authhelper/src/main/javahelp/org/zaproxy/addon/authhelper/resources/help/map.jhm b/addOns/authhelper/src/main/javahelp/org/zaproxy/addon/authhelper/resources/help/map.jhm index 5b4e1aac5f0..310751a3f25 100644 --- a/addOns/authhelper/src/main/javahelp/org/zaproxy/addon/authhelper/resources/help/map.jhm +++ b/addOns/authhelper/src/main/javahelp/org/zaproxy/addon/authhelper/resources/help/map.jhm @@ -9,6 +9,7 @@ + diff --git a/addOns/authhelper/src/main/javahelp/org/zaproxy/addon/authhelper/resources/help/toc.xml b/addOns/authhelper/src/main/javahelp/org/zaproxy/addon/authhelper/resources/help/toc.xml index 433ac2e1506..f102bae2933 100644 --- a/addOns/authhelper/src/main/javahelp/org/zaproxy/addon/authhelper/resources/help/toc.xml +++ b/addOns/authhelper/src/main/javahelp/org/zaproxy/addon/authhelper/resources/help/toc.xml @@ -11,6 +11,7 @@ + diff --git a/addOns/authhelper/src/main/resources/org/zaproxy/addon/authhelper/resources/Messages.properties b/addOns/authhelper/src/main/resources/org/zaproxy/addon/authhelper/resources/Messages.properties index e49a1235cd9..ab440ec5434 100644 --- a/addOns/authhelper/src/main/resources/org/zaproxy/addon/authhelper/resources/Messages.properties +++ b/addOns/authhelper/src/main/resources/org/zaproxy/addon/authhelper/resources/Messages.properties @@ -9,6 +9,8 @@ authhelper.auth.method.browser.label.loginWait = Login Wait in Seconds: authhelper.auth.method.browser.name = Browser-based Authentication authhelper.auth.method.browser.output.sessionid = Session token identified in History ID: {0} +authhelper.auth.method.clientscript.name = Client Script Authentication + authhelper.auth.test.dialog.button.copy = Copy authhelper.auth.test.dialog.button.save = Test diff --git a/addOns/automation/CHANGELOG.md b/addOns/automation/CHANGELOG.md index 89eb754fd29..3c878792d8d 100644 --- a/addOns/automation/CHANGELOG.md +++ b/addOns/automation/CHANGELOG.md @@ -8,6 +8,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Address malformed HTML in the help. - Correct default value of `threadPerHost` property of the `activeScan-config` job's help. +### Added +- Added support for Client Script Authentication when the Ajax Spider is used in conjunction with the Auth Helper add-on. + ## [0.44.0] - 2025-01-09 ### Added - Active scan policy job. diff --git a/addOns/automation/src/main/java/org/zaproxy/addon/automation/AuthenticationData.java b/addOns/automation/src/main/java/org/zaproxy/addon/automation/AuthenticationData.java index 81231295138..24a95331b14 100644 --- a/addOns/automation/src/main/java/org/zaproxy/addon/automation/AuthenticationData.java +++ b/addOns/automation/src/main/java/org/zaproxy/addon/automation/AuthenticationData.java @@ -30,6 +30,7 @@ import org.apache.commons.configuration.Configuration; import org.apache.commons.configuration.ConfigurationException; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.reflect.MethodUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.parosproxy.paros.Constant; @@ -50,6 +51,7 @@ import org.zaproxy.zap.utils.ZapXmlConfiguration; public class AuthenticationData extends AutomationData { + public static final String METHOD_HTTP = "http"; public static final String METHOD_FORM = "form"; public static final String METHOD_JSON = "json"; @@ -57,6 +59,7 @@ public class AuthenticationData extends AutomationData { public static final String METHOD_SCRIPT = "script"; public static final String METHOD_BROWSER = "browser"; public static final String METHOD_AUTO = "autodetect"; + public static final String METHOD_CLIENT = "client"; public static final String PARAM_HOSTNAME = "hostname"; public static final String PARAM_REALM = "realm"; @@ -69,6 +72,9 @@ public class AuthenticationData extends AutomationData { public static final String PARAM_SCRIPT = "script"; public static final String PARAM_SCRIPT_ENGINE = "scriptEngine"; + // TODO: Plan to change once the core supports dynamic methods better + protected static final String CLIENT_SCRIPT_BASED_AUTH_METHOD_CLASSNAME = + "org.zaproxy.addon.authhelper.client.ClientScriptBasedAuthenticationMethodType.ClientScriptBasedAuthenticationMethod"; protected static final String BROWSER_BASED_AUTH_METHOD_CLASSNAME = "org.zaproxy.addon.authhelper.BrowserBasedAuthenticationMethodType.BrowserBasedAuthenticationMethod"; @@ -76,6 +82,7 @@ public class AuthenticationData extends AutomationData { protected static final String FIELD_LOGIN_REQUEST_URL = "loginRequestURL"; private static final String BAD_FIELD_ERROR_MSG = "automation.error.env.auth.field.bad"; + private static final String PRIVATE_FIELD_SCRIPT = "script"; public static final String VERIFICATION_ELEMENT = "verification"; @@ -87,7 +94,8 @@ public class AuthenticationData extends AutomationData { METHOD_JSON, METHOD_SCRIPT, METHOD_BROWSER, - METHOD_AUTO); + METHOD_AUTO, + METHOD_CLIENT); private String method; private Map parameters = new LinkedHashMap<>(); @@ -121,10 +129,29 @@ public AuthenticationData(Context context) { JobUtils.addPrivateField( parameters, PARAM_LOGIN_REQUEST_URL, FIELD_LOGIN_REQUEST_URL, jsonAuthMethod); JobUtils.addPrivateField(parameters, PARAM_LOGIN_REQUEST_BODY, jsonAuthMethod); - } else if (authMethod instanceof ScriptBasedAuthenticationMethod) { - ScriptBasedAuthenticationMethod scriptAuthMethod = - (ScriptBasedAuthenticationMethod) authMethod; - ScriptWrapper sw = (ScriptWrapper) JobUtils.getPrivateField(scriptAuthMethod, "script"); + } else if (authMethod != null + && authMethod + .getClass() + .getCanonicalName() + .equals(CLIENT_SCRIPT_BASED_AUTH_METHOD_CLASSNAME)) { + ScriptWrapper sw = + (ScriptWrapper) JobUtils.getPrivateField(authMethod, PRIVATE_FIELD_SCRIPT); + LOGGER.debug("Matched client script class"); + if (sw != null) { + setMethod(METHOD_CLIENT); + parameters.put(PARAM_SCRIPT, sw.getFile().getAbsolutePath()); + parameters.put(PARAM_SCRIPT_ENGINE, sw.getEngineName()); + @SuppressWarnings("unchecked") + Map paramValues = + (Map) JobUtils.getPrivateField(authMethod, "paramValues"); + for (Entry entry : paramValues.entrySet()) { + parameters.put(entry.getKey(), entry.getValue()); + } + } + } else if (authMethod instanceof ScriptBasedAuthenticationMethod scriptAuthMethod) { + ScriptWrapper sw = + (ScriptWrapper) + JobUtils.getPrivateField(scriptAuthMethod, PRIVATE_FIELD_SCRIPT); if (sw != null) { setMethod(AuthenticationData.METHOD_SCRIPT); parameters.put(PARAM_SCRIPT, sw.getFile().getAbsolutePath()); @@ -291,6 +318,51 @@ public void initContextAuthentication( .get(AuthenticationData.PARAM_LOGIN_REQUEST_BODY))); context.setAuthenticationMethod(jsonAuthMethod); break; + case AuthenticationData.METHOD_CLIENT: + File clientScript = + JobUtils.getFile( + parameters.getOrDefault(PARAM_SCRIPT, "").toString(), + env.getPlan()); + if (!clientScript.exists() || !clientScript.canRead()) { + progress.error( + Constant.messages.getString( + "automation.error.env.sessionmgmt.script.bad", + clientScript.getAbsolutePath())); + } else { + ScriptWrapper sw = + JobUtils.getScriptWrapper( + clientScript, + ScriptBasedAuthenticationMethodType.SCRIPT_TYPE_AUTH, + parameters.getOrDefault(PARAM_SCRIPT_ENGINE, "").toString(), + progress); + + AuthenticationMethodType clientScriptType = + extAuth.getAuthenticationMethodTypeForIdentifier(8); + LOGGER.info("Loaded client script auth method type {}.", clientScriptType); + AuthenticationMethod clientScriptMethod = + clientScriptType.createAuthenticationMethod(context.getId()); + + if (sw == null) { + LOGGER.error( + "Error setting script authentication - failed to find script wrapper"); + progress.error( + Constant.messages.getString( + "automation.error.env.auth.script.bad", + clientScript.getAbsolutePath())); + } else { + try { + MethodUtils.invokeMethod(clientScriptMethod, "loadScript", sw); + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + } + JobUtils.setPrivateField( + clientScriptMethod, "paramValues", getScriptParameters(env)); + + reloadAuthenticationMethod(clientScriptMethod, progress); + context.setAuthenticationMethod(clientScriptMethod); + } + } + break; case AuthenticationData.METHOD_SCRIPT: File f = JobUtils.getFile( @@ -308,9 +380,12 @@ public void initContextAuthentication( ScriptBasedAuthenticationMethodType.SCRIPT_TYPE_AUTH, parameters.getOrDefault(PARAM_SCRIPT_ENGINE, "").toString(), progress); - ScriptBasedAuthenticationMethodType scriptType = + + AuthenticationMethodType scriptType = new ScriptBasedAuthenticationMethodType(); - ScriptBasedAuthenticationMethod scriptMethod = + LOGGER.debug("Loaded script auth method type"); + + AuthenticationMethod scriptMethod = scriptType.createAuthenticationMethod(context.getId()); if (sw == null) { @@ -321,7 +396,11 @@ public void initContextAuthentication( "automation.error.env.auth.script.bad", f.getAbsolutePath())); } else { - scriptMethod.loadScript(sw); + try { + MethodUtils.invokeMethod(scriptMethod, "loadScript", sw); + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + } JobUtils.setPrivateField( scriptMethod, "paramValues", getScriptParameters(env)); diff --git a/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/contents/authentication.html b/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/contents/authentication.html index b4172ed0cc1..e840e0ba4b7 100644 --- a/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/contents/authentication.html +++ b/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/contents/authentication.html @@ -29,6 +29,13 @@

    Context Based Authentication

  • Script
  • +The Automation Framework supports the following methods of authentication provided by the Authentication Helper add-on: +
      +
    • Auto-Detect Authentication
    • +
    • Browser-based Authentication
    • +
    • Client Script Authentication
    • +
    +

    Authentication Statistics

    ZAP maintains authentication statistics - search for 'auth' in the key field on the diff --git a/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/contents/environment.html b/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/contents/environment.html index d734bf76779..5ce42dad977 100644 --- a/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/contents/environment.html +++ b/addOns/automation/src/main/javahelp/org/zaproxy/addon/automation/resources/help/contents/environment.html @@ -27,7 +27,7 @@

    Automation Framework - Environment

    includePaths: # An optional list of regexes to include excludePaths: # An optional list of regexes to exclude authentication: - method: # String, one of 'manual', 'http', 'form', 'json' or 'script' + method: # String, one of 'manual', 'http', 'form', 'json', 'script', "autodetect", "browser", or "client" parameters: # May include any required for scripts. All of the parameters support vars except for the port hostname: # String, only for 'http' authentication port: # Int, only for 'http' authentication @@ -65,9 +65,9 @@

    Automation Framework - Environment

    password: # String, the password to use when authenticating, vars supported vars: # List of 0 or more custom variables to be used throughout the config file myVarOne: CustomConfigVarOne # Can be used as ${myVarOne} anywhere throughout the config - myVarTwo: ${myVarOne}.VarTwo # Can refer other vars + myVarTwo: ${myVarOne}.VarTwo # Can refer other vars parameters: - failOnError: true # If set exit on an error + failOnError: true # If set exit on an error failOnWarning: false # If set exit on a warning continueOnFailure: false # Continue running all jobs, even if one fails progressToStdout: true # If set will write job progress to stdout diff --git a/addOns/zest/src/main/java/org/zaproxy/zap/extension/zest/ZestAuthenticationRunner.java b/addOns/zest/src/main/java/org/zaproxy/zap/extension/zest/ZestAuthenticationRunner.java index 3d5279b3b43..bb8194adc1f 100644 --- a/addOns/zest/src/main/java/org/zaproxy/zap/extension/zest/ZestAuthenticationRunner.java +++ b/addOns/zest/src/main/java/org/zaproxy/zap/extension/zest/ZestAuthenticationRunner.java @@ -94,6 +94,16 @@ public String[] getCredentialsParamsNames() { return new String[] {USERNAME, PASSWORD}; } + private HttpMessageHandler handler; + + public void registerHandler(HttpMessageHandler handler) { + if (handler != null) { + LOGGER.debug( + "ZestAuthRunner register handler: {}", handler.getClass().getCanonicalName()); + } + this.handler = handler; + } + @Override public HttpMessage authenticate( AuthenticationHelper helper, @@ -110,7 +120,7 @@ public HttpMessage authenticate( getExtensionNetwork() .createHttpProxy( helper.getHttpSender(), - new ZestMessageHandler(this, helper)); + new ZestMessageHandler(this, helper, handler)); int port = proxyServer.start(PROXY_ADDRESS, Server.ANY_PORT); this.setProxy(PROXY_ADDRESS, port); } @@ -175,10 +185,13 @@ private static class ZestMessageHandler implements HttpMessageHandler { private final ZestBasicRunner runner; private final AuthenticationHelper helper; + private final HttpMessageHandler handler; - private ZestMessageHandler(ZestBasicRunner runner, AuthenticationHelper helper) { + private ZestMessageHandler( + ZestBasicRunner runner, AuthenticationHelper helper, HttpMessageHandler handler) { this.runner = runner; this.helper = helper; + this.handler = handler; } @Override @@ -199,6 +212,14 @@ public void handleMessage(HttpMessageHandlerContext ctx, HttpMessage msg) { ZestVariables.RESPONSE_URL, msg.getRequestHeader().getURI().toString()); runner.setVariable(ZestVariables.RESPONSE_HEADER, msg.getResponseHeader().toString()); runner.setVariable(ZestVariables.RESPONSE_BODY, msg.getResponseBody().toString()); + + if (handler != null) { + handler.handleMessage(ctx, msg); + } } } + + public ZestScriptWrapper getScript() { + return script; + } }