Commit 4b914f8c authored by Minseong Choi's avatar Minseong Choi 💬
Browse files

Add Team 14 CSRF team project materials (정보보호, Korea Univ.)

parent f53f19e2
Loading
Loading
Loading
Loading

.gitattributes

0 → 100644
+5 −0
Changes for .gitattributes: 5 added lines, 0 removed lines.
Original line number Diff line number Diff line
team-project-csrf/**/*.pptx filter=lfs diff=lfs merge=lfs -text
team-project-csrf/**/*.docx filter=lfs diff=lfs merge=lfs -text
team-project-csrf/**/*.pdf filter=lfs diff=lfs merge=lfs -text
team-project-csrf/**/*.zip filter=lfs diff=lfs merge=lfs -text
team-project-csrf/**/*.png filter=lfs diff=lfs merge=lfs -text
+26 −0
Changes for team-project-csrf/README.md: 26 added lines, 0 removed lines.
Original line number Diff line number Diff line
# team-project-csrf

Korea University · 정보보호 (Information Security) — **Team 14** team project, topic: **Cross-Site Request Forgery (CSRF)**.

Archive of project materials, copied as-is from the original files (no content edited). Original filenames used an em dash (—); they are renamed here to ASCII-safe names.

> **Deadline:** Final Report due **2026-10-08 23:59 KST**.

## File index

Dates are the original files' last-modified times (KST).

| Path | Original filename | Date |
|---|---|---|
| `report/Team14-CSRF-Presentation-Materials-Report.docx` | `Team 14 — CSRF Presentation Materials (Report).docx` | 2026-10-07 17:25 |
| `report/Cross-Site-Request-Forgery_Team14.pdf` | `Cross-Site Request Forgery_Team14.pdf` | 2026-10-07 09:58 |
| `slides/Team14-CSRF-Revised-Slides.pptx` | `Team 14 — CSRF (Revised Slides).pptx` (current slides) | 2026-10-07 17:25 |
| `slides/archive/Cross-Site-Request-Forgery_Team14_original-2026-09-30.pptx` | `Cross-Site Request Forgery_Team14.pptx` (original slides) | 2026-09-30 23:23 |
| `figures/fig1..fig9_annotated.png`, `figures/annotate.py` | contents of `figures_annotated.zip` | 2026-10-07 17:24 (zip) |
| `figures/fig9_experiment_c.png` | `fig9_experiment_c.png` | 2026-10-07 10:58 |
| `experiments/experiment_c/` (`csrf_gate.py`, `replay.sh`, `render_fig9.py`, `client.log`, `server.log`) | contents of `experiment_c.zip` | 2026-10-07 10:57 (zip) |

## Notes

- Binary files (`*.pptx *.docx *.pdf *.zip *.png`) are stored with Git LFS.
- 本仓库仅作归档 / archive only.
+11 −0
Changes for team-project-csrf/experiments/experiment_c/client.log: 11 added lines, 0 removed lines.
Original line number Diff line number Diff line
client: replay.sh
----------------------------------------------------------------------------------------
C0  control: genuine form, same-origin, token present -> wiener.new@ -> HTTP 200
A1  §3.1  GET /change-email?email=attacker, token dropped, cross-site -> HTTP 405
A2  §3.2B GET …&_method=POST (method override), cross-site        -> HTTP 405
A3  §3.2A POST, token absent, cross-site form                       -> HTTP 403
A4  POST from evil.example.com (same-site sibling), token absent     -> HTTP 403
A5  POST with allowed Origin, token absent (layer 3 alone must hold) -> HTTP 403
A6  POST with allowed Origin, token from another session             -> HTTP 403
----------------------------------------------------------------------------------------
final state: current email: [email protected]
+87 −0
Changes for team-project-csrf/experiments/experiment_c/csrf_gate.py: 87 added lines, 0 removed lines.
Original line number Diff line number Diff line
# csrf_gate.py — Experiment C: one gate, every unsafe method, nothing routes around it
# Run:  python3 csrf_gate.py        (HTTPS on 127.0.0.1:8443, self-signed cert.pem/key.pem)
# Then: bash replay.sh              (replays the attack shapes of §3.1 / §3.2 and a control)
import hmac, logging, secrets, sys
from flask import Flask, abort, request, session, render_template_string

app = Flask(__name__)
app.secret_key = secrets.token_bytes(32)          # per-process for the lab; persist in production
app.config.update(
    SESSION_COOKIE_SAMESITE="Lax",                # layer 1 — enforced by the browser (experiment B)
    SESSION_COOKIE_SECURE=True,
    SESSION_COOKIE_HTTPONLY=True,
)
SAFE = {"GET", "HEAD", "OPTIONS"}
ALLOWED_ORIGINS = {"https://app.example.com"}     # exact origins, never bare domains

logging.basicConfig(stream=sys.stdout, level=logging.INFO, format="%(message)s")
log = logging.getLogger("gate")


