Chapter 17. Development

As we have discussed in previous chapters, any code that is executed on a system can contain errors, and if these errors can be leveraged by an attacker then this becomes a vulnerability in the system. This is, of course, something that we do not want.

The aim of securely developing code is, somewhat obviously, to reduce the chances of this occurring, and to reduce the impact if it does.

Secure coding insofar as particulars within any one language is a large and complex field, far too expansive to cover in its entirety within this book. However, we have covered the high-level concepts so that you can understand enough of the topic to be able identify specific areas that are useful for you to go and research separately.

Language Selection

Anyone who codes with any regularity is probably aware that a variety of programming languages are available. She is probably also aware that the choice of programming language can have an effect on a number of areas, including ease of development, speed of execution, availability of libraries, resources required, operating system compatibility, and a wide array of other factors that contribute to the decision. One of the lesser considered factors is the impact upon security.

Of course, the choice cannot be entirely about security. Failure to meet other business objectives for the sake of security alone tends to lead to organizations going out of business (unless the purpose of the business is security). But security should be a factor, especially when other requirements leave several options open.

In the interests of keeping this section from spilling over into an insane number of pages and trying to characterize every language, we have selected some common examples to illustrate the main themes and characteristics. If you are running a development department, you should conduct your own research into your language options, however these examples should serve as guide to the types of features to consider.

0xAssembly

Assembly is about as close to the hardware as you can get, and with that power comes enormous scope for error. In comparison to other languages, writing directly into assembly provides very few checks that what you are asking the processor to execute makes any sense or will work. The potential issues this can create are far beyond those of a simple off-by-one error in other languages.

Assembly is made further complicated by having no abstraction from the processor, and so unlike most other languages, needs to be written with specific knowledge of the type of processor being used.

Thankfully, with the exception of a few key disciplines, assembly is a very unlikely choice of language that you will face unless you are already experienced.

/* C and C++ */

C, and to a lesser degree, C++, are the next best thing after assembly for being close to the hardware but without the burden of needing to deal with nitty gritty like processor registers. C and C++ are written in an ASCII file that is compiled into a binary executable. They offer familiar statements such as if and else for flow control. A lot of code at the operating system layer is written in C, C++, or a combination. Prime examples of this are the Linux and BSD kernels.

At compile time, a modern compiler will perform some basic checks against the code. However, the very features that provide the power to C and C++ are also their downfall. For example, both languages allow the use of pointers—variables that point to another location in memory that holds a variable, or another pointer, as opposed to holding a value themselves. Features such as the ability to use pointers, leaving lots of scope for “unchecked buffers,” “use after free,” “double free,” and other memory-related conditions as pointers that are not managed properly can easily end up pointing to the wrong location in memory. These conditions in turn lead to entire classes of bugs that are much more difficult to introduce when using other languages.

GO func()

GO, often referred to as GOLANG, is also a compiled language, but one that is much more modern and with more protections traditionally offered within scripting languages rather than compiled languages. It is a garbage-collected language, meaning that unlike C, C++, or assembly, GO is designed to manage memory (mostly) automatically rather than leaving it in the hands of the developer, closing many of the bugs that cna be introduced in languages that run closer to the bare metal.

Pointers still exist, but pointer arithmetic does not. Additionally, accessing an out-of-bounds memory location results in a panic, reducing the impact of their presence as the code will simply stop rather than exhibiting unknown, potentially dangerous behaviors. This makes accidentally introducing an exploitable buffer overflow very difficult.

GO is also very strongly typed—that is, every object is of a known type at compile time. Variables cannot be cast to another type; rather, they need to be type switched, which is a safer operation from the perspective of introducing vulnerabilities to the system.

#!/Python/Ruby/Perl

Scripting languages such as Python, Ruby, and Perl are written into a human-readable file that is parsed by an interpreter at run time, rather than being compiled into an executable binary. The interpreter internally translates each statement into a series of precompiled routines and executes those. Typically, there is very little in the way of memory management required of the developer with variable sizes automatically growing, garbage collection being automated, and such. This removes many of the flaws associated with memory-related vulnerabilities. Of course if there is a flaw in the interpreter itself, then problems will manifest across all scripts. However, they are equally all positively affected by a successful patch of said buggy interpreter.

