Skip to content

Commit

Permalink
Merge pull request #21 from brokenhandsio/vapor4
Browse files Browse the repository at this point in the history
Vapor 4
  • Loading branch information
0xTim committed Apr 10, 2020
2 parents f265e2e + 20c0f7e commit 8796636
Show file tree
Hide file tree
Showing 18 changed files with 222 additions and 289 deletions.
13 changes: 7 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
name: CI
on:
- push
push:
pull_request:
jobs:
xenial:
container:
image: vapor/swift:5.1-xenial
image: vapor/swift:5.2-xenial
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- run: swift test --enable-test-discovery --enable-code-coverage
- run: swift test --enable-test-discovery --enable-code-coverage --sanitize=thread
bionic:
container:
image: vapor/swift:5.1-bionic
image: vapor/swift:5.2-bionic
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Run Bionic Tests
run: swift test --enable-test-discovery --enable-code-coverage
run: swift test --enable-test-discovery --enable-code-coverage --sanitize=thread
- name: Setup container for codecov upload
run: apt-get update && apt-get install curl
- name: Process coverage file
run: llvm-cov show .build/x86_64-unknown-linux/debug/VaporSecurityHeadersPackageTests.xctest -instr-profile=.build/x86_64-unknown-linux/debug/codecov/default.profdata > coverage.txt
run: llvm-cov show .build/x86_64-unknown-linux-gnu/debug/VaporSecurityHeadersPackageTests.xctest -instr-profile=.build/debug/codecov/default.profdata > coverage.txt
- name: Upload code coverage
uses: codecov/codecov-action@v1
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
DerivedData/
Package.pins
Package.resolved
.swiftpm/
11 changes: 8 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
// swift-tools-version:4.0
// swift-tools-version:5.2

import PackageDescription

let package = Package(
name: "VaporSecurityHeaders",
platforms: [
.macOS(.v10_15)
],
products: [
.library(name: "VaporSecurityHeaders", targets: ["VaporSecurityHeaders"]),
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),
.package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
],
targets: [
.target(name: "VaporSecurityHeaders", dependencies: ["Vapor"]),
.target(name: "VaporSecurityHeaders", dependencies: [
.product(name: "Vapor", package: "vapor")
]),
.testTarget(name: "VaporSecurityHeadersTests", dependencies: ["VaporSecurityHeaders"]),
]
)
71 changes: 34 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<br>
<br>
<a href="https://swift.org">
<img src="http://img.shields.io/badge/Swift-4.1-brightgreen.svg" alt="Language">
<img src="http://img.shields.io/badge/Swift-5.2-brightgreen.svg" alt="Language">
</a>
<a href="https://github.com/brokenhandsio/VaporSecurityHeaders/actions">
<img src="https://github.com/brokenhandsio/VaporSecurityHeaders/workflows/CI/badge.svg?branch=master" alt="Build Status">
Expand Down Expand Up @@ -34,16 +34,37 @@ These headers will *help* prevent cross-site scripting attacks, SSL downgrade at

# Usage

To use Vapor Security Headers, just register the middleware with your services and add it to your `MiddlewareConfig`. Vapor Security Headers makes this easy to do with a `build` function on the factory. In `configure.swift` add:
## Add the package

Add the package as a dependency in your `Package.swift` manifest:

```swift
dependencies: [
...,
.package(url: "https://github.com/brokenhandsio/VaporSecurityHeaders.git", from: "3.0.0")
]
```

Then add the dependency to your target:

```swift
.target(name: "App",
dependencies: [
// ...
"VaporSecurityHeaders"]),
```

## Configuration

To use Vapor Security Headers, you need to add the middleware to your `Application`'s `Middlewares`. Vapor Security Headers makes this easy to do with a `build` function on the factory. **Note:** if you want security headers added to error reponses (recommended), you need to initialise the `Middlewares` from fresh and add the middleware in _after_ the `SecuriyHeaders`. In `configure.swift` add:

```swift
let securityHeadersFactory = SecurityHeadersFactory()
services.register(securityHeadersFactory.build())

var middlewareConfig = MiddlewareConfig()
// ...
middlewareConfig.use(SecurityHeaders.self)
services.register(middlewareConfig)
application.middleware = Middlewares()
application.middleware.use(securityHeadersFactory.build())
application.middleware.use(ErrorMiddleware.default(environment: application.environment))
// Add other middlewares...
```

