Skip to content

Commit f8aa995

Browse files
authored
fix(google-meet): validate chrome node policy params (#103752)
1 parent 8756cc4 commit f8aa995

2 files changed

Lines changed: 97 additions & 22 deletions

File tree

extensions/google-meet/src/node-invoke-policy.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,48 @@ describe("Google Meet node invoke policy", () => {
121121
expect(invokeNode).toHaveBeenCalledWith({ params: { action: "setup" } });
122122
});
123123

124+
it("rejects malformed bridge control before node dispatch", async () => {
125+
const policy = createGoogleMeetChromeNodeInvokePolicy(resolveGoogleMeetConfig({}));
126+
127+
for (const params of [
128+
{ action: "pullAudio" },
129+
{ action: "pushAudio", bridgeId: "bridge-1" },
130+
{ action: "clearAudio", bridgeId: "" },
131+
{ action: "stopByUrl" },
132+
{ action: "stopByUrl", url: "https://example.com/not-meet" },
133+
]) {
134+
const { ctx, invokeNode } = createContext(params);
135+
136+
await expect(policy.handle(ctx)).resolves.toMatchObject({
137+
ok: false,
138+
code: "GOOGLE_MEET_NODE_POLICY_DENIED",
139+
});
140+
expect(invokeNode).not.toHaveBeenCalled();
141+
}
142+
});
143+
144+
it("normalizes forwarded Meet URL filters before node dispatch", async () => {
145+
const policy = createGoogleMeetChromeNodeInvokePolicy(resolveGoogleMeetConfig({}));
146+
const { ctx, invokeNode } = createContext({
147+
action: "stopByUrl",
148+
url: "https://meet.google.com/abc-defg-hij?authuser=1",
149+
mode: "agent",
150+
exceptBridgeId: "bridge-1",
151+
audioBridgeCommand: ["node", "-e", "process.exit(99)"],
152+
});
153+
154+
await policy.handle(ctx);
155+
156+
expect(invokeNode).toHaveBeenCalledWith({
157+
params: {
158+
action: "stopByUrl",
159+
url: "https://meet.google.com/abc-defg-hij?authuser=1",
160+
mode: "agent",
161+
exceptBridgeId: "bridge-1",
162+
},
163+
});
164+
});
165+
124166
it("rejects unsupported googlemeet.chrome actions before node dispatch", async () => {
125167
const policy = createGoogleMeetChromeNodeInvokePolicy(resolveGoogleMeetConfig({}));
126168
const { ctx, invokeNode } = createContext({ action: "exec", command: ["id"] });

extensions/google-meet/src/node-invoke-policy.ts

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -91,71 +91,104 @@ function buildStartParams(
9191
return approved(startParams);
9292
}
9393

94-
function buildForwardParams(params: Record<string, unknown>): Record<string, unknown> | null {
94+
function denyMissing(action: string, field: string): PolicyDecision {
95+
return {
96+
approved: false,
97+
result: denied(`googlemeet.chrome ${action} requires ${field}`),
98+
};
99+
}
100+
101+
function buildForwardParams(params: Record<string, unknown>): PolicyDecision | null {
95102
const action = readString(params.action);
96103
switch (action) {
97104
case "setup":
98-
return { action };
105+
return approved({ action });
99106
case "status": {
100107
const bridgeId = readString(params.bridgeId);
101-
return bridgeId ? { action, bridgeId } : { action };
108+
return approved(bridgeId ? { action, bridgeId } : { action });
102109
}
103110
case "list": {
104111
const forwarded: Record<string, unknown> = { action };
105112
const url = readString(params.url);
106113
const mode = readString(params.mode);
107114
if (url) {
108-
forwarded.url = url;
115+
try {
116+
forwarded.url = normalizeMeetUrl(url);
117+
} catch (error) {
118+
return {
119+
approved: false,
120+
result: denied(error instanceof Error ? error.message : "googlemeet.chrome list url"),
121+
};
122+
}
109123
}
110124
if (mode) {
111125
forwarded.mode = mode;
112126
}
113-
return forwarded;
127+
return approved(forwarded);
114128
}
115129
case "stopByUrl": {
116130
const forwarded: Record<string, unknown> = { action };
117131
const url = readString(params.url);
118132
const mode = readString(params.mode);
119133
const exceptBridgeId = readString(params.exceptBridgeId);
120-
if (url) {
121-
forwarded.url = url;
134+
if (!url) {
135+
return denyMissing(action, "url");
136+
}
137+
try {
138+
forwarded.url = normalizeMeetUrl(url);
139+
} catch (error) {
140+
return {
141+
approved: false,
142+
result: denied(
143+
error instanceof Error ? error.message : "googlemeet.chrome stopByUrl url",
144+
),
145+
};
122146
}
123147
if (mode) {
124148
forwarded.mode = mode;
125149
}
126150
if (exceptBridgeId) {
127151
forwarded.exceptBridgeId = exceptBridgeId;
128152
}
129-
return forwarded;
153+
return approved(forwarded);
130154
}
131155
case "pullAudio": {
132156
const forwarded: Record<string, unknown> = { action };
133157
const bridgeId = readString(params.bridgeId);
134158
const timeoutMs = readPositiveNumber(params.timeoutMs);
135-
if (bridgeId) {
136-
forwarded.bridgeId = bridgeId;
159+
if (!bridgeId) {
160+
return denyMissing(action, "bridgeId");
137161
}
162+
forwarded.bridgeId = bridgeId;
138163
if (timeoutMs) {
139164
forwarded.timeoutMs = timeoutMs;
140165
}
141-
return forwarded;
166+
return approved(forwarded);
142167
}
143168
case "pushAudio": {
144169
const forwarded: Record<string, unknown> = { action };
145170
const bridgeId = readString(params.bridgeId);
146171
const base64 = readString(params.base64);
147-
if (bridgeId) {
148-
forwarded.bridgeId = bridgeId;
172+
if (!bridgeId) {
173+
return denyMissing(action, "bridgeId");
174+
}
175+
if (!base64) {
176+
return denyMissing(action, "base64");
149177
}
150-
if (base64) {
151-
forwarded.base64 = base64;
178+
forwarded.bridgeId = bridgeId;
179+
forwarded.base64 = base64;
180+
return approved(forwarded);
181+
}
182+
case "clearAudio": {
183+
const bridgeId = readString(params.bridgeId);
184+
if (!bridgeId) {
185+
return denyMissing(action, "bridgeId");
152186
}
153-
return forwarded;
187+
return approved({ action, bridgeId });
154188
}
155-
case "clearAudio":
156189
case "stop": {
157190
const bridgeId = readString(params.bridgeId);
158-
return bridgeId ? { action, bridgeId } : { action };
191+
return approved(bridgeId ? { action, bridgeId } : { action });
159192
}
160193
default:
161194
return null;
@@ -178,10 +211,10 @@ export function createGoogleMeetChromeNodeInvokePolicy(
178211
if (action === "start") {
179212
decision = buildStartParams(params, config);
180213
} else {
181-
const forwardParams = buildForwardParams(params);
182-
decision = forwardParams
183-
? approved(forwardParams)
184-
: { approved: false, result: denied("unsupported googlemeet.chrome action") };
214+
decision = buildForwardParams(params) ?? {
215+
approved: false,
216+
result: denied("unsupported googlemeet.chrome action"),
217+
};
185218
}
186219
if (!decision.approved) {
187220
return decision.result;

0 commit comments

Comments
 (0)