Step 320. 1-day Vulnerability Analysis — Reverse-Engineering the Flaw from a Patch diff

Step 320. 1-day Vulnerability Analysis — Reverse-Engineering the Flaw from a Patch diff

Level 4 — Reporting, CVE Analysis & Open-Source Contribution | Difficulty ★★★★☆ | Estimated time: 3 hours

Prerequisites: Git basics from Steps 86–87 (diff, log, show), reading CVEs from Step 115, and basic Python syntax.

  • What you need: a computer with Git Bash and Python (3.10+). Today, instead of downloading an external CVE, you build your own practice repository with two commits — "vulnerable version → patched version" — and measure the analysis hands-on. The principle is identical to real CVE analysis.
  • Caution: ⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime. This environment has no external network access, so real CVE pages are marked as "screen examples," and all code and diff analysis is measured locally.

When a vulnerability is patched, the vendor publishes "the fixed code." Read that difference (the diff) and you can compute backward "what was broken." This is 1-day analysis — reverse-deriving the vulnerability’s principle from a published patch, an advanced skill shared by bug hunters, penetration testers, and security researchers. Today you analyze the "patch commit" of a mini web app you build yourself, practicing the whole process of reading a SQL injection’s location and principle from a single diff.


1. Learning Objectives

By the end of this chapter, you will be able to:

  • Explain what a 1-day vulnerability and patch-diff analysis are, and why they’re powerful
  • Read the git diff output between two commits — "vulnerable version → patched version" — and interpret what the changes mean
  • Find the typical patterns of security patches (added validation · escaping · length checks) in a diff
  • Reverse-derive the vulnerability type (CWE), trigger conditions, and impact from a single changed line
  • Organize analysis results in the "root-cause analysis note" format

2. Background Knowledge — Today’s Tools and Concepts

Today’s Tools at a Glance

Category Details
Language/environment Git Bash + Python 3.10+ (standard library only)
Today’s commands git log --oneline, git diff <vulnerable> <patched>, git show <hash>:file
Concepts needed 1-day vulnerabilities, reading diffs (+/-), SQL injection, CWE, parameter binding
Today’s deliverable 1 root-cause analysis note — including type/trigger/impact/patch principle

2-1. 1-day Vulnerabilities and the n-day World

A 0-day is a vulnerability known while no patch exists; a 1-day is a vulnerability whose patch is public but not yet applied on many systems. The time gap of "the patch is out but the world is still vulnerable" is a 1-day’s value.

Attackers reverse-derive the vulnerability’s principle from the patch diff to target unpatched systems; defenders use the same diff to judge "are our systems at risk." Both sides start from the same single diff. That’s why the ability to read a diff is a common language across offense and defense.

2-2. Why the diff — A Patch Is a Map of the Vulnerability

Finding "where the vulnerability is" in an entire codebase is finding a needle on a sandy beach. But a patch gives away the needle’s location — because the place that got fixed is the place that hurt.

Security patches mostly appear with three faces.

Pattern What it looks like The vulnerability it reverse-implies
Added validation A new conditional like if not is_valid(...): return Missing input validation (injection, path manipulation, etc.)
Escaping/binding String assembly replaced by ? binding or an escape function SQL/command/XSS injection
Length/range checks A len(x) > N check, fixed buffer sizes Buffer overflow, resource exhaustion

With this table in hand as you look at a diff, you can pick out "security-relevant changes" first even in an unfamiliar codebase.

2-3. SQL Injection Review — Today’s Textbook Vulnerability

Today’s practice repository contains a SQL injection vulnerability. Build a database query by string concatenation, and user input changes the query’s very grammar. Enter admin' -- in the username field, and the password check after it gets commented out (--) — logging you in without a password.

The solution is parameter binding (parameterized queries) — passing the query statement and the data separately. In today’s diff you’ll see this replacement directly. By CWE number it’s CWE-89 (SQL Injection). If a CVE, from Step 115, is a vulnerability’s case number, a CWE is the vulnerability’s kind number.

2-4. The Analysis Note — Reading Ends in Writing

1-day analysis’s deliverable is not code but a document. From today, fix the four-box format.

Type (CWE):    the classification of what's wrong
Trigger:       with what input/state it fires
Impact:        what becomes possible on success
Patch principle: why the fixed code blocks this vulnerability

3. Follow Along

3-1. Making the Practice Repository — Let’s Be the Vendor

A real CVE’s patch is made by someone else, but for analysis practice, a repository you made yourself is best — because you know the answer. First, make the vulnerable version of a mini login program.

Input (Git Bash — start in a new folder):

mkdir oneday-lab && cd oneday-lab
git init

Create the file app.py with the contents below (save with Notepad or an editor):

