Richard Wilson [HREF1], Student, Department of Computer Science and Software Engineering [HREF2] , University of Melbourne [HREF3], Victoria, 3010. rewilson@students.cs.mu.oz.au
Daniel Lowes [HREF1], Student, Department of Computer Science [HREF4] , University of Pretoria [HREF5], Pretoria, South Africa, 0002. bejorgen@tuks.co.za
The complexity of web applications is increasing on a continual basis. With the increased complexity comes a corresponding rise in the need for fast, yet powerful, programmatic security. This security needs to offer services to the application that allow it to implement custom access and authentication control schemes - specifically, restricting users from individual system operations and from individual system entities (files, database records, and so on). We discuss the chosen abstract entities for representing Permissions in a database-driven environment, and then show how these entities can be used to implement fast and flexible security routines that can be applied to a wide range of web environments. We describe the algorithms used for dynamic and static checks, and finally present an extension to the basic system that improves functionality without additional overhead.
Web applications are rapidly becoming as complex as their client-side counterparts. E-commerce sites have been large for some time, but, while their bulkiness is often attributable more to the sheer volume of information than to the actual complexity of the managing code, this is changing. Modern sites, even middle-scale ones, are being powered by increasingly complex codebases known as content management systems, or CMSs. The basic definition of a CMS is "a system for facilitating the creation and manipulation of multimedia information in a website, to improve communication services". This broad definition encompasses such well-known applications as news and poll scripts, message boards, and chat facilities.
As such applications grow in sophistication, so do their security requirements. These sites carry information which can be as valuable as any corporate knowledge base, and so must possess the mechanisms to support access control. An application running over, or in some way using, the Internet is fundamentally different to an application running on a lone computer, unconnected to anything else in the world, and accordingly has fundamentally different potential exploits and security requirements. Many new forms of incursion become apparent with the introduction of database-driven web applications, where the entire system is located and executed on a remote system to which the site owner may not even have physical access. A recent survey indicated that up to 97% of major sites contained (to differing degrees) security flaws that allowed malicious users to attack the site in some way (Rescher, 2001). Fully a quarter of these flaws would lead to complete privacy breach. This level of security vulnerability is clearly unacceptable, and the problem is not going to go away: vulnerabilities in web-based applications increased by 4% in 2003 (Friedrich, 2004).
Parties wishing to secure a web-based application must consider two primary requirements. The first is to ensure that the web server software, such as Apache or IIS, is secure (declarative security (Hall, 2002)). These products act as the first line of defence against intrusion, by providing services such as authentication and access control (Menenzes, 1996). The server software can ensure that visiting users do not have access to specified folders or files, and that users who wish to verify themselves for access to such information, can do so in a secure and certified fashion. The second requirement is that of implementing security logic in the application itself: programmatic security (Hall, 2002). This is just as important as declarative security, and failure to handle it is the cause of most of the security holes described in Desmond (2004).
There exists no simple solution to the issue of programmatic security: requirements can vary dramatically from application to application, and often require extensive investments of time to debug and validate. In addition, some implementations provide security to the detriment of execution time and memory. A security implementation that is fully secure, but takes seconds and megabytes to compute a certificate for a single operation, is obviously not suited for an application receiving tens or hundreds of simultaneous requests. Moreover, when one is dealing with a group of applications that are interacting and interdependent, some very specific security requirements become necessary, namely the ability to restrict users from individual operations, and the ability to restrict users from individual system items (such as logical entities in the database). Thus what is needed is a generalised, secure, easily adapted, fast mechanism for use on interdependent, interacting web applications, and that is what this paper will present. The ideal implementation must, firstly, combat and prevent known security problems, and, secondly, extend the ability of an application to secure itself, without introducing new security flaws. The application we shall use to illustrate our security measures is Chimaera2, an upcoming ASP.NET application suite. (It should be noted that Chimaera2 is not a CMS; rather, it is a collection of applications that CMSs can use within their own frameworks - a sort of third party application library, so to speak).
While there has been much activity in the arena of security and web applications, there has not been an attempt to create a theory-based system both powerful enough and flexible enough to be utilised in a wide range of web applications. Some authors have drawn attention to the lack of security certifications for CMSs, highlighting the need for security in many of these applications, and noting the current lack of a universal entity tasked with verifying their theoretical security. If the services of such an entity cannot be obtained, the next best thing is to ensure that the security to which you are entrusting your system is verifiable both in theory and in practice, by anyone who wishes to test it. Applications have been developed (Huang et al, 2003) that test web applications for common flaws such as cross-site scripting and SQL injection vulnerabilities. Once again, though, these merely highlight the errors and do not actually provide a standard mechanism for repairing them which would always be successful. Essentially, what is required in this field, and what is patently missing, is a realistic way of guaranteeing that web applications conform to a particular level of security (much like, for example, the US Department of Defence ratings for computer system security, but from a software perspective).
There are a number of powerful and comprehensive tools for testing a web application once it has been deployed (for example, Whitehat Arsenal [HREF6], and the @Stake Web Proxy [HREF7]); the problem is that these "validation"-type tools is that they shift the burden of securing a web application to the testing phase of the production process. This is not an ideal situation, since, in the interests of robust and accountable design, security considerations should form a part of the analysis and design processes. Furthermore, security analysis at such a late stage of an application's development can be extremely expensive: companies like NetCraft offer security testing with prices starting from about $12 000 for a basic six day audit [HREF8].
In the spirit of early detection and prevention, several organisations have compiled lists of the most common security flaws in applications (Desmond, 2004); the ideal security solution should render invalid as many of these as possible. Of the list provided in Desmond, those problems which can be invalidated by good programmatic security are: non-validated input, broken access control (within the context of a custom authentication scheme), broken authentication and session management, injection flaws, and improper error handling. We will go on to show how our system overcomes these issues.
Issues that are NOT addressed here are those of database security, and denying access to the database, which are essential for the system to function correctly, since someone with unrestricted access to the system database can quite easily bypass or alter any of the security measures described. We take it as given, therefore, that these and similarly obvious issues of access control have been addressed.
In terms of the actual security system, two types of security privilege must be exposed for use by a multi-application website: static, and dynamic. Static permissions should correspond to operations that are hard-coded into the system. As an example, the Chimaera2 system includes a message board product: static operations in this application include deleting of threads, use of avatars, and rating of members. Such operations cannot be changed (discounting custom editing of the code, as this adds complexity to the system which is beyond the scope of this paper), and so can be expressed in a binary manner: a user of the system either has access to the operation, or does not. There can be no ambiguity, and if a security check fails, the system must default to denying access. The idea can be expanded into the multibinary permission, in which binary permissions for more than one operation are expressed in a single value (using bit masks). However, expression is not the issue: it is the sheer number of these operations that causes problems. Chimaera2 currently exposes over 60 individual static operations that require permissions, and this number is expected to double within the next 12 months as post-release development continues. How does one not only store, but also validate against a collection of permissions of this size, in an efficient manner?
The problems inherent in static permissioning are multiplied when one considers permissions allocated to items created dynamically. Usually (almost invariably), this translates as items that can be added and removed from the system by the administrator, within the framework of the application. In our example of a database-driven application, this will refer to logical database entities, but in other applications could just as well refer to items such as XML files and images - anything that the system exposes as a manipulatable item for user access. These permissions are of a fundamentally different nature. If we simply wished to mirror static permissions for dynamic items, it would require assigning a binary permission to each dynamic item, per user of the system. Whether or not most of the users would ever access the item, is irrelevant: to guarantee security, the system would need to be able to verify any user of the system against that item if asked to. Yet this completely prevents extendability, which is necessary for an evolving system to which new types of dynamic item might be introduced in the future. Something different is needed.
In order to make it possible to implement both static and dynamic security, some way is needed of abstracting the idea of a "permission". The chosen entity is the same as that used by operating systems like Windows NT and *nix: the group. A group is simply an entity with a globally unique identifier, or GUID, which can be used to co-ordinate some system users into a cohesive whole. If several users are associated with a group, the system should be able to read the group as that set of users, rather than as a single item. However, the system should still be able to refer to the group as a singular entity. Users should be able to be part of multiple groups simultaneously - parallelism - and also be part of the same group multiple times without causing anomalous behaviour - redundancy. Finally, each group should be able to carry a set of multibinary permissions specifying the operations (static or otherwise) that it is qualified to perform. These operations will be dependent on the client application, but ideally the storage mechanism should be optimised for rapid retrieval and checking, to allow many security operations to be run on a page without a detrimental effect on performance.
Chimaera2 has implemented all of the above suggestions in a security subsystem called the GLS, or Global Level System. Groups of the style described above are called Levels (for historical reasons) and exist as records in the application database. Each record contains six 32-bit integer fields, as well as a single byte field specifying the type of the Level, and some additional fields for application use. The 32-bit fields are used to hold the multibinary permissions associated with the Level. Up to 31 distinct binary permissions can be stored in each, using bit masking, resulting in a total restriction space of 186 values. This is large enough for most applications, and yet the database storage is minimal. Adding new fields is easy enough, and feasibly several hundred such operations could be stored in a record without slowing the database. Few applications expose more than four or five hundred distinct static operations.
The byte field can be set to one of two values, indicating if the Level is an Allow Level or a Deny Level. Allow and Deny Levels take their behaviour directly from the Windows NT model, and exist because of the fact that the system allows parallelism. Deny Levels implicitly override any defined permissions in other Levels, which means that no matter how many Allow Levels allow a particular operation, a single Deny Level restricting it hold precedence.
We have therefore solved the problem of storage. The next consideration is how the stored permissions should be accessed and manipulated in order to solve the validation problem. Each 32-bit field is backed up by an enumeration in the system code. These enumerations define the associations between the bit mask values, and the operations they represent. Having these operations publicly visible (the Chimaera2 codebase is licensed under the GNU GPL, and so can be viewed by anyone who cares to download it) is not a security risk because they do not represent access in and of themselves. An example of part of one of these enumerations is as follows:
[Flags]
public enum Restriction1 {
None = 0,
DeleteTopic = 1,
ViewIP = 2,
DeleteReply = 4,
CreateTopic = 8,
...
}
To allow validation of an operation, the system first has to validate that the user requesting the operation has permission to use the system; this is the role played by authentication. Chimaera2, although running on the ASP.NET framework and so having access to several options for server-managed authentication, makes use of a custom model involving cookies. There are two reasons for this choice: firstly, it allows us to disable some aspects of the .NET framework which affect speed, and secondly it gives us precise control over session management. When a user logs into the system, a cookie with a random session ID, their user ID, and their password (as well as trivial application-specific data) is written to their browser. This is the first possible entrypoint into the system, and is mentioned as such in Desmond [B] and [C]. If a malicious user were to gain access to the cookie, he could use the details in it to log into the system under an assumed name, and perhaps gain extra security privileges. Hence, we required a way to make the data stored in the cookie completely harmless. This was achieved by hashing the user's password before adding it to the cookie. If a malicious users reads the cookie, he will only learn that the user with ID X has a password that hashes to value Y. Reverse engineering a hash value, even knowing the hash algorithm, is essentially noncomputable; his only recourse would be to begin hashing random strings in the hope of encountering the same value. We chose a well-engineered, non-funnelling, 32-bit hash algorithm (Jenkins, 1997) which ensures a very even spread of values over the 4-billion-odd value space. Thus, the cookie data can be assumed to be harmless, although fully usable by the system. For additional security, even the password values stored in the database are hashed, making them useless to an intruder. He may be able to replace values with his own precomputed hashes, and thus gain access to user accounts, but this is a separate issue, and the attacker would still not know what the users had chosen as their passwords. In this manner, two of the common security flaws are negated.
Now, imagine that the application has asked the GLS to validate a given operation. It provides the GLS with the enumeration value requiring validation. The Chimaera2 system is entirely login-oriented, and so such a request would not be issued unless the system had already checked that the user is logged in. Such a check involves validating the cookie- stored password hash and user ID against the database. Here we see two further possibilities for a cracker to infiltrate the system: firstly, a cracker might insert values into the cookie which would act as injections into the SQL used for performing the check, and so log him in under the first valid account in the database. This situation is easily avoided: input that is expected to be numeric (i.e. the password hash) is validated as such before being used. If it is non- numeric, its value is reset to zero, and any checks it is used for will fail. This is done throughout Chimaera2, and avoids the flaw described in Desmond [A]. A similar mechanism is applied to strings used in database operations: all potentially dangerous values (such as unmanaged single quotes) are replaced with safe ones. It should be further be noted that the related risk of improper error-handling, as mentioned in Desmond, is eliminated through the system's consistent validation of input, and its trapping of error conditions in the code itself. Evidence of this is present throughout the Chimaera2 security code.
The second vulnerability, unfortunately, is not as easy to remedy. If an attacker were to spoof a cookie containing values stolen from another user's cookie, he may be able to fool the system into considering it a valid login, and allowing him access to that account. Some browsers have mechanisms to prevent idle tampering of cookie values, but these can be circumvented by determined crackers. A possible option is to fully encrypt the cookie, preventing its information from being read at all. Full encryption of cookies was not feasible for Chimaera2, and so this vulnerability remains as a flaw in the system. However, gaining access to another user's system to such an extent as to be able to read their cookies (discounting physical access to the machine) is nontrivial, as it would require, among other things, faking the domain from which the cookie originates. As such, the risk can be regarded as relatively minor.
Finally, then, the GLS receives the operation to validate, and knows that the current user is authentic. How does the system proceed? An initial solution would be to simply extract the relevant field from the database, and perform a simple OR test to see if the permission is present. Indeed, this is the ideal way to do the test - but it has a hitch. While checking a single Level's permission does not necessarily take a long time, checking ALL the Levels in a user's Group might. Here, the fundamental latency in web applications begins to become a factor. The application is executing on a remote server, which might be serving many simultaneous requests (for the same, or other, applications) and must both perform its security checking, and return the results, in a timeframe that the user will perceive as acceptable. Connection speeds vary dramatically, and an application geared towards worldwide use (as Chimaera2 is, and most applications are) cannot make assumptions about the bandwidth it will have available to transfer data. If it takes too long, the request will either time out, or users will get frustrated and move on. Thus, executing operations in minimal time is very important. Running a loop on an indefinitely large set of records is not a good way to speed things up.
There are a number of popular non-technical approaches to improving an application's performance: the most relevant of which is the concept of "perceived latency". This is the time that a user is willing to wait before he starts to think that a request has gone on for too long. Thus, if we can piggy-back the security checking on another expensive operation for which the user is already willing to wait a longer period of time (because it is a kind of operation that they imagine *should* take longer, like logging in and posting a thread), we can squeeze it though without the user noticing the additional delay - even though it is still present - and thus make the entire application seem "faster". An excellent idea - were it not for the fact that security checking cannot always be piggy- backed. By their very nature, security checks have to occur in certain places, particularly at points before an operation is executed, verifying that operation. Operations that are expensive enough to allow piggy-backing do not occur frequently enough to mask all the security checks performed in the application, and when they do, they are not always in the same location as some security checks. So, piggy-backing the security checks is not an option. This leaves us with actually reducing the time taken to perform the checks. Ideally, we would like a way to reduce the O(N) security checking loop to O(1), making the time to check permissions negligible.
We have developed a feasible way of doing this; namely, by taking advantage of the storage mechanism. A user might be part of an indefinitely large collection of Levels, but those Levels are merely defining binary permissions for the same options, over and over again. It is possible to reduce all of these definitions to a single, super-definition that is equivalent to all of the Levels, because the permissions are defined as bit masks. OR-ing a set of bit masks together results on a single bit mark that is logically identical to the sum of all the masks. In our case, all of the Levels can be reduced to a single Level, or superlevel: a set of six 32-bit integers. Once this is accomplished, security checking is a single bitwise operation, and we have our O(1) complexity. To further improve speed, these six integers are placed into a cache at login time, and maintained there for the length of the user session. Caching allows us to remove another slow operation, which is the database extraction performed on each page to re-extract the superlevel for checking. The final check is of complexity O(1), and need not involve disk accesses at all: a near- perfect solution.
There may be one final flaw: if a malicious user gains access to the security cache, he may be able to alter it to grant himself extra permissions. In order to prevent this, the cache is stored purely on the server, and is never sent to the client in any form. Accessing a server-side object of this nature is practically impossible, short of breaking into the server and gaining full administrative rights. Preventing this type of intrusion, however, is a separate class of security problem entirely, and will not be examined here. We assume that the server, like the database, is sufficiently secured.
Now, imagine that our example software has been running for some time, and has accumulated an extensive user base - some tens of thousands of registered users. Suddenly, the administrator decides that he wishes to add a new forum. He also wishes to restrict access to this forum (one of the dynamic security operations offered by Chimaera2). Using the static permission verification system, this would mean that at the point of creation each user of the system needs to be assigned a binary permission for accessing that forum. The logistics are horrific: firstly, the permission records need to be created. Secondly, they need to be associated with the users in a logical and relational manner. Finally, the software needs to be told to check these new permissions whenever a user accesses the forum. All of this incurs overhead, much of which is unnecessary.
Before dynamic restriction can be effectively implemented, however, it is necessary to create some supporting structures. In Chimaera2, these structures are the logical entities Collections and Instances. Very simply, a Collection is made up of Instances. There are different kinds of Collections, and different kinds of Instances (they are distinguished by using the values of code-resident enumerations). A Collection can consist of indefinitely many Instances of almost indefinitely many different types (up to the 32-bit limit).
Using Collections, then, the core idea of the dynamic restriction system is that each database entity that wishes to restrict access to itself in some fashion, no matter what type of entity it is, must have a reference to a Collection. This Collection type will probably be unique in the application; for access control of forums in Chimaera2, the Collection is known as a ForumAccessCollection in the relevant enumeration. When an administrator wishes to restrict a Level (and thus, by extension, all of the members that are part of that Level) from accessing the forum, he needs only add to the Collection in question, an Instance reference to that Level. This is only the first part of the process, though. In order to make the dynamic restriction system flexible enough to be applied to any dynamic restriction situation, the process of validating the collection must be generalised. The following pseudocode algorithm is used:
/* The user's Group is cached for better performance, but this is not a necessity */
Array[] currUserLevels = systemCache.Retrieve(UserId);
/* We concatenate the array elements with a SQL-oriented string */
String tail = String.Join(" OR I_Target = ", currUserLevels);
/* Combine "tail" and some more SQL to get an executable SQL statement */
return dbModule.Execute("SELECT COUNT(*) FROM Instances WHERE I_Target = " + tail);
When the final SQL statement executes, it returns an integer (making use of COUNT(*) for faster execution) indicating how many matching rows were found. This is all that is needed; if, for example, no matching rows were found, then none of the Levels in the user's Group have Instance references in the database, and thus do not exist in the Collection being tested. It is then up to the calling code to interpret this result in whatever fashion best suits it. Most parts of Chimaera2 interpret it as meaning that the user is restricted from the current operation, but some (most notably user exclusion code) interpret it in the opposite manner.
This is a very general method, which performs a single, low-level system operation. Contexts of its calls, and ways of handling and interpreting its return value, are left entirely up to client applications. This enables a high degree of decoupling, and makes it suitable for use in practically any dynamic restriction system that follows this model. Execution time is minimal, and the implementation is straightforward. It acts as the perfect complement to the static restriction system.
That completes the description of the standard restriction system; the basic framework, if you will, for a robust security solution. There are, however, various "optional extras" that can be implemented. One such enhancement that has been added to the Chimaera2 application is that of "Group Membership Transparency."
Consider the following scenario: an administrator has a system which contains several hundred Levels. These Levels all contain multiple users, and many of them are used for dynamic restriction of various items. Now, the admin wishes to restrict a given user from dynamic operation O on item X. The problem is, that user is not yet part of any of the Levels that are currently implementing O for X. Adding that user to a whole new Level simply to restrict him might be overkill; more so if this process happens fairly frequently. Furthermore, this might have the undesirable side-effect of giving the user additional permissions (those permissions defined in the Level). This is definitely a situation that needs to be avoided, but the only way of doing that in the above system is to define a brand new, permission-free Level L, add the single user to L, and add L to the relevant restriction Collection. This is not only overkill, it is decidedly counter- productive. In a very short period of time, there might be an explosion of Levels that exist purely to allow individual users to be restricted from various items. Ideally, the system should allow you to restrict individual users, as well as users grouped into a Level. Furthermore, it should allow them at the same time.
This is eminently possible within our system. The key to the implementation is the code- based enumerations which define the unique IDs for the various kinds of Instances that can be added to Collections. As mentioned, the system allows Instances of any type to be added to a Collection; it is then up to code to extract the type identifier and then handle the record as appropriate. However, having to perform an extraction of this nature for dynamic restriction would hurt the speed of the operation; one could not just do a row count, but would need to check each record individually to determine whether the reference it held was to a Level, or to a user. Instead, we use two separate enumerations: the first simply defines the permissions that can be allowed or denied (which are represented as Instances in the database), without regard to the entity (level, user, or anything else) being dealt with. A sample enumeration might be:
public enum Permissions {
ForumAccess = 1,
ForumViewing = 2,
ForumPosting = 3,
// and so on
}
The second enumeration defines the separate entitites that must be differentiated:
public enum PermissionEntities {
User = 1,
Level = 2,
SomeOtherEntity = 3,
// and so on
}
Finally, the system must know the maximum number of permissions that exist - in other words, the largest value in the Permissions enumeration. This requires little extra work, as it can be determined programmatically by the security system:
Array arr = Enum.GetValues(typeof(Permissions)); int max_permissions = (int) arr.GetValue(arr.Length-1);
This allows us to calculate the range of values to apply to each entity, using multiplication (as in the SQL below) to obtain a unique value for each operation/entity combination. So, in the above example, Instances applying to Users would have values from 1 to 3, while those applying to Levels would have values from 4 to 6. How does this improve security checks? It allows SQL of the following nature:
SELECT COUNT(*) FROM Instances WHERE I_Type > " + (((int)PermissionEntities.User)-1) * (max_permissions + 1) + " AND I_Target = " + UserId + " AND I_Collection = " + RestrictionCol;
One can assume that validation checks were run on the variables. This determines if the current user, with id UserId, exists in the collection RestrictionCol (for a forum, say). The type limiter ensures that only users, not Levels, are included in the search. By offloading security checks to the database server in this way, an extra layer of abstraction is added to the system, which further increases the difficulty a malicious user experiences when trying to break the system.
Clearly, such a statement will execute far more efficiently than a code block which extracts the I_Type field for each record and compares it against a table to determine it's type. Furthermore, this approach is not only clean and efficient, it also possesses the vital property of extendability: assuming a Permissions enumeration based on 32-bit integers (although it could be larger), then, where N is the maximum number of entities:
max_permissions <= ((2^32)-1 / N) - 1
Simply put, if an application were to have ten logical entities, it could have ~400 million permissions, all using two 32-bit enumerations, which is clearly sufficient for any web application.
We have demonstrated in this paper that there exist viable mechanisms for advanced security in web applications which allow high-level functions to restrict multiple- user systems without affecting the system performance. Both static and dynamic permissions can be expressed and validated in a well-defined and secure manner. The system outlined here also allows for numerous extensions that improve the usability and power of the system to a great degree. The implementations can also be used in many disparate systems with few changes.
There are some directions that might be worth future exploration. Development of a mechanism for defining static permissions in a not-so-static way is possible; if integrated with an event-logging system of sufficient complexity and connectability, "static" operations might be reduced to a subclass of dynamic permission. This would, in turn, permit entire static/dynamic security patterns to be exported between widely different applications; the possibilities for security configuration backup and security layout reuse are high. Research in these areas is being conducted by the authors of this paper, and we anticipate significant improvements in the field of web application security in the near future.
Desmond, John. (2004). Top 10 Most Critical Web Application Security Flaws, 30 January. Available online [HREF9].
Friedrich, O, ed. (2004). Symantec Internet Security Threat Report. March 2004. Available online [HREF10].
Hall, Marty. (2002). "Declarative Web Application Security with Servlets and JSP", in Hall, M. More Servlets and JSP. Prentice Hall, USA. Available online [HREF11].
Huang, Yao-Wen, Shih-Kun Huang, Chung-Hung Tsai, and Tsung-Po Lin. (2003). "Web Application Security Assessment by Fault Injection and Behavior Monitoring" in Proceedings of the Twelfth International Conference on World Wide Web, May 20-24, Hungary. ACM Press, New York.
Jenkins, R Jr. (1997).Hash Functions for Hash Table Lookup. Available online [HREF12].
Menezes, A., Van Oorschot, P. and Vanstone, S. (1996). Applied Cryptography. CRC Press, California.
SPI Dynamics. (2002). Complete Web Application Security. SPI Dynamics, Atlanta.
Reschef, E. and Bar-Gad, I. (2001). Web Application Security. Available online [HREF13].
Van Der Walt, Charl. (2002). Assessing Internet Security Risk, Part Four: Custom Web Applications. Available online [HREF14].
Van Der Walt, Charl. (2002). Assessing Internet Security Risk, Part Five: Custom Web Applications Continued. Available online [HREF15].