Secure Coding Practices in C++

Alexander Obregon
6 min readJun 23, 2024

--

Image Source

Introduction

In software development, security is always a paramount concern. Writing secure code is critical, especially in languages like C++ that provide low-level memory access and control. This article highlights common security issues in C++ programming, such as buffer overflows, memory leaks, and race conditions. We will also be going over some basic guidelines on how to write secure code and use tools to find vulnerabilities.

Common Security Issues in C++

Buffer Overflows

A buffer overflow occurs when data written to a buffer exceeds its allocated size, overwriting adjacent memory. This can lead to unpredictable behavior, crashes, or even exploitable vulnerabilities if the overwritten memory includes executable code or critical data structures.

Example of a buffer overflow:

#include <iostream>
#include <cstring>

void unsafeCopy(char* dest, const char* src) {
strcpy(dest, src); // No bounds checking
}

int main() {
char buffer[10];
unsafeCopy(buffer, "This is a very long string that exceeds the buffer size");
std::cout << buffer << std::endl;
return 0;
}

Mitigation:

  • Use safer functions like strncpy instead of strcpy.
  • Implement bounds checking manually if using standard functions.
  • Utilize safer C++ classes like std::string.

Secure version:

#include <iostream>
#include <cstring>

void safeCopy(char* dest, const char* src, size_t destSize) {
strncpy(dest, src, destSize - 1);
dest[destSize - 1] = '\0'; // Ensure null-termination
}

int main() {
char buffer[10];
safeCopy(buffer, "This is a very long string that exceeds the buffer size", sizeof(buffer));
std::cout << buffer << std::endl;
return 0;
}

Buffer overflows can be particularly dangerous in systems programming and applications that require high reliability and security. Attackers can exploit buffer overflows to execute arbitrary code, gain unauthorized access to systems, or cause denial of service.

Memory Leaks

Memory leaks occur when dynamically allocated memory is not properly deallocated, leading to resource exhaustion and degraded performance over time. In long-running applications, memory leaks can cause the application to consume increasing amounts of memory, eventually leading to system crashes or slowdowns.

Example of a memory leak:

#include <iostream>

void createLeak() {
int* ptr = new int[10]; // Memory allocated but never deallocated
}

int main() {
createLeak();
return 0;
}

Mitigation:

  • Always pair new with delete and new[] with delete[].
  • Use smart pointers (std::unique_ptr, std::shared_ptr) for automatic memory management.

Secure version using smart pointers:

#include <iostream>
#include <memory>

void noLeak() {
std::unique_ptr<int[]> ptr(new int[10]); // Automatically deallocated
}

int main() {
noLeak();
return 0;
}

Memory management is a crucial aspect of C++ programming. Improper memory management not only causes memory leaks but can also lead to more severe issues like dangling pointers, which can result in undefined behavior or security vulnerabilities.

Race Conditions

Race conditions occur when multiple threads access shared data concurrently, leading to unpredictable and incorrect behavior. Race conditions can cause data corruption, crashes, and unpredictable behavior, making applications unreliable and difficult to debug.

Example of a race condition:

#include <iostream>
#include <thread>

int counter = 0;

void increment() {
for (int i = 0; i < 100000; ++i) {
++counter;
}
}

int main() {
std::thread t1(increment);
std::thread t2(increment);

t1.join();
t2.join();

std::cout << "Final counter value: " << counter << std::endl; // Expected 200000, but result is non-deterministic
return 0;
}

Mitigation:

  • Use synchronization primitives like mutexes (std::mutex) to control access to shared data.
  • Make sure that critical sections are properly locked to prevent simultaneous access.

Secure version using a mutex:

#include <iostream>
#include <thread>
#include <mutex>

int counter = 0;
std::mutex mtx;

void increment() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mtx);
++counter;
}
}

int main() {
std::thread t1(increment);
std::thread t2(increment);

t1.join();
t2.join();

std::cout << "Final counter value: " << counter << std::endl; // Correctly outputs 200000
return 0;
}

Race conditions are particularly problematic in multi-threaded applications where timing and order of execution are critical. Properly synchronizing access to shared resources is essential to avoid these issues.

Additional Security Issues

While buffer overflows, memory leaks, and race conditions are some of the most common security issues in C++, other potential vulnerabilities include:

Use-after-Free: Occurs when a program continues to use a pointer after the memory it points to has been freed. This can lead to undefined behavior, crashes, or security breaches.

Mitigation:

  • Set pointers to nullptr after freeing memory.
  • Use smart pointers to automatically manage object lifetimes.

Example of use-after-free:

#include <iostream>

void useAfterFree() {
int* ptr = new int(10);
delete ptr;
std::cout << *ptr << std::endl; // Undefined behavior
}

