-
Notifications
You must be signed in to change notification settings - Fork 38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: reject non-HTTPS originating requests #1002
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,7 @@ import ( | |
"github.com/cloudfoundry/cloud-service-broker/dbservice" | ||
"github.com/cloudfoundry/cloud-service-broker/internal/displaycatalog" | ||
"github.com/cloudfoundry/cloud-service-broker/internal/encryption" | ||
"github.com/cloudfoundry/cloud-service-broker/internal/httpsmiddleware" | ||
"github.com/cloudfoundry/cloud-service-broker/internal/infohandler" | ||
"github.com/cloudfoundry/cloud-service-broker/internal/storage" | ||
pakBroker "github.com/cloudfoundry/cloud-service-broker/pkg/broker" | ||
|
@@ -46,6 +47,7 @@ const ( | |
apiPasswordProp = "api.password" | ||
apiPortProp = "api.port" | ||
apiHostProp = "api.host" | ||
apiAllowHTTP = "api.allow.http" // Disables the middleware that redirects http traffic to https | ||
encryptionPasswords = "db.encryption.passwords" | ||
encryptionEnabled = "db.encryption.enabled" | ||
) | ||
|
@@ -79,6 +81,7 @@ func init() { | |
_ = viper.BindEnv(apiHostProp, "CSB_LISTENER_HOST") | ||
_ = viper.BindEnv(encryptionPasswords, "ENCRYPTION_PASSWORDS") | ||
_ = viper.BindEnv(encryptionEnabled, "ENCRYPTION_ENABLED") | ||
_ = viper.BindEnv(apiAllowHTTP, "CSB_LISTENER_ALLOW_HTTP") | ||
} | ||
|
||
func serve() { | ||
|
@@ -201,10 +204,11 @@ func startServer(registry pakBroker.BrokerRegistry, db *sql.DB, brokerapi http.H | |
} | ||
}) | ||
|
||
allowHTTP := len(viper.GetString(apiAllowHTTP)) != 0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: Is it accurate to say that setting |
||
port := viper.GetString(apiPortProp) | ||
host := viper.GetString(apiHostProp) | ||
logger.Info("Serving", lager.Data{"port": port}) | ||
_ = http.ListenAndServe(fmt.Sprintf("%s:%s", host, port), router) | ||
_ = http.ListenAndServe(fmt.Sprintf("%s:%s", host, port), httpsmiddleware.EnsureHTTPS(router, logger, allowHTTP)) | ||
} | ||
|
||
func labelName(label string) string { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
version: 1 | ||
name: alpha-service | ||
|
||
id: 76c5725c-b246-11eb-871f-ffc97563fbd0 | ||
description: description | ||
display_name: Alpha Service | ||
image_url: https://example.com/icon.jpg | ||
documentation_url: https://example.com | ||
support_url: https://example.com/support.html | ||
plans: | ||
- name: alpha | ||
id: 8b52a460-b246-11eb-a8f5-d349948e2480 | ||
description: Alpha plan | ||
display_name: Alpha | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
packversion: 1 | ||
name: fake-brokerpak | ||
version: 0.1.0 | ||
metadata: | ||
author: [email protected] | ||
platforms: | ||
- os: linux | ||
arch: amd64 | ||
- os: darwin | ||
arch: amd64 | ||
terraform_binaries: | ||
- name: tofu | ||
version: 1.6.0 | ||
source: https://github.com/opentofu/opentofu/archive/refs/tags/v1.6.0.zip | ||
service_definitions: | ||
- fake-service.yml |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package integrationtest_test | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
|
||
"github.com/cloudfoundry/cloud-service-broker/integrationtest/packer" | ||
"github.com/cloudfoundry/cloud-service-broker/internal/testdrive" | ||
. "github.com/onsi/ginkgo/v2" | ||
. "github.com/onsi/gomega" | ||
) | ||
|
||
var _ = Describe("HTTPS redirect", func() { | ||
var broker *testdrive.Broker | ||
|
||
BeforeEach(func() { | ||
brokerpak := must(packer.BuildBrokerpak(csb, fixtures("https-redirect"))) | ||
broker = must(testdrive.StartBroker(csb, brokerpak, database, testdrive.WithOutputs(GinkgoWriter, GinkgoWriter), testdrive.WithHTTPRedirect())) | ||
|
||
DeferCleanup(func() { | ||
Expect(broker.Stop()).To(Succeed()) | ||
cleanup(brokerpak) | ||
}) | ||
}) | ||
|
||
It("redirects HTTP traffic", func() { | ||
By("redirecting an HTTP connection") | ||
errRedirected := fmt.Errorf("redirected") | ||
client := http.Client{ | ||
CheckRedirect: func(*http.Request, []*http.Request) error { | ||
return errRedirected | ||
}, | ||
} | ||
|
||
_, err := client.Get(fmt.Sprintf("http://localhost:%d/info", broker.Port)) | ||
Expect(err).To(MatchError(errRedirected)) | ||
|
||
By("allowing a connection that originated as HTTPS") | ||
request, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:%d/info", broker.Port), nil) | ||
Expect(err).NotTo(HaveOccurred()) | ||
request.Header.Add("X-Forwarded-Proto", "https") | ||
response, err := client.Do(request) | ||
Expect(err).NotTo(HaveOccurred()) | ||
Expect(response).To(HaveHTTPStatus(http.StatusOK)) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
// Package httpsmiddleware is used to ensure that the broker only accepts HTTPS connections | ||
package httpsmiddleware | ||
|
||
import ( | ||
"net/http" | ||
"strings" | ||
|
||
"code.cloudfoundry.org/lager/v3" | ||
) | ||
|
||
func EnsureHTTPS(next http.Handler, logger lager.Logger, disable bool) http.Handler { | ||
if disable { | ||
logger.Info("https-redirect-disabled") | ||
return next | ||
} | ||
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
proto := r.Header.Values("X-Forwarded-Proto") | ||
if isHTTPS(proto) { | ||
next.ServeHTTP(w, r) | ||
return | ||
} | ||
|
||
location := "https://" + r.Host + r.URL.String() | ||
logger.Debug("redirecting-to-https", lager.Data{"to": location, "X-Forwarded-Proto": proto}) | ||
http.Redirect(w, r, location, http.StatusMovedPermanently) | ||
}) | ||
} | ||
|
||
// isHTTPS returns true when the protocol is definitely HTTPS | ||
// | ||
// From the docs: https://docs.vmware.com/en/VMware-Tanzu-Application-Service/4.0/tas-for-vms/http-routing.html | ||
// | ||
// Developers can configure their apps to reject insecure requests by inspecting the X-Forwarded-Proto | ||
// HTTP header on incoming traffic. The header may have multiple values represented as a comma-separated | ||
// list, so developers must ensure the app rejects traffic that includes any X-Forwarded-Proto values | ||
// that are not HTTPS. | ||
func isHTTPS(xForwardProto []string) bool { | ||
xForwardProto = strings.Split(strings.Join(xForwardProto, ","), ",") | ||
if len(xForwardProto) == 0 { | ||
return false | ||
} | ||
|
||
for _, proto := range xForwardProto { | ||
if proto != "https" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue: the HTTP specification allows for optional white-space around the comma separator in a list of header field values. If I add this test it fails. The ServeHTTPCallCount is 0:
EnsureHTTPS() [It] allows an HTTPS connection with with-spaces
/home/andrea/workspace/csb/cloud-service-broker/internal/httpsmiddleware/ensurehttps_test.go:88
[FAILED] Expected
<int>: 0
to equal
<int>: 1
In [It] at: /home/andrea/workspace/csb/cloud-service-broker/internal/httpsmiddleware/ensurehttps_test.go:95 @ 04/10/24 15:13:23.18
------------------------------
Summarizing 1 Failure:
[FAIL] EnsureHTTPS() [It] allows an HTTPS connection with with-spaces
/home/andrea/workspace/csb/cloud-service-broker/internal/httpsmiddleware/ensurehttps_test.go:95
|
||
return false | ||
} | ||
} | ||
|
||
return true | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
package httpsmiddleware_test | ||
|
||
import ( | ||
"net/http" | ||
"net/url" | ||
|
||
"code.cloudfoundry.org/lager/v3/lagertest" | ||
"github.com/cloudfoundry/cloud-service-broker/internal/httpsmiddleware" | ||
"github.com/cloudfoundry/cloud-service-broker/internal/httpsmiddleware/httpsmiddlewarefakes" | ||
. "github.com/onsi/ginkgo/v2" | ||
. "github.com/onsi/gomega" | ||
) | ||
|
||
//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate | ||
//counterfeiter:generate net/http.Handler | ||
//counterfeiter:generate net/http.ResponseWriter | ||
|
||
var _ = Describe("EnsureHTTPS()", func() { | ||
const xForwardProto = "X-Forwarded-Proto" | ||
|
||
var ( | ||
logger *lagertest.TestLogger | ||
fakeNext *httpsmiddlewarefakes.FakeHandler | ||
fakeResponseWriter *httpsmiddlewarefakes.FakeResponseWriter | ||
fakeResponseHeader http.Header | ||
fakeURL url.URL | ||
) | ||
|
||
BeforeEach(func() { | ||
logger = lagertest.NewTestLogger("test") | ||
fakeNext = &httpsmiddlewarefakes.FakeHandler{} | ||
fakeURL = url.URL{ | ||
Scheme: "http", | ||
Host: "fake-host", | ||
Path: "fake-path", | ||
} | ||
|
||
fakeResponseWriter = &httpsmiddlewarefakes.FakeResponseWriter{} | ||
fakeResponseHeader = make(http.Header) | ||
fakeResponseWriter.HeaderReturns(fakeResponseHeader) | ||
}) | ||
|
||
It("does nothing when disabled", func() { | ||
httpsmiddleware.EnsureHTTPS(fakeNext, logger, true).ServeHTTP(fakeResponseWriter, &http.Request{ | ||
Method: http.MethodHead, | ||
URL: &fakeURL, | ||
Header: http.Header{xForwardProto: []string{"http"}}, | ||
}) | ||
|
||
Expect(fakeNext.ServeHTTPCallCount()).To(Equal(1)) | ||
Expect(fakeResponseWriter.WriteHeaderCallCount()).To(BeZero()) | ||
}) | ||
|
||
DescribeTable( | ||
"redirects non-HTTPS connections", | ||
func(header http.Header) { | ||
httpsmiddleware.EnsureHTTPS(fakeNext, logger, false).ServeHTTP(fakeResponseWriter, &http.Request{ | ||
Method: http.MethodHead, | ||
URL: &fakeURL, | ||
Header: header, | ||
}) | ||
|
||
Expect(fakeNext.ServeHTTPCallCount()).To(BeZero()) | ||
Expect(fakeResponseWriter.WriteHeaderCallCount()).To(Equal(1)) | ||
Expect(fakeResponseWriter.WriteHeaderArgsForCall(0)).To(Equal(http.StatusMovedPermanently)) | ||
Expect(fakeResponseHeader.Get("Location")).To(Equal("https://http://fake-host/fake-path")) | ||
|
||
Expect(logger.Buffer().Contents()).To(ContainSubstring("redirecting-to-https")) | ||
}, | ||
Entry("empty", nil), | ||
Entry("http", http.Header{xForwardProto: []string{"http"}}), | ||
Entry("other", http.Header{xForwardProto: []string{"foo"}}), | ||
Entry("https with extra (comma separated)", http.Header{xForwardProto: []string{"https,foo"}}), | ||
Entry("https with extra (slice)", http.Header{xForwardProto: []string{"https", "foo"}}), | ||
) | ||
|
||
It("allows an HTTPS connection", func() { | ||
httpsmiddleware.EnsureHTTPS(fakeNext, logger, false).ServeHTTP(fakeResponseWriter, &http.Request{ | ||
Method: http.MethodHead, | ||
URL: &fakeURL, | ||
Header: http.Header{xForwardProto: []string{"https"}}, | ||
}) | ||
|
||
Expect(fakeNext.ServeHTTPCallCount()).To(Equal(1)) | ||
Expect(fakeResponseWriter.WriteHeaderCallCount()).To(BeZero()) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package httpsmiddleware_test | ||
|
||
import ( | ||
"testing" | ||
|
||
. "github.com/onsi/ginkgo/v2" | ||
. "github.com/onsi/gomega" | ||
) | ||
|
||
func TestHTTPSMiddleware(t *testing.T) { | ||
RegisterFailHandler(Fail) | ||
RunSpecs(t, "HTTPS middleware Suite") | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: Why not use
viper.GetBool
instead ofviper.GetString
? This way, we could rely on Viper to cast the value.