"""guestbook v1.0 — mini login (vulnerable version)"""
import sqlite3


def init_db(path=":memory:"):
    conn = sqlite3.connect(path)
    conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT, password TEXT)")
    conn.execute("INSERT INTO users (username, password) VALUES ('admin', 's3cr3t!')")
    conn.execute("INSERT INTO users (username, password) VALUES ('guest', 'guest123')")
    conn.commit()
    return conn


def login(conn, username, password):
    query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
    print(f"[DEBUG] executed query: {query}")
    cur = conn.execute(query)
    row = cur.fetchone()
    if row:
        print(f"[OK] login successful: {row[1]}")
        return True
    print("[FAIL] login failed")
    return False


if __name__ == "__main__":
    conn = init_db()
    u = input("Username: ")
    p = input("Password: ")
    login(conn, u, p)

How to read it: look at the login() function. It assembles the query with an f-string — user input goes straight into the query statement. This is today’s "vulnerable version v1.0."

Commit it.

git add app.py
git commit -m "guestbook 1.0 — login feature"

Output (measured 2026-09-09):

[main (root-commit) a36349e] guestbook 1.0 — login feature
 1 file changed, 25 insertions(+)

3-2. Making the Patch-Version Commit — The Vendor’s Fix

Now reproduce the situation where the vendor fixes the vulnerability. Change only the login() function in app.py like this.

def login(conn, username, password):
    query = "SELECT * FROM users WHERE username = ? AND password = ?"
    cur = conn.execute(query, (username, password))
    row = cur.fetchone()
    if row:
        print(f"[OK] login successful: {row[1]}")
        return True
    print("[FAIL] login failed")
    return False

Also change the version marker on the file’s top line to v1.1, and commit. Like a real security patch, let’s put a vulnerability marker in the commit message (today’s fictional number CVE-2026-EX01 is a fabricated practice number).

git add app.py
git commit -m "fix: SQL injection in login query (CVE-2026-EX01)"
git log --oneline

Output (measured 2026-09-09):

6f4cc5b fix: SQL injection in login query (CVE-2026-EX01)
a36349e guestbook 1.0 — login feature

How to read it: this repository now has the same structure as a real CVE situation — the vulnerable version below, the patched version above. In real analysis, GitHub release tags (e.g., v1.0v1.1) or the "fix commit" link in an advisory sit where these two hashes are.

3-3. Extracting the Patch diff — The Raw Material of Analysis

Extract the difference between the two commits. The order is "vulnerable → patched."

Input:

git diff HEAD~1 HEAD

Output (measured 2026-09-09):

diff --git a/app.py b/app.py
index 521b589..13aef8e 100644
--- a/app.py
+++ b/app.py
@@ -1,4 +1,4 @@
-"""guestbook v1.0 — mini login (vulnerable version)"""
+"""guestbook v1.1 — mini login (patched version)"""
 import sqlite3


@@ -12,9 +12,8 @@ def init_db(path=":memory:"):


 def login(conn, username, password):
-    query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
-    print(f"[DEBUG] executed query: {query}")
-    cur = conn.execute(query)
+    query = "SELECT * FROM users WHERE username = ? AND password = ?"
+    cur = conn.execute(query, (username, password))
     row = cur.fetchone()
     if row:
         print(f"[OK] login successful: {row[1]}")

How to read it: as you learned in Step 86, - is a deleted line and + is an added line. @@ -12,9 +12,8 @@ is a position marker meaning "near the login function." The core change is three lines — the f-string query assembly disappeared, replaced by ? placeholders and a (username, password) tuple.

In real analysis, the "Files changed" tab on a GitHub commit page shows this output on the web (screen example) — the same convention, deleted lines in red on the left, added lines in green on the right.

3-4. Dissecting the diff — Reverse-Deriving the Vulnerability from Three Lines

Now for today’s core. Dissect each change in the diff with three questions.

Question 1 — What disappeared? The deleted lines are these.

query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"

User input was embedded directly into the query as a string. The fact this line existed means that, before the patch, input could become not the query’s "data" but its "grammar."

Question 2 — What arrived? The new lines.

query = "SELECT * FROM users WHERE username = ? AND password = ?"
cur = conn.execute(query, (username, password))

Recall 2-2’s table — this is the "escaping/binding" pattern. The ? holds the place, and the actual values are passed as a separate tuple. The database driver handles the values safely, so even quotes in the input can’t change the query’s grammar.

Question 3 — Without that fix, what becomes possible? Here you reverse-derive the vulnerability. Input like ' OR '1'='1 in the username field pollutes the query condition into true, and input like admin' -- comments out the entire password check. Conclusion: logging in as any account without a password was possible.

