Quoted
An indie reading-highlights service for power-readers. Users can customise the layout of their weekly digest email through a small sandboxed templating language — the kind of feature a careful developer adds, then tries to lock down.
The Scenario
Quoted is a four-person team out of Edinburgh who built the tool they
wished existed: a calm, opinionated home for the sentences you underline
on your Kindle, save in Pocket, or pull out of an RSS feed. The weekly
digest is the main product loop — Sunday morning, a single email with
six highlights you'd otherwise forget. Power-users kept asking to
rearrange the layout, so Cate (their backend lead) shipped a small
templating surface in /settings/digest-template last quarter and went
to some lengths to lock it down before launch. She read the Jinja2
sandbox docs, denied the obvious class-walk attributes, and called it
done. The QA tasks for that PR were closed in an hour.
Challenge Intel
Synopsis
The digest-template editor uses Jinja2's SandboxedEnvironment with an overridden is_safe_attribute that bans the usual class-walk dunders (__class__, __bases__, __subclasses__, __mro__, mro). The block is real — the canonical {{ ''.__class__.__mro__[1].__subclasses__() }} payload returns a SecurityError. But the override does not block __globals__, and Jinja2 ships several callable globals (lipsum, cycler, joiner, namespace) whose function/class objects expose __globals__ directly. From there it's a one-hop to os.popen.
What It Is
Logged-in users have a /settings/digest-template page with a textarea and a "Preview" submit. The submitted template is rendered through a SandboxedEnvironment subclass whose is_safe_attribute returns False for the five class-walk dunders ('__class__', '__bases__', '__subclasses__', '__mro__', 'mro') and True for everything else — intentionally relaxing Jinja2's default UNSAFE_FUNCTION_ATTRIBUTES set that would normally also block __globals__. The player must recognise that the blocklist is incomplete and reach a Python object through a non-class-walk path. The cleanest payload: {{ lipsum.__globals__['os'].popen('cat /flag.txt').read() }} `lipsum` is a Jinja2 utility function; `lipsum.__globals__` is the `jinja2.utils` module dict; `['os']` is the imported `os` module; `popen('cat /flag.txt').read()` returns the flag. The rendered preview is reflected back in the page, so this is in-band exfil. Equivalent payloads via `cycler.__init__.__globals__.os.popen(...)` or `joiner.__init__.__globals__.os.popen(...)` work too — any Jinja2 callable global with `__globals__` reachable via its function object will do.
Who It's For
Players who have hit the easy unsandboxed Jinja2 SSTI case (where `{{ ''.__class__.__mro__[1].__subclasses__() }}` walks straight to `os.popen`) and want to learn what to do when the sandbox actually has a blocklist. Prereq: comfort with Python attribute traversal and an awareness that `__class__` is not the only path to executable Python objects.
Skills You'll Practice
- Reading a sandbox's allow/block-list and finding the gap
- Reaching Python module objects through callable globals (`lipsum`, `cycler`, `joiner`, `namespace`)
- Using `__globals__` to access imported modules without a class walk
- Invoking `os.popen` from a function-object's globals dict
What You'll Gain
- Mental model: sandbox blocklists fail by omission, not by being weak
- A second SSTI primitive (the `lipsum.__globals__` family) for when the class walk is blocked
- Reading Jinja2's sandbox source to know what's actually filtered vs documented