Skip to content

Commit 2c4390a

Browse files
geroplroboquat
authored andcommitted
[server] strict same site origin for /api/gitpod endpoint
1 parent 3e40a62 commit 2c4390a

File tree

3 files changed

+97
-20
lines changed

3 files changed

+97
-20
lines changed

components/server/src/express-util.spec.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ describe("express-util", function () {
1515
const result = isAllowedWebsocketDomain(
1616
"http://3000-moccasin-ferret-155799b3.ws-eu.gpl-2732-ws-csrf.staging.gitpod.io",
1717
HOSTURL_HOSTNAME,
18+
false,
1819
);
1920
expect(result).to.be.false;
2021
});
@@ -23,12 +24,43 @@ describe("express-util", function () {
2324
const result = isAllowedWebsocketDomain(
2425
"http://moccasin-ferret-155799b3.ws-eu.gpl-2732-ws-csrf.staging.gitpod.io",
2526
HOSTURL_HOSTNAME,
27+
false,
2628
);
2729
expect(result).to.be.true;
2830
});
2931

3032
it("should return true for dashboard", function () {
31-
const result = isAllowedWebsocketDomain("http://gpl-2732-ws-csrf.staging.gitpod.io", HOSTURL_HOSTNAME);
33+
const result = isAllowedWebsocketDomain(
34+
"http://gpl-2732-ws-csrf.staging.gitpod.io",
35+
HOSTURL_HOSTNAME,
36+
false,
37+
);
38+
expect(result).to.be.true;
39+
});
40+
it("should return false for workspace-port locations (strict)", function () {
41+
const result = isAllowedWebsocketDomain(
42+
"http://3000-moccasin-ferret-155799b3.ws-eu.gpl-2732-ws-csrf.staging.gitpod.io",
43+
HOSTURL_HOSTNAME,
44+
true,
45+
);
46+
expect(result).to.be.false;
47+
});
48+
49+
it("should return true for workspace locations (strict)", function () {
50+
const result = isAllowedWebsocketDomain(
51+
"http://moccasin-ferret-155799b3.ws-eu.gpl-2732-ws-csrf.staging.gitpod.io",
52+
HOSTURL_HOSTNAME,
53+
true,
54+
);
55+
expect(result).to.be.false;
56+
});
57+
58+
it("should return true for dashboard (strict)", function () {
59+
const result = isAllowedWebsocketDomain(
60+
"http://gpl-2732-ws-csrf.staging.gitpod.io",
61+
HOSTURL_HOSTNAME,
62+
true,
63+
);
3264
expect(result).to.be.true;
3365
});
3466
});
@@ -38,12 +70,34 @@ describe("express-util", function () {
3870
const result = isAllowedWebsocketDomain(
3971
"https://8000-black-capybara-dy6e3fgz.ws-eu08.gitpod.io",
4072
HOSTURL_HOSTNAME,
73+
false,
4174
);
4275
expect(result).to.be.false;
4376
});
4477

4578
it("should return true for workspace locations", function () {
46-
const result = isAllowedWebsocketDomain("https://bronze-bird-p2q226d8.ws-eu08.gitpod.io", HOSTURL_HOSTNAME);
79+
const result = isAllowedWebsocketDomain(
80+
"https://bronze-bird-p2q226d8.ws-eu08.gitpod.io",
81+
HOSTURL_HOSTNAME,
82+
false,
83+
);
84+
expect(result).to.be.true;
85+
});
86+
it("should return false for workspace-port locations (strict)", function () {
87+
const result = isAllowedWebsocketDomain(
88+
"https://8000-black-capybara-dy6e3fgz.ws-eu08.gitpod.io",
89+
HOSTURL_HOSTNAME,
90+
true,
91+
);
92+
expect(result).to.be.false;
93+
});
94+
95+
it("should return false for workspace locations (strict)", function () {
96+
const result = isAllowedWebsocketDomain(
97+
"https://bronze-bird-p2q226d8.ws-eu08.gitpod.io",
98+
HOSTURL_HOSTNAME,
99+
true,
100+
);
47101
expect(result).to.be.true;
48102
});
49103
});

components/server/src/express-util.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
import { URL } from "url";
88
import * as express from "express";
99
import * as crypto from "crypto";
10-
import { GitpodHostUrl } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url";
1110
import * as session from "express-session";
11+
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
12+
import { GitpodHostUrl } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url";
1213

