From fef1337b2b98c89e6a71cb71e255f376f2bad542 Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Thu, 20 Aug 2026 12:40:48 -0700 Subject: [PATCH] Let a browser send the device fingerprint header it is offered `fingerprint.ts` reads the device fingerprint from a request body or, for authenticated requests with no body to carry it, from `x-puter-device-fingerprint`. That header is documented there as "the header the GUI may send the device fingerprint on for non-signup requests", and the middleware has always honoured it. A browser could never actually send it. The CORS allowlist in `#installCors` does not include it, so the preflight comes back without it in `Access-Control-Allow-Headers`, the browser abandons the request, and `fetch` rejects before anything reaches the server. Nothing in tree sends the header today, which is why this went unnoticed: the first client to try it sees every call fail with a network error rather than a readable status. Adds the header to the allowlist, and a preflight test asserting the browser is allowed to ask for it. --- src/backend/server.test.ts | 23 +++++++++++++++++++++++ src/backend/server.ts | 6 ++++++ 2 files changed, 29 insertions(+) diff --git a/src/backend/server.test.ts b/src/backend/server.test.ts index 8afdf8b7b..53d8c1ba2 100644 --- a/src/backend/server.test.ts +++ b/src/backend/server.test.ts @@ -272,6 +272,29 @@ describe('PuterServer host header validation', () => { ); }); + it('lets a cross-origin preflight ask for the device fingerprint header', async () => { + // `fingerprint.ts` reads the device fingerprint off + // `x-puter-device-fingerprint` for authenticated requests with no body + // to carry it. That channel only exists if the preflight admits the + // header: a browser abandons the request otherwise, and the call never + // leaves it. + const res = await request( + '/healthcheck', + { + host: `api.puter.localhost:${port}`, + origin: 'http://puter.localhost', + 'access-control-request-method': 'GET', + 'access-control-request-headers': + 'authorization,x-puter-device-fingerprint', + }, + 'OPTIONS', + ); + expect(res.status).toBe(200); + expect( + String(res.headers['access-control-allow-headers']).toLowerCase(), + ).toContain('x-puter-device-fingerprint'); + }); + it('still short-circuits OPTIONS preflight off the dav subdomain', async () => { const res = await request( '/some-path', diff --git a/src/backend/server.ts b/src/backend/server.ts index 2e604a8b4..8602270b4 100644 --- a/src/backend/server.ts +++ b/src/backend/server.ts @@ -685,6 +685,12 @@ export class PuterServer { 'X-Expected-Entity-Length', 'DAV', 'stripe-signature', + // The GUI's fallback channel for the device fingerprint on + // authenticated requests that have no body to carry it (see + // core/http/middleware/fingerprint.ts). Without it here the + // preflight refuses the header and the request never leaves the + // browser. + 'x-puter-device-fingerprint', ].join(', '); // What a browser DAV client is allowed to read back off a response.