CWE-242: Use of Inherently Dangerous Function - C
Overview
gets() is the C standard library's example of a function with no safe calling convention: it reads a line from stdin into a caller-supplied buffer with no argument for the buffer's size, so it cannot stop even the most careful caller from writing past the end of it. It was removed from the C11 standard for exactly this reason - not deprecated, removed. Any remaining call needs to be replaced, not guarded.
C++ has a version of the same weakness in operator>> reading into a character buffer, with one qualification that decides whether a finding is real. Until C++20 the overload took a bare CharT*, so extraction ran until whitespace with no knowledge of the destination's capacity unless the caller set std::setw. C++20 replaced it with an overload that takes the array by reference, CharT (&s)[N], and therefore knows N: it stores at most N - 1 characters plus the terminator. So char buf[100]; std::cin >> buf; is bounded when compiled as C++20 or later, and unbounded when compiled to an earlier standard. The pointer shape - char *p = ...; in >> p; - is where the size was never deducible from the type in the first place, and it is unbounded on every standard up to C++17. C++20 did not leave it alone either: P0487R1 replaced the CharT* overload rather than adding beside it, so there is no overload a char * can bind to and the extraction no longer compiles - a build error rather than a live finding, and a better outcome than a bounded call because it cannot be silently reintroduced.
So there are three states, and the standard decides which one applies: bounded (array, C++20+), unbounded (either shape, C++17 and earlier), or ill-formed (pointer, C++20+). Before deciding, check what the translation unit is actually compiled with rather than the project's stated baseline - a build that has not moved its -std flag is still on the old rules whatever the documentation says. Where the code must build under both, std::setw(sizeof buf) bounds the extraction on the older standards and is harmless on the newer ones; a std::string destination avoids the question entirely.
Common Vulnerable Patterns
Unbounded Line Input
#include <stdio.h>
// VULNERABLE - no argument exists to tell gets() how big buffer is
void read_input_bad(void) {
char buffer[100];
gets(buffer); // 100 bytes of input overflows this buffer, and enlarging it changes only the number
}
Why this is vulnerable: gets()'s signature is char *gets(char *s) - it takes only the destination pointer, never a size. There is no way to pass it a limit, so it keeps copying from stdin until it reaches a newline or EOF regardless of how much space is actually available at s. A caller cannot fix this by allocating a bigger buffer or validating input beforehand, because the function itself has no way to know where the buffer ends.
That signature is the test to apply to anything else a scan flags in C: if it writes into caller-supplied memory and takes no parameter describing how much of that memory it may use, no call site can be made safe and the call has to be replaced rather than guarded.
Secure Patterns
Bounded Line Input
#include <stdio.h>
#include <string.h>
// SECURE - fgets takes the buffer's capacity as a required argument
void read_input_safe(void) {
char buffer[100];
if (fgets(buffer, sizeof(buffer), stdin) != NULL) {
// fgets keeps the trailing newline if it fits - strip it if not wanted
buffer[strcspn(buffer, "\n")] = '\0';
}
}
Why this works: fgets() takes the destination's capacity as its second argument and writes at most that many bytes including the terminator, so the safety property is enforced by the function's own contract instead of depending on the caller getting a size check right. Unlike gets(), fgets() also tells the caller when nothing was read: it returns NULL at end-of-file with no characters consumed, or on error, where gets() gave the caller no way to distinguish either from a successful empty read.
What the return value does not report is truncation. A non-NULL return means some bytes were read, not that a whole line was - if the line was longer than the buffer, fgets() fills it, stops, and returns the buffer exactly as it would for a complete short line. Detecting that takes a separate check: a full line ends with '\n', so its absence means either truncation or a final line with no newline, and the remainder of the input line is still sitting in the stream waiting to be read as though it were the next one. That last part is what turns a display bug into a parsing one, and it is why the testing note below checks truncation separately rather than treating a non-NULL return as success.
Making Reintroduction a Build Error
#include <stdio.h>
#include <string.h>
// SECURE - after the standard headers, poison the identifier so any later use fails to compile
#pragma GCC poison gets
Why this works: #pragma GCC poison (supported by both GCC and Clang) makes the preprocessor reject any later appearance of the identifier, so a reintroduced call fails the build instead of producing a warning someone can scroll past. Put it after the standard headers are included, since the pragma fires on a declaration as readily as on a call. It only covers translation units that include the header carrying it, so pair it with a static analysis rule - clang-tidy's bugprone-unsafe-functions (aliased as cert-msc24-c) - running over the whole codebase in CI.
Considerations
- Confirm the flagged function genuinely has no safe convention before choosing the fix. That determines the shape of the work: a function with no size parameter has to be replaced, while
strcpy()orsprintf()(which is CWE-676, not this weakness) can stay if the call site supplies the bound the function won't enforce. Replacing every flagged function on reflex produces a much larger diff than the finding justifies. - A live
gets()finding usually means the target still builds as C99 or earlier, since a C11 toolchain no longer declares it. The choice is between moving the target to-std=c11or newer, which is the real fix but can surface unrelated conformance breaks in the same build, and poisoning the identifier locally, which is a smaller change that only protects the files including the poisoning header.
Testing
- Feed an input line one byte under, exactly at, and well over the destination buffer's capacity. The
fgets()version should read at most capacity-1 bytes and terminate the string; nothing should be written past the buffer. Build with-fsanitize=addressfor these runs - an overflow that an ordinary build absorbs silently aborts with a report. - Confirm callers still behave correctly on a truncated line.
fgets()leaves the remainder of an over-long line in the stream, so code that previously assumed one call meant one line may now read the tail as a second record. - Reintroduce a
gets()call on a scratch branch and confirm the build fails rather than warning. A warning is not enforcement, and this is the only way to tell a working ban from one that was never wired up. - Re-run static analysis (clang-tidy with
bugprone-unsafe-functions, orcppcheck) and confirm no calls to the banned function remain anywhere in the codebase.
Common Pitfalls
- Adding a length check on the input source before calling
gets(): validating that stdin "shouldn't" send more than N bytes doesn't change whatgets()does with input that violates that assumption - the function still has no way to stop at N. This makes the risk depend on an assumption elsewhere holding true instead of removing it. - Swapping in
scanf("%s", buffer)as the "safe" fix: an unbounded%sconversion has the exact same missing-size problem asgets(); the safe form needs an explicit field width (scanf("%99s", buffer)for a 100-byte buffer) or, better,fgets()followed by parsing. - Assuming a newer compiler silently makes old
gets()calls safe: a compiler that rejectsgets()under-std=c11will often still compile it under an older standard flag or with the deprecation warning suppressed - the ban has to be enforced by tooling that checks the flags actually in use, not assumed from the toolchain's release date alone.