Top 5 tips for C++ security
2022年7月19日
0 分で読めますC++ has become a pivotal part of the modern day tech industry. It has been used for multiple purposes, such as desktop applications, server applications, gaming, virtual reality, internet of things firmware, and even as the foundation for many modern day programming languages.
Since the initial C++ release in 1985, as an extension to the C programming language, it was designed with an orientation towards system programming and embedded resource development.
As with any widely adopted or user based development, it’s important to ensure that elements of security have been integrated throughout the application. So, let's look at some security tips to keep in mind when building with C++.
1. Input validation and sanitization for C++ applications
Handling user based input is paramount to protecting an application from unwarranted intrusion. C++ has been used extensively over the years because it is able to handle object-oriented memory manipulation — which, while particularly useful for application development, can also be utilized as an attack vector by malicious actors.
One method is using the std:cin
function for handling variables. This can be employed in a few ways, such as:
1int main()
2{
3 int varnum;
4
5 while (true) {
6 cout << "Enter a number: ";
7 if (cin >> varnum) {
8 break;
9 } else {
10 cout << "Enter a valid number!\n";
11 cin.clear();
12 cin.ignore(numeric_limits<std::streamsize>::max(), '\n');
13 }
14 }
15
16 return EXIT_SUCCESS;
17}
In this function, cin.clear()
clears the error status flags and cin.ignore()
removes all characters left in the stream up to the new line character. It's important to handle those error messages so that they don't get displayed as part of the end user output. Error and alert messages can be utilized to surface unwanted attack vectors.
2. Buffer overflows or underflows
It's important to check input length, range, format, and type when handling any user input in C++. It's more granular approach to interaction with the core operating system means that when functions are injected with a large amount of input buffer an overflow attack can occur.
Arithmetic overflow can occur when an arithmetic result exceeds the values represented by the expression type. For example:
1uint16_t x = 65535;
2x++;
3
4std::cout << x;
The above function will output 0
as a response because x is an unsigned integer which can represent values between 0 and 65535. Exceeding the range means that the value cannot be represented by unit16_t
, thus causing an overflow to occur.
One way to handle overflow and underflow is to set catch when processing a variable type between its minimum and maximum values — effectively detecting anything that would fall above or below the range boundaries. Using templates sets this as a range and, in turn, sets up a reusable catch all for occurrences.
1#include <limits>
2
3template <typename V>
4constexpr bool AdditionOverflows(const V& a, const V& b) {
5 return (b >= 0) && (a > std::numeric_limits<V>::max() - b);
6}
7
8template <typename V>
9constexpr bool AdditionUnderflows(const V& a, const V& b) {
10 return (b < 0) && (a < std::numeric_limits<V>::min() - b);
11}
It's important to make sure you avoid undefined behavior when using C++. Most compilers will warn you for undefined behavior when you build your application. GCC and Clang for example have the -fsanitize=undefined flag
.
Additionally, be careful using functions like strcpy() and strcat() in the C++ suite. While useful for handling variables, they can cause overflows and underflows if the value being stored exceeds the storage type.
3. Incorrect file system operations risk path traversal vulnerabilities
Improper handling of files can lead to file path traversal vulnerabilities. Which is hazardous in any framework, but especially in an operating system with a granular language like C++. In C++, files are handled as a stream and as an abstraction that represents stream input and output. A stream can be represented as a stream of characters of indefinite length, depending on code structure.
C++ uses a set of file handling techniques, including ifstream()
, ofstream()
, and fstream()
. These functions are derived from fstreambase
and iostream
class groups, and are designed to manage the fisk files that are declared in fstream
.
The difference between the different file handling classes are:
ofstream
: This class signifies the output stream and is applied to create files for writing information to filesifstream
: This class signifies the input file stream and is applied for reading information from filesfstream
: This class can be used to both read and write from/to files.
It’s important to make sure that file streams are handled securely. Starting with the new_file.open()
to initiate a new file, then writing the file stream to the file. Lastly it's important to close the file once it’s complete, and have a catch statement for any errors that may occur during transit.
For example:
1#include <iostream>
2#include <fstream>
3using namespace std;
4int main()
5{
6 fstream newFile;
7 newFile.open("file_write.txt",ios::out);
8
9 if(!newFile)
10 {
11 cout<<"File failed";
12 }
13 else
14 {
15 cout<<"File created";
16 newFile<<"File write"; //File write
17 newFile.close(); // Close file
18 }
19 return 0;
20}
4. Limit the use of system() and process execution calls to avoid command injection
Like with many other languages, core system interaction through code-based functions should be approached with great caution. In C++, functions like system() should be avoided or utilized carefully. These system based functions will vary depending on the chipset and hardware the application is running on.
It's also important to ensure that the application will be installed and run in a way that allows for additional executable functions like exec()
, execp()
, and CreateProcess()
. However, these should also be approached cautiously — especially considering that they can open core operating system access.
Keep in mind that C++ based applications have a lot more granular access to critical system information, which can expose more malicious activities and vulnerable paths.
5. Scan project libraries with a comprehensive static code analysis tool
Keeping track of the potentially vulnerable paths in your project libraries and dependencies is vital to maintaining the security. Vulnerable paths can allow for unwarranted behavior from malicious actors, especially in C++ projects which are installed on a variety of systems at different levels of architecture.
Static code analysis, or SCA, is handy because it can alert you to potential vulnerabilities while you are building an application, as well as notify you when a vulnerability is identified in the project being deployed.
Getting started with Snyk for C++ SCA scanning
To get started with Snyk, you’ll need to install the Snyk CLI into your development environment. There's a number of ways to install the CLI, including npm, Yarn, and standalone executables.
The next step is to connect the CLI to your Snyk account. If you don’t already have a a Snyk account, signing up is quick and easy. Head on over to the sign up page and follow the steps to get started.
Once you’ve installed the CLI and signed into your Snyk account, run snyk auth
from the command line to finish connecting your account. This will trigger an authentication link and automatically open a browser to the authentication page. Click Authenticate and you’re ready to use the Snyk CLI.
Now, for the final step, navigate to the root directory of the project you want to scan and run snyk test –unmanaged
. The scan process will identify the folder in the root of the project that contains the open source libraries and run the scan.
Once complete you will see the full output of anything found and allow you to investigate further. One thing I really like here is that you can delve into the identified issues using the links from the scan to better understand the vulnerability further.
Sign up now and get scanning
As developers, devops, and technology folk, it’s important to prioritize security and do everything we can to keep the applications we’re building safe and secure. Building security into application development helps ensure that we protect our end users and keep them — and their data — safe.