Skip to content

Commit c4ee3d7

Browse files
authored
fix: harden gateway auth with timing-safe compare, null checks, trust proxy (#137)
Co-authored-by: Flegma <Flegma@users.noreply.github.com>
1 parent 6201579 commit c4ee3d7

3 files changed

Lines changed: 62 additions & 28 deletions

File tree

src/main.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,7 @@ async function bootstrap() {
6363
},
6464
});
6565

66-
app.set("trust proxy", () => {
67-
// TODO - trust proxy
68-
return true;
69-
});
66+
app.set("trust proxy", 1);
7067

7168
const appConfig = configService.get<AppConfig>("app");
7269

src/matches/match-events.gateway.ts

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
SubscribeMessage,
55
WebSocketGateway,
66
} from "@nestjs/websockets";
7+
import { timingSafeEqual } from "crypto";
78
import WebSocket from "ws";
89
import { Request } from "express";
910
import { ModuleRef } from "@nestjs/core";
@@ -29,37 +30,58 @@ export class MatchEventsGateway {
2930
private readonly cache: CacheService,
3031
) {}
3132

33+
private safeCompare(a: string, b: string): boolean {
34+
if (a.length !== b.length) return false;
35+
return timingSafeEqual(Buffer.from(a), Buffer.from(b));
36+
}
37+
3238
async handleConnection(
3339
@ConnectedSocket() client: WebSocket.WebSocket,
3440
request: Request,
3541
) {
3642
try {
3743
const authHeader = request.headers.authorization;
3844

39-
if (authHeader && authHeader.startsWith("Basic ")) {
40-
const base64Credentials = authHeader.split(" ").at(1);
45+
if (!authHeader || !authHeader.startsWith("Basic ")) {
46+
client.close();
47+
return;
48+
}
49+
50+
const base64Credentials = authHeader.split(" ").at(1);
51+
if (!base64Credentials) {
52+
client.close();
53+
return;
54+
}
55+
56+
const decoded = Buffer.from(base64Credentials, "base64").toString();
57+
const colonIndex = decoded.indexOf(":");
58+
if (colonIndex === -1) {
59+
client.close();
60+
return;
61+
}
4162

42-
const [serverId, apiPassword] = Buffer.from(base64Credentials, "base64")
43-
.toString()
44-
.split(":");
63+
const serverId = decoded.substring(0, colonIndex);
64+
const apiPassword = decoded.substring(colonIndex + 1);
4565

46-
const { servers_by_pk } = await this.hasura.query({
47-
servers_by_pk: {
48-
__args: {
49-
id: serverId,
50-
},
51-
id: true,
52-
api_password: true,
66+
const { servers_by_pk } = await this.hasura.query({
67+
servers_by_pk: {
68+
__args: {
69+
id: serverId,
5370
},
71+
id: true,
72+
api_password: true,
73+
},
74+
});
75+
76+
if (
77+
!servers_by_pk?.api_password ||
78+
!this.safeCompare(servers_by_pk.api_password, apiPassword)
79+
) {
80+
client.close();
81+
this.logger.warn("game server auth failure", {
82+
serverId,
83+
ip: request.headers["cf-connecting-ip"],
5484
});
55-
56-
if (servers_by_pk?.api_password !== apiPassword) {
57-
client.close();
58-
this.logger.warn("game server auth failure", {
59-
serverId,
60-
ip: request.headers["cf-connecting-ip"],
61-
});
62-
}
6385
}
6486
} catch {
6587
client.close();

src/matches/match-relay/match-relay-auth-middleware.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { timingSafeEqual } from "crypto";
12
import { CacheService } from "src/cache/cache.service";
23
import { Request, Response, NextFunction } from "express";
34
import { HasuraService } from "src/hasura/hasura.service";
@@ -11,11 +12,25 @@ export class MatchRelayAuthMiddleware implements NestMiddleware {
1112
private readonly hasura: HasuraService,
1213
) {}
1314

15+
private safeCompare(a: string, b: string): boolean {
16+
if (a.length !== b.length) return false;
17+
return timingSafeEqual(Buffer.from(a), Buffer.from(b));
18+
}
19+
1420
async use(request: Request, response: Response, next: NextFunction) {
1521
try {
16-
const [matchId, apiPassword] = (
17-
request.headers["x-origin-auth"] as string
18-
)?.split(":");
22+
const originAuth = request.headers["x-origin-auth"];
23+
if (!originAuth || typeof originAuth !== "string") {
24+
return response.status(401).end();
25+
}
26+
27+
const colonIndex = originAuth.indexOf(":");
28+
if (colonIndex === -1) {
29+
return response.status(401).end();
30+
}
31+
32+
const matchId = originAuth.substring(0, colonIndex);
33+
const apiPassword = originAuth.substring(colonIndex + 1);
1934

2035
const token = request.url.split("/")?.[3];
2136

@@ -36,7 +51,7 @@ export class MatchRelayAuthMiddleware implements NestMiddleware {
3651
60 * 1000,
3752
);
3853

39-
if (matchPassword !== apiPassword) {
54+
if (!matchPassword || !this.safeCompare(matchPassword, apiPassword)) {
4055
return response.status(401).end();
4156
}
4257
} catch (error) {

0 commit comments

Comments
 (0)