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.
This commit is contained in:
Nariman Jelveh
2026-08-20 12:40:48 -07:00
parent 58794f4f76
commit fef1337b2b
2 changed files with 29 additions and 0 deletions
+23
View File
@@ -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',
+6
View File
@@ -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.