This episode covers C++ code security: memory safety and buffer overflow mitigation, safe use of C string APIs compared to std::string, input validation with boundary checks, as well as cybersecurity best practices for production applications.

C++ gives you full control over memory — and full control means small mistakes can become severe security holes. Most vulnerabilities in C/C++ applications are rooted in wrong memory management: buffer overflows, use-after-free, and unsafe format strings.
Episode 14 teaches secure coding: why buffer overflows happen and how to prevent them, why std::string is safer than char[] arrays, how to validate and sanitize input, mitigations provided by compilers, as well as cybersecurity best practices applied in the industry.
A buffer overflow happens when a program writes more data than the buffer can hold. The data spills into the surrounding memory, and an attacker can exploit it to overwrite a function's return address or execute their own code. A classic example:
cat > rentan.cpp <<'EOF'
#include <iostream>
#include <cstring>
int main() {
char nama[8];
std::cout << "Masukkan nama: ";
std::cin >> nama;
std::cout << "Halo " << nama << "\n";
}
EOF
g++ -std=c++20 rentan.cpp -o rentanstd::cin >> nama writes to the char nama[8] buffer without checking the input length. If the user types more than 7 characters, the data overflows and overwrites other memory — undefined behavior that can become a vulnerability. Code like this must never appear in production.
The main rule: never let data into a buffer without checking its size. Use types that know their own length. std::string, std::vector, and STL containers manage their bounds automatically, so overflows are impossible through the normal API.
The classic C functions strcpy, strcat, sprintf, and gets don't check the destination's bounds. gets was even removed from the C standard because it can't be used safely. Replace them all with std::string and std::string_view:
cat > aman.cpp <<'EOF'
#include <iostream>
#include <string>
int main() {
const char* raw = "teks panjang melebihi buffer";
char buffer[8];
// strcpy(buffer, raw); // overflow!
std::string aman = raw;
std::cout << "Panjang: " << aman.size() << "\n";
std::cout << aman << "\n";
}
EOF
g++ -std=c++20 aman.cpp -o aman
./amanstd::string aman = raw copies the string with a self-managed size — safe without thinking about buffer bounds. std::string_view provides a non-owning view of the same string, useful for function parameters without copying.
The classic printf carries the risk of a format string vulnerability: if the format comes from the user, an attacker can read and write memory through specifiers like %x and %n. Never feed user input directly to printf. In C++20, std::format is the safe replacement:
#include <format>
// std::print("Nama: {}\n", nama); // C++23, format amanstd::format("Nama: {} ...)" separates the format from the arguments and validates both at compile time, eliminating an entire class of format bugs.
All input that comes from the outside — users, networks, files — can't be trusted. Validate it first before using it: length, range, and format:
cat > validasi.cpp <<'EOF'
#include <iostream>
#include <string>
#include <cctype>
bool valid_umur(const std::string& s) {
if (s.empty() || s.size() > 3) return false;
for (char c : s) {
if (!std::isdigit(c)) return false;
}
return true;
}
int main() {
std::string input;
std::cin >> input;
if (valid_umur(input)) {
int umur = std::stoi(input);
std::cout << "Umur valid: " << umur << "\n";
} else {
std::cerr << "Input tidak valid\n";
}
}
EOF
g++ -std=c++20 validasi.cpp -o validasi
./validasivalid_umur(s) checks for empty input, maximum length, and that all characters are digits before converting. This pattern — validate before converting — prevents std::stoi from throwing or producing out-of-expectation values.
When accessing container elements with an index that might be invalid, use at(), which throws std::out_of_range, instead of operator[], which does no checking. For iteration, range-based for and iterators eliminate the need for manual indices entirely — and with them, the need for boundary checks.
Modern compilers provide mitigations you can enable with flags: PIE (-fPIE) for address space randomization, the stack protector (-fstack-protector-strong) to detect stack corruption, and Fortify (-D_FORTIFY_SOURCE=2) to check buffers on libc functions:
g++ -std=c++20 -fstack-protector-strong -D_FORTIFY_SOURCE=2 \
-Wl,-z,relro,-z,now app.cpp -o appThe flag -fstack-protector-strong installs a canary to detect stack overflows, and -Wl,-z,relro,-z,now makes the GOT segment read-only after linking. Combine all of these in release builds to make exploitation harder.
Use AddressSanitizer and UndefinedBehaviorSanitizer during development to catch bugs before release. -fsanitize=address,undefined detects overflows, use-after-free, and other undefined behavior. In CI, add clang-tidy static analysis with security checks. Episodes 11 and 19 will go deeper into both of these.
Warning
Compiler mitigations raise the cost of exploitation, not eliminate the bug. The primary fix remains safe code: std::string, boundary checks, and input validation.
A few security principles that apply universally to C++ code:
Sanitization means cleaning input of harmful characters or structures before use — for example, avoiding injection when building queries or logs. In C++, avoid building dynamic execution strings; prefer structured mechanisms.
Here's what to take away:
std::string, std::string_view, and STL containers instead of raw arrays.strcpy, strcat, sprintf, and gets.In the next episode, episode 15, we'll discuss performance and optimization — profiling with perf and Valgrind, memory layout optimization and cache friendliness, compiler optimization flags, inline and loop unrolling, as well as the tradeoff between readability and performance.