package org.zaproxy.zap.extension.authorization;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.parosproxy.paros.db.DatabaseException;
import org.parosproxy.paros.db.RecordContext;
import org.parosproxy.paros.model.Session;
import org.parosproxy.paros.network.HttpMessage;
import org.zaproxy.zap.extension.api.ApiResponse;
import org.zaproxy.zap.extension.api.ApiResponseSet;
/**
* A simple authorization detection method based on matching the status code of the response and
* identifying patterns in the response's body and header.
*/
public class BasicAuthorizationDetectionMethod implements AuthorizationDetectionMethod {
public static final int METHOD_UNIQUE_ID = 0;
public static final int NO_STATUS_CODE = -1;
public static final String CONTEXT_CONFIG_AUTH_BASIC = AuthorizationDetectionMethod.CONTEXT_CONFIG_AUTH
+ ".basic";
public static final String CONTEXT_CONFIG_AUTH_BASIC_HEADER = CONTEXT_CONFIG_AUTH_BASIC + ".header";
public static final String CONTEXT_CONFIG_AUTH_BASIC_BODY = CONTEXT_CONFIG_AUTH_BASIC + ".body";
public static final String CONTEXT_CONFIG_AUTH_BASIC_LOGIC = CONTEXT_CONFIG_AUTH_BASIC + ".logic";
public static final String CONTEXT_CONFIG_AUTH_BASIC_CODE = CONTEXT_CONFIG_AUTH_BASIC + ".code";
/**
* Defines how the conditions are composed one with another to obtain the final result.
*/
public enum LogicalOperator {
AND, OR
};
protected LogicalOperator logicalOperator;
protected int statusCode;
protected Pattern headerPattern;
protected Pattern bodyPattern;
public BasicAuthorizationDetectionMethod(Integer statusCode, String headerRegex, String bodyRegex,
LogicalOperator logicalOperator) {
this.headerPattern = buildPattern(headerRegex);
this.bodyPattern = buildPattern(bodyRegex);
this.logicalOperator = logicalOperator;
this.statusCode = statusCode != null ? statusCode : NO_STATUS_CODE;
}
public BasicAuthorizationDetectionMethod(Configuration config) throws ConfigurationException {
this.headerPattern = buildPattern(config.getString(CONTEXT_CONFIG_AUTH_BASIC_HEADER));
this.bodyPattern = buildPattern(config.getString(CONTEXT_CONFIG_AUTH_BASIC_BODY));
this.logicalOperator = LogicalOperator.valueOf(config.getString(CONTEXT_CONFIG_AUTH_BASIC_LOGIC));
this.statusCode = config.getInt(CONTEXT_CONFIG_AUTH_BASIC_CODE);
}
private BasicAuthorizationDetectionMethod(int statusCode, Pattern headerPattern, Pattern bodyPattern,
LogicalOperator composition) {
this.headerPattern = headerPattern;
this.bodyPattern = bodyPattern;
this.logicalOperator = composition;
this.statusCode = statusCode;
}
private static Pattern buildPattern(String regex) {
if (regex == null || regex.isEmpty())
return null;
return Pattern.compile(regex);
}
private static String getPatternString(Pattern pattern) {
if (pattern == null) {
return "";
}
return pattern.pattern();
}
@Override
public boolean isResponseForUnauthorizedRequest(HttpMessage message) {
// NOTE: In case nothing is configured, we default to "not match" when composition is "OR"
// and
// "matches" when composition is "AND" so not configuring
boolean statusCodeMatch = message.getResponseHeader().getStatusCode() == statusCode;
boolean headerMatch = headerPattern != null ? headerPattern.matcher(
message.getResponseHeader().toString()).find() : false;
boolean bodyMatch = bodyPattern != null ? bodyPattern.matcher(message.getResponseBody().toString())
.find() : false;
switch (logicalOperator) {
case AND:
// If nothing is set, we default to false so we get the expected behavior
if (statusCode == NO_STATUS_CODE && headerPattern == null && bodyPattern == null)
return false;
// All of them must match or not be set
return (statusCodeMatch || statusCode == NO_STATUS_CODE)
&& (headerPattern == null || headerMatch) && (bodyPattern == null || bodyMatch);
case OR:
// At least one of them must match
return statusCodeMatch || headerMatch || bodyMatch;
default:
return false;
}
}
@Override
public String toString() {
return "BasicAuthorizationDetectionMethod [" + logicalOperator + ": code=" + statusCode + ", header="
+ headerPattern + ", body=" + bodyPattern + "]";
}
@Override
public AuthorizationDetectionMethod clone() {
return new BasicAuthorizationDetectionMethod(this.statusCode, this.headerPattern, this.bodyPattern,
this.logicalOperator);
}
@Override
public int getMethodUniqueIdentifier() {
return METHOD_UNIQUE_ID;
}
@Override
public void persistMethodToSession(Session session, int contextId) throws DatabaseException {
session.setContextData(contextId, RecordContext.TYPE_AUTHORIZATION_METHOD_FIELD_1,
Integer.toString(statusCode));
// Add the patterns, making sure we delete existing data if there's are no patterns,
// otherwise old data would get loaded
if (headerPattern != null)
session.setContextData(contextId, RecordContext.TYPE_AUTHORIZATION_METHOD_FIELD_2,
headerPattern.pattern());
else
session.clearContextDataForType(contextId, RecordContext.TYPE_AUTHORIZATION_METHOD_FIELD_2);
if (bodyPattern != null)
session.setContextData(contextId, RecordContext.TYPE_AUTHORIZATION_METHOD_FIELD_3,
bodyPattern.pattern());
else
session.clearContextDataForType(contextId, RecordContext.TYPE_AUTHORIZATION_METHOD_FIELD_3);
session.setContextData(contextId, RecordContext.TYPE_AUTHORIZATION_METHOD_FIELD_4,
logicalOperator.name());
}
/**
* Creates a {@link BasicAuthorizationDetectionMethod} object based on data loaded from the
* session database for a given context. For proper results, data should have been saved to the
* session using the {@link #persistMethodToSession(Session, int)} method.
*
* @throws DatabaseException if an error occurred while reading from the database
*/
public static BasicAuthorizationDetectionMethod loadMethodFromSession(Session session, int contextId)
throws DatabaseException {
int statusCode = NO_STATUS_CODE;
try {
List<String> statusCodeL = session.getContextDataStrings(contextId,
RecordContext.TYPE_AUTHORIZATION_METHOD_FIELD_1);
statusCode = Integer.parseInt(statusCodeL.get(0));
} catch (NullPointerException | IndexOutOfBoundsException | NumberFormatException ex) {
// There was no valid data so use the defaults
}
String headerRegex = null;
try {
List<String> loadedData = session.getContextDataStrings(contextId,
RecordContext.TYPE_AUTHORIZATION_METHOD_FIELD_2);
headerRegex = loadedData.get(0);
} catch (NullPointerException | IndexOutOfBoundsException ex) {
// There was no valid data so use the defaults
}
String bodyRegex = null;
try {
List<String> loadedData = session.getContextDataStrings(contextId,
RecordContext.TYPE_AUTHORIZATION_METHOD_FIELD_3);
bodyRegex = loadedData.get(0);
} catch (NullPointerException | IndexOutOfBoundsException ex) {
// There was no valid data so use the defaults
}
LogicalOperator operator = LogicalOperator.OR;
try {
List<String> loadedData = session.getContextDataStrings(contextId,
RecordContext.TYPE_AUTHORIZATION_METHOD_FIELD_4);
operator = LogicalOperator.valueOf(loadedData.get(0));
} catch (NullPointerException | IndexOutOfBoundsException | IllegalArgumentException ex) {
// There was no valid data so use the defaults
}
return new BasicAuthorizationDetectionMethod(statusCode, headerRegex, bodyRegex, operator);
}
@Override
public void exportMethodData(Configuration config) {
config.setProperty(CONTEXT_CONFIG_AUTH_BASIC_HEADER, getPatternString(this.headerPattern));
config.setProperty(CONTEXT_CONFIG_AUTH_BASIC_BODY, getPatternString(this.bodyPattern));
config.setProperty(CONTEXT_CONFIG_AUTH_BASIC_LOGIC, this.logicalOperator.name());
config.setProperty(CONTEXT_CONFIG_AUTH_BASIC_CODE, this.statusCode);
}
@Override
public ApiResponse getApiResponseRepresentation() {
Map<String, String> values = new HashMap<>();
values.put(AuthorizationAPI.PARAM_HEADER_REGEX, headerPattern == null ? "" : headerPattern.pattern());
values.put(AuthorizationAPI.PARAM_BODY_REGEX, bodyPattern == null ? "" : bodyPattern.pattern());
values.put(AuthorizationAPI.PARAM_STATUS_CODE, Integer.toString(this.statusCode));
values.put(AuthorizationAPI.PARAM_LOGICAL_OPERATOR, this.logicalOperator.name());
values.put(AuthorizationAPI.RESPONSE_TYPE, "basic");
return new ApiResponseSet<String>(AuthorizationAPI.RESPONSE_TAG, values);
}
}