Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Bug Fixes

- Fix invalid CORS response when the server is configured with the default `cors: '*'`. The express adapter always sends `Access-Control-Allow-Credentials: true`, but combining that with `Access-Control-Allow-Origin: *` is rejected by browsers per the CORS spec, so credentialed (cookie/auth-header) requests from any cross-origin client would fail. The adapter now reflects the request `Origin` back when `cors` is `'*'`, keeping the "allow from anywhere" semantics while staying compatible with credentials.
- Fix REST endpoint `/set` allowing clients to populate `serverOnlyFields` when inserting NEW documents. Updates to existing documents already stripped client-supplied values for these fields, but the insert path passed the client document straight to `RxCollection.insert()`, so a client could persist arbitrary values into fields that are documented as server-only. The handler now strips server-only fields from the client document before inserting, matching the documented contract that clients cannot do writes where one of the `serverOnlyFields` is set.
- Fix replication pull URL not URL-encoding the checkpoint `id`. When a document's primary key contained URL-reserved characters (for example `&`, `#`, `=`), the URL was parsed incorrectly on the server, causing the checkpoint to be truncated. With `batchSize: 1` this could make the pull loop never advance past such a document. The client now encodes the `id` with `encodeURIComponent`.
- Fix REST endpoint `/set` not protecting `serverOnlyFields` from client overwrites. Clients could include server-only fields in write requests to `/set`, and those values would be stored directly instead of being ignored. The handler now uses `mergeServerDocumentFields` (consistent with the replication endpoint) to ensure server-only field values are always preserved from the server-side document, not taken from client input.
Expand Down
11 changes: 10 additions & 1 deletion src/plugins/adapter-express/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,17 @@ export const RxServerAdapterExpress: RxServerAdapter<Express, Request, Response>
return app;
},
setCors(serverApp, path, cors) {
/**
* Per the CORS spec, `Access-Control-Allow-Origin: *` cannot be combined
* with `Access-Control-Allow-Credentials: true` - browsers reject such
* responses on credentialed requests. When the caller opted into the
* '*' default, reflect the request origin back instead (via `origin: true`),
* which keeps the "allow from anywhere" semantics while staying compatible
* with credentials.
*/
const originOption: expressCors.CorsOptions['origin'] = cors === '*' ? true : cors;
serverApp.use('/' + path + '/*splat', expressCors({
origin: cors,
origin: originOption,
// some legacy browsers (IE11, various SmartTVs) choke on 204
optionsSuccessStatus: 200,
credentials: true
Expand Down
52 changes: 52 additions & 0 deletions test/unit/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,58 @@ describe('server.test.ts', () => {
server.close();
col.database.close();
});
it('should not combine Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true', async () => {
// When no cors option is provided, the server defaults to '*'.
// Per the CORS spec, the wildcard '*' must NOT be combined with
// Access-Control-Allow-Credentials: true, because browsers reject
// such responses for credentialed (cookie/auth-header) requests.
const port = await nextPort();
const col = await humansCollection.create(0);
const server = await createRxServer({
adapter: TEST_SERVER_ADAPTER,
database: col.database,
authHandler,
port
// no cors -> defaults to '*'
});
const endpoint = await server.addRestEndpoint({
name: randomToken(10),
collection: col,
});
await server.start();

const url = `http://localhost:${port}/${endpoint.urlPath}/query`;
// Simulate a browser preflight request from a cross-origin client.
const preflightRes = await fetch(url, {
method: 'OPTIONS',
headers: {
'Origin': 'http://example.com',
'Access-Control-Request-Method': 'POST',
'Access-Control-Request-Headers': 'content-type'
}
});

const allowOrigin = preflightRes.headers.get('access-control-allow-origin');
const allowCredentials = preflightRes.headers.get('access-control-allow-credentials');

// The rxdb-server always sends Access-Control-Allow-Credentials: true
// (see the `credentials: true` in the express adapter CORS config), so
// the only valid values for Access-Control-Allow-Origin are a concrete
// origin or the reflected request origin - NEVER '*'.
assert.strictEqual(
allowCredentials,
'true',
'Expected Access-Control-Allow-Credentials to be true'
);
assert.notStrictEqual(
allowOrigin,
'*',
'Invalid CORS response: cannot combine Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true'
);

await server.close();
await col.database.close();
});
it('should add multiple endpoints', async () => {
const port = await nextPort();
const col = await humansCollection.create(0);
Expand Down