Skip to content

Input Validation

Estimate Time: 6 min read


Emma is building a tiny command-line admin tool called ClubDesk to help her student club manage tasks. The program provides a simple login prompt so designated organizers can perform updates, and the feature matters because it gates privileged operations the club relies on during meetings and event planning.

While the interface is minimal, it accepts free-form text from users and then makes decisions based on that input. A subtle flaw in how ClubDesk accepts and handles that text can break the program's assumptions and interrupt the service; the problem emerges not from the login logic itself but from how the input is processed before that logic runs.

Introduction to the vulnerable program

Continuing Emma's story, she implemented ClubDesk to read a password from stdin, copy what the user typed into a fixed-size buffer, and compare that buffer to a stored secret. The intended behavior is straightforward: read the password, compare it to the stored value, and print either a welcome or an access-denied message.

User input flows from the terminal into a std::string and then is copied into a small preallocated char array before the comparison. The program trusts that the input will fit the array and does not check the length before copying. That trust assumption—"input will always be short enough to fit the buffer"—is where the vulnerability arises.

Because the copy happens without bounds checking, attacker-controlled long input can overwrite memory past the destination buffer. The vulnerable conditions are simple: an unbounded input source, a fixed-size target buffer, and an unchecked copy operation.

#include <iostream>
#include <cstring>

int main() {
  char stored[16] = "clubpass";
  std::string input;
  std::cout << "Admin password: ";
  std::getline(std::cin, input);
  char entered[16];
  std::strcpy(entered, input.c_str()); // unsafe: no length check
  if (std::strcmp(entered, stored) == 0) {
    std::cout << "Welcome, organizer!\n";
  } else {
    std::cout << "Access denied.\n";
  }
  return 0;
}

In this program the unsafe call is obvious: std::strcpy copies the entire input into entered without verifying that entered is large enough. If a user types a password longer than fifteen characters, the copy will write past entered, corrupting adjacent stack data and destabilizing the process before the comparison completes.

Exploring an exploit against the vulnerable program

An attacker interested in disrupting ClubDesk's availability simply needs to provide a password string longer than the fixed buffer. The goal is denial of service: make the program crash or abort so organizers cannot log in until the process is restarted.

Step one: supply an overly long string at the password prompt. Step two: the program copies that string into the small buffer with std::strcpy. Step three: the write overruns the buffer, corrupts return addresses or stack metadata, and the runtime detects the corruption or executes invalid instructions, causing a crash.

Because the vulnerability is a straightforward out-of-bounds write, the exploit does not need special encoding or deep protocol knowledge—only a long input. The observable result is immediate: the program terminates with a segmentation fault or abort message instead of printing either access result, denying service to legitimate users.

#include <iostream>
#include <string>

int main() {
  // print a long string that can be piped into the vulnerable program
  std::string long_input(1024, 'A');
  std::cout << long_input << "\n";
  return 0;
}

Running the exploit program and piping its output into the vulnerable ClubDesk replicates the attack: the long line reaches std::getline, std::strcpy writes past entered, and the process crashes. The exploit succeeds because the code assumes inputs are already bounded and never enforces that assumption at the trust boundary.

Patching the program to prevent the exploit

The primary mitigation is to enforce bounds at the trust boundary and to avoid copying unbounded input into fixed-size buffers. One clear approach is to use std::string for stored credentials and authenticated comparisons, and to reject inputs that exceed a documented maximum length before any copy occurs.

This mitigation works because it removes the unsafe copy from the execution path and enforces the original assumption explicitly: inputs are either within the allowed size or are rejected. It addresses the root cause—an unchecked copy into a fixed buffer—and keeps the program logic intact.

#include <iostream>
#include <string>

int main() {
  const std::size_t MAX_PASS = 64;
  const std::string stored = "clubpass";
  std::string input;
  std::cout << "Admin password: ";
  std::getline(std::cin, input);
  if (input.size() > MAX_PASS) {
    std::cout << "Input too long.\n";
    return 1;
  }
  if (input == stored) {
    std::cout << "Welcome, organizer!\n";
  } else {
    std::cout << "Access denied.\n";
  }
  return 0;
}

In the corrected version there is no fixed-size char buffer for user data and the program explicitly rejects overly long input. The comparison uses std::string semantics, which eliminates the unsafe memory copy and enforces the trust boundary in code rather than by assumption. The tradeoff is a clear rejection path for malformed input, which is preferable to unpredictable crashes and keeps the original functionality intact.