Skip to content

Commit

Permalink
Merge pull request #29 from brokenhandsio/allowed-hosts
Browse files Browse the repository at this point in the history
Add Allowed Hosts to HTTPSRedirectMiddleware
  • Loading branch information
0xTim committed May 18, 2023
2 parents 732edf8 + 40d9968 commit e1369d2
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 16 deletions.
17 changes: 5 additions & 12 deletions .github/workflows/ci.yml
Expand Up @@ -3,21 +3,14 @@ on:
push:
pull_request:
jobs:
xenial:
test:
container:
image: vapor/swift:5.2-xenial
image: swift:5.8
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- run: swift test --enable-test-discovery --enable-code-coverage --sanitize=thread
bionic:
container:
image: vapor/swift:5.2-bionic
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Run Bionic Tests
run: swift test --enable-test-discovery --enable-code-coverage --sanitize=thread
- uses: actions/checkout@v3
- name: Run Tests
run: swift test --enable-code-coverage --sanitize=thread
- name: Setup container for codecov upload
run: apt-get update && apt-get install curl -y
- name: Process coverage file
Expand Down
8 changes: 8 additions & 0 deletions README.md
Expand Up @@ -413,6 +413,14 @@ To use the HTTPS Redirect Middleware, you can add the following line in **config
app.middleware.use(HTTPSRedirectMiddleware())
```

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:

```swift
app.middleware.use(HTTPSRedirectMiddleware(allowedHosts: ["www.brokenhands.io", "brokenhands.io", "static.brokenhands.io"))
```

Any attempts to redirect to another host, for example `attacker.com` will result in a **400 Bad Request** response.

## Server

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.
Expand Down
Expand Up @@ -2,7 +2,11 @@ import Vapor

public class HTTPSRedirectMiddleware: Middleware {

public init() {}
let allowedHosts: [String]?

public init(allowedHosts: [String]? = nil) {
self.allowedHosts = allowedHosts
}

public func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture<Response> {
if request.application.environment == .development {
Expand All @@ -17,8 +21,15 @@ public class HTTPSRedirectMiddleware: Middleware {
guard let host = request.headers.first(name: .host) else {
return request.eventLoop.makeFailedFuture(Abort(.badRequest))
}

if let allowedHosts = allowedHosts {
guard allowedHosts.contains(host) else {
return request.eventLoop.makeFailedFuture(Abort(.badRequest))
}
}

let httpsURL = "https://" + host + "\(request.url)"
return request.redirect(to: "\(httpsURL)", type: .permanent).encodeResponse(for: request)
return request.redirect(to: "\(httpsURL)", redirectType: .permanent).encodeResponse(for: request)
}
return next.respond(to: request)
}
Expand Down
22 changes: 20 additions & 2 deletions Tests/VaporSecurityHeadersTests/RedirectionTest.swift
Expand Up @@ -29,6 +29,24 @@ class RedirectionTest: XCTestCase {
let responseRedirected = try makeTestResponse(for: request, withRedirection: true)
XCTAssertEqual(expectedRedirectStatus, responseRedirected.status)
}

func testWithRedirectMiddlewareWithAllowedHost() throws {
let expectedRedirectStatus: HTTPStatus = HTTPResponseStatus(statusCode: 301, reasonPhrase: "Moved permanently")
request.headers.add(name: .host, value: "localhost:8080")
let responseRedirected = try makeTestResponse(for: request, withRedirection: true, allowedHosts: ["localhost:8080", "example.com"])
XCTAssertEqual(expectedRedirectStatus, responseRedirected.status)
}

func testWithRedirectMiddlewareWithDisallowedHost() throws {
let expectedOutcome: String = "Abort.400: Bad Request"
do {
request.headers.add(name: .host, value: "localhost:8080")
_ = try makeTestResponse(for: request, withRedirection: true, allowedHosts: ["localhost:8081", "example.com"])
} catch (let error) {
XCTAssertEqual(expectedOutcome, error.localizedDescription)
}
}

func testWithoutRedirectionMiddleware() throws {
let expectedNoRedirectStatus: HTTPStatus = HTTPResponseStatus(statusCode: 200, reasonPhrase: "Ok")
request.headers.add(name: .host, value: "localhost:8080")
Expand Down Expand Up @@ -59,13 +77,13 @@ class RedirectionTest: XCTestCase {
XCTAssertEqual(expectedStatus, response.status)
}

private func makeTestResponse(for request: Request, withRedirection: Bool, environment: Environment? = nil) throws -> Response {
private func makeTestResponse(for request: Request, withRedirection: Bool, environment: Environment? = nil, allowedHosts: [String]? = nil) throws -> Response {
application.middleware = Middlewares()
if let environment = environment {
application.environment = environment
}
if withRedirection == true {
application.middleware.use(HTTPSRedirectMiddleware())
application.middleware.use(HTTPSRedirectMiddleware(allowedHosts: allowedHosts))
}
try routes(application)
return try application.responder.respond(to: request).wait()
Expand Down

0 comments on commit e1369d2

Please sign in to comment.