Skip to content

Commit c97ea18

Browse files
committed
Report malformed percent-escapes in Google resource URLs
getGatekeeperClassFor() decoded user-supplied URL parts with a bare decodeURIComponent(). A bare `%` -- ordinary in a Gmail search or label like "50% off" -- made it throw `URIError: URI malformed` instead of the friendly errors the rest of the function raises for bad input. Route the four user-input decodes in that function through a decodePercentEncoded() helper that names the offending URL and points at `%25`. Malformed input is rejected rather than read literally: these values scope the binding, so guessing at intent could scope it to something other than what the user thinks they connected. Fixes #55
1 parent 0eaec6c commit c97ea18

1 file changed

Lines changed: 18 additions & 4 deletions

File tree

packages/gatekeeper-google/src/google.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,19 @@ function validateGmailQueryForGrouping(query: string): void {
160160
if (quote || stack.length > 0) throw new Error("Gmail query has unterminated grouping or quotes.");
161161
}
162162

163+
// decodeURIComponent(), but a malformed escape (e.g. the bare `%` in a search for "50% off")
164+
// reports which URL was bad instead of throwing a raw `URIError: URI malformed`. Such input is
165+
// rejected rather than read literally: these values scope the binding, and guessing at what the
166+
// user meant could scope it to something other than what they think they connected.
167+
function decodePercentEncoded(what: string, encoded: string): string {
168+
try {
169+
return decodeURIComponent(encoded);
170+
} catch {
171+
throw new Error(
172+
`Invalid ${what}: not valid percent-encoded text. Write a literal "%" as "%25".`);
173+
}
174+
}
175+
163176
function getBaseUrl(env: Env) {
164177
return stripTrailingSlashes(env.BASE_URL || "http://localhost:8787/gatekeeper/google");
165178
}
@@ -866,7 +879,8 @@ export class GatekeeperUserImpl extends WorkerEntrypoint<Env, GatekeeperUserImpl
866879
}
867880

868881
if (parsed.hostname === "calendar.google.com" && parsed.pathname.startsWith("/calendar/")) {
869-
let calendarId = decodeURIComponent(parsed.pathname.split("/")[2] ?? "");
882+
let calendarId = decodePercentEncoded(
883+
"Google Calendar URL", parsed.pathname.split("/")[2] ?? "");
870884
if (!calendarId) {
871885
throw new Error("Invalid Google Calendar URL: no calendar ID found");
872886
}
@@ -899,7 +913,7 @@ export class GatekeeperUserImpl extends WorkerEntrypoint<Env, GatekeeperUserImpl
899913

900914
// Synthetic path: /<projectId>/<datasetId>/<tableId> (each segment optional after the first).
901915
let segments = parsed.pathname.replace(/^\/+|\/+$/g, "").split("/").filter(Boolean)
902-
.map(segment => decodeURIComponent(segment));
916+
.map(segment => decodePercentEncoded("BigQuery resource URL", segment));
903917
if (segments.length > 3) {
904918
throw new Error(
905919
"BigQuery resource URLs must be /<projectId>, /<projectId>/<datasetId>, " +
@@ -938,11 +952,11 @@ export class GatekeeperUserImpl extends WorkerEntrypoint<Env, GatekeeperUserImpl
938952
// Gmail's own UI encodes spaces in hash searches as `+`, while
939953
// decodeURIComponent() only decodes `%20`. Normalize both forms.
940954
const encodedQuery = hash.slice("#search/".length).replace(/\+/g, " ");
941-
const query = decodeURIComponent(encodedQuery);
955+
const query = decodePercentEncoded("Gmail search URL", encodedQuery);
942956
validateGmailQueryForGrouping(query);
943957
props.searchQuery = query;
944958
} else if (hash.startsWith("#label/")) {
945-
const labelName = decodeURIComponent(hash.slice("#label/".length));
959+
const labelName = decodePercentEncoded("Gmail label URL", hash.slice("#label/".length));
946960
if (!labelName || new TextEncoder().encode(labelName).byteLength > 320) {
947961
throw new Error("Gmail label name must be between 1 and 320 bytes.");
948962
}

0 commit comments

Comments
 (0)