@middy/http-security-headers
Version:
Applies best practice security headers to responses. It's a simplified port of HelmetJS
319 lines (295 loc) • 8.98 kB
JavaScript
import { normalizeHttpResponse } from "@middy/util";
// Code and Defaults heavily based off https://helmetjs.github.io/
const defaults = {
contentSecurityPolicy: {
// Fetch directives
// 'child-src': '', // fallback default-src
// 'connect-src': '', // fallback default-src
"default-src": "'none'",
// 'font-src':'', // fallback default-src
// 'frame-src':'', // fallback child-src > default-src
// 'img-src':'', // fallback default-src
// 'manifest-src':'', // fallback default-src
// 'media-src':'', // fallback default-src
// 'object-src':'', // fallback default-src
// 'prefetch-src':'', // fallback default-src
// 'script-src':'', // fallback default-src
// 'script-src-elem':'', // fallback script-src > default-src
// 'script-src-attr':'', // fallback script-src > default-src
// 'style-src':'', // fallback default-src
// 'style-src-elem':'', // fallback style-src > default-src
// 'style-src-attr':'', // fallback style-src > default-src
// 'worker-src':'', // fallback child-src > script-src > default-src
// Document directives
"base-uri": "'none'",
sandbox: "",
// Navigation directives
"form-action": "'none'",
"frame-ancestors": "'none'",
"navigate-to": "'none'",
// Reporting directives
"report-to": "csp",
// Other directives
"require-trusted-types-for": "'script'",
"trusted-types": "'none'",
"upgrade-insecure-requests": "",
},
contentSecurityPolicyReportOnly: false,
contentTypeOptions: {
action: "nosniff",
},
crossOriginEmbedderPolicy: {
policy: "require-corp",
},
crossOriginOpenerPolicy: {
policy: "same-origin",
},
crossOriginResourcePolicy: {
policy: "same-origin",
},
dnsPrefetchControl: {
allow: false,
},
downloadOptions: {
action: "noopen",
},
frameOptions: {
action: "deny",
},
originAgentCluster: {},
permissionsPolicy: {
// Standard
accelerometer: "",
"ambient-light-sensor": "",
autoplay: "",
battery: "",
camera: "",
"cross-origin-isolated": "",
"display-capture": "",
"document-domain": "",
"encrypted-media": "",
"execution-while-not-rendered": "",
"execution-while-out-of-viewport": "",
fullscreen: "",
geolocation: "",
gyroscope: "",
"keyboard-map": "",
magnetometer: "",
microphone: "",
midi: "",
"navigation-override": "",
payment: "",
"picture-in-picture": "",
"publickey-credentials-get": "",
"screen-wake-lock": "",
"sync-xhr": "",
usb: "",
"web-share": "",
"xr-spatial-tracking": "",
// Proposed
"clipboard-read": "",
"clipboard-write": "",
gamepad: "",
"speaker-selection": "",
// Experimental
"conversion-measurement": "",
"focus-without-user-activation": "",
hid: "",
"idle-detection": "",
"interest-cohort": "",
serial: "",
"sync-script": "",
"trust-token-redemption": "",
"window-placement": "",
"vertical-scroll": "",
},
permittedCrossDomainPolicies: {
policy: "none", // none, master-only, by-content-type, by-ftp-filename, all
},
poweredBy: {
server: "",
},
referrerPolicy: {
policy: "no-referrer",
},
reportingEndpoints: {},
reportTo: {
maxAge: 365 * 24 * 60 * 60,
// default: '',
includeSubdomains: true,
// csp: '',
// permissions: '',
// staple: '',
// xss: ''
},
strictTransportSecurity: {
maxAge: 180 * 24 * 60 * 60,
includeSubDomains: true,
preload: true,
},
xssProtection: {
reportTo: "xss",
},
};
const helmet = {};
const helmetHtmlOnly = {};
// *** https://github.com/helmetjs/helmet/tree/main/middlewares *** //
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
helmetHtmlOnly.contentSecurityPolicy = (reportOnly) => (headers, config) => {
let header = Object.keys(config)
.map((policy) => (config[policy] ? `${policy} ${config[policy]}` : ""))
.filter((str) => str)
.join("; ");
if (config.sandbox === "") {
header += "; sandbox";
}
if (config["upgrade-insecure-requests"] === "") {
header += "; upgrade-insecure-requests";
}
const cspHeaderName = reportOnly
? "Content-Security-Policy-Report-Only"
: "Content-Security-Policy";
headers[cspHeaderName] = header;
};
// crossdomain - N/A - for Adobe products
helmetHtmlOnly.crossOriginEmbedderPolicy = (headers, config) => {
headers["Cross-Origin-Embedder-Policy"] = config.policy;
};
helmetHtmlOnly.crossOriginOpenerPolicy = (headers, config) => {
headers["Cross-Origin-Opener-Policy"] = config.policy;
};
helmetHtmlOnly.crossOriginResourcePolicy = (headers, config) => {
headers["Cross-Origin-Resource-Policy"] = config.policy;
};
// DEPRECATED: expectCt
// DEPRECATED: hpkp
// https://www.permissionspolicy.com/
helmetHtmlOnly.permissionsPolicy = (headers, config) => {
headers["Permissions-Policy"] = Object.keys(config)
.map(
(policy) =>
`${policy}=${config[policy] === "*" ? "*" : `(${config[policy]})`}`,
)
.join(", ");
};
helmet.originAgentCluster = (headers, _config) => {
headers["Origin-Agent-Cluster"] = "?1";
};
// https://github.com/helmetjs/referrer-policy
helmet.referrerPolicy = (headers, config) => {
headers["Referrer-Policy"] = config.policy;
};
// DEPRECATED by reportingEndpoints
helmetHtmlOnly.reportTo = (headers, config) => {
headers["Report-To"] = "";
const keys = Object.keys(config);
headers["Report-To"] = keys
.map((group) => {
if (group === "includeSubdomains" || group === "maxAge") return "";
const includeSubdomains =
group === "default"
? `, "include_subdomains": ${config.includeSubdomains}`
: "";
return config[group] && group !== "includeSubdomains"
? `{ "group": "default", "max_age": ${config.maxAge}, "endpoints": [ { "url": "${config[group]}" } ]${includeSubdomains} }`
: "";
})
.filter((str) => str)
.join(", ");
};
helmet.reportingEndpoints = (headers, config) => {
headers["Reporting-Endpoints"] = "";
const keys = Object.keys(config);
for (let i = 0, l = keys.length; i < l; i++) {
if (i) headers["Reporting-Endpoints"] += ", ";
const key = keys[i];
headers["Reporting-Endpoints"] += `${key}="${config[key]}"`;
}
};
// https://github.com/helmetjs/hsts
helmet.strictTransportSecurity = (headers, config) => {
let header = `max-age=${Math.round(config.maxAge)}`;
if (config.includeSubDomains) {
header += "; includeSubDomains";
}
if (config.preload) {
header += "; preload";
}
headers["Strict-Transport-Security"] = header;
};
// noCache - N/A - separate middleware
// X-* //
// https://github.com/helmetjs/dont-sniff-mimetype
helmet.contentTypeOptions = (headers, config) => {
headers["X-Content-Type-Options"] = config.action;
};
// https://github.com/helmetjs/dns-Prefetch-control
helmet.dnsPrefetchControl = (headers, config) => {
headers["X-DNS-Prefetch-Control"] = config.allow ? "on" : "off";
};
// https://github.com/helmetjs/ienoopen
helmet.downloadOptions = (headers, config) => {
headers["X-Download-Options"] = config.action;
};
// https://github.com/helmetjs/frameOptions
helmetHtmlOnly.frameOptions = (headers, config) => {
headers["X-Frame-Options"] = config.action.toUpperCase();
};
// https://github.com/helmetjs/crossdomain
helmet.permittedCrossDomainPolicies = (headers, config) => {
headers["X-Permitted-Cross-Domain-Policies"] = config.policy;
};
// https://github.com/helmetjs/hide-powered-by
helmet.poweredBy = (headers, config) => {
if (config.server) {
headers["X-Powered-By"] = config.server;
} else {
headers.Server = undefined;
headers["X-Powered-By"] = undefined;
}
};
// https://github.com/helmetjs/x-xss-protection
helmetHtmlOnly.xssProtection = (headers, config) => {
let header = "1; mode=block";
if (config.reportTo) {
header += `; report=${config.reportTo}`;
}
headers["X-XSS-Protection"] = header;
};
const httpSecurityHeadersMiddleware = (opts = {}) => {
const options = { ...defaults, ...opts };
const httpSecurityHeadersMiddlewareAfter = async (request) => {
normalizeHttpResponse(request);
for (const key of Object.keys(helmet)) {
if (!options[key]) continue;
const config = { ...defaults[key], ...options[key] };
helmet[key](request.response.headers, config);
}
const contentTypeHeader =
request.response.headers["Content-Type"] ??
request.response.headers["content-type"];
if (contentTypeHeader?.includes("text/html")) {
for (const key of Object.keys(helmetHtmlOnly)) {
if (!options[key]) continue;
const config = { ...defaults[key], ...options[key] };
if (key === "contentSecurityPolicy") {
helmetHtmlOnly[key](options.contentSecurityPolicyReportOnly)(
request.response.headers,
config,
);
} else {
helmetHtmlOnly[key](request.response.headers, config);
}
}
}
};
const httpSecurityHeadersMiddlewareOnError = async (request) => {
if (request.response === undefined) return;
await httpSecurityHeadersMiddlewareAfter(request);
};
return {
after: httpSecurityHeadersMiddlewareAfter,
onError: httpSecurityHeadersMiddlewareOnError,
};
};
export default httpSecurityHeadersMiddleware;