Security – an introduction
Security is an extremely important and deep topic. Bad things happen when security is compromised, and we have to keep that in mind at every stage of the software development life cycle. Unlike some other non-functional requirements, security cannot (usually) be baked into the system as an afterthought. ISO 9126, which describes quality attributes of a software system, names six major categories:
Security comes as a quality attribute under Functionality. Under security, we have quality attributes like Confidentiality, Integrity, Availability (together, famously known by the acronym CIA), Non-Repudiation, Authenticity, and Accountability.
Security has a direct influence on its sub-attributes. This shouldn’t come as a surprise. What is surprising, though, is the effect of security on other quality attributes. For example, security has an indirect effect on performance and reliability. If the security of the system is compromised, the system may become unresponsive following a deliberate attack such as a denial-of-service (DoS) or distributed-denial-of-service (DDoS), or merely due to accidental high traffic that is not well managed and gets the service down. Similarly, if security is compromised, the system can become unreliable, spewing out unexpected results. Here is how I would depict it:
The current trend in security is Zero Trust Architecture where we apply the “never trust, always verify” paradigm to protect our system. We adhere to the secure by design paradigm once the architecture is in place. Only when both architecture and design are secure, we come to secure coding practices –the topic of this blog – and more specifically, top ten best practices in secure C++ development.
Let’s begin!
-
Understand that there are no safety nets provided by the compiler or runtime while coding in C++.
C++ compiler generates the code the programmer asked it to generate, without adding any safety checks. While coding in C# or Java, for example, incorrect array access would lead to a runtime exception, whereas in C++ this leads to incorrect memory access or memory corruption in case of writing. Incorrect or sloppy coding can lead to overflows (stack, heap, and buffer overflows) which can easily be used for an attack.
-
Don’t misuse APIs. Don’t rely on undocumented behavior. Don’t use APIs that are established to be vulnerable.
Depending on undocumented behavior is a doorway to security breaches that would arise if the actual behavior is not as assumed or has changed over time. This is explained in CWE (the community-based Common Weakness Enumeration) under item 440: expected behavior violation.
At the same time, it is highly advisable to refrain from using APIs that are well known to be vulnerable, including but not limited to strcpy, sprintf and system. It’s not that these functions are always unsecure, but used incautiously, they can be abused by an attacker. Static code analysis can usually detect and warn of bad usage of such APIs.
-
Validate input. Another classic.
There is no reason for anyone in 2021 to be writing oodles about input validation, but rest assured that if you trust the user with correct input, an attacker will find a way to breach the security of your application.This vulnerability is not specific for C++ but is of course also relevant to it. Input validation doesn’t only mean user input; in case your input comes from any source outside your system, it might be malformed. Even if it comes from another system that you do not control but consider reliable, the other system might have been penetrated and you don’t want to be relying on the security measures taken by any external system.
It is interesting to note that in the OWASP top ten vulnerabilities list, Unvalidated input was the number 1 threat in 2004. But since then two things happened: (a) the notion that input should be validated and sanitized has permeated into code review checklists and best practices, and (b) OWASP decided to rename this category, focusing on the security threat evolving from invalid data, creating a new category called “Injection” and moving some bad input vulnerabilities which are not related to “Injection” into other categories.
In the following years “Injection” was still high in the list (1st or 2nd), moving down to being number 3 in 2021.
Read more about techniques for input validation in the OWASP cheat sheet for input validation.
-
Type safety: Types are your friends. Don’t intentionally bypass type checking!
Gone are the days when we used passed around void*, bypassing type checking. Gone should also be the practice of casting to bypass strong type checking. Improper use of void* and casting can result in retrieval of bad data, which then can be exploited. Similarly, do not just downcast by reinterpret_cast or with C-style casting. Do not remove constness with const_cast or with C-style cast just because you need to and the compiler lets you. C-style casting, pointer casting and reinterpret_cast are generally risky and can be a source for exploitable bugs.
On the same note, though more as a best practice approach, it would be better working with strong types that carry with them their measurement units. You can read more about it here.
-
Be careful about arithmetic overflows or underflows in code.
Trick question. Why is the code below:
#include
int main() { for (size_t i = 5; i >= 0; --i) std::cout << i << ' '; }