diff --git a/README.md b/README.md index f133173..c7b9405 100644 --- a/README.md +++ b/README.md @@ -108,13 +108,134 @@ The Vapor Security Headers package will set a default CSP of `default-src: 'self The API default CSP is `default-src: 'none'` as an API should only return data and never be loading scripts or images to display! -I plan on massively improving creating the CSP configurations, but for now to configure your CSP you can add it to your `ContentSecurityPolicyConfiguration` like so: +You can build a CSP header (`ContentSecurityPolicy`) with the following directives: + +- baseUri(sources) +- blockAllMixedContent() +- connectSrc(sources) +- defaultSrc(sources) +- fontSrc(sources) +- formAction(sources) +- frameAncestors(sources) +- frameSrc(sources) +- imgSrc(sources) +- manifestSrc(sources) +- mediaSrc(sources) +- objectSrc(sources) +- pluginTypes(types) +- reportTo(json_object) +- reportUri(uri) +- requireSriFor(values) +- sandbox(values) +- scriptSrc(sources) +- styleSrc(sources) +- upgradeInsecureRequests() +- workerSrc(sources) + +*Example:* ```swift -let cspConfig = ContentSecurityPolicyConfiguration(value: "default-src 'none'; script-src https://static.brokenhands.io; style-src https://static.brokenhands.io; img-src https://static.brokenhands.io; font-src https://static.brokenhands.io; connect-src https://*.brokenhands.io; form-action 'self'; upgrade-insecure-requests; block-all-mixed-content; require-sri-for script style; report-uri https://csp-report.brokenhands.io") +let cspConfig = ContentSecurityPolicy() + .scriptSrc(sources: "https://static.brokenhands.io") + .styleSrc(sources: "https://static.brokenhands.io") + .imgSrc(sources: "https://static.brokenhands.io") +``` + +```http +Content-Security-Policy: script-src https://static.brokenhands.io; style-src https://static.brokenhands.io; img-src https://static.brokenhands.io +``` + +You can set a custom header with ContentSecurityPolicy().set(value) or ContentSecurityPolicyConfiguration(value). + +**ContentSecurityPolicy().set(value)** + +```swift +let cspBuilder = ContentSecurityPolicy().set(value: "default-src: 'none'") + +let cspConfig = ContentSecurityPolicyConfiguration(value: cspBuilder) + let securityHeaders = SecurityHeaders(contentSecurityPolicyConfiguration: cspConfig) ``` +**ContentSecurityPolicyConfiguration(value)** + +```swift +let cspConfig = ContentSecurityPolicyConfiguration(value: "default-src 'none'") + +let securityHeaders = SecurityHeaders(contentSecurityPolicyConfiguration: cspConfig) +``` + +```http +Content-Security-Policy: default-src: 'none' +``` + +The following CSP keywords (`CSPKeywords`) are also available to you: + +* CSPKeywords.all = * +* CSPKeywords.none = 'none' +* CSPKeywords.\`self\` = 'self' +* CSPKeywords.strictDynamic = 'strict-dynamic' +* CSPKeywords.unsafeEval = 'unsafe-eval' +* CSPKeywords.unsafeHashedAttributes = 'unsafe-hashed-attributes' +* CSPKeywords.unsafeInline = 'unsafe-inline' + +*Example:* + +``` swift +CSPKeywords.`self` // “‘self’” +ContentSecurityPolicy().defaultSrc(sources: CSPKeywords.`self`) +``` + +```http +Content-Security-Policy: default-src 'self' +``` + +You can also utilize the `Report-To` directive: + +```swift +let reportToEndpoint = CSPReportToEndpoint(url: "https://csp-report.brokenhands.io/csp-reports") + +let reportToValue = CSPReportTo(group: "vapor-csp", max_age: 10886400, endpoints: [reportToEndpoint], include_subdomains: true) + +let cspValue = ContentSecurityPolicy() + .defaultSrc(sources: CSPKeywords.none) + .scriptSrc(sources: "https://static.brokenhands.io") + .reportTo(reportToObject: reportToValue) +``` + +```http +Content-Security-Policy: default-src 'none'; script-src https://static.brokenhands.io; report-to {"group":"vapor-csp","endpoints":[{"url":"https:\/\/csp-report.brokenhands.io\/csp-reports"}],"include_subdomains":true,"max_age":10886400} +``` + +See [Google Developers - The Reporting API](https://developers.google.com/web/updates/2018/09/reportingapi) for more information on the Report-To directive. + +#### Content Security Policy Configuration + +To configure your CSP you can add it to your `ContentSecurityPolicyConfiguration` like so: + +```swift +let cspBuilder = ContentSecurityPolicy() + .defaultSrc(sources: CSPKeywords.none) + .scriptSrc(sources: "https://static.brokenhands.io") + .styleSrc(sources: "https://static.brokenhands.io") + .imgSrc(sources: "https://static.brokenhands.io") + .fontSrc(sources: "https://static.brokenhands.io") + .connectSrc(sources: "https://*.brokenhands.io") + .formAction(sources: CSPKeywords.`self`) + .upgradeInsecureRequests() + .blockAllMixedContent() + .requireSriFor(values: "script", "style") + .reportUri(uri: "https://csp-report.brokenhands.io") + +let cspConfig = ContentSecurityPolicyConfiguration(value: cspBuilder) + +let securityHeaders = SecurityHeaders(contentSecurityPolicyConfiguration: cspConfig) +``` + +```http +Content-Security-Policy: default-src 'none'; script-src https://static.brokenhands.io; style-src https://static.brokenhands.io; img-src https://static.brokenhands.io; font-src https://static.brokenhands.io; connect-src https://*.brokenhands.io; form-action 'self'; upgrade-insecure-requests; block-all-mixed-content; require-sri-for script style; report-uri https://csp-report.brokenhands.io +``` + This policy means that by default everything is blocked, however: * Scripts can be loaded from `https://static.brokenhands.io` @@ -135,11 +256,18 @@ Check out [https://report-uri.io/](https://report-uri.io/) for a free tool to se Vapor Security Headers also supports setting the CSP on a route or request basis. If the middleware has been added to the `MiddlewareConfig`, you can override the CSP for a request. This allows you to have a strict default CSP, but allow content from extra sources when required, such as only allowing the Javascript for blog comments on the blog page. Create a separate `ContentSecurityPolicyConfiguration` and then add it to the request. For example, inside a route handler, you could do: ```swift -let pageSpecificCSPVaue = "default-src 'none'; script-src https://comments.disqus.com;" +let cspConfig = ContentSecurityPolicy() + .defaultSrc(sources: CSPKeywords.none) + .scriptSrc(sources: "https://comments.disqus.com") + let pageSpecificCSP = ContentSecurityPolicyConfiguration(value: pageSpecificCSPValue) request.contentSecurityPolicy = pageSpecificCSP ``` +```http +Content-Security-Policy: default-src 'none'; script-src https://comments.disqus.com +``` + You must also enable the `CSPRequestConfiguration` service for this to work. In `configure.swift` add: ```swift diff --git a/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift b/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift index 155f6d8..0f31a89 100644 --- a/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift +++ b/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift @@ -1,13 +1,17 @@ import Vapor +import Foundation public struct ContentSecurityPolicyConfiguration: SecurityHeaderConfiguration { - private let value: String public init(value: String) { self.value = value } + public init(value: ContentSecurityPolicy) { + self.value = value.value + } + func setHeader(on response: Response, from request: Request) { if let requestCSP = request.contentSecurityPolicy { response.http.headers.replaceOrAdd(name: .contentSecurityPolicy, value: requestCSP.value) @@ -38,3 +42,174 @@ extension Request { } } } + +public struct CSPReportTo: Codable { + private let group: String? + private let max_age: Int + private let endpoints: [CSPReportToEndpoint] + private let include_subdomains: Bool? + + public init(group: String? = nil, max_age: Int, + endpoints: [CSPReportToEndpoint], include_subdomains: Bool? = nil) { + self.group = group + self.max_age = max_age + self.endpoints = endpoints + self.include_subdomains = include_subdomains + } +} + +public struct CSPReportToEndpoint: Codable { + private let url: String + + public init(url: String) { + self.url = url + } +} + +extension CSPReportToEndpoint: Equatable { + public static func == (lhs: CSPReportToEndpoint, rhs: CSPReportToEndpoint) -> Bool { + return lhs.url == rhs.url + } +} + +extension CSPReportTo: Equatable { + public static func == (lhs: CSPReportTo, rhs: CSPReportTo) -> Bool { + return lhs.group == rhs.group && + lhs.max_age == rhs.max_age && + lhs.endpoints == rhs.endpoints && + lhs.include_subdomains == rhs.include_subdomains + } +} + +public struct CSPKeywords { + public static let all = "*" + public static let none = "'none'" + public static let `self` = "'self'" + public static let strictDynamic = "'strict-dynamic'" + public static let unsafeEval = "'unsafe-eval'" + public static let unsafeHashedAttributes = "'unsafe-hashed-attributes'" + public static let unsafeInline = "'unsafe-inline'" +} + +public class ContentSecurityPolicy { + private var policy: [String] = [] + + var value: String { + return policy.joined(separator: "; ") + } + + public func set(value: String) -> ContentSecurityPolicy { + policy.append(value) + return self + } + + public func baseUri(sources: String...) -> ContentSecurityPolicy { + policy.append("base-uri \(sources.joined(separator: " "))") + return self + } + + public func blockAllMixedContent() -> ContentSecurityPolicy { + policy.append("block-all-mixed-content") + return self + } + + public func connectSrc(sources: String...) -> ContentSecurityPolicy { + policy.append("connect-src \(sources.joined(separator: " "))") + return self + } + + public func defaultSrc(sources: String...) -> ContentSecurityPolicy { + policy.append("default-src \(sources.joined(separator: " "))") + return self + } + + public func fontSrc(sources: String...) -> ContentSecurityPolicy { + policy.append("font-src \(sources.joined(separator: " "))") + return self + } + + public func formAction(sources: String...) -> ContentSecurityPolicy { + policy.append("form-action \(sources.joined(separator: " "))") + return self + } + + public func frameAncestors(sources: String...) -> ContentSecurityPolicy { + policy.append("frame-ancestors \(sources.joined(separator: " "))") + return self + } + + public func frameSrc(sources: String...) -> ContentSecurityPolicy { + policy.append("frame-src \(sources.joined(separator: " "))") + return self + } + + public func imgSrc(sources: String...) -> ContentSecurityPolicy { + policy.append("img-src \(sources.joined(separator: " "))") + return self + } + + public func manifestSrc(sources: String...) -> ContentSecurityPolicy { + policy.append("manifest-src \(sources.joined(separator: " "))") + return self + } + + public func mediaSrc(sources: String...) -> ContentSecurityPolicy { + policy.append("media-src \(sources.joined(separator: " "))") + return self + } + + public func objectSrc(sources: String...) -> ContentSecurityPolicy { + policy.append("object-src \(sources.joined(separator: " "))") + return self + } + + public func pluginTypes(types: String...) -> ContentSecurityPolicy { + policy.append("plugin-types \(types.joined(separator: " "))") + return self + } + + public func requireSriFor(values: String...) -> ContentSecurityPolicy { + policy.append("require-sri-for \(values.joined(separator: " "))") + return self + } + + public func reportTo(reportToObject: CSPReportTo) -> ContentSecurityPolicy { + let encoder = JSONEncoder() + guard let data = try? encoder.encode(reportToObject) else { return self } + guard let jsonString = String(data: data, encoding: .utf8) else { return self } + policy.append("report-to \(String(describing: jsonString))") + return self + } + + public func reportUri(uri: String) -> ContentSecurityPolicy { + policy.append("report-uri \(uri)") + return self + } + + public func sandbox(values: String...) -> ContentSecurityPolicy { + policy.append("sandbox \(values.joined(separator: " "))") + return self + } + + public func scriptSrc(sources: String...) -> ContentSecurityPolicy { + policy.append("script-src \(sources.joined(separator: " "))") + return self + } + + public func styleSrc(sources: String...) -> ContentSecurityPolicy { + policy.append("style-src \(sources.joined(separator: " "))") + return self + } + + public func upgradeInsecureRequests() -> ContentSecurityPolicy { + policy.append("upgrade-insecure-requests") + return self + } + + public func workerSrc(sources: String...) -> ContentSecurityPolicy { + policy.append("worker-src \(sources.joined(separator: " "))") + return self + } + + public init() {} +} diff --git a/Sources/VaporSecurityHeaders/SecurityHeaders.swift b/Sources/VaporSecurityHeaders/SecurityHeaders.swift index b69ff60..c7476a9 100644 --- a/Sources/VaporSecurityHeaders/SecurityHeaders.swift +++ b/Sources/VaporSecurityHeaders/SecurityHeaders.swift @@ -6,7 +6,7 @@ public struct SecurityHeaders { var configurations: [SecurityHeaderConfiguration] init(contentTypeConfiguration: ContentTypeOptionsConfiguration = ContentTypeOptionsConfiguration(option: .nosniff), - contentSecurityPolicyConfiguration: ContentSecurityPolicyConfiguration = ContentSecurityPolicyConfiguration(value: "default-src 'self'"), + contentSecurityPolicyConfiguration: ContentSecurityPolicyConfiguration = ContentSecurityPolicyConfiguration(value: ContentSecurityPolicy().defaultSrc(sources: CSPKeywords.`self`)), frameOptionsConfiguration: FrameOptionsConfiguration = FrameOptionsConfiguration(option: .deny), xssProtectionConfiguration: XSSProtectionConfiguration = XSSProtectionConfiguration(option: .block), hstsConfiguration: StrictTransportSecurityConfiguration? = nil, diff --git a/Sources/VaporSecurityHeaders/SecurityHeadersFactory.swift b/Sources/VaporSecurityHeaders/SecurityHeadersFactory.swift index bea069a..9d883b8 100644 --- a/Sources/VaporSecurityHeaders/SecurityHeadersFactory.swift +++ b/Sources/VaporSecurityHeaders/SecurityHeadersFactory.swift @@ -2,7 +2,7 @@ import Vapor public class SecurityHeadersFactory { var contentTypeOptions = ContentTypeOptionsConfiguration(option: .nosniff) - var contentSecurityPolicy = ContentSecurityPolicyConfiguration(value: "default-src 'self'") + var contentSecurityPolicy = ContentSecurityPolicyConfiguration(value: ContentSecurityPolicy().defaultSrc(sources: CSPKeywords.`self`)) var frameOptions = FrameOptionsConfiguration(option: .deny) var xssProtection = XSSProtectionConfiguration(option: .block) var hsts: StrictTransportSecurityConfiguration? @@ -14,7 +14,7 @@ public class SecurityHeadersFactory { public static func api() -> SecurityHeadersFactory { let apiFactory = SecurityHeadersFactory() - apiFactory.contentSecurityPolicy = ContentSecurityPolicyConfiguration(value: "default-src 'none'") + apiFactory.contentSecurityPolicy = ContentSecurityPolicyConfiguration(value: ContentSecurityPolicy().defaultSrc(sources: CSPKeywords.none)) return apiFactory } diff --git a/Tests/VaporSecurityHeadersTests/HeaderTests.swift b/Tests/VaporSecurityHeadersTests/HeaderTests.swift index 97907ca..8a94483 100644 --- a/Tests/VaporSecurityHeadersTests/HeaderTests.swift +++ b/Tests/VaporSecurityHeadersTests/HeaderTests.swift @@ -33,6 +33,10 @@ class HeaderTests: XCTestCase { ("testHeadersWithHSTSwithSubdomainAndPreloadFalse", testHeadersWithHSTSwithSubdomainAndPreloadFalse), ("testHeadersWithServerValue", testHeadersWithServerValue), ("testHeadersWithCSP", testHeadersWithCSP), + ("testHeadersWithStringCSP", testHeadersWithStringCSP), + ("testHeadersWithSetCSP", testHeadersWithSetCSP), + ("testHeadersWithReportToCSP", testHeadersWithReportToCSP), + ("testHeadersWithExhaustiveCSP", testHeadersWithExhaustiveCSP), ("testHeadersWithReportOnlyCSP", testHeadersWithReportOnlyCSP), ("testHeadersWithReferrerPolicyEmpty", testHeadersWithReferrerPolicyEmpty), ("testHeadersWithReferrerPolicyNoReferrer", testHeadersWithReferrerPolicyNoReferrer), @@ -278,7 +282,26 @@ class HeaderTests: XCTestCase { } func testHeadersWithCSP() throws { - let csp = "default-src 'none'; script-src https://static.brokenhands.io; style-src https://static.brokenhands.io; img-src https://static.brokenhands.io; font-src https://static.brokenhands.io; connect-src https://*.brokenhands.io; form-action 'self'; upgrade-insecure-requests; block-all-mixed-content; require-sri-for script style;" + let csp = "default-src 'none'; script-src https://static.brokenhands.io; style-src https://static.brokenhands.io; img-src https://static.brokenhands.io; font-src https://static.brokenhands.io; connect-src https://*.brokenhands.io; form-action 'self'; upgrade-insecure-requests; block-all-mixed-content; require-sri-for script style" + let cspBuilder = ContentSecurityPolicy() + .defaultSrc(sources: CSPKeywords.none) + .scriptSrc(sources: "https://static.brokenhands.io").styleSrc(sources: "https://static.brokenhands.io") + .imgSrc(sources: "https://static.brokenhands.io") + .fontSrc(sources: "https://static.brokenhands.io") + .connectSrc(sources: "https://*.brokenhands.io") + .formAction(sources: CSPKeywords.`self`) + .upgradeInsecureRequests() + .blockAllMixedContent() + .requireSriFor(values: "script", "style") + let cspConfig = ContentSecurityPolicyConfiguration(value: cspBuilder) + let factory = SecurityHeadersFactory().with(contentSecurityPolicy: cspConfig) + let response = try makeTestResponse(for: request, securityHeadersToAdd: factory) + + XCTAssertEqual(csp, response.http.headers[.contentSecurityPolicy].first) + } + + func testHeadersWithStringCSP() throws { + let csp = "default-src 'none'; script-src https://static.brokenhands.io; style-src https://static.brokenhands.io; img-src https://static.brokenhands.io; font-src https://static.brokenhands.io; connect-src https://*.brokenhands.io; form-action 'self'; upgrade-insecure-requests; block-all-mixed-content; require-sri-for script style" let cspConfig = ContentSecurityPolicyConfiguration(value: csp) let factory = SecurityHeadersFactory().with(contentSecurityPolicy: cspConfig) let response = try makeTestResponse(for: request, securityHeadersToAdd: factory) @@ -286,6 +309,63 @@ class HeaderTests: XCTestCase { XCTAssertEqual(csp, response.http.headers[.contentSecurityPolicy].first) } + func testHeadersWithSetCSP() throws { + let csp = "default-src 'none'; script-src https://static.brokenhands.io; style-src https://static.brokenhands.io; img-src https://static.brokenhands.io; font-src https://static.brokenhands.io; connect-src https://*.brokenhands.io; form-action 'self'; upgrade-insecure-requests; block-all-mixed-content; require-sri-for script style" + let cspBuilder = ContentSecurityPolicy().set(value: csp) + let cspConfig = ContentSecurityPolicyConfiguration(value: cspBuilder) + let factory = SecurityHeadersFactory().with(contentSecurityPolicy: cspConfig) + let response = try makeTestResponse(for: request, securityHeadersToAdd: factory) + + XCTAssertEqual(csp, response.http.headers[.contentSecurityPolicy].first) + } + + func testHeadersWithReportToCSP() throws { + let reportToEndpoint = CSPReportToEndpoint(url: "https://csp-report.brokenhands.io/csp-reports") + let reportToValue = CSPReportTo(group: "vapor-csp", max_age: 10886400, endpoints: [reportToEndpoint], include_subdomains: true) + let cspValue = ContentSecurityPolicy() + .defaultSrc(sources: CSPKeywords.none) + .scriptSrc(sources: "https://static.brokenhands.io") + .reportTo(reportToObject: reportToValue) + let cspConfig = ContentSecurityPolicyConfiguration(value: cspValue) + let factory = SecurityHeadersFactory().with(contentSecurityPolicy: cspConfig) + let response = try makeTestResponse(for: request, securityHeadersToAdd: factory) + guard let cspResponseHeader = response.http.headers[.contentSecurityPolicy].first else { + XCTFail("Expected a CSP Response Header") + return + } + let replacedCSPHeader = cspResponseHeader.replacingOccurrences(of: "default-src 'none'; script-src https://static.brokenhands.io; report-to", with: "") + guard let reportToJson = replacedCSPHeader.data(using: .utf8) else { + XCTFail("Expected String CSP Response Header") + return + } + let decoder = JSONDecoder() + guard let reportToData = try? decoder.decode(CSPReportTo.self, from: reportToJson) else { + XCTFail("Expected JSON CSP Response Header") + return + } + + XCTAssertEqual(reportToValue, reportToData) + } + + func testHeadersWithExhaustiveCSP() throws { + let csp = "base-uri 'self'; frame-ancestors 'none'; frame-src 'self'; manifest-src https://brokenhands.io; object-src 'self'; plugin-types application/pdf; report-uri https://csp-report.brokenhands.io; sandbox allow-forms allow-scripts; worker-src https://brokenhands.io; media-src https://brokenhands.io" + let cspBuilder = ContentSecurityPolicy() + .baseUri(sources: CSPKeywords.`self`) + .frameAncestors(sources: CSPKeywords.none) + .frameSrc(sources: CSPKeywords.`self`) + .manifestSrc(sources: "https://brokenhands.io") + .objectSrc(sources: CSPKeywords.`self`) + .pluginTypes(types: "application/pdf") + .reportUri(uri: "https://csp-report.brokenhands.io").sandbox(values: "allow-forms", "allow-scripts") + .workerSrc(sources: "https://brokenhands.io") + .mediaSrc(sources: "https://brokenhands.io") + let cspConfig = ContentSecurityPolicyConfiguration(value: cspBuilder) + let factory = SecurityHeadersFactory().with(contentSecurityPolicy: cspConfig) + let response = try makeTestResponse(for: request, securityHeadersToAdd: factory) + + XCTAssertEqual(csp, response.http.headers[.contentSecurityPolicy].first) + } + func testHeadersWithReportOnlyCSP() throws { let csp = "default-src https:; report-uri https://csp-report.brokenhands.io" let cspConfig = ContentSecurityPolicyReportOnlyConfiguration(value: csp) @@ -376,10 +456,20 @@ class HeaderTests: XCTestCase { } func testCustomCSPOnSingleRoute() throws { - let expectedCsp = "default-src 'none'; script-src https://static.brokenhands.io; style-src https://static.brokenhands.io; img-src https://static.brokenhands.io; font-src https://static.brokenhands.io; connect-src https://*.brokenhands.io; form-action 'self'; upgrade-insecure-requests; block-all-mixed-content; require-sri-for script style;" + let expectedCsp = "default-src 'none'; script-src https://static.brokenhands.io; style-src https://static.brokenhands.io; img-src https://static.brokenhands.io; font-src https://static.brokenhands.io; connect-src https://*.brokenhands.io; form-action 'self'; upgrade-insecure-requests; block-all-mixed-content; require-sri-for script style" + let cspBuilder = ContentSecurityPolicy() + .defaultSrc(sources: CSPKeywords.none) + .scriptSrc(sources: "https://static.brokenhands.io").styleSrc(sources: "https://static.brokenhands.io") + .imgSrc(sources: "https://static.brokenhands.io") + .fontSrc(sources: "https://static.brokenhands.io") + .connectSrc(sources: "https://*.brokenhands.io") + .formAction(sources: CSPKeywords.`self`) + .upgradeInsecureRequests() + .blockAllMixedContent() + .requireSriFor(values: "script", "style") let factory = SecurityHeadersFactory.api() let cspSettingRouteHandler: (Request) throws -> String = { req in - req.contentSecurityPolicy = ContentSecurityPolicyConfiguration(value: expectedCsp) + req.contentSecurityPolicy = ContentSecurityPolicyConfiguration(value: cspBuilder) return "Different CSP!" } let response = try makeTestResponse(for: routeRequest, securityHeadersToAdd: factory, routeHandler: cspSettingRouteHandler, perRouteCSP: true) @@ -388,7 +478,16 @@ class HeaderTests: XCTestCase { } func testCustomCSPDoesntAffectSecondRoute() throws { - let customCSP = "default-src 'none'; script-src https://static.brokenhands.io; style-src https://static.brokenhands.io; img-src https://static.brokenhands.io; font-src https://static.brokenhands.io; connect-src https://*.brokenhands.io; form-action 'self'; upgrade-insecure-requests; block-all-mixed-content; require-sri-for script style;" + let customCSP = ContentSecurityPolicy() + .defaultSrc(sources: CSPKeywords.none) + .scriptSrc(sources: "https://static.brokenhands.io").styleSrc(sources: "https://static.brokenhands.io") + .imgSrc(sources: "https://static.brokenhands.io") + .fontSrc(sources: "https://static.brokenhands.io") + .connectSrc(sources: "https://*.brokenhands.io") + .formAction(sources: CSPKeywords.`self`) + .upgradeInsecureRequests() + .blockAllMixedContent() + .requireSriFor(values: "script", "style") let factory = SecurityHeadersFactory.api() let cspSettingRouteHandler: (Request) throws -> String = { req in req.contentSecurityPolicy = ContentSecurityPolicyConfiguration(value: customCSP) @@ -401,7 +500,9 @@ class HeaderTests: XCTestCase { } func testDifferentRequestReturnsDefaultCSPWhenSettingCustomCSPOnRoute() throws { - let differentCsp = "default-src 'none'; script-src test;" + let differentCsp = ContentSecurityPolicy() + .defaultSrc(sources: CSPKeywords.none) + .scriptSrc(sources: "test") let factory = SecurityHeadersFactory.api() let cspSettingRouteHandler: (Request) throws -> String = { req in req.contentSecurityPolicy = ContentSecurityPolicyConfiguration(value: differentCsp) @@ -431,7 +532,7 @@ class HeaderTests: XCTestCase { let expectedCSPHeaderValue = "default-src 'none'" let expectedXFOHeaderValue = "DENY" let expectedXSSProtectionHeaderValue = "1; mode=block" - let response = try makeTestResponse(for: fileRequest, securityHeadersToAdd: SecurityHeadersFactory.api(), fileMiddleware: StubFileMiddleware()) + let response = try makeTestResponse(for: fileRequest, securityHeadersToAdd: SecurityHeadersFactory.api(), fileMiddleware: StubFileMiddleware()) XCTAssertEqual("Hello World!", String(data: response.http.body.data!, encoding: String.Encoding.utf8)) XCTAssertEqual(expectedXCTOHeaderValue, response.http.headers[.xContentTypeOptions].first) @@ -442,11 +543,14 @@ class HeaderTests: XCTestCase { func testMockFileMiddlewareDifferentRequestReturnsDefaultCSPWhenSettingCustomCSPOnRoute() throws { let expectedXCTOHeaderValue = "nosniff" - let expectedCSPHeaderValue = "default-src 'none'; script-src test;" + let expectedCSPHeaderValue = "default-src 'none'; script-src test" + let csp = ContentSecurityPolicy() + .defaultSrc(sources: CSPKeywords.none) + .scriptSrc(sources: "test") let expectedXFOHeaderValue = "DENY" let expectedXSSProtectionHeaderValue = "1; mode=block" - let response = try makeTestResponse(for: fileRequest, securityHeadersToAdd: SecurityHeadersFactory.api(), fileMiddleware: StubFileMiddleware(cspConfig: ContentSecurityPolicyConfiguration(value: expectedCSPHeaderValue)), perRouteCSP: true) + let response = try makeTestResponse(for: fileRequest, securityHeadersToAdd: SecurityHeadersFactory.api(), fileMiddleware: StubFileMiddleware(cspConfig: ContentSecurityPolicyConfiguration(value: csp)), perRouteCSP: true) XCTAssertEqual("Hello World!", String(data: response.http.body.data!, encoding: String.Encoding.utf8)) XCTAssertEqual(expectedXCTOHeaderValue, response.http.headers[.xContentTypeOptions].first)