secure-headers.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. // src/middleware/secure-headers/secure-headers.ts
  2. import { encodeBase64 } from "../../utils/encode.js";
  3. var HEADERS_MAP = {
  4. crossOriginEmbedderPolicy: ["Cross-Origin-Embedder-Policy", "require-corp"],
  5. crossOriginResourcePolicy: ["Cross-Origin-Resource-Policy", "same-origin"],
  6. crossOriginOpenerPolicy: ["Cross-Origin-Opener-Policy", "same-origin"],
  7. originAgentCluster: ["Origin-Agent-Cluster", "?1"],
  8. referrerPolicy: ["Referrer-Policy", "no-referrer"],
  9. strictTransportSecurity: ["Strict-Transport-Security", "max-age=15552000; includeSubDomains"],
  10. xContentTypeOptions: ["X-Content-Type-Options", "nosniff"],
  11. xDnsPrefetchControl: ["X-DNS-Prefetch-Control", "off"],
  12. xDownloadOptions: ["X-Download-Options", "noopen"],
  13. xFrameOptions: ["X-Frame-Options", "SAMEORIGIN"],
  14. xPermittedCrossDomainPolicies: ["X-Permitted-Cross-Domain-Policies", "none"],
  15. xXssProtection: ["X-XSS-Protection", "0"]
  16. };
  17. var DEFAULT_OPTIONS = {
  18. crossOriginEmbedderPolicy: false,
  19. crossOriginResourcePolicy: true,
  20. crossOriginOpenerPolicy: true,
  21. originAgentCluster: true,
  22. referrerPolicy: true,
  23. strictTransportSecurity: true,
  24. xContentTypeOptions: true,
  25. xDnsPrefetchControl: true,
  26. xDownloadOptions: true,
  27. xFrameOptions: true,
  28. xPermittedCrossDomainPolicies: true,
  29. xXssProtection: true,
  30. removePoweredBy: true,
  31. permissionsPolicy: {}
  32. };
  33. var generateNonce = () => {
  34. const arrayBuffer = new Uint8Array(16);
  35. crypto.getRandomValues(arrayBuffer);
  36. return encodeBase64(arrayBuffer.buffer);
  37. };
  38. var NONCE = (ctx) => {
  39. const key = "secureHeadersNonce";
  40. const init = ctx.get(key);
  41. const nonce = init || generateNonce();
  42. if (init == null) {
  43. ctx.set(key, nonce);
  44. }
  45. return `'nonce-${nonce}'`;
  46. };
  47. var secureHeaders = (customOptions) => {
  48. const options = { ...DEFAULT_OPTIONS, ...customOptions };
  49. const headersToSet = getFilteredHeaders(options);
  50. const callbacks = [];
  51. if (options.contentSecurityPolicy) {
  52. const [callback, value] = getCSPDirectives(options.contentSecurityPolicy);
  53. if (callback) {
  54. callbacks.push(callback);
  55. }
  56. headersToSet.push(["Content-Security-Policy", value]);
  57. }
  58. if (options.contentSecurityPolicyReportOnly) {
  59. const [callback, value] = getCSPDirectives(options.contentSecurityPolicyReportOnly);
  60. if (callback) {
  61. callbacks.push(callback);
  62. }
  63. headersToSet.push(["Content-Security-Policy-Report-Only", value]);
  64. }
  65. if (options.permissionsPolicy && Object.keys(options.permissionsPolicy).length > 0) {
  66. headersToSet.push([
  67. "Permissions-Policy",
  68. getPermissionsPolicyDirectives(options.permissionsPolicy)
  69. ]);
  70. }
  71. if (options.reportingEndpoints) {
  72. headersToSet.push(["Reporting-Endpoints", getReportingEndpoints(options.reportingEndpoints)]);
  73. }
  74. if (options.reportTo) {
  75. headersToSet.push(["Report-To", getReportToOptions(options.reportTo)]);
  76. }
  77. return async function secureHeaders2(ctx, next) {
  78. const headersToSetForReq = callbacks.length === 0 ? headersToSet : callbacks.reduce((acc, cb) => cb(ctx, acc), headersToSet);
  79. await next();
  80. setHeaders(ctx, headersToSetForReq);
  81. if (options?.removePoweredBy) {
  82. ctx.res.headers.delete("X-Powered-By");
  83. }
  84. };
  85. };
  86. function getFilteredHeaders(options) {
  87. return Object.entries(HEADERS_MAP).filter(([key]) => options[key]).map(([key, defaultValue]) => {
  88. const overrideValue = options[key];
  89. return typeof overrideValue === "string" ? [defaultValue[0], overrideValue] : defaultValue;
  90. });
  91. }
  92. function getCSPDirectives(contentSecurityPolicy) {
  93. const callbacks = [];
  94. const resultValues = [];
  95. for (const [directive, value] of Object.entries(contentSecurityPolicy)) {
  96. const valueArray = Array.isArray(value) ? value : [value];
  97. valueArray.forEach((value2, i) => {
  98. if (typeof value2 === "function") {
  99. const index = i * 2 + 2 + resultValues.length;
  100. callbacks.push((ctx, values) => {
  101. values[index] = value2(ctx, directive);
  102. });
  103. }
  104. });
  105. resultValues.push(
  106. directive.replace(
  107. /[A-Z]+(?![a-z])|[A-Z]/g,
  108. (match, offset) => offset ? "-" + match.toLowerCase() : match.toLowerCase()
  109. ),
  110. ...valueArray.flatMap((value2) => [" ", value2]),
  111. "; "
  112. );
  113. }
  114. resultValues.pop();
  115. return callbacks.length === 0 ? [void 0, resultValues.join("")] : [
  116. (ctx, headersToSet) => headersToSet.map((values) => {
  117. if (values[0] === "Content-Security-Policy" || values[0] === "Content-Security-Policy-Report-Only") {
  118. const clone = values[1].slice();
  119. callbacks.forEach((cb) => {
  120. cb(ctx, clone);
  121. });
  122. return [values[0], clone.join("")];
  123. } else {
  124. return values;
  125. }
  126. }),
  127. resultValues
  128. ];
  129. }
  130. function getPermissionsPolicyDirectives(policy) {
  131. return Object.entries(policy).map(([directive, value]) => {
  132. const kebabDirective = camelToKebab(directive);
  133. if (typeof value === "boolean") {
  134. return `${kebabDirective}=${value ? "*" : "none"}`;
  135. }
  136. if (Array.isArray(value)) {
  137. if (value.length === 0) {
  138. return `${kebabDirective}=()`;
  139. }
  140. if (value.length === 1 && (value[0] === "*" || value[0] === "none")) {
  141. return `${kebabDirective}=${value[0]}`;
  142. }
  143. const allowlist = value.map((item) => ["self", "src"].includes(item) ? item : `"${item}"`);
  144. return `${kebabDirective}=(${allowlist.join(" ")})`;
  145. }
  146. return "";
  147. }).filter(Boolean).join(", ");
  148. }
  149. function camelToKebab(str) {
  150. return str.replace(/([a-z\d])([A-Z])/g, "$1-$2").toLowerCase();
  151. }
  152. function getReportingEndpoints(reportingEndpoints = []) {
  153. return reportingEndpoints.map((endpoint) => `${endpoint.name}="${endpoint.url}"`).join(", ");
  154. }
  155. function getReportToOptions(reportTo = []) {
  156. return reportTo.map((option) => JSON.stringify(option)).join(", ");
  157. }
  158. function setHeaders(ctx, headersToSet) {
  159. headersToSet.forEach(([header, value]) => {
  160. ctx.res.headers.set(header, value);
  161. });
  162. }
  163. export {
  164. NONCE,
  165. secureHeaders
  166. };