A beginner CLI under PROJECTS/beginner that decodes JSON Web Tokens and
audits them for common mistakes: the alg none downgrade, weak HMAC
secrets, the RS256 to HS256 confusion attack, missing or long
expirations, and secrets carried in the payload.
Standard library signatures (no PyJWT), Typer plus Rich CLI, 60 tests,
ruff and mypy clean, pylint 10/10, formatted with the repository YAPF
config, and a full learn/ folder following the template. Demo tokens are
generated at runtime so no credentials appear in the source.
_PATTERN_RULES list had XXE_INJECTION (0.82) after XSS (0.80) and
CRLF_INJECTION (0.65) after PATH_TRAVERSAL (0.60), placing higher-
scored rules below lower-scored ones. Reordered to strict descending
order and updated the module docstring to match. No change to
scoring behavior — score_request() takes max + boost regardless
of list position.
overrides.vite was pinned to npm:rolldown-vite@7.2.5 while
devDependencies and pnpm-lock.yaml both resolved 7.3.1. The mismatch
can produce ERR_PNPM_OUTDATED_LOCKFILE on --frozen-lockfile installs.
Updated override to 7.3.1 to match the resolved version throughout
the lockfile. No lockfile edits required.
Moved entrypoint.sh copy target from /app/entrypoint.sh to
/entrypoint.sh in the dev Dockerfile. The dev compose bind mount
./backend:/app overlays /app entirely; placing the entrypoint above
that boundary ensures the image-baked script is never shadowed by
host directory contents. No startup failure was reproduced — this
is a preventive hardening change.
urllib.parse.quote(path, safe='/?&=') was re-encoding % to %25 in
already-encoded attack paths (XXE, XSS, traversal payloads). Replaced
with a manual first-? split: path component encoded with safe='/%\'
(preserving existing percent sequences and backslash traversal vectors),
query string passed through untouched. Covers all 53 attack payloads
across 7 simulation modes. Inline comments removed from XXE_PAYLOADS
to match the style of all other payload lists.
The final branch of _XXE_INJECTION was missing % separators between
hex pairs. Pattern %(?:25)?53(?:25)?59... never matched because after
consuming %53 the next sub-pattern expected 5 or 2, not %. Fixed by
inserting % anchors between each pair so both single-encoded
(%53%59%53%54%45%4D) and double-encoded (%2553%2559...) SYSTEM strings
match correctly. All existing XXE branches are unaffected.
M4:
- Add 01-read-the-hex (hex-reading) and 02-find-the-entry (elf-anatomy)
gradeable challenges over the gate binary, completing the five-module
pedagogy
- Swap in-memory progress for a SQLite ProgressStore behind the existing
Protocol; routes untouched, migration-free, thread-safe
M5:
- Self-host Docker: prod (nginx + uvicorn) and dev (nginx + Vite HMR) compose,
justfile, Dockerfiles, and nginx configs; zero-config clone-and-run
- learn/ teaching docs (00-04) and public README
- One-shot install.sh and uninstall.sh
M3 React web app over the existing FastAPI: hex viewer, disassembly pane, section map, strings, challenge runner with reveal-after-solve, and progress. Scaffolded from the open no-auth react-scss template; components kept self-contained for CertGames extraction.
M3.5 static-analysis depth, all still no-execution and KAT-traced to the gate binary: PLT/GOT import resolution (plt.py), RIP-relative cross-references and callers (xref.py), basic-block control-flow graphs (cfg.py), and prologue-scan function discovery for stripped binaries (discover.py). Adds the 06-stripped-gate challenge. The API now resolves call names, exposes rip targets, and serves /cfg, /xrefs, and discovered functions.
M2 API hardening from a read-only audit: challenge-loader error isolation, a request body-size middleware, session length bounds, CORS, and the disasm gate annotation gated behind solved state so it no longer pre-solves the challenge.
96 backend tests green; frontend typecheck, build, and lint clean.
Framework-free analysis engine (hex dump, hand-rolled ELF64 parser,
capstone x86-64 disassembly, string extraction, byte patching, challenge
grading) behind a thin read-only FastAPI layer over curated pre-compiled
challenge binaries.
No-execution security posture: the backend only reads and decodes bytes,
never runs a target; patch challenges are graded by static byte diff.
Reveal-after-solve pedagogy: challenge source is returned only on a
correct submission. Input size-caps enforced at the API boundary.
M0 research (ELF format, x86-64/capstone, pedagogy, no-execution) is
cross-checked against a real compiled binary. 63 tests green, including
an ELF parser cross-check against pyelftools and disassembly that matches
objdump instruction-for-instruction.
connect-scan: raw non-blocking connect() + poll(POLLOUT) + SO_ERROR via std.os.linux (std.Io.net connect-with-timeout is unimplemented in 0.16); own sized std.Io.Threaded with N io.async workers off a mutex+token-bucket dispenser; --backend connect / --connect, --concurrency, --connect-timeout.
cloud/VM detection: two-socket AF_PACKET self-probe (send on one, observe the tagged frame on a second - single-socket cannot see its own send); --backend auto checks CAP_NET_RAW + egress and auto-falls-back to connect with a notice; forced raw backend disables fallback and hard-fails; zero-response raw scan hints --connect.
raw IPv6 SYN: packet.Addr union result + RFC5952 v6 render; Ipv6Hdr + pseudoChecksum6/tcpChecksum6; 128-bit SipHash cookie (generate6/seq6); parseIpv6, resolveSrcIp6 (/proc/net/if_inet6), defaultGateway6 (/proc/net/ipv6_route); Engine6 (bounded prefix, RFC6890 reserved floor, ::/0 reject, host cap); SynTemplate6; classifyTcp6 + ICMPv6 type1; ndp.zig NDP neighbor resolution; runV6Scan dispatch; connect-path v6 rides the connect engine. v6 scope = TCP SYN.
240 tests Debug+ReleaseSafe (-Dxdp on/off); KAT + two-namespace netns e2e proven on the wire; two read-only audits, 0 Critical/High/Medium, all findings fixed in-phase.
OS-realistic SYN templates (Linux/Windows/macOS/masscan JA4T option chains
plus varying IP-id), Poisson jitter, source-port rotation (RX recomputes off
the reply, zero classify changes), scoped RST-suppression (iptables plus
ambient CAP_NET_ADMIN, self-healing delete-before-insert), decoys (bogon-free
RND, real probe always sent), and FIN/NULL/Xmas/Maimon/ACK/Window flag scans
with per-mode cookie matching plus State.unfiltered.
Dead theater (idle scan, fragmentation, TTL, MAC/source-route spoof, badsum)
omitted and documented as obsolete with citations.
Cursor-based TX emission with token refund plus cold-start pacing; non-stealth
path byte-identical to M7.
Turn the scan from sequential TX-then-RX into two io.concurrent engines on
std.Io.Threaded, fixing the M4 bug where replies arriving during transmit were
dropped and the RX deadline (anchored at socket-open) expired before draining.
The main thread is the non-blocking Io.Queue consumer and dashboard renderer.
- rx: Receiver keyed off a shared tx_done atomic; new pure planDrain anchors the
drain window at TX-completion, not socket-open, killing the quiet-gap early exit
during a slow TX. Hard-cap safety backstop anchored at drain start.
- output (new): cache-line-padded atomic Stats, truecolor/256/none palette
(violet gradient, neon-green, muted-gray), in-place multi-line live dashboard,
Unicode results table, NDJSON to stdout with visuals on stderr, full
NO_COLOR/CLICOLOR_FORCE/COLORTERM/--color chain plus a narrow-terminal fallback.
- tx: wall-clock deadline so a stalled TX ring can no longer hang the scan.
- cli: banner recolored to the violet gradient and routed through the color chain.
- targets: add 192.88.99.0/24 (6to4 relay anycast) to the RFC 6890 exclude floor.
78/78 unit tests pass under Debug and ReleaseSafe; proven end-to-end (open/closed,
dedup, dashboard, NDJSON) in an unshare -r -n netns under ReleaseSafe.
classify.zig pure frame parser: SYN-ACK -> open, RST/ACK -> closed,
ICMP type-3 {1,2,3,9,10,13} -> filtered, every verdict gated on a
wrapping SipHash cookie re-check (ack == seq+1 for replies, inner seq
== cookie for ICMP) so spoofed/stale frames are dropped state-free;
byte-offset readInt parsing, fully length-guarded. dedup.zig power-of-2
open-addressed (ip,port) set, single-owned allocator, grows at load
factor with a bounded probe (never wedges). rx.zig receive engine
generic over an injected Source + Sink (proven over hand-built frames),
Io.Queue found-host handoff across producer/consumer fibers, and a
privileged AF_PACKET Receiver (poll-bounded drain with a hard deadline,
ignore-outgoing, EINTR-retry, errno-gated, fail-closed on missing
CAP_NET_RAW). scan subcommand shares one per-run cookie across the TX
template + RX validation; consumer uses a dedicated allocator to keep
the producer/consumer fibers race-free. netutil.zig factors the shared
arg-parse/iface-resolve helpers out of txcmd. Proven end-to-end with a
live SYN scan inside a unshare -r -n netns over a veth pair
(open/closed against a real listener, dedup verified). Green Debug +
ReleaseSafe, leak-free, 61 unit tests.
PACKET_TX_RING (PACKET_MMAP, TPACKET_V2) over AF_PACKET with
PACKET_QDISC_BYPASS: socket + version + TX_RING setsockopt + mmap'd ring,
errno-gated, fail-closed on missing CAP_NET_RAW. ratelimit.zig integer
token bucket (banked-nanoseconds, no float, saturating, exact KATs).
template.zig SYN frame template: stamp dst IP/port + SipHash seq, recompute
IP + TCP checksums (reuses the M1 RFC 1071 + cookie code, both self-verify).
afpacket.zig pure Ring slot bookkeeping (inline tpacket2_hdr, comptime size
assert, reserve/fill/atomic-status, self-defending bounds, kernel-drain
reuse) split from the privileged Backend so the hot-path accounting is
unit-tested over an in-process buffer. tx.zig transmit engine generic over
an injected Sink + Clock: the M2 cyclic-group bijection is proven to survive
stamp -> ratelimit -> submit end-to-end via a fake sink + fake clock.
Privileged tx subcommand for on-hardware benchmarking (setcap + tcpdump/perf).
Green Debug + ReleaseSafe, leak-free, 45/45 tests.
SIMD @Vector internet checksum proven byte-equivalent to scalar across
every length 0..256 plus the 0xb861 IPv4 KAT. RFC 1624 incremental
update proven against the RFC section 4 worked example
(0xDD2F/0x5555/0x3285 -> 0x0000) plus 4096 random full-recompute trials.
Reusable TCP pseudo-header checksum helper; smoke.zig now reuses it.
New cookie.zig: stateless SipHash64(2,4) SYN-cookie over the 4-tuple,
full-128-bit randomSecure key, u64 generate with write-site u32
truncation, ack == cookie +% 1 wrapping validation. Reproduces the
published SipHash reference vector plus a pinned golden cookie KAT.
All green Debug + ReleaseSafe (14/14). Version bumped to 0.0.0-m1.