Designing for Security
Designing for Security
Everyone's heard of the need to 'shift security left.' But how? In this article, we take a look at how to implement security checks on each layer of an app.
Join the DZone community and get the full member experience.Join For Free
When we talk about designing for security in this document, we are essentially talking about application level security at the program level and not talking about Application Server or Operating System Security. This document talks about design principles which when applied while designing an application would ensure that the application can easily incorporate security checks of varying complexity, from basic validation such as invalid logins to OWASP vulnerabilities to finding out hacking attempts through behavioral analysis. The design principles in this document provide a mechanism to incorporate such checks within the application without much hassle.
Today, security is a big concern for any organization. Organizations today are spending exorbitant sums of money, paying various consultants to ensure that their applications are secure. The trouble is often when the consultants give their advice, it becomes difficult for the developers to implement their advice since their application is not really designed in a manner that security can be implemented. The application is currently designed in a manner which makes it cumbersome to perform security checks. Below are some of the ways in which application design inhibits security.
Security as an Add-On:
Today, most applications are designed while keeping responsiveness or functionality in mind and not keeping security in mind. This often results in non-secure applications which are not flexible enough to accommodate security checks. Applications need to be designed with security first, ensuring through the design that any security checks which may need to be applied in the future can easily be incorporated into the application.
To achieve this, each tier of the application should be properly layered, with each layer containing multiple stages with proper linkages and hierarchies between stages as and when necessary.
A layer is defined as the logical unit within a tier. In an n-tier application, each tier typically has a number of layers. For example, in a 3 tier application (UI, Middleware, DB) the UI, middleware, and DB would contain multiple layers. A tier contains multiple layers and a tier, for the purpose of this article, refers to the physical unit where the code runs.
Another frequent observation linked to security is that teams try to create a separate layer for security and apply it to a single layer within single or multiple tiers. Applying security on only one layer might work in some cases but is not a definitive approach as an attacker might be able to get past that layer and might hack into the next layer. Thus, all layers and their stages need to be made secure and a holistic approach needs to be taken to keep the application secure.
Layers Not Defined Clearly:
A common occurrence which becomes an anti-pattern for application security is the lack of a proper definition of layers for a particular application. While dividing the application into layers, each layer must have a proper definition and fixed responsibilities. However, if the definition and responsibility of the layers are not fixed, then one layer might have code to perform logic which ideally belongs to another layer, resulting in the intermingling of code. Implementing security rules within this intermingled code base proves to be problematic because different security rules need to be applied for different, particular, functionalities and since, here, functionalities are not demarcated, applying these rules becomes problematic.
A very good example of this is business logic being written in the controller and the controller also handling the HTTP request. Now, if business logic related to security checks needs to be implemented or a request related to security checks needs to be implemented then the intermingling caused here makes either implementation difficult for the developer since there is no clear demarcation between request related functionality and business logic.
Lack of Understanding of the Responsibilities of a Layer:
Even if layers are defined, their responsibilities are often not clearly defined. Thus, you could have a layer for handling web requests and a layer for handling business logic. However, if the demarcation between the layer handling web and business logic is not clearly defined, you could have scenarios of session objects being passed from the web to business logic layer.
Each layer should have a clear responsibility defined and based on the responsibility for which the protocols security check.
For example, the web layer should ensure that protected resources are accessed only if the request has a valid session since the session is a web layer entity. However, checking whether a particular operation can be carried out by the current user should not be implemented in the web layer since knowing the role rights of the user is not the responsibility of the web layer.
It is important to understand that each layer, by itself, will never be fully secure. It will only have as much security as pertains to its responsibilities. However, note that if the responsibilities of a layer are properly demarcated and each layer is totally secure with respect to its responsibility then the overall application flow which would go through a number of layers would be completely secure.
Lack of Abstraction Within a Layer:
In today's agile world, business always wants things fast. They want prototypes with minimum requirements. Gone are the days where businesses would list out all the requirements, thus it now becomes the responsbility of the development team.
Consider a scenario where there is a web flow which involves a user filling in multiple forms, and, based on the data entered, moving to the next level. Now, with only this requirement in mind, its quite easy to assume that for each form there would be a controller getting the form data upon submission and invoking a service method to save all the data. This would be a simple way to implement the problem but would later be a hindrance for applying security checks, because as the number of forms increase this pattern would result in multiple service methods being invoked by the controller with no linkage between them. Thus, if any common security check has to be implemented, it has to be implemented individually in all the service methods which would result in code duplicity and maintenance issues.
To avoid this, it is important to discern a commonality between the actions of each service method. Removing this commonality from the method definition would result in an implementation unique to that service which then could be abstracted. Defining a template based on the commonality and abstraction would ensure better control over the way services are being implemented. The template would invariably help in breaking the main operation of a service into a finite number of stages, all linked together in a particular order. This template would be fixed for a layer abstracting only that part which is specific to a particular implementation. In the case of our example related to forms, since our application is a web-flow, for each form the template could check that all data required for the form submission to be valid is present in the system, and, if not, then it would not allow the submission. The actual submitting of data might be handled by a separate service which conforms to the template's definition. Thus, now any common security check can be added to the template instead of being added to each service since now each service conforms to the template definition.
This design approach ensures that within a layer, based on the definition of the layer, all the required stages are properly defined and demarcated, thus ensuring that the design is more security friendly as opposed to those implementations where for each functionality, to which the layer caters to, a separate implementation is created independently of all other implementations in the layer.
When Communication Between and Within Layers Is Not Clear:
The following points need to be kept in mind when we talk about communication between layers.
Information Transferred Between Layers:
The passing of information between layers is critical for security. As discussed, each layer has a responsibility and the layer is only responsible for securing the functions pertaining to its responsibility. When data is passed to a layer, only as much data as is required for the layer to fulfill its responsibility should be passed. If more data is passed, then the layer needs to fulfill its responsibility or the layer is allowed to perform operations on the data which are not needed to fulfill its responsibility and this increases the difficulty of implementing security checks.
For example, consider an application which routes data from a form into a web layer. The web layer takes data from the form and invokes the business layer, once the business operation is successful, the web layer is notified of the success and it adds the data to an existing data map which is maintained at the session level.
Now, in this case, when the web layer wants to execute the business logic based on the new data entered by the user, it can invoke the business layer by passing the map as-is along with the new data. Note that the role of the business layer is just to perform the business operation based on the new data entered. However, since the web layer has passed the map as-is, it's possible for a developer to write an implementation which modifies the map in the business layer which is not supposed to be done in that layer. Thus, the map now contains data which is not only entered into the web layer but also in the business logic layer. Due to this violation, all checks assuming that the map would only be validated in the web layer would be invalidated.
In such a case, the web layer should pass the map to the business layer as a read-only map so that the business layer can only read from this map as required. Note that a similar issue could occur if, instead of just passing the map, the entire session object is passed.
Communication Between Layers in Case of Success and Failure:
Having a defined way of communicating between layers would allow security rules to be defined for the entire layer instead of being defined for a specific implementation in the layer.
Consider a scenario like the one above where the web layer invokes the business layer but the business layer, in the case of errors, throws exceptions and at times returns a Boolean or some custom Object. In such a scenario, it would be difficult for the web layer to identify exception cases from the business layer since the return type for exception cases is not fixed.
In such a case, the web layer would have to write some specific logic for each implementation to analyze why the exception occurred. If, however, the business layer always throws an exception in case of any issue, with data sent to the exception object to indicate the cause of the exception, the web layer can now have a single algorithm to analyze the exception thrown by the business layer and would thus be able to easily identify whether an exception was caused due to a security breach or not, and would then be able to apply rules to either block such requests or perform some other action.
Anemic Domain Model:
SOA architecture has given rise to POJOs (Plain Old Java Objects) which are being used as dumb objects with only properties and zero behavior. This causes other layers which create these POJOs to perform validations to check the validity of the POJOs that perform various operations in their layer when these operations could have well been executed in the POJOs. These extra operations often result in the layer doing more than what it signed up for, which invariably results in issues while enhancing the layer with security rules.
If the entity was not a dumb object, but an object with behavior, a lot of these validations could have been moved to the entity itself, relieving the layer of the responsibility of having to perform those validations. Enabling each entity to validate its state, both from a data and security perspective, would ensure that such validations would always be applied whenever the entity was created, irrespective of the layer and would thus serve in unifying the rules used for creating entities and would not require each layer to take the additional responsibility of validating the entity. This would ensure that the layer would have code only pertaining to its responsibility and thus adding security rules would be easier.
Putting it All Together:
Once all the above points are incorporated in an application's design, the application can be made as secure as needed. For that, it's important to incorporate checks within tiers and their respective layers in such a manner so that if one layer is bypassed, the subsequent layer has checks to handle such a breach. A layer, when invoked, should validate the data being sent to it by previous layers, ensuring that the data sent is valid and the behavior that the data signifies is expected and does not deviate from expectations.
Consider a scenario where, in a social media application, the user's credentials are compromised. The UI tier sends the data to the service, invoking the layer responsible for authentication. In case of a login, the UI tier, for this social media application, typically sends the username, password, device used, and IP address to the server.
The authentication layer, the layer designated to handle authentication for the social media application, should take the user data, wrap it in an entity which would be responsible for validating its state, thus also validating the login data, ensuring that the user data contains the username, password, device details and IP address. The entity can validate the data stored in each of the parameters. Thus, if the hacker tries to spoof a request without knowing the type of data for each parameter, the entity itself would reject the request, thus forming the first line of defense. Note that in this case, the creation of the entity can be considered the first stage of the authentication layer.
If the hacker logs in through the application, bypassing the first line of defense, given that the IP address and device details are available, existing data for a particular user can be checked on these parameters and the layer can capture any deviation and can block the request. This detecting of abnormalities in behavior based on IP address and device is, in this case, the second stage of the authentication layer.
Let's, however, assume that the hacker passes this stage also. Now the authentication layer has been bypassed. Let's now say he starts removing friends from his existing list. The layer responsible for the removal of friends can keep a count of how many friends are being removed and using a pre-computed threshold value based on previous instances where friends were removed from this account and can raise an alarm if the hacker removes more friends than the threshold value, highlighting abnormal behavior.
Thus, we can see how every layer and stage can play a part in ensuring that in the event of a breach, the damage done is minimal.
This approach of each layer and stage ensuring security within itself ensures security for the entire application and ensures that complicated security checks can be incorporated by taking a layered approach to implementing each check in its respective layer, thus designing the application for security and ensuring that any security patches can be incorporated without much hassle.
Opinions expressed by DZone contributors are their own.