From c025545f7099656c07ba025b52eb1735e6c58517 Mon Sep 17 00:00:00 2001 From: Caleb Kinney Date: Thu, 14 Feb 2019 04:57:43 -0500 Subject: [PATCH 01/11] Add Content Security Policy builder - Adjust SecurityHeaders., SecurityHeadersFactory for new CSP builder - Modify tests for new CSP builder --- README.md | 96 +++++++++++- .../ContentSecurityPolicyConfiguration.swift | 147 +++++++++++++++++- .../SecurityHeaders.swift | 2 +- .../SecurityHeadersFactory.swift | 4 +- .../HeaderTests.swift | 50 +++++- 5 files changed, 283 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index f133173..1433374 100644 --- a/README.md +++ b/README.md @@ -108,13 +108,96 @@ 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 set(value). + +```swift +ContentSecurityPolicy().set(value: "default-src: 'none'") +``` + +```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 cspConfig = 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 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 +218,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..bb139a6 100644 --- a/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift +++ b/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift @@ -1,11 +1,12 @@ 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) { @@ -38,3 +39,145 @@ extension Request { } } } + +public struct CSPReportTo: Codable { + var group: String? + var max_age: Int + var endpoints: [CSPReportToEndpoint] + var include_subdomains: Bool? +} + +public struct CSPReportToEndpoint: Codable { + var url: String +} + +public struct CSPKeywords { + static let all = "*" + static let none = "'none'" + static let `self` = "'self'" + static let strictDynamic = "'strict-dynamic'" + static let unsafeEval = "'unsafe-eval'" + static let unsafeHashedAttributes = "'unsafe-hashed-attributes'" + static let unsafeInline = "'unsafe-inline'" +} + +public class ContentSecurityPolicy { + private var policy: [String] = [] + + var value: String { + return policy.joined(separator: "; ") + } + + func set(value: String) -> ContentSecurityPolicy { + policy.append(value) + return self + } + + func baseUri(sources: String...) -> ContentSecurityPolicy { + policy.append("base-uri \(sources.joined(separator: " "))") + return self + } + + func blockAllMixedContent() -> ContentSecurityPolicy { + policy.append("block-all-mixed-content") + return self + } + + func connectSrc(sources: String...) -> ContentSecurityPolicy { + policy.append("connect-src \(sources.joined(separator: " "))") + return self + } + + func defaultSrc(sources: String...) -> ContentSecurityPolicy { + policy.append("default-src \(sources.joined(separator: " "))") + return self + } + + func fontSrc(sources: String...) -> ContentSecurityPolicy { + policy.append("font-src \(sources.joined(separator: " "))") + return self + } + + func formAction(sources: String...) -> ContentSecurityPolicy { + policy.append("form-action \(sources.joined(separator: " "))") + return self + } + + func frameAncestors(sources: String...) -> ContentSecurityPolicy { + policy.append("frame-ancestors \(sources.joined(separator: " "))") + return self + } + + func frameSrc(sources: String...) -> ContentSecurityPolicy { + policy.append("frame-src \(sources.joined(separator: " "))") + return self + } + + func imgSrc(sources: String...) -> ContentSecurityPolicy { + policy.append("img-src \(sources.joined(separator: " "))") + return self + } + + func manifestSrc(sources: String...) -> ContentSecurityPolicy { + policy.append("manifest-src \(sources.joined(separator: " "))") + return self + } + + func mediaSrc(sources: String...) -> ContentSecurityPolicy { + policy.append("media-src \(sources.joined(separator: " "))") + return self + } + + func objectSrc(sources: String...) -> ContentSecurityPolicy { + policy.append("object-src \(sources.joined(separator: " "))") + return self + } + + func pluginTypes(types: String...) -> ContentSecurityPolicy { + policy.append("plugin-types \(types.joined(separator: " "))") + return self + } + + func requireSriFor(values: String...) -> ContentSecurityPolicy { + policy.append("require-sri-for \(values.joined(separator: " "))") + return self + } + + func reportTo(reportToObject: CSPReportTo) -> ContentSecurityPolicy { + let encoder = JSONEncoder() + let data = try! encoder.encode(reportToObject) + guard let jsonString = String(data: data, encoding: .utf8) else { return self } + policy.append("report-to \(String(describing: jsonString))") + return self + } + + func reportUri(uri: String) -> ContentSecurityPolicy { + policy.append("report-uri \(uri)") + return self + } + + func sandbox(values: String...) -> ContentSecurityPolicy { + policy.append("sandbox \(values.joined(separator: " "))") + return self + } + + func scriptSrc(sources: String...) -> ContentSecurityPolicy { + policy.append("script-src \(sources.joined(separator: " "))") + return self + } + + func styleSrc(sources: String...) -> ContentSecurityPolicy { + policy.append("style-src \(sources.joined(separator: " "))") + return self + } + + func upgradeInsecureRequests() -> ContentSecurityPolicy { + policy.append("upgrade-insecure-requests") + return self + } + + func workerSrc(sources: String...) -> ContentSecurityPolicy { + policy.append("worker-src \(sources.joined(separator: " "))") + return self + } +} 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 9c952a7..9fc993f 100644 --- a/Tests/VaporSecurityHeadersTests/HeaderTests.swift +++ b/Tests/VaporSecurityHeadersTests/HeaderTests.swift @@ -278,8 +278,18 @@ 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 cspConfig = ContentSecurityPolicyConfiguration(value: csp) + 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) @@ -376,10 +386,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 expectedCspBuilder = 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: expectedCspBuilder) return "Different CSP!" } let response = try makeTestResponse(for: routeRequest, securityHeadersToAdd: factory, routeHandler: cspSettingRouteHandler, perRouteCSP: true) @@ -388,7 +408,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 +430,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 +462,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) @@ -443,10 +474,13 @@ class HeaderTests: XCTestCase { func testMockFileMiddlewareDifferentRequestReturnsDefaultCSPWhenSettingCustomCSPOnRoute() throws { let expectedXCTOHeaderValue = "nosniff" let expectedCSPHeaderValue = "default-src 'none'; script-src test;" + let expectedCSPHeaderValueBuilder = 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: expectedCSPHeaderValueBuilder)), perRouteCSP: true) XCTAssertEqual("Hello World!", String(data: response.http.body.data!, encoding: String.Encoding.utf8)) XCTAssertEqual(expectedXCTOHeaderValue, response.http.headers[.xContentTypeOptions].first) From 3fd068e60bd1dfb2c9e2efed70df77f3892daf1e Mon Sep 17 00:00:00 2001 From: Caleb Kinney Date: Thu, 14 Feb 2019 05:35:07 -0500 Subject: [PATCH 02/11] SwiftLint and SwiftFormat fixes - SwiftLint: (https://github.com/realm/SwiftLint) - SwiftFormat (https://github.com/nicklockwood/SwiftFormat) --- .../Configurations/ContentSecurityPolicyConfiguration.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift b/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift index bb139a6..ed17b62 100644 --- a/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift +++ b/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift @@ -2,7 +2,7 @@ import Vapor import Foundation public struct ContentSecurityPolicyConfiguration: SecurityHeaderConfiguration { - + private let value: String public init(value: ContentSecurityPolicy) { @@ -145,7 +145,7 @@ public class ContentSecurityPolicy { func reportTo(reportToObject: CSPReportTo) -> ContentSecurityPolicy { let encoder = JSONEncoder() - let data = try! encoder.encode(reportToObject) + 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 @@ -165,7 +165,7 @@ public class ContentSecurityPolicy { policy.append("script-src \(sources.joined(separator: " "))") return self } - + func styleSrc(sources: String...) -> ContentSecurityPolicy { policy.append("style-src \(sources.joined(separator: " "))") return self From ca571ee137bc484382557835a391683570404378 Mon Sep 17 00:00:00 2001 From: Caleb Kinney Date: Thu, 14 Feb 2019 06:13:08 -0500 Subject: [PATCH 03/11] Mark ContentSecurityPolicy initializer and functions public --- .../ContentSecurityPolicyConfiguration.swift | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift b/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift index ed17b62..7eeaa9e 100644 --- a/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift +++ b/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift @@ -68,82 +68,82 @@ public class ContentSecurityPolicy { return policy.joined(separator: "; ") } - func set(value: String) -> ContentSecurityPolicy { + public func set(value: String) -> ContentSecurityPolicy { policy.append(value) return self } - func baseUri(sources: String...) -> ContentSecurityPolicy { + public func baseUri(sources: String...) -> ContentSecurityPolicy { policy.append("base-uri \(sources.joined(separator: " "))") return self } - func blockAllMixedContent() -> ContentSecurityPolicy { + public func blockAllMixedContent() -> ContentSecurityPolicy { policy.append("block-all-mixed-content") return self } - func connectSrc(sources: String...) -> ContentSecurityPolicy { + public func connectSrc(sources: String...) -> ContentSecurityPolicy { policy.append("connect-src \(sources.joined(separator: " "))") return self } - func defaultSrc(sources: String...) -> ContentSecurityPolicy { + public func defaultSrc(sources: String...) -> ContentSecurityPolicy { policy.append("default-src \(sources.joined(separator: " "))") return self } - func fontSrc(sources: String...) -> ContentSecurityPolicy { + public func fontSrc(sources: String...) -> ContentSecurityPolicy { policy.append("font-src \(sources.joined(separator: " "))") return self } - func formAction(sources: String...) -> ContentSecurityPolicy { + public func formAction(sources: String...) -> ContentSecurityPolicy { policy.append("form-action \(sources.joined(separator: " "))") return self } - func frameAncestors(sources: String...) -> ContentSecurityPolicy { + public func frameAncestors(sources: String...) -> ContentSecurityPolicy { policy.append("frame-ancestors \(sources.joined(separator: " "))") return self } - func frameSrc(sources: String...) -> ContentSecurityPolicy { + public func frameSrc(sources: String...) -> ContentSecurityPolicy { policy.append("frame-src \(sources.joined(separator: " "))") return self } - func imgSrc(sources: String...) -> ContentSecurityPolicy { + public func imgSrc(sources: String...) -> ContentSecurityPolicy { policy.append("img-src \(sources.joined(separator: " "))") return self } - func manifestSrc(sources: String...) -> ContentSecurityPolicy { + public func manifestSrc(sources: String...) -> ContentSecurityPolicy { policy.append("manifest-src \(sources.joined(separator: " "))") return self } - func mediaSrc(sources: String...) -> ContentSecurityPolicy { + public func mediaSrc(sources: String...) -> ContentSecurityPolicy { policy.append("media-src \(sources.joined(separator: " "))") return self } - func objectSrc(sources: String...) -> ContentSecurityPolicy { + public func objectSrc(sources: String...) -> ContentSecurityPolicy { policy.append("object-src \(sources.joined(separator: " "))") return self } - func pluginTypes(types: String...) -> ContentSecurityPolicy { + public func pluginTypes(types: String...) -> ContentSecurityPolicy { policy.append("plugin-types \(types.joined(separator: " "))") return self } - func requireSriFor(values: String...) -> ContentSecurityPolicy { + public func requireSriFor(values: String...) -> ContentSecurityPolicy { policy.append("require-sri-for \(values.joined(separator: " "))") return self } - func reportTo(reportToObject: CSPReportTo) -> ContentSecurityPolicy { + 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 } @@ -151,33 +151,35 @@ public class ContentSecurityPolicy { return self } - func reportUri(uri: String) -> ContentSecurityPolicy { + public func reportUri(uri: String) -> ContentSecurityPolicy { policy.append("report-uri \(uri)") return self } - func sandbox(values: String...) -> ContentSecurityPolicy { + public func sandbox(values: String...) -> ContentSecurityPolicy { policy.append("sandbox \(values.joined(separator: " "))") return self } - func scriptSrc(sources: String...) -> ContentSecurityPolicy { + public func scriptSrc(sources: String...) -> ContentSecurityPolicy { policy.append("script-src \(sources.joined(separator: " "))") return self } - func styleSrc(sources: String...) -> ContentSecurityPolicy { + public func styleSrc(sources: String...) -> ContentSecurityPolicy { policy.append("style-src \(sources.joined(separator: " "))") return self } - func upgradeInsecureRequests() -> ContentSecurityPolicy { + public func upgradeInsecureRequests() -> ContentSecurityPolicy { policy.append("upgrade-insecure-requests") return self } - func workerSrc(sources: String...) -> ContentSecurityPolicy { + public func workerSrc(sources: String...) -> ContentSecurityPolicy { policy.append("worker-src \(sources.joined(separator: " "))") return self } + + public init() {} } From f6bdfa64459e169b3e6802394612685994d5402c Mon Sep 17 00:00:00 2001 From: Caleb Kinney Date: Thu, 14 Feb 2019 06:53:13 -0500 Subject: [PATCH 04/11] Make initializers and constants public - Mark CSPReportToEndpoint and CSPReportTo initializers public - Mark CSPKeywords static constants public --- .../ContentSecurityPolicyConfiguration.swift | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift b/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift index 7eeaa9e..654ab8b 100644 --- a/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift +++ b/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift @@ -41,24 +41,36 @@ extension Request { } public struct CSPReportTo: Codable { - var group: String? - var max_age: Int - var endpoints: [CSPReportToEndpoint] - var include_subdomains: Bool? + 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 { - var url: String + private let url: String + + public init(url: String) { + self.url = url + } } public struct CSPKeywords { - static let all = "*" - static let none = "'none'" - static let `self` = "'self'" - static let strictDynamic = "'strict-dynamic'" - static let unsafeEval = "'unsafe-eval'" - static let unsafeHashedAttributes = "'unsafe-hashed-attributes'" - static let unsafeInline = "'unsafe-inline'" + 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 { From 176d8cd6d2136c7da8d7c871d8338142f8364d5a Mon Sep 17 00:00:00 2001 From: Caleb Kinney Date: Fri, 15 Feb 2019 05:52:30 -0500 Subject: [PATCH 05/11] Add constructor override --- .../Configurations/ContentSecurityPolicyConfiguration.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift b/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift index 654ab8b..6a97813 100644 --- a/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift +++ b/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift @@ -5,6 +5,10 @@ public struct ContentSecurityPolicyConfiguration: SecurityHeaderConfiguration { private let value: String + public init(value: String) { + self.value = value + } + public init(value: ContentSecurityPolicy) { self.value = value.value } From b9ecb99ce9c781a2c3efcec5bca36a1060982d81 Mon Sep 17 00:00:00 2001 From: Caleb Kinney Date: Fri, 15 Feb 2019 05:55:48 -0500 Subject: [PATCH 06/11] README CSP updates - Format CSP builder directives - Add custom header via ContentSecurityPolicy().set(value) or ContentSecurityPolicyConfiguration(value) - Fix configure CSP missing step --- README.md | 48 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1433374..c7b9405 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,29 @@ 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! -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) +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:* @@ -123,10 +145,24 @@ let cspConfig = ContentSecurityPolicy() 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 set(value). +You can set a custom header with ContentSecurityPolicy().set(value) or ContentSecurityPolicyConfiguration(value). + +**ContentSecurityPolicy().set(value)** ```swift -ContentSecurityPolicy().set(value: "default-src: 'none'") +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 @@ -178,7 +214,7 @@ See [Google Developers - The Reporting API](https://developers.google.com/web/up To configure your CSP you can add it to your `ContentSecurityPolicyConfiguration` like so: ```swift -let cspConfig = ContentSecurityPolicy() +let cspBuilder = ContentSecurityPolicy() .defaultSrc(sources: CSPKeywords.none) .scriptSrc(sources: "https://static.brokenhands.io") .styleSrc(sources: "https://static.brokenhands.io") @@ -190,7 +226,9 @@ let cspConfig = ContentSecurityPolicy() .blockAllMixedContent() .requireSriFor(values: "script", "style") .reportUri(uri: "https://csp-report.brokenhands.io") - + +let cspConfig = ContentSecurityPolicyConfiguration(value: cspBuilder) + let securityHeaders = SecurityHeaders(contentSecurityPolicyConfiguration: cspConfig) ``` From f50d00fb8e95eea64e63efd56145bee4f61803ba Mon Sep 17 00:00:00 2001 From: Caleb Kinney Date: Fri, 15 Feb 2019 06:00:14 -0500 Subject: [PATCH 07/11] CSP test adjustments - Better names for constants - Remove semicolon on expectedCSPHeaderValue - Merge middleware test fix --- .../HeaderTests.swift | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/Tests/VaporSecurityHeadersTests/HeaderTests.swift b/Tests/VaporSecurityHeadersTests/HeaderTests.swift index 9fc993f..f04ca13 100644 --- a/Tests/VaporSecurityHeadersTests/HeaderTests.swift +++ b/Tests/VaporSecurityHeadersTests/HeaderTests.swift @@ -387,7 +387,7 @@ 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 expectedCspBuilder = ContentSecurityPolicy() + 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") @@ -399,7 +399,7 @@ class HeaderTests: XCTestCase { .requireSriFor(values: "script", "style") let factory = SecurityHeadersFactory.api() let cspSettingRouteHandler: (Request) throws -> String = { req in - req.contentSecurityPolicy = ContentSecurityPolicyConfiguration(value: expectedCspBuilder) + req.contentSecurityPolicy = ContentSecurityPolicyConfiguration(value: cspBuilder) return "Different CSP!" } let response = try makeTestResponse(for: routeRequest, securityHeadersToAdd: factory, routeHandler: cspSettingRouteHandler, perRouteCSP: true) @@ -473,14 +473,14 @@ class HeaderTests: XCTestCase { func testMockFileMiddlewareDifferentRequestReturnsDefaultCSPWhenSettingCustomCSPOnRoute() throws { let expectedXCTOHeaderValue = "nosniff" - let expectedCSPHeaderValue = "default-src 'none'; script-src test;" - let expectedCSPHeaderValueBuilder = ContentSecurityPolicy() + 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: expectedCSPHeaderValueBuilder)), 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) @@ -496,11 +496,6 @@ class HeaderTests: XCTestCase { var services = Services.default() var middlewareConfig = MiddlewareConfig() - if let fileMiddleware = fileMiddleware { - middlewareConfig.use(StubFileMiddleware.self) - services.register(fileMiddleware) - } - middlewareConfig.use(ErrorMiddleware.self) services.register { worker in return ErrorMiddleware() { request, error in @@ -509,6 +504,12 @@ class HeaderTests: XCTestCase { } middlewareConfig.use(SecurityHeaders.self) services.register(securityHeadersToAdd.build()) + + if let fileMiddleware = fileMiddleware { + middlewareConfig.use(StubFileMiddleware.self) + services.register(fileMiddleware) + } + services.register(middlewareConfig) if perRouteCSP { From 5104a4b0b73abae0924bb0a664226296d7859347 Mon Sep 17 00:00:00 2001 From: Caleb Kinney Date: Sat, 16 Feb 2019 05:44:18 -0500 Subject: [PATCH 08/11] Add CSP header tests --- .../HeaderTests.swift | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/Tests/VaporSecurityHeadersTests/HeaderTests.swift b/Tests/VaporSecurityHeadersTests/HeaderTests.swift index f04ca13..c8c8a13 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), @@ -296,6 +300,58 @@ class HeaderTests: XCTestCase { 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) + + 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 csp = "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}" + 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) + + XCTAssertEqual(csp, response.http.headers[.contentSecurityPolicy].first) + } + + 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" + 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") + 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) From f91ebfaefd81f497b955ed8493fdcd4220338013 Mon Sep 17 00:00:00 2001 From: Caleb Kinney Date: Sun, 17 Feb 2019 05:20:21 -0500 Subject: [PATCH 09/11] Fix testHeadersWithReportToCSP test - Make CSPReportTo and CSPReportToEndpoint Equatable - SwiftLint corrections --- .../ContentSecurityPolicyConfiguration.swift | 6 ++++-- .../VaporSecurityHeadersTests/HeaderTests.swift | 17 +++++++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift b/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift index 6a97813..4c0ea4a 100644 --- a/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift +++ b/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift @@ -2,7 +2,6 @@ import Vapor import Foundation public struct ContentSecurityPolicyConfiguration: SecurityHeaderConfiguration { - private let value: String public init(value: String) { @@ -67,6 +66,9 @@ public struct CSPReportToEndpoint: Codable { } } +extension CSPReportToEndpoint: Equatable {} +extension CSPReportTo: Equatable {} + public struct CSPKeywords { public static let all = "*" public static let none = "'none'" @@ -196,6 +198,6 @@ public class ContentSecurityPolicy { policy.append("worker-src \(sources.joined(separator: " "))") return self } - + public init() {} } diff --git a/Tests/VaporSecurityHeadersTests/HeaderTests.swift b/Tests/VaporSecurityHeadersTests/HeaderTests.swift index c8c8a13..bc8596a 100644 --- a/Tests/VaporSecurityHeadersTests/HeaderTests.swift +++ b/Tests/VaporSecurityHeadersTests/HeaderTests.swift @@ -320,7 +320,6 @@ class HeaderTests: XCTestCase { } func testHeadersWithReportToCSP() throws { - let csp = "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}" 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() @@ -330,8 +329,22 @@ class HeaderTests: XCTestCase { 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(csp, response.http.headers[.contentSecurityPolicy].first) + XCTAssertEqual(reportToValue, reportToData) } func testHeadersWithExhaustiveCSP() throws { From cc5f3d878e960997dc1b79af4ae457e0f8365033 Mon Sep 17 00:00:00 2001 From: Caleb Kinney Date: Sun, 17 Feb 2019 05:42:55 -0500 Subject: [PATCH 10/11] Add test coverage for mediaSrc --- Tests/VaporSecurityHeadersTests/HeaderTests.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/VaporSecurityHeadersTests/HeaderTests.swift b/Tests/VaporSecurityHeadersTests/HeaderTests.swift index bc8596a..8a94483 100644 --- a/Tests/VaporSecurityHeadersTests/HeaderTests.swift +++ b/Tests/VaporSecurityHeadersTests/HeaderTests.swift @@ -348,7 +348,7 @@ class HeaderTests: XCTestCase { } 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" + 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) @@ -358,6 +358,7 @@ class HeaderTests: XCTestCase { .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) From 75ffdfa95066ec8eafeec0f31edeca12f8f04111 Mon Sep 17 00:00:00 2001 From: Caleb Kinney Date: Sun, 17 Feb 2019 05:45:51 -0500 Subject: [PATCH 11/11] Remove automatic synthesis of Equatable --- .../ContentSecurityPolicyConfiguration.swift | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift b/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift index 4c0ea4a..0f31a89 100644 --- a/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift +++ b/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift @@ -66,8 +66,20 @@ public struct CSPReportToEndpoint: Codable { } } -extension CSPReportToEndpoint: Equatable {} -extension CSPReportTo: Equatable {} +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 = "*"