1314
export const query = (...tuples: [string, string][]) => {
1415
if (tuples.length === 0) {
@@ -17,22 +18,29 @@ export const query = (...tuples: [string, string][]) => {
1718
return "?" + tuples.map((t) => `${t[0]}=${encodeURIComponent(t[1])}`).join("&");
1819
};
1920

20-
// We do not precise UUID parsing here, we just want to distinguish three cases:
21-
// - the base domain
22-
// - a frontend domain (workspace domain)
23-
// - a workspace port domain
24-
// We control all of those values and the base domain, so we don't need to much effort
25-
export const isAllowedWebsocketDomain = (originHeader: string, gitpodHostName: string): boolean => {
21+
// Strict: We only allow connections from the base domain, so disallow connections from all other Origins
22+
// Only (current) exception: If no Origin header is set, skip the check!
23+
// Non-Strict: "rely" on subdomain parsing (do we still need this?)
24+
export const isAllowedWebsocketDomain = (originHeader: string, gitpodHostName: string, strict: boolean): boolean => {
2625
if (!originHeader) {
26+
// TODO(gpl) Can we get rid of this dependency alltogether?
27+
log.warn("Origin header check not applied because of empty Origin header!");
2728
return true;
2829
}
29-
var originHostname = "";
30+
3031
try {
3132
const originUrl = new URL(originHeader);
32-
originHostname = originUrl.hostname;
33+
const originHostname = originUrl.hostname;
3334
if (originHostname === gitpodHostName) {
3435
return true;
3536
}
37+
38+
if (strict) {
39+
return false;
40+
}
41+
// TODO(gpl) This is only used for Bearer-Token authentication.
42+
// Does this restriction help at all, or can we remove it entirely?
43+
log.warn("Origin header check based on looksLikeWorkspaceHostname");
3644
if (looksLikeWorkspaceHostname(originUrl, gitpodHostName)) {
3745
return true;
3846
} else {

components/server/src/server.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -154,13 +154,14 @@ export class Server<C extends GitpodClient, S extends GitpodServer> {
154154
// Websocket for client connection
155155
const websocketConnectionHandler = this.websocketConnectionHandler;
156156
this.eventEmitter.on(Server.EVENT_ON_START, (httpServer) => {
157-
// CSRF protection: check "Origin" header, it must be either:
158-
// - gitpod.io (hostUrl.hostname) or
159-
// - a workspace location (ending of hostUrl.hostname)
157+
// CSRF protection: check "Origin" header:
158+
// - for cookie/session auth: MUST be gitpod.io (hostUrl.hostname)
159+
// - for Bearer auth: MUST be sth with the same base domain (*.gitpod.io) (is this required?)
160+
// - edge case: empty "Origin" is always permitted (can this be removed?)
160161
// We rely on the origin header being set correctly (needed by regular clients to use Gitpod:
161162
// CORS allows subdomains to access gitpod.io)
162-
const verifyCSRF = (origin: string) => {
163-
let allowedRequest = isAllowedWebsocketDomain(origin, this.config.hostUrl.url.hostname);
163+
const verifyOrigin = (origin: string, strict: boolean) => {
164+
let allowedRequest = isAllowedWebsocketDomain(origin, this.config.hostUrl.url.hostname, strict);
164165
if (!allowedRequest && this.config.insecureNoDomain) {
165166
log.warn("Websocket connection CSRF guard disabled");
166167
allowedRequest = true;
@@ -172,21 +173,35 @@ export class Server<C extends GitpodClient, S extends GitpodServer> {
172173
* Verify the web socket handshake request.
173174
*/
174175
const verifyClient: ws.VerifyClientCallbackAsync = async (info, callback) => {
175-
if (!verifyCSRF(info.origin)) {
176-
log.warn("Websocket connection attempt with non-matching Origin header: " + info.origin);
177-
return callback(false, 403);
178-
}
176+
let authenticatedUsingBearerToken = false;
179177
if (info.req.url === "/v1") {
178+
// Connection attempt with Bearer-Token: be less strict for now
179+
if (!verifyOrigin(info.origin, false)) {
180+
log.warn("Websocket connection attempt with non-matching Origin header: " + info.origin);
181+
return callback(false, 403);
182+
}
183+
180184
try {
181185
await this.bearerAuth.auth(info.req as express.Request);
186+
authenticatedUsingBearerToken = true;
182187
} catch (e) {
183188
if (isBearerAuthError(e)) {
184189
return callback(false, 401, e.message);
185190
}
186191
log.warn("authentication failed: ", e);
187192
return callback(false, 500);
188193
}
194+
// intentional fall-through to cookie/session based authentication
189195
}
196+
197+
if (!authenticatedUsingBearerToken) {
198+
// Connection attempt with cookie/session based authentication: be strict about where we accept connections from!
199+
if (!verifyOrigin(info.origin, true)) {
200+
log.warn("Websocket connection attempt with non-matching Origin header: " + info.origin);
201+
return callback(false, 403);
202+
}
203+
}
204+
190205
return callback(true);
191206
};
192207

0 commit comments

Comments
 (0)