int main() {
useAfterFree();
return 0;
}

Secure version:

#include <iostream>
#include <memory>

void safeUse() {
std::unique_ptr<int> ptr(new int(10));
std::cout << *ptr << std::endl; // Safe access
}

int main() {
safeUse();
return 0;
}

Integer Overflow: Occurs when an arithmetic operation produces a result that exceeds the storage capacity of the integer type. This can lead to incorrect calculations and vulnerabilities.

Mitigation:

  • Use larger integer types if necessary.
  • Check for overflow conditions explicitly.

Example of integer overflow:

#include <iostream>

void integerOverflow() {
unsigned int max = 4294967295;
max += 1; // Overflow
std::cout << "Max value: " << max << std::endl; // Wraps around to 0
}

int main() {
integerOverflow();
return 0;
}

Secure version:

#include <iostream>
#include <limits>

void checkOverflow() {
unsigned int max = 4294967295;
if (max + 1 > max) {
max += 1;
std::cout << "Max value: " << max << std::endl;
} else {
std::cout << "Overflow detected" << std::endl;
}
}

int main() {
checkOverflow();
return 0;
}

Guidelines for Writing Secure C++ Code

Prefer Modern C++ Features

Modern C++ (C++11 and beyond) introduces several features that help write safer and more secure code. Leveraging these features can significantly reduce the risk of security vulnerabilities.

  • Smart Pointers: Use std::unique_ptr and std::shared_ptr to manage dynamic memory automatically. These smart pointers make sure that memory is properly deallocated when it is no longer needed, preventing memory leaks and dangling pointers.
#include <iostream>
#include <memory>

void exampleUniquePtr() {
std::unique_ptr<int> ptr(new int(10));
std::cout << *ptr << std::endl;
} // Memory is automatically deallocated here

int main() {
exampleUniquePtr();
return 0;
}
  • RAII (Resource Acquisition Is Initialization): Encapsulate resources, such as file handles or network connections, in objects whose destructors automatically release the resources. This makes sure that the resources are properly cleaned up, even in the presence of exceptions.
#include <iostream>
#include <fstream>

void exampleRAII() {
std::ifstream file("example.txt");
if (file.is_open()) {
std::string line;
while (std::getline(file, line)) {
std::cout << line << std::endl;
}
}
} // File is automatically closed here

int main() {
exampleRAII();
return 0;
}
  • Auto Keyword: Use auto to avoid type mismatches and make the code cleaner and less error-prone.
#include <iostream>
#include <vector>

void exampleAuto() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
for (auto it = numbers.begin(); it != numbers.end(); ++it) {
std::cout << *it << std::endl;
}
}

int main() {
exampleAuto();
return 0;
}

Regularly Update and Use Static Analysis Tools

Static analysis tools can detect potential vulnerabilities and coding errors early in the development process. Regularly updating these tools makes sure that the latest known issues are caught and addressed.

  • Clang-Tidy: A popular tool that integrates with Clang to provide linting and static analysis. It can detect various issues, such as memory leaks, buffer overflows, and undefined behavior.
  • Cppcheck: A versatile tool that analyzes C++ code for potential errors, such as null pointer dereferences, memory leaks, and buffer overflows.

Example command to run Cppcheck:

cppcheck --enable=all --inconclusive --std=c++11 .

Follow Secure Coding Standards

Following established secure coding standards can help prevent common vulnerabilities and promote best practices in C++ development.

  • CERT C++ Secure Coding Standard: This standard provides comprehensive guidelines for writing secure C++ code. It covers various aspects, such as input validation, memory management, and concurrency.
  • MISRA C++: Originally developed for automotive software, MISRA C++ provides a set of guidelines that help ensure safety and reliability in C++ programs.

Conclusion

Secure coding practices are essential for developing strong and reliable C++ applications. By understanding and addressing common security issues like buffer overflows, memory leaks, race conditions, use-after-free errors, and integer overflows, developers can mitigate potential vulnerabilities. Adopting modern C++ features, leveraging static analysis tools, following established secure coding standards, and conducting regular code reviews and penetration testing further enhance the security of C++ code. These practices not only protect applications from malicious attacks but also ensure their stability and performance in the long run. By committing to secure coding principles, developers contribute to creating safer and more trustworthy software systems.

  1. CERT C++ Secure Coding Standard
  2. Cppcheck
  3. Clang-Tidy
  4. C++ Core Guidelines

Thank you for reading! If you find this article helpful, please consider highlighting, clapping, responding or connecting with me on Twitter/X as it’s very appreciated and helps keeps content like this free!

--

--

Alexander Obregon

Software Engineer, fervent coder & writer. Devoted to learning & assisting others. Connect on LinkedIn: https://www.linkedin.com/in/alexander-obregon-97849b229/