ipfs/in-web-browsers

Sandbox resources loaded via a path gateway

lidel opened this issue · 7 comments

lidel commented

Motivation

Websites loaded via path gateway are able to access cookies and storage of the entire domain. While we are moving to subdomain gateways (#89), requests made to path gateways will continue to lack origin isolation between content roots. Some will be redirected to subdomain ones, but we should look into other means of improving the situation.

TL;DR

  1. subdomain gateways will provide Origin isolation
  2. path gateways do not
  3. Various headers can be leveraged for limiting what can be used on the origin of path gateway.

Headers to investigate

Clear-Site-Data header

The Clear-Site-Data header clears browsing data (cookies, storage, cache) associated with the requesting website. It allows web developers to have more control over the data stored locally by a browser for their origins.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data

We could leverage Clear-Site-Data header and send a hint to user agent to clear any preexisting cookies and storage. This is a "nuclear option", but could incentivize users to switch to subdomain gateways when access Web APIs relying on Origin is required.

Note: this requires native subdomain support (ipfs/kubo#6498) to land first.

To purge cookies and storage without reloading any contexts, below header would be returned with every response from /ipfs/{cid} and /ipns/{foo} paths:

Clear-Site-Data: "cookies", "storage"

Content-Security-Policy

Disabling JS and various security features.

Ref. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy

Highlights:

  • sandbox directive may be the most elegant way, it would apply the same logic as <iframe> sandbox for entire page.

Prior art:

  • blogpost: https://blog.web3.storage/posts/badbits-and-goodbits-csp-in-w3link
  • web3.storage returns:
    • content-security-policy: default-src 'self' 'unsafe-inline' 'unsafe-eval' blob: data: https://*.w3s.link https://*.nftstorage.link https://*.dweb.link https://ipfs.io/ipfs/ https://*.githubusercontent.com https://tableland.network https://*.tableland.network ; form-action 'self'; navigate-to 'self'; connect-src 'self' blob: data: https://*.w3s.link https://*.nftstorage.link https://*.dweb.link https://ipfs.io/ipfs/ https://*.githubusercontent.com https://tableland.network https://*.tableland.network ; report-to csp-endpoint ; report-uri https://csp-report-to.web3.storage
    • reporting-endpoints: csp-endpoint="https://csp-report-to.web3.storage"

Feature-Policy

Another way of disabling various APIs and behaviors

Ref. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy

TODO

  • create PoC proxy to validate the idea
    • setting Gateway.HTTPHeaders in go-ipfs config may be enough for initial tests
  • document behavior in Firefox (
  • document behavior in Chromium
  • document behavior in Safari
lidel commented

Blocking cookies is a nuclear option that may break some deployments behind reverse proxies that pass all headers. We would need NoWebSecurity flag (false by default) in case someone really wants to disable things like Clear-Site-Data in contexts that provide no origin isolation.

lidel commented

Something to be aware of: executionContexts and wildcard directives are not recognized by Chromium (bug-898503)

lidel commented

Interesting fact: w3c spec suggests a discrepancy between clearing cookies and storage:
https://www.w3.org/TR/2017/WD-clear-site-data-20171130/#grammardef-storage
https://www.w3.org/TR/2017/WD-clear-site-data-20171130/#grammardef-cookies

According to the spec storage is supposed to be purged for origin, while cookies cleanup is listed as origin + all origins based on subdomains of the current one.

I tested behavior in Chromium 76 and Firefox 74 to see if it negatively impacts subdomain gateways in go-ipfs (ipfs/kubo#6096). This does not seem to be the case: subdomain cookies are not purged if Clear-Site-Data is present in localhost/ipfs/$cid 301 response.

lidel commented

Sidenote: we could reuse this on locked-down subdomain namespace ipfs/kubo#7318 (think cors.dweb.link/ipfs/) that is optimized for loading data/subresources and not website roots:

  • path gateway
  • has liberal CORS (*)
  • has locked-down web APIs like storage

This could be greatly deduplicated and simplified, ideally, we would introduce everything (hardening path gateways; hardening/removing CORS from subdomains; support for long CIDs; support for CORS without compromising any local storage) in a single PR/release.

I would like to suggest taking the restrictions on Permissions-Policy (which was called Features-Policy when this issue was opened — @lidel mentions it at the top) further (spec, mdn). When you load a random web page, a number of features are available generally gated by some kind of permission or interaction. That is often okay (or at least it's understood to be a managed risk) but it is open to phishing or other forms of social engineering.

I think that in a pathed context, we can restrict this further and make those powerful capabilities unavailable. The header would look like this:

Permissions-Policy: accelerometer=(),ambient-light-sensor=(),attribution-reporting=(),autoplay=(),battery=(),
                    bluetooth=(),browsing-topics=(),camera=(),ch-device-memory=(),ch-downlink=(),
                    ch-dpr=(),ch-ect=(),ch-lang=(),ch-prefers-color-scheme=(),ch-rtt=(),ch-save-data=(),
                    ch-ua=(),ch-ua-arch=(),ch-ua-bitness=(),ch-ua-full=(),ch-ua-full-version=(),
                    ch-ua-full-version-list=(),ch-ua-mobile=(),ch-ua-model=(),ch-ua-platform=(),
                    ch-ua-platform-version=(),ch-ua-reduced=(),ch-ua-wow64=(),ch-viewport-height=(),
                    ch-viewport-width=(),ch-width=(),clipboard-read=(),clipboard-write=(),conversion-measurement=(),
                    cross-origin-isolated=(),direct-sockets=(),display-capture=(),document-domain=(),
                    encrypted-media=(),execution-while-not-rendered=(),execution-while-out-of-viewport=(),
                    federated-credentials=(),focus-without-user-activation=(),fullscreen=(),gamepad=(),
                    geolocation=(),gyroscope=(),hid=(),idle-detection=(),interest-cohort=(),
                    join-ad-interest-group=(),keyboard-map=(),local-fonts=(),magnetometer=(),microphone=(),
                    midi=(),navigation-override=(),otp-credentials=(),payment=(),picture-in-picture=(),
                    publickey-credentials-get=(),run-ad-auction=(),screen-wake-lock=(),serial=(),shared-autofill=(),
                    shared-storage=(),speaker-selection=(),storage-access-api=(),sync-script=(),sync-xhr=(),
                    trust-token-redemption=(),unload=(),usb=(),vertical-scroll=(),wake-lock=(),web-share=(),
                    window-placement=(),xr-spatial-tracking=()

So, errr, yeah, your eyes might be bleeding right now and I'm sorry about that. Also, that list grows all the time. It's not great. There are issues about making a blanket "remove anything that might be dangerous" mode but so far they haven't been accepted.

We could decide to keep some things allowed (like camera or sync XHR) if we're worried about breaking legit use cases. But at least this list (which is the most comprehensive I could find — I'm looking into whether there's a reliable way to get an up-to-date list) makes things pretty tight and safe, on top of CSP and clearing the data.

lidel commented

Pretty hardcore, but for sure will do the trick of forcing people to move to subdomain gateways.

I propose we do a test run: set Permissions-Policy, Clear-Site-Data and a strict Content-Security-Policy on ipfs.io first, in Nginx, and see if there are any screams from the distance.

If the sky does not fall, me and @hacdias can apply this to all path requests in go-libipfs/gateway library.

In addition to Permissions-Policy, I think the following would be good:

Content-Security-Policy: default-src 'self' 'unsafe-inline' 'unsafe-eval' data: ; form-action 'self'; connect-src 'self' data: ; manifest-src 'none' ; object-src 'none' ; sandbox allow-forms allow-modals allow-scripts allow-top-navigation-by-user-activation
Clear-Site-Data: "cookies", "storage"
X-Content-Type-Options: nosniff
X-Frame-Options: sameorigin

Some quick notes:

  • I reviewed the options for Clear-Site-Data and agree with your original proposal.
  • For the CSP, I don't include blob: which means no file uploads (even locally). One thing that we could add (dynamically) would be the URL up to the CID inclusive (eg. https://ipfs.io/ipfs/someCID/) instead of 'self' for default/connect/form.
  • I didn't block navigate-to, I think linking to other sites should be ok (but I can change my mind).
  • I didn't block worker-src because I don't think that there's much left that a worker could do, but it's easy to add.
  • I don't think that we want CSP reporting, but it's easy enough to add.
  • I didn't include allow-same-origin in sandbox, that's pretty strict and might break things. What this does is that it puts the document in a weird origin of its own, all alone, and every request from that becomes cross-origin, which means that doing things that would require CORS when talking to another server now require CORS when talking to the same server, which is pretty neat. One issue is that it blocks font loading (because fonts require CORS) — maybe that's a problem? Alternatively, we could keep this strong and add Access-Control-Allow-Origin: this.site on requests that match fonts. I haven't found a way to have this sandboxing but not for fonts.
  • I believe that you already have nosniff, threw it in for completeness.
  • X-Frame-Options in case anyone is trying to inject this in an iframe elsewhere.

I did some very superficial testing on static content, and it works. If you want to play with it locally, run this with a path to a dir to serve from:

#!/usr/bin/env node

import process from 'process';
import express from 'express';
const app = express();

app.use((req, res, next) => {
  res.set({
    'Permissions-Policy': 'accelerometer=(),ambient-light-sensor=(),attribution-reporting=(),autoplay=(),battery=(),bluetooth=(),browsing-topics=(),camera=(),ch-device-memory=(),ch-downlink=(),ch-dpr=(),ch-ect=(),ch-lang=(),ch-prefers-color-scheme=(),ch-rtt=(),ch-save-data=(),ch-ua=(),ch-ua-arch=(),ch-ua-bitness=(),ch-ua-full=(),ch-ua-full-version=(),ch-ua-full-version-list=(),ch-ua-mobile=(),ch-ua-model=(),ch-ua-platform=(),ch-ua-platform-version=(),ch-ua-reduced=(),ch-ua-wow64=(),ch-viewport-height=(),ch-viewport-width=(),ch-width=(),clipboard-read=(),clipboard-write=(),conversion-measurement=(),cross-origin-isolated=(),direct-sockets=(),display-capture=(),document-domain=(),encrypted-media=(),execution-while-not-rendered=(),execution-while-out-of-viewport=(),federated-credentials=(),focus-without-user-activation=(),fullscreen=(),gamepad=(),geolocation=(),gyroscope=(),hid=(),idle-detection=(),interest-cohort=(),join-ad-interest-group=(),keyboard-map=(),local-fonts=(),magnetometer=(),microphone=(),midi=(),navigation-override=(),otp-credentials=(),payment=(),picture-in-picture=(),publickey-credentials-get=(),run-ad-auction=(),screen-wake-lock=(),serial=(),shared-autofill=(),shared-storage=(),speaker-selection=(),storage-access-api=(),sync-script=(),sync-xhr=(),trust-token-redemption=(),unload=(),usb=(),vertical-scroll=(),wake-lock=(),web-share=(),window-placement=(),xr-spatial-tracking=()',
    'Content-Security-Policy': `default-src 'self' 'unsafe-inline' 'unsafe-eval' data: ; form-action 'self'; connect-src 'self' data: ; manifest-src 'none' ; object-src 'none' ; sandbox allow-forms allow-modals allow-scripts allow-top-navigation-by-user-activation`,
    'X-Content-Type-Options': 'nosniff',
    'X-Frame-Options': 'sameorigin',
  });
  next();
});
app.use(express.static(process.argv[2]));
app.listen(8888, () => console.warn(`Listening on http://localhost:8888`));