The default factory will add default values to your site for Content-Security-Policy, X-XSS-Protection, X-Frame-Options and X-Content-Type-Options.
Expand All @@ -55,7 +76,7 @@ x-frame-options: DENY
x-xss-protection: 1; mode=block
```

***Note:*** You should ensure you set the security headers as the last middleware in your `MiddlewareConfig` (i.e., the first middleware to be applied to responses) to make sure the headers get added to all responses.
***Note:*** You should ensure you set the security headers as the first middleware in your `Middlewares` (i.e., the first middleware to be applied to responses) to make sure the headers get added to all responses.

If you want to add your own values, it is easy to do using the factory. For instance, to add a content security policy configuration, just do:

Expand All @@ -65,7 +86,7 @@ let cspValue = "default-src 'none'; script-src https://static.brokenhands.io;"
let cspConfig = ContentSecurityPolicyConfiguration(value: cspValue)

let securityHeadersFactory = SecurityHeadersFactory().with(contentSecurityPolicy: cspConfig)
services.register(securityHeadersFactory.build())
application.middleware.use(securityHeadersFactory.build())
```

```HTTP
Expand All @@ -75,15 +96,6 @@ x-frame-options: DENY
x-xss-protection: 1; mode=block
```

You will need to add it as a dependency in your `Package.swift` file:

```swift
dependencies: [
...,
.package(url: "https://github.com/brokenhandsio/VaporSecurityHeaders.git", from: "2.0.0")
]
```

Each different header has its own configuration and options, details of which can be found below.

