├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── VaporSecurityHeaders
│ ├── Configurations
│ ├── ContentSecurityPolicyConfiguration.swift
│ ├── ContentSecurityPolicyReportOnlyConfiguration.swift
│ ├── ContentTypeOptionsConfiguration.swift
│ ├── FrameOptionsConfiguration.swift
│ ├── HTTPSRedirectMiddleware.swift
│ ├── ReferrerPolicyConfiguration.swift
│ ├── ServerConfiguration.swift
│ ├── StrictTransportSecurityConfiguration.swift
│ └── XSSProtectionConfiguration.swift
│ ├── SecurityHeaderConfiguration.swift
│ ├── SecurityHeaders+HeaderKey.swift
│ ├── SecurityHeaders.swift
│ └── SecurityHeadersFactory.swift
├── Tests
└── VaporSecurityHeadersTests
│ ├── Fakes
│ └── StubFileMiddleware.swift
│ ├── HeaderTests.swift
│ └── RedirectionTest.swift
└── codecov.yml
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | pull_request:
5 | jobs:
6 | test:
7 | container:
8 | image: swift:5.8
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v3
12 | - name: Run Tests
13 | run: swift test --enable-code-coverage --sanitize=thread
14 | - name: Setup container for codecov upload
15 | run: apt-get update && apt-get install curl -y
16 | - name: Process coverage file
17 | run: llvm-cov show .build/x86_64-unknown-linux-gnu/debug/VaporSecurityHeadersPackageTests.xctest -instr-profile=.build/debug/codecov/default.profdata > coverage.txt
18 | - name: Upload code coverage
19 | uses: codecov/codecov-action@v1
20 | with:
21 | token: ${{ secrets.CODECOV_UPLOAD_KEY }}
22 | file: coverage.txt
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | DerivedData/
6 | Package.pins
7 | Package.resolved
8 | .swiftpm/
9 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at support@brokenhands.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to a Broken Hands project
2 |
3 | :+1::tada: Thank you for wanting to contribute to this project! :tada::+1:
4 |
5 | We ask that you follow a few guidelines when contributing to one of our projects.
6 |
7 | ## Code of Conduct
8 |
9 | This project and everyone participating in it is governed by the [Broken Hands Code of Conduct](https://github.com/brokenhandsio/SteamPress/blob/master/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [support@brokenhands.io](mailto:support@brokenhandsio).
10 |
11 | # How Can I Contribute?
12 |
13 | ### Reporting Bugs
14 |
15 | This section guides you through submitting a bug report for a Broken Hands project. Following these guidelines helps maintainers and the community understand your report :pencil:, reproduce the behavior :computer: :computer:, and find related reports :mag_right:.
16 |
17 | Before creating bug reports, please check [this list](#before-submitting-a-bug-report) as you might find out that you don't need to create one. When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report).
18 |
19 | > **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one.
20 |
21 | #### Before Submitting A Bug Report
22 |
23 | * **Perform a [cursory search](https://github.com/issues?q=+is%3Aissue+user%3Abrokenhandsio)** to see if the problem has already been reported. If it has **and the issue is still open**, add a comment to the existing issue instead of opening a new one.
24 |
25 | #### How Do I Submit A (Good) Bug Report?
26 |
27 | Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/). Create an issue on the repository and provide the following information by filling in the issue form.
28 |
29 | Explain the problem and include additional details to help maintainers reproduce the problem:
30 |
31 | * **Use a clear and descriptive title** for the issue to identify the problem.
32 | * **Describe the exact steps which reproduce the problem** in as many details as possible. This usually means including some code, as well as __full__ error messages if applicable.
33 | * **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).
34 | * **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior.
35 | * **Explain which behavior you expected to see instead and why.**
36 | * **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below.
37 |
38 | ### Suggesting Enhancements
39 |
40 | This section guides you through submitting an enhancement suggestion for a Broken Hands project, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion :pencil: and find related suggestions :mag_right:.
41 |
42 | Before creating enhancement suggestions, please check [this list](#before-submitting-an-enhancement-suggestion) as you might find out that you don't need to create one. When you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-a-good-enhancement-suggestion). Fill in issue form, including the steps that you imagine you would take if the feature you're requesting existed.
43 |
44 | #### Before Submitting An Enhancement Suggestion
45 |
46 | * **Perform a [cursory search](https://github.com/issues?q=+is%3Aissue+user%3Abrokenhandsio)** to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
47 |
48 | #### How Do I Submit A (Good) Enhancement Suggestion?
49 |
50 | Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). Create an issue and provide the following information:
51 |
52 | * **Use a clear and descriptive title** for the issue to identify the suggestion.
53 | * **Provide a step-by-step description of the suggested enhancement** in as many details as possible.
54 | * **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).
55 | * **Describe the current behavior** and **explain which behavior you expected to see instead** and why.
56 | * **Explain why this enhancement would be useful** to other users and isn't something that can or should be implemented as a separate package.
57 |
58 | ### Pull Requests
59 |
60 | * Do not include issue numbers in the PR title
61 | * End all files with a newline
62 | * All new code should be run through `swiftlint`
63 | * All code must run on both Linux and macOS
64 | * All new code must be covered by tests
65 | * All bug fixes must be accompanied by a test which would fail if the bug fix was not implemented
66 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Broken Hands
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.2
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "VaporSecurityHeaders",
7 | platforms: [
8 | .macOS(.v10_15)
9 | ],
10 | products: [
11 | .library(name: "VaporSecurityHeaders", targets: ["VaporSecurityHeaders"]),
12 | ],
13 | dependencies: [
14 | .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
15 | ],
16 | targets: [
17 | .target(name: "VaporSecurityHeaders", dependencies: [
18 | .product(name: "Vapor", package: "vapor")
19 | ]),
20 | .testTarget(name: "VaporSecurityHeadersTests", dependencies: ["VaporSecurityHeaders"]),
21 | ]
22 | )
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | A Middleware library for adding security headers to your Vapor application.
19 |
20 | # Features
21 |
22 | Easily add headers to all your responses for improving the security of your site for you and your users. Currently supports:
23 |
24 | * Content-Security-Policy
25 | * Content-Security-Policy-Report-Only
26 | * X-XSS-Protection
27 | * X-Frame-Options
28 | * X-Content-Type-Options
29 | * Strict-Transport-Security (HSTS)
30 | * Redirect HTTP to HTTPS
31 | * Server
32 | * Referrer Policy
33 |
34 | These headers will *help* prevent cross-site scripting attacks, SSL downgrade attacks, content injection attacks, click-jacking etc. They will not help for any attacks directly against your server, but they will help your users and help secure sensitive information (CSRF tokens). Please note that this library does not guarantee anything and nothing is ever completely secure.
35 |
36 | # Usage
37 |
38 | ## Add the package
39 |
40 | Add the package as a dependency in your `Package.swift` manifest:
41 |
42 | ```swift
43 | dependencies: [
44 | ...,
45 | .package(url: "https://github.com/brokenhandsio/VaporSecurityHeaders.git", from: "3.0.0")
46 | ]
47 | ```
48 |
49 | Then add the dependency to your target:
50 |
51 | ```swift
52 | .target(name: "App",
53 | dependencies: [
54 | // ...
55 | "VaporSecurityHeaders"]),
56 | ```
57 |
58 | ## Configuration
59 |
60 | 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:
61 |
62 | ```swift
63 | let securityHeadersFactory = SecurityHeadersFactory()
64 |
65 | application.middleware = Middlewares()
66 | application.middleware.use(securityHeadersFactory.build())
67 | application.middleware.use(ErrorMiddleware.default(environment: application.environment))
68 | // Add other middlewares...
69 | ```
70 |
71 | 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.
72 |
73 | ```HTTP
74 | x-content-type-options: nosniff
75 | content-security-policy: default-src 'self'
76 | x-frame-options: DENY
77 | x-xss-protection: 0
78 | ```
79 |
80 | ***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.
81 |
82 | 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:
83 |
84 | ```swift
85 | let cspValue = "default-src 'none'; script-src https://static.brokenhands.io;"
86 |
87 | let cspConfig = ContentSecurityPolicyConfiguration(value: cspValue)
88 |
89 | let securityHeadersFactory = SecurityHeadersFactory().with(contentSecurityPolicy: cspConfig)
90 | application.middleware.use(securityHeadersFactory.build())
91 | ```
92 |
93 | ```HTTP
94 | x-content-type-options: nosniff
95 | content-security-policy: default-src 'none'; script-src https://static.brokenhands.io;
96 | x-frame-options: DENY
97 | x-xss-protection: 0
98 | ```
99 |
100 | Each different header has its own configuration and options, details of which can be found below.
101 |
102 | You can test your site by visiting the awesome [Security Headers](https://securityheaders.io) (no affiliation) website.
103 |
104 | ## API Headers
105 |
106 | If you are running an API you can choose a default configuration for that by creating it with:
107 |
108 | ```swift
109 | let securityHeaders = SecurityHeadersFactory.api()
110 | application.middleware.use(securityHeaders.build())
111 | ```
112 |
113 | ```http
114 | x-content-type-options: nosniff
115 | content-security-policy: default-src 'none'
116 | x-frame-options: DENY
117 | x-xss-protection: 0
118 | ```
119 |
120 | # Server Configuration
121 |
122 | ## Vapor
123 |
124 | 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!
125 |
126 | ## Nginx, Apache and 3rd Party Services
127 |
128 | Both web servers should pass on the response headers from Vapor without issue when running as a reverse proxy. Some servers and providers (such as Heroku) will inject their own headers or block certain headers (such as HSTS to stop you locking out their whole site). You will need to check with your provider to see what is enabled and allowed.
129 |
130 | # Security Header Information
131 |
132 | ## Content-Security-Policy
133 |
134 | Content Security Policy is one of the most effective tools for protecting against cross-site scripting attacks. In essence it is a way of whitelisting sources for content so that you only load from known and trusted sources. For more information about CSP, read Scott Helme's [awesome blog post](https://scotthelme.co.uk/content-security-policy-an-introduction/) which tells you how to configure it and what to use.
135 |
136 | The Vapor Security Headers package will set a default CSP of `default-src: 'self'`, which means that you can load images, scripts, fonts, CSS etc **only** from your domain. It also means that you cannot have any inline Javascript or CSS, which is one of the most effective measures you can take in protecting your site, and will wipe out a large proportion of content-injection attacks.
137 |
138 | The API default CSP is `default-src: 'none'` as an API should only return data and never be loading scripts or images to display!
139 |
140 | You can build a CSP header (`ContentSecurityPolicy`) with the following directives:
141 |
142 | - baseUri(sources)
143 | - blockAllMixedContent()
144 | - connectSrc(sources)
145 | - defaultSrc(sources)
146 | - fontSrc(sources)
147 | - formAction(sources)
148 | - frameAncestors(sources)
149 | - frameSrc(sources)
150 | - imgSrc(sources)
151 | - manifestSrc(sources)
152 | - mediaSrc(sources)
153 | - objectSrc(sources)
154 | - pluginTypes(types)
155 | - reportTo(json_object)
156 | - reportUri(uri)
157 | - requireSriFor(values)
158 | - sandbox(values)
159 | - scriptSrc(sources)
160 | - styleSrc(sources)
161 | - upgradeInsecureRequests()
162 | - workerSrc(sources)
163 |
164 | *Example:*
165 |
166 | ```swift
167 | let cspConfig = ContentSecurityPolicy()
168 | .scriptSrc(sources: "https://static.brokenhands.io")
169 | .styleSrc(sources: "https://static.brokenhands.io")
170 | .imgSrc(sources: "https://static.brokenhands.io")
171 | ```
172 |
173 | ```http
174 | Content-Security-Policy: script-src https://static.brokenhands.io; style-src https://static.brokenhands.io; img-src https://static.brokenhands.io
175 | ```
176 |
177 | You can set a custom header with ContentSecurityPolicy().set(value) or ContentSecurityPolicyConfiguration(value).
178 |
179 | **ContentSecurityPolicy().set(value)**
180 |
181 | ```swift
182 | let cspBuilder = ContentSecurityPolicy().set(value: "default-src: 'none'")
183 |
184 | let cspConfig = ContentSecurityPolicyConfiguration(value: cspBuilder)
185 |
186 | let securityHeadersFactory = SecurityHeadersFactory().with(contentSecurityPolicy: cspConfig)
187 | ```
188 |
189 | **ContentSecurityPolicyConfiguration(value)**
190 |
191 | ```swift
192 | let cspConfig = ContentSecurityPolicyConfiguration(value: "default-src 'none'")
193 |
194 | let securityHeadersFactory = SecurityHeadersFactory().with(contentSecurityPolicy: cspConfig)
195 | ```
196 |
197 | ```http
198 | Content-Security-Policy: default-src: 'none'
199 | ```
200 |
201 | The following CSP keywords (`CSPKeywords`) are also available to you:
202 |
203 | * CSPKeywords.all = *
204 | * CSPKeywords.none = 'none'
205 | * CSPKeywords.\`self\` = 'self'
206 | * CSPKeywords.strictDynamic = 'strict-dynamic'
207 | * CSPKeywords.unsafeEval = 'unsafe-eval'
208 | * CSPKeywords.unsafeHashedAttributes = 'unsafe-hashed-attributes'
209 | * CSPKeywords.unsafeInline = 'unsafe-inline'
210 |
211 | *Example:*
212 |
213 | ``` swift
214 | CSPKeywords.`self` // “‘self’”
215 | ContentSecurityPolicy().defaultSrc(sources: CSPKeywords.`self`)
216 | ```
217 |
218 | ```http
219 | Content-Security-Policy: default-src 'self'
220 | ```
221 |
222 | You can also utilize the `Report-To` directive:
223 |
224 | ```swift
225 | let reportToEndpoint = CSPReportToEndpoint(url: "https://csp-report.brokenhands.io/csp-reports")
226 |
227 | let reportToValue = CSPReportTo(group: "vapor-csp", max_age: 10886400, endpoints: [reportToEndpoint], include_subdomains: true)
228 |
229 | let cspValue = ContentSecurityPolicy()
230 | .defaultSrc(sources: CSPKeywords.none)
231 | .scriptSrc(sources: "https://static.brokenhands.io")
232 | .reportTo(reportToObject: reportToValue)
233 | ```
234 |
235 | ```http
236 | 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}
237 | ```
238 |
239 | See [Google Developers - The Reporting API](https://developers.google.com/web/updates/2018/09/reportingapi) for more information on the Report-To directive.
240 |
241 | #### Content Security Policy Configuration
242 |
243 | To configure your CSP you can add it to your `ContentSecurityPolicyConfiguration` like so:
244 |
245 | ```swift
246 | let cspBuilder = ContentSecurityPolicy()
247 | .defaultSrc(sources: CSPKeywords.none)
248 | .scriptSrc(sources: "https://static.brokenhands.io")
249 | .styleSrc(sources: "https://static.brokenhands.io")
250 | .imgSrc(sources: "https://static.brokenhands.io")
251 | .fontSrc(sources: "https://static.brokenhands.io")
252 | .connectSrc(sources: "https://*.brokenhands.io")
253 | .formAction(sources: CSPKeywords.`self`)
254 | .upgradeInsecureRequests()
255 | .blockAllMixedContent()
256 | .requireSriFor(values: "script", "style")
257 | .reportUri(uri: "https://csp-report.brokenhands.io")
258 |
259 | let cspConfig = ContentSecurityPolicyConfiguration(value: cspBuilder)
260 |
261 | let securityHeadersFactory = SecurityHeadersFactory().with(contentSecurityPolicy: cspConfig)
262 | ```
263 |
264 | ```http
265 | 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
266 | ```
267 |
268 | This policy means that by default everything is blocked, however:
269 |
270 | * Scripts can be loaded from `https://static.brokenhands.io`
271 | * CSS can be loaded from `https://static.brokenhands.io`
272 | * Images can be loaded from `https://static.brokenhands.io`
273 | * Fonts can be loaded from `https://static.brokenhands.io`
274 | * Any JS connections can only be made to any `brokenhands.io` subdomain over HTTPS
275 | * Form actions go only go to the same site
276 | * Any HTTP requests will be sent over HTTPS
277 | * Any attempts to load HTTP content will be blocked
278 | * Any scripts and style links must have [SRI](https://scotthelme.co.uk/subresource-integrity/) values
279 | * Any policy violations will be sent to `https://csp-report.brokenhands.io`
280 |
281 | Check out [https://report-uri.io/](https://report-uri.io/) for a free tool to send all of your CSP reports to.
282 |
283 | ### Page Specific CSP
284 |
285 | 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:
286 |
287 | ```swift
288 | let cspConfig = ContentSecurityPolicy()
289 | .defaultSrc(sources: CSPKeywords.none)
290 | .scriptSrc(sources: "https://comments.disqus.com")
291 |
292 | let pageSpecificCSP = ContentSecurityPolicyConfiguration(value: cspConfig)
293 | req.contentSecurityPolicy = pageSpecificCSP
294 | ```
295 |
296 | ```http
297 | content-security-policy: default-src 'none'; script-src https://comments.disqus.com
298 | ```
299 |
300 | ## Content-Security-Policy-Report-Only
301 |
302 | 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!
303 |
304 | To configure this, just pass in your policy to the `ContentSecurityPolicyReportOnlyConfiguration`:
305 |
306 | ```swift
307 | let cspConfig = ContentSecurityPolicyReportOnlyConfiguration(value: "default-src https:; report-uri https://csp-report.brokenhands.io")
308 |
309 | let securityHeadersFactory = SecurityHeadersFactory().with(contentSecurityPolicyReportOnly: cspConfig)
310 | ```
311 |
312 | ```http
313 | content-security-policy-report-only: default-src https:; report-uri https://csp-report.brokenhands.io
314 | ```
315 |
316 | The [above blog post](https://scotthelme.co.uk/content-security-policy-an-introduction/) goes into more details about this.
317 |
318 | ## X-XSS-Protection
319 |
320 | X-XSS-Protection configures the browser's cross-site scripting filter. This package configures the header to be disabled, which (surprisingly) offers security benefits. See [this article on MDN for more information](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection).
321 |
322 | ```swift
323 | let xssProtectionConfig = XSSProtectionConfiguration()
324 |
325 | let securityHeadersFactory = SecurityHeadersFactory().with(XSSProtection: xssProtectionConfig)
326 | ```
327 |
328 | ```http
329 | x-xss-protection: 0
330 | ```
331 |
332 | ## X-Content-Type-Options
333 |
334 | X-Content-Type-Options stops a browser from trying to MIME-sniff content types from requests and makes sure that the declared content type is used. It only has one option, which is `nosniff`. To use this, set your `ContentTypeOptionsConfiguration` as so (this is set by default on any `SecurityHeaders` object):
335 |
336 | ```swift
337 | let contentTypeConfig = ContentTypeOptionsConfiguration(option: .nosniff)
338 |
339 | let securityHeadersFactory = SecurityHeadersFactory().with(contentTypeOptions: contentTypeConfig)
340 | ```
341 |
342 | ```http
343 | x-content-type-options: nosniff
344 | ```
345 |
346 | To disable it:
347 |
348 | ```swift
349 | let contentTypeConfig = ContentTypeOptionsConfiguration(option: .none)
350 | ```
351 |
352 | ## X-Frame-Options
353 |
354 | The X-Frame-Options header is for click-jacking attacks and tells the browser whether your site can be framed. To stop your site from being framed completely (the default setting):
355 |
356 | ```swift
357 | let frameOptionsConfig = FrameOptionsConfiguration(option: .deny)
358 |
359 | let securityHeadersFactory = SecurityHeadersFactory().with(frameOptions: frameOptionsConfig)
360 | ```
361 |
362 | ```http
363 | x-frame-options: DENY
364 | ```
365 |
366 | To allow you to frame your own site:
367 |
368 | ```swift
369 | let frameOptionsConfig = FrameOptionsConfiguration(option: .sameOrigin)
370 | ```
371 |
372 | ```http
373 | x-frame-options: SAMEORIGIN
374 | ```
375 |
376 | To allow a specific site to frame yours, use:
377 |
378 | ```swift
379 | let frameOptionsConfig = FrameOptionsConfiguration(option: .allow(from: "https://mytrustedsite.com"))
380 | ```
381 |
382 | ```http
383 | x-frame-options: ALLOW-FROM https://mytrustedsite.com
384 | ```
385 |
386 | ## Strict-Transport-Security
387 |
388 | Strict-Transport-Security is an improvement over 301/302 redirects or HTTPS forwarding. Browsers will default to HTTP when you navigate to an address but HSTS (HTTP Strict Transport Security) tells the browser that it should always connect over HTTPS, so all future requests will be HTTPS, even if you click on an HTTP link. By default this is not turned on with the Security Headers library as it can cause issues if you haven't got HTTPS set up properly. If you specify this header and then at a future date you don't renew your SSL certificate or disable SSL then the browser will refuse to load your site! However, it is highly recommended as it ensures that all connections are over HTTPS, even if a user clicks on an HTTP link.
389 |
390 | The default configuration is `max-age=31536000; includeSubDomains; preload`. This tells the browser to force HTTPS for a year, and for *every* subdomain as well. So if you specify this, make sure you have SSL properly configured for all subdomains, e.g. `test.mysite.com`, `dev.mysite.com` etc.
391 |
392 | The `preload` tag tells Chrome that you want to be preloaded. This will add you to the preload list, which means that the browser will automatically know you want an HTTPS connection before you have even visited the site, so removes the initial HTTP handshake the first time you specify the header. However, this has now been superseded and you should now submit your site at [https://hstspreload.org](https://hstspreload.org). This will add your site to Chrome's source to preload it in the future and it is the list that other browsers use as well. Note that it is difficult to remove yourself from the list (and can take months to get it rolled out to the browsers), so by submitting your site you are effectively guaranteeing working HTTPS for the rest of the life of your site. However, these days it shouldn't be a problem - use [Let's Encrypt](https://letsencrypt.org)! **Note**: You should be careful about using this on deployment sites such as Heroku as it may cause issues.
393 |
394 | To use the Strict-Transport-Security header, you can configure and add it as so (default values are shown):
395 |
396 | ```swift
397 | let strictTransportSecurityConfig = StrictTransportSecurityConfiguration(maxAge: 31536000, includeSubdomains: true, preload: true)
398 |
399 | let securityHeadersFactory = SecurityHeadersFactory().with(strictTransportSecurity: strictTransportSecurityConfig)
400 | ```
401 |
402 | ```http
403 | strict-transport-security: max-age=31536000; includeSubDomains; preload
404 | ```
405 |
406 | ## Redirect HTTP to HTTPS
407 |
408 | If Strict-Transport-Security is not enough to accomplish a forwarding connection to HTTPS from the browsers, you can opt to add an additional middleware who provides this redirection if clients try to reach your site with an HTTP connection.
409 |
410 | To use the HTTPS Redirect Middleware, you can add the following line in **configure.swift** to enable the middleware. This must be done before `securityHeadersFactory.build()` to ensure HSTS works:
411 |
412 | ```swift
413 | app.middleware.use(HTTPSRedirectMiddleware())
414 | ```
415 |
416 | The `HTTPSRedirectMiddleware` allows you to set an array of allowed hosts that the application can redirect to. This prevents attackers poisoning the `Host` header and forcing a redirect to a domain under their control. To use this, provide the list of allowed hosts to the initialiser:
417 |
418 | ```swift
419 | app.middleware.use(HTTPSRedirectMiddleware(allowedHosts: ["www.brokenhands.io", "brokenhands.io", "static.brokenhands.io"))
420 | ```
421 |
422 | Any attempts to redirect to another host, for example `attacker.com` will result in a **400 Bad Request** response.
423 |
424 | ## Server
425 |
426 | 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.
427 |
428 | 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:
429 |
430 | ```swift
431 | let serverConfig = ServerConfiguration(value: "brokenhands.io")
432 |
433 | let securityHeadersFactory = SecurityHeadersFactory().with(server: serverConfig)
434 | ```
435 |
436 | ```http
437 | server: brokenhands.io
438 | ```
439 |
440 | ## Referrer Policy
441 |
442 | The Referrer Policy is the latest header to have been introduced (the spec can be found [here](https://www.w3.org/TR/referrer-policy/)). It basically defines when the `Referrer` header can be sent with a request. You may want to not send the header when going from HTTPS to HTTP for example.
443 |
444 | The different options are:
445 |
446 | * ""
447 | * "no-referrer"
448 | * "no-referrer-when-downgrade"
449 | * "same-origin"
450 | * "origin"
451 | * "strict-origin"
452 | * "origin-when-cross-origin"
453 | * "strict-origin-when-cross-origin"
454 | * "unsafe-url"
455 |
456 | I won't go into details about each one, I will point you in the direction of a far better explanation [by Scott Helme](https://scotthelme.co.uk/a-new-security-header-referrer-policy/).
457 |
458 | ```swift
459 | let referrerPolicyConfig = ReferrerPolicyConfiguration(.noReferrer)
460 |
461 | let securityHeadersFactory = SecurityHeadersFactory().with(referrerPolicy: referrerPolicyConfig)
462 | ```
463 |
464 | ```http
465 | referrer-policy: no-referrer
466 | ```
467 |
468 | You can also [set a fallback policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#specify_a_fallback_policy).
469 |
470 | ```swift
471 | let referrerPolicyConfig = ReferrerPolicyConfiguration([.noReferrer, .strictOriginWhenCrossOrigin])
472 |
473 | let securityHeadersFactory = SecurityHeadersFactory().with(referrerPolicy: referrerPolicyConfig)
474 | ```
475 |
476 | ```http
477 | referrer-policy: no-referrer, strict-origin-when-cross-origin
478 | ```
479 |
--------------------------------------------------------------------------------
/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyConfiguration.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 | import Foundation
3 |
4 | public struct ContentSecurityPolicyConfiguration: SecurityHeaderConfiguration {
5 | private let value: String
6 |
7 | public init(value: String) {
8 | self.value = value
9 | }
10 |
11 | public init(value: ContentSecurityPolicy) {
12 | self.value = value.value
13 | }
14 |
15 | func setHeader(on response: Response, from request: Request) {
16 | if let requestCSP = request.contentSecurityPolicy {
17 | response.headers.replaceOrAdd(name: .contentSecurityPolicy, value: requestCSP.value)
18 | } else {
19 | response.headers.replaceOrAdd(name: .contentSecurityPolicy, value: value)
20 | }
21 | }
22 | }
23 |
24 | extension ContentSecurityPolicyConfiguration: StorageKey {
25 | public typealias Value = Self
26 | }
27 |
28 | extension Request {
29 |
30 | public var contentSecurityPolicy: ContentSecurityPolicyConfiguration? {
31 | get {
32 | return self.storage[ContentSecurityPolicyConfiguration.self]
33 | }
34 | set {
35 | self.storage[ContentSecurityPolicyConfiguration.self] = newValue
36 | }
37 | }
38 | }
39 |
40 | public struct CSPReportTo: Codable {
41 | private let group: String?
42 | private let max_age: Int
43 | private let endpoints: [CSPReportToEndpoint]
44 | private let include_subdomains: Bool?
45 |
46 | public init(group: String? = nil, max_age: Int,
47 | endpoints: [CSPReportToEndpoint], include_subdomains: Bool? = nil) {
48 | self.group = group
49 | self.max_age = max_age
50 | self.endpoints = endpoints
51 | self.include_subdomains = include_subdomains
52 | }
53 | }
54 |
55 | public struct CSPReportToEndpoint: Codable {
56 | private let url: String
57 |
58 | public init(url: String) {
59 | self.url = url
60 | }
61 | }
62 |
63 | extension CSPReportToEndpoint: Equatable {
64 | public static func == (lhs: CSPReportToEndpoint, rhs: CSPReportToEndpoint) -> Bool {
65 | return lhs.url == rhs.url
66 | }
67 | }
68 |
69 | extension CSPReportTo: Equatable {
70 | public static func == (lhs: CSPReportTo, rhs: CSPReportTo) -> Bool {
71 | return lhs.group == rhs.group &&
72 | lhs.max_age == rhs.max_age &&
73 | lhs.endpoints == rhs.endpoints &&
74 | lhs.include_subdomains == rhs.include_subdomains
75 | }
76 | }
77 |
78 | public struct CSPKeywords {
79 | public static let all = "*"
80 | public static let none = "'none'"
81 | public static let `self` = "'self'"
82 | public static let strictDynamic = "'strict-dynamic'"
83 | public static let unsafeEval = "'unsafe-eval'"
84 | public static let unsafeHashedAttributes = "'unsafe-hashed-attributes'"
85 | public static let unsafeInline = "'unsafe-inline'"
86 | }
87 |
88 | public class ContentSecurityPolicy {
89 | private var policy: [String] = []
90 |
91 | var value: String {
92 | return policy.joined(separator: "; ")
93 | }
94 |
95 | @discardableResult
96 | public func set(value: String) -> ContentSecurityPolicy {
97 | policy.append(value)
98 | return self
99 | }
100 |
101 | @discardableResult
102 | public func baseUri(sources: String...) -> ContentSecurityPolicy {
103 | join(sources: sources, to: "base-uri")
104 | return self
105 | }
106 |
107 | @discardableResult
108 | public func baseUri(sources: [String]) -> ContentSecurityPolicy {
109 | join(sources: sources, to: "base-uri")
110 | return self
111 | }
112 |
113 | @discardableResult
114 | public func blockAllMixedContent() -> ContentSecurityPolicy {
115 | policy.append("block-all-mixed-content")
116 | return self
117 | }
118 |
119 | @discardableResult
120 | public func childSrc(sources: String...) -> ContentSecurityPolicy {
121 | join(sources: sources, to: "child-src")
122 | return self
123 | }
124 |
125 | @discardableResult
126 | public func childSrc(sources: [String]) -> ContentSecurityPolicy {
127 | join(sources: sources, to: "child-src")
128 | return self
129 | }
130 |
131 | @discardableResult
132 | public func connectSrc(sources: String...) -> ContentSecurityPolicy {
133 | join(sources: sources, to: "connect-src")
134 | return self
135 | }
136 |
137 | @discardableResult
138 | public func connectSrc(sources: [String]) -> ContentSecurityPolicy {
139 | join(sources: sources, to: "connect-src")
140 | return self
141 | }
142 |
143 | @discardableResult
144 | public func defaultSrc(sources: String...) -> ContentSecurityPolicy {
145 | join(sources: sources, to: "default-src")
146 | return self
147 | }
148 |
149 | @discardableResult
150 | public func defaultSrc(sources: [String]) -> ContentSecurityPolicy {
151 | join(sources: sources, to: "default-src")
152 | return self
153 | }
154 |
155 | @discardableResult
156 | public func fontSrc(sources: String...) -> ContentSecurityPolicy {
157 | join(sources: sources, to: "font-src")
158 | return self
159 | }
160 |
161 | @discardableResult
162 | public func fontSrc(sources: [String]) -> ContentSecurityPolicy {
163 | join(sources: sources, to: "font-src")
164 | return self
165 | }
166 |
167 | @discardableResult
168 | public func formAction(sources: String...) -> ContentSecurityPolicy {
169 | join(sources: sources, to: "form-action")
170 | return self
171 | }
172 |
173 | @discardableResult
174 | public func formAction(sources: [String]) -> ContentSecurityPolicy {
175 | join(sources: sources, to: "form-action")
176 | return self
177 | }
178 |
179 | @discardableResult
180 | public func frameAncestors(sources: String...) -> ContentSecurityPolicy {
181 | join(sources: sources, to: "frame-ancestors")
182 | return self
183 | }
184 |
185 | @discardableResult
186 | public func frameAncestors(sources: [String]) -> ContentSecurityPolicy {
187 | join(sources: sources, to: "frame-ancestors")
188 | return self
189 | }
190 |
191 | @discardableResult
192 | public func frameSrc(sources: String...) -> ContentSecurityPolicy {
193 | join(sources: sources, to: "frame-src")
194 | return self
195 | }
196 |
197 | @discardableResult
198 | public func frameSrc(sources: [String]) -> ContentSecurityPolicy {
199 | join(sources: sources, to: "frame-src")
200 | return self
201 | }
202 |
203 | @discardableResult
204 | public func imgSrc(sources: String...) -> ContentSecurityPolicy {
205 | join(sources: sources, to: "img-src")
206 | return self
207 | }
208 |
209 | @discardableResult
210 | public func imgSrc(sources: [String]) -> ContentSecurityPolicy {
211 | join(sources: sources, to: "img-src")
212 | return self
213 | }
214 |
215 | @discardableResult
216 | public func manifestSrc(sources: String...) -> ContentSecurityPolicy {
217 | join(sources: sources, to: "manifest-src")
218 | return self
219 | }
220 |
221 | @discardableResult
222 | public func manifestSrc(sources: [String]) -> ContentSecurityPolicy {
223 | join(sources: sources, to: "manifest-src")
224 | return self
225 | }
226 |
227 | @discardableResult
228 | public func mediaSrc(sources: String...) -> ContentSecurityPolicy {
229 | join(sources: sources, to: "media-src")
230 | return self
231 | }
232 |
233 | @discardableResult
234 | public func mediaSrc(sources: [String]) -> ContentSecurityPolicy {
235 | join(sources: sources, to: "media-src")
236 | return self
237 | }
238 |
239 | @discardableResult
240 | public func objectSrc(sources: String...) -> ContentSecurityPolicy {
241 | join(sources: sources, to: "object-src")
242 | return self
243 | }
244 |
245 | @discardableResult
246 | public func objectSrc(sources: [String]) -> ContentSecurityPolicy {
247 | join(sources: sources, to: "object-src")
248 | return self
249 | }
250 |
251 | @discardableResult
252 | public func pluginTypes(types: String...) -> ContentSecurityPolicy {
253 | join(sources: types, to: "plugin-types")
254 | return self
255 | }
256 |
257 | @discardableResult
258 | public func pluginTypes(types: [String]) -> ContentSecurityPolicy {
259 | join(sources: types, to: "plugin-types")
260 | return self
261 | }
262 |
263 | @discardableResult
264 | public func requireSriFor(values: String...) -> ContentSecurityPolicy {
265 | join(sources: values, to: "require-sri-for")
266 | return self
267 | }
268 |
269 | @discardableResult
270 | public func requireSriFor(values: [String]) -> ContentSecurityPolicy {
271 | join(sources: values, to: "require-sri-for")
272 | return self
273 | }
274 |
275 | @discardableResult
276 | public func reportTo(reportToObject: CSPReportTo) -> ContentSecurityPolicy {
277 | let encoder = JSONEncoder()
278 | guard let data = try? encoder.encode(reportToObject) else { return self }
279 | guard let jsonString = String(data: data, encoding: .utf8) else { return self }
280 | policy.append("report-to \(String(describing: jsonString))")
281 | return self
282 | }
283 |
284 | @discardableResult
285 | public func reportUri(uri: String) -> ContentSecurityPolicy {
286 | policy.append("report-uri \(uri)")
287 | return self
288 | }
289 |
290 | @discardableResult
291 | public func sandbox(values: String...) -> ContentSecurityPolicy {
292 | join(sources: values, to: "sandbox")
293 | return self
294 | }
295 |
296 | @discardableResult
297 | public func sandbox(values: [String]) -> ContentSecurityPolicy {
298 | join(sources: values, to: "sandbox")
299 | return self
300 | }
301 |
302 | @discardableResult
303 | public func scriptSrc(sources: String...) -> ContentSecurityPolicy {
304 | join(sources: sources, to: "script-src")
305 | return self
306 | }
307 |
308 | @discardableResult
309 | public func scriptSrc(sources: [String]) -> ContentSecurityPolicy {
310 | join(sources: sources, to: "script-src")
311 | return self
312 | }
313 |
314 | @discardableResult
315 | public func styleSrc(sources: String...) -> ContentSecurityPolicy {
316 | join(sources: sources, to: "style-src")
317 | return self
318 | }
319 |
320 | @discardableResult
321 | public func styleSrc(sources: [String]) -> ContentSecurityPolicy {
322 | join(sources: sources, to: "style-src")
323 | return self
324 | }
325 |
326 | @discardableResult
327 | public func upgradeInsecureRequests() -> ContentSecurityPolicy {
328 | policy.append("upgrade-insecure-requests")
329 | return self
330 | }
331 |
332 | @discardableResult
333 | public func workerSrc(sources: String...) -> ContentSecurityPolicy {
334 | join(sources: sources, to: "worker-src")
335 | return self
336 | }
337 |
338 | @discardableResult
339 | public func workerSrc(sources: [String]) -> ContentSecurityPolicy {
340 | join(sources: sources, to: "worker-src")
341 | return self
342 | }
343 |
344 | private func join(sources: [String], to directive: String) {
345 | policy.append("\(directive) \(sources.joined(separator: " "))")
346 | }
347 |
348 | public init() {}
349 | }
350 |
--------------------------------------------------------------------------------
/Sources/VaporSecurityHeaders/Configurations/ContentSecurityPolicyReportOnlyConfiguration.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | public struct ContentSecurityPolicyReportOnlyConfiguration: SecurityHeaderConfiguration {
4 |
5 | private let value: String
6 |
7 | public init(value: String) {
8 | self.value = value
9 | }
10 |
11 | func setHeader(on response: Response, from request: Request) {
12 | response.headers.replaceOrAdd(name: .contentSecurityPolicyReportOnly, value: value)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/VaporSecurityHeaders/Configurations/ContentTypeOptionsConfiguration.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | public struct ContentTypeOptionsConfiguration: SecurityHeaderConfiguration {
4 |
5 | private let option: Options
6 |
7 | public init(option: Options) {
8 | self.option = option
9 | }
10 |
11 | public enum Options {
12 | case nosniff
13 | case none
14 | }
15 |
16 | func setHeader(on response: Response, from request: Request) {
17 | switch option {
18 | case .nosniff:
19 | response.headers.replaceOrAdd(name: .xContentTypeOptions, value: "nosniff")
20 | default:
21 | break
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/VaporSecurityHeaders/Configurations/FrameOptionsConfiguration.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | public struct FrameOptionsConfiguration: SecurityHeaderConfiguration {
4 |
5 | public enum Options {
6 | case deny
7 | case sameOrigin
8 | case allow(from: String)
9 | }
10 |
11 | private let option: Options
12 |
13 | public init(option: Options) {
14 | self.option = option
15 | }
16 |
17 | func setHeader(on response: Response, from request: Request) {
18 | switch option {
19 | case .deny:
20 | response.headers.replaceOrAdd(name: .xFrameOptions, value: "DENY")
21 | case .sameOrigin:
22 | response.headers.replaceOrAdd(name: .xFrameOptions, value: "SAMEORIGIN")
23 | case .allow(let from):
24 | response.headers.replaceOrAdd(name: .xFrameOptions, value: "ALLOW-FROM \(from)")
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/VaporSecurityHeaders/Configurations/HTTPSRedirectMiddleware.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | public class HTTPSRedirectMiddleware: Middleware {
4 |
5 | let allowedHosts: [String]?
6 |
7 | public init(allowedHosts: [String]? = nil) {
8 | self.allowedHosts = allowedHosts
9 | }
10 |
11 | public func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture {
12 | if request.application.environment == .development {
13 | return next.respond(to: request)
14 | }
15 |
16 | let proto = request.headers.first(name: "X-Forwarded-Proto")
17 | ?? request.url.scheme
18 | ?? "http"
19 |
20 | guard proto == "https" else {
21 | guard let host = request.headers.first(name: .host) else {
22 | return request.eventLoop.makeFailedFuture(Abort(.badRequest))
23 | }
24 |
25 | if let allowedHosts = allowedHosts {
26 | guard allowedHosts.contains(host) else {
27 | return request.eventLoop.makeFailedFuture(Abort(.badRequest))
28 | }
29 | }
30 |
31 | let httpsURL = "https://" + host + "\(request.url)"
32 | return request.redirect(to: "\(httpsURL)", redirectType: .permanent).encodeResponse(for: request)
33 | }
34 | return next.respond(to: request)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/VaporSecurityHeaders/Configurations/ReferrerPolicyConfiguration.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | public struct ReferrerPolicyConfiguration: SecurityHeaderConfiguration {
4 |
5 | public enum Directive: String {
6 | case empty = ""
7 | case noReferrer = "no-referrer"
8 | case noReferrerWhenDowngrade = "no-referrer-when-downgrade"
9 | case sameOrigin = "same-origin"
10 | case origin = "origin"
11 | case strictOrigin = "strict-origin"
12 | case originWhenCrossOrigin = "origin-when-cross-origin"
13 | case strictOriginWhenCrossOrigin = "strict-origin-when-cross-origin"
14 | case unsafeUrl = "unsafe-url"
15 | }
16 |
17 | private let directives: [Directive]
18 |
19 | public init(_ directive: Directive) {
20 | self.directives = [directive]
21 | }
22 |
23 | public init(_ directives: [Directive]) {
24 | self.directives = directives
25 | }
26 |
27 | func setHeader(on response: Response, from request: Request) {
28 | response.headers.replaceOrAdd(name: .referrerPolicy, value: directives.map({ $0.rawValue }).joined(separator: ", "))
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/VaporSecurityHeaders/Configurations/ServerConfiguration.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | public struct ServerConfiguration: SecurityHeaderConfiguration {
4 | private let value: String
5 |
6 | public init(value: String) {
7 | self.value = value
8 | }
9 |
10 | func setHeader(on response: Response, from request: Request) {
11 | response.headers.replaceOrAdd(name: .server, value: value)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/VaporSecurityHeaders/Configurations/StrictTransportSecurityConfiguration.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | public struct StrictTransportSecurityConfiguration: SecurityHeaderConfiguration {
4 |
5 | private let maxAge: Int
6 | private let includeSubdomains: Bool
7 | private let preload: Bool
8 |
9 | public init(maxAge: Int = 31536000, includeSubdomains: Bool = true, preload: Bool = true) {
10 | self.maxAge = maxAge
11 | self.includeSubdomains = includeSubdomains
12 | self.preload = preload
13 | }
14 |
15 | func setHeader(on response: Response, from request: Request) {
16 | var headerValue = "max-age=\(maxAge);"
17 | if includeSubdomains {
18 | headerValue += " includeSubDomains;"
19 | }
20 | if preload {
21 | headerValue += " preload"
22 | }
23 |
24 | response.headers.replaceOrAdd(name: .strictTransportSecurity, value: headerValue)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/VaporSecurityHeaders/Configurations/XSSProtectionConfiguration.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | public struct XSSProtectionConfiguration: SecurityHeaderConfiguration {
4 | public init () {}
5 |
6 | func setHeader(on response: Response, from request: Request) {
7 | response.headers.replaceOrAdd(name: .xssProtection, value: "0")
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Sources/VaporSecurityHeaders/SecurityHeaderConfiguration.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | protocol SecurityHeaderConfiguration {
4 | func setHeader(on response: Response, from request: Request)
5 | }
6 |
--------------------------------------------------------------------------------
/Sources/VaporSecurityHeaders/SecurityHeaders+HeaderKey.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | public extension HTTPHeaders.Name {
4 |
5 | static let contentSecurityPolicyReportOnly = HTTPHeaders.Name("Content-Security-Policy-Report-Only")
6 | static let referrerPolicy = HTTPHeaders.Name("Referrer-Policy")
7 | }
8 |
9 |
--------------------------------------------------------------------------------
/Sources/VaporSecurityHeaders/SecurityHeaders.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | public struct SecurityHeaders {
4 |
5 | var configurations: [SecurityHeaderConfiguration]
6 |
7 | init(contentTypeConfiguration: ContentTypeOptionsConfiguration = ContentTypeOptionsConfiguration(option: .nosniff),
8 | contentSecurityPolicyConfiguration: ContentSecurityPolicyConfiguration = ContentSecurityPolicyConfiguration(value: ContentSecurityPolicy().defaultSrc(sources: CSPKeywords.`self`)),
9 | frameOptionsConfiguration: FrameOptionsConfiguration = FrameOptionsConfiguration(option: .deny),
10 | xssProtectionConfiguration: XSSProtectionConfiguration? = XSSProtectionConfiguration(),
11 | hstsConfiguration: StrictTransportSecurityConfiguration? = nil,
12 | serverConfiguration: ServerConfiguration? = nil,
13 | contentSecurityPolicyReportOnlyConfiguration: ContentSecurityPolicyReportOnlyConfiguration? = nil,
14 | referrerPolicyConfiguration: ReferrerPolicyConfiguration? = nil) {
15 | configurations = [contentTypeConfiguration, contentSecurityPolicyConfiguration, frameOptionsConfiguration]
16 |
17 | if let xssProtectionConfiguration {
18 | configurations.append(xssProtectionConfiguration)
19 | }
20 |
21 | if let hstsConfiguration = hstsConfiguration {
22 | configurations.append(hstsConfiguration)
23 | }
24 |
25 | if let serverConfiguration = serverConfiguration {
26 | configurations.append(serverConfiguration)
27 | }
28 |
29 | if let contentSecurityPolicyReportOnlyConfiguration = contentSecurityPolicyReportOnlyConfiguration {
30 | configurations.append(contentSecurityPolicyReportOnlyConfiguration)
31 | }
32 |
33 | if let referrerPolicyConfiguration = referrerPolicyConfiguration {
34 | configurations.append(referrerPolicyConfiguration)
35 | }
36 | }
37 |
38 | }
39 |
40 | extension SecurityHeaders: Middleware {
41 |
42 | public func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture {
43 | next.respond(to: request).map { response in
44 | for spec in self.configurations {
45 | spec.setHeader(on: response, from: request)
46 | }
47 | return response
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/VaporSecurityHeaders/SecurityHeadersFactory.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | public class SecurityHeadersFactory {
4 | var contentTypeOptions = ContentTypeOptionsConfiguration(option: .nosniff)
5 | var contentSecurityPolicy = ContentSecurityPolicyConfiguration(value: ContentSecurityPolicy().defaultSrc(sources: CSPKeywords.`self`))
6 | var frameOptions = FrameOptionsConfiguration(option: .deny)
7 | var xssProtection: XSSProtectionConfiguration? = XSSProtectionConfiguration()
8 | var hsts: StrictTransportSecurityConfiguration?
9 | var server: ServerConfiguration?
10 | var referrerPolicy: ReferrerPolicyConfiguration?
11 | var contentSecurityPolicyReportOnly: ContentSecurityPolicyReportOnlyConfiguration?
12 |
13 | public init() {}
14 |
15 | public static func api() -> SecurityHeadersFactory {
16 | let apiFactory = SecurityHeadersFactory()
17 | apiFactory.contentSecurityPolicy = ContentSecurityPolicyConfiguration(value: ContentSecurityPolicy().defaultSrc(sources: CSPKeywords.none))
18 | return apiFactory
19 | }
20 |
21 | @discardableResult public func with(contentTypeOptions configuration: ContentTypeOptionsConfiguration) -> SecurityHeadersFactory {
22 | contentTypeOptions = configuration
23 | return self
24 | }
25 |
26 | @discardableResult public func with(contentSecurityPolicy configuration: ContentSecurityPolicyConfiguration) -> SecurityHeadersFactory {
27 | contentSecurityPolicy = configuration
28 | return self
29 | }
30 |
31 | @discardableResult public func with(frameOptions configuration: FrameOptionsConfiguration) -> SecurityHeadersFactory {
32 | frameOptions = configuration
33 | return self
34 | }
35 |
36 | @discardableResult public func with(XSSProtection configuration: XSSProtectionConfiguration?) -> SecurityHeadersFactory {
37 | xssProtection = configuration
38 | return self
39 | }
40 |
41 | @discardableResult public func with(strictTransportSecurity configuration: StrictTransportSecurityConfiguration) -> SecurityHeadersFactory {
42 | hsts = configuration
43 | return self
44 | }
45 |
46 | @discardableResult public func with(server configuration: ServerConfiguration) -> SecurityHeadersFactory {
47 | server = configuration
48 | return self
49 | }
50 |
51 | @discardableResult public func with(referrerPolicy configuration: ReferrerPolicyConfiguration) -> SecurityHeadersFactory {
52 | referrerPolicy = configuration
53 | return self
54 | }
55 |
56 | @discardableResult public func with(contentSecurityPolicyReportOnly configuration: ContentSecurityPolicyReportOnlyConfiguration) -> SecurityHeadersFactory {
57 | contentSecurityPolicyReportOnly = configuration
58 | return self
59 | }
60 |
61 | public func build() -> SecurityHeaders {
62 | return SecurityHeaders(contentTypeConfiguration: contentTypeOptions,
63 | contentSecurityPolicyConfiguration: contentSecurityPolicy,
64 | frameOptionsConfiguration: frameOptions,
65 | xssProtectionConfiguration: xssProtection,
66 | hstsConfiguration: hsts,
67 | serverConfiguration: server,
68 | contentSecurityPolicyReportOnlyConfiguration: contentSecurityPolicyReportOnly,
69 | referrerPolicyConfiguration: referrerPolicy)
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/Tests/VaporSecurityHeadersTests/Fakes/StubFileMiddleware.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 | import VaporSecurityHeaders
3 |
4 | struct StubFileMiddleware: Middleware {
5 | var cspConfig: ContentSecurityPolicyConfiguration?
6 | init(cspConfig: ContentSecurityPolicyConfiguration? = nil) {
7 | self.cspConfig = cspConfig
8 | }
9 |
10 | func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture {
11 | if request.url.path == "/file/" {
12 | request.contentSecurityPolicy = self.cspConfig
13 |
14 | let body = Response.Body(string: "Hello World!")
15 | var headers = HTTPHeaders()
16 | headers.add(name: .eTag, value: "1491512490-\(body.count)")
17 | headers.add(name: .contentType, value: "text/plain")
18 | let response = Response(status: .ok, headers: headers, body: body)
19 | return request.eventLoop.future(response)
20 | }
21 | else {
22 | return next.respond(to: request)
23 | }
24 | }
25 | }
26 |
27 |
--------------------------------------------------------------------------------
/Tests/VaporSecurityHeadersTests/HeaderTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | @testable import Vapor
4 |
5 | import VaporSecurityHeaders
6 |
7 | class HeaderTests: XCTestCase {
8 |
9 | // MARK: - Properties
10 |
11 | private var application: Application!
12 | private var eventLoopGroup: EventLoopGroup!
13 | private var request: Request!
14 | private var routeRequest: Request!
15 | private var abortRequest: Request!
16 | private var fileRequest: Request!
17 |
18 | // MARK: - Overrides
19 |
20 | override func setUp() {
21 | eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
22 | application = Application(.testing, .shared(eventLoopGroup))
23 | request = Request(application: application, method: .GET, url: URI(string: "/test/"), on: eventLoopGroup.next())
24 | routeRequest = Request(application: application, method: .GET, url: URI(string: "/route/"), on: eventLoopGroup.next())
25 | abortRequest = Request(application: application, method: .GET, url: URI(string: "/abort/"), on: eventLoopGroup.next())
26 | fileRequest = Request(application: application, method: .GET, url: URI(string: "/file/"), on: eventLoopGroup.next())
27 | }
28 |
29 | override func tearDownWithError() throws {
30 | application.shutdown()
31 | try eventLoopGroup.syncShutdownGracefully()
32 | }
33 |
34 | // MARK: - Tests
35 |
36 | func testDefaultHeaders() throws {
37 | let expectedXCTOHeaderValue = "nosniff"
38 | let expectedCSPHeaderValue = "default-src 'self'"
39 | let expectedXFOHeaderValue = "DENY"
40 | let expectedXSSProtectionHeaderValue = "0"
41 |
42 | let response = try makeTestResponse(for: request, securityHeadersToAdd: SecurityHeadersFactory())
43 |
44 | XCTAssertEqual(expectedXCTOHeaderValue, response.headers[.xContentTypeOptions].first)
45 | XCTAssertEqual(expectedCSPHeaderValue, response.headers[.contentSecurityPolicy].first)
46 | XCTAssertEqual(expectedXFOHeaderValue, response.headers[.xFrameOptions].first)
47 | XCTAssertEqual(expectedXSSProtectionHeaderValue, response.headers[.xssProtection].first)
48 | }
49 |
50 | func testDefaultHeadersWithHSTS() throws {
51 | let expectedXCTOHeaderValue = "nosniff"
52 | let expectedCSPHeaderValue = "default-src 'self'"
53 | let expectedXFOHeaderValue = "DENY"
54 | let expectedXSSProtectionHeaderValue = "0"
55 | let expectedHSTSHeaderValue = "max-age=31536000; includeSubDomains; preload"
56 |
57 | let response = try makeTestResponse(for: request, securityHeadersToAdd: SecurityHeadersFactory().with(strictTransportSecurity: StrictTransportSecurityConfiguration()))
58 |
59 | XCTAssertEqual(expectedXCTOHeaderValue, response.headers[.xContentTypeOptions].first)
60 | XCTAssertEqual(expectedCSPHeaderValue, response.headers[.contentSecurityPolicy].first)
61 | XCTAssertEqual(expectedXFOHeaderValue, response.headers[.xFrameOptions].first)
62 | XCTAssertEqual(expectedXSSProtectionHeaderValue, response.headers[.xssProtection].first)
63 | XCTAssertEqual(expectedHSTSHeaderValue, response.headers[.strictTransportSecurity].first)
64 | }
65 |
66 | func testAllHeadersForApi() throws {
67 | let expectedXCTOHeaderValue = "nosniff"
68 | let expectedCSPHeaderValue = "default-src 'none'"
69 | let expectedXFOHeaderValue = "DENY"
70 | let expectedXSSProtectionHeaderValue = "0"
71 |
72 | let response = try makeTestResponse(for: request, securityHeadersToAdd: SecurityHeadersFactory.api())
73 |
74 | XCTAssertEqual(expectedXCTOHeaderValue, response.headers[.xContentTypeOptions].first)
75 | XCTAssertEqual(expectedCSPHeaderValue, response.headers[.contentSecurityPolicy].first)
76 | XCTAssertEqual(expectedXFOHeaderValue, response.headers[.xFrameOptions].first)
77 | XCTAssertEqual(expectedXSSProtectionHeaderValue, response.headers[.xssProtection].first)
78 | }
79 |
80 | func testAPIHeadersWithHSTS() throws {
81 | let expectedXCTOHeaderValue = "nosniff"
82 | let expectedCSPHeaderValue = "default-src 'none'"
83 | let expectedXFOHeaderValue = "DENY"
84 | let expectedXSSProtectionHeaderValue = "0"
85 | let expectedHSTSHeaderValue = "max-age=31536000; includeSubDomains; preload"
86 |
87 | let response = try makeTestResponse(for: request, securityHeadersToAdd: SecurityHeadersFactory.api().with(strictTransportSecurity: StrictTransportSecurityConfiguration()))
88 |
89 | XCTAssertEqual(expectedXCTOHeaderValue, response.headers[.xContentTypeOptions].first)
90 | XCTAssertEqual(expectedCSPHeaderValue, response.headers[.contentSecurityPolicy].first)
91 | XCTAssertEqual(expectedXFOHeaderValue, response.headers[.xFrameOptions].first)
92 | XCTAssertEqual(expectedXSSProtectionHeaderValue, response.headers[.xssProtection].first)
93 | XCTAssertEqual(expectedHSTSHeaderValue, response.headers[.strictTransportSecurity].first)
94 | }
95 |
96 | func testHeadersWithContentTypeOptionsTurnedOff() throws {
97 | let contentTypeConfig = ContentTypeOptionsConfiguration(option: .none)
98 | let factory = SecurityHeadersFactory().with(contentTypeOptions: contentTypeConfig)
99 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
100 |
101 | XCTAssertNil(response.headers[.xContentTypeOptions].first)
102 | }
103 |
104 | func testHeadersWithContentTypeOptionsNosniff() throws {
105 | let contentTypeConfig = ContentTypeOptionsConfiguration(option: .nosniff)
106 | let factory = SecurityHeadersFactory().with(contentTypeOptions: contentTypeConfig)
107 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
108 |
109 | XCTAssertEqual("nosniff", response.headers[.xContentTypeOptions].first)
110 | }
111 |
112 | func testHeaderWithFrameOptionsDeny() throws {
113 | let frameOptionsConfig = FrameOptionsConfiguration(option: .deny)
114 | let factory = SecurityHeadersFactory().with(frameOptions: frameOptionsConfig)
115 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
116 |
117 | XCTAssertEqual("DENY", response.headers[.xFrameOptions].first)
118 | }
119 |
120 | func testHeaderWithFrameOptionsSameOrigin() throws {
121 | let frameOptionsConfig = FrameOptionsConfiguration(option: .sameOrigin)
122 | let factory = SecurityHeadersFactory().with(frameOptions: frameOptionsConfig)
123 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
124 |
125 | XCTAssertEqual("SAMEORIGIN", response.headers[.xFrameOptions].first)
126 | }
127 |
128 | func testHeaderWithFrameOptionsAllowFrom() throws {
129 | let frameOptionsConfig = FrameOptionsConfiguration(option: .allow(from: "https://test.com"))
130 | let factory = SecurityHeadersFactory().with(frameOptions: frameOptionsConfig)
131 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
132 |
133 | XCTAssertEqual("ALLOW-FROM https://test.com", response.headers[.xFrameOptions].first)
134 | }
135 |
136 | func testHeaderWithXssProtection() throws {
137 | let xssProtectionConfig = XSSProtectionConfiguration()
138 | let factory = SecurityHeadersFactory().with(XSSProtection: xssProtectionConfig)
139 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
140 |
141 | XCTAssertEqual("0", response.headers[.xssProtection].first)
142 | }
143 |
144 | func testHeaderWithXssProtectionDisabled() throws {
145 | let factory = SecurityHeadersFactory().with(XSSProtection: nil)
146 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
147 |
148 | XCTAssertNil(response.headers[.xssProtection].first)
149 | }
150 |
151 | func testHeaderWithHSTSwithMaxAge() throws {
152 | let hstsConfig = StrictTransportSecurityConfiguration(maxAge: 30)
153 | let factory = SecurityHeadersFactory().with(strictTransportSecurity: hstsConfig)
154 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
155 |
156 | XCTAssertEqual("max-age=30; includeSubDomains; preload", response.headers[.strictTransportSecurity].first)
157 | }
158 |
159 | func testHeadersWithHSTSwithSubdomains() throws {
160 | let hstsConfig = StrictTransportSecurityConfiguration(maxAge: 30, includeSubdomains: true)
161 | let factory = SecurityHeadersFactory().with(strictTransportSecurity: hstsConfig)
162 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
163 |
164 | XCTAssertEqual("max-age=30; includeSubDomains; preload", response.headers[.strictTransportSecurity].first)
165 | }
166 |
167 | func testHeadersWithHSTSwithPreload() throws {
168 | let hstsConfig = StrictTransportSecurityConfiguration(maxAge: 30, preload: true)
169 | let factory = SecurityHeadersFactory().with(strictTransportSecurity: hstsConfig)
170 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
171 |
172 | XCTAssertEqual("max-age=30; includeSubDomains; preload", response.headers[.strictTransportSecurity].first)
173 | }
174 |
175 | func testHeadersWithHSTSwithPreloadAndSubdomain() throws {
176 | let hstsConfig = StrictTransportSecurityConfiguration(maxAge: 30, includeSubdomains: true, preload: true)
177 | let factory = SecurityHeadersFactory().with(strictTransportSecurity: hstsConfig)
178 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
179 |
180 | XCTAssertEqual("max-age=30; includeSubDomains; preload", response.headers[.strictTransportSecurity].first)
181 | }
182 |
183 | func testHeadersWithHSTSwithSubdomainsFalse() throws {
184 | let hstsConfig = StrictTransportSecurityConfiguration(maxAge: 30, includeSubdomains: false)
185 | let factory = SecurityHeadersFactory().with(strictTransportSecurity: hstsConfig)
186 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
187 |
188 | XCTAssertEqual("max-age=30; preload", response.headers[.strictTransportSecurity].first)
189 | }
190 |
191 | func testHeadersWithHSTSwithPreloadFalse() throws {
192 | let hstsConfig = StrictTransportSecurityConfiguration(maxAge: 30, preload: false)
193 | let factory = SecurityHeadersFactory().with(strictTransportSecurity: hstsConfig)
194 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
195 |
196 | XCTAssertEqual("max-age=30; includeSubDomains;", response.headers[.strictTransportSecurity].first)
197 | }
198 |
199 | func testHeadersWithHSTSwithSubdomainAndPreloadFalse() throws {
200 | let hstsConfig = StrictTransportSecurityConfiguration(maxAge: 30, includeSubdomains: false, preload: false)
201 | let factory = SecurityHeadersFactory().with(strictTransportSecurity: hstsConfig)
202 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
203 |
204 | XCTAssertEqual("max-age=30;", response.headers[.strictTransportSecurity].first)
205 | }
206 |
207 | func testHeadersWithServerValue() throws {
208 | let serverConfig = ServerConfiguration(value: "brokenhands.io")
209 | let factory = SecurityHeadersFactory().with(server: serverConfig)
210 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
211 |
212 | XCTAssertEqual("brokenhands.io", response.headers[.server].first)
213 | }
214 |
215 | func testHeadersWithCSP() throws {
216 | 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"
217 | let cspBuilder = ContentSecurityPolicy()
218 | .defaultSrc(sources: CSPKeywords.none)
219 | .scriptSrc(sources: "https://static.brokenhands.io")
220 | .styleSrc(sources: "https://static.brokenhands.io")
221 | .imgSrc(sources: "https://static.brokenhands.io")
222 | .fontSrc(sources: "https://static.brokenhands.io")
223 | .connectSrc(sources: "https://*.brokenhands.io")
224 | .formAction(sources: CSPKeywords.`self`)
225 | .upgradeInsecureRequests()
226 | .blockAllMixedContent()
227 | .requireSriFor(values: "script", "style")
228 | let cspConfig = ContentSecurityPolicyConfiguration(value: cspBuilder)
229 | let factory = SecurityHeadersFactory().with(contentSecurityPolicy: cspConfig)
230 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
231 |
232 | XCTAssertEqual(csp, response.headers[.contentSecurityPolicy].first)
233 | }
234 |
235 | func testNonVariadicHeadersWithCSP() throws {
236 | 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"
237 | let cspBuilder = ContentSecurityPolicy()
238 | .defaultSrc(sources: CSPKeywords.none)
239 | .scriptSrc(sources: ["https://static.brokenhands.io"])
240 | .styleSrc(sources: ["https://static.brokenhands.io"])
241 | .imgSrc(sources: ["https://static.brokenhands.io"])
242 | .fontSrc(sources: ["https://static.brokenhands.io"])
243 | .connectSrc(sources: ["https://*.brokenhands.io"])
244 | .formAction(sources: CSPKeywords.`self`)
245 | .upgradeInsecureRequests()
246 | .blockAllMixedContent()
247 | .requireSriFor(values: "script", "style")
248 | let cspConfig = ContentSecurityPolicyConfiguration(value: cspBuilder)
249 | let factory = SecurityHeadersFactory().with(contentSecurityPolicy: cspConfig)
250 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
251 |
252 | XCTAssertEqual(csp, response.headers[.contentSecurityPolicy].first)
253 | }
254 |
255 | func testHeadersWithStringCSP() throws {
256 | 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"
257 | let cspConfig = ContentSecurityPolicyConfiguration(value: csp)
258 | let factory = SecurityHeadersFactory().with(contentSecurityPolicy: cspConfig)
259 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
260 |
261 | XCTAssertEqual(csp, response.headers[.contentSecurityPolicy].first)
262 | }
263 |
264 | func testHeadersWithSetCSP() throws {
265 | 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"
266 | let cspBuilder = ContentSecurityPolicy().set(value: csp)
267 | let cspConfig = ContentSecurityPolicyConfiguration(value: cspBuilder)
268 | let factory = SecurityHeadersFactory().with(contentSecurityPolicy: cspConfig)
269 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
270 |
271 | XCTAssertEqual(csp, response.headers[.contentSecurityPolicy].first)
272 | }
273 |
274 | func testHeadersWithReportToCSP() throws {
275 | let reportToEndpoint = CSPReportToEndpoint(url: "https://csp-report.brokenhands.io/csp-reports")
276 | let reportToValue = CSPReportTo(group: "vapor-csp", max_age: 10886400, endpoints: [reportToEndpoint], include_subdomains: true)
277 | let cspValue = ContentSecurityPolicy()
278 | .defaultSrc(sources: CSPKeywords.none)
279 | .scriptSrc(sources: "https://static.brokenhands.io")
280 | .reportTo(reportToObject: reportToValue)
281 | let cspConfig = ContentSecurityPolicyConfiguration(value: cspValue)
282 | let factory = SecurityHeadersFactory().with(contentSecurityPolicy: cspConfig)
283 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
284 | guard let cspResponseHeader = response.headers[.contentSecurityPolicy].first else {
285 | XCTFail("Expected a CSP Response Header")
286 | return
287 | }
288 | let replacedCSPHeader = cspResponseHeader.replacingOccurrences(of: "default-src 'none'; script-src https://static.brokenhands.io; report-to", with: "")
289 | guard let reportToJson = replacedCSPHeader.data(using: .utf8) else {
290 | XCTFail("Expected String CSP Response Header")
291 | return
292 | }
293 | let decoder = JSONDecoder()
294 | guard let reportToData = try? decoder.decode(CSPReportTo.self, from: reportToJson) else {
295 | XCTFail("Expected JSON CSP Response Header")
296 | return
297 | }
298 |
299 | XCTAssertEqual(reportToValue, reportToData)
300 | }
301 |
302 | func testHeadersWithExhaustiveCSP() throws {
303 | 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"
304 | let cspBuilder = ContentSecurityPolicy()
305 | .baseUri(sources: CSPKeywords.`self`)
306 | .frameAncestors(sources: CSPKeywords.none)
307 | .frameSrc(sources: CSPKeywords.`self`)
308 | .manifestSrc(sources: "https://brokenhands.io")
309 | .objectSrc(sources: CSPKeywords.`self`)
310 | .pluginTypes(types: "application/pdf")
311 | .reportUri(uri: "https://csp-report.brokenhands.io")
312 | .sandbox(values: "allow-forms", "allow-scripts")
313 | .workerSrc(sources: "https://brokenhands.io")
314 | .mediaSrc(sources: "https://brokenhands.io")
315 | let cspConfig = ContentSecurityPolicyConfiguration(value: cspBuilder)
316 | let factory = SecurityHeadersFactory().with(contentSecurityPolicy: cspConfig)
317 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
318 |
319 | XCTAssertEqual(csp, response.headers[.contentSecurityPolicy].first)
320 | }
321 |
322 | func testNonVariadicHeadersWithExhaustiveCSP() throws {
323 | 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"
324 | let cspBuilder = ContentSecurityPolicy()
325 | .baseUri(sources: [CSPKeywords.`self`])
326 | .frameAncestors(sources: [CSPKeywords.none])
327 | .frameSrc(sources: [CSPKeywords.`self`])
328 | .manifestSrc(sources: ["https://brokenhands.io"])
329 | .objectSrc(sources: [CSPKeywords.`self`])
330 | .pluginTypes(types: ["application/pdf"])
331 | .reportUri(uri: "https://csp-report.brokenhands.io")
332 | .sandbox(values: ["allow-forms", "allow-scripts"])
333 | .workerSrc(sources: ["https://brokenhands.io"])
334 | .mediaSrc(sources: ["https://brokenhands.io"])
335 | let cspConfig = ContentSecurityPolicyConfiguration(value: cspBuilder)
336 | let factory = SecurityHeadersFactory().with(contentSecurityPolicy: cspConfig)
337 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
338 |
339 | XCTAssertEqual(csp, response.headers[.contentSecurityPolicy].first)
340 | }
341 |
342 | func testCombineVariadicNonVariadicHeadersWithExhaustiveCSP() throws {
343 | 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"
344 | let cspBuilder = ContentSecurityPolicy()
345 | .baseUri(sources: CSPKeywords.`self`)
346 | .frameAncestors(sources: [CSPKeywords.none])
347 | .frameSrc(sources: [CSPKeywords.`self`])
348 | .manifestSrc(sources: ["https://brokenhands.io"])
349 | .objectSrc(sources: CSPKeywords.`self`)
350 | .pluginTypes(types: ["application/pdf"])
351 | .reportUri(uri: "https://csp-report.brokenhands.io")
352 | .sandbox(values: ["allow-forms", "allow-scripts"])
353 | .workerSrc(sources: "https://brokenhands.io")
354 | .mediaSrc(sources: ["https://brokenhands.io"])
355 | let cspConfig = ContentSecurityPolicyConfiguration(value: cspBuilder)
356 | let factory = SecurityHeadersFactory().with(contentSecurityPolicy: cspConfig)
357 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
358 |
359 | XCTAssertEqual(csp, response.headers[.contentSecurityPolicy].first)
360 | }
361 |
362 | func testHeadersWithReportOnlyCSP() throws {
363 | let csp = "default-src https:; report-uri https://csp-report.brokenhands.io"
364 | let cspConfig = ContentSecurityPolicyReportOnlyConfiguration(value: csp)
365 | let factory = SecurityHeadersFactory().with(contentSecurityPolicyReportOnly: cspConfig)
366 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
367 |
368 | XCTAssertEqual(csp, response.headers[.contentSecurityPolicyReportOnly].first)
369 | }
370 |
371 | func testHeadersWithReferrerPolicyEmpty() throws {
372 | let expected = ""
373 | let referrerConfig = ReferrerPolicyConfiguration(.empty)
374 | let factory = SecurityHeadersFactory().with(referrerPolicy: referrerConfig)
375 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
376 | XCTAssertEqual(expected, response.headers[.referrerPolicy].first)
377 | }
378 |
379 | func testHeadersWithReferrerPolicyNoReferrer() throws {
380 | let expected = "no-referrer"
381 | let referrerConfig = ReferrerPolicyConfiguration(.noReferrer)
382 | let factory = SecurityHeadersFactory().with(referrerPolicy: referrerConfig)
383 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
384 | XCTAssertEqual(expected, response.headers[.referrerPolicy].first)
385 | }
386 |
387 | func testHeadersWithReferrerPolicyNoReferrerWhenDowngrade() throws {
388 | let expected = "no-referrer-when-downgrade"
389 | let referrerConfig = ReferrerPolicyConfiguration(.noReferrerWhenDowngrade)
390 | let factory = SecurityHeadersFactory().with(referrerPolicy: referrerConfig)
391 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
392 | XCTAssertEqual(expected, response.headers[.referrerPolicy].first)
393 | }
394 |
395 | func testHeadersWithReferrerPolicySameOrigin() throws {
396 | let expected = "same-origin"
397 | let referrerConfig = ReferrerPolicyConfiguration(.sameOrigin)
398 | let factory = SecurityHeadersFactory().with(referrerPolicy: referrerConfig)
399 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
400 | XCTAssertEqual(expected, response.headers[.referrerPolicy].first)
401 | }
402 |
403 | func testHeadersWithReferrerPolicyOrigin() throws {
404 | let expected = "origin"
405 | let referrerConfig = ReferrerPolicyConfiguration(.origin)
406 | let factory = SecurityHeadersFactory().with(referrerPolicy: referrerConfig)
407 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
408 | XCTAssertEqual(expected, response.headers[.referrerPolicy].first)
409 | }
410 |
411 | func testHeadersWithReferrerPolicyStrictOrigin() throws {
412 | let expected = "strict-origin"
413 | let referrerConfig = ReferrerPolicyConfiguration(.strictOrigin)
414 | let factory = SecurityHeadersFactory().with(referrerPolicy: referrerConfig)
415 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
416 | XCTAssertEqual(expected, response.headers[.referrerPolicy].first)
417 | }
418 |
419 | func testHeadersWithReferrerPolicyOriginWhenCrossOrigin() throws {
420 | let expected = "origin-when-cross-origin"
421 | let referrerConfig = ReferrerPolicyConfiguration(.originWhenCrossOrigin)
422 | let factory = SecurityHeadersFactory().with(referrerPolicy: referrerConfig)
423 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
424 | XCTAssertEqual(expected, response.headers[.referrerPolicy].first)
425 | }
426 |
427 | func testHeadersWithReferrerPolicyStrictOriginWhenCrossOrigin() throws {
428 | let expected = "strict-origin-when-cross-origin"
429 | let referrerConfig = ReferrerPolicyConfiguration(.strictOriginWhenCrossOrigin)
430 | let factory = SecurityHeadersFactory().with(referrerPolicy: referrerConfig)
431 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
432 | XCTAssertEqual(expected, response.headers[.referrerPolicy].first)
433 | }
434 |
435 | func testHeadersWithReferrerPolicyUnsafeUrl() throws {
436 | let expected = "unsafe-url"
437 | let referrerConfig = ReferrerPolicyConfiguration(.unsafeUrl)
438 | let factory = SecurityHeadersFactory().with(referrerPolicy: referrerConfig)
439 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
440 | XCTAssertEqual(expected, response.headers[.referrerPolicy].first)
441 | }
442 |
443 | func testHeadersWithReferrerPolicyFallbacks() throws {
444 | let expected = "no-referrer, strict-origin-when-cross-origin"
445 | let referrerConfig = ReferrerPolicyConfiguration([.noReferrer, .strictOriginWhenCrossOrigin])
446 | let factory = SecurityHeadersFactory().with(referrerPolicy: referrerConfig)
447 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
448 | XCTAssertEqual(expected, response.headers[.referrerPolicy].first)
449 | }
450 |
451 | func testApiPolicyWithAddedReferrerPolicy() throws {
452 | let expected = "strict-origin"
453 | let referrerConfig = ReferrerPolicyConfiguration(.strictOrigin)
454 | let factory = SecurityHeadersFactory.api().with(referrerPolicy: referrerConfig)
455 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory)
456 | XCTAssertEqual(expected, response.headers[.referrerPolicy].first)
457 | }
458 |
459 | func testCustomCSPOnSingleRoute() throws {
460 | 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; child-src 'self'; form-action 'self'; upgrade-insecure-requests; block-all-mixed-content; require-sri-for script style"
461 | let cspBuilder = ContentSecurityPolicy()
462 | .defaultSrc(sources: CSPKeywords.none)
463 | .scriptSrc(sources: "https://static.brokenhands.io")
464 | .styleSrc(sources: "https://static.brokenhands.io")
465 | .imgSrc(sources: "https://static.brokenhands.io")
466 | .fontSrc(sources: "https://static.brokenhands.io")
467 | .connectSrc(sources: "https://*.brokenhands.io")
468 | .childSrc(sources: CSPKeywords.`self`)
469 | .formAction(sources: CSPKeywords.`self`)
470 | .upgradeInsecureRequests()
471 | .blockAllMixedContent()
472 | .requireSriFor(values: "script", "style")
473 | let factory = SecurityHeadersFactory.api()
474 | let cspSettingRouteHandler: (Request) throws -> String = { req in
475 | req.contentSecurityPolicy = ContentSecurityPolicyConfiguration(value: cspBuilder)
476 | return "Different CSP!"
477 | }
478 | let response = try makeTestResponse(for: routeRequest, securityHeadersToAdd: factory, routeHandler: cspSettingRouteHandler)
479 |
480 | XCTAssertEqual(expectedCsp, response.headers[.contentSecurityPolicy].first)
481 | }
482 |
483 | func testNonVariadicCustomCSPOnSingleRoute() throws {
484 | 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; child-src 'self'; form-action 'self'; upgrade-insecure-requests; block-all-mixed-content; require-sri-for script style"
485 | let cspBuilder = ContentSecurityPolicy()
486 | .defaultSrc(sources: [CSPKeywords.none])
487 | .scriptSrc(sources: ["https://static.brokenhands.io"])
488 | .styleSrc(sources: ["https://static.brokenhands.io"])
489 | .imgSrc(sources: ["https://static.brokenhands.io"])
490 | .fontSrc(sources: ["https://static.brokenhands.io"])
491 | .connectSrc(sources: ["https://*.brokenhands.io"])
492 | .childSrc(sources: [CSPKeywords.`self`])
493 | .formAction(sources: [CSPKeywords.`self`])
494 | .upgradeInsecureRequests()
495 | .blockAllMixedContent()
496 | .requireSriFor(values: ["script", "style"])
497 | let factory = SecurityHeadersFactory.api()
498 | let cspSettingRouteHandler: (Request) throws -> String = { req in
499 | req.contentSecurityPolicy = ContentSecurityPolicyConfiguration(value: cspBuilder)
500 | return "Different CSP!"
501 | }
502 | let response = try makeTestResponse(for: routeRequest, securityHeadersToAdd: factory, routeHandler: cspSettingRouteHandler)
503 |
504 | XCTAssertEqual(expectedCsp, response.headers[.contentSecurityPolicy].first)
505 | }
506 |
507 | func testCustomCSPDoesntAffectSecondRoute() throws {
508 | let customCSP = ContentSecurityPolicy()
509 | .defaultSrc(sources: CSPKeywords.none)
510 | .scriptSrc(sources: "https://static.brokenhands.io")
511 | .styleSrc(sources: "https://static.brokenhands.io")
512 | .imgSrc(sources: "https://static.brokenhands.io")
513 | .fontSrc(sources: "https://static.brokenhands.io")
514 | .connectSrc(sources: "https://*.brokenhands.io")
515 | .formAction(sources: CSPKeywords.`self`)
516 | .upgradeInsecureRequests()
517 | .blockAllMixedContent()
518 | .requireSriFor(values: "script", "style")
519 | let factory = SecurityHeadersFactory.api()
520 | let cspSettingRouteHandler: (Request) throws -> String = { req in
521 | req.contentSecurityPolicy = ContentSecurityPolicyConfiguration(value: customCSP)
522 | return "Different CSP!"
523 | }
524 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory, routeHandler: cspSettingRouteHandler, initialRequest: routeRequest)
525 | let expectedCSPHeaderValue = "default-src 'none'"
526 |
527 | XCTAssertEqual(expectedCSPHeaderValue, response.headers[.contentSecurityPolicy].first)
528 | }
529 |
530 | func testNonVariadicCustomCSPDoesntAffectSecondRoute() throws {
531 | let customCSP = ContentSecurityPolicy()
532 | .defaultSrc(sources: [CSPKeywords.none])
533 | .scriptSrc(sources: ["https://static.brokenhands.io"])
534 | .styleSrc(sources: ["https://static.brokenhands.io"])
535 | .imgSrc(sources: ["https://static.brokenhands.io"])
536 | .fontSrc(sources: ["https://static.brokenhands.io"])
537 | .connectSrc(sources: ["https://*.brokenhands.io"])
538 | .formAction(sources: [CSPKeywords.`self`])
539 | .upgradeInsecureRequests()
540 | .blockAllMixedContent()
541 | .requireSriFor(values: ["script", "style"])
542 | let factory = SecurityHeadersFactory.api()
543 | let cspSettingRouteHandler: (Request) throws -> String = { req in
544 | req.contentSecurityPolicy = ContentSecurityPolicyConfiguration(value: customCSP)
545 | return "Different CSP!"
546 | }
547 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory, routeHandler: cspSettingRouteHandler, initialRequest: routeRequest)
548 | let expectedCSPHeaderValue = "default-src 'none'"
549 |
550 | XCTAssertEqual(expectedCSPHeaderValue, response.headers[.contentSecurityPolicy].first)
551 | }
552 |
553 | func testDifferentRequestReturnsDefaultCSPWhenSettingCustomCSPOnRoute() throws {
554 | let differentCsp = ContentSecurityPolicy()
555 | .defaultSrc(sources: CSPKeywords.none)
556 | .scriptSrc(sources: "test")
557 | let factory = SecurityHeadersFactory.api()
558 | let cspSettingRouteHandler: (Request) throws -> String = { req in
559 | req.contentSecurityPolicy = ContentSecurityPolicyConfiguration(value: differentCsp)
560 | return "Different CSP!"
561 | }
562 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory, routeHandler: cspSettingRouteHandler)
563 |
564 | XCTAssertEqual("default-src 'none'", response.headers[.contentSecurityPolicy].first)
565 | }
566 |
567 | func testNonVariadicDifferentRequestReturnsDefaultCSPWhenSettingCustomCSPOnRoute() throws {
568 | let differentCsp = ContentSecurityPolicy()
569 | .defaultSrc(sources: [CSPKeywords.none])
570 | .scriptSrc(sources: ["test"])
571 | let factory = SecurityHeadersFactory.api()
572 | let cspSettingRouteHandler: (Request) throws -> String = { req in
573 | req.contentSecurityPolicy = ContentSecurityPolicyConfiguration(value: differentCsp)
574 | return "Different CSP!"
575 | }
576 | let response = try makeTestResponse(for: request, securityHeadersToAdd: factory, routeHandler: cspSettingRouteHandler)
577 |
578 | XCTAssertEqual("default-src 'none'", response.headers[.contentSecurityPolicy].first)
579 | }
580 |
581 | func testAbortMiddleware() throws {
582 | let expectedXCTOHeaderValue = "nosniff"
583 | let expectedCSPHeaderValue = "default-src 'none'"
584 | let expectedXFOHeaderValue = "DENY"
585 | let expectedXSSProtectionHeaderValue = "0"
586 |
587 | let response = try makeTestResponse(for: abortRequest, securityHeadersToAdd: SecurityHeadersFactory.api())
588 |
589 | XCTAssertEqual(expectedXCTOHeaderValue, response.headers[.xContentTypeOptions].first)
590 | XCTAssertEqual(expectedCSPHeaderValue, response.headers[.contentSecurityPolicy].first)
591 | XCTAssertEqual(expectedXFOHeaderValue, response.headers[.xFrameOptions].first)
592 | XCTAssertEqual(expectedXSSProtectionHeaderValue, response.headers[.xssProtection].first)
593 | }
594 |
595 | func testStubFileMiddleware() throws {
596 | let expectedXCTOHeaderValue = "nosniff"
597 | let expectedCSPHeaderValue = "default-src 'none'"
598 | let expectedXFOHeaderValue = "DENY"
599 | let expectedXSSProtectionHeaderValue = "0"
600 | let response = try makeTestResponse(for: fileRequest, securityHeadersToAdd: SecurityHeadersFactory.api(), fileMiddleware: StubFileMiddleware())
601 |
602 | XCTAssertEqual("Hello World!", String(data: response.body.data!, encoding: String.Encoding.utf8))
603 | XCTAssertEqual(expectedXCTOHeaderValue, response.headers[.xContentTypeOptions].first)
604 | XCTAssertEqual(expectedCSPHeaderValue, response.headers[.contentSecurityPolicy].first)
605 | XCTAssertEqual(expectedXFOHeaderValue, response.headers[.xFrameOptions].first)
606 | XCTAssertEqual(expectedXSSProtectionHeaderValue, response.headers[.xssProtection].first)
607 | }
608 |
609 | func testStubFileMiddlewareDifferentRequestReturnsDefaultCSPWhenSettingCustomCSPOnRoute() throws {
610 | let expectedXCTOHeaderValue = "nosniff"
611 | let expectedCSPHeaderValue = "default-src 'none'; script-src test"
612 | let csp = ContentSecurityPolicy()
613 | .defaultSrc(sources: CSPKeywords.none)
614 | .scriptSrc(sources: "test")
615 | let expectedXFOHeaderValue = "DENY"
616 | let expectedXSSProtectionHeaderValue = "0"
617 |
618 | let response = try makeTestResponse(for: fileRequest, securityHeadersToAdd: SecurityHeadersFactory.api(), fileMiddleware: StubFileMiddleware(cspConfig: ContentSecurityPolicyConfiguration(value: csp)))
619 |
620 | XCTAssertEqual("Hello World!", String(data: response.body.data!, encoding: String.Encoding.utf8))
621 | XCTAssertEqual(expectedXCTOHeaderValue, response.headers[.xContentTypeOptions].first)
622 | XCTAssertEqual(expectedCSPHeaderValue, response.headers[.contentSecurityPolicy].first)
623 | XCTAssertEqual(expectedXFOHeaderValue, response.headers[.xFrameOptions].first)
624 | XCTAssertEqual(expectedXSSProtectionHeaderValue, response.headers[.xssProtection].first)
625 | }
626 |
627 | // MARK: - Private functions
628 |
629 | private func makeTestResponse(for request: Request, securityHeadersToAdd: SecurityHeadersFactory, routeHandler: ((Request) throws -> String)? = nil, fileMiddleware: StubFileMiddleware? = nil, initialRequest: Request? = nil) throws -> Response {
630 |
631 | application.middleware = Middlewares()
632 | application.middleware.use(securityHeadersToAdd.build())
633 | application.middleware.use(ErrorMiddleware.default(environment: request.application.environment))
634 |
635 | if let fileMiddleware = fileMiddleware {
636 | application.middleware.use(fileMiddleware)
637 | }
638 |
639 | application.routes.get("test") { req in
640 | return "TEST"
641 | }
642 |
643 | if let routeHandler = routeHandler {
644 | application.routes.get("route", use: routeHandler)
645 | }
646 |
647 | application.routes.get("abort") { req -> EventLoopFuture in
648 | throw Abort(.badRequest)
649 | }
650 |
651 | if let dummyRequest = initialRequest {
652 | _ = try application.responder.respond(to: dummyRequest).wait()
653 | }
654 |
655 | return try application.responder.respond(to: request).wait()
656 | }
657 |
658 | }
659 |
660 | struct ResponseData: Content {
661 | let string: String
662 | }
663 |
--------------------------------------------------------------------------------
/Tests/VaporSecurityHeadersTests/RedirectionTest.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | @testable import Vapor
4 |
5 | import VaporSecurityHeaders
6 |
7 | class RedirectionTest: XCTestCase {
8 |
9 | // MARK: - Properties
10 |
11 | private var application: Application!
12 | private var eventLoopGroup: EventLoopGroup!
13 | private var request: Request!
14 |
15 | override func setUp() {
16 | eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
17 | application = Application(.testing, .shared(eventLoopGroup))
18 | request = Request(application: application, method: .GET, on: eventLoopGroup.next())
19 | }
20 |
21 | override func tearDownWithError() throws {
22 | application.shutdown()
23 | try eventLoopGroup.syncShutdownGracefully()
24 | }
25 |
26 | func testWithRedirectionMiddleware() throws {
27 | let expectedRedirectStatus: HTTPStatus = HTTPResponseStatus(statusCode: 301, reasonPhrase: "Moved permanently")
28 | request.headers.add(name: .host, value: "localhost:8080")
29 | let responseRedirected = try makeTestResponse(for: request, withRedirection: true)
30 | XCTAssertEqual(expectedRedirectStatus, responseRedirected.status)
31 | }
32 |
33 | func testWithRedirectMiddlewareWithAllowedHost() throws {
34 | let expectedRedirectStatus: HTTPStatus = HTTPResponseStatus(statusCode: 301, reasonPhrase: "Moved permanently")
35 | request.headers.add(name: .host, value: "localhost:8080")
36 | let responseRedirected = try makeTestResponse(for: request, withRedirection: true, allowedHosts: ["localhost:8080", "example.com"])
37 | XCTAssertEqual(expectedRedirectStatus, responseRedirected.status)
38 | }
39 |
40 | func testWithRedirectMiddlewareWithDisallowedHost() throws {
41 | let expectedOutcome: String = "Abort.400: Bad Request"
42 | do {
43 | request.headers.add(name: .host, value: "localhost:8080")
44 | _ = try makeTestResponse(for: request, withRedirection: true, allowedHosts: ["localhost:8081", "example.com"])
45 | } catch (let error) {
46 | XCTAssertEqual(expectedOutcome, error.localizedDescription)
47 | }
48 | }
49 |
50 | func testWithoutRedirectionMiddleware() throws {
51 | let expectedNoRedirectStatus: HTTPStatus = HTTPResponseStatus(statusCode: 200, reasonPhrase: "Ok")
52 | request.headers.add(name: .host, value: "localhost:8080")
53 | let response = try makeTestResponse(for: request, withRedirection: false)
54 | XCTAssertEqual(expectedNoRedirectStatus, response.status)
55 | }
56 |
57 | func testOnDevelopmentEnvironment() throws {
58 | let expectedStatus: HTTPStatus = HTTPResponseStatus(statusCode: 200, reasonPhrase: "Ok")
59 | request.headers.add(name: .host, value: "localhost:8080")
60 | let response = try makeTestResponse(for: request, withRedirection: true, environment: .development)
61 | XCTAssertEqual(expectedStatus, response.status)
62 | }
63 |
64 | func testWithoutHost() throws {
65 | let expectedOutcome: String = "Abort.400: Bad Request"
66 | do {
67 | _ = try makeTestResponse(for: request, withRedirection: true)
68 | } catch (let error) {
69 | XCTAssertEqual(expectedOutcome, error.localizedDescription)
70 | }
71 | }
72 |
73 | func testWithProtoSet() throws {
74 | let expectedStatus: HTTPStatus = HTTPResponseStatus(statusCode: 200, reasonPhrase: "Ok")
75 | request.headers.add(name: .xForwardedProto, value: "https")
76 | let response = try makeTestResponse(for: request, withRedirection: true)
77 | XCTAssertEqual(expectedStatus, response.status)
78 | }
79 |
80 | private func makeTestResponse(for request: Request, withRedirection: Bool, environment: Environment? = nil, allowedHosts: [String]? = nil) throws -> Response {
81 | application.middleware = Middlewares()
82 | if let environment = environment {
83 | application.environment = environment
84 | }
85 | if withRedirection == true {
86 | application.middleware.use(HTTPSRedirectMiddleware(allowedHosts: allowedHosts))
87 | }
88 | try routes(application)
89 | return try application.responder.respond(to: request).wait()
90 | }
91 |
92 | func routes(_ app: Application) throws {
93 | try app.register(collection: RouteController())
94 | }
95 |
96 | struct RouteController: RouteCollection {
97 | func boot(routes: RoutesBuilder) throws {
98 | routes.get(use: testing)
99 | }
100 | func testing(req: Request) throws -> String {
101 | return "Test"
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | range: "0...100"
3 | ignore:
4 | - "Tests/"
5 | - ".build/"
6 |
--------------------------------------------------------------------------------