It does not, of course, remove all bugs. For example, interpreted languages, by virtue of their flexibility and ease of development, fall foul of other vulnerabilities. The ease with which data can be consumed can often lead to failure to correctly parse input. Suffering from unexpected function behaviors and lazy management of data due to automated memory management are both common flaws in interpreted scripts.

<? PHP ?>

Strictly speaking, PHP should fall into the same category as Python, Ruby, and Perl as it too is interpreted and has many of the same advantages and disadvantages. However, PHP deserves a special mention because it is often derided for security issues in many common scripts.

Some of the negative press toward PHP is not because the interpreter itself contains vulnerabilities per se, but that the nature of the language makes it incredibly easy for developers to implement flaws into their code without realizing it. This has, in turn, led to many issues in a wide range of popular PHP-based applications.

For example, vulnerabilities have been introduced into PHP-based applications because a programmer will dynamically generate the filename for a server-side include based on some user input, without realizing that the function to include files also permits remote includes as an argument. A prime example of this is having the name of a theme stored in a cookie, which dynamically loads a locally included file to present that theme to the user. However, PHP is flexible in that if a URL is provided to the include argument, it will perform a remote include. This means an attacker can craft a cookie to provide a theme name that is a URL to a script that he controls, which causes a remote include of a file of his choice and subsequently runs his code.

Secure Coding Guidelines

When you work on a project alone or in a small group it can seem pointless to have secure coding guidelines. However, once you move into a larger development team, outsource components of development, or have rotations in staffing, the necessity becomes clear. Secure coding guidelines not only educate less experienced staff with regard to expectations surrounding the level of code quality in areas such as input validation and memory management, but also provides a level of consistency across the entire code base.

Secure coding guidelines are in many ways like policies, standards, and procedures as they pertain to code.

For example, with regard to validation of user input, the following statements could be applicable, depending on the environment:

  • User-supplied input is defined as data that is received over a network, from a file, from the command line, from interactions with the GUI, or from a peripheral device.

  • User input must never be processed without prior validation.

  • Validation includes:

    • Data is of the expected type (e.g., numeric, alphanumeric, binary blob)

    • Data is of the length expected

    • Data is within the range expected

    • Data is received in conjunction with other data that passes validation

  • Data that fails to correctly validate will be discarded, along with any other data, valid or not, received in the same transaction.

Additionally, coding guidelines may stipulate that specific libraries or code snippets should be used for commonly occurring tasks to ensure further consistency across the codebase.

Similar guidelines should be included for a wide range of commonly occurring areas such as the use of cryptography, database access, memory management, network communication, error handling, authentication, audit trails, and access management. There may be further guidelines within specific types of applications. For example, a web application will almost certainly require guidelines with regard to session management and differences between requirements on client-side and server-side components.

Testing

Once code has been written it should be tested before being released into production. This is a fairly well-understood process from the perspective of quality assurance, performance, and meeting functional requirements. However, many forget that security testing should also be performed in order to prevent detectable bugs from making their way into a production environment.

There are many subtle variants on types of testing, and which one is most appropriate and will yield the most results will vary depending on the environment. For example, the average lifespan of a release can vary from minutes in a continuous deployment environment to years in some more traditional configurations. The amount of time consumed by any particular testing methodology could easily be a factor if rapid turnaround to production is a factor.

Automated Static Testing

Static testing is a type of analysis whereby the code is not executed as part of the testing process, but rather source code is analyzed by specialist tools that search for a number of programming flaws that can often lead to the introduction of a vulnerability.

This kind of testing is quick and fairly easy to set up, but it is prone to false positives and can often require someone who understands secure development practices. This understanding gives her the ability to explain to the development team why the issues highlighted are a problem and to determine which of the findings are false positives.

