Pocketmic
A buy-me-a-coffee-style tip jar for indie podcasters. Listeners leave a few bucks and an optional thank-you note that gets emailed to the host. No response, no preview — but the thank-you redirect carries a tiny performance metric in its headers.
The Scenario
Pocketmic is a side project from two ex-audio-engineers who got tired of
watching their favorite indie shows burn out from no income. The pitch is
simple: a single page per show, a few preset tip amounts, an optional
note, and the host gets a thank-you summary email at the end of every
day. The team shipped the daily-digest fast in beta, then a regular
asked if hosts could see notes in real time. So they switched to
per-tip emails — synchronous, blocking the response, rendered through
the same Jinja2 layout the team uses for the daily summary.
Challenge Intel
Synopsis
The tip form's "note" field is rendered through Jinja2 into a thank-you email for the podcast host. The HTTP response never shows the rendered output, but the redirect carries a Server-Timing header with the real render duration — a side channel that turns blind SSTI into a binary timing oracle.
What It Is
POST /tip/<slug> takes a name, amount, and an optional note. The handler builds an HTML email body by rendering a Jinja2 template string that splices the note in raw, hands the result to a local aiosmtpd stub on 127.0.0.1:1025 synchronously, and only then issues a 302 to /tip/<slug>/thanks. The listener sees no rendered output. The render duration is real (gunicorn blocks on render → SMTP send → redirect) and the team surfaces it back to the client via the standard Server-Timing response header on the redirect: `Server-Timing: render;dur=<ms>, send;dur=<ms>, total;dur=<ms>`. They use this header for their own performance dashboards. That header turns the blind SSTI into an oracle. Payloads like `{% if open('/flag.txt').read()[0]=='W' %}{{ range(8000000)|last }}{% endif %}` make the render branch expensive only when the guess is right. The player walks the flag byte-by-byte by submitting one tip per byte and reading the dur= value from Server-Timing. No __subclasses__ chain needed — Jinja2's default globals don't include `open`, so the app intentionally exposes `open` to the template context (the team uses it elsewhere to attach signed receipt PDFs). The vulnerability is the template-string splice + that globals exposure.
Who It's For
Players comfortable with the basic Jinja2 SSTI primitive who want to learn the blind-with-timing-oracle variant. Familiarity with binary or per-character extraction over a side channel helps.
Skills You'll Practice
- Spotting that user input is being rendered server-side even when no output is reflected
- Using Server-Timing as a binary oracle when the response body is opaque
- Writing a Jinja2 conditional whose branch cost depends on the guessed byte
- Per-byte flag extraction across many small HTTP requests
What You'll Gain
- Confidence that 'I can't see the output' isn't enough to call an SSTI safe
- Pattern for converting any blind server-side bug into an oracle via a measurable side effect
- A reusable timing-conditional payload skeleton