You can test your site by visiting the awesome [Security Headers](https://securityheaders.io) (no affiliation) website.
Expand All @@ -94,6 +106,7 @@ If you are running an API you can choose a default configuration for that by cre

```swift
let securityHeaders = SecurityHeadersFactory.api()
application.middleware.use(securityHeaders)
```

```http
Expand All @@ -103,19 +116,11 @@ x-frame-options: DENY
x-xss-protection: 1; mode=block
```

## Manual Initialization

You can also build the middleware manually like so:

```swift
let securityHeadersMiddleware = SecurityHeadersFactory().build()
```

# Server Configuration

## Vapor

If you are running Vapor on it's own (i.e. not as a CGI application or behind and reverse proxy) then you do not need to do anything more to get it running!
If you are running Vapor on it's own (i.e. not as a CGI application or behind a reverse proxy) then you do not need to do anything more to get it running!

## Nginx, Apache and 3rd Party Services

Expand Down Expand Up @@ -276,7 +281,7 @@ Check out [https://report-uri.io/](https://report-uri.io/) for a free tool to se

### Page Specific CSP

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:
Vapor Security Headers also supports setting the CSP on a route or request basis. If the middleware has been added to the `Middlewares`, 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 cspConfig = ContentSecurityPolicy()
Expand All @@ -291,14 +296,6 @@ req.contentSecurityPolicy = pageSpecificCSP
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
services.register { _ in
return CSPRequestConfiguration()
}
```

## Content-Security-Policy-Report-Only

Content-Security-Policy-Report-Only works in exactly the same way as Content-Security-Policy except that any violations will not block content, but they will be reported back to you. This is extremely useful for testing a CSP before rolling it out over your site. You can run both side by side - so for example have a fairly simply policy under Content-Security-Policy but test a more restrictive policy over Content-Security-Policy-Report-Only. The great thing about this is that your users do all your testing for you!
Expand Down Expand Up @@ -443,7 +440,7 @@ strict-transport-security: max-age=31536000; includeSubDomains; preload

The Server header is usually hidden from responses in order to not give away what type of server you are running and what version you are using. This is to stop attackers from scanning your site and using known vulnerabilities against it easily. By default Vapor does not show the server header in responses for this reason.

However, it can be fun to add in a custom server configuration for a bit of personalization, such as your website name, or company name (look at Github's response) and the `ServerConfiguraiton` is to allow this. So, for example, if I wanted my `Server` header to be `brokenhands.io`, I would configure it like:
However, it can be fun to add in a custom server configuration for a bit of personalization, such as your website name, or company name (look at Github's response) and the `ServerConfiguraiton` allows this. So, for example, if I wanted my `Server` header to be `brokenhands.io`, I would configure it like:

```swift
let serverConfig = ServerConfiguration(value: "brokenhands.io")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,25 @@ public struct ContentSecurityPolicyConfiguration: SecurityHeaderConfiguration {

func setHeader(on response: Response, from request: Request) {
if let requestCSP = request.contentSecurityPolicy {
response.http.headers.replaceOrAdd(name: .contentSecurityPolicy, value: requestCSP.value)
response.headers.replaceOrAdd(name: .contentSecurityPolicy, value: requestCSP.value)
} else {
response.http.headers.replaceOrAdd(name: .contentSecurityPolicy, value: value)
response.headers.replaceOrAdd(name: .contentSecurityPolicy, value: value)
}
}
}

public class CSPRequestConfiguration: Service {
var configuration: ContentSecurityPolicyConfiguration?
public init() {}
extension ContentSecurityPolicyConfiguration: StorageKey {
public typealias Value = Self
}

extension Request {

public var contentSecurityPolicy: ContentSecurityPolicyConfiguration? {
get {
if let requestConfig = try? privateContainer.make(CSPRequestConfiguration.self) {
return requestConfig.configuration
} else {
return nil
}
return self.storage[ContentSecurityPolicyConfiguration.self]
}
set {
if let requestConfig = try? privateContainer.make(CSPRequestConfiguration.self) {
requestConfig.configuration = newValue
}
self.storage[ContentSecurityPolicyConfiguration.self] = newValue
}
}
}
Expand Down Expand Up @@ -98,81 +92,103 @@ public class ContentSecurityPolicy {
return policy.joined(separator: "; ")
}

@discardableResult
public func set(value: String) -> ContentSecurityPolicy {
policy.append(value)
return self
}


@discardableResult
public func baseUri(sources: String...) -> ContentSecurityPolicy {
policy.append("base-uri \(sources.joined(separator: " "))")
return self
}

@discardableResult
public func blockAllMixedContent() -> ContentSecurityPolicy {
policy.append("block-all-mixed-content")
return self
}

@discardableResult
public func childSrc(sources: String...) -> ContentSecurityPolicy {
policy.append("child-src \(sources.joined(separator: " "))")
return self
}

@discardableResult
public func connectSrc(sources: String...) -> ContentSecurityPolicy {
policy.append("connect-src \(sources.joined(separator: " "))")
return self
}

@discardableResult
public func defaultSrc(sources: String...) -> ContentSecurityPolicy {
policy.append("default-src \(sources.joined(separator: " "))")
return self
}

@discardableResult
public func fontSrc(sources: String...) -> ContentSecurityPolicy {
policy.append("font-src \(sources.joined(separator: " "))")
return self
}

@discardableResult
public func formAction(sources: String...) -> ContentSecurityPolicy {
policy.append("form-action \(sources.joined(separator: " "))")
return self
}

@discardableResult
public func frameAncestors(sources: String...) -> ContentSecurityPolicy {
policy.append("frame-ancestors \(sources.joined(separator: " "))")
return self
}

@discardableResult
public func frameSrc(sources: String...) -> ContentSecurityPolicy {
policy.append("frame-src \(sources.joined(separator: " "))")
return self
}

@discardableResult
public func imgSrc(sources: String...) -> ContentSecurityPolicy {
policy.append("img-src \(sources.joined(separator: " "))")
return self
}

@discardableResult
public func manifestSrc(sources: String...) -> ContentSecurityPolicy {
policy.append("manifest-src \(sources.joined(separator: " "))")
return self
}

@discardableResult
public func mediaSrc(sources: String...) -> ContentSecurityPolicy {
policy.append("media-src \(sources.joined(separator: " "))")
return self
}

@discardableResult
public func objectSrc(sources: String...) -> ContentSecurityPolicy {
policy.append("object-src \(sources.joined(separator: " "))")
return self
}

@discardableResult
public func pluginTypes(types: String...) -> ContentSecurityPolicy {
policy.append("plugin-types \(types.joined(separator: " "))")
return self
}

@discardableResult
public func requireSriFor(values: String...) -> ContentSecurityPolicy {
policy.append("require-sri-for \(values.joined(separator: " "))")
return self
}

@discardableResult
public func reportTo(reportToObject: CSPReportTo) -> ContentSecurityPolicy {
let encoder = JSONEncoder()
guard let data = try? encoder.encode(reportToObject) else { return self }
Expand All @@ -181,31 +197,37 @@ public class ContentSecurityPolicy {
return self
}

@discardableResult
public func reportUri(uri: String) -> ContentSecurityPolicy {
policy.append("report-uri \(uri)")
return self
}

@discardableResult
public func sandbox(values: String...) -> ContentSecurityPolicy {
policy.append("sandbox \(values.joined(separator: " "))")
return self
}

@discardableResult
public func scriptSrc(sources: String...) -> ContentSecurityPolicy {
policy.append("script-src \(sources.joined(separator: " "))")
return self
}

@discardableResult
public func styleSrc(sources: String...) -> ContentSecurityPolicy {
policy.append("style-src \(sources.joined(separator: " "))")
return self
}

@discardableResult
public func upgradeInsecureRequests() -> ContentSecurityPolicy {
policy.append("upgrade-insecure-requests")
return self
}

@discardableResult
public func workerSrc(sources: String...) -> ContentSecurityPolicy {
policy.append("worker-src \(sources.joined(separator: " "))")
return self
Expand Down

0 comments on commit 8796636

Please sign in to comment.