CWE-170: Improper Null Termination - C++
Overview
std::string manages its own length and always null-terminates internally, so most of CWE-170 disappears once code is fully converted to it - .c_str() is guaranteed to return a null-terminated buffer regardless of the string's content. The weakness reappears at the boundaries where C++ still touches raw buffers: a std::vector<char> or raw char* used to receive data from a C API such as recv() has no terminator guarantee at all, and older code that predates full std::string adoption may still build C-style buffers by hand.
Common Vulnerable Patterns
Receiving raw bytes into a buffer without terminating it
void read_network_cpp(int socket) {
std::vector<char> buffer(256);
ssize_t bytes_read = recv(socket, buffer.data(), buffer.size(), 0);
// VULNERABLE - recv() fills raw bytes; buffer.data() is not null-terminated
printf("Received: %s\n", buffer.data());
// if 256 bytes were received, this reads past the vector's storage
}
Why this is vulnerable: std::vector<char>::data() returns a pointer to the underlying storage, with no null-termination guarantee - recv() writing exactly buffer.size() bytes leaves nothing reserved for a terminator, so passing buffer.data() to a C string function reads past the vector's actual storage.
Copying a std::string into a fixed-size C buffer
void build_legacy_header(const std::string &name) {
char header[64];
// VULNERABLE - strncpy's non-termination guarantee applies here too,
// even though the source happens to be a std::string
strncpy(header, name.c_str(), sizeof(header));
legacy_c_api(header);
}
Why this is vulnerable: std::string::c_str() itself is always safely terminated, but copying it into a fixed C buffer with strncpy reintroduces the same non-termination behavior as plain C - if name is sizeof(header) bytes or longer, header ends up unterminated exactly as it would with a char* source.
Secure Patterns
Use std::string end-to-end and only touch C APIs at the boundary
#include <string>
void process_input(const std::string &input) {
std::string buffer = input.substr(0, 9); // safe truncation, always terminated
buffer += " suffix"; // still safely terminated
// Convert to a C string only at the point of the actual C API call
legacy_c_api(buffer.c_str());
}
Why this works: Every std::string operation (substr, +=, append) maintains the class's own internal termination guarantee - there is no operation that can produce an unterminated std::string. Calling .c_str() only at the point where a C API actually needs a raw pointer keeps the unsafe boundary as small and as late as possible, rather than manually managing a C buffer through the whole function.
Terminate raw buffers explicitly at the actual C API boundary
#include <vector>
#include <string>
std::string read_network_cpp(int socket) {
std::vector<char> buffer(256);
ssize_t bytes_read = recv(socket, buffer.data(), buffer.size(), 0);
if (bytes_read <= 0) {
return {};
}
// Construct a std::string with an explicit length - no reliance on a terminator
// ever being present in the raw buffer at all
return std::string(buffer.data(), static_cast<size_t>(bytes_read));
}
Why this works: Constructing std::string from a pointer and an explicit length (rather than from a presumed-terminated char*) never depends on the source buffer containing a terminator - the resulting std::string is correctly sized and safely terminated regardless of what recv() wrote, which sidesteps the raw buffer's null-termination question instead of trying to satisfy it.
Convert C strings to std::string immediately at the interop boundary
void handle_c_string(const char *c_str) {
if (c_str == nullptr) return;
// Convert once, immediately - every operation after this line is memory-safe
std::string safe_str(c_str);
safe_str.append(" more data");
}
Why this works: Converting a raw const char* to std::string at the moment it crosses into C++ code means only that single conversion needs to trust the C side's termination contract - every subsequent operation works against the safe std::string type instead of the original raw pointer.
Filling a Fixed-Size C Buffer for a Legacy API
#include <cstring>
#include <string>
// SECURE - when a C API insists on a fixed-size char[], apply the same
// explicit-termination discipline as plain C rather than trusting strncpy.
char dest[64];
const std::string source = get_value();
if (source.size() >= sizeof(dest)) {
return error::value_too_long; // decide, do not silently truncate
}
std::memcpy(dest, source.data(), source.size());
dest[source.size()] = '\0'; // terminate explicitly, always
Why this works: strncpy writes no terminator whenever the source is at
least as long as its size argument - filling the buffer exactly is only the
first of those cases - which is the defect this CWE describes. Copying an explicitly
checked length and writing the terminator yourself removes the ambiguity, and
rejecting an oversized value is a decision rather than a silent truncation that
corrupts the data. The same reasoning, and the surrounding C idioms, are covered
on the C page.
Testing
- Test buffers filled by
recv()/fread()with input that completely fills the raw buffer's capacity, then confirm thestd::stringconstructed from it is correctly sized rather than reading past the buffer. - Build with AddressSanitizer and exercise every raw-buffer-to-C-API path with boundary-length input.
- Grep for
strncpy/sprintf/strcatcalls in otherwise-modern C++ code as a signal that a legacy C buffer pattern - and its termination requirements - is still present.
Common Pitfalls
- Assuming a modified buffer's null-termination is preserved automatically: since C++11,
data()andc_str()both return a null-terminated view of the string's current content, but code that writes directly into that buffer through a non-const pointer, or code written against a pre-C++11 standard wheredata()carried no termination guarantee, can break that assumption - never rely on a terminator being present unless it was written throughstd::string's own API. - Treating a
std::vector<char>like astd::string:std::vectorhas no null-termination concept at all - calling a C string function onvector.data()is exactly as unsafe as calling it on a raw C buffer, however modern the surrounding code is. - Holding onto a pointer from
.c_str()past the string's next mutation: any non-const operation on thestd::string(including one that triggers reallocation) can invalidate a previously obtained.c_str()pointer - a terminator that was valid at the call site isn't guaranteed to still be valid later.