@middy/http-cors
Version:
CORS (Cross-Origin Resource Sharing) middleware for the middy framework
274 lines (251 loc) • 7.67 kB
JavaScript
// Copyright 2017 - 2026 will Farrell, Luciano Mammino, and Middy contributors.
// SPDX-License-Identifier: MIT
import { normalizeHttpResponse } from "@middy/util";
const hostnameToPunycode = (hostname) => {
const placeholder = "-_ANY_-";
const tempHostname = hostname.replace(/\*/g, placeholder);
try {
const url = new URL(`https://${tempHostname}`);
return url.host.replaceAll(placeholder.toLowerCase(), "*");
} catch {
return hostname;
}
};
const originToPunycode = (origin) => {
if (!origin || origin === "*") return origin;
const match = origin.match(/^(https?:\/\/)(.+)$/);
if (!match) return origin;
const [, protocol, host] = match;
return protocol + hostnameToPunycode(host);
};
// CORS-safelisted request headers
// https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_request_header
const corsSafelistedRequestHeaders = [
"accept",
"accept-language",
"content-language",
"content-type",
"range",
];
const defaults = {
disableBeforePreflightResponse: true,
getOrigin: undefined, // default inserted below
credentials: undefined,
headers: undefined,
methods: undefined,
origin: undefined,
origins: [],
exposeHeaders: undefined,
maxAge: undefined,
requestHeaders: undefined,
requestMethods: undefined,
cacheControl: undefined,
vary: undefined,
};
const httpCorsMiddleware = (opts = {}) => {
const getOrigin = (incomingOrigin, options = {}) => {
if (options.origins.length > 0) {
if (originStatic[incomingOrigin]) {
return incomingOrigin;
}
if (originAny) {
if (options.credentials) {
return incomingOrigin;
}
return "*";
}
if (originDynamic.some((regExp) => regExp.test(incomingOrigin))) {
return incomingOrigin;
}
} else {
if (incomingOrigin && options.credentials && options.origin === "*") {
return incomingOrigin;
}
return options.origin;
}
return null;
};
const options = {
...defaults,
getOrigin,
...opts,
};
if (
options.requestHeaders !== undefined &&
!Array.isArray(options.requestHeaders)
) {
throw new Error("requestHeaders must be an array", {
cause: { package: "@middy/http-cors" },
});
}
if (
options.requestMethods !== undefined &&
!Array.isArray(options.requestMethods)
) {
throw new Error("requestMethods must be an array", {
cause: { package: "@middy/http-cors" },
});
}
options.requestHeaders = options.requestHeaders?.map((v) => v.toLowerCase());
options.requestMethods = options.requestMethods?.map((v) => v.toUpperCase());
let originAny = false;
let originMany = options.origins.length > 1;
const originStatic = {};
const originDynamic = [];
for (let origin of [options.origin, ...options.origins]) {
if (!origin) {
continue;
}
origin = originToPunycode(origin);
// All
if (origin === "*") {
originAny = true;
continue;
}
// Static
if (!origin.includes("*")) {
originStatic[origin] = true;
continue;
}
originMany = true;
// Dynamic
const regExpStr = origin
.replace(/[.+?^${}()|[\]\\]/g, "\\$&")
.replaceAll("*", "[^.]*");
// SAST Skipped: Not accessible by users
// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp
originDynamic.push(new RegExp(`^${regExpStr}$`));
}
const modifyHeaders = (headers, options, request) => {
if (Object.hasOwn(headers, "Access-Control-Allow-Credentials")) {
options.credentials =
headers["Access-Control-Allow-Credentials"] === "true";
}
if (options.credentials) {
headers["Access-Control-Allow-Credentials"] = String(options.credentials);
}
if (
options.headers &&
!Object.hasOwn(headers, "Access-Control-Allow-Headers")
) {
headers["Access-Control-Allow-Headers"] = options.headers;
}
if (
options.methods &&
!Object.hasOwn(headers, "Access-Control-Allow-Methods")
) {
headers["Access-Control-Allow-Methods"] = options.methods;
}
let newOrigin;
if (!Object.hasOwn(headers, "Access-Control-Allow-Origin")) {
const eventHeaders = request.event.headers ?? {};
const incomingOrigin = eventHeaders.Origin ?? eventHeaders.origin;
newOrigin = options.getOrigin(incomingOrigin, options);
if (newOrigin) {
headers["Access-Control-Allow-Origin"] = newOrigin;
}
}
if (!headers.Vary) {
addHeaderPart(headers, "Vary", options.vary);
}
if (
originMany ||
(originAny && newOrigin !== "*") ||
(newOrigin === "*" && options.credentials)
) {
addHeaderPart(headers, "Vary", "Origin");
}
if (
options.exposeHeaders &&
!Object.hasOwn(headers, "Access-Control-Expose-Headers")
) {
headers["Access-Control-Expose-Headers"] = options.exposeHeaders;
}
if (options.maxAge && !Object.hasOwn(headers, "Access-Control-Max-Age")) {
headers["Access-Control-Max-Age"] = String(options.maxAge);
}
const httpMethod = getVersionHttpMethod[request.event.version ?? "1.0"]?.(
request.event,
);
if (
httpMethod === "OPTIONS" &&
options.cacheControl &&
!Object.hasOwn(headers, "Cache-Control")
) {
headers["Cache-Control"] = options.cacheControl;
}
};
const httpCorsMiddlewareBefore = (request) => {
if (options.disableBeforePreflightResponse) return;
const method = getVersionHttpMethod[request.event.version ?? "1.0"]?.(
request.event,
);
if (method === "OPTIONS") {
normalizeHttpResponse(request);
const eventHeaders = request.event.headers ?? {};
const requestMethod =
eventHeaders["Access-Control-Request-Method"] ??
eventHeaders["access-control-request-method"];
if (options.requestMethods?.length && requestMethod) {
if (!options.requestMethods.includes(requestMethod)) {
request.response.statusCode = 204;
request.response.headers = {};
return request.response;
}
}
const requestHeadersValue =
eventHeaders["Access-Control-Request-Headers"] ??
eventHeaders["access-control-request-headers"];
if (options.requestHeaders?.length && requestHeadersValue) {
const requestedHeaders = requestHeadersValue
.split(",")
.map((h) => h.trim().toLowerCase());
const nonSafelistedHeaders = requestedHeaders.filter(
(h) => !corsSafelistedRequestHeaders.includes(h),
);
const hasDisallowedHeader = nonSafelistedHeaders.some(
(h) => !options.requestHeaders.includes(h),
);
if (hasDisallowedHeader) {
request.response.statusCode = 204;
request.response.headers = {};
return request.response;
}
}
const headers = {};
modifyHeaders(headers, options, request);
request.response.headers = headers;
request.response.statusCode = 204;
return request.response;
}
};
const httpCorsMiddlewareAfter = (request) => {
normalizeHttpResponse(request);
const headers = { ...request.response.headers };
modifyHeaders(headers, options, request);
request.response.headers = headers;
};
const httpCorsMiddlewareOnError = (request) => {
if (typeof request.response === "undefined") return;
httpCorsMiddlewareAfter(request);
};
return {
before: httpCorsMiddlewareBefore,
after: httpCorsMiddlewareAfter,
onError: httpCorsMiddlewareOnError,
};
};
const getVersionHttpMethod = {
"1.0": (event) => event.httpMethod,
"2.0": (event) => event.requestContext.http.method,
};
// header in official name, lowercase variant handled
const addHeaderPart = (headers, header, value) => {
if (!value) return;
const headerLower = header.toLowerCase();
const sanitizedHeader = headers[headerLower] ? headerLower : header;
headers[sanitizedHeader] ??= "";
headers[sanitizedHeader] &&= `${headers[sanitizedHeader]}, `;
headers[sanitizedHeader] += value;
};
export default httpCorsMiddleware;