Environment Variables, Input Validation, and Runtime Assumptions

 

Don’t Trust Inputs or the Environment
Chapter 38.8 โ€” Environment Variables, Input Validation, and Runtime Assumptions
๐ŸŒ PATH / IFS
โœ”๏ธ Input Validation
๐Ÿ”Œ FD Assumptions

The Paranoid Programmer’s Mindset

A secure privileged program treats everything from outside the program as potentially hostile. This includes command-line arguments, environment variables, user input, files read from disk, data from pipes, data from network connections, and even the initial state of the process (open file descriptors, resource limits, etc.).

The attacker controls everything you receive from the outside world. Your job is to validate, sanitize, and never make assumptions.

๐ŸŒ Danger 1: Malicious Environment Variables

When a user runs a set-UID program, they set the environment before execution. Environment variables can contain anything. Two variables are especially dangerous:

PATH

Determines where the shell and functions like execlp(), execvp(), and system() search for executables. An attacker sets PATH to point to a directory with a fake ls or sh binary. When your privileged program calls system("ls /tmp"), it runs the attacker’s binary as root.

IFS

IFS (Internal Field Separator) tells the shell which characters split words in a command line. In older shells, a malicious IFS value could cause a command like system("/bin/ls") to be interpreted as calling a different program by splitting the path. Always set IFS="" in scripts and clear it in programs that call the shell.

Defense: Sanitize or Clear the Environment

The safest approach is to either clear the entire environment and rebuild it with known-safe values, or at minimum set critical variables to safe values before calling any function that uses them.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

/* Known-safe environment for a privileged program */
static char *safe_env[] = {
    "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
    "IFS= \t\n",    /* safe IFS: only space, tab, newline */
    "HOME=/",       /* safe home โ€” not user's $HOME */
    "TERM=dumb",    /* safe terminal setting */
    NULL
};

/*
 * sanitize_environment(): Replace entire environment with known-safe values.
 * 
 * clearenv() removes all environment variables.
 * We then rebuild with only the variables we need.
 */
void sanitize_environment(void)
{
    /* Method 1: clearenv() then set individual vars (glibc extension) */
    if (clearenv() != 0) {
        perror("clearenv");
        exit(EXIT_FAILURE);
    }

    if (setenv("PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", 1) == -1 ||
        setenv("IFS", " \t\n", 1) == -1) {
        perror("setenv");
        exit(EXIT_FAILURE);
    }

    printf("[Security] Environment sanitized.\n");
}

/*
 * Method 2: Use execve() with an explicit environment array.
 * This is the most reliable approach โ€” no surprises.
 */
void exec_with_safe_env(const char *prog_path, char *const argv[])
{
    /* execve takes environment as third argument โ€” we control it completely */
    execve(prog_path, argv, safe_env);
    perror("execve"); /* only reached on failure */
    exit(EXIT_FAILURE);
}

int main(void)
{
    printf("Before sanitization:\n");
    printf("  PATH = %s\n", getenv("PATH") ? getenv("PATH") : "(null)");

    sanitize_environment();

    printf("After sanitization:\n");
    printf("  PATH = %s\n", getenv("PATH") ? getenv("PATH") : "(null)");
    printf("  IFS  = '%s'\n", getenv("IFS") ? getenv("IFS") : "(null)");

    return 0;
}

โœ”๏ธ Danger 2: Untrusted User Input

Every input from outside the program โ€” command-line arguments, interactive input, file contents, CGI inputs, IPC data, network packets โ€” must be validated before use.

Input Source What to Validate
argv[] (command-line) Length, character set, numeric range, path traversal (..)
stdin / fgets() Maximum length, null bytes, special characters
Files read from disk Line length, format, expected values, no shell metacharacters
Environment variables Never trust โ€” sanitize or clear
Network packets / CGI All of the above, plus injection attacks
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#define MAX_USERNAME_LEN  32
#define MAX_FILENAME_LEN  256

/*
 * validate_username(): Ensure a username contains only safe characters.
 * Valid: alphanumeric, underscore, hyphen. Max 32 chars.
 * Invalid: spaces, shell metacharacters, path separators.
 */
int validate_username(const char *name)
{
    size_t len;

    if (name == NULL) return 0;

    len = strlen(name);
    if (len == 0 || len > MAX_USERNAME_LEN) {
        fprintf(stderr, "Username too short or too long: %zu chars\n", len);
        return 0;
    }

    for (size_t i = 0; i < len; i++) {
        if (!isalnum((unsigned char)name[i]) &&
            name[i] != '_' && name[i] != '-') {
            fprintf(stderr, "Invalid character in username at pos %zu: '%c'\n",
                    i, name[i]);
            return 0;
        }
    }

    return 1;  /* Valid */
}

/*
 * validate_filename(): Reject path traversal and dangerous characters.
 */
int validate_filename(const char *filename)
{
    if (filename == NULL) return 0;
    if (strlen(filename) == 0 || strlen(filename) > MAX_FILENAME_LEN) return 0;

    /* Reject absolute paths */
    if (filename[0] == '/') {
        fprintf(stderr, "Absolute paths not allowed\n");
        return 0;
    }

    /* Reject path traversal */
    if (strstr(filename, "..") != NULL) {
        fprintf(stderr, "Path traversal detected: %s\n", filename);
        return 0;
    }

    /* Reject null bytes (can confuse C string functions) */
    if (memchr(filename, '\0', strlen(filename)) != NULL) {
        fprintf(stderr, "Null byte in filename\n");
        return 0;
    }

    return 1;  /* Valid */
}

