All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.zaproxy.zap.extension.api.API Maven / Gradle / Ivy

Go to download

The Zed Attack Proxy (ZAP) is an easy to use integrated penetration testing tool for finding vulnerabilities in web applications. It is designed to be used by people with a wide range of security experience and as such is ideal for developers and functional testers who are new to penetration testing. ZAP provides automated scanners as well as a set of tools that allow you to find security vulnerabilities manually.

The newest version!
/*
 * Zed Attack Proxy (ZAP) and its related class files.
 *
 * ZAP is an HTTP/HTTPS proxy for assessing web application security.
 *
 * Copyright 2011 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.extension.api;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import net.sf.json.JSONObject;
import org.apache.commons.httpclient.URIException;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.parosproxy.paros.core.scanner.NameValuePair;
import org.parosproxy.paros.core.scanner.VariantMultipartFormParameters;
import org.parosproxy.paros.model.Model;
import org.parosproxy.paros.network.HttpHeader;
import org.parosproxy.paros.network.HttpInputStream;
import org.parosproxy.paros.network.HttpMalformedHeaderException;
import org.parosproxy.paros.network.HttpMessage;
import org.parosproxy.paros.network.HttpOutputStream;
import org.parosproxy.paros.network.HttpRequestHeader;
import org.parosproxy.paros.view.View;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.zaproxy.zap.utils.JsonUtil;
import org.zaproxy.zap.utils.Stats;

public class API {
    public enum Format {
        XML,
        HTML,
        JSON,
        JSONP,
        UI,
        OTHER
    }

    public enum RequestType {
        action,
        view,
        other,
        pconn
    }

    /**
     * The custom domain to access the ZAP API while proxying through ZAP.
     *
     * @see #getBaseURL(boolean)
     */
    public static final String API_DOMAIN = "zap";

    /**
     * The HTTP URL to access the ZAP API while proxying through ZAP.
     *
     * @see #getBaseURL(boolean)
     */
    public static final String API_URL = "http://" + API_DOMAIN + "/";

    /**
     * The HTTPS URL to access the ZAP API while proxying through ZAP.
     *
     * @see #getBaseURL(boolean)
     */
    public static final String API_URL_S = "https://" + API_DOMAIN + "/";

    public static final String TRANSFER_DIR_TOKEN = "${XFER}";

    public static final String API_KEY_PARAM = "apikey";
    public static final String API_NONCE_PARAM = "apinonce";

    private static Pattern patternParam = Pattern.compile("&", Pattern.CASE_INSENSITIVE);
    private static final String CALL_BACK_URL = "/zapCallBackUrl/";

    private static final String STATUS_OK = "200 OK";
    private static final String STATUS_BAD_REQUEST = "400 Bad Request";
    private static final String STATUS_INTERNAL_SERVER_ERROR = "500 Internal Server Error";

    private static final String STATS_PREFIX = "stats.api.";

    private Map implementors = new HashMap<>();
    private static API api = null;
    private WebUI webUI = new WebUI(this);
    private Map callBacks = new HashMap<>();

    private Map shortcuts = new HashMap<>();

    private Map nonces = Collections.synchronizedMap(new HashMap<>());

    /**
     * The options for the API.
     *
     * @see #getOptionsParamApi()
     */
    private OptionsParamApi optionsParamApi;

    /**
     * The options of the local proxy.
     *
     * @see #getProxyParam()
     */
    @SuppressWarnings("deprecation")
    private org.parosproxy.paros.core.proxy.ProxyParam proxyParam;

    private Random random = new SecureRandom();
    private static final Logger LOGGER = LogManager.getLogger(API.class);

    private static synchronized API newInstance() {
        if (api == null) {
            api = new API();
        }
        return api;
    }

    public static API getInstance() {
        if (api == null) {
            newInstance();
        }
        return api;
    }

    /**
     * Registers the given {@code ApiImplementor} to the ZAP API.
     *
     * 

The implementor is not registered if the {@link ApiImplementor#getPrefix() API implementor * prefix} is already in use. * *

Note: The preferred method to add an {@code ApiImplementor} is through * the method {@link * org.parosproxy.paros.extension.ExtensionHook#addApiImplementor(ApiImplementor) * ExtensionHook.addApiImplementor(ApiImplementor)} when the corresponding {@link * org.parosproxy.paros.extension.Extension#hook(org.parosproxy.paros.extension.ExtensionHook) * extension is hooked}. Only use this method if really necessary. * * @param impl the implementor that will be registered * @see #removeApiImplementor(ApiImplementor) */ public void registerApiImplementor(ApiImplementor impl) { if (implementors.get(impl.getPrefix()) != null) { LOGGER.error( "Second attempt to register API implementor with prefix of {}", impl.getPrefix()); return; } implementors.put(impl.getPrefix(), impl); for (String shortcut : impl.getApiShortcuts()) { LOGGER.debug("Registering API shortcut: {}", shortcut); if (this.shortcuts.containsKey(shortcut)) { LOGGER.error("Duplicate API shortcut: {}", shortcut); } this.shortcuts.put("/" + shortcut, impl); } } /** * Removes the given {@code ApiImplementor} from the ZAP API. * *

Since 2.8.0, the callbacks previously created for the implementor are removed as well. * * @param impl the implementor that will be removed * @since 2.1.0 * @see #registerApiImplementor(ApiImplementor) * @see #getCallBackUrl(ApiImplementor, String) */ public void removeApiImplementor(ApiImplementor impl) { if (!implementors.containsKey(impl.getPrefix())) { LOGGER.warn( "Attempting to remove an API implementor not registered, with prefix: {}", impl.getPrefix()); return; } implementors.remove(impl.getPrefix()); for (String shortcut : impl.getApiShortcuts()) { String key = "/" + shortcut; if (this.shortcuts.containsKey(key)) { LOGGER.debug("Removing registered API shortcut: {}", shortcut); this.shortcuts.remove(key); } } removeCallBackUrls(impl); } public boolean isEnabled() { // Check API is enabled (its always enabled if run from the cmdline) if (View.isInitialised() && !getOptionsParamApi().isEnabled()) { return false; } return true; } private OptionsParamApi getOptionsParamApi() { if (optionsParamApi == null) { optionsParamApi = Model.getSingleton().getOptionsParam().getApiParam(); } return optionsParamApi; } void setOptionsParamApi(OptionsParamApi optionsParamApi) { this.optionsParamApi = optionsParamApi; } @SuppressWarnings("deprecation") private org.parosproxy.paros.core.proxy.ProxyParam getProxyParam() { if (proxyParam == null) { proxyParam = Model.getSingleton().getOptionsParam().getProxyParam(); } return proxyParam; } @SuppressWarnings("deprecation") void setProxyParam(org.parosproxy.paros.core.proxy.ProxyParam proxyParam) { this.proxyParam = proxyParam; } /** * Handles an API request, if necessary. * * @param requestHeader the request header * @param httpIn the HTTP input stream * @param httpOut the HTTP output stream * @return null if its not an API request, an empty message if it was silently dropped or the * API response sent. * @throws IOException */ public HttpMessage handleApiRequest( HttpRequestHeader requestHeader, HttpInputStream httpIn, HttpOutputStream httpOut) throws IOException { return this.handleApiRequest(requestHeader, httpIn, httpOut, false); } private boolean isPermittedAddr(HttpRequestHeader requestHeader) { if (getOptionsParamApi() .isPermittedAddress(requestHeader.getSenderAddress().getHostAddress())) { if (getOptionsParamApi().isPermittedAddress(requestHeader.getHostName())) { return true; } LOGGER.warn( "Request to API URL {} with host header {} not permitted", requestHeader.getURI(), requestHeader.getHostName()); return false; } LOGGER.warn( "Request to API URL {} from {} not permitted", requestHeader.getURI(), requestHeader.getSenderAddress().getHostAddress()); return false; } /** * Handles an API request, if necessary. * * @param requestHeader the request header * @param httpIn the HTTP input stream * @param httpOut the HTTP output stream * @param force if set then always handle an API request (will not return null) * @return null if its not an API request, an empty message if it was silently dropped or the * API response sent. * @throws IOException */ public HttpMessage handleApiRequest( HttpRequestHeader requestHeader, HttpInputStream httpIn, HttpOutputStream httpOut, boolean force) throws IOException { String url = requestHeader.getURI().toString(); Format format = Format.OTHER; ApiImplementor callbackImpl = null; ApiImplementor shortcutImpl = null; // Check for callbacks if (url.contains(CALL_BACK_URL)) { LOGGER.debug("handleApiRequest Callback: {}", url); for (Entry callback : callBacks.entrySet()) { if (url.startsWith(callback.getKey())) { callbackImpl = callback.getValue(); break; } } if (callbackImpl == null) { for (Entry entry : this.getOptionsParamApi().getPersistentCallBacks().entrySet()) { if (url.startsWith(entry.getKey())) { callbackImpl = this.getImplementors().get(entry.getValue()); if (callbackImpl != null) { break; } } } } if (callbackImpl == null) { LOGGER.warn( "Request to callback URL {} from {} not found - this could be a callback url from a previous session or possibly an attempt to attack ZAP", requestHeader.getURI(), requestHeader.getSenderAddress().getHostAddress()); return new HttpMessage(); } } String path = requestHeader.getURI().getPath(); if (path != null) { for (Entry shortcut : shortcuts.entrySet()) { if (path.startsWith(shortcut.getKey())) { shortcutImpl = shortcut.getValue(); break; } } } if (callbackImpl == null && !url.startsWith(API_URL) && !url.startsWith(API_URL_S) && !force) { return null; } if (callbackImpl == null && !isPermittedAddr(requestHeader)) { // Callback by their very nature are on the target domain return new HttpMessage(); } if (getOptionsParamApi().isSecureOnly() && !requestHeader.isSecure()) { // Insecure request with secure only set, always ignore LOGGER.debug("handleApiRequest rejecting insecure request"); return new HttpMessage(); } LOGGER.debug("handleApiRequest {}", url); HttpMessage msg = new HttpMessage(); msg.setRequestHeader(requestHeader); if (requestHeader.getContentLength() > 0) { msg.setRequestBody(httpIn.readRequestBody(requestHeader)); } String component = null; ApiImplementor impl = null; RequestType reqType = null; String contentType = "text/plain; charset=UTF-8"; String response = ""; String name = null; boolean error = false; try { if (shortcutImpl != null) { if (!getOptionsParamApi().isDisableKey() && !getOptionsParamApi().isNoKeyForSafeOps()) { if (!this.hasValidKey(requestHeader, getParams(requestHeader))) { throw new ApiException(ApiException.Type.BAD_API_KEY); } } msg = shortcutImpl.handleShortcut(msg); impl = shortcutImpl; } else if (callbackImpl != null) { // Callbacks have suitably random URLs and therefore don't require keys/nonces response = callbackImpl.handleCallBack(msg); impl = callbackImpl; } else { JSONObject params = getParams(requestHeader); // Parse the query: // format of url is http://zap/format/component/reqtype/name/?params // 0 1 2 3 4 5 6 String[] elements = url.split("/"); if (elements.length > 3 && elements[3].equalsIgnoreCase("favicon.ico")) { // Treat the favicon as a special case:) if (!getOptionsParamApi().isUiEnabled()) { throw new ApiException(ApiException.Type.DISABLED); } InputStream is = API.class.getResourceAsStream("/resource/zap.ico"); byte[] icon = new byte[is.available()]; is.read(icon); is.close(); msg.setResponseHeader(getDefaultResponseHeader(contentType)); msg.getResponseHeader().setContentLength(icon.length); msg.getResponseBody().setBody(icon); httpOut.write(msg.getResponseHeader()); httpOut.write(icon); httpOut.flush(); httpOut.close(); httpIn.close(); return msg; } else if (elements.length > 3) { try { format = Format.valueOf(elements[3].toUpperCase()); switch (format) { case JSONP: contentType = "application/javascript; charset=UTF-8"; break; case XML: contentType = "text/xml; charset=UTF-8"; break; case HTML: contentType = "text/html; charset=UTF-8"; break; case UI: contentType = "text/html; charset=UTF-8"; break; case JSON: default: contentType = "application/json; charset=UTF-8"; break; } } catch (IllegalArgumentException e) { format = Format.HTML; throw new ApiException(ApiException.Type.BAD_FORMAT, e); } } if (elements.length > 4) { component = elements[4]; impl = implementors.get(component); if (impl == null) { throw new ApiException(ApiException.Type.NO_IMPLEMENTOR); } } if (elements.length > 5) { try { reqType = RequestType.valueOf(elements[5]); } catch (IllegalArgumentException e) { throw new ApiException(ApiException.Type.BAD_TYPE, e); } } if (elements.length > 6) { name = elements[6]; if (name != null && name.indexOf("?") > 0) { name = name.substring(0, name.indexOf("?")); } } if (format.equals(Format.UI)) { if (!isEnabled() || !getOptionsParamApi().isUiEnabled()) { throw new ApiException(ApiException.Type.DISABLED); } response = webUI.handleRequest(component, impl, reqType, name); contentType = "text/html; charset=UTF-8"; } else if (name != null) { if (!isEnabled()) { throw new ApiException(ApiException.Type.DISABLED); } // Do this now as it might contain the api key/nonce if (requestHeader.getMethod().equalsIgnoreCase(HttpRequestHeader.POST)) { String contentTypeHeader = requestHeader.getHeader(HttpHeader.CONTENT_TYPE); if (contentTypeHeader != null && contentTypeHeader.equals( HttpHeader.FORM_URLENCODED_CONTENT_TYPE)) { params = getParams(msg.getRequestBody().toString()); } else if (contentTypeHeader != null && contentTypeHeader.startsWith( HttpHeader.FORM_MULTIPART_CONTENT_TYPE)) { VariantMultipartFormParameters tmpVarent = new VariantMultipartFormParameters(); tmpVarent.setMessage(msg); params = new JSONObject(); for (NameValuePair param : tmpVarent.getParamList()) { params.put( param.getName(), JsonUtil.getJsonFriendlyString(param.getValue())); } } else { throw new ApiException(ApiException.Type.CONTENT_TYPE_NOT_SUPPORTED); } } if (format.equals(Format.JSONP)) { if (!getOptionsParamApi().isEnableJSONP()) { // Not enabled throw new ApiException(ApiException.Type.DISABLED); } if (!this.hasValidKey(requestHeader, params)) { // An api key is required for ALL JSONP requests throw new ApiException(ApiException.Type.BAD_API_KEY); } } ApiResponse res; if (reqType == null) { throw new ApiException( ApiException.Type.BAD_TYPE, "Request Type was not provided."); } if (impl == null) { throw new ApiException( ApiException.Type.NO_IMPLEMENTOR, "Implementor was not provided."); } incStatistic("call", format, component, reqType, name); switch (reqType) { case action: if (!getOptionsParamApi().isDisableKey()) { if (!this.hasValidKey(requestHeader, params)) { throw new ApiException(ApiException.Type.BAD_API_KEY); } } validateFormatForViewAction(format); ApiAction action = impl.getApiAction(name); validateMandatoryParams(params, action); res = impl.handleApiOptionAction(name, params); if (res == null) { res = impl.handleApiAction(name, params); } response = convertViewActionApiResponse(format, name, res); break; case view: if (!getOptionsParamApi().isDisableKey() && !getOptionsParamApi().isNoKeyForSafeOps()) { if (!this.hasValidKey(requestHeader, params)) { throw new ApiException(ApiException.Type.BAD_API_KEY); } } validateFormatForViewAction(format); ApiView view = impl.getApiView(name); validateMandatoryParams(params, view); res = impl.handleApiOptionView(name, params); if (res == null) { res = impl.handleApiView(name, params); } response = convertViewActionApiResponse(format, name, res); break; case other: ApiOther other = impl.getApiOther(name); if (other != null) { // Checking for null to handle option actions if (!getOptionsParamApi().isDisableKey() && (!getOptionsParamApi().isNoKeyForSafeOps() || other.isRequiresApiKey())) { // Check if a valid api key has been used if (!this.hasValidKey(requestHeader, params)) { throw new ApiException(ApiException.Type.BAD_API_KEY); } } validateMandatoryParams(params, other); } msg = impl.handleApiOther(msg, name, params); break; case pconn: ApiPersistentConnection pconn = impl.getApiPersistentConnection(name); if (pconn != null) { if (!getOptionsParamApi().isDisableKey() && !getOptionsParamApi().isNoKeyForSafeOps()) { if (!this.hasValidKey(requestHeader, params)) { throw new ApiException(ApiException.Type.BAD_API_KEY); } } validateMandatoryParams(params, pconn); } impl.handleApiPersistentConnection(msg, httpIn, httpOut, name, params); return new HttpMessage(); } } else { // Handle default front page, unless if the API UI is disabled if (!isEnabled() || !getOptionsParamApi().isUiEnabled()) { throw new ApiException(ApiException.Type.DISABLED); } response = webUI.handleRequest(requestHeader.getURI(), this.isEnabled()); format = Format.UI; contentType = "text/html; charset=UTF-8"; } } } catch (Exception e) { if (!getOptionsParamApi().isReportPermErrors()) { if (component != null && format != null && reqType != null && name != null) { incStatistic("error", format, component, reqType, name); } if (e instanceof ApiException) { ApiException exception = (ApiException) e; if (exception.getType().equals(ApiException.Type.DISABLED) || exception.getType().equals(ApiException.Type.BAD_API_KEY)) { // Fail silently return new HttpMessage(); } } } handleException(msg, reqType, format, contentType, e); error = true; } if (!error && !format.equals(Format.OTHER) && shortcutImpl == null && callbackImpl == null) { msg.setResponseHeader(getDefaultResponseHeader(contentType)); msg.setResponseBody(response); msg.getResponseHeader().setContentLength(msg.getResponseBody().length()); } if (impl != null) { impl.addCustomHeaders(name, reqType, msg); } httpOut.write(msg.getResponseHeader()); httpOut.write(msg.getResponseBody().getBytes()); httpOut.flush(); if (!msg.isWebSocketUpgrade()) { httpOut.close(); httpIn.close(); } return msg; } private void incStatistic( String type, Format format, String component, RequestType reqType, String name) { Stats.incCounter( STATS_PREFIX + type + "." + format.name().toLowerCase(Locale.ROOT) + "." + component + "." + reqType.name() + "." + name); } /** * Validates that the mandatory parameters of the given {@code ApiElement} are present, throwing * an {@code ApiException} if not. * * @param params the parameters of the API request. * @param element the API element to validate. * @throws ApiException if any of the mandatory parameters is missing. */ private void validateMandatoryParams(JSONObject params, ApiElement element) throws ApiException { if (element == null) { return; } for (ApiParameter parameter : element.getParameters()) { if (!parameter.isRequired()) { continue; } String name = parameter.getName(); if (!params.has(name) || params.getString(name).length() == 0) { throw new ApiException(ApiException.Type.MISSING_PARAMETER, name); } } } /** * Converts the given {@code ApiResponse} to {@code String} representation. * *

This is expected to be used just for views and actions. * * @param format the format to convert to. * @param name the name of the view or action. * @param res the {@code ApiResponse} to convert. * @return the string representation of the {@code ApiResponse}. * @throws ApiException if an error occurred while converting the response or if the format was * not handled. * @see #validateFormatForViewAction(Format) */ private static String convertViewActionApiResponse(Format format, String name, ApiResponse res) throws ApiException { switch (format) { case JSON: return res.toJSON().toString(); case JSONP: return getJsonpWrapper(res.toJSON().toString()); case XML: return responseToXml(name, res); case HTML: return responseToHtml(res); default: // Should not happen, format validation should prevent this case... LOGGER.error("Unhandled format: {}", format); throw new ApiException(ApiException.Type.INTERNAL_ERROR); } } /** * Validates that the given format is supported for views and actions. * * @param format the format to validate. * @throws ApiException if the format is not valid. * @see #convertViewActionApiResponse(Format, String, ApiResponse) */ private static void validateFormatForViewAction(Format format) throws ApiException { switch (format) { case JSON: case JSONP: case XML: case HTML: return; default: throw new ApiException( ApiException.Type.BAD_FORMAT, "The format OTHER should not be used with views and actions."); } } /** * Returns a URI for the specified parameters. * *

An {@link #getOneTimeNonce(String) one time nonce query parameter} is added to the * resulting URL, if required (that is, not a view). In this case the URL is ended with an * ampersand (for example, {@code https://zap/format/prefix/action/name/?apinonce=xyz&}), * otherwise it has a trailing slash (for example, {@code http://zap/format/prefix/view/name/}). * * @param format the format of the API response * @param prefix the prefix of the API implementor * @param type the request type * @param name the name of the endpoint * @param proxy if true then the URI returned will only work if proxying via ZAP, i.e. it will * start with http://zap/.. * @return the URL to access the defined endpoint * @see #getBaseURL(boolean) */ public String getBaseURL( API.Format format, String prefix, API.RequestType type, String name, boolean proxy) { String apiPath = format.name() + "/" + prefix + "/" + type.name() + "/" + name + "/"; if (!RequestType.view.equals(type)) { return getBaseURL(proxy) + apiPath + "?" + API_NONCE_PARAM + "=" + this.getOneTimeNonce("/" + apiPath) + "&"; } return getBaseURL(proxy) + apiPath; } /** * Gets the base URL to access the ZAP API, possibly proxying through ZAP. * *

If proxying through ZAP the base URL will use the custom domain, {@value #API_DOMAIN}. * *

The resulting base URL has a trailing slash, for example, {@code https://127.0.0.1/} or * {@code https://zap/}. * * @param proxy {@code true} if the URL will be accessed while proxying through ZAP, {@code * false} otherwise. * @return the base URL to access the ZAP API. * @since 2.7.0 */ @SuppressWarnings("deprecation") public String getBaseURL(boolean proxy) { if (proxy) { return getOptionsParamApi().isSecureOnly() ? API_URL_S : API_URL; } StringBuilder strBuilder = new StringBuilder(50); strBuilder.append("http"); if (getOptionsParamApi().isSecureOnly()) { strBuilder.append('s'); } strBuilder .append("://") .append(getProxyParam().getProxyIp()) .append(':') .append(getProxyParam().getProxyPort()) .append('/'); return strBuilder.toString(); } /** * Gets the HTML representation of the given API {@code response}. * *

An empty HTML head with the HTML body containing the API response (as given by {@link * ApiResponse#toHTML(StringBuilder)}. * * @param response the API response, must not be {@code null}. * @return the HTML representation of the given response. */ static String responseToHtml(ApiResponse response) { StringBuilder sb = new StringBuilder(); sb.append("\n"); sb.append("\n"); sb.append("\n"); response.toHTML(sb); sb.append("\n"); return sb.toString(); } /** * Gets the XML representation of the given API {@code response}. * *

An XML element named with name of the endpoint and with child elements as given by {@link * ApiResponse#toXML(Document, Element)}. * * @param endpointName the name of the API endpoint, must not be {@code null}. * @param response the API response, must not be {@code null}. * @return the XML representation of the given response. * @throws ApiException if an error occurred while converting the response. */ static String responseToXml(String endpointName, ApiResponse response) throws ApiException { try { DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); Document doc = docBuilder.newDocument(); Element rootElement = doc.createElement(endpointName); doc.appendChild(rootElement); response.toXML(doc, rootElement); TransformerFactory transformerFactory = TransformerFactory.newInstance(); Transformer transformer = transformerFactory.newTransformer(); DOMSource source = new DOMSource(doc); StringWriter sw = new StringWriter(); StreamResult result = new StreamResult(sw); transformer.transform(source, result); return sw.toString(); } catch (Exception e) { LOGGER.error("Failed to convert API response to XML: {}", e.getMessage(), e); throw new ApiException(ApiException.Type.INTERNAL_ERROR, e); } } private static JSONObject getParams(HttpRequestHeader requestHeader) throws ApiException { return getParams(requestHeader.getURI().getEscapedQuery()); } /** * Detects and replaces the TRANSFER_DIR_TOKEN at the start of relevant params * * @param key the parameter key * @param value the parameter value * @return the value with the TRANSFER_DIR_TOKEN replaced if relevant */ private static String replaceXferTokens(String key, String value) { String keyLc = key.toLowerCase(Locale.ROOT); if ((keyLc.contains("file") || keyLc.contains("path") || keyLc.contains("dir")) && value.startsWith(TRANSFER_DIR_TOKEN)) { String relPath = value.substring(TRANSFER_DIR_TOKEN.length()); if (!relPath.startsWith("/") && !relPath.startsWith("\\")) { // Cope with the user not specifying a slash relPath = "/" + relPath; } return Model.getSingleton().getOptionsParam().getApiParam().getTransferDir().toString() + relPath; } return value; } private static String decodeParam(String param) throws UnsupportedEncodingException { return URLDecoder.decode(param, "UTF-8"); } public static JSONObject getParams(String params) throws ApiException { JSONObject jp = new JSONObject(); if (params == null || params.length() == 0) { return jp; } String[] keyValue = patternParam.split(params); String key = null; String value = null; int pos = 0; for (int i = 0; i < keyValue.length; i++) { key = null; pos = keyValue[i].indexOf('='); if (pos > 0) { // param found try { key = decodeParam(keyValue[i].substring(0, pos)); value = replaceXferTokens(key, decodeParam(keyValue[i].substring(pos + 1))); jp.put(key, JsonUtil.getJsonFriendlyString(value)); } catch (UnsupportedEncodingException | IllegalArgumentException e) { // Carry on anyway Exception apiException = new ApiException(ApiException.Type.ILLEGAL_PARAMETER, params, e); LOGGER.error(apiException.getMessage(), apiException); } } else { // Carry on anyway Exception e = new ApiException(ApiException.Type.ILLEGAL_PARAMETER, params); LOGGER.error(e.getMessage(), e); } } return jp; } private static String getJsonpWrapper(String json) { return "zapJsonpResult (" + json + " )"; } public Map getImplementors() { return Collections.unmodifiableMap(implementors); } /** * Gets (and registers) a new callback URL for the given implementor and site. * * @param impl the implementor that will handle the callback. * @param site the site that will call the callback. * @return the callback URL. * @since 2.0.0 * @see #removeCallBackUrl(String) * @see #removeCallBackUrls(ApiImplementor) * @see #removeApiImplementor(ApiImplementor) */ public String getCallBackUrl(ApiImplementor impl, String site) { String url = site + CALL_BACK_URL + random.nextLong(); this.callBacks.put(url, impl); LOGGER.debug("Callback {} registered for {}", url, impl.getClass().getCanonicalName()); return url; } /** * Removes the given callback URL. * * @param url the callback URL. * @since 2.8.0 * @see #getCallBackUrl(ApiImplementor, String) * @see #removeCallBackUrls(ApiImplementor) * @see #removeApiImplementor(ApiImplementor) */ public void removeCallBackUrl(String url) { LOGGER.debug("Callback {} removed", url); this.callBacks.remove(url); } /** * Removes the given implementor as a callback handler. * * @param impl the implementor to remove. * @throws IllegalArgumentException if the given parameter is {@code null}. * @since 2.8.0 * @see #getCallBackUrl(ApiImplementor, String) * @see #removeCallBackUrl(String) * @see #removeApiImplementor(ApiImplementor) */ public void removeCallBackUrls(ApiImplementor impl) { if (impl == null) { throw new IllegalArgumentException("Parameter impl must not be null."); } LOGGER.debug("All callbacks removed for {}", impl.getClass().getCanonicalName()); this.callBacks.values().removeIf(impl::equals); } /** * Get a callback which persists over ZAP restarts - will create one if it doesn't exist * * @param impl the ApiImplementor requesting the callback URL * @param site the site the callback will be on * @return a callback URL * @since 2.12.0 */ public String getPersistentCallBackUrl(ApiImplementor impl, String site) { for (Entry entry : this.getOptionsParamApi().getPersistentCallBacks().entrySet()) { String url = entry.getKey(); if (url.startsWith(site) && entry.getValue().equals(impl.getPrefix())) { return url; } } String url = site + CALL_BACK_URL + random.nextLong(); this.getOptionsParamApi().addPersistantCallBack(url, impl.getPrefix()); return url; } /** * Remove a callback which would otherwise persist over ZAP restarts * * @param url the persistent callback URL * @return true if the callback was deleted - false means the callback URL was not found * @since 2.12.0 */ public boolean removePersistentCallBackUrl(String url) { return this.getOptionsParamApi().removePersistantCallBack(url) != null; } /** * Returns a one time nonce to be used with the API call specified by the URL * * @param apiUrl the API URL * @return a one time nonce * @since 2.6.0 */ public String getOneTimeNonce(String apiUrl) { String nonce = Long.toHexString(random.nextLong()); this.nonces.put(nonce, new Nonce(nonce, apiUrl, true)); return nonce; } /** * Returns a nonce that will be valid for the lifetime of the ZAP process to used with the API * call specified by the URL * * @param apiUrl the API URL * @return a nonce that will be valid for the lifetime of the ZAP process * @since 2.6.0 */ public String getLongLivedNonce(String apiUrl) { String nonce = Long.toHexString(random.nextLong()); this.nonces.put(nonce, new Nonce(nonce, apiUrl, false)); return nonce; } /** * Returns true if the API call has a valid key * * @param msg the message * @return true if the API call has a valid key * @since 2.6.0 */ public boolean hasValidKey(HttpMessage msg) { try { HttpRequestHeader requestHeader = msg.getRequestHeader(); return this.hasValidKey(requestHeader, getParams(requestHeader)); } catch (ApiException e) { LOGGER.error(e.getMessage(), e); return false; } } /** * Returns true if the API call has a valid key * * @param reqHeader the request header * @param params the parameters * @return true if the API call has a valid key * @since 2.6.0 */ public boolean hasValidKey(HttpRequestHeader reqHeader, JSONObject params) { try { String apiPath; try { apiPath = reqHeader.getURI().getPath(); } catch (URIException e) { LOGGER.error(e.getMessage(), e); return false; } String nonceParam = reqHeader.getHeader(HttpHeader.X_ZAP_API_NONCE); if (nonceParam == null && params.has(API_NONCE_PARAM)) { nonceParam = params.getString(API_NONCE_PARAM); } if (nonceParam != null) { Nonce nonce = nonces.get(nonceParam); if (nonce == null) { LOGGER.warn( "API nonce {} not found in request from {}", nonceParam, reqHeader.getSenderAddress().getHostAddress()); return false; } else if (nonce.isOneTime()) { nonces.remove(nonceParam); } if (!nonce.isValid()) { LOGGER.warn( "API nonce {} expired at {} in request from {}", nonce.getNonceKey(), nonce.getExpires(), reqHeader.getSenderAddress().getHostAddress()); return false; } if (!apiPath.equals(nonce.getApiPath())) { LOGGER.warn( "API nonce path was {} but call was for {} in request from {}", nonce.getApiPath(), apiPath, reqHeader.getSenderAddress().getHostAddress()); return false; } } else { String keyParam = reqHeader.getHeader(HttpHeader.X_ZAP_API_KEY); if (keyParam == null && params.has(API_KEY_PARAM)) { keyParam = params.getString(API_KEY_PARAM); } if (!getOptionsParamApi().getKey().equals(keyParam)) { LOGGER.warn( "API key incorrect or not supplied: {} in request from {}", keyParam, reqHeader.getSenderAddress().getHostAddress()); return false; } } return true; } finally { synchronized (nonces) { for (Iterator it = nonces.values().iterator(); it.hasNext(); ) { if (!it.next().isValid()) { it.remove(); } } } } } public static String getDefaultResponseHeader(String contentType) { return getDefaultResponseHeader(contentType, 0); } public static String getDefaultResponseHeader(String contentType, int contentLength) { return getDefaultResponseHeader(STATUS_OK, contentType, contentLength, false); } public static String getDefaultResponseHeader( String contentType, int contentLength, boolean canCache) { return getDefaultResponseHeader(STATUS_OK, contentType, contentLength, canCache); } public static String getDefaultResponseHeader( String responseStatus, String contentType, int contentLength) { return getDefaultResponseHeader(responseStatus, contentType, contentLength, false); } public static String getDefaultResponseHeader( String responseStatus, String contentType, int contentLength, boolean canCache) { StringBuilder sb = new StringBuilder(250); sb.append("HTTP/1.1 ").append(responseStatus).append("\r\n"); if (!canCache) { sb.append("Pragma: no-cache\r\n"); sb.append("Cache-Control: no-cache, no-store, must-revalidate\r\n"); } sb.append( "Content-Security-Policy: default-src 'none'; script-src 'self'; connect-src 'self'; child-src 'self'; img-src 'self' data:; font-src 'self' data:; style-src 'self'\r\n"); sb.append("Referrer-Policy: no-referrer\r\n"); sb.append("Access-Control-Allow-Methods: GET,POST,OPTIONS\r\n"); sb.append("Access-Control-Allow-Headers: ZAP-Header\r\n"); sb.append("X-Frame-Options: DENY\r\n"); sb.append("X-XSS-Protection: 1; mode=block\r\n"); sb.append("X-Content-Type-Options: nosniff\r\n"); sb.append("X-Clacks-Overhead: GNU Terry Pratchett\r\n"); sb.append("Content-Length: ").append(contentLength).append("\r\n"); if (contentType != null) { sb.append("Content-Type: ").append(contentType).append("\r\n"); } return sb.toString(); } private void handleException( HttpMessage msg, RequestType reqType, Format format, String contentType, Exception cause) { String responseStatus = STATUS_INTERNAL_SERVER_ERROR; if (reqType == RequestType.other) { boolean logError = true; if (cause instanceof ApiException) { switch (((ApiException) cause).getType()) { case DISABLED: case BAD_TYPE: case NO_IMPLEMENTOR: case BAD_API_KEY: case MISSING_PARAMETER: case BAD_ACTION: case BAD_VIEW: case BAD_OTHER: responseStatus = STATUS_BAD_REQUEST; logBadRequest(msg, cause); logError = false; break; default: } } if (logError) { LOGGER.error("API 'other' endpoint didn't handle exception:", cause); } } else { ApiException exception; if (cause instanceof ApiException) { exception = (ApiException) cause; if (!ApiException.Type.INTERNAL_ERROR.equals(exception.getType())) { responseStatus = STATUS_BAD_REQUEST; logBadRequest(msg, cause); } } else { exception = new ApiException(ApiException.Type.INTERNAL_ERROR, cause); LOGGER.error("Exception while handling API request:", cause); } String response = exception.toString( format != Format.OTHER ? format : Format.JSON, getOptionsParamApi().isIncErrorDetails()); msg.getResponseBody().setCharset(getCharset(contentType)); msg.getResponseBody().setBody(response); } try { msg.setResponseHeader( getDefaultResponseHeader( responseStatus, contentType, msg.getResponseBody().length())); } catch (HttpMalformedHeaderException e) { LOGGER.warn("Failed to build API error response:", e); } } private static void logBadRequest(HttpMessage msg, Exception cause) { LOGGER.warn( "Bad request to API endpoint [{}] from [{}]:", msg.getRequestHeader().getURI().getEscapedPath(), msg.getRequestHeader().getSenderAddress().getHostAddress(), cause); } private static String getCharset(String contentType) { int idx = contentType.indexOf("charset="); if (idx == -1) { return "UTF-8"; } return contentType.substring(idx + 8); } private class Nonce { private final String nonceKey; private final String apiPath; private final boolean oneTime; private final Date expires; public Nonce(String nonceKey, String apiStr, boolean oneTime) { this.nonceKey = nonceKey; this.apiPath = apiStr; this.oneTime = oneTime; this.expires = DateUtils.addSeconds( new Date(), getOptionsParamApi().getNonceTimeToLiveInSecs()); } public String getNonceKey() { return nonceKey; } public String getApiPath() { return apiPath; } public boolean isOneTime() { return oneTime; } public boolean isValid() { return !oneTime || expires.after(new Date()); } public Date getExpires() { return expires; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy