diff --git a/src/params/types.ts b/src/params/types.ts index 0d9028b17..d6e05fcc5 100644 --- a/src/params/types.ts +++ b/src/params/types.ts @@ -634,9 +634,17 @@ export class ListParam extends Param { /** @internal */ runtimeValue(): string[] { - const val = JSON.parse(process.env[this.name]); - if (!Array.isArray(val) || !(val as string[]).every((v) => typeof v === "string")) { - return []; + const raw = process.env[this.name]; + if (!raw) { + throw new Error( + `Parameter "${this.name}" is not set. Set it in .env or .env.local, or ensure the Functions runtime has provided it.` + ); + } + const val = JSON.parse(raw); + if (!Array.isArray(val) || !val.every((v) => typeof v === "string")) { + throw new Error( + `Parameter "${this.name}" is not a valid JSON array of strings. Value is: ${raw}` + ); } return val as string[]; } diff --git a/src/v2/providers/https.ts b/src/v2/providers/https.ts index f38400029..c2f08996f 100644 --- a/src/v2/providers/https.ts +++ b/src/v2/providers/https.ts @@ -286,6 +286,64 @@ export interface CallableFunction extends HttpsFunc ): { stream: AsyncIterable; output: Return }; } +/** + * Builds a CORS origin callback for a static value (boolean, string, RegExp, or array). + * Used by onRequest and onCall for non-Expression cors; function form avoids CodeQL permissive CORS alert. + */ +function buildStaticCorsOriginCallback( + origin: string | boolean | RegExp | Array +): NonNullable { + return (reqOrigin: string | undefined, cb: (err: Error | null, allow?: boolean | string) => void) => { + if (typeof origin === "boolean" || typeof origin === "string") { + return cb(null, origin); + } + if (reqOrigin === undefined) { + return cb(null, true); + } + if (origin instanceof RegExp) { + return cb(null, origin.test(reqOrigin) ? reqOrigin : false); + } + if ( + Array.isArray(origin) && + origin.some((o) => (typeof o === "string" ? o === reqOrigin : o.test(reqOrigin))) + ) { + return cb(null, reqOrigin); + } + return cb(null, false); + }; +} + +/** + * Builds a CORS origin callback that resolves an Expression (e.g. defineList) at request time. + * Used by onRequest and onCall so params are not read during deployment. + */ +function buildCorsOriginFromExpression( + corsExpression: Expression, + options: { respectCorsFalse?: boolean; corsOpt?: unknown } +): NonNullable { + return (reqOrigin: string | undefined, callback: (err: Error | null, allow?: boolean | string) => void) => { + if (isDebugFeatureEnabled("enableCors") && (!options.respectCorsFalse || options.corsOpt !== false)) { + callback(null, true); + return; + } + const resolved = corsExpression.runtimeValue(); + if (Array.isArray(resolved)) { + if (resolved.length === 1) { + callback(null, resolved[0]); + return; + } + if (reqOrigin === undefined) { + callback(null, true); + return; + } + const allowed = resolved.indexOf(reqOrigin) !== -1; + callback(null, allowed ? reqOrigin : false); + } else { + callback(null, resolved as string); + } + }; +} + /** * Handles HTTPS requests. * @param opts - Options to set on this function @@ -324,18 +382,32 @@ export function onRequest( handler = withErrorHandler(handler); if (isDebugFeatureEnabled("enableCors") || "cors" in opts) { - let origin = opts.cors instanceof Expression ? opts.cors.value() : opts.cors; - if (isDebugFeatureEnabled("enableCors")) { - // Respect `cors: false` to turn off cors even if debug feature is enabled. - origin = opts.cors === false ? false : true; - } - // Arrays cause the access-control-allow-origin header to be dynamic based - // on the origin header of the request. If there is only one element in the - // array, this is unnecessary. - if (Array.isArray(origin) && origin.length === 1) { - origin = origin[0]; + let corsOptions: cors.CorsOptions; + if (opts.cors instanceof Expression) { + // Defer resolution to request time so params are not read during deployment. + corsOptions = { + origin: buildCorsOriginFromExpression(opts.cors, { + respectCorsFalse: true, + corsOpt: opts.cors, + }), + }; + } else { + let origin = opts.cors; + if (isDebugFeatureEnabled("enableCors")) { + // Respect `cors: false` to turn off cors even if debug feature is enabled. + origin = opts.cors === false ? false : true; + } + // Arrays cause the access-control-allow-origin header to be dynamic based + // on the origin header of the request. If there is only one element in the + // array, this is unnecessary. + if (Array.isArray(origin) && origin.length === 1) { + origin = origin[0]; + } + corsOptions = { + origin: buildStaticCorsOriginCallback(origin), + }; } - const middleware = cors({ origin }); + const middleware = cors(corsOptions); const userProvidedHandler = handler; handler = (req: Request, res: express.Response): void | Promise => { @@ -434,30 +506,38 @@ export function onCall, Stream = unknown>( opts = optsOrHandler as CallableOptions; } - let cors: string | boolean | RegExp | Array | undefined; - if ("cors" in opts) { - if (opts.cors instanceof Expression) { - cors = opts.cors.value(); + let corsOptions: cors.CorsOptions; + if ("cors" in opts && opts.cors instanceof Expression) { + // Defer resolution to request time so params are not read during deployment. + corsOptions = { + origin: buildCorsOriginFromExpression(opts.cors, {}), + methods: "POST", + }; + } else { + let cors: string | boolean | RegExp | Array | undefined; + if ("cors" in opts) { + cors = opts.cors as string | boolean | RegExp | Array; } else { - cors = opts.cors; + cors = true; } - } else { - cors = true; - } - - let origin = isDebugFeatureEnabled("enableCors") ? true : cors; - // Arrays cause the access-control-allow-origin header to be dynamic based - // on the origin header of the request. If there is only one element in the - // array, this is unnecessary. - if (Array.isArray(origin) && origin.length === 1) { - origin = origin[0]; + let origin = isDebugFeatureEnabled("enableCors") ? true : cors; + // Arrays cause the access-control-allow-origin header to be dynamic based + // on the origin header of the request. If there is only one element in the + // array, this is unnecessary. + if (Array.isArray(origin) && origin.length === 1) { + origin = origin[0]; + } + corsOptions = { + origin: buildStaticCorsOriginCallback(origin), + methods: "POST", + }; } // fix the length of handler to make the call to handler consistent const fixedLen = (req: CallableRequest, resp?: CallableResponse) => handler(req, resp); let func: any = onCallHandler( { - cors: { origin, methods: "POST" }, + cors: corsOptions, enforceAppCheck: opts.enforceAppCheck ?? options.getGlobalOptions().enforceAppCheck, consumeAppCheckToken: opts.consumeAppCheckToken, heartbeatSeconds: opts.heartbeatSeconds,