Secure Coding Practices
C’s power comes with responsibility. Buffer overflows, format string attacks, and integer overflows have caused billions of dollars in damage. Learn to write C code that’s hardened against attack.The Security Mindset
Never trust user input. Every byte from outside your program—files, network, environment variables, command-line arguments—is potentially malicious. Validate everything.
Common C Vulnerabilities (CWE Top 25)
| Vulnerability | Impact | Prevention |
|---|---|---|
| Buffer Overflow (CWE-120) | Code execution | Bounds checking, safe functions |
| Integer Overflow (CWE-190) | Memory corruption | Overflow checks, safe math |
| Format String (CWE-134) | Information leak, code exec | Never use user data as format |
| Use After Free (CWE-416) | Code execution | Null after free, ownership |
| Null Dereference (CWE-476) | Denial of service | Null checks, defensive code |
Buffer Overflow Prevention
The Classic Vulnerability
Copy
#include <stdio.h>
#include <string.h>
// VULNERABLE: Buffer overflow
void vulnerable_login(const char *username) {
char buffer[16];
strcpy(buffer, username); // No bounds check!
printf("Welcome, %s\n", buffer);
}
// What happens with 32-character username?
// Stack layout:
// [buffer 16 bytes][saved RBP 8 bytes][return address 8 bytes]
// Attacker overwrites return address → arbitrary code execution
Safe String Handling
Copy
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
// SAFE: Using strncpy with explicit null termination
void safe_copy_strncpy(char *dest, size_t dest_size, const char *src) {
if (dest_size == 0) return;
strncpy(dest, src, dest_size - 1);
dest[dest_size - 1] = '\0'; // ALWAYS null-terminate!
}
// SAFER: Using snprintf (recommended)
void safe_copy_snprintf(char *dest, size_t dest_size, const char *src) {
if (dest_size == 0) return;
int written = snprintf(dest, dest_size, "%s", src);
// Check for truncation
if (written >= (int)dest_size) {
// Handle truncation - log warning or error
fprintf(stderr, "Warning: string truncated\n");
}
}
// SAFEST: Using strlcpy (BSD/macOS, or implement yourself)
#ifndef HAVE_STRLCPY
size_t strlcpy(char *dst, const char *src, size_t size) {
size_t src_len = strlen(src);
if (size > 0) {
size_t copy_len = (src_len >= size) ? size - 1 : src_len;
memcpy(dst, src, copy_len);
dst[copy_len] = '\0';
}
return src_len; // Returns what WOULD have been copied (for truncation detection)
}
#endif
// Usage example
void process_username(const char *username) {
char buffer[32];
size_t needed = strlcpy(buffer, username, sizeof(buffer));
if (needed >= sizeof(buffer)) {
fprintf(stderr, "Username too long (max %zu chars)\n", sizeof(buffer) - 1);
return;
}
printf("Welcome, %s\n", buffer);
}
Safe String Concatenation
Copy
#include <stdio.h>
#include <string.h>
// VULNERABLE
void vulnerable_concat(void) {
char buffer[64] = "Hello, ";
char *username = get_user_input(); // Could be 100+ chars
strcat(buffer, username); // Overflow!
}
// SAFE: Using strlcat or manual calculation
size_t strlcat(char *dst, const char *src, size_t size) {
size_t dst_len = strnlen(dst, size);
size_t src_len = strlen(src);
if (dst_len == size) {
return size + src_len; // No room, return what would be needed
}
size_t remaining = size - dst_len - 1;
size_t copy_len = (src_len > remaining) ? remaining : src_len;
memcpy(dst + dst_len, src, copy_len);
dst[dst_len + copy_len] = '\0';
return dst_len + src_len;
}
// SAFEST: Calculate sizes first
int safe_concat(char *dst, size_t dst_size,
const char *s1, const char *s2) {
size_t len1 = strlen(s1);
size_t len2 = strlen(s2);
// Check for overflow in addition
if (len1 > SIZE_MAX - len2 || len1 + len2 >= dst_size) {
return -1; // Would overflow
}
memcpy(dst, s1, len1);
memcpy(dst + len1, s2, len2);
dst[len1 + len2] = '\0';
return 0;
}
Array Bounds Checking
Copy
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
// VULNERABLE
int vulnerable_array_access(int *arr, int index) {
return arr[index]; // No bounds check!
}
// SAFE: Bounds-checked array wrapper
typedef struct {
int *data;
size_t size;
} SafeArray;
SafeArray *array_create(size_t size) {
SafeArray *arr = malloc(sizeof(SafeArray));
if (!arr) return NULL;
arr->data = calloc(size, sizeof(int));
if (!arr->data) {
free(arr);
return NULL;
}
arr->size = size;
return arr;
}
bool array_set(SafeArray *arr, size_t index, int value) {
if (!arr || index >= arr->size) {
return false; // Out of bounds
}
arr->data[index] = value;
return true;
}
bool array_get(SafeArray *arr, size_t index, int *value) {
if (!arr || !value || index >= arr->size) {
return false; // Out of bounds or null
}
*value = arr->data[index];
return true;
}
// Or use assert for debug builds
#include <assert.h>
int checked_array_get(int *arr, size_t size, size_t index) {
assert(index < size && "Array index out of bounds");
return arr[index];
}
Integer Overflow Prevention
The Hidden Danger
Copy
#include <stdio.h>
#include <stdint.h>
#include <limits.h>
// VULNERABLE: Integer overflow in allocation size
void *vulnerable_alloc(size_t count, size_t size) {
return malloc(count * size); // Can overflow to small value!
}
// Example: count=0x100000001, size=8
// Multiplication: 0x100000001 * 8 = 0x800000008
// On 32-bit: wraps to 8 bytes
// Attacker gets tiny buffer but can write count*size bytes
// SAFE: Check before multiply
void *safe_alloc(size_t count, size_t size) {
if (count > 0 && size > SIZE_MAX / count) {
return NULL; // Would overflow
}
return malloc(count * size);
}
// Even better: use calloc (does the check internally)
void *safer_alloc(size_t count, size_t size) {
return calloc(count, size); // Checks for overflow internally
}
Safe Integer Arithmetic
Copy
#include <stdbool.h>
#include <stdint.h>
#include <limits.h>
// Safe addition (signed)
bool safe_add_int(int a, int b, int *result) {
if ((b > 0 && a > INT_MAX - b) ||
(b < 0 && a < INT_MIN - b)) {
return false; // Would overflow
}
*result = a + b;
return true;
}
// Safe multiplication (signed)
bool safe_mult_int(int a, int b, int *result) {
if (a > 0) {
if (b > 0) {
if (a > INT_MAX / b) return false;
} else {
if (b < INT_MIN / a) return false;
}
} else {
if (b > 0) {
if (a < INT_MIN / b) return false;
} else {
if (a != 0 && b < INT_MAX / a) return false;
}
}
*result = a * b;
return true;
}
// Using GCC/Clang builtins (preferred)
bool safe_add_builtin(int a, int b, int *result) {
return !__builtin_add_overflow(a, b, result);
}
bool safe_mult_builtin(int a, int b, int *result) {
return !__builtin_mul_overflow(a, b, result);
}
bool safe_sub_builtin(int a, int b, int *result) {
return !__builtin_sub_overflow(a, b, result);
}
// Safe size calculation
bool safe_array_size(size_t count, size_t elem_size, size_t *total) {
return !__builtin_mul_overflow(count, elem_size, total);
}
Signed vs Unsigned Comparisons
Copy
#include <stdio.h>
#include <stdint.h>
// VULNERABLE: Mixed signed/unsigned comparison
void vulnerable_check(int user_index, size_t array_size) {
// If user_index is negative, it converts to huge unsigned value!
if (user_index < array_size) {
// Attacker passes -1, it becomes 0xFFFFFFFF > array_size
// Check passes, but negative index wraps to huge offset
}
}
// SAFE: Check for negative first
void safe_check(int user_index, size_t array_size) {
if (user_index < 0 || (size_t)user_index >= array_size) {
return; // Invalid index
}
// Now safe to use
}
// SAFER: Use size_t for all sizes and indices
void safest_check(size_t user_index, size_t array_size) {
if (user_index >= array_size) {
return; // Invalid
}
// Safe
}
// Compile with -Wsign-compare to catch these issues!
Format String Attacks
The Vulnerability
Copy
#include <stdio.h>
// VULNERABLE: User input as format string
void vulnerable_log(const char *user_input) {
printf(user_input); // NEVER do this!
}
// Attack 1: Information disclosure
// Input: "%x %x %x %x" → prints stack values
// Attack 2: Stack smashing
// Input: "%s%s%s%s%s" → crashes trying to dereference invalid pointers
// Attack 3: Arbitrary write (using %n)
// Input: "AAAA%08x.%08x.%08x.%n" → writes to address 0x41414141
// SAFE: Always use format specifier
void safe_log(const char *user_input) {
printf("%s", user_input); // Treats input as data, not format
}
// SAFE: Or use puts/fputs for simple strings
void safest_log(const char *user_input) {
puts(user_input); // No format interpretation
}
Secure Logging Pattern
Copy
#include <stdio.h>
#include <stdarg.h>
#include <time.h>
// Secure variadic logging function
void secure_log(const char *level, const char *fmt, ...) {
// Get timestamp
time_t now = time(NULL);
struct tm *tm_info = localtime(&now);
char timestamp[26];
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm_info);
// Print header (controlled format)
fprintf(stderr, "[%s] [%s] ", timestamp, level);
// Print user message (format is trusted, args are data)
va_list args;
va_start(args, fmt);
vfprintf(stderr, fmt, args); // fmt MUST be a literal, not user input!
va_end(args);
fprintf(stderr, "\n");
}
// Usage (format string is literal, never from user)
void process_request(const char *username) {
secure_log("INFO", "Processing request for user: %s", username);
}
// GCC attribute to check format strings
__attribute__((format(printf, 2, 3)))
void checked_log(const char *level, const char *fmt, ...);
Memory Safety
Use After Free Prevention
Copy
#include <stdlib.h>
#include <string.h>
// Pattern 1: NULL after free
void safe_free(void **ptr) {
if (ptr && *ptr) {
free(*ptr);
*ptr = NULL;
}
}
#define SAFE_FREE(ptr) safe_free((void**)&(ptr))
// Usage
void example(void) {
char *buffer = malloc(100);
// ... use buffer ...
SAFE_FREE(buffer); // buffer is now NULL
// Double-free is now safe (free(NULL) is no-op)
SAFE_FREE(buffer);
}
// Pattern 2: Ownership tracking
typedef struct {
void *data;
int owner_id;
} OwnedPtr;
OwnedPtr owned_alloc(size_t size, int owner) {
return (OwnedPtr){ .data = malloc(size), .owner_id = owner };
}
void owned_free(OwnedPtr *ptr, int owner) {
if (ptr->owner_id != owner) {
fprintf(stderr, "ERROR: Non-owner trying to free!\n");
abort();
}
free(ptr->data);
ptr->data = NULL;
ptr->owner_id = -1;
}
Double-Free Detection
Copy
#include <stdlib.h>
#include <stdio.h>
#ifdef DEBUG
// Debug allocator that detects double-free
typedef struct AllocRecord {
void *ptr;
size_t size;
const char *file;
int line;
struct AllocRecord *next;
} AllocRecord;
static AllocRecord *alloc_list = NULL;
void *debug_malloc(size_t size, const char *file, int line) {
void *ptr = malloc(size);
if (!ptr) return NULL;
AllocRecord *record = malloc(sizeof(AllocRecord));
record->ptr = ptr;
record->size = size;
record->file = file;
record->line = line;
record->next = alloc_list;
alloc_list = record;
return ptr;
}
void debug_free(void *ptr, const char *file, int line) {
if (!ptr) return;
AllocRecord **pp = &alloc_list;
while (*pp) {
if ((*pp)->ptr == ptr) {
AllocRecord *record = *pp;
*pp = record->next;
free(record);
free(ptr);
return;
}
pp = &(*pp)->next;
}
fprintf(stderr, "DOUBLE FREE detected at %s:%d for ptr %p\n",
file, line, ptr);
abort();
}
#define malloc(size) debug_malloc(size, __FILE__, __LINE__)
#define free(ptr) debug_free(ptr, __FILE__, __LINE__)
#endif
Input Validation
General Principles
Copy
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdbool.h>
#include <limits.h>
#include <errno.h>
// Validate string length
bool validate_length(const char *str, size_t min_len, size_t max_len) {
if (!str) return false;
size_t len = strlen(str);
return len >= min_len && len <= max_len;
}
// Validate string contains only allowed characters
bool validate_charset(const char *str, const char *allowed) {
if (!str) return false;
while (*str) {
if (!strchr(allowed, *str)) {
return false;
}
str++;
}
return true;
}
// Validate alphanumeric
bool validate_alnum(const char *str) {
if (!str || !*str) return false;
while (*str) {
if (!isalnum((unsigned char)*str)) {
return false;
}
str++;
}
return true;
}
// Validate integer in range
bool validate_int(const char *str, int min_val, int max_val, int *result) {
if (!str || !*str) return false;
char *endptr;
errno = 0;
long val = strtol(str, &endptr, 10);
// Check for conversion errors
if (errno == ERANGE || val < INT_MIN || val > INT_MAX) {
return false; // Overflow
}
if (endptr == str || *endptr != '\0') {
return false; // No digits or trailing garbage
}
if (val < min_val || val > max_val) {
return false; // Out of range
}
*result = (int)val;
return true;
}
// Validate email (simplified)
bool validate_email(const char *email) {
if (!email) return false;
const char *at = strchr(email, '@');
if (!at || at == email) return false; // No @ or starts with @
const char *dot = strrchr(at, '.');
if (!dot || dot == at + 1) return false; // No dot after @ or @.
if (strlen(dot) < 3) return false; // Need at least .xx
return true;
}
// Validate path (prevent directory traversal)
bool validate_path(const char *path) {
if (!path) return false;
// Block directory traversal
if (strstr(path, "..") != NULL) {
return false;
}
// Block absolute paths
if (path[0] == '/' || path[0] == '\\') {
return false;
}
// Block Windows drive letters
if (isalpha((unsigned char)path[0]) && path[1] == ':') {
return false;
}
return true;
}
Command Injection Prevention
Copy
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <ctype.h>
// VULNERABLE: Command injection
void vulnerable_ping(const char *host) {
char cmd[256];
sprintf(cmd, "ping -c 1 %s", host);
system(cmd); // If host = "8.8.8.8; rm -rf /", disaster!
}
// SAFE: Validate input strictly
bool is_valid_hostname(const char *host) {
if (!host || strlen(host) > 253) return false;
// Only allow alphanumeric, dots, and hyphens
for (const char *p = host; *p; p++) {
if (!isalnum((unsigned char)*p) && *p != '.' && *p != '-') {
return false;
}
}
return true;
}
// SAFER: Use execve instead of system
#include <unistd.h>
#include <sys/wait.h>
int safe_ping(const char *host) {
if (!is_valid_hostname(host)) {
return -1;
}
pid_t pid = fork();
if (pid == 0) {
// Child: exec ping directly (no shell!)
char *argv[] = {"ping", "-c", "1", (char*)host, NULL};
execve("/bin/ping", argv, NULL);
_exit(127); // execve failed
} else if (pid > 0) {
// Parent: wait for child
int status;
waitpid(pid, &status, 0);
return WEXITSTATUS(status);
}
return -1;
}
Compiler Security Features
Recommended Compiler Flags
Copy
# Security-hardened compilation
gcc -Wall -Wextra -Werror \
-Wformat=2 -Wformat-security \
-Wstack-protector \
-fstack-protector-strong \
-D_FORTIFY_SOURCE=2 \
-O2 \
-fPIE -pie \
-Wl,-z,now \
-Wl,-z,relro \
program.c -o program
# What each flag does:
# -fstack-protector-strong: Stack canary protection
# -D_FORTIFY_SOURCE=2: Runtime buffer overflow checks
# -fPIE -pie: Position Independent Executable (ASLR)
# -Wl,-z,now: Immediate binding (prevents GOT overwrite)
# -Wl,-z,relro: Make GOT read-only
Static Analysis
Copy
# Clang Static Analyzer
scan-build gcc -c program.c
# Cppcheck
cppcheck --enable=all program.c
# PVS-Studio (commercial)
pvs-studio-analyzer analyze -o report.log
# Flawfinder (security focused)
flawfinder program.c
# Infer (Facebook)
infer run -- gcc -c program.c
Runtime Protection
Copy
# AddressSanitizer - buffer overflows, use-after-free
gcc -fsanitize=address -g program.c -o program
# UndefinedBehaviorSanitizer - integer overflow, null deref
gcc -fsanitize=undefined -g program.c -o program
# LeakSanitizer - memory leaks (included with ASan)
gcc -fsanitize=leak -g program.c -o program
# All together (development builds)
gcc -fsanitize=address,undefined -fno-omit-frame-pointer -g \
program.c -o program
Security Checklist
1
Input Validation
- All external input is validated
- Length limits enforced
- Character sets whitelisted
- Path traversal blocked
2
Memory Safety
- Using safe string functions (snprintf, strlcpy)
- Array bounds checked
- Integer overflow checked before allocation
- NULL checks on all pointers
3
Resource Management
- All allocations have matching frees
- Pointers NULLed after free
- File descriptors closed
- No resource leaks on error paths
4
Compilation
- All warnings enabled and treated as errors
- Stack protection enabled
- FORTIFY_SOURCE enabled
- PIE/ASLR enabled
5
Testing
- AddressSanitizer clean
- UBSan clean
- Static analysis clean
- Fuzz testing performed