CVE-2024-21583: Insufficient Information (NVD-CWE-noinfo)
value: Versions of the package github.com/gitpod-io/gitpod/components/server/go/pkg/lib before main-gha.27122; versions of the package github.com/gitpod-io/gitpod/components/ws-proxy/pkg/proxy before main-gha.27122; versions of the package github.com/gitpod-io/gitpod/install/installer/pkg/components/auth before main-gha.27122; versions of the package github.com/gitpod-io/gitpod/install/installer/pkg/components/public-api-server before main-gha.27122; versions of the package github.com/gitpod-io/gitpod/install/installer/pkg/components/server before main-gha.27122; versions of the package @gitpod/gitpod-protocol before 0.1.5-main-gha.27122 are vulnerable to Cookie Tossing due to a missing __Host- prefix on the _gitpod_io_jwt2_ session cookie. This allows an adversary who controls a subdomain to set the value of the cookie on the Gitpod control plane, which can be assigned to an attacker’s own JWT so that specific actions taken by the victim (such as connecting a new Github organization) are actioned by the attackers session.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-07-19T05:15Z
Repository: https://github.com/gitpod-io/gitpod
Language: TypeScript
Files changed: components/gitpod-protocol/src/util/gitpod-host-url.ts, components/server/go/pkg/lib/cookie.go, components/server/src/auth/generic-auth-provider.ts, components/server/src/auth/login-completion-handler.ts, components/server/src/prometheus-metrics.ts, components/server/src/session-handler.spec.db.ts, components/server/src/session-handler.ts, components/server/src/test/service-testing-container-module.ts, components/server/src/user/user-controller.ts, components/ws-proxy/pkg/proxy/routes.go, components/ws-proxy/pkg/proxy/routes_test.go, install/installer/pkg/components/auth/config_test.go, install/installer/pkg/components/public-api-server/configmap_test.go, install/installer/pkg/components/server/configmap_test.go
Commit date: 2024-07-01 08:53:14 +0200
Commit message: Make auth cookie stricter (#19973)

* Use `__Host-` prefix for cookie

* Fix tests

* Remove domain from cookie

* Fix logout

* remove unused fn

* fix user logout properly

* [server] Make domain-only cookie work for GitHub oauth login

... by adding additional step so we can set the cookie for the base domain only

* test: fix by redirecting before callbacl/authorize

* [server] SessionHandler: Allow to login with both primary and secondary cookies

* [server] Clear 2ndary cookie on logout

* Fix filtering cookie values when primary cookie is empty

* Fix logouts

* Fix tests

---------

Co-authored-by: Gero Posmyk-Leinemann <gero@gitpod.io>
Diff: diff --git a/components/gitpod-protocol/src/util/gitpod-host-url.ts b/components/gitpod-protocol/src/util/gitpod-host-url.ts
index 74915fdac..39988cf2c 100644
--- a/components/gitpod-protocol/src/util/gitpod-host-url.ts
+++ b/components/gitpod-protocol/src/util/gitpod-host-url.ts
@@ -214,6 +214,14 @@ export class GitpodHostUrl {
         if (hostSegments[0] !== "ide") {
             newUrl = newUrl.asIDEProxy();
         }
         return newUrl.with((url) => ({ pathname: "/metrics-api" }));
     }
+
+    asLoginWithOTS(userId: string, key: string, returnToUrl?: string) {
+        const result = this.withApi({ pathname: `/login/ots/${userId}/${key}` });
+        if (returnToUrl) {
+            return result.with({ search: `returnTo=${encodeURIComponent(returnToUrl)}` });
+        }
+        return result;
+    }
 }
diff --git a/components/server/go/pkg/lib/cookie.go b/components/server/go/pkg/lib/cookie.go
index 3168cf8aa..1596d9cfc 100644
--- a/components/server/go/pkg/lib/cookie.go
+++ b/components/server/go/pkg/lib/cookie.go
@@ -7,7 +7,7 @@ package lib
 import "regexp"
 
 func CookieNameFromDomain(domain string) string {
 	// replace all non-word characters with underscores
 	derived := regexp.MustCompile(`[\W_]+`).ReplaceAllString(domain, "_")
-	return "_" + derived + "_jwt2_"
+	return "__Host-_" + derived + "_jwt2_"
 }
diff --git a/components/server/src/auth/generic-auth-provider.ts b/components/server/src/auth/generic-auth-provider.ts
index 499549434..1295f3d96 100644
--- a/components/server/src/auth/generic-auth-provider.ts
+++ b/components/server/src/auth/generic-auth-provider.ts
@@ -300,10 +300,18 @@ export abstract class GenericAuthProvider implements AuthProvider {
             reportLoginCompleted("failed", "git");
             response.redirect(this.getSorryUrl(`Auth flow state is missing.`));
             return;
         }
 
