Skip to content

[API] Minor robustness: tautological bake auth check and redis handler crash on malformed input #557

Description

@Flegma

Two low-severity api robustness gaps grouped for one PR.

Bake controller 'x-origin-auth' check is tautological, allowing snapshot/status spoofing

Location: src/matches/game-streamer/game-server-node-bake.controller.ts:40 (api)

What: reportBakeStatus (line 39-43) and putSnapshot (line 70-74) authenticate by requiring the request header x-origin-auth to equal the :nodeId path parameter. Since the nodeId is already part of the request URL the caller controls, this is not a secret: any caller can trivially set x-origin-auth to the same value they put in the path. storeSnapshot writes the uploaded image into Redis and broadcasts snapshot:updated to viewers (game-streamer.service.ts lines 171-188). Exposure is limited because game_server_nodes.id is only selectable by the administrator role in Hasura (public_game_server_nodes.yaml), so an attacker must first learn/guess a node UUID.

Impact: An attacker who learns a game_server_node UUID POSTs /game-server-nodes//snapshot with x-origin-auth: and a chosen image; the check passes and the forged image is cached and pushed to all viewers as the node's bake snapshot, and /bake-status lets them report fake bake progress for that node.

Suggested fix: Replace the header==path check with a real per-node shared secret (or the game-server-node signing scheme used elsewhere) that is not derivable from the URL, and reject requests whose credential is not independently verified.

Verifier evidence

game-server-node-bake.controller.ts:39-40 const auth = request.headers["x-origin-auth"]; / if (auth !== nodeId) { (also :70-71 for snapshot). Controller decorator :21 @Controller("game-server-nodes/:nodeId") and :30 @Param("nodeId") nodeId: string confirm nodeId is caller-supplied URL. Contrast: game-streamer.controller.ts:42 !(await this.gameStreamer.validateStatusOriginAuth(matchId, request.headers["x-origin-auth"])); game-streamer.service.ts:2704-2707 splits header and does timingSafeStringEqual(matchPassword, apiPassword) against matches_by_pk.password. storeSnapshot side effect game-streamer.service.ts:180-187 this.redis.setex(snapshotRedisKey(kind,id)...) then this.redis.publish("broadcast-message", JSON.stringify({ event: "snapshot:updated", data: { kind, id } })). game-streamer.module.ts has no configure()/NestModule; matches.module.ts:395-399 forRoutes only cover matches/demos/match-relay paths, not game-server-nodes/:nodeId.


Redis pub/sub 'message' handler parses payload without try/catch, crashing the pod on malformed input

Location: src/sockets/sockets.service.ts:44 (api)

What: The constructor subscribes to the 'broadcast-message' and 'send-message-to-steam-id' Redis channels and, inside the ioredis sub.on('message', ...) listener, calls JSON.parse(message) and destructures the result with no try/catch. A synchronous throw inside an EventEmitter listener propagates out of ioredis's internal emit(); combined with the absence of any process.on('uncaughtException') handler in main.ts, an exception here terminates the entire process rather than being contained to the one message.

Impact: Any code path (another service, a future publisher, a manual redis-cli PUBLISH, or a truncated payload) that publishes a non-JSON or shape-mismatched message to broadcast-message or send-message-to-steam-id makes JSON.parse throw synchronously in the message listener -> uncaughtException with no handler -> the api pod exits, dropping all live WebSocket clients and in-flight requests on that instance.

Suggested fix: Wrap the parse-and-dispatch body of the sub.on('message', ...) callback in try/catch, log and drop on parse failure, and validate that steamId/event/data are present before dispatching.

Verifier evidence

sockets.service.ts:43-48: sub.on("message", (channel, message) => { const { steamId, event, data } = JSON.parse(message) as {...}, no try/catch around JSON.parse. main.ts:117-119 only handles unhandledRejection: process.on("unhandledRejection", (reason, p) => { console.warn(...) }); and grep for "uncaughtException" across src/ returns nothing. Internal publishers always stringify, e.g. chat.service.ts:391-397 this.redis.publish("send-message-to-steam-id", JSON.stringify({ steamId, event: eventName, data })) and matchmake.service.ts:113-120.


Found by the 2026-07 multi-agent code audit; adversarially verified (CONFIRMED, P3). One of a batch of findings from that pass.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3-lowQuality of lifeaudit-2026-07Findings from the 2026-07 code audit / review passreliabilityReliability or availability concernsecuritySecurity vulnerability or hardeningservice:api5stackgg/api service

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions