2023-08-20 15:24:59 +00:00
|
|
|
import type { MiddlewareHandler } from '../../types.ts'
|
|
|
|
|
|
|
|
interface ContentSecurityPolicyOptions {
|
|
|
|
defaultSrc?: string[]
|
|
|
|
baseUri?: string[]
|
2023-09-02 22:51:36 +00:00
|
|
|
childSrc?: string[]
|
|
|
|
connectSrc?: string[]
|
2023-08-20 15:24:59 +00:00
|
|
|
fontSrc?: string[]
|
2023-09-02 22:51:36 +00:00
|
|
|
formAction?: string[]
|
2023-08-20 15:24:59 +00:00
|
|
|
frameAncestors?: string[]
|
2023-09-02 22:51:36 +00:00
|
|
|
frameSrc?: string[]
|
2023-08-20 15:24:59 +00:00
|
|
|
imgSrc?: string[]
|
2023-09-02 22:51:36 +00:00
|
|
|
manifestSrc?: string[]
|
|
|
|
mediaSrc?: string[]
|
2023-08-20 15:24:59 +00:00
|
|
|
objectSrc?: string[]
|
2023-09-02 22:51:36 +00:00
|
|
|
reportTo?: string
|
|
|
|
sandbox?: string[]
|
2023-08-20 15:24:59 +00:00
|
|
|
scriptSrc?: string[]
|
|
|
|
scriptSrcAttr?: string[]
|
2023-09-02 22:51:36 +00:00
|
|
|
scriptSrcElem?: string[]
|
2023-08-20 15:24:59 +00:00
|
|
|
styleSrc?: string[]
|
2023-09-02 22:51:36 +00:00
|
|
|
styleSrcAttr?: string[]
|
|
|
|
styleSrcElem?: string[]
|
2023-08-20 15:24:59 +00:00
|
|
|
upgradeInsecureRequests?: string[]
|
2023-09-02 22:51:36 +00:00
|
|
|
workerSrc?: string[]
|
2023-08-20 15:24:59 +00:00
|
|
|
}
|
|
|
|
|
2023-09-02 22:51:36 +00:00
|
|
|
interface ReportToOptions {
|
|
|
|
group: string
|
|
|
|
max_age: number
|
|
|
|
endpoints: ReportToEndpoint[]
|
|
|
|
}
|
|
|
|
|
|
|
|
interface ReportToEndpoint {
|
|
|
|
url: string
|
|
|
|
}
|
|
|
|
|
|
|
|
interface ReportingEndpointOptions {
|
|
|
|
name: string
|
|
|
|
url: string
|
|
|
|
}
|
|
|
|
|
|
|
|
type overridableHeader = boolean | string
|
|
|
|
|
2023-08-20 15:24:59 +00:00
|
|
|
interface SecureHeadersOptions {
|
|
|
|
contentSecurityPolicy?: ContentSecurityPolicyOptions
|
2023-09-02 22:51:36 +00:00
|
|
|
crossOriginEmbedderPolicy?: overridableHeader
|
|
|
|
crossOriginResourcePolicy?: overridableHeader
|
|
|
|
crossOriginOpenerPolicy?: overridableHeader
|
|
|
|
originAgentCluster: overridableHeader
|
|
|
|
referrerPolicy?: overridableHeader
|
|
|
|
reportingEndpoints?: ReportingEndpointOptions[]
|
|
|
|
reportTo?: ReportToOptions[]
|
|
|
|
strictTransportSecurity?: overridableHeader
|
|
|
|
xContentTypeOptions?: overridableHeader
|
|
|
|
xDnsPrefetchControl?: overridableHeader
|
|
|
|
xDownloadOptions?: overridableHeader
|
|
|
|
xFrameOptions?: overridableHeader
|
|
|
|
xPermittedCrossDomainPolicies?: overridableHeader
|
|
|
|
xXssProtection?: overridableHeader
|
2023-08-20 15:24:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type HeadersMap = {
|
|
|
|
[key in keyof SecureHeadersOptions]: [string, string]
|
|
|
|
}
|
|
|
|
|
|
|
|
const HEADERS_MAP: HeadersMap = {
|
|
|
|
crossOriginEmbedderPolicy: ['Cross-Origin-Embedder-Policy', 'require-corp'],
|
|
|
|
crossOriginResourcePolicy: ['Cross-Origin-Resource-Policy', 'same-origin'],
|
|
|
|
crossOriginOpenerPolicy: ['Cross-Origin-Opener-Policy', 'same-origin'],
|
|
|
|
originAgentCluster: ['Origin-Agent-Cluster', '?1'],
|
|
|
|
referrerPolicy: ['Referrer-Policy', 'no-referrer'],
|
|
|
|
strictTransportSecurity: ['Strict-Transport-Security', 'max-age=15552000; includeSubDomains'],
|
|
|
|
xContentTypeOptions: ['X-Content-Type-Options', 'nosniff'],
|
|
|
|
xDnsPrefetchControl: ['X-DNS-Prefetch-Control', 'off'],
|
|
|
|
xDownloadOptions: ['X-Download-Options', 'noopen'],
|
|
|
|
xFrameOptions: ['X-Frame-Options', 'SAMEORIGIN'],
|
|
|
|
xPermittedCrossDomainPolicies: ['X-Permitted-Cross-Domain-Policies', 'none'],
|
|
|
|
xXssProtection: ['X-XSS-Protection', '0'],
|
|
|
|
}
|
|
|
|
|
2023-09-02 22:51:36 +00:00
|
|
|
const DEFAULT_OPTIONS: SecureHeadersOptions = {
|
2023-08-20 15:24:59 +00:00
|
|
|
crossOriginEmbedderPolicy: false,
|
|
|
|
crossOriginResourcePolicy: true,
|
|
|
|
crossOriginOpenerPolicy: true,
|
|
|
|
originAgentCluster: true,
|
|
|
|
referrerPolicy: true,
|
|
|
|
strictTransportSecurity: true,
|
|
|
|
xContentTypeOptions: true,
|
|
|
|
xDnsPrefetchControl: true,
|
|
|
|
xDownloadOptions: true,
|
|
|
|
xFrameOptions: true,
|
|
|
|
xPermittedCrossDomainPolicies: true,
|
|
|
|
xXssProtection: true,
|
|
|
|
}
|
|
|
|
|
|
|
|
export const secureHeaders = (customOptions?: Partial<SecureHeadersOptions>): MiddlewareHandler => {
|
|
|
|
const options = { ...DEFAULT_OPTIONS, ...customOptions }
|
|
|
|
const headersToSet = Object.entries(HEADERS_MAP)
|
|
|
|
.filter(([key]) => options[key as keyof SecureHeadersOptions])
|
2023-09-02 22:51:36 +00:00
|
|
|
.map(([key, defaultValue]) => {
|
|
|
|
const overrideValue = options[key as keyof SecureHeadersOptions]
|
|
|
|
if (typeof overrideValue === 'string') return [defaultValue[0], overrideValue]
|
|
|
|
return defaultValue
|
|
|
|
})
|
|
|
|
|
|
|
|
if (options.contentSecurityPolicy) {
|
|
|
|
const cspDirectives = Object.entries(options.contentSecurityPolicy)
|
|
|
|
.map(([directive, value]) => {
|
|
|
|
// convert camelCase to kebab-case directives (e.g. `defaultSrc` -> `default-src`)
|
|
|
|
directive = directive.replace(
|
|
|
|
/[A-Z]+(?![a-z])|[A-Z]/g,
|
|
|
|
(match, offset) => (offset ? '-' : '') + match.toLowerCase()
|
|
|
|
)
|
|
|
|
return `${directive} ${Array.isArray(value) ? value.join(' ') : value}`
|
|
|
|
})
|
|
|
|
.join('; ')
|
|
|
|
headersToSet.push(['Content-Security-Policy', cspDirectives])
|
|
|
|
}
|
|
|
|
|
|
|
|
if (options.reportingEndpoints) {
|
|
|
|
const reportingEndpoints = options.reportingEndpoints
|
|
|
|
.map((endpoint) => `${endpoint.name}="${endpoint.url}"`)
|
|
|
|
.join(', ')
|
|
|
|
headersToSet.push(['Reporting-Endpoints', reportingEndpoints])
|
|
|
|
}
|
|
|
|
|
|
|
|
if (options.reportTo) {
|
|
|
|
const reportToOptions = options.reportTo.map((option) => JSON.stringify(option)).join(', ')
|
|
|
|
headersToSet.push(['Report-To', reportToOptions])
|
|
|
|
}
|
2023-08-20 15:24:59 +00:00
|
|
|
|
|
|
|
return async (ctx, next) => {
|
|
|
|
await next()
|
|
|
|
headersToSet.forEach(([header, value]) => {
|
|
|
|
ctx.res.headers.set(header, value)
|
|
|
|
})
|
|
|
|
|
|
|
|
ctx.res.headers.delete('X-Powered-By')
|
|
|
|
}
|
|
|
|
}
|