Skip to content

Commit ff1d380

Browse files
Fix: form-action CSP blocked atproto login form submission (v0.4.2) (#107)
* Fix: drop form-action CSP that blocked the atproto handle form submit 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 <noreply@anthropic.com> * Release v0.4.2 --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 41fcfb8 commit ff1d380

4 files changed

Lines changed: 12 additions & 7 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@startup-api/cloudflare",
3-
"version": "0.4.1",
3+
"version": "0.4.2",
44
"license": "Apache-2.0",
55
"publishConfig": {
66
"access": "public"

src/auth/AtprotoProvider.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -304,10 +304,12 @@ export class AtprotoProvider extends OAuthProvider {
304304
status,
305305
headers: {
306306
'Content-Type': 'text/html; charset=utf-8',
307-
// Same hardening as the auth error page: only same-origin styles/form, never framed, never cached
308-
// (the re-rendered form reflects the user-supplied handle).
309-
'Content-Security-Policy':
310-
"default-src 'none'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'",
307+
// Hardening: same-origin styles only, never framed, never cached (the form reflects the
308+
// user-supplied handle). NOTE: deliberately NO `form-action` directive — submitting this form
309+
// hits our endpoint, which 302-redirects to the user's *own* authorization server (any PDS).
310+
// `form-action` is enforced across the whole redirect chain, so `'self'` (or any fixed list)
311+
// would block that cross-origin redirect and the login would silently fail.
312+
'Content-Security-Policy': "default-src 'none'; style-src 'self' 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'",
311313
'X-Content-Type-Options': 'nosniff',
312314
'Referrer-Policy': 'no-referrer',
313315
'Cache-Control': 'no-store',

test/atproto.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ describe('atproto provider', () => {
101101
// Auth page hardening: not framable, not cached (it can reflect the user's handle).
102102
expect(res.headers.get('Content-Security-Policy')).toContain("frame-ancestors 'none'");
103103
expect(res.headers.get('Cache-Control')).toBe('no-store');
104+
// Must NOT set form-action: submitting this form 302-redirects to the user's own auth server
105+
// (any PDS), and form-action is enforced across the redirect chain — restricting it breaks login.
106+
expect(res.headers.get('Content-Security-Policy')).not.toContain('form-action');
104107
const html = await res.text();
105108
expect(html).toContain('name="handle"');
106109
});

0 commit comments

Comments
 (0)