🏠 back to Observable

Clickjacking attacks and notebook security?

I have recently been looking into the idea of using oauth2 on observablehq to access third-party resources. Essentially, building an API client to sign-in the user. But I was wondering about the security setup here.

Some background on a click jacking attack:

In other threads, I’ve seen discussion that because the notebook runs in a sandbox, it is secure. But the attack above describes someone hosting your notebook “invisibly” on their own site, and getting the user to interact with it unknowingly, making use of any persistent session data that the user has previously made available to your notebook when they interacted with it in a legitimate context.

This means that if we create controls in our notebooks that are able to setup an oauth2 context by retrieving access or id tokens or other secrets, we need to be careful about where they are persisted, if at all.

Has anyone explored this idea and whether a secure solution is possible?


Each author’s or team’s notebooks are hosted under a separate subdomain (e.g. “https://keystroke.static.observableusercontent.com”), which means that storage is isolated for each notebook owner. Is there a specific attack that you envision?


I described the attack in my original post, but the flow would go something like this:

You write a notebook with an oauth2 control to retrieve an identity token to authenticate a user. Technologies like single-sign on will store this auth session data associated with your personal sub domain. This means another notebook could embed your static content subdomain in an invisible iframe that would load any existing auth session with your sub domain, and they could render this invisibly on their page, making the user thinking that they are click on a “save the puppies” button in your notebook, but really they had the “delete my account” icon from your notebook positioned right over it and transparent. This is the click jacking attack and the link in my post shares additional example like how it was used to trick users to disable their flash security settings and take over their computer.

I don’t know what iframe headers can or need to be set to help prevent this, if it’s even possible. The embed option for notebooks will host all the notebook iframes from a common domain on observable, so we should definitely avoid that as a valid redirect / reply URL in our oauth2 configurations, which means using oauth2 widgets from this site should only be done by direct module import of the code on your own custom domain with iframe options disabled or set to same origin.

I’m not a complete expert on all the subtle points and setup here, so wanted to get other folks who have looked into this to weigh-in.


This applies to the separation of notebook code and Observable UI.

And I’d like to add a caveat: Within the context of a notebook, you’re only as secure as the code that you import. At any point some notebook in your import chain could alter its code to, e.g., track storage or keydown events. And even if you pin all your direct imports, any transitive imports will still include the latest version (unless they are also pinned, which is rare).

As far as I’m aware, @mbostock has been wanting to implement automatic version pinning (similar to npm/yarn lock files) for quite some time, but there is no concrete roadmap or date yet.

You need to redirect to observablehq.com/… for Oauth 2.0 but you also have the state parameter and the client_secret to avoid other sites using your application credentials.

I have a Reddit Oauth 2.0 with client secret integration working here: The Reddit Research Assistant / Tom Larkworthy / Observable

Observablehq.com sets ‘frame-ancestors ‘none’’ so it’s not possible to put observablehq.com in an iframe. You can use the embedding though:-

<iframe src="https://observablehq.com/embed/@tomlarkworthy/ui-linter?cells=viewof+suite"></iframe>

So this would be where I would investigate more.

Thanks Tom! I was actually using your Minecraft server notebook to test, and can confirm that my oauth2 session could be click-jacked to delete my server :open_mouth:

For the oauth2 clients, I was finding that the reply address was my static content worker js file (absolute URI must be used for reply address, Microsoft auth provider won’t allow wildcard redirect URIs) and I couldn’t find info about the worker URL and how it was generated and whether it would be static. I also messed-around with using the device auth flow, and using flows for SPA public clients with no backend which was working but leave a persistent auth session, so I need to think more about the flow as I’m not familiar with iframe and CSP header stuff to understand if its secure to be serving oauth2 session from the observable domains (I could also look into hosting my own site with the notebook module and only set those up in my oauth app reply addresses).

yeah thats a bit worrying. To clarify you were hijacking an /embed page?

I sent you a private message with more details.

1 Like

So a quick fix for “@tomlarkworthy/firebase” was to break the user reference when not served from the main domain:

user = {
    if (!html`<a href>`.href.startsWith("https://observablehq.com"))
        throw new Error("Not allowed to be embedded");

Without a reference to the auth payload (user) no privileged operations can take place. (it turned out @endpointservices/login naturally broke itself on embed anyway.)

I guess this mitigation would work for any notebooks carrying privileged cookies/storage. It not ideal as the notebook still has the credentials loaded, but if the application is broken when embedded then clickjacking cannot take place. So as long as the application depends on authentication as suggested here Authentication Simulator / Mike Bostock / Observable you can quite easily hold back the application from loading based on top level URL breaking the auth state.

I think it probably long term we should host creds on a dedicated domain but I think thats enough for now WDYT?

EDIT: I credited you in firebase notebook change log. A really awesome find thanks again.

1 Like