Why do this: these three questions — "what disappeared / what arrived / what happens without it" — are a universal procedure that applies as-is to any language, any CVE’s patch.

3-5. Pulling Out Past-Version Files — git show

For Step 321’s reproduction, pull out the vulnerable and patched versions of app.py as separate files. A command that extracts straight from the repository without checkout.

Input:

git show HEAD~1:app.py > app_v1.py
git show HEAD:app.py > app_v2.py
ls

Output (measured 2026-09-09):

app.py  app_v1.py  app_v2.py

How to read it: git show hash:file prints "this file’s content at that commit." HEAD~1 is "one commit back" (the vulnerable version), and HEAD is the latest (the patched version). These two files become the experimental material for the next chapter’s reproduction exercise. You already used this command form in Step 87’s 3-6 (pulling out a deleted key).

3-6. Writing the Analysis Note — Fixing the Reading into a Document

Write what you’ve read so far in 2-4’s format. This is today’s deliverable.

Analysis note example (measured analysis, 2026-09-09):

Analysis target: guestbook v1.0 → v1.1 patch (practice repo, fictional CVE-2026-EX01)
Type (CWE):      CWE-89 SQL Injection
Location:        app.py login() — the line that assembled the query with an f-string
Trigger:         username input containing ' and a SQL comment (--). E.g.: admin' --
Impact:          login as any account without a password (authentication bypass)
Patch principle: separates the query statement from the data (? binding), blocking
                 input from becoming grammar
Patch check:     the diff shows a binding replacement, not validation/length checks
                 — the 'escaping' pattern from 2-2's table

How to read it: with this one note, you can explain this vulnerability to anyone within 5 minutes. And in Step 321 this note gets reused as the reproduction plan, and in Step 322 as the template for solo analysis.

Why do this: analysis that ends in your head evaporates. The moment you write it in the four boxes — "type/trigger/impact/patch principle" — it becomes yours.


4. Missions & Exercises

Mission — A diff Analysis Note for a Second Vulnerability

  1. Create a new file search.py in the oneday-lab repo — one function that takes a search keyword, assembles it as f"SELECT * FROM users WHERE username LIKE '%{kw}%'", and executes it
  2. Commit this state as "add search feature" (the vulnerable version)
  3. Fix the same file to the ? binding style and commit as "fix: SQL injection in search feature"
  4. Extract the patch diff with git diff HEAD~1 HEAD, and answer 3-4’s three questions (disappeared / arrived / what without it) in sentences
  5. Complete an analysis note in the 3-6 format — write a concrete input example in the trigger box

Exercises

Exercise 1. What is a 1-day vulnerability? Explain its difference from a 0-day in terms of "whether a patch exists."

Exercise 2. What does @@ -12,9 +12,8 @@ in diff output mean? And state the meaning of - lines and + lines respectively.

Exercise 3. Of 2-2’s three patterns (added validation / escaping·binding / length·range checks), which does today’s practice patch correspond to? Also write what vulnerability kinds each pattern implies.

Exercise 4. Explain a trick for finding security-relevant changes first in a huge diff (dozens of files), using the commit message and 2-2’s pattern table.


5. Model Answers & Completion Criteria

Mission Model Answer

The command flow is identical to 3-1~3-3. Example core line of the vulnerable version:

def search(conn, kw):
    query = f"SELECT * FROM users WHERE username LIKE '%{kw}%'"
    return conn.execute(query).fetchall()

Patched version:

def search(conn, kw):
    return conn.execute(
        "SELECT * FROM users WHERE username LIKE ?", ('%' + kw + '%',)
    ).fetchall()

Example answers to the three questions: ① disappeared — the assembly style that embedded the search keyword directly into the query as a string. ② arrived — a ? placeholder with values passed as a tuple. ③ without it — the whole user list could leak via input like ' OR '1'='1 in the search box.

Example trigger box of the analysis note: search input containing ' or SQL keywords. E.g.: %' OR '1'='1.

How to verify: ① does git log --oneline show the vulnerable and patch commits in order? ② does the diff cleanly show only the difference between the two styles? ③ are the note’s four boxes (type/trigger/impact/patch principle) all filled, with a concrete input in the trigger?

Exercise Answers

Answer 1. A 1-day is a vulnerability whose patch (fix code) is public. The name means "a vulnerability within 1 day (a short time) of patch publication," and the time gap in which unpatched systems remain is the body of the risk. A 0-day is a vulnerability known while no patch exists at all — so there’s no official means of defense.

Answer 2. @@ -12,9 +12,8 @@ is a position marker for the changed region: "9 lines starting at line 12 of the original, 8 lines starting at line 12 of the result." Lines starting with - are deleted lines (present only in the vulnerable version); lines starting with + are added lines (newly appearing in the patched version).

