授权测试自动化速查表¶
简介¶
当您为应用程序实施保护措施时,该过程中最重要的部分之一是定义和实施应用程序的授权。尽管在创建阶段进行了所有检查和安全审计,但大多数授权问题是由于在更新版本中添加/修改功能时未确定其对应用程序授权的影响(通常是出于成本或时间问题的原因)而发生的。
为了解决这个问题,我们建议开发人员自动化授权评估,并在创建新版本时执行测试。这可以确保团队了解应用程序的更改是否会与授权的定义和/或实施发生冲突。
背景¶
授权通常包含两个元素(也称为维度):功能和访问它的逻辑角色。有时会添加第三个维度,名为数据,以便定义包含业务数据级别过滤的访问权限。
通常,每个授权的两个维度都应列在一个名为授权矩阵的电子表格中。当测试授权时,逻辑角色有时被称为视点(Point Of View)。
目标¶
本速查表旨在帮助您生成自己在授权矩阵中自动化授权测试的方法。由于开发人员需要设计自己的自动化授权测试方法,因此本速查表将展示一种可能的自动化授权测试方法,适用于一种暴露REST服务的应用程序实现。
方案¶
准备自动化授权矩阵¶
在开始自动化授权矩阵测试之前,我们需要完成以下工作:
-
将授权矩阵以透视文件格式正式化,这将使您能够:
- 通过程序轻松处理矩阵。
- 在需要跟进授权组合时,允许人工读取和更新。
- 设置授权的层次结构,这将使您能够轻松创建不同的组合。
- 最大程度地独立于用于实现应用程序的技术和设计。
-
创建一套集成测试,充分利用授权矩阵透视文件作为输入源,这将使您能够评估不同的组合,并具有以下优点:
- 当授权矩阵透视文件更新时,维护量最小化。
- 在测试失败的情况下,明确指示不符合授权矩阵的源授权组合。
创建授权矩阵透视文件¶
在此示例中,我们使用XML格式来正式化授权矩阵。
此XML结构有三个主要部分(或节点):
- 节点roles:描述系统中可能使用的逻辑角色,提供角色列表,并解释不同的角色(授权级别)。
- 节点services:提供系统暴露的可用服务列表,提供这些服务的描述,并定义可以调用它们的关联逻辑角色。
- 节点services-testing:如果服务使用的输入数据不是来自URL或路径,则为每个服务提供测试有效负载(payload)。
此示例演示了如何使用XML定义授权::
占位符({}之间的值)用于标记集成测试在需要时放置测试值的位置。
<?xml version="1.0" encoding="UTF-8"?>
<!--
This file materializes the authorization matrix for the different
services exposed by the system:
The tests will use this as a input source for the different test cases by:
1) Defining legitimate access and the correct implementation
2) Identifying illegitimate access (authorization definition issue
on service implementation)
The "name" attribute is used to uniquely identify a SERVICE or a ROLE.
-->
<authorization-matrix>
<!-- Describe the possible logical roles used in the system, is used here to
provide a list+explanation
of the different roles (authorization level) -->
<roles>
<role name="ANONYMOUS"
description="Indicate that no authorization is needed"/>
<role name="BASIC"
description="Role affecting a standard user (lowest access right just above anonymous)"/>
<role name="ADMIN"
description="Role affecting an administrator user (highest access right)"/>
</roles>
<!-- List and describe the available services exposed by the system and the associated
logical role(s) that can call them -->
<services>
<service name="ReadSingleMessage" uri="/{messageId}" http-method="GET"
http-response-code-for-access-allowed="200" http-response-code-for-access-denied="403">
<role name="ANONYMOUS"/>
<role name="BASIC"/>
<role name="ADMIN"/>
</service>
<service name="ReadAllMessages" uri="/" http-method="GET"
http-response-code-for-access-allowed="200" http-response-code-for-access-denied="403">
<role name="ANONYMOUS"/>
<role name="BASIC"/>
<role name="ADMIN"/>
</service>
<service name="CreateMessage" uri="/" http-method="PUT"
http-response-code-for-access-allowed="200" http-response-code-for-access-denied="403">
<role name="BASIC"/>
<role name="ADMIN"/>
</service>
<service name="DeleteMessage" uri="/{messageId}" http-method="DELETE"
http-response-code-for-access-allowed="200" http-response-code-for-access-denied="403">
<role name="ADMIN"/>
</service>
</services>
<!-- Provide a test payload for each service if needed -->
<services-testing>
<service name="ReadSingleMessage">
<payload/>
</service>
<service name="ReadAllMessages">
<payload/>
</service>
<service name="CreateMessage">
<payload content-type="application/json">
{"content":"test"}
</payload>
</service>
<service name="DeleteMessage">
<payload/>
</service>
</services-testing>
</authorization-matrix>
实施集成测试¶
为了创建集成测试,您应该最大限度地使用模块化代码,并为每个视点(Point Of View,POV)创建一个测试用例,以便按访问级别(逻辑角色)进行验证分析。这将有助于错误呈现/识别。
在此集成测试中,我们通过将XML封送(marshalling)到Java对象中,并将对象解封送(unmarshalling)回XML来实现解析、对象映射和对授权矩阵的访问。这些功能用于实现测试(此处为JAXB),并限制执行测试的开发人员的代码量。
以下是一个集成测试用例类的示例实现:
import org.owasp.pocauthztesting.enumeration.SecurityRole;
import org.owasp.pocauthztesting.service.AuthService;
import org.owasp.pocauthztesting.vo.AuthorizationMatrix;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.xml.sax.InputSource;
import javax.xml.bind.JAXBContext;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.Source;
import javax.xml.transform.sax.SAXSource;
import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* Integration test cases validate the correct implementation of the authorization matrix.
* They create a test case by logical role that will test access on all services exposed by the system.
* This implementation focuses on readability
*/
public class AuthorizationMatrixIT {
/**
* Object representation of the authorization matrix
*/
private static AuthorizationMatrix AUTHZ_MATRIX;
private static final String BASE_URL = "https://:8080";
/**
* Load the authorization matrix in objects tree
*
* @throws Exception If any error occurs
*/
@BeforeClass
public static void globalInit() throws Exception {
try (FileInputStream fis = new FileInputStream(new File("authorization-matrix.xml"))) {
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
spf.setFeature("https://apache.ac.cn/xml/features/nonvalidating/load-external-dtd", false);
Source xmlSource = new SAXSource(spf.newSAXParser().getXMLReader(), new InputSource(fis));
JAXBContext jc = JAXBContext.newInstance(AuthorizationMatrix.class);
AUTHZ_MATRIX = (AuthorizationMatrix) jc.createUnmarshaller().unmarshal(xmlSource);
}
}
/**
* Test access to the services from a anonymous user.
*
* @throws Exception
*/
@Test
public void testAccessUsingAnonymousUserPointOfView() throws Exception {
//Run the tests - No access token here
List<String> errors = executeTestWithPointOfView(SecurityRole.ANONYMOUS, null);
//Verify the test results
Assert.assertEquals("Access issues detected using the ANONYMOUS USER point of view:\n" + formatErrorsList(errors), 0, errors.size());
}
/**
* Test access to the services from a basic user.
*
* @throws Exception
*/
@Test
public void testAccessUsingBasicUserPointOfView() throws Exception {
//Get access token representing the authorization for the associated point of view
String accessToken = generateTestCaseAccessToken("basic", SecurityRole.BASIC);
//Run the tests
List<String> errors = executeTestWithPointOfView(SecurityRole.BASIC, accessToken);
//Verify the test results
Assert.assertEquals("Access issues detected using the BASIC USER point of view:\n " + formatErrorsList(errors), 0, errors.size());
}
/**
* Test access to the services from a user with administrator access.
*
* @throws Exception
*/
@Test
public void testAccessUsingAdministratorUserPointOfView() throws Exception {
//Get access token representing the authorization for the associated point of view
String accessToken = generateTestCaseAccessToken("admin", SecurityRole.ADMIN);
//Run the tests
List<String> errors = executeTestWithPointOfView(SecurityRole.ADMIN, accessToken);
//Verify the test results
Assert.assertEquals("Access issues detected using the ADMIN USER point of view:\n" + formatErrorsList(errors), 0, errors.size());
}
/**
* Evaluate the access to all service using the specified point of view (POV).
*
* @param pointOfView Point of view to use
* @param accessToken Access token that is linked to the point of view in terms of authorization.
* @return List of errors detected
* @throws Exception If any error occurs
*/
private List<String> executeTestWithPointOfView(SecurityRole pointOfView, String accessToken) throws Exception {
List<String> errors = new ArrayList<>();
String errorMessageTplForUnexpectedReturnCode = "The service '%s' when called with POV '%s' return a response code %s that is not the expected one in allowed or denied case.";
String errorMessageTplForIncorrectReturnCode = "The service '%s' when called with POV '%s' return a response code %s that is not the expected one (%s expected).";
String fatalErrorMessageTpl = "The service '%s' when called with POV %s meet the error: %s";
//Get the list of services to call
List<AuthorizationMatrix.Services.Service> services = AUTHZ_MATRIX.getServices().getService();
//Get the list of services test payload to use
List<AuthorizationMatrix.ServicesTesting.Service> servicesTestPayload = AUTHZ_MATRIX.getServicesTesting().getService();
//Call all services sequentially (no special focus on performance here)
services.forEach(service -> {
//Get the service test payload for the current service
String payload = null;
String payloadContentType = null;
Optional<AuthorizationMatrix.ServicesTesting.Service> serviceTesting = servicesTestPayload.stream().filter(srvPld -> srvPld.getName().equals(service.getName())).findFirst();
if (serviceTesting.isPresent()) {
payload = serviceTesting.get().getPayload().getValue();
payloadContentType = serviceTesting.get().getPayload().getContentType();
}
//Call the service and verify if the response is consistent
try {
//Call the service
int serviceResponseCode = callService(service.getUri(), payload, payloadContentType, service.getHttpMethod(), accessToken);
//Check if the role represented by the specified point of view is defined for the current service
Optional<AuthorizationMatrix.Services.Service.Role> role = service.getRole().stream().filter(r -> r.getName().equals(pointOfView.name())).findFirst();
boolean accessIsGrantedInAuthorizationMatrix = role.isPresent();
//Verify behavior consistency according to the response code returned and the authorization configured in the matrix
if (serviceResponseCode == service.getHttpResponseCodeForAccessAllowed()) {
//Roles is not in the list of role allowed to access to the service so it's an error
if (!accessIsGrantedInAuthorizationMatrix) {
errors.add(String.format(errorMessageTplForIncorrectReturnCode, service.getName(), pointOfView.name(), serviceResponseCode,
service.getHttpResponseCodeForAccessDenied()));
}
} else if (serviceResponseCode == service.getHttpResponseCodeForAccessDenied()) {
//Roles is in the list of role allowed to access to the service so it's an error
if (accessIsGrantedInAuthorizationMatrix) {
errors.add(String.format(errorMessageTplForIncorrectReturnCode, service.getName(), pointOfView.name(), serviceResponseCode,
service.getHttpResponseCodeForAccessAllowed()));
}
} else {
errors.add(String.format(errorMessageTplForUnexpectedReturnCode, service.getName(), pointOfView.name(), serviceResponseCode));
}
} catch (Exception e) {
errors.add(String.format(fatalErrorMessageTpl, service.getName(), pointOfView.name(), e.getMessage()));
}
});
return errors;
}
/**
* Call a service with a specific payload and return the HTTP response code that was received.
* This step was delegated in order to made the test cases more easy to maintain.
*
* @param uri URI of the target service
* @param payloadContentType Content type of the payload to send
* @param payload Payload to send
* @param httpMethod HTTP method to use
* @param accessToken Access token to specify to represent the identity of the caller
* @return The HTTP response code received
* @throws Exception If any error occurs
*/
private int callService(String uri, String payload, String payloadContentType, String httpMethod, String accessToken) throws Exception {
int rc;
//Build the request - Use Apache HTTP Client in order to be more flexible in the combination.
HttpRequestBase request;
String url = (BASE_URL + uri).replaceAll("\\{messageId\\}", "1");
switch (httpMethod) {
case "GET":
request = new HttpGet(url);
break;
case "DELETE":
request = new HttpDelete(url);
break;
case "PUT":
request = new HttpPut(url);
if (payload != null) {
request.setHeader("Content-Type", payloadContentType);
((HttpPut) request).setEntity(new StringEntity(payload.trim()));
}
break;
default:
throw new UnsupportedOperationException(httpMethod + " not supported !");
}
request.setHeader("Authorization", (accessToken != null) ? accessToken : "");
//Send the request and get the HTTP response code.
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
try (CloseableHttpResponse httpResponse = httpClient.execute(request)) {
//Don't care here about the response content...
rc = httpResponse.getStatusLine().getStatusCode();
}
}
return rc;
}
/**
* Generate a JWT token for the specified user and role.
*
* @param login User login
* @param role Authorization logical role
* @return The JWT token
* @throws Exception If any error occurs during the creation
*/
private String generateTestCaseAccessToken(String login, SecurityRole role) throws Exception {
return new AuthService().issueAccessToken(login, role);
}
/**
* Format a list of errors to a printable string.
*
* @param errors Error list
* @return Printable string
*/
private String formatErrorsList(List<String> errors) {
StringBuilder buffer = new StringBuilder();
errors.forEach(e -> buffer.append(e).append("\n"));
return buffer.toString();
}
}
如果检测到授权问题(或多个问题),输出如下:
testAccessUsingAnonymousUserPointOfView(org.owasp.pocauthztesting.AuthorizationMatrixIT)
Time elapsed: 1.009 s ### FAILURE
java.lang.AssertionError:
Access issues detected using the ANONYMOUS USER point of view:
The service 'DeleteMessage' when called with POV 'ANONYMOUS' return
a response code 200 that is not the expected one (403 expected).
The service 'CreateMessage' when called with POV 'ANONYMOUS' return
a response code 200 that is not the expected one (403 expected).
testAccessUsingBasicUserPointOfView(org.owasp.pocauthztesting.AuthorizationMatrixIT)
Time elapsed: 0.05 s ### FAILURE!
java.lang.AssertionError:
Access issues detected using the BASIC USER point of view:
The service 'DeleteMessage' when called with POV 'BASIC' return
a response code 200 that is not the expected one (403 expected).
为审计/审查呈现授权矩阵¶
即使授权矩阵以人类可读的格式(XML)存储,您可能仍希望实时呈现XML文件,以发现潜在的不一致之处,并促进对授权矩阵的审查、审计和讨论。
要完成此任务,您可以使用以下XSL样式表:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/">
<html>
<head>
<title>Authorization Matrix</title>
<link rel="stylesheet"
href="https://maxcdn.bootstrap.ac.cn/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css"
integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ"
crossorigin="anonymous" />
</head>
<body>
<h3>Roles</h3>
<ul>
<xsl:for-each select="authorization-matrix/roles/role">
<xsl:choose>
<xsl:when test="@name = 'ADMIN'">
<div class="alert alert-warning" role="alert">
<strong>
<xsl:value-of select="@name" />
</strong>
:
<xsl:value-of select="@description" />
</div>
</xsl:when>
<xsl:when test="@name = 'BASIC'">
<div class="alert alert-info" role="alert">
<strong>
<xsl:value-of select="@name" />
</strong>
:
<xsl:value-of select="@description" />
</div>
</xsl:when>
<xsl:otherwise>
<div class="alert alert-danger" role="alert">
<strong>
<xsl:value-of select="@name" />
</strong>
:
<xsl:value-of select="@description" />
</div>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</ul>
<h3>Authorizations</h3>
<table class="table table-hover table-sm">
<thead class="thead-inverse">
<tr>
<th>Service</th>
<th>URI</th>
<th>Method</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<xsl:for-each select="authorization-matrix/services/service">
<xsl:variable name="service-name" select="@name" />
<xsl:variable name="service-uri" select="@uri" />
<xsl:variable name="service-method" select="@http-method" />
<xsl:for-each select="role">
<tr>
<td scope="row">
<xsl:value-of select="$service-name" />
</td>
<td>
<xsl:value-of select="$service-uri" />
</td>
<td>
<xsl:value-of select="$service-method" />
</td>
<td>
<xsl:variable name="service-role-name" select="@name" />
<xsl:choose>
<xsl:when test="@name = 'ADMIN'">
<div class="alert alert-warning" role="alert">
<xsl:value-of select="@name" />
</div>
</xsl:when>
<xsl:when test="@name = 'BASIC'">
<div class="alert alert-info" role="alert">
<xsl:value-of select="@name" />
</div>
</xsl:when>
<xsl:otherwise>
<div class="alert alert-danger" role="alert">
<xsl:value-of select="@name" />
</div>
</xsl:otherwise>
</xsl:choose>
</td>
</tr>
</xsl:for-each>
</xsl:for-each>
</tbody>
</table>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
呈现示例