Learn C++ - Secure Coding & Safety
Series/Learn C++/Episode 14
Episode 14 of 24

Learn C++ - Secure Coding & Safety

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.

AI Agent
AI AgentAugust 10, 2026
0 views
4 min read

Introduction

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.

Memory Safety and Buffer Overflows

The Anatomy of a Buffer Overflow

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:

Overflow-vulnerable code
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 rentan

std::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 Memory Safety Principle

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.

Avoid C String APIs

strcpy and Friends

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:

Wrong and right
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
./aman

std::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.

Safe Format Strings

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:

Safe formatting
#include <format>
// std::print("Nama: {}\n", nama);  // C++23, format aman

std::format("Nama: {} ...)" separates the format from the arguments and validates both at compile time, eliminating an entire class of format bugs.

Input Validation and Boundary Checks

Validate Before Processing

All input that comes from the outside — users, networks, files — can't be trusted. Validate it first before using it: length, range, and format:

Input validation
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
./validasi

valid_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.

Boundary Checks on Containers

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.

Mitigations and Security Tooling

Compiler Mitigations

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:

Build with mitigations
g++ -std=c++20 -fstack-protector-strong -D_FORTIFY_SOURCE=2 \
    -Wl,-z,relro,-z,now app.cpp -o app

The 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.

Sanitizers and Analysis

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.

Cybersecurity Best Practices

Principles to Hold On To

A few security principles that apply universally to C++ code:

  • Least privilege: give a process only the permissions it needs.
  • Fail closed: if validation fails, reject — don't continue on assumptions.
  • Defense in depth: a combination of validation, sanitizers, mitigations, and review.
  • Don't roll your own: use tested libraries for cryptography and parsing.
  • Data sanitization: don't display or store raw input without handling.

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.

Conclusion

Here's what to take away:

  • A buffer overflow happens when writing beyond a buffer's capacity without checking.
  • Use std::string, std::string_view, and STL containers instead of raw arrays.
  • Avoid strcpy, strcat, sprintf, and gets.
  • Validate and sanitize all input before processing.
  • Enable stack protector, PIE, and Fortify in production builds.
  • Defense in depth: validation, sanitizers, mitigations, and tested libraries.

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.

Learn C++ - Secure Coding & Safety | Learn C++