Files
ccn/Services/Website/Sources/Library/Public/Middlewares/SecurityHeadersMiddleware.swift
T

147 lines
5.9 KiB
Swift
Raw Normal View History

import HTTPTypes
import Hummingbird
/// Stamps a set of security-related HTTP headers onto every response.
///
/// Placed at (or near) the top of the middleware chain, it adds the configured headers to whatever
/// response bubbles back up — the rendered landing page, the ``ErrorPage`` produced by
/// ``NotFoundMiddleware``, and every static file served by `FileMiddleware` — so the browser applies
/// the strict, hardened interpretation of the content instead of its lenient legacy defaults.
///
/// The headers are precomputed once from the ``Configuration`` at initialization and reused for
/// every request, so the per-request cost is a handful of header copies.
public struct SecurityHeadersMiddleware<Context: RequestContext> {
// MARK: Properties
/// The precomputed headers applied to every response.
private let fields: HTTPFields
// MARK: Initializers
/// Creates a security-headers middleware.
/// - Parameter configuration: the headers applied to every response. Defaults to a hardened
/// baseline suitable for a static site, with `Strict-Transport-Security` left off (see
/// ``Configuration``).
public init(
configuration: Configuration = .init()
) {
self.fields = configuration.fields
}
}
// MARK: - RouterMiddleware
extension SecurityHeadersMiddleware: RouterMiddleware {
// MARK: Functions
/// Passes the request down the chain and stamps the configured security headers onto the
/// response on the way back up.
///
/// Existing values for the same header names are replaced so downstream middleware cannot leave
/// a weaker policy in place.
/// - Parameters:
/// - request: the incoming request.
/// - context: the context the request is resolved against.
/// - next: the next responder in the middleware chain.
/// - Returns: the downstream response with the security headers applied.
/// - Throws: any error thrown downstream.
public func handle(
_ request: Request,
context: Context,
next: (Request, Context) async throws -> Response
) async throws -> Response {
var response = try await next(request, context)
for field in fields {
response.headers[field.name] = field.value
}
return response
}
}
// MARK: - Helpers
private extension SecurityHeadersMiddleware.Configuration {
// MARK: Computed
/// The configuration expressed as the headers to apply, omitting any whose value is `nil`.
var fields: HTTPFields {
var fields = HTTPFields()
fields[.contentSecurityPolicy] = contentSecurityPolicy
fields[.xContentTypeOptions] = contentTypeOptions
fields[.frameOptions] = frameOptions
fields[.referrerPolicy] = referrerPolicy
fields[.permissionsPolicy] = permissionsPolicy
fields[.strictTransportSecurity] = strictTransportSecurity
return fields
}
}
// MARK: - Configuration
extension SecurityHeadersMiddleware {
/// The set of security headers a ``SecurityHeadersMiddleware`` applies.
///
/// Each property maps to a single response header. A `nil` value omits that header entirely,
/// which is how `Strict-Transport-Security` stays disabled by default: it is only safe to send
/// over HTTPS and is "sticky" in browsers, so it must stay off in plain-HTTP development and be
/// switched on (via configuration) only in TLS-terminated production.
public struct Configuration: Sendable {
// MARK: Properties
/// The `Content-Security-Policy` value (controls which sources the browser will load).
public let contentSecurityPolicy: String?
/// The `X-Content-Type-Options` value (disables MIME sniffing when set to `nosniff`).
public let contentTypeOptions: String?
/// The `X-Frame-Options` value (controls whether the page may be framed).
public let frameOptions: String?
/// The `Referrer-Policy` value (controls how much referrer information is shared).
public let referrerPolicy: String?
/// The `Permissions-Policy` value (gates access to powerful browser features).
public let permissionsPolicy: String?
/// The `Strict-Transport-Security` value, or `nil` to omit the header (the default).
public let strictTransportSecurity: String?
// MARK: Initializers
/// Creates a security-headers configuration.
///
/// Every parameter defaults to the hardened baseline defined in `String.Security`, except
/// `strictTransportSecurity`, which defaults to `nil` (omitted). Pass `nil` for any header
/// to drop it from the response.
/// - Parameters:
/// - contentSecurityPolicy: the `Content-Security-Policy` value.
/// - contentTypeOptions: the `X-Content-Type-Options` value.
/// - frameOptions: the `X-Frame-Options` value.
/// - referrerPolicy: the `Referrer-Policy` value.
/// - permissionsPolicy: the `Permissions-Policy` value.
/// - strictTransportSecurity: the `Strict-Transport-Security` value, or `nil` to omit it.
public init(
contentSecurityPolicy: String? = String.Security.contentSecurityPolicy,
contentTypeOptions: String? = String.Security.contentTypeOptions,
frameOptions: String? = String.Security.frameOptions,
referrerPolicy: String? = String.Security.referrerPolicy,
permissionsPolicy: String? = String.Security.permissionsPolicy,
strictTransportSecurity: String? = nil
) {
self.contentSecurityPolicy = contentSecurityPolicy
self.contentTypeOptions = contentTypeOptions
self.frameOptions = frameOptions
self.referrerPolicy = referrerPolicy
self.permissionsPolicy = permissionsPolicy
self.strictTransportSecurity = strictTransportSecurity
}
}
}