The Hotwire attack surface

Free preview 4 min to read

A background job renders a partial. Nobody is logged in inside that job, because nobody is anything inside a job. The partial asks for the current user, gets nil and renders the signed-out version of a card. Action Cable sends that same HTML to every browser subscribed to the stream.

That isn't a Turbo bug. Turbo did what you asked. The problem is that "render a partial" stopped meaning "render it for the person who asked", and nothing in the code you wrote says so.

Hotwire is full of moments like that. Work moves off the request you were picturing and onto something else: a job, a hover, a DOM merge, a frame that fetches its own URL. Each is a place where an assumption you carried from plain Rails quietly stops holding.

What this course isn't

The mid-level course "Turbo Streams: security" already covers securing Turbo Stream broadcasts, and it owns one specific question: who may receive a stream. It covers signed stream names and why a signature proves origin, not permission. It shows why anyone can decode a signed name without the key, and why the page you render is the real authorization boundary. And it shows how to scope a stream down to a single record.

All of that stays there. This course assumes you've already decided correctly who may subscribe, and asks the next question: given a legitimate subscriber list, what can still go wrong?

Four places the request isn't what you think

Here are the four moments worth naming up front, because the rest of the course builds on them:

Moment What is missing or different
A broadcast job No request, no session, no current user
A hover A real GET fires before any click
A morph The DOM is merged, not replaced
A frame A URL you may not control renders inside your page

None of those four is exotic. This application does three of them on ordinary pages, and the fourth is one attribute away.

What this course covers

Lesson 2 is the center of the course: what a broadcast renders without. You'll see the real rendering path in the installed turbo-rails gem and watch a partial render with an empty session and a hostname of example.org. Then you'll learn the one rule that falls out of it. That lesson carries the coding challenge.

Lesson 3 covers Content Security Policy with Hotwire. It covers what a nonce protects, why an importmap application can't boot without one and how unsafe-inline creeps into a policy. It also shows how to evaluate a nonce generator honestly instead of by rumor. This application has a real policy, and we read it.

Lesson 4 covers CSRF: how Turbo finds the token and sends it, why a fetch you write by hand has to do that work itself, and what breaks when a form ends up inside a cached fragment.

Lesson 5 covers prefetch. Turbo requests pages you hovered over and never clicked. If any GET in your application changes state, it's now a mutation triggered by mouse movement, and the fix isn't a Turbo setting.

Lesson 6 covers morphing: what a page refresh with morph does to the DOM a user is standing in the middle of, and how element identity is decided. It also shows why "the node survived" is a security statement, beyond a UX one.

Lesson 7 puts the pieces together on frames, origins and the DOM as untrusted input, and leaves you with one checklist.

Start with the broadcast. It's the failure that ships silently and reaches the most people at once.

Lesson 1 of 7 · free

Keep going with lesson 2

What a broadcast renders without

Shows that broadcasts render through ApplicationController.render with no request, session or user, and why refresh broadcasts avoid it.

A free account also gives you the quizzes, your progress and notes, and the discussion.

Type to start searching...