From 7bb7061ed616d5175fdcf472ef2c557086ab240a Mon Sep 17 00:00:00 2001 From: Sergey Chernyshev Date: Sun, 28 Jun 2026 22:35:31 -0400 Subject: [PATCH 1/2] Fix: drop form-action CSP that blocked the atproto handle form submit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The handle-entry form sets a strict CSP. The `form-action 'self'` directive (added for defense-in-depth) silently broke login: submitting the form hits our endpoint, which immediately 302-redirects to the user's own authorization server (any PDS, cross-origin). `form-action` is enforced across the entire redirect chain, so `'self'` blocks the submission and the browser does nothing — exactly the "form doesn't send me there" symptom, while navigating to the auth URL directly worked (no form-action involved). Remove the `form-action` directive entirely — it is fundamentally incompatible with a login form that must redirect to per-user auth servers. The rest of the hardening (default-src 'none', no scripts, frame-ancestors, nosniff, no-store, style-src 'self') stays. Add a regression test asserting the form CSP never carries form-action. Reproduced and verified the fix by driving the form in headless Chrome. Co-Authored-By: Claude Opus 4.8 --- src/auth/AtprotoProvider.ts | 10 ++++++---- test/atproto.spec.ts | 3 +++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/auth/AtprotoProvider.ts b/src/auth/AtprotoProvider.ts index 44229c8..7ca4c49 100644 --- a/src/auth/AtprotoProvider.ts +++ b/src/auth/AtprotoProvider.ts @@ -304,10 +304,12 @@ export class AtprotoProvider extends OAuthProvider { status, headers: { 'Content-Type': 'text/html; charset=utf-8', - // Same hardening as the auth error page: only same-origin styles/form, never framed, never cached - // (the re-rendered form reflects the user-supplied handle). - 'Content-Security-Policy': - "default-src 'none'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'", + // Hardening: same-origin styles only, never framed, never cached (the form reflects the + // user-supplied handle). NOTE: deliberately NO `form-action` directive — submitting this form + // hits our endpoint, which 302-redirects to the user's *own* authorization server (any PDS). + // `form-action` is enforced across the whole redirect chain, so `'self'` (or any fixed list) + // would block that cross-origin redirect and the login would silently fail. + 'Content-Security-Policy': "default-src 'none'; style-src 'self' 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'", 'X-Content-Type-Options': 'nosniff', 'Referrer-Policy': 'no-referrer', 'Cache-Control': 'no-store', diff --git a/test/atproto.spec.ts b/test/atproto.spec.ts index fffa45f..5fc72e6 100644 --- a/test/atproto.spec.ts +++ b/test/atproto.spec.ts @@ -101,6 +101,9 @@ describe('atproto provider', () => { // Auth page hardening: not framable, not cached (it can reflect the user's handle). expect(res.headers.get('Content-Security-Policy')).toContain("frame-ancestors 'none'"); expect(res.headers.get('Cache-Control')).toBe('no-store'); + // Must NOT set form-action: submitting this form 302-redirects to the user's own auth server + // (any PDS), and form-action is enforced across the redirect chain — restricting it breaks login. + expect(res.headers.get('Content-Security-Policy')).not.toContain('form-action'); const html = await res.text(); expect(html).toContain('name="handle"'); }); From 1ef82e632e15a1a3bc14a9c3d1b49a1de94d0a01 Mon Sep 17 00:00:00 2001 From: Sergey Chernyshev Date: Sun, 28 Jun 2026 22:35:33 -0400 Subject: [PATCH 2/2] Release v0.4.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e70cae3..cd67873 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@startup-api/cloudflare", - "version": "0.4.1", + "version": "0.4.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@startup-api/cloudflare", - "version": "0.4.1", + "version": "0.4.2", "license": "Apache-2.0", "dependencies": { "he": "^1.2.0", diff --git a/package.json b/package.json index af74f77..904f3cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@startup-api/cloudflare", - "version": "0.4.1", + "version": "0.4.2", "license": "Apache-2.0", "publishConfig": { "access": "public"