int main(int argc, char *argv[])
{
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <username> <filename>\n", argv[0]);
        return 1;
    }

    printf("Validating inputs...\n");

    if (!validate_username(argv[1])) {
        fprintf(stderr, "Invalid username\n");
        return 1;
    }
    printf("[OK] Username: %s\n", argv[1]);

    if (!validate_filename(argv[2])) {
        fprintf(stderr, "Invalid filename\n");
        return 1;
    }
    printf("[OK] Filename: %s\n", argv[2]);

    return 0;
}

๐Ÿ”Œ Danger 3: Unreliable Runtime Assumptions

A set-UID program should never assume its initial runtime environment is normal. The program that exec’d it might have deliberately set up a hostile environment.

Standard FD Assumption โ€” A Classic Trap

Programs assume that stdin (FD 0), stdout (FD 1), and stderr (FD 2) are open. But an attacker can close some or all of these before exec-ing the set-UID program. When the program then opens a sensitive file (e.g., a log file), it gets FD 1 โ€” and thinking it’s writing to stdout, it’s actually writing to that sensitive file.

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>

/*
 * ensure_std_fds_open():
 *
 * At program startup, ensure fd 0, 1, 2 (stdin/stdout/stderr) are open.
 * If any is closed, open /dev/null on it so subsequent opens
 * don't accidentally get assigned one of these reserved FDs.
 */
void ensure_std_fds_open(void)
{
    int fd;
    struct stat sb;

    /* Check each of the three standard FDs */
    for (int stdfd = 0; stdfd <= 2; stdfd++) {
        if (fstat(stdfd, &sb) == -1) {
            /* This FD is closed โ€” open /dev/null on it */
            fd = open("/dev/null", O_RDWR);
            if (fd == -1) {
                /* Can't open /dev/null โ€” fatal */
                _exit(EXIT_FAILURE);
            }

            if (fd != stdfd) {
                /* Should not happen since we opened the lowest available FD,
                 * but handle it just in case */
                dup2(fd, stdfd);
                close(fd);
            }

            fprintf(stderr, "[Security] fd %d was closed โ€” replaced with /dev/null\n", stdfd);
        }
    }
}

int main(void)
{
    /* First thing: guarantee standard FDs are sane */
    ensure_std_fds_open();

    /* Now safe to open files โ€” we know fd 3+ will not be stdin/stdout/stderr */
    FILE *log = fopen("/var/log/myprog.log", "a");
    if (log) {
        fprintf(log, "Program started safely\n");
        fclose(log);
    }

    printf("Program running normally.\n");
    return 0;
}

Other Dangerous Assumptions

Wrong Assumption What Could Go Wrong Fix
stdin/stdout/stderr are open Opened file gets FD 1, writes to sensitive file Check with fstat() at startup
Resource limits are reasonable fork() fails, file operations fail, unexpected signals Check and set resource limits explicitly
CWD is a safe directory Relative paths resolve unexpectedly chdir(“/”) or a known-safe directory at startup
Environment variables are safe PATH attack, LD_PRELOAD injection clearenv() and rebuild

๐Ÿ“Œ Key Terms

PATH Attack IFS Attack clearenv() Input Validation Path Traversal Null Byte Attack FD Assumption /dev/null LD_PRELOAD Resource Exhaustion Environment Sanitization
๐ŸŽฏ Interview Questions โ€” Trusting Inputs
Q1. How can a malicious PATH environment variable compromise a privileged program?If the program calls system(), popen(), execlp(), or execvp() with a command name (not absolute path), these functions search PATH. An attacker sets PATH=/tmp/evil:/usr/bin and creates /tmp/evil/ls. When the program calls system(“ls”), it runs the attacker’s binary instead of /bin/ls โ€” with root privileges.

Q2. What is the standard file descriptor assumption attack?Before exec-ing a set-UID program, an attacker closes fd 1 (stdout). When the privileged program opens a sensitive file, the OS assigns it fd 1. When the program calls printf() or write(1, …) thinking it’s writing to stdout, it actually writes to the sensitive file. Always ensure fd 0/1/2 are open at startup.

Q3. What inputs must a privileged program validate?All external inputs: command-line arguments, interactive input, environment variables, file contents, CGI/web inputs, IPC data (pipes, shared memory, FIFOs), and network packets. Validation should check length, character set, numeric range, path traversal patterns, and format.

Q4. What is a path traversal attack and how do you prevent it?An attacker provides a filename containing “../” sequences like “../../etc/passwd” to make the program access files outside the intended directory. Prevention: reject any input containing “..”, reject absolute paths, and use chroot to confine the accessible filesystem.

Q5. Why should clearenv() be called in privileged programs that exec other programs?The inherited environment may contain attacker-controlled values for PATH, IFS, LD_PRELOAD, LD_LIBRARY_PATH, and other variables that can redirect execution. clearenv() removes all environment variables; the program then sets only the ones it needs with known-safe values.

Next: Buffer Overruns โ†’
Stack smashing, safe string functions, snprintf/strncpy

Continue to Part 9 โ† Part 7

Leave a Reply

Your email address will not be published. Required fields are marked *