Skip to content

Commit cf91128

Browse files
authored
fix: stricter CORS if Origin header exists (#7349)
1 parent 1316d9a commit cf91128

File tree

3 files changed

+85
-17
lines changed

3 files changed

+85
-17
lines changed

.changeset/sixty-things-occur.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'hive': patch
3+
---
4+
5+
Stricter CORS assessment for requests sending a Origin header.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { ensureEnv } from '../../testkit/env';
2+
import { getServiceHost } from '../../testkit/utils';
3+
4+
const registryAddress = await getServiceHost('server', 8082);
5+
6+
const endpoint = `http://${registryAddress}`;
7+
8+
test('no origin header -> no CORS checks', async () => {
9+
const request = await fetch(`${endpoint}/graphql`);
10+
expect(request.headers.get('access-control-allow-origin')).toEqual(null);
11+
expect(request.headers.get('access-control-allow-credentials')).toEqual(null);
12+
expect(request.status).toEqual(200);
13+
expect(await request.text()).toMatchInlineSnapshot(
14+
`{"errors":[{"message":"Must provide query string.","extensions":{"code":"BAD_REQUEST"}}]}`,
15+
);
16+
});
17+
18+
test('unmatching origin -> send CORS error', async () => {
19+
const request = await fetch(`${endpoint}/graphql`, {
20+
headers: {
21+
origin: 'https://evil.com',
22+
},
23+
});
24+
expect(request.headers.get('access-control-allow-origin')).toEqual(null);
25+
expect(request.headers.get('access-control-allow-credentials')).toEqual(null);
26+
expect(request.status).toEqual(403);
27+
expect(await request.text()).toMatchInlineSnapshot(`CORS origin not allowed.`);
28+
});
29+
30+
test('matching origin -> send correct CORS headers', async () => {
31+
const request = await fetch(`${endpoint}/graphql`, {
32+
headers: {
33+
origin: ensureEnv('HIVE_APP_BASE_URL'),
34+
},
35+
});
36+
expect(request.headers.get('access-control-allow-origin')).toEqual(
37+
ensureEnv('HIVE_APP_BASE_URL'),
38+
);
39+
expect(request.headers.get('access-control-allow-credentials')).toEqual('true');
40+
expect(request.status).toEqual(200);
41+
expect(await request.text()).toMatchInlineSnapshot(
42+
`{"errors":[{"message":"Must provide query string.","extensions":{"code":"BAD_REQUEST"}}]}`,
43+
);
44+
});

packages/services/server/src/index.ts

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ import { createOtelAuthEndpoint } from './otel-auth-endpoint';
7171
import { createPublicGraphQLHandler } from './public-graphql-handler';
7272
import { initSupertokens, oidcIdLookup } from './supertokens';
7373

74+
class CorsError extends Error {
75+
constructor() {
76+
super('CORS origin not allowed.');
77+
}
78+
}
79+
7480
export async function main() {
7581
let tracing: TracingInstance | undefined;
7682

@@ -146,28 +152,41 @@ export async function main() {
146152
},
147153
);
148154

149-
server.setErrorHandler(supertokensErrorHandler());
155+
server.setErrorHandler((err, req, res) => {
156+
if (err instanceof CorsError) {
157+
return res.status(403).send(err.message);
158+
}
159+
160+
return supertokensErrorHandler()(err, req, res);
161+
});
150162
await server.register(cors, (_: unknown): FastifyCorsOptionsDelegateCallback => {
151163
return (req, callback) => {
152-
if (req.headers.origin?.startsWith(env.hiveServices.webApp.url)) {
153-
// We need to treat requests from the web app a bit differently than others.
154-
// The web app requires to define the `Access-Control-Allow-Origin` header (not *).
155-
callback(null, {
156-
origin: env.hiveServices.webApp.url,
157-
credentials: true,
158-
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
159-
allowedHeaders: [
160-
'Content-Type',
161-
'graphql-client-version',
162-
'graphql-client-name',
163-
'x-request-id',
164-
...supertokens.getAllCORSHeaders(),
165-
],
164+
// For CLI user we do not have a origin
165+
if (req.headers.origin == null) {
166+
// this is the easiest way to omit all cors headers for our version of the cors plugin.
167+
return callback(null, {
168+
origin: [],
166169
});
167-
return;
168170
}
169171

170-
callback(null, {});
172+
if (req.headers.origin !== env.hiveServices.webApp.url) {
173+
return callback(new CorsError());
174+
}
175+
176+
// We need to treat requests from the web app a bit differently than others.
177+
// The web app requires to define the `Access-Control-Allow-Origin` header (not *).
178+
callback(null, {
179+
origin: env.hiveServices.webApp.url,
180+
credentials: true,
181+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
182+
allowedHeaders: [
183+
'Content-Type',
184+
'graphql-client-version',
185+
'graphql-client-name',
186+
'x-request-id',
187+
...supertokens.getAllCORSHeaders(),
188+
],
189+
});
171190
};
172191
});
173192

0 commit comments

Comments
 (0)