+        if (!this.loginCompletionHandler.isBaseDomain(request)) {
+            // For auth requests that are not targetting the base domain, we redirect to the base domain, so they come with our cookie.
+            log.info(`(${strategyName}) Auth request on subdomain, redirecting to base domain`, { clientInfo });
+            const target = new URL(request.url, this.config.hostUrl.url.toString()).toString();
+            response.redirect(target);
+            return;
+        }
+
         if (isAlreadyLoggedIn) {
             if (!authFlow) {
                 log.warn(
                     cxt,
                     `(${strategyName}) User is already logged in. No auth info provided. Redirecting to dashboard.`,
diff --git a/components/server/src/auth/login-completion-handler.ts b/components/server/src/auth/login-completion-handler.ts
index 799faa4f5..078987f29 100644
--- a/components/server/src/auth/login-completion-handler.ts
+++ b/components/server/src/auth/login-completion-handler.ts
@@ -4,20 +4,22 @@
  * See License.AGPL.txt in the project root for license information.
  */
 
 import { inject, injectable } from "inversify";
 import express from "express";
+import * as crypto from "crypto";
 import { User } from "@gitpod/gitpod-protocol";
 import { log, LogContext } from "@gitpod/gitpod-protocol/lib/util/logging";
 import { Config } from "../config";
 import { HostContextProvider } from "./host-context-provider";
 import { AuthProviderService } from "./auth-provider-service";
 import { reportJWTCookieIssued, reportLoginCompleted } from "../prometheus-metrics";
 import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics";
 import { trackLogin } from "../analytics";
 import { SessionHandler } from "../session-handler";
 import { AuthJWT } from "./jwt";
+import { OneTimeSecretServer } from "../one-time-secret-server";
 
 /**
  * The login completion handler pulls the strings between the OAuth2 flow, the ToS flow, and the session management.
  */
 @injectable()
@@ -26,10 +28,11 @@ export class LoginCompletionHandler {
     @inject(HostContextProvider) protected readonly hostContextProvider: HostContextProvider;
     @inject(IAnalyticsWriter) protected readonly analytics: IAnalyticsWriter;
     @inject(AuthProviderService) protected readonly authProviderService: AuthProviderService;
     @inject(AuthJWT) protected readonly authJWT: AuthJWT;
     @inject(SessionHandler) protected readonly session: SessionHandler;
+    @inject(OneTimeSecretServer) private readonly otsServer: OneTimeSecretServer;
 
     async complete(
         request: express.Request,
         response: express.Response,
         { user, returnToUrl, authHost, elevateScopes }: LoginCompletionHandler.CompleteParams,
@@ -76,19 +79,43 @@ export class LoginCompletionHandler {
             /** no await */ trackLogin(user, request, authHost, this.analytics).catch((err) =>
                 log.error({ userId: user.id }, "Failed to track Login.", err),
             );
         }
 
+        if (!this.isBaseDomain(request)) {
+            // (GitHub edge case) If we got redirected here onto a sub-domain (e.g. api.gitpod.io), we need to redirect to the base domain in order to Set-Cookie properly.
+            const secret = crypto
+                .createHash("sha256")
+                .update(user.id + this.config.session.secret)
+                .digest("hex");
+            const expirationDate = new Date(Date.now() + 1000 * 60); // 1 minutes
+            const token = await this.otsServer.serveToken({}, secret, expirationDate);
+
+            reportLoginCompleted("succeeded_via_ots", "git");
+            log.info(
+                logContext,
+                `User will be logged in via OTS on the base domain. (Indirect) redirect to: ${returnTo}`,
+            );
+            const baseDomainRedirect = this.config.hostUrl.asLoginWithOTS(user.id, token.token, returnTo).toString();
+            response.redirect(baseDomainRedirect);
+            return;
+        }
+
+        // (default case) If we got redirected here onto the base domain of the Gitpod installation, we can just issue the cookie right away.
         const cookie = await this.session.createJWTSessionCookie(user.id);
         response.cookie(cookie.name, cookie.value, cookie.opts);
         reportJWTCookieIssued();
 
         log.info(logContext, `User is logged in successfully. Redirect to: ${returnTo}`);
         reportLoginCompleted("succeeded", "git");
         response.redirect(returnTo);
     }
 
+    public isBaseDomain(req: express.Request): boolean {
+        return req.hostname === this.config.hostUrl.url.hostname;
+    }
+
     public async updateAuthProviderAsVerified(hostname: string, user: User) {
         const hostCtx = this.hostContextProvider.get(hostname);
         log.info("Updating auth provider as verified", { hostname });
         if (hostCtx) {
             const { params: config } = hostCtx.authProvider;
diff --git a/components/server/src/prometheus-metrics.ts b/components/server/src/prometheus-metrics.ts
index 388f9eced..6f1d6e5ad 100644
--- a/components/server/src/prometheus-metrics.ts
+++ b/components/server/src/prometheus-metrics.ts
@@ -83,10 +83,12 @@ export function reportLoginCompleted(status: LoginCounterStatus, type: "git" | "
 type LoginCounterStatus =
     // The login attempt failed due to a system error (picked up by alerts)
     | "failed"
     // The login attempt succeeded
     | "succeeded"
+    // The login was successful, but we need to defer cookie creation via an OTS
+    | "succeeded_via_ots"
     // The login attempt failed, because the client failed to provide complete session information, for instance.
     | "failed_client";
 
 const apiConnectionCounter = new prometheusClient.Counter({
     name: "gitpod_server_api_connections_total",
diff --git a/components/server/src/session-handler.spec.db.ts b/components/server/src/session-handler.spec.db.ts
index 182adaa5f..d4620fd55 100644
--- a/components/server/src/session-handler.spec.db.ts
+++ b/components/server/src/session-handler.spec.db.ts
@@ -161,13 +161,13 @@ describe("SessionHandler", () => {
 
             // Check cookie options
             expect(opts.httpOnly).to.equal(true);
             expect(opts.secure).to.equal(true);
             expect(opts.maxAge).to.equal(maxAge * 1000);
-            expect(opts.sameSite).to.equal("strict");
+            expect(opts.sameSite).to.equal("lax");
 
-            expect(name, "Check cookie name").to.equal("_gitpod_dev_jwt_");
+            expect(name, "Check cookie name").to.equal("__Host-_gitpod_dev_jwt_");
         });
     });
     describe("jwtSessionConvertor", () => {
         it("user is not authenticated", async () => {
             const res = await handle();
@@ -210,7 +210,37 @@ describe("SessionHandler", () => {
             const res = await handle(existingUser, "_gitpod_dev_jwt_=invalid");
             expect(res.status).to.equal(401);
             expect(res.value).to.equal("JWT Session is invalid");
             expect(res.cookie).to.be.undefined;
         });
+
+        it("old JWT cookie is present, is accepted (!), and we get a new one", async () => {
+            const oldExpiredCookie = await sessionHandler.createJWTSessionCookie(existingUser.id, {
+                issuedAtMs: Date.now() - SessionHandler.JWT_REFRESH_THRESHOLD - 1,
+            });
+            oldExpiredCookie.name = "_gitpod_dev_jwt_";
+            const newCookie = await sessionHandler.createJWTSessionCookie(existingUser.id);
+
+            const res = await handle(existingUser, `${oldExpiredCookie.name}=${oldExpiredCookie.value}`);
+            expect(res.status).to.equal(200);
+            expect(res.value).to.equal("Refreshed JWT cookie issued.");
+            expect(res.cookie).to.not.be.undefined;
+            expect(res.cookie?.split("=")[0]).to.equal(newCookie.name);
+        });
+
+        it("old expired AND new one JWT cookies are present, new one is accepted", async () => {
+            const oldExpiredCookie = await sessionHandler.createJWTSessionCookie(existingUser.id, {
+                issuedAtMs: Date.now() - SessionHandler.JWT_REFRESH_THRESHOLD - 1,
+            });
+            oldExpiredCookie.name = "_gitpod_dev_jwt_";
+            const newCookie = await sessionHandler.createJWTSessionCookie(existingUser.id);
+
+            const res = await handle(
+                existingUser,
+                `${oldExpiredCookie.name}=${oldExpiredCookie.value}; ${newCookie.name}=${newCookie.value}`,
+            );
+            expect(res.status).to.equal(200);
+            expect(res.value).to.equal("User session already has a valid JWT session.");
+            expect(res.cookie).to.be.undefined;
+        });
     });
 });
diff --git a/components/server/src/session-handler.ts b/components/server/src/session-handler.ts
index f2860be85..0fa308047 100644
--- a/components/server/src/session-handler.ts
+++ b/components/server/src/session-handler.ts
@@ -25,19 +25,19 @@ export class SessionHandler {
     @inject(AuthJWT) protected readonly authJWT: AuthJWT;
     @inject(UserService) protected userService: UserService;
 
     public jwtSessionConvertor(): express.Handler {
         return async (req, res) => {
-            const user = req.user;
+            const { user } = req;
             if (!user) {
                 res.status(401);
                 res.send("User has no valid session.");
                 return;
             }
 
             const cookies = parseCookieHeader(req.headers.cookie || "");
-            const jwtTokens = cookies[getJWTCookieName(this.config)];
+            const jwtTokens = this.filterCookieValues(cookies);
 
             let decoded: { payload: JwtPayload; keyId: string } | undefined = undefined;
             try {
                 // will throw if the token is expired
                 decoded = await this.verifyFirstValidJwt(jwtTokens);
@@ -144,16 +144,33 @@ export class SessionHandler {
      * @param cookie
      * @returns
      */
     async verifyJWTCookie(cookie: string): Promise<JwtPayload | undefined> {
         const cookies = parseCookieHeader(cookie);
-        const cookieValues = cookies[getJWTCookieName(this.config)];
+        const cookieValues = this.filterCookieValues(cookies);
 
         const token = await this.verifyFirstValidJwt(cookieValues);
         return token?.payload;
     }
 
+    /**
+     * @param cookies
+     * @returns Primary (the cookie name we set) AND secondary cookie (old accepted cookie name) values (in that order).
+     */
+    private filterCookieValues(cookies: { [key: string]: string[] }): string[] {
+        const cookieValues = cookies[getPrimaryJWTCookieName(this.config)] ?? [];
+
+        const secondaryCookieName = getSecondaryJWTCookieName(this.config);
+        if (secondaryCookieName) {
+            const secondaryCookieValues = cookies[secondaryCookieName];
+            if (secondaryCookieValues) {
+                cookieValues.push(...secondaryCookieValues);
+            }
+        }
+        return cookieValues;
+    }
+
     /**
      * Returns the first valid session token it finds.
      * Edge cases:
      *  - If there is no token, undefined is returned
      *  - If there is no valid token, throws the error of the first verification attempt.
@@ -202,35 +219,48 @@ export class SessionHandler {
             Object.assign(payload, { iat: Math.floor(options.issuedAtMs / 1000) });
         }
         const token = await this.authJWT.sign(userID, payload, options?.expirySeconds);
 
         return {
-            name: getJWTCookieName(this.config),
+            name: getPrimaryJWTCookieName(this.config),
             value: token,
             opts: {
-                domain: getJWTCookieDomain(this.config),
                 maxAge: this.config.auth.session.cookie.maxAge * 1000, // express does not match the HTTP spec and uses milliseconds
                 httpOnly: this.config.auth.session.cookie.httpOnly,
                 sameSite: this.config.auth.session.cookie.sameSite,
                 secure: this.config.auth.session.cookie.secure,
             },
         };
     }
 
-    public clearSessionCookie(res: express.Response, config: Config): void {
-        res.clearCookie(getJWTCookieName(this.config), {
-            domain: getJWTCookieDomain(config),
+    public clearSessionCookie(res: express.Response): void {
+        const { secure, sameSite, httpOnly } = this.config.auth.session.cookie;
+        res.clearCookie(getPrimaryJWTCookieName(this.config), {
+            httpOnly,
+            sameSite,
+            secure,
         });
+
+        const secondaryCookieName = getSecondaryJWTCookieName(this.config);
+        if (secondaryCookieName) {
+            res.clearCookie(secondaryCookieName, {
+                domain: this.config.hostUrl.url.hostname,
+            });
+        }
     }
 }
 
-function getJWTCookieName(config: Config) {
+function getPrimaryJWTCookieName(config: Config) {
     return config.auth.session.cookie.name;
 }
 
-function getJWTCookieDomain(config: Config): string {
-    return config.hostUrl.url.hostname;
+function getSecondaryJWTCookieName(config: Config) {
+    const PREFIX = "__Host-";
+    if (!config.auth.session.cookie.name.startsWith(PREFIX)) {
+        return undefined;
+    }
+    return config.auth.session.cookie.name.slice(PREFIX.length);
 }
 
 function parseCookieHeader(c: string): { [key: string]: string[] } {
     return c
         .split("; ")
diff --git a/components/server/src/test/service-testing-container-module.ts b/components/server/src/test/service-testing-container-module.ts
index 441bcb2c7..f494f7d12 100644
--- a/components/server/src/test/service-testing-container-module.ts
+++ b/components/server/src/test/service-testing-container-module.ts
@@ -53,15 +53,15 @@ export const mockAuthConfig: AuthConfig = {
     },
     session: {
         issuer: "https://mp-server-d7650ec945.preview.gitpod-dev.com",
         lifetimeSeconds: 7 * 24 * 60 * 60,
         cookie: {
-            name: "_gitpod_dev_jwt_",
+            name: "__Host-_gitpod_dev_jwt_",
             secure: true,
             httpOnly: true,
             maxAge: 7 * 24 * 60 * 60,
-            sameSite: "strict",
+            sameSite: "lax",
         },
     },
 };
 
 function toKeyPair(
diff --git a/components/server/src/user/user-controller.ts b/components/server/src/user/user-controller.ts
index 64fe6411b..5fa91dd28 100644
--- a/components/server/src/user/user-controller.ts
+++ b/components/server/src/user/user-controller.ts
@@ -35,10 +35,11 @@ import { GitpodServerImpl } from "../workspace/gitpod-server-impl";
 import { StopWorkspacePolicy } from "@gitpod/ws-manager/lib";
 import { UserService } from "./user-service";
 import { WorkspaceService } from "../workspace/workspace-service";
 import { runWithSubjectId } from "../util/request-context";
 import { SubjectId } from "../auth/subject-id";
+import { TrustedValue } from "@gitpod/gitpod-protocol/lib/util/scrubbing";
 
 export const ServerFactory = Symbol("ServerFactory");
 export type ServerFactory = () => GitpodServerImpl;
 
 @injectable()
@@ -211,10 +212,18 @@ export class UserController {
 
                 const cookie = await this.sessionHandler.createJWTSessionCookie(user.id);
                 res.cookie(cookie.name, cookie.value, cookie.opts);
                 reportJWTCookieIssued();
 
+                // If returnTo was passed and it's safe, redirect to it
+                const returnTo = this.getSafeReturnToParam(req);
+                if (returnTo) {
+                    log.info(`Redirecting after OTS login ${returnTo}`);
+                    res.redirect(returnTo);
+                    return;
+                }
+
                 res.sendStatus(200);
             }),
         );
 
         router.get("/authorize", (req: express.Request, res: express.Response, next: express.NextFunction) => {
@@ -267,11 +276,11 @@ export class UserController {
             if (req.isAuthenticated()) {
                 req.logout();
             }
 
             // clear cookies
-            this.sessionHandler.clearSessionCookie(res, this.config);
+            this.sessionHandler.clearSessionCookie(res);
 
             // then redirect
             log.info(logContext, "(Logout) Redirecting...", { redirectToUrl, ...logPayload });
             res.redirect(redirectToUrl);
         });
@@ -616,11 +625,11 @@ export class UserController {
             this.urlStartsWith(returnToURL, "https://www.gitpod.io")
         ) {
             return returnToURL;
         }
 
-        log.debug("The redirect URL does not match", { query: req.query });
+        log.debug("The redirect URL does not match", { query: new TrustedValue(req.query).value });
         return;
     }
 
     private createGitpodServer(user: User, resourceGuard: ResourceAccessGuard) {
         const server = this.serverFactory();
diff --git a/components/ws-proxy/pkg/proxy/routes.go b/components/ws-proxy/pkg/proxy/routes.go
index 4c783ad73..6af4d7bc5 100644
--- a/components/ws-proxy/pkg/proxy/routes.go
+++ b/components/ws-proxy/pkg/proxy/routes.go
@@ -820,12 +820,12 @@ func removeSensitiveCookies(cookies []*http.Cookie, domain string) []*http.Cooki
 	}
 	hostnamePrefix = "_" + hostnamePrefix + "_"
 
 	n := 0
 	for _, c := range cookies {
-		if strings.HasPrefix(c.Name, hostnamePrefix) {
-			// skip session cookie
+		if strings.HasPrefix(c.Name, hostnamePrefix) || strings.HasPrefix(c.Name, "__Host-"+hostnamePrefix) {
+			// skip session cookies
 			continue
 		}
 		log.WithField("hostnamePrefix", hostnamePrefix).WithField("name", c.Name).Debug("keeping cookie")
 		cookies[n] = c
 		n++
diff --git a/components/ws-proxy/pkg/proxy/routes_test.go b/components/ws-proxy/pkg/proxy/routes_test.go
index 0dbc08952..187eac00f 100644
--- a/components/ws-proxy/pkg/proxy/routes_test.go
+++ b/components/ws-proxy/pkg/proxy/routes_test.go
@@ -981,11 +981,11 @@ func TestNoSSHGatewayRouter(t *testing.T) {
 
 func TestRemoveSensitiveCookies(t *testing.T) {
 	var (
 		domain                  = "test-domain.com"
 		sessionCookie           = &http.Cookie{Domain: domain, Name: "_test_domain_com_", Value: "fobar"}
-		sessionCookieJwt2       = &http.Cookie{Domain: domain, Name: "_test_domain_com_jwt2_", Value: "fobar"}
+		sessionCookieJwt2       = &http.Cookie{Domain: domain, Name: "__Host-_test_domain_com_jwt2_", Value: "fobar"}
 		realGitpodSessionCookie = &http.Cookie{Domain: domain, Name: server_lib.CookieNameFromDomain(domain), Value: "fobar"}
 		portAuthCookie          = &http.Cookie{Domain: domain, Name: "_test_domain_com_ws_77f6b236_3456_4b88_8284_81ca543a9d65_port_auth_", Value: "some-token"}
 		ownerCookie             = &http.Cookie{Domain: domain, Name: "_test_domain_com_ws_77f6b236_3456_4b88_8284_81ca543a9d65_owner_", Value: "some-other-token"}
 		ownerCookieGen          = ownerTokenCookie(domain, "77f6b236_3456_4b88_8284_81ca543a9d65", "owner-token-gen")
 		miscCookie              = &http.Cookie{Domain: domain, Name: "some-other-cookie", Value: "I like cookies"}
diff --git a/install/installer/pkg/components/auth/config_test.go b/install/installer/pkg/components/auth/config_test.go
index c432feda7..99e806e54 100644
--- a/install/installer/pkg/components/auth/config_test.go
+++ b/install/installer/pkg/components/auth/config_test.go
@@ -17,46 +17,46 @@ func TestCookieNameFromDomain(t *testing.T) {
 		expectedOutcome string
 	}{
 		{
 			name:            "Simple Domain",
 			domain:          "example.com",
-			expectedOutcome: "_example_com_jwt2_",
+			expectedOutcome: "__Host-_example_com_jwt2_",
 		},
 		{
 			name:            "Domain with Underscore",
 			domain:          "example_test.com",
-			expectedOutcome: "_example_test_com_jwt2_",
+			expectedOutcome: "__Host-_example_test_com_jwt2_",
 		},
 		{
 			name:            "Domain with Hyphen",
 			domain:          "example-test.com",
-			expectedOutcome: "_example_test_com_jwt2_",
+			expectedOutcome: "__Host-_example_test_com_jwt2_",
 		},
 		{
 			name:            "Domain with Special Characters",
 			domain:          "example&test.com",
-			expectedOutcome: "_example_test_com_jwt2_",
+			expectedOutcome: "__Host-_example_test_com_jwt2_",
 		},
 		{
 			name:            "Subdomain",
 			domain:          "subdomain.example.com",
-			expectedOutcome: "_subdomain_example_com_jwt2_",
+			expectedOutcome: "__Host-_subdomain_example_com_jwt2_",
 		},
 		{
 			name:            "Subdomain with Hyphen",
 			domain:          "sub-domain.example.com",
-			expectedOutcome: "_sub_domain_example_com_jwt2_",
+			expectedOutcome: "__Host-_sub_domain_example_com_jwt2_",
 		},
 		{
 			name:            "Subdomain with Underscore",
 			domain:          "sub_domain.example.com",
-			expectedOutcome: "_sub_domain_example_com_jwt2_",
+			expectedOutcome: "__Host-_sub_domain_example_com_jwt2_",
 		},
 		{
 			name:            "Subdomain with Special Characters",
 			domain:          "sub&domain.example.com",
-			expectedOutcome: "_sub_domain_example_com_jwt2_",
+			expectedOutcome: "__Host-_sub_domain_example_com_jwt2_",
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/install/installer/pkg/components/public-api-server/configmap_test.go b/install/installer/pkg/components/public-api-server/configmap_test.go
index ea8b06fd3..29dcca9ee 100644
--- a/install/installer/pkg/components/public-api-server/configmap_test.go
+++ b/install/installer/pkg/components/public-api-server/configmap_test.go
@@ -61,11 +61,11 @@ func TestConfigMap(t *testing.T) {
 			},
 			Session: config.SessionConfig{
 				LifetimeSeconds: int64((24 * 7 * time.Hour).Seconds()),
 				Issuer:          "https://test.domain.everything.awesome.is",
 				Cookie: config.CookieConfig{
-					Name:     "_test_domain_everything_awesome_is_jwt2_",
+					Name:     "__Host-_test_domain_everything_awesome_is_jwt2_",
 					MaxAge:   int64((24 * 7 * time.Hour).Seconds()),
 					SameSite: "lax",
 					Secure:   true,
 					HTTPOnly: true,
 				},
diff --git a/install/installer/pkg/components/server/configmap_test.go b/install/installer/pkg/components/server/configmap_test.go
index 651189f94..a23df0f63 100644
--- a/install/installer/pkg/components/server/configmap_test.go
+++ b/install/installer/pkg/components/server/configmap_test.go
@@ -67,11 +67,11 @@ func TestConfigMap(t *testing.T) {
 			},
 			Session: auth.SessionConfig{
 				LifetimeSeconds: int64((7 * 24 * time.Hour).Seconds()),
 				Issuer:          "https://awesome.domain",
 				Cookie: auth.CookieConfig{
-					Name:     "_awesome_domain_jwt2_",
+					Name:     "__Host-_awesome_domain_jwt2_",
 					MaxAge:   int64((7 * 24 * time.Hour).Seconds()),
 					SameSite: "lax",
 					Secure:   true,
 					HTTPOnly: true,
 				},

Vulnerable code: 	return "_" + derived + "_jwt2_"
            expect(opts.sameSite).to.equal("strict");
            expect(name, "Check cookie name").to.equal("_gitpod_dev_jwt_");
            const user = req.user;
            const jwtTokens = cookies[getJWTCookieName(this.config)];
        const cookieValues = cookies[getJWTCookieName(this.config)];
            name: getJWTCookieName(this.config),
                domain: getJWTCookieDomain(this.config),
    public clearSessionCookie(res: express.Response, config: Config): void {
        res.clearCookie(getJWTCookieName(this.config), {
            domain: getJWTCookieDomain(config),
function getJWTCookieName(config: Config) {
function getJWTCookieDomain(config: Config): string {
    return config.hostUrl.url.hostname;
            name: "_gitpod_dev_jwt_",
            sameSite: "strict",
            this.sessionHandler.clearSessionCookie(res, this.config);
        log.debug("The redirect URL does not match", { query: req.query });
		if strings.HasPrefix(c.Name, hostnamePrefix) {
			// skip session cookie
		sessionCookieJwt2       = &http.Cookie{Domain: domain, Name: "_test_domain_com_jwt2_", Value: "fobar"}
			expectedOutcome: "_example_com_jwt2_",
			expectedOutcome: "_example_test_com_jwt2_",
			expectedOutcome: "_example_test_com_jwt2_",
			expectedOutcome: "_example_test_com_jwt2_",
			expectedOutcome: "_subdomain_example_com_jwt2_",
			expectedOutcome: "_sub_domain_example_com_jwt2_",
			expectedOutcome: "_sub_domain_example_com_jwt2_",
			expectedOutcome: "_sub_domain_example_com_jwt2_",
					Name:     "_test_domain_everything_awesome_is_jwt2_",
					Name:     "_awesome_domain_jwt2_",
Fixed code: 
    asLoginWithOTS(userId: string, key: string, returnToUrl?: string) {
        const result = this.withApi({ pathname: `/login/ots/${userId}/${key}` });
        if (returnToUrl) {
            return result.with({ search: `returnTo=${encodeURIComponent(returnToUrl)}` });
        }
        return result;
    }
	return "__Host-_" + derived + "_jwt2_"
        if (!this.loginCompletionHandler.isBaseDomain(request)) {
            // For auth requests that are not targetting the base domain, we redirect to the base domain, so they come with our cookie.
            log.info(`(${strategyName}) Auth request on subdomain, redirecting to base domain`, { clientInfo });
            const target = new URL(request.url, this.config.hostUrl.url.toString()).toString();
            response.redirect(target);
            return;
        }

import * as crypto from "crypto";
import { OneTimeSecretServer } from "../one-time-secret-server";
    @inject(OneTimeSecretServer) private readonly otsServer: OneTimeSecretServer;
        if (!this.isBaseDomain(request)) {
            // (GitHub edge case) If we got redirected here onto a sub-domain (e.g. api.gitpod.io), we need to redirect to the base domain in order to Set-Cookie properly.
            const secret = crypto
                .createHash("sha256")
                .update(user.id + this.config.session.secret)
                .digest("hex");
            const expirationDate = new Date(Date.now() + 1000 * 60); // 1 minutes
            const token = await this.otsServer.serveToken({}, secret, expirationDate);

            reportLoginCompleted("succeeded_via_ots", "git");
            log.info(
                logContext,
                `User will be logged in via OTS on the base domain. (Indirect) redirect to: ${returnTo}`,
            );
            const baseDomainRedirect = this.config.hostUrl.asLoginWithOTS(user.id, token.token, returnTo).toString();
            response.redirect(baseDomainRedirect);
            return;
        }

        // (default case) If we got redirected here onto the base domain of the Gitpod installation, we can just issue the cookie right away.
    public isBaseDomain(req: express.Request): boolean {
        return req.hostname === this.config.hostUrl.url.hostname;
    }

    // The login was successful, but we need to defer cookie creation via an OTS
    | "succeeded_via_ots"
            expect(opts.sameSite).to.equal("lax");
            expect(name, "Check cookie name").to.equal("__Host-_gitpod_dev_jwt_");

        it("old JWT cookie is present, is accepted (!), and we get a new one", async () => {
            const oldExpiredCookie = await sessionHandler.createJWTSessionCookie(existingUser.id, {
                issuedAtMs: Date.now() - SessionHandler.JWT_REFRESH_THRESHOLD - 1,
            });
            oldExpiredCookie.name = "_gitpod_dev_jwt_";
            const newCookie = await sessionHandler.createJWTSessionCookie(existingUser.id);

            const res = await handle(existingUser, `${oldExpiredCookie.name}=${oldExpiredCookie.value}`);
            expect(res.status).to.equal(200);
            expect(res.value).to.equal("Refreshed JWT cookie issued.");
            expect(res.cookie).to.not.be.undefined;
            expect(res.cookie?.split("=")[0]).to.equal(newCookie.name);
        });

        it("old expired AND new one JWT cookies are present, new one is accepted", async () => {
            const oldExpiredCookie = await sessionHandler.createJWTSessionCookie(existingUser.id, {
                issuedAtMs: Date.now() - SessionHandler.JWT_REFRESH_THRESHOLD - 1,
            });
            oldExpiredCookie.name = "_gitpod_dev_jwt_";
            const newCookie = await sessionHandler.createJWTSessionCookie(existingUser.id);

            const res = await handle(
                existingUser,
                `${oldExpiredCookie.name}=${oldExpiredCookie.value}; ${newCookie.name}=${newCookie.value}`,
            );
            expect(res.status).to.equal(200);
            expect(res.value).to.equal("User session already has a valid JWT session.");
            expect(res.cookie).to.be.undefined;
        });
            const { user } = req;
            const jwtTokens = this.filterCookieValues(cookies);
        const cookieValues = this.filterCookieValues(cookies);
    /**
     * @param cookies
     * @returns Primary (the cookie name we set) AND secondary cookie (old accepted cookie name) values (in that order).
     */
    private filterCookieValues(cookies: { [key: string]: string[] }): string[] {
        const cookieValues = cookies[getPrimaryJWTCookieName(this.config)] ?? [];

        const secondaryCookieName = getSecondaryJWTCookieName(this.config);
        if (secondaryCookieName) {
            const secondaryCookieValues = cookies[secondaryCookieName];
            if (secondaryCookieValues) {
                cookieValues.push(...secondaryCookieValues);
            }
        }
        return cookieValues;
    }

            name: getPrimaryJWTCookieName(this.config),
    public clearSessionCookie(res: express.Response): void {
        const { secure, sameSite, httpOnly } = this.config.auth.session.cookie;
        res.clearCookie(getPrimaryJWTCookieName(this.config), {
            httpOnly,
            sameSite,
            secure,

        const secondaryCookieName = getSecondaryJWTCookieName(this.config);
        if (secondaryCookieName) {
            res.clearCookie(secondaryCookieName, {
                domain: this.config.hostUrl.url.hostname,
            });
        }
function getPrimaryJWTCookieName(config: Config) {
function getSecondaryJWTCookieName(config: Config) {
    const PREFIX = "__Host-";
    if (!config.auth.session.cookie.name.startsWith(PREFIX)) {
        return undefined;
    }
    return config.auth.session.cookie.name.slice(PREFIX.length);
            name: "__Host-_gitpod_dev_jwt_",
            sameSite: "lax",
import { TrustedValue } from "@gitpod/gitpod-protocol/lib/util/scrubbing";
                // If returnTo was passed and it's safe, redirect to it
                const returnTo = this.getSafeReturnToParam(req);
                if (returnTo) {
                    log.info(`Redirecting after OTS login ${returnTo}`);
                    res.redirect(returnTo);
                    return;
                }

            this.sessionHandler.clearSessionCookie(res);
        log.debug("The redirect URL does not match", { query: new TrustedValue(req.query).value });
		if strings.HasPrefix(c.Name, hostnamePrefix) || strings.HasPrefix(c.Name, "__Host-"+hostnamePrefix) {
			// skip session cookies
		sessionCookieJwt2       = &http.Cookie{Domain: domain, Name: "__Host-_test_domain_com_jwt2_", Value: "fobar"}
			expectedOutcome: "__Host-_example_com_jwt2_",
			expectedOutcome: "__Host-_example_test_com_jwt2_",
			expectedOutcome: "__Host-_example_test_com_jwt2_",
			expectedOutcome: "__Host-_example_test_com_jwt2_",
			expectedOutcome: "__Host-_subdomain_example_com_jwt2_",
			expectedOutcome: "__Host-_sub_domain_example_com_jwt2_",
			expectedOutcome: "__Host-_sub_domain_example_com_jwt2_",
			expectedOutcome: "__Host-_sub_domain_example_com_jwt2_",
					Name:     "__Host-_test_domain_everything_awesome_is_jwt2_",
					Name:     "__Host-_awesome_domain_jwt2_",