C++ Security Review Skill
This skill ensures all C++ code follows security best practices and identifies potential vulnerabilities.
When to Activate
- •Handling user input or parsing data
- •Memory allocation and pointer operations
- •File I/O and filesystem operations
- •Network programming and sockets
- •Multi-threaded programming
- •Working with external libraries
- •System calls and OS interactions
Security Checklist
1. Memory Safety
Buffer Overflow Prevention
cpp
// NEVER: Unbounded buffer operations char buffer[256]; strcpy(buffer, user_input); // DANGEROUS! sprintf(buffer, "%s", user_input); // DANGEROUS! // ALWAYS: Use bounded operations std::string safe_string = user_input; // If C-style buffers needed, use safe variants char buffer[256]; strncpy(buffer, user_input, sizeof(buffer) - 1); buffer[sizeof(buffer) - 1] = '\0'; snprintf(buffer, sizeof(buffer), "%s", user_input);
Smart Pointer Usage
cpp
// NEVER: Raw new/delete Resource* r = new Resource(); // ... code that might throw ... delete r; // May never be called! // ALWAYS: Smart pointers auto r = std::make_unique<Resource>(); // Automatic cleanup, exception-safe // NEVER: Array new with raw pointer int* arr = new int[100]; delete[] arr; // ALWAYS: Use containers or smart pointers std::vector<int> arr(100); auto arr = std::make_unique<int[]>(100);
Avoid Use-After-Free
cpp
// DANGEROUS: Returning reference to local
std::string& get_name() {
std::string name = "local";
return name; // UNDEFINED BEHAVIOR!
}
// SAFE: Return by value
std::string get_name() {
std::string name = "local";
return name; // Move semantics apply
}
// DANGEROUS: Dangling pointer
int* ptr;
{
int x = 42;
ptr = &x;
}
*ptr = 10; // UNDEFINED BEHAVIOR!
// SAFE: Proper lifetime management
std::unique_ptr<int> ptr = std::make_unique<int>(42);
Verification Steps
- • No raw new/delete (use smart pointers)
- • No unbounded string operations (strcpy, sprintf, gets)
- • No returning references/pointers to local variables
- • All pointers checked for nullptr before use
- • No manual memory management
2. Input Validation
String Input Validation
cpp
#include <regex>
#include <limits>
// Validate string length
bool validate_input(const std::string& input, size_t max_len) {
if (input.empty()) {
return false; // Reject empty input
}
if (input.length() > max_len) {
return false; // Reject too long input
}
return true;
}
// Validate with regex
bool is_valid_email(const std::string& email) {
static const std::regex email_regex(
R"([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})"
);
return std::regex_match(email, email_regex);
}
// Safe integer parsing
std::optional<int> parse_int(const std::string& str) {
try {
size_t pos;
int value = std::stoi(str, &pos);
// Ensure entire string was parsed
if (pos != str.length()) {
return std::nullopt;
}
return value;
} catch (const std::exception&) {
return std::nullopt;
}
}
Numeric Overflow Prevention
cpp
#include <limits>
#include <stdexcept>
// Safe addition
int safe_add(int a, int b) {
if (b > 0 && a > std::numeric_limits<int>::max() - b) {
throw std::overflow_error("Integer overflow");
}
if (b < 0 && a < std::numeric_limits<int>::min() - b) {
throw std::underflow_error("Integer underflow");
}
return a + b;
}
// Safe multiplication
int safe_multiply(int a, int b) {
if (a > 0 && b > 0 && a > std::numeric_limits<int>::max() / b) {
throw std::overflow_error("Integer overflow");
}
if (a > 0 && b < 0 && b < std::numeric_limits<int>::min() / a) {
throw std::underflow_error("Integer underflow");
}
if (a < 0 && b > 0 && a < std::numeric_limits<int>::min() / b) {
throw std::underflow_error("Integer underflow");
}
if (a < 0 && b < 0 && a < std::numeric_limits<int>::max() / b) {
throw std::overflow_error("Integer overflow");
}
return a * b;
}
// Safe size calculation for allocation
size_t safe_array_size(size_t count, size_t element_size) {
if (count > 0 && element_size > std::numeric_limits<size_t>::max() / count) {
throw std::overflow_error("Size overflow");
}
return count * element_size;
}
Verification Steps
- • All user inputs validated before use
- • String lengths checked and bounded
- • Integer operations checked for overflow
- • Array indices bounds-checked
- • File paths sanitized (no path traversal)
3. Format String Vulnerabilities
cpp
// NEVER: User input as format string
printf(user_input); // DANGEROUS!
fprintf(file, user_input); // DANGEROUS!
// ALWAYS: Use format string with user input as argument
printf("%s", user_input);
fprintf(file, "%s", user_input);
// BETTER: Use type-safe formatting
#include <fmt/core.h>
fmt::print("{}", user_input);
// Or iostream
std::cout << user_input << std::endl;
4. File Operations Security
Path Traversal Prevention
cpp
#include <filesystem>
namespace fs = std::filesystem;
// Validate file path is within allowed directory
bool is_safe_path(const fs::path& base_dir, const fs::path& user_path) {
// Normalize paths
auto canonical_base = fs::canonical(base_dir);
auto requested = fs::weakly_canonical(base_dir / user_path);
// Check if requested path is under base directory
auto [end, nothing] = std::mismatch(
canonical_base.begin(), canonical_base.end(),
requested.begin()
);
return end == canonical_base.end();
}
// Safe file open
std::optional<std::ifstream> safe_open_file(
const fs::path& base_dir,
const std::string& filename
) {
// Reject absolute paths and path traversal
if (filename.find("..") != std::string::npos) {
return std::nullopt;
}
fs::path file_path = base_dir / filename;
if (!is_safe_path(base_dir, filename)) {
return std::nullopt;
}
std::ifstream file(file_path);
if (!file.is_open()) {
return std::nullopt;
}
return file;
}
Secure File Permissions
cpp
#include <filesystem>
#include <fstream>
namespace fs = std::filesystem;
// Create file with restricted permissions
void create_secure_file(const fs::path& path, const std::string& content) {
// Write content
std::ofstream file(path);
file << content;
file.close();
// Set restrictive permissions (owner read/write only)
fs::permissions(path,
fs::perms::owner_read | fs::perms::owner_write,
fs::perm_options::replace
);
}
// Check file permissions before reading sensitive data
bool is_secure_permissions(const fs::path& path) {
auto perms = fs::status(path).permissions();
// Check no group or other access
bool group_access = (perms & fs::perms::group_all) != fs::perms::none;
bool other_access = (perms & fs::perms::others_all) != fs::perms::none;
return !group_access && !other_access;
}
Verification Steps
- • All file paths validated (no ".." traversal)
- • File permissions set appropriately
- • Temporary files created securely
- • File handles properly closed (RAII)
- • Symlink attacks considered
5. Secrets Management
cpp
// NEVER: Hardcoded secrets
const char* api_key = "sk-xxxxxxxxxxxxx"; // DANGEROUS!
std::string password = "admin123"; // DANGEROUS!
// ALWAYS: Environment variables
const char* api_key = std::getenv("API_KEY");
if (!api_key) {
throw std::runtime_error("API_KEY not set");
}
// Secure secret handling class
class SecureString {
public:
explicit SecureString(std::string value)
: m_value(std::move(value)) {}
~SecureString() {
// Overwrite memory before destruction
std::fill(m_value.begin(), m_value.end(), '\0');
}
// No copying
SecureString(const SecureString&) = delete;
SecureString& operator=(const SecureString&) = delete;
// Allow moving
SecureString(SecureString&& other) noexcept
: m_value(std::move(other.m_value)) {}
const std::string& get() const { return m_value; }
private:
std::string m_value;
};
Verification Steps
- • No hardcoded secrets in source code
- • Secrets loaded from environment or secure storage
- • Secrets cleared from memory when no longer needed
- • No secrets in logs or error messages
- • .gitignore includes sensitive files
6. Thread Safety
Race Condition Prevention
cpp
#include <mutex>
#include <shared_mutex>
#include <atomic>
class ThreadSafeCounter {
public:
void increment() {
std::lock_guard<std::mutex> lock(m_mutex);
++m_count;
}
int get() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_count;
}
private:
mutable std::mutex m_mutex;
int m_count = 0;
};
// For read-heavy workloads
class ThreadSafeCache {
public:
std::string get(const std::string& key) const {
std::shared_lock<std::shared_mutex> lock(m_mutex);
auto it = m_cache.find(key);
return it != m_cache.end() ? it->second : "";
}
void set(const std::string& key, const std::string& value) {
std::unique_lock<std::shared_mutex> lock(m_mutex);
m_cache[key] = value;
}
private:
mutable std::shared_mutex m_mutex;
std::unordered_map<std::string, std::string> m_cache;
};
// For simple counters, use atomics
class AtomicCounter {
public:
void increment() { ++m_count; }
int get() const { return m_count.load(); }
private:
std::atomic<int> m_count{0};
};
Deadlock Prevention
cpp
// DANGEROUS: Inconsistent lock ordering
void transfer_dangerous(Account& from, Account& to, int amount) {
std::lock_guard<std::mutex> lock1(from.mutex);
std::lock_guard<std::mutex> lock2(to.mutex); // May deadlock!
// ...
}
// SAFE: Use std::lock for multiple mutexes
void transfer_safe(Account& from, Account& to, int amount) {
std::scoped_lock lock(from.mutex, to.mutex); // C++17
// Or pre-C++17:
// std::lock(from.mutex, to.mutex);
// std::lock_guard<std::mutex> lock1(from.mutex, std::adopt_lock);
// std::lock_guard<std::mutex> lock2(to.mutex, std::adopt_lock);
from.balance -= amount;
to.balance += amount;
}
Verification Steps
- • All shared data protected by mutex
- • Consistent lock ordering to prevent deadlocks
- • RAII lock guards used (no manual lock/unlock)
- • Atomic operations for simple counters
- • No data races (verified with thread sanitizer)
7. Command Injection Prevention
cpp
// NEVER: Build command strings from user input
std::string cmd = "ls " + user_input;
system(cmd.c_str()); // DANGEROUS!
// SAFER: Use execve-style functions with argument arrays
#include <unistd.h>
#include <sys/wait.h>
int execute_safely(const std::vector<std::string>& args) {
pid_t pid = fork();
if (pid == 0) {
// Child process
std::vector<char*> c_args;
for (const auto& arg : args) {
c_args.push_back(const_cast<char*>(arg.c_str()));
}
c_args.push_back(nullptr);
execvp(c_args[0], c_args.data());
_exit(127); // exec failed
}
// Parent process
int status;
waitpid(pid, &status, 0);
return WEXITSTATUS(status);
}
// Usage
execute_safely({"ls", "-la", sanitized_path});
8. Network Security
TLS/SSL for Network Communication
cpp
// Use libraries like OpenSSL, Boost.Asio with SSL
#include <boost/asio/ssl.hpp>
class SecureClient {
public:
SecureClient(boost::asio::io_context& io_ctx)
: m_ssl_ctx(boost::asio::ssl::context::tlsv12_client)
, m_socket(io_ctx, m_ssl_ctx) {
// Verify peer certificate
m_ssl_ctx.set_verify_mode(
boost::asio::ssl::verify_peer |
boost::asio::ssl::verify_fail_if_no_peer_cert
);
// Load CA certificates
m_ssl_ctx.set_default_verify_paths();
}
void connect(const std::string& host, const std::string& port) {
// Set SNI hostname
SSL_set_tlsext_host_name(m_socket.native_handle(), host.c_str());
// Resolve and connect
boost::asio::ip::tcp::resolver resolver(m_socket.get_executor());
auto endpoints = resolver.resolve(host, port);
boost::asio::connect(m_socket.lowest_layer(), endpoints);
// Perform SSL handshake
m_socket.handshake(boost::asio::ssl::stream_base::client);
}
private:
boost::asio::ssl::context m_ssl_ctx;
boost::asio::ssl::stream<boost::asio::ip::tcp::socket> m_socket;
};
Input Sanitization for Network Data
cpp
// Validate and sanitize network input
struct NetworkMessage {
uint32_t type;
uint32_t length;
std::vector<uint8_t> data;
};
std::optional<NetworkMessage> parse_message(const std::vector<uint8_t>& buffer) {
constexpr size_t HEADER_SIZE = 8;
constexpr size_t MAX_MESSAGE_SIZE = 1024 * 1024; // 1MB limit
if (buffer.size() < HEADER_SIZE) {
return std::nullopt; // Incomplete header
}
NetworkMessage msg;
std::memcpy(&msg.type, buffer.data(), 4);
std::memcpy(&msg.length, buffer.data() + 4, 4);
// Validate length
if (msg.length > MAX_MESSAGE_SIZE) {
return std::nullopt; // Message too large
}
if (buffer.size() < HEADER_SIZE + msg.length) {
return std::nullopt; // Incomplete message
}
msg.data.assign(
buffer.begin() + HEADER_SIZE,
buffer.begin() + HEADER_SIZE + msg.length
);
return msg;
}
9. Cryptography Best Practices
cpp
#include <openssl/evp.h>
#include <openssl/rand.h>
// Generate cryptographically secure random bytes
std::vector<uint8_t> generate_random_bytes(size_t length) {
std::vector<uint8_t> buffer(length);
if (RAND_bytes(buffer.data(), static_cast<int>(length)) != 1) {
throw std::runtime_error("Failed to generate random bytes");
}
return buffer;
}
// Secure password hashing (use a proper library like libsodium)
#include <sodium.h>
std::string hash_password(const std::string& password) {
char hashed[crypto_pwhash_STRBYTES];
if (crypto_pwhash_str(
hashed,
password.c_str(),
password.length(),
crypto_pwhash_OPSLIMIT_INTERACTIVE,
crypto_pwhash_MEMLIMIT_INTERACTIVE) != 0) {
throw std::runtime_error("Password hashing failed");
}
return std::string(hashed);
}
bool verify_password(const std::string& password, const std::string& hash) {
return crypto_pwhash_str_verify(
hash.c_str(),
password.c_str(),
password.length()) == 0;
}
10. Static Analysis & Sanitizers
Compiler Warnings
cmake
# Enable all warnings
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(${PROJECT_NAME} PRIVATE
-Wall
-Wextra
-Wpedantic
-Werror
-Wconversion
-Wshadow
-Wformat=2
-Wformat-security
)
endif()
if(MSVC)
target_compile_options(${PROJECT_NAME} PRIVATE
/W4
/WX
)
endif()
Address Sanitizer (ASan)
cmake
option(ENABLE_ASAN "Enable Address Sanitizer" OFF)
if(ENABLE_ASAN)
target_compile_options(${PROJECT_NAME} PRIVATE -fsanitize=address -fno-omit-frame-pointer)
target_link_options(${PROJECT_NAME} PRIVATE -fsanitize=address)
endif()
Thread Sanitizer (TSan)
cmake
option(ENABLE_TSAN "Enable Thread Sanitizer" OFF)
if(ENABLE_TSAN)
target_compile_options(${PROJECT_NAME} PRIVATE -fsanitize=thread)
target_link_options(${PROJECT_NAME} PRIVATE -fsanitize=thread)
endif()
Undefined Behavior Sanitizer (UBSan)
cmake
option(ENABLE_UBSAN "Enable Undefined Behavior Sanitizer" OFF)
if(ENABLE_UBSAN)
target_compile_options(${PROJECT_NAME} PRIVATE -fsanitize=undefined)
target_link_options(${PROJECT_NAME} PRIVATE -fsanitize=undefined)
endif()
Pre-Deployment Security Checklist
Before ANY production deployment:
- • Memory Safety: No raw new/delete, smart pointers used
- • Buffer Safety: No unbounded string operations
- • Input Validation: All inputs validated and sanitized
- • Integer Safety: Overflow checks on arithmetic
- • Format Strings: No user input as format strings
- • File Security: Path traversal prevented, permissions set
- • Secrets: No hardcoded secrets, secure handling
- • Thread Safety: Proper synchronization, no data races
- • Network Security: TLS used, input validated
- • Command Injection: No shell command construction
- • Cryptography: Using vetted libraries correctly
- • Compiler Warnings: All warnings enabled and fixed
- • Sanitizers: Tested with ASan, TSan, UBSan
- • Static Analysis: Passed clang-tidy, cppcheck
- • Dependencies: Updated, no known vulnerabilities
Security Testing
cpp
// Test for buffer overflow handling
TEST(SecurityTest, RejectsOversizedInput) {
std::string oversized(10000, 'A');
EXPECT_FALSE(validate_input(oversized, 1000));
}
// Test for path traversal
TEST(SecurityTest, RejectsPathTraversal) {
EXPECT_FALSE(is_safe_path("/data", "../etc/passwd"));
EXPECT_FALSE(is_safe_path("/data", "../../root/.ssh/id_rsa"));
}
// Test for integer overflow
TEST(SecurityTest, DetectsIntegerOverflow) {
EXPECT_THROW(safe_add(INT_MAX, 1), std::overflow_error);
EXPECT_THROW(safe_multiply(INT_MAX, 2), std::overflow_error);
}
Resources
Remember: Security is not optional. One vulnerability can compromise the entire system. When in doubt, err on the side of caution.