To build secure software you need to understand the threats you face, where malicious inputs might enter the system, anticipate failure conditions, understand the principles of security, and have a processes to correct security issues and learn from them as they are discovered.
Let’s discuss some ways to address these concerns.
Understanding who might want to compromise your application and how they might do so is key to defending yourself. A useful starting point in visualizing threats is the data flow diagram, used by systems engineers to illustrate the data flows, data stores, processes, interactions and trust-boundaries that make up the application. Such a diagram will illustrate various access points that may act as attack vectors, and illustrate where malicious input may enter the system. For instance - can credentials coming over HTTPS be spoofed? Is your database accessible over the public internet?
With a good sketch of your system in hand, it can help to apply the STRIDE model for identifying threats at each access point. Developed at Microsoft, STRIDE is a mnemonic for six categories of security risk:
- Information disclosure (privacy breach or data leak)
- Denial of service
- Elevation of privilege
Applications that handle sensitive data may also require a data classification strategy. This will break your data into broad categories,so you can concentrate on protecting the most sensitive data. For instance, you might classify data items as one of:
- Public data - data available to everyone, like company blogs
- Private data - data available to authenticated users
- Restricted data - sensitive data that may be owned by particular users, like profile information
- High risk data - access tokens, passwords and credentials for third-party APIs that must never be shared
This will help you pick out high-value information likely to be targeted by attackers.
Securing the Development Lifecycle
Once you have modelled your threats, it’s time to turn that information into security requirements. A prerequisite for meeting such requirements is implementing a disciplined Software Development Life-Cycle (SDLC), so you can verify the code you write is actually solving the correct problems.
A good SDLC consists of the following elements:
Source control. All code your team writes should eventually enter source control, and certainly should be kept in a tool like
gitbefore being released. This will allow you to analyze code changes for potential security flaws, and review code running in your production environment.
Build Automation. Automate your build process to avoid any disparities between the code you test against and the code you run in production. This generally means using a package manager to manage dependencies and writing a build script that compiles code or generates assets.
Unit Testing. Your build process should also execute any unit tests you have. Writing unit tests is a good way to verify security requirements have been met (e.g. “only authenticated users can view the profile page”), and act as a form of documentation as the code is changed.
Continuous Integration. You should run your build process and unit tests whenever code is pushed into source control, so your development team gets immediate feedback when security requirements are violated by a set of code changes.
Code Reviews. Each code change should be reviewed by someone other than the author of the code change before being deployed. A reviewer acting as a second pair of eyes will often be able to spot possible security flaws before they go live.
Push-button Deployment. Your process for deploying to test and then production environments should be as simple as possible. Anything that requires human intervention opens the door for user-error, meaning you code may not deploy cleanly or at all.
Rollback Capability. You should have a process for undoing releases and quickly deploying a prior version of the code. This is essential if you ever deploy code with a security vulnerability, since you need to undo the mistake as soon as it is discovered.
Applying Security Principles
Secure design is a matter of philosophy, too. There are a handful of touchstones you should always seek to apply when writing code, which help protect against security risks you many not have even conceived of yet. These are:
Principle of least privilege. Each process, program and user should operate with the least amount of privilege required to achieve their goals. Applying this principle will mitigate the harm an attacker can do if they successfully compromise your system.
Validation of input. Validate any input coming from an untrusted source (like an HTTP request) to ensure it is the expected format, and reject it otherwise.
Segregation of tenants. Different environments (production and test) should be on separate networks and not share resources or configuration.
Encryption. Encrypting data at rest and in transit will protect against man-in-the-middle attacks and prevent sensitive information from being read if it is stolen.
Fail securely. Anticipate failure conditions, and ensure you do not leak internal architectural details in error messages. If unexpected error conditions arise, ensure you can roll back any database transactions to ensure data in not left in a corrupted state.
Observability. Your running code should issue logging statements and logs should be viewable at runtime. Your team should be able to observe the type and volume of traffic hitting your servers and how much of your available bandwidth is being used.
In all likelihood, your application will continue to add features and change over time in light of new requirements. Requirements should be documented - for example, as issues in an issue tracker - and code changes should reference the issues they are resolving. When reviewing code changes, you need to ensure that the new version of the code still meets overarching security requirements.
Pay particular attention to trust boundaries, where untrusted input enters the application. Any changes to how input is handled should be put under scrutiny. If often helps to think about sources - where input enters the system - and syncs - places in the code where sensitive operations are performed. Tracing through the code from each source to each sync will help highlight potential vulnerabilities.
Learning from Your Mistakes
Mistakes happen, and it’s key that you learn lessons any time a vulnerability makes it into production. A post-mortem should be performed to identity where safeguards failed to protect you. Remember, you are generally looking for failures in the process rather than individuals to blame - since nobody should be acting alone, and errors are generally caused by oversights rather than recklessness.
Once you have decided what safeguards are necessary, document the new process, and it’s helpful, add tests to ensure the issue does not occur.
Keep it Usable
Finally, it’s important not to make security protocols too onerous for your development team or users too onerous, or people will neglect to follow them. It’s true that the securest software is the application with no users, but that’s not the goal here!
- Threat Modeling (OWASP)
- Data Flow Diagrams
- The STRIDE Methodology
- Data Classification
- Secure Software Development Lifecycle
- What is a Software Post-Mortem and How Do You Write One?