Skip to content

Commit

Permalink
Merge pull request #14 from cak/CSP-builder
Browse files Browse the repository at this point in the history
Add Content Security Policy builder
  • Loading branch information
0xTim authored Mar 1, 2019
2 parents 931b5a3 + 75ffdfa commit beecf53
Show file tree
Hide file tree
Showing 5 changed files with 422 additions and 15 deletions.
134 changes: 131 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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() {}
}
2 changes: 1 addition & 1 deletion Sources/VaporSecurityHeaders/SecurityHeaders.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions Sources/VaporSecurityHeaders/SecurityHeadersFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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
}

Expand Down
Loading

0 comments on commit beecf53

Please sign in to comment.