The security header I was sure was useless

Share

Preface

This is a small story about a security header I ran into at work, decided was pointless, and then slowly realized I'd completely misunderstood. I'm a 3D artist by trait, not a developer developer, let alone a security researcher, so if I've gotten something wrong here, please tell me. I'm writing it down mostly because the moment it finally clicked was very satisfying, and maybe it'll click for someone else too.

The setup

I was building a site that embeds the statistics pages of some apps we'd made. Simple enough, until a few of those pages flat-out refused to show up. Empty frames, angry console messages, the works.

The culprit was a header called X-Frame-Options, which basically tells browsers "do not load me inside a frame." Since these were our own apps, the fix was easy: change the header, allow the embed, done. Lunchtime!

The shower thought

Then that evening it started bugging me. Isn't this header... completely useless? It's just a server politely asking not to be embedded. If I really wanted to, couldn't I write my own client, pretend to be a normal browser, and embed the page anyway? Who's going to stop me?

So I looked it up (fine, I asked Claude). The answer was both "yes, obviously" and "you've missed the point entirely," which is my favorite kind of answer.

Here's the bit I had backwards. The server doesn't refuse to serve an embedded page. It sends the exact same HTML either way. It's the browser that reads X-Frame-Options and decides not to render the page inside a frame. The enforcement lives entirely on the client.

My first reaction was the obvious one: what a stupid design. A security control that only works if the client agrees to cooperate? That's not security, that's a suggestion.

Why it's actually fine (and I'm the silly one)

Blocking the embed server-side was never the goal. That's the part I didn't get. The whole thing rests on the assumption that real (I mean most) people use normal browsers that honor the header, and that assumption isn't a weakness, it's the entire design.

The attack this defends against is clickjacking. Picture a malicious page that frames a real, logged-in site, say your bank. It makes the frame invisible, or hides it under some innocent-looking decoy buttons, and tricks you into clicking "confirm transfer" while you think you're clicking "play video." It works precisely because it's your browser, with your session, rendering the real site.

(And no, the attacker can't just read what you type into that frame. The same-origin policy already stops one site from reading another site's frame contents, keystrokes, or cookies. Clickjacking is about stealing clicks, not input. Don't quote me on the exact wording, but that's the gist.)

Now flip my original "genius" idea around. If I write my own client and ignore the header, what have I actually won? Nothing. I'm fetching public HTML I could've grabbed with a plain HTTP request anyway, and I don't have any victim's session. The one client that would actually need to disobey the header to pull off the attack is the victim's browser, and that's exactly the client I can't control.

So who's the idiot for turning off iframe protection in a browser? Turns out it's whoever does it, because they'd be sabotaging the exact thing protecting them.

Conclusion

The server was never trying to hide anything. The browser is trying to protect you, by honoring a server's request not to have its sensitive pages framed by someone else. The "client decides" part that looked like a flaw is the whole point.

Anyway, that's my little realization. If I've butchered some detail, please get in contact, I'd much rather be corrected than confidently wrong. Thanks for reading.