secure by design - summary
I really like Secure by Design. The key idea is that there is a big overlap between secure code and good software design. Code that is strict, clear and focused will be easier to reason about, and will have fewer bugs. This in turn makes it less vulnerable to attacks. This is easy to say, but Secure by Design is full of techniques for how to actually do this. Here are the ideas from the book that I liked the most.
Domain Primitives
Domain primitives are similar to value objects in Domain-Driven Design (DDD). They are immutable, defined only by their values, and form a conceptual whole. Any invariants are enforced at the time of creation. This means that if a domain primitive exists, it is valid.
For example, say that you want to represent the number of books ordered. Instead of using an integer for this, define a class called Quantity. It contains an integer, but also ensures that the value is always between 1 and 240 (if that’s the upper limit). Or for a user name, instead of just using a string, define a class called UserName. It contains a string holding the user name, but also enforces all the domain rules for a valid user name. This can include minimum and maximum lengths, allowed characters etc.
The goal is that nothing in the domain should be represented by primitive types in the language (int, float, string etc). Every domain value should instead be represented by a domain primitive. There are several advantages to this approach. All the validation for each domain primitive is in one place. No validation is needed in the state handling business logic – if the value exists, it is automatically valid. This makes the business logic a lot cleaner. There is also less risk of mixing up parameters in method calls – Quantity and DeliveryDays is better than int and int. Furthermore, bugs of the type where a negative amount of books is ordered become impossible.
As they state in a tip in the book: “Any integer between -2 billion and 2 billion is seldom a good representation of anything.”
Validation
External input needs to be validated before it is used in the system. To minimize the risk of denial of service attacks, the validation should be done in the following order:
- Origin. Where does the data come from? Can check the IP address, or check an access key in the request.
- Size. A payload of one million characters should probably be rejected without further analysis. As well as checking the total size, it is good to check the sizes of the parts.
- Lexical content. Only the right type of tokens should be allowed.
- Syntax. For example, if the format is XML, this checks that there is a closing tag for each opening tag, and that attributes inside tags are well formed.
- Semantics. This is often part of the business logic. Is this a valid product number? The format of the number can be correct, but if there is no product in the product catalog with that number, then it is semantically invalid. Often this step requires a database lookup, which is expensive, which is why this check is performed last.
Entities
In DDD, the business logic typically resides in entities. An entity has an identity, so it can be distinguished from other entities. It can contain other objects, both entities and value objects. To perform the business logic, it needs to mutate state. The entity is responsible for coordinating the objects it owns, including ensuring internal invariants. The following techniques help with this:
Consistent at creation. The entity should always be consistent to the outside world. This means that all parameters necessary for consistency should be provided in the constructor. If there are complicated rules when creating the entity, the builder pattern can be used. Also, using private final fields for values that can’t be changed is good, since the compiler will enforce that they are not changed.
Limited operations. Don’t have methods that can do more than what is allowed by the business logic. For example, if an order entity has a field for whether it is paid or not, it should default to false at creation. Then there should only be a method that is called for example markPaid, that sets the field to true. This is better than a setPaid method that could set the value to either true or false, since that would make it possible to go from paid to not paid, which is not valid (if this is the business rule).
Not sharing mutable objects. For the entity to be able to uphold its internal constraints, it must not leak references to internal objects. Suppose there is a Customer object, and it has a reference to a CreditScore object. Even if the variable holding the CreditScore reference is final, the CreditScore object can still be modified by anybody holding a reference to it. To be sure the CreditScore is set once and never modified, a copy of it must be stored in the Customer object (and the reference to that must never be exposed outside the Customer object). The same problem exists for collections, like lists. They are mutable by default. If an internal collection needs to be exposed outside the entity, a copy of it (using for example a copy constructor) should be returned.
Sometimes, if there are complicated consistency rules that must be upheld, it can be good to define a method called e.g. checkInvariants(). This can be called at the end of regular mutating methods, to make sure the entity is still internally consistent.
The Three R’s
For applications run in the cloud, with a high degree of automation, it is possible to take advantage of the three R’s to increase security:
- Rotate secrets automatically every few hours
- Repave servers and applications every few hours. This means redeploying the same software – if an attacker has compromised a server, the deploy will wipe out the attacker’s foothold there
- Repair vulnerable software as soon as possible (within a few hours) after a patch is available.
Other Topics
There are many other topics covered in the book. Here I will just mention a few of those. There is a chapter on how to include security-focused tests in the automatic tests suites. It also has a good discussion on how to detect changes in default behaviors of external components and frameworks. There is also advice on logging, admin processes and how to refactor legacy systems to use more domain primitives.
Odds and Ends
Consider that domain rules can be exploited too. For example, booking a lot of rooms at a hotel, then cancelling all bookings at the last minute is a form of denial of service attack.
Don’t use exceptions for the control flow. Account not found, or insufficient funds when instructing a money transfer are normal results that should not cause exceptions to be thrown.
I like the way Apache Validate returns the validated object, so you can write code like this: this.name = notNull(name)
The code examples are all in Java, and do a good job of illustrating the points made in the text. I really like the use of arrows that highlight parts of the code – they are quite helpful, and should be used in other books as well.
Conclusion
Secure by Design is quite a practical book, with many ideas that can be used right away. It shows concrete ways of coding that improve security by limiting the ways in which bugs can slip in. There are quite a few code examples that help explaining the concepts. Sometimes the book is too wordy, such as the example in chapter 11, where insurance policies where issued without payment. But overall it is great resource for developers that want to write more secure code.