Skip to content

Commit 80a701b

Browse files
committed
fix(webapp): harden session channel routes from review
Reject channel initialize on an expired session (matching the append route), sanitize a client-forged webhook action source on channel .in appends, resolve the channel records route replica-first with a writer fallback so a fresh session isn't a spurious 404, and URL-encode the session id and channel in the span inspector's channel stream path.
1 parent ba3fbc6 commit 80a701b

4 files changed

Lines changed: 15 additions & 10 deletions

File tree

apps/webapp/app/routes/realtime.v1.sessions.$session.channels.$channel.$io.append.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { nanoid } from "nanoid";
44
import { z } from "zod";
55
import { logger } from "~/services/logger.server";
66
import { S2RealtimeStreams } from "~/services/realtime/s2realtimeStreams.server";
7+
import { stripClientWebhookActionSource } from "~/services/realtime/sanitizeSessionInput.server";
78
import {
89
SESSION_CHANNEL_NAME_REGEX,
910
sessionChannelResources,
@@ -83,7 +84,10 @@ const { action, loader } = createActionApiRoute(
8384
const addressingKey = canonicalSessionAddressingKey(session, params.session);
8485
const claimKey = `${addressingKey}:channels:${params.channel}`;
8586

86-
const part = await request.text();
87+
let part = await request.text();
88+
if (params.io === "in") {
89+
part = stripClientWebhookActionSource(part);
90+
}
8791

8892
const clientPartId = request.headers.get("X-Part-Id");
8993
const partId = clientPartId ?? nanoid(7);

apps/webapp/app/routes/realtime.v1.sessions.$session.channels.$channel.$io.records.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { json } from "@remix-run/server-runtime";
22
import { z } from "zod";
3-
import { $replica } from "~/db.server";
43
import { S2RealtimeStreams } from "~/services/realtime/s2realtimeStreams.server";
54
import {
65
SESSION_CHANNEL_NAME_REGEX,
@@ -9,7 +8,7 @@ import {
98
import {
109
canonicalSessionAddressingKey,
1110
isSessionFriendlyIdForm,
12-
resolveSessionByIdOrExternalId,
11+
resolveSessionWithWriterFallback,
1312
} from "~/services/realtime/sessions.server";
1413
import { getRealtimeStreamInstance } from "~/services/realtime/v1StreamsGlobal.server";
1514
import { anyResource, createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
@@ -31,11 +30,7 @@ export const loader = createLoaderApiRoute(
3130
allowJWT: true,
3231
corsStrategy: "all",
3332
findResource: async (params, auth) => {
34-
const row = await resolveSessionByIdOrExternalId(
35-
$replica,
36-
auth.environment.id,
37-
params.session
38-
);
33+
const row = await resolveSessionWithWriterFallback(auth.environment.id, params.session);
3934
if (!row && isSessionFriendlyIdForm(params.session)) {
4035
return undefined;
4136
}

apps/webapp/app/routes/realtime.v1.sessions.$session.channels.$channel.$io.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ const { action } = createActionApiRoute(
5656
return new Response("Cannot initialize a channel on a closed session", { status: 400 });
5757
}
5858

59+
if (maybeSession?.expiresAt && maybeSession.expiresAt.getTime() < Date.now()) {
60+
return new Response("Cannot initialize a channel on an expired session", { status: 400 });
61+
}
62+
5963
const realtimeStream = getRealtimeStreamInstance(authentication.environment, "v2", {
6064
session: maybeSession,
6165
organization: maybeSession ? null : authentication.environment.organization,

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1805,8 +1805,10 @@ function SpanEntity({ span }: { span: Span }) {
18051805
}
18061806
case "session-stream": {
18071807
const { runId, sessionId, channel, io } = span.entity.object;
1808-
const base = `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/runs/${runId}/realtime/v1/sessions/${sessionId}`;
1809-
const resourcePath = channel ? `${base}/channels/${channel}/${io}` : `${base}/${io}`;
1808+
const base = `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/runs/${runId}/realtime/v1/sessions/${encodeURIComponent(sessionId)}`;
1809+
const resourcePath = channel
1810+
? `${base}/channels/${encodeURIComponent(channel)}/${io}`
1811+
: `${base}/${io}`;
18101812
const displayName = channel ? `${channel}.${io}` : `${sessionId}.${io}`;
18111813
return (
18121814
<RealtimeStreamViewer

0 commit comments

Comments
 (0)