The types of issues that are easily highlighted are often of the ilk of memory bugs, such as buffer overflows, and the use of unsafe functions. Static testing does not normally fare well with “design” type issues, such as the incorrect use of cryptography, flaws in authentication mechanisms, and replay attacks, as these flaws are due to implementation choices, not code that is operating in an unexpected manner.

Automated Dynamic Testing

Dynamic testing is performed by executing the application in real time and analyzing how the running code operates under various conditions. Rather than trying to formulaically “understand” the operation of the application based on the content of the source code, dynamic testing will apply various inputs. These inputs in turn create outputs and internal state changes, which are used to determine potential flaws in the executing code.

This type of testing is more complex and time consuming to set up compared to static testing. It may lack complete coverage, as no source code visibility means that some areas of the application may not be tested due to a lack of visibility to the test tool. That said, dynamic testing can uncover entire classes of vulnerabilities that are much more difficult to test for using static testing techniques, such as temporal issues, certain types of injection, and reflection techniques.

Peer Review

Peer review, or code review, is a manual process whereby another developer will conduct a systematic review of either the entire application or, more commonly, the area that has most recently been updated. The intention is to assess the code for mistakes of all types using an experienced eye.

This is not an automated process, so the number of false positives is often lower. The reviewer will typically validate her findings and so will naturally filter out false positives in the process. Of course this is a human process, and as with the initial creation of the code, is subject to human error. Additionally, the level of coverage will probably depend on the experience of the person conducting the testing. Someone who is not well versed in a particular type of issue or the specific nuances of the language being used could well miss classes of vulnerabilities.

System Development Lifecycle

As we have touched on in other chapters, consistency of process is a good way to ensure that you are in control of your environment and able to spot when things are not working quite as they should.

It is of note that in many systems (or software) development lifecycles (SDLC), security is not mentioned formally. If possible, being able to inject security directly into the SDLC could be very beneficial. Otherwise, security can be treated as an afterthought or optional extra, rather than a core component, which leads to it being lower priority than other criteria.

The SDLC is the normal way that processes around development are defined. There are a few variations on the SDLC from various organizations. However, most align to approximately the same structure with a few changes in nomenclature:

Training

This is the building block upon which all other work is derived. Staff should not only be trained in the art of software development, but should also receive training in secure software development, threat modeling, security testing, and privacy principles. This will equip developers with the knowledge to not only write better code in the first place, but to understand the results and take appropriate action during testing and review.

Requirements

It is a common tendency to define functional requirements and security requirements as two separate lists. We would recommend that security requirements be included as functional requirements. This avoids ambiguity as to whether or not security requirements are mandatory. It also simplifies documentation and keeps security forefront of mind, as opposed to being an afterthought addressed after the all-important functional requirements.

Architecture and design

Determining an architectural model for a system prior to coding should lead to a more secure system overall. This is because the process of architecture and design should set high-level expectations such as where encryption will be used, where access controls are enforced, and where sensitive data will be located. Additionally, part of the architecture and design process should include threat modeling exercises to focus on high-level methods of attacking proposed solution designs. This will facilitate redesigns to reduce attack surface to an acceptable level and ensure that the appropriate controls are implemented in the right places.

Code and build

Coding takes place based on a combination on the architecture and design specifications, which in turn should meet the requirements laid out previously. Code should be built using the predetermined language, conform to any coding guidelines that have been set out by the organization, and align with any other agreed-upon best practices.

Test and results

The code should be tested, not just functionally, but using security testing methodologies—multiple if applicable—to determine any security defects. Defects should be assessed and entered back into the development process for the appropriate fix.

Release

Release the code as per the internal release process. This includes a final security review, ensuring that the system has been operationalized in terms of documentation, processes, and procedures such as incident response and business continuity, as well as handing over to operational support teams.

If you would like to learn more about SDLC, we would suggest reading the work that Microsoft has done in this field.

Conclusion

If your organization produces code either as a product or for internal use, there is always a risk that the code contains errors. Errors are what leads to vulnerabilities. By creating a software development lifecycle, testing your code, and making wide decisions about your code, these risks can be greatly reduced.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.121.160