Answer 3. The "escaping/binding" pattern. String assembly was replaced by ? binding, so the reverse-implied vulnerability is injection (here SQL injection, CWE-89). Added validation implies injection, path manipulation, authorization bypass, and the like; length·range checks imply buffer overflow or resource exhaustion.

Answer 4. First, narrow down the changed files by clues like "fix", "security", "CVE", "injection", "overflow" in the commit message and advisory. Then, in the file-level diffs, search first for 2-2’s three patterns — newly appeared if validations, escape/binding function calls, length comparisons. Feature additions (new files, UI changes) and security fixes are often pushed mixed together, so this filtering is what cuts analysis time. Once you find the core function, trace back up its call paths to confirm the trigger.

Completion Criteria Checklist

  • [ ] I built a practice repository with two commits — "vulnerable → patched" — myself
  • [ ] I can extract a patch diff with git diff HEAD~1 HEAD
  • [ ] I can read the -/+/@@ notations of a diff
  • [ ] I can list the three patterns of security patches (validation/escaping/length checks)
  • [ ] I can pull out a past-version file with git show hash:file
  • [ ] I can reverse-derive the vulnerability type, trigger, impact, and patch principle from a single diff
  • [ ] Mission: I completed the analysis note for the second vulnerability

6. Common Pitfalls & Fixes

Wall 1. The diff is too big — I can’t tell what the core is

Symptom: you opened a real project’s patch diff and it’s dozens of files, thousands of lines.

Cause: a release mixes feature changes and security fixes. Trying to read it all is the trap.
Fix: use Wall 1’s filter sequence — ① narrow files by security clues (fix/security/CVE) in the commit message → ② search for 2-2’s three patterns (validation/escaping/length checks) → ③ trace the found function’s call paths to confirm the trigger. Practice this sequence on today’s small diff, and the same hand movements come out on big diffs.

Wall 2. I typed the git diff direction backwards

Symptom: you typed git diff HEAD HEAD~1 and the + and - look reversed.

Cause: a diff shows "what must change to go from left to right." Flip the order and you get "the diff that reverts the patch."
Fix: always type in the order git diff <vulnerable> <patched>. If the vulnerable code appears as - in the output, the order is right.

Wall 3. "They fixed it with binding" — but I don’t get why that’s safe

Symptom: you read the diff but can’t explain why ? is the solution.

Cause: in string assembly, input becomes part of the query statement, but in binding, input is passed only as a value. The database driver treats a quote as a character, not grammar.
Fix: you’ll confirm this directly in Step 321 — the reproduction of feeding admin' -- to the vulnerable and patched versions respectively is the complete answer to this question.

Wall 4. The git show redirect file looks odd

Symptom (Windows environment difference): you made a file with git show HEAD~1:app.py > app_v1.py and opened it in Notepad, and the line breaks look broken or the encoding differs.

Cause: Git Bash’s output is UTF-8/LF, while Windows Notepad may use different defaults.
Fix: open it in an editor that knows UTF-8 like VS Code, or just proceed — it doesn’t affect Python execution. If python app_v1.py runs, it’s fine.

Wall 5. Mistaking the fictional CVE number in the commit message for a real one

Symptom: you search for CVE-2026-EX01.

Cause: today’s number is a fabricated practice number (EX = exercise).
Fix: in real analysis, confirm the real number in the advisory or NVD. As you learned in Step 115, keep the habit of checking the number’s status (Analyzed/REJECT) too.


7. Summary

Today’s Concepts

Concept One-line explanation
1-day vulnerability A vulnerability in the time gap where the patch is public but not applied
Patch-diff analysis The technique of reverse-deriving a vulnerability’s principle from the fixed code
Security patch 3 patterns Added validation / escaping·binding / length·range checks
SQL injection (CWE-89) A vulnerability where input becomes query grammar
Parameter binding The injection-blocking style that separates query statement from data
CWE A vulnerability’s "kind number" — CVE is an individual case, CWE is a classification
Analysis note A four-box document: type/trigger/impact/patch principle

Today’s Commands

Command What it does
git log --oneline Check the vulnerable/patch commit hashes
git diff <vulnerable> <patched> Extract the patch diff (mind the order)
git show <hash>:<file> Pull out a file’s content at a specific commit
git show <hash>:<file> > output_file Extract a past version into a file

The Core Instinct

Today’s universal procedure is three questions — what disappeared / what arrived / what becomes possible without it. With these three questions, you can reverse-derive the vulnerability from any security patch, Python or C or JavaScript.

And one more. The deliverable of diff analysis is not code but the four-box analysis note. Reading skill is completed by writing. Today’s note becomes the reproduction plan in Step 321, and the template for standing alone in Step 322.


Once every box is checked, Step 320 is complete.