def reject(status, layer, reason):
    log.info(f"[GATE] REJECT {status} {request.method:<4} {request.full_path.rstrip('?'):<48} layer={layer:<2} {reason}")
    abort(status)


@app.before_request
def csrf_gate():
    if request.method in SAFE:                     # safe methods never change state (routing enforces it)
        return
    # layer 2a — browser-asserted provenance; page script cannot set this header
    if request.headers.get("Sec-Fetch-Site") == "cross-site":
        reject(403, "2a", "Sec-Fetch-Site: cross-site")
    # layer 2b — exact-origin allow-list; catches sibling subdomains that are same-site
    origin = request.headers.get("Origin")
    if origin is None or origin not in ALLOWED_ORIGINS:
        reject(403, "2b", f"Origin {origin} not in allow-list")
    # layer 3 — session-bound synchronizer token, constant-time compare
    expected = session.get("csrf", "")
    supplied = request.form.get("csrf") or request.headers.get("X-CSRF-Token", "")
    if not expected or not hmac.compare_digest(expected.encode(), supplied.encode()):
        reject(403, "3", "token missing or mismatched")


def csrf_token():                                  # called by the template that renders the form
    return session.setdefault("csrf", secrets.token_urlsafe(32))


@app.errorhandler(405)
def method_not_allowed(_e):                        # layer 0 — the route itself refuses GET
    log.info(f"[GATE] REJECT 405 {request.method:<4} {request.full_path.rstrip('?'):<48} layer=0  method not allowed on state-changing route")
    return "Method Not Allowed\n", 405


# ---- scaffolding for the experiment (login, account page, state) — not part of the gate ----
USERS = {"wiener": {"email": "[email protected]"}}


@app.get("/login")
def login():
    session["user"] = "wiener"
    csrf_token()
    return "logged in as wiener\n"


@app.get("/my-account")
def my_account():
    if "user" not in session:
        abort(401)
    return render_template_string(
        '<form method="POST" action="/change-email">'
        '<input type="hidden" name="csrf" value="{{ t }}">'
        '<input name="email"><button>Update email</button></form>\n'
        'current email: {{ e }}\n',
        t=csrf_token(), e=USERS["wiener"]["email"])


@app.post("/change-email")                        # layer 0: POST only → GET receives 405 from Flask
def change_email():
    if "user" not in session:
        abort(401)
    USERS[session["user"]]["email"] = request.form["email"]
    log.info(f"[APP ] ACCEPT 200 POST /change-email{'':<36} layers 0,2a,2b,3 passed  email={request.form['email']}")
    return f"email changed to {request.form['email']}\n"


if __name__ == "__main__":
    app.run(host="127.0.0.1", port=8443, ssl_context=("cert.pem", "key.pem"))
+48 −0
Changes for team-project-csrf/experiments/experiment_c/render_fig9.py: 48 added lines, 0 removed lines.
Original line number Diff line number Diff line
# render_fig9.py — turn client.log + server.log into one terminal-style PNG (Fig. 9)
import re
from PIL import Image, ImageDraw, ImageFont

MONO = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"
BOLD = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf"
font = ImageFont.truetype(MONO, 22)
bold = ImageFont.truetype(BOLD, 22)

client = open("client.log", encoding="utf-8").read().rstrip().splitlines()
server = [l for l in open("server.log", encoding="utf-8").read().splitlines()
          if l.startswith(("[GATE]", "[APP ]", " * Running"))]

BG, FG, DIM, OK, BAD, HEAD = (24, 26, 36), (220, 223, 228), (130, 135, 150), (120, 220, 140), (255, 120, 120), (140, 180, 255)

def color_for(line):
    if "ACCEPT" in line or "HTTP 200" in line:
        return OK
    if "REJECT" in line or "HTTP 4" in line:
        return BAD
    if line.startswith(("client:", "server:")):
        return HEAD
    if set(line) <= {"-"} or line.startswith(" *"):
        return DIM
    return FG

blocks = [("client: replay.sh  (victim's browser, cookie attached in every request)", client[1:]),
          ("server: csrf_gate.py  (gate decisions)", server)]

lines = []
for title, body in blocks:
    lines.append((title, HEAD, bold))
    for l in body:
        lines.append((l, color_for(l), bold if ("ACCEPT" in l or "REJECT" in l or "HTTP" in l) else font))
    lines.append(("", FG, font))

LH, PAD = 32, 36
width = max(int(d.textlength(t, font=f)) for t, _, f in lines for d in [ImageDraw.Draw(Image.new("RGB", (1, 1)))]) + 2 * PAD
height = LH * len(lines) + 2 * PAD + 40
img = Image.new("RGB", (width, height), BG)
d = ImageDraw.Draw(img)
d.text((PAD, 18), "Experiment C — layered CSRF gate vs. the attack shapes of §3.1 / §3.2   (Flask 3.1, curl, 2026-10-07)", fill=DIM, font=bold)
y = PAD + 40
for t, c, f in lines:
    d.text((PAD, y), t, fill=c, font=f)
    y += LH
img.save("fig9_experiment_c.png")
print(img.size)
Loading