From d282d6abac12c29dfbe9690ba5cd285ee3960f91 Mon Sep 17 00:00:00 2001 From: Travis Vachon Date: Fri, 10 Feb 2023 10:11:55 +0800 Subject: [PATCH] fix: avoid email delegation via GET request (#430) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The email validation approval process is now split into two stages: a GET request with no side effects except to load a page, that then auto-submits a POST request to actually continue the flow. ## Summary of problem This fixes the API so as to follow [proper HTTP semantics](https://github.com/web3-storage/w3protocol/issues/333#issuecomment-1380925535): > The purpose of distinguishing between safe [i.e. like GET] and unsafe [like PUT/POST] methods is to allow automated retrieval processes (spiders) and cache performance optimization (pre-fetching) to work without fear of causing harm. In addition, it allows a user agent to apply appropriate constraints on the automated use of unsafe methods when processing potentially untrusted content. That is, a `PUT` or `POST` (rather than a `GET`) **must** be the method used in order to do things like * cause a message to be sent (forwarding a UCAN delegation via a separate connection's websocket) * cause an untrusted keypair to be associated with a billable email address (which is the outcome of that forwarding, in practice!) Fixing the HTTP semantics should address all of #348, and is the first step to addressing the security concerns in #333. ## Summary of solution Clicking (or scanning/pre-fetching/previewing/etc.) the link in the email no longer finishes the validation process. Instead, it loads a (harmless to scan/pre-fetch/preview) landing page which simply says "Validating Email" while using JavaScript to auto-complete the process. This patch is able to fix the core HTTP semantics in a very self-contained way: * no changes needed to the email templates * will not break any existing unexpired links at the moment it is deployed * is essentially the exact same UX from a user's perspective (they might notice just a little extra blink) * does degrade gracefully if user has JS disabled, and any non-browser clients could still trigger the `POST` ± just as easy as before * no changes needed on the `w3ui` side for this part of the email validation improvements --------- Co-authored-by: Nathan Vander Wilt --- packages/access-api/src/index.js | 5 +-- .../access-api/src/routes/validate-email.js | 15 ++++++++ packages/access-api/src/utils/html.js | 34 +++++++++++++++++++ .../access-api/test/access-authorize.test.js | 4 +-- .../access-api/test/space-recover.test.js | 2 +- 5 files changed, 55 insertions(+), 5 deletions(-) diff --git a/packages/access-api/src/index.js b/packages/access-api/src/index.js index b67819f83..46f318360 100644 --- a/packages/access-api/src/index.js +++ b/packages/access-api/src/index.js @@ -4,7 +4,7 @@ import { notFound } from '@web3-storage/worker-utils/response' import { Router } from '@web3-storage/worker-utils/router' import { postRaw } from './routes/raw.js' import { postRoot } from './routes/root.js' -import { validateEmail } from './routes/validate-email.js' +import { preValidateEmail, validateEmail } from './routes/validate-email.js' import { validateWS } from './routes/validate-ws.js' import { version } from './routes/version.js' import { getContext } from './utils/context.js' @@ -14,7 +14,8 @@ const r = new Router({ onNotFound: notFound }) r.add('options', '*', preflight) r.add('get', '/version', version) -r.add('get', '/validate-email', validateEmail) +r.add('get', '/validate-email', preValidateEmail) +r.add('post', '/validate-email', validateEmail) r.add('get', '/validate-ws', validateWS) r.add('post', '/', postRoot) r.add('post', '/raw', postRaw) diff --git a/packages/access-api/src/routes/validate-email.js b/packages/access-api/src/routes/validate-email.js index 3da06bce7..bb91b696f 100644 --- a/packages/access-api/src/routes/validate-email.js +++ b/packages/access-api/src/routes/validate-email.js @@ -6,8 +6,23 @@ import { HtmlResponse, ValidateEmail, ValidateEmailError, + PendingValidateEmail, } from '../utils/html.js' +/** + * @param {import('@web3-storage/worker-utils/router').ParsedRequest} req + * @param {import('../bindings.js').RouteContext} env + */ +export async function preValidateEmail(req, env) { + if (!req.query?.ucan) { + return new HtmlResponse( + + ) + } + + return new HtmlResponse() +} + /** * @param {import('@web3-storage/worker-utils/router').ParsedRequest} req * @param {import('../bindings.js').RouteContext} env diff --git a/packages/access-api/src/utils/html.js b/packages/access-api/src/utils/html.js index 9db97041b..56814b4b8 100644 --- a/packages/access-api/src/utils/html.js +++ b/packages/access-api/src/utils/html.js @@ -95,6 +95,40 @@ export class HtmlResponse extends Response { } } +/** + * + * @param {object} props + * @param {boolean} [props.autoApprove] + */ +export const PendingValidateEmail = ({ autoApprove }) => ( +
+ +
+

Validating Email

+
+ +
+ {autoApprove ? ( +