Skip to content

Commit 192b1ba

Browse files
committed
Address feedback
1 parent 0599110 commit 192b1ba

2 files changed

Lines changed: 534 additions & 40 deletions

File tree

packages/realm-server/handlers/webhook-filter-handlers.ts

Lines changed: 47 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ function extractPrBodyFromPayload(payload: Record<string, any>): string | null {
8686
/**
8787
* Look up the realm URL for a PrCard with the given PR number by querying
8888
* the card index database.
89+
*
90+
* The query restricts results to PrCard instances (URL contains '/PrCard/')
91+
* to avoid matching GithubEventCard instances which also carry a prNumber
92+
* field but may exist in a different realm.
8993
*/
9094
async function lookupRealmByPrNumber(
9195
dbAdapter: DBAdapter,
@@ -96,8 +100,10 @@ async function lookupRealmByPrNumber(
96100
`SELECT realm_url FROM boxel_index`,
97101
`WHERE type = 'instance'`,
98102
`AND is_deleted = FALSE`,
103+
`AND url LIKE '%/PrCard/%'`,
99104
`AND search_doc->>'prNumber' =`,
100105
param(String(prNumber)),
106+
`ORDER BY indexed_at DESC`,
101107
`LIMIT 1`,
102108
]);
103109
if (rows.length > 0) {
@@ -110,34 +116,45 @@ async function lookupRealmByPrNumber(
110116
}
111117

112118
/**
113-
* Resolve the realm URL dynamically from the GitHub webhook payload.
119+
* Resolve the origin of the realm that a GitHub webhook event belongs to.
114120
*
115121
* Strategy:
116-
* 1. If the payload contains a PR body, extract the realm from the Submission Card URL
117-
* 2. Otherwise, look up the PrCard by prNumber in the index DB
118-
* 3. Fall back to the static filter.realm if neither works
122+
* 1. If the payload contains a PR body, extract the origin from the Submission Card URL
123+
* 2. Otherwise, look up the PrCard by prNumber in the index DB and extract its origin
124+
*
125+
* Returns null if the origin cannot be determined.
119126
*/
120-
async function resolveRealmFromPayload(
127+
async function resolveOriginFromPayload(
121128
payload: Record<string, any>,
122-
filter: Record<string, any>,
123129
dbAdapter?: DBAdapter,
124130
): Promise<string | null> {
125131
// Strategy 1: Extract from PR body (available on pull_request, pull_request_review events)
126132
let prBody = extractPrBodyFromPayload(payload);
127133
let realm = extractRealmFromPrBody(prBody);
128-
if (realm) return realm;
134+
if (realm) {
135+
try {
136+
return new URL(realm).origin;
137+
} catch {
138+
// fall through
139+
}
140+
}
129141

130142
// Strategy 2: Look up PrCard by prNumber (for check_run, check_suite events)
131143
if (dbAdapter) {
132144
let prNumber = extractPrNumberFromPayload(payload);
133145
if (prNumber != null) {
134-
realm = await lookupRealmByPrNumber(dbAdapter, prNumber);
135-
if (realm) return realm;
146+
let realmUrl = await lookupRealmByPrNumber(dbAdapter, prNumber);
147+
if (realmUrl) {
148+
try {
149+
return new URL(realmUrl).origin;
150+
} catch {
151+
// fall through
152+
}
153+
}
136154
}
137155
}
138156

139-
// Strategy 3: Fall back to static filter.realm
140-
return (filter.realm as string) ?? null;
157+
return null;
141158
}
142159

143160
/**
@@ -166,37 +183,32 @@ class GithubEventFilterHandler implements WebhookFilterHandler {
166183
// If that's not available, fall back to the full resolution strategy
167184
// (which may query the DB for check_run/check_suite events).
168185
if (filter.realm) {
169-
let prBody = extractPrBodyFromPayload(payload);
170-
let resolvedRealm = extractRealmFromPrBody(prBody);
186+
let resolvedOrigin = await resolveOriginFromPayload(payload, dbAdapter);
171187

172-
if (!resolvedRealm && dbAdapter) {
173-
let prNumber = extractPrNumberFromPayload(payload);
174-
if (prNumber != null) {
175-
resolvedRealm = await lookupRealmByPrNumber(dbAdapter, prNumber);
176-
}
177-
}
178-
179-
if (resolvedRealm) {
188+
if (resolvedOrigin) {
180189
try {
181190
let filterOrigin = new URL(filter.realm as string).origin;
182-
let resolvedOrigin = new URL(resolvedRealm).origin;
183191
if (filterOrigin !== resolvedOrigin) {
184192
return false;
185193
}
186194
} catch {
195+
// filter.realm is malformed — reject to avoid bypassing origin check
187196
console.warn(
188-
`Failed to compare realm origins for webhook filter ` +
189-
`(filter.realm=${filter.realm}, resolvedRealm=${resolvedRealm}), ` +
190-
`allowing match as fallback`,
197+
`Failed to parse filter.realm URL (${filter.realm}), rejecting match`,
191198
);
199+
return false;
192200
}
193201
} else {
202+
// Could not resolve origin from payload — reject the match to prevent
203+
// cross-environment broadcast. This is the safer default: if we can't
204+
// determine which environment the PR belongs to, don't process it.
194205
let prNumber = extractPrNumberFromPayload(payload);
195206
console.warn(
196-
`Could not resolve realm from webhook payload ` +
197-
`(eventType=${eventType}, prNumber=${prNumber ?? 'unknown'}). ` +
198-
`Falling back to broadcast — event will be processed by all environments.`,
207+
`Could not resolve realm origin from webhook payload ` +
208+
`(eventType=${eventType}, prNumber=${prNumber ?? 'unknown'}), ` +
209+
`rejecting match`,
199210
);
211+
return false;
200212
}
201213
}
202214

@@ -207,16 +219,16 @@ class GithubEventFilterHandler implements WebhookFilterHandler {
207219
payload: Record<string, any>,
208220
headers: Koa.Context['req']['headers'],
209221
filter: Record<string, any>,
210-
dbAdapter?: DBAdapter,
211222
): Promise<Record<string, any>> {
212223
let eventType = (headers['x-github-event'] as string) ?? '';
213224

214-
let realm = await resolveRealmFromPayload(payload, filter, dbAdapter);
225+
// Always use the static filter.realm (the submissions realm) for the
226+
// command input. The dynamic origin check in matches() already ensures
227+
// we only reach here for the correct environment.
228+
let realm = filter.realm as string | undefined;
215229
if (!realm) {
216230
throw new Error(
217-
'Could not determine realm for github-event webhook command: ' +
218-
'no Submission Card URL found in PR body, no PrCard found in index, ' +
219-
'and no static realm configured in filter',
231+
'realm must be provided in the filter for github-event webhook commands',
220232
);
221233
}
222234

@@ -230,14 +242,9 @@ class GithubEventFilterHandler implements WebhookFilterHandler {
230242
async getRealmURL(
231243
filter: Record<string, any>,
232244
commandURL: string,
233-
payload?: Record<string, any>,
234-
_headers?: Koa.Context['req']['headers'],
235-
dbAdapter?: DBAdapter,
236245
): Promise<string> {
237-
if (payload) {
238-
let realm = await resolveRealmFromPayload(payload, filter, dbAdapter);
239-
if (realm) return realm;
240-
}
246+
// Always use the static filter.realm. The dynamic origin check in
247+
// matches() already ensures we only reach here for the correct environment.
241248
return (
242249
(filter.realm as string | undefined) ??
243250
new URL('/submissions/', commandURL).href

0 commit comments

Comments
 (0)