This episode discusses C code security: buffer overflow and format string vulnerabilities along with their mitigations, secure coding with bounds checking using strncpy and snprintf, the differences between stack and heap overflow, as well as input validation and defensive programming.

C gives you full control over memory, and full control means full responsibility. Episode 13 discusses the most dangerous class of bugs in C programming: buffer overflow and format string vulnerabilities, which for years have caused the most famous exploits on the internet.
The good news: most of these bugs can be prevented with disciplined patterns — using functions that accept a buffer size, checking bounds before access, and treating all input as untrusted data.
This episode is a turning point: after it, you will no longer write vulnerable string functions, and you'll build defensive programming habits that protect the users of your programs.
Buffer overflow happens when data is written beyond a buffer's capacity, trampling the memory around it. A classic example:
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]) {
char buffer[16];
strcpy(buffer, argv[1]);
printf("%s\n", buffer);
return 0;
}
EOFstrcpy(buffer, argv[1]) copies without checking length. If argv[1] is longer than 15 characters, the data overwrites memory around buffer, potentially changing function return addresses. An attacker who controls the overflow contents can execute arbitrary code.
Modern systems layer defenses. The compiler adds a stack protector that detects overwrites; the kernel marks stack and heap pages as non-executable. Enable the defenses at compile time:
gcc -fstack-protector-strong -D_FORTIFY_SOURCE=2 program.c -o programThe flag -fstack-protector-strong inserts canary checks into functions that use buffers. These defenses slow down exploits, but you still must write non-vulnerable code from the start — mitigation is no substitute for safe code.
When a user string is used directly as a printf format string, an attacker can read or write memory. The following code is vulnerable:
printf(buffer);
printf("%s", buffer);The call printf(buffer) treats the buffer's contents as a format string, so tokens like %x and %n are executed. The safe version printf("%s", buffer) always treats the buffer as data. The golden rule: format strings must always be literals, never derived from input.
Format string attacks can leak stack contents and write to arbitrary memory through %n. Tools such as -Wformat-security and clang-tidy warn when a format string isn't a literal. Compile with -Wformat=2 -Wformat-security from the start to catch this pattern earlier.
Legacy string functions copy without bounds. Their replacements accept the buffer size as a parameter:
#include <stdio.h>
#include <string.h>
int main(void) {
char nama[16];
strncpy(nama, "Arman Dwi Pangestu", sizeof(nama) - 1);
nama[sizeof(nama) - 1] = '\0';
char pesan[64];
snprintf(pesan, sizeof(pesan), "Halo %s", nama);
puts(pesan);
return 0;
}
EOFstrncpy(nama, "...", sizeof(nama) - 1) limits the number of copied characters, and the next line guarantees a null terminator at the end. snprintf writes at most sizeof(pesan) bytes and always ends with a null. Both functions are the gold standard for safely copying and formatting strings.
The gets function has no size parameter and must never be used. To read a line, always use fgets with the buffer size. When reading numbers with scanf, limit the field width, e.g. %19s for a 20-char string.
All external input — arguments, files, network — must be considered untrusted. The first step is to validate before processing:
if (argc != 2) {
fprintf(stderr, "gunakan: program <nilai>\n");
return 1;
}
long nilai = strtol(argv[1], NULL, 10);
if (nilai < 0 || nilai > 1000) {
fprintf(stderr, "nilai di luar jangkauan\n");
return 1;
}strtol(argv[1], NULL, 10) converts a string to a number with error checks detectable through errno and the end-position pointer. Range validation happens before the value is used. This pattern keeps strange input from reaching internal logic.
Three principles to always apply: check every return value, check the bounds of every array access, and assume other functions can fail. Assert strengthens internal assumptions, the sanitizers in episode 11 validate correctness during development, and testing with extreme inputs becomes routine.
Warning
Validation isn't only about security. Unexpected input also crashes a program in an inelegant way. Rejecting input from the start, with a clear message, is far better than failing in the middle of processing.
Key takeaways:
In the next episode 14 we will discuss cryptography and data protection — the basics of cryptography in C, the OpenSSL and libsodium libraries for encryption, hashing, HMAC, and random number generation, up to secure key handling and the management of sensitive data.