Files

126 lines
5.4 KiB
Python

# annotate.py — draw numbered red boxes + dark callout labels on the lab screenshots.
# Coordinates below are in a 2000x1200 reference space; S scales them to the real 2880x1728.
from PIL import Image, ImageDraw, ImageFont
S = 2880 / 2000
RED = (229, 50, 45)
NAVY = (27, 29, 58)
WHITE = (255, 255, 255)
FONT = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 38)
BADGE = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 34)
STROKE = 6
def sc(v):
return int(round(v * S))
def annotate(src, dst, items):
im = Image.open(src).convert("RGBA")
ov = Image.new("RGBA", im.size, (0, 0, 0, 0))
d = ImageDraw.Draw(ov)
for n, it in enumerate(items, 1):
x1, y1, x2, y2 = map(sc, it["box"])
d.rounded_rectangle((x1, y1, x2, y2), radius=10, outline=RED, width=STROKE)
# badge at the box's top-left corner
bx, by, r = x1, y1, 26
d.ellipse((bx - r, by - r, bx + r, by + r), fill=RED)
tw = d.textlength(str(n), font=BADGE)
d.text((bx - tw / 2, by - 24), str(n), fill=WHITE, font=BADGE)
# callout label
lx, ly = map(sc, it["label_at"])
text = f"{n} {it['label']}"
tw = d.textlength(text, font=FONT)
pad = 16
lbox = (lx, ly, lx + tw + 2 * pad, ly + 38 + 2 * pad)
# leader line from label to the nearest box edge
cx = min(max((lbox[0] + lbox[2]) / 2, x1), x2)
cy = y1 if lbox[3] < y1 else (y2 if lbox[1] > y2 else (y1 + y2) / 2)
ax = lbox[0] if cx < lbox[0] else (lbox[2] if cx > lbox[2] else cx)
ay = lbox[3] if cy > lbox[3] else (lbox[1] if cy < lbox[1] else (lbox[1] + lbox[3]) / 2)
if it.get("leader", True):
d.line((ax, ay, cx, cy), fill=RED, width=5)
d.rounded_rectangle(lbox, radius=12, fill=NAVY + (235,))
d.text((lx + pad, ly + pad - 2), text, fill=WHITE, font=FONT)
out = Image.alpha_composite(im, ov).convert("RGB")
out.save(dst, optimize=True)
print(dst, out.size)
annotate("fig1_original_post.png", "fig1_annotated.png", [
{"box": (576, 594, 1424, 619), "label": "Genuine POST /change-email -> 302 (email changed)",
"label_at": (640, 300)},
{"box": (14, 709, 562, 733), "label": "Session cookie: attached by the browser automatically",
"label_at": (600, 900)},
{"box": (1436, 644, 1780, 690), "label": "csrf token in the form body",
"label_at": (1445, 720)},
])
annotate("fig2_wrong_token_400.png", "fig2_annotated.png", [
{"box": (8, 1060, 566, 1092), "label": "Same POST resent with the csrf value altered",
"label_at": (14, 1112)},
{"box": (576, 844, 1424, 869), "label": "Server rejects it: 400",
"label_at": (640, 960)},
{"box": (1436, 678, 1780, 702), "label": "\"Invalid CSRF token\": the check works for POST",
"label_at": (600, 1040)},
])
annotate("fig3_get_no_token_302.png", "fig3_annotated.png", [
{"box": (20, 949, 1424, 974), "label": "GET /change-email?email=... with NO token -> 302 accepted",
"label_at": (300, 898)},
{"box": (1436, 676, 1780, 836), "label": "302 -> Location: /my-account",
"label_at": (1445, 850)},
{"box": (398, 262, 664, 294), "label": "Email changed - the token was never checked on GET",
"label_at": (690, 262)},
{"box": (20, 699, 1424, 723), "label": "Control: same GET on a lab WITHOUT the flaw -> 405",
"label_at": (300, 648)},
])
annotate("fig4_solved.png", "fig4_annotated.png", [
{"box": (398, 110, 802, 157), "label": "Lab solved by the delivered page",
"label_at": (840, 112)},
{"box": (406, 858, 1292, 982), "label": "A form with no method attribute (= GET) and no csrf field",
"label_at": (414, 1000)},
])
annotate("fig5_no_token_302.png", "fig5_annotated.png", [
{"box": (8, 972, 566, 1000), "label": "Resent POST: csrf parameter DELETED, not altered",
"label_at": (14, 1030)},
{"box": (576, 593, 1424, 617), "label": "Accepted: 302 with no csrf parameter at all",
"label_at": (640, 720)},
{"box": (398, 246, 662, 278), "label": "Email changed with no token at all",
"label_at": (690, 244)},
])
annotate("fig6_labA_solved.png", "fig6_annotated.png", [
{"box": (398, 248, 800, 286), "label": "Victim's email changed by a POST form carrying only email",
"label_at": (406, 192)},
{"box": (1398, 108, 1598, 150), "label": "Lab status: Solved",
"label_at": (1640, 170)},
])
annotate("fig7_plain_get_405.png", "fig7_annotated.png", [
{"box": (20, 367, 1800, 392), "label": "Plain GET /change-email?email=... (no _method) -> 405",
"label_at": (300, 460)},
{"box": (8, 150, 220, 176), "label": "Method Not Allowed: the route refuses GET",
"label_at": (260, 148)},
])
annotate("fig8_method_override_302.png", "fig8_annotated.png", [
{"box": (330, 54, 1330, 80), "label": "Same GET with &_method=POST appended",
"label_at": (660, 190)},
{"box": (20, 417, 1800, 442), "label": "Accepted: 302 - the override turns the GET into a POST",
"label_at": (300, 700)},
{"box": (398, 238, 662, 266), "label": "Email changed",
"label_at": (690, 236)},
{"box": (20, 367, 1800, 392), "label": "Control: no _method -> 405 (Fig. 7)",
"label_at": (1300, 700)},
])
annotate("fig9_labB_solved.png", "fig9_annotated.png", [
{"box": (398, 104, 800, 143), "label": "Lab solved",
"label_at": (830, 103)},
{"box": (406, 850, 1350, 932), "label": "Delivered page: a top-level navigation to the GET URL carrying _method=POST",
"label_at": (414, 955)},
])