A Standards-Detour Into the Web’s Security Model

This year, in collaboration with Noam Rosenthal, I spent a good deal of my professional life trying to standardize a new web platform feature: a way to modify the intrinsic size and resolution of images. We succeeded. But the road there was a learning experience — not about image rendering, but about the core privacy and security model of the web.

Our proposal seemed benign on its face. By default, an embedded image displays at the size of its pixel dimensions: an 800×600 image displays at 800 CSS pixels by 600 CSS pixels, a default "density" of 1×. The intent was to give image hosts—who often can’t control the HTML or CSS that renders their resources—a way to have embedded images reflect an intended display size or resolution.

The solution we landed on:

  • Have browsers read metadata within image resources that declares an intended display size and resolution.
  • By default, respect that metadata, while allowing it to be overridden via CSS (image-resolution) or markup (like srcset’s x descriptors).

This built on an existing pattern—the way image-orientation handles EXIF orientation. We felt good about it. Then Anne van Kesteren, an editor of the HTML spec, disagreed. The proposal, he argued, would violate the Same-Origin Policy. image-orientation, it turned out, had the same problem and needed a rethink.

I’d previously dismissed the Same-Origin Policy as the source of CORS errors and other mundane frustrating moments. Now it was blocking a work initiative, and I had to explain why to people who knew even less about web security than I did.

The Policy Is a Collection of Defaults

What follows is what I learned, and it’s not a single rule. The Same-Origin Policy is more of a philosophy, evolved over time and inconsistently implemented, centered on a simple principle: the fundamental security boundary on the web is the origin. Same-origin resources get full and free interaction with each other. Cross-origin resources, not so much. Not at all, is more like it, for certain operations.

The specific boundary that matters is reading. By default, a website may:

  • Write across origins—sending POST requests via forms, for example.
  • Embed cross-origin resources, from images and iframes to fonts, which will render for visitors.
  • Not read anything about those cross-origin resources, without explicit permission granted by CORS.

The reason for this asymmetry is the one that finally made it click for me: as end-users, we all see different webs. A given URL—like a bank’s homepage—renders one way when I’m logged in, and quite another when I’m not (or am someone else entirely, with different cookies). If distant, unrelated sites could make requests to that URL from within my browser, append my cookies, and then read the results, they’d see the web through my eyes. That is a very bad thing. The broad defaults exist to prevent that scenario.

The "Same-Origin Policy" is the name, shorthand, for those defaults: cross-origin reads are forbidden, because cross-origin reads let one site spy on its visitor’s other browsing contexts.

The Attack Was Minuscule, But Real

What did any of that have to do with image sizes? Let me construct an attack to show you.

Consider an image URL, https://coolbank.com/hero.jpg, that serves a different resource depending on whether the person fetching it is logged into coolbank.com. The logged-in version contains EXIF resolution information; the logged-out version has none. Now, consider an evil page that embeds that image. First, it checks the intrinsic size of the image on the page. Then, it turns off the EXIF-based sizing with image-resolution: none and checks again. Even if CORS blocks reading a single pixel of actual image data, the page has successfully read a piece of metadata from the cross-origin image—the very fact of there being EXIF information to strip off. A single bit of information, but a clean cross-origin read, and enough to infer a login state.

At first glance, compromising to satisfy that scenario seems absurd. But as Jen Simmons once put it:

This one of the best & most important things about THE WEB. Go to a website, it’s safe from malware. Download an app, you are at risk. You can’t download random apps from random places. You can go to random websites, and expect to be safe. We must fight to keep the web like this. https://t.co/xKQ5vVNCaU

— Jen Simmons (@jensimmons) March 11, 2020

The web requires users to run arbitrary, untrusted code from across the internet all day, every day. The security model permits this only by enforcing absolutes: no cross-origin reads, no matter how seemingly harmless or insignificant. A tiny hole is still a hole, and any exploitable surface is a potential attack.

Fixing It

The fix to our spec was straightforward, once the problem became clear: make the EXIF-derived sizing and orientation un-readable across origins by making it un-turn-off-able. In cross-origin contexts, if an 800×600 image declares it should be displayed as 400×300, it simply behaves as a 450×300 image, no matter what the embedding context tries to do. No JavaScript, CSS, or markup can change that.

That realization doesn’t just patch one proposal; it unlocks an entire mental model for the web’s related security mechanisms:

  • CSRF attacks abuse the fact that cross-origin writes are allowed by default, and that an API isn’t careful about how it handles uncalled-for POST requests.
  • CSP exists precisely because cross-origin embeds are allowed widely, creating openings for cross-site scripting vectors.
  • The newer alphabet of restrictions—COOP, COEP, CORP, CORB—seeks to shut down cross-origin interactivity more completely, addressing historical inconsistencies in the Same-Origin Policy, and achieving “cross-origin isolation,” a necessary state in the era of Spectre, where merely loading cross-origin data can be turned into reading it.

So: security and privacy on the web aren’t happy, hindering accidents. They’re the point. The default restrictions on cross-origin interaction protect an assumption so fundamental it’s easy to miss: you cannot see what your visitors see. Any behavior that breaks that contract—no matter how slight—is a policy violation worth closing. I know that now. And in 2020, I accidentally got a lesson in just how the web enforces it. Here’s to carrying that forward into the new one.