Skip to main content

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)

VulnerabilityImpactPrevention
Buffer Overflow (CWE-120)Code executionBounds checking, safe functions
Integer Overflow (CWE-190)Memory corruptionOverflow checks, safe math
Format String (CWE-134)Information leak, code execNever use user data as format
Use After Free (CWE-416)Code executionNull after free, ownership
Null Dereference (CWE-476)Denial of serviceNull checks, defensive code

Buffer Overflow Prevention

The Classic Vulnerability

#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

#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

#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

#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

#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

#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

#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

#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

#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

#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

#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

#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

#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

# 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

# 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

# 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

Further Reading