Skip to content
Courses

Logic Bugs

Bug Classes and PrimitivesLesson 2 of 860 minutesLessonNo lab next

The till needed an oversized name before it got its bookkeeping wrong. A parcel depot can make a rather different mistake: accept a perfectly ordinary request from someone who has no business making it. There needn’t be anything unusual about the request itself. Asking for locker 4 is fine if locker 4 belongs to you.

In the requests below, the program stays inside its buffers and both of its checks work. The problem is the question neither check asks: does this locker belong to the signed-in courier? We’ll compare those checks with the depot’s stated rules, then see what happens when Sam asks for the master key.

Before calling a request wrong, we need to know what the depot is supposed to allow. Conveniently, this program prints its own rules. We can compare them with the code instead of guessing what its author meant.

In the Ubuntu terminal, create the working directory if needed and move into it:

Terminal window
mkdir -p ~/underdunn-course/bug-classes ~/underdunn-course/notes
cd ~/underdunn-course/bug-classes

Save the complete program below as depot.c. Keep the terminal in this directory for the build commands; no source file from an earlier lesson is needed.

/*
* A parcel depot with six lockers.
*
* The rules the depot is supposed to follow are printed by the `rules`
* command, so you do not have to infer them.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define LOCKERS 6
static const char *contents[LOCKERS] = {
"(empty)",
"parcel 8841, for Nadia",
"parcel 8842, for Omar",
"(empty)",
"depot master key",
"parcel 8843, for Priya",
};
static const char *assigned_to[LOCKERS] = {
"sam", "sam", "ines", "ines", "depot", "sam",
};
static char courier[32] = "";
static void rules(void)
{
puts(" 1. A courier must sign in before opening anything.");
puts(" 2. A courier may open a locker assigned to them.");
puts(" 3. Locker 4 is assigned to depot staff.");
puts(" Assignments: 0 sam 1 sam 2 ines 3 ines 4 depot 5 sam");
}
static void open_locker(int n)
{
if (courier[0] == '\0') {
puts(" denied: not signed in");
return;
}
if (n < 0 || n >= LOCKERS) {
puts(" denied: no such locker");
return;
}
printf(" locker %d: %s\n", n, contents[n]);
}
int main(void)
{
char line[128];
setvbuf(stdout, NULL, _IONBF, 0);
puts("Parcel depot. Commands: rules, signin <name>, open <n>, quit");
while (fgets(line, sizeof line, stdin)) {
line[strcspn(line, "\n")] = '\0';
if (strcmp(line, "rules") == 0) {
rules();
} else if (strncmp(line, "signin ", 7) == 0) {
snprintf(courier, sizeof courier, "%s", line + 7);
printf(" signed in as %s\n", courier);
} else if (strncmp(line, "open ", 5) == 0) {
open_locker(atoi(line + 5));
} else if (strcmp(line, "quit") == 0) {
break;
} else if (line[0] != '\0') {
puts(" unknown command");
}
}
return 0;
}

The parser removes the newline from fgets, compares the command names, and uses atoi to turn the locker text into an integer. snprintf copies the supplied courier name into its fixed buffer with a size limit. setvbuf makes the replies appear immediately. Those details keep this example’s input handling bounded; none of them performs the missing ownership check.

Terminal window
gcc -g -O0 -fno-omit-frame-pointer -Wall -Wextra depot.c -o depot

-Wall -Wextra enables the warning groups used later on this page. Warnings can point us toward suspicious code, but they do not know the depot’s authorization rules.

Run ./depot and enter:

rules
quit
Parcel depot. Commands: rules, signin <name>, open <n>, quit
1. A courier must sign in before opening anything.
2. A courier may open a locker assigned to them.
3. Locker 4 is assigned to depot staff.
Assignments: 0 sam 1 sam 2 ines 3 ines 4 depot 5 sam

That’s the intended machine, written down by the machine itself. Sam works here and has three lockers. We’ll compare those four rules with what open_locker actually allows.

Sam signs in and collects the parcels assigned to him, which is an unremarkable morning. Run ./depot and enter:

signin sam
open 0
open 1
quit
signed in as sam
locker 0: (empty)
locker 1: parcel 8841, for Nadia

Now the same command with a different number. Run ./depot and enter:

signin sam
open 4
quit
signed in as sam
locker 4: depot master key

No input was malformed. Nothing overflowed, the number was in range, and open is a command Sam is entitled to use. But compare the rules with the checks:

  • The rules say locker 4 belongs to depot staff, and a courier may open a locker assigned to them.
  • The code checks that somebody is signed in and that the locker exists. It never looks at who the locker belongs to.

Opening locker 4 doesn’t tell us whether we can skip sign-in or request a locker that doesn’t exist. Those are separate checks in open_locker, and we can test them without changing anything else. Run ./depot and enter:

open 4
quit

Run it again and enter:

signin sam
open 6
open -1
open 99
quit
denied: not signed in
signed in as sam
denied: no such locker
denied: no such locker
denied: no such locker

So the bound is real on both sides. You need some sign-in, and the locker number has to be one of the six that exist. There’s no command that writes, either, which you can confirm by trying one. Run ./depot and enter:

signin sam
write 4 hello
open 4
quit
signed in as sam
unknown command
locker 4: depot master key

We can now describe the access without making the master key stand in for the whole bug:

  • A signed-in user can read any of the six lockers, rather than only their assigned ones.
  • The range check still keeps the index between zero and five.
  • There is no command to replace a locker’s contents. The rejected write command agrees with the source.

Sam has more access than he should, but not unrestricted access to the depot’s memory.

We can check the same requests with tools that look for memory errors and certain undefined operations. AddressSanitizer instruments memory accesses, while UndefinedBehaviorSanitizer adds checks for selected operations whose behavior C does not define. Valgrind’s Memcheck observes memory use while the program runs. None of them knows the depot’s ownership rules, though. Here’s the instrumented build:

Terminal window
gcc -g -O0 -fsanitize=address,undefined depot.c -o depot-san

Run ./depot-san and enter:

signin sam
open 4
open 99
quit

Then ask the shell what status it exited with:

Terminal window
echo "exit $?"
signed in as sam
locker 4: depot master key
denied: no such locker
exit 0
Terminal window
printf 'signin sam\nopen 4\nquit\n' | valgrind -q --error-exitcode=9 ./depot > /dev/null
echo "exit $?"
exit 0

None of the tools reports an error in these runs. The source explains why the unauthorized read can escape their attention: locker 4 is an existing element of contents, and printing it uses the same operation as printing Sam’s own locker. The missing ownership comparison doesn’t require an invalid memory access.

You built with -Wall -Wextra, so you already saw this:

depot.c:22:20: warning: ‘assigned_to’ defined but not used [-Wunused-variable]

The table of who owns what is in the program and nothing reads it. That’s a fairly direct clue that an authorization decision isn’t being made.

If another command used assigned_to to print a roster, the warning could disappear while open_locker still omitted the check. So enjoy the convenient clue, but follow it back to the ownership decision.

Four lines, using the table that was already there:

if (strcmp(assigned_to[n], courier) != 0) {
puts(" denied: not your locker");
return;
}
Terminal window
cp depot.c depot-fixed.c # then add the check above, after the range check
gcc -g -O0 -Wall -Wextra depot-fixed.c -o depot-fixed

Run ./depot-fixed and enter:

signin sam
open 1
open 4
quit

Run it again and enter:

signin depot
open 4
quit
signed in as sam
locker 1: parcel 8841, for Nadia
denied: not your locker
signed in as depot
locker 4: depot master key

Sam keeps his own lockers, loses the master key, and depot staff are unaffected. The unused-variable warning goes away at the same time, for the same reason.

The new comparison ties the request to the assignment table without changing the buffer layout or the range check.

There is still a separate problem with signing in. The repair controls access under the supplied name, but this toy program lets anyone type signin depot. A real sign-in system would also have to establish that the caller is entitled to that identity.

Keep the identity fixed as Sam when testing the ownership check, so we’re comparing the same request before and after the repair.

Knowing a locker number isn’t permission

Section titled “Knowing a locker number isn’t permission”

This is a missing authorization check: knowing which resource to request is being treated as permission to read it. CWE-639 describes this pattern when a user-controlled key selects another user’s resource. Our key is a locker number, but the same question applies to a document identifier or an account number: where does the program connect that resource to the caller’s permission?

Knowledge check

Question 1 of 3

ASan, UBSan and valgrind all report nothing for the unrepaired depot. What does that establish?

  • If open 4 is denied on your first build, check you haven’t already added the repair. The unrepaired open_locker has exactly two if statements.
  • If every command reports unknown command, your input lines are picking up carriage returns. Use printf with explicit \n as shown rather than pasting from a document.
  • If the build fails on atoi, add #include <stdlib.h>. The listing above has it.
  • If you see no assigned_to warning, you built without -Wextra. The warning is incidental to the lesson and its absence changes nothing about the program’s behaviour.

To recover a known state, rebuild from the listing above. The depot writes nothing to disk and keeps no state between runs.

Describe the access Sam gained and the checks that still limit it. Then use the assignment table to answer the underlying question: if the requests caused no memory error, what makes this a vulnerability?

Compare your reasoning

The basis is the depot’s own published rules. Rule 2 grants a courier access to lockers assigned to them, the assignment table puts locker 4 with depot staff, and Sam reached it while signed in as himself. That is the program contradicting its own specification, and it needed no malformed input and no memory error to do it.

If you found yourself reaching for impact instead, notice that you don’t actually know what the master key opens. Neither do I. That is a question about the depot, not about the program, and the program alone cannot answer it.

The depot’s range check is the only thing standing between a courier and memory that isn’t a locker at all. You supplied 99 and it said no.

The next lesson takes that check away and asks what a read at an index nobody validated is actually worth, which is where the sanitizers start earning their place.