lib/config, cmd/syncthing: Enforce localhost only connections

When the GUI/API is bound to localhost, we enforce that the Host header
looks like localhost. This can be disabled by setting
insecureSkipHostCheck in the GUI config.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3558
This commit is contained in:
Jakob Borg
2016-09-03 08:33:34 +00:00
committed by Audrius Butkevicius
parent 46a143e80e
commit 49910a1d85
3 changed files with 233 additions and 12 deletions

View File

@@ -16,6 +16,7 @@ import (
"net"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
@@ -460,7 +461,6 @@ func TestHTTPLogin(t *testing.T) {
if resp.StatusCode != http.StatusOK {
t.Errorf("Unexpected non-200 return code %d for authed request (ISO-8859-1)", resp.StatusCode)
}
}
func startHTTP(cfg *mockedConfig) (string, error) {
@@ -491,7 +491,12 @@ func startHTTP(cfg *mockedConfig) (string, error) {
if err != nil {
return "", fmt.Errorf("Weird address from API service: %v", err)
}
baseURL := fmt.Sprintf("http://127.0.0.1:%d", tcpAddr.Port)
host, _, _ := net.SplitHostPort(cfg.gui.RawAddress)
if host == "" || host == "0.0.0.0" {
host = "127.0.0.1"
}
baseURL := fmt.Sprintf("http://%s", net.JoinHostPort(host, strconv.Itoa(tcpAddr.Port)))
return baseURL, nil
}
@@ -666,3 +671,189 @@ func testConfigPost(data io.Reader) (*http.Response, error) {
req.Header.Set("X-API-Key", testAPIKey)
return cli.Do(req)
}
func TestHostCheck(t *testing.T) {
// An API service bound to localhost should reject non-localhost host Headers
cfg := new(mockedConfig)
cfg.gui.RawAddress = "127.0.0.1:0"
baseURL, err := startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
// A normal HTTP get to the localhost-bound service should succeed
resp, err := http.Get(baseURL)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Error("Regular HTTP get: expected 200 OK, not", resp.Status)
}
// A request with a suspicious Host header should fail
req, _ := http.NewRequest("GET", baseURL, nil)
req.Host = "example.com"
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusForbidden {
t.Error("Suspicious Host header: expected 403 Forbidden, not", resp.Status)
}
// A request with an explicit "localhost:8384" Host header should pass
req, _ = http.NewRequest("GET", baseURL, nil)
req.Host = "localhost:8384"
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Error("Explicit localhost:8384: expected 200 OK, not", resp.Status)
}
// A request with an explicit "localhost" Host header (no port) should pass
req, _ = http.NewRequest("GET", baseURL, nil)
req.Host = "localhost"
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Error("Explicit localhost: expected 200 OK, not", resp.Status)
}
// A server with InsecureSkipHostCheck set behaves differently
cfg = new(mockedConfig)
cfg.gui.RawAddress = "127.0.0.1:0"
cfg.gui.InsecureSkipHostCheck = true
baseURL, err = startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
// A request with a suspicious Host header should be allowed
req, _ = http.NewRequest("GET", baseURL, nil)
req.Host = "example.com"
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Error("Incorrect host header, check disabled: expected 200 OK, not", resp.Status)
}
// A server bound to a wildcard address also doesn't do the check
cfg = new(mockedConfig)
cfg.gui.RawAddress = "0.0.0.0:0"
cfg.gui.InsecureSkipHostCheck = true
baseURL, err = startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
// A request with a suspicious Host header should be allowed
req, _ = http.NewRequest("GET", baseURL, nil)
req.Host = "example.com"
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Error("Incorrect host header, wildcard bound: expected 200 OK, not", resp.Status)
}
// This should all work over IPv6 as well
cfg = new(mockedConfig)
cfg.gui.RawAddress = "[::1]:0"
baseURL, err = startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
// A normal HTTP get to the localhost-bound service should succeed
resp, err = http.Get(baseURL)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Error("Regular HTTP get (IPv6): expected 200 OK, not", resp.Status)
}
// A request with a suspicious Host header should fail
req, _ = http.NewRequest("GET", baseURL, nil)
req.Host = "example.com"
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusForbidden {
t.Error("Suspicious Host header (IPv6): expected 403 Forbidden, not", resp.Status)
}
// A request with an explicit "localhost:8384" Host header should pass
req, _ = http.NewRequest("GET", baseURL, nil)
req.Host = "localhost:8384"
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Error("Explicit localhost:8384 (IPv6): expected 200 OK, not", resp.Status)
}
}
func TestAddressIsLocalhost(t *testing.T) {
testcases := []struct {
address string
result bool
}{
// These are all valid localhost addresses
{"localhost", true},
{"::1", true},
{"127.0.0.1", true},
{"localhost:8080", true},
{"[::1]:8080", true},
{"127.0.0.1:8080", true},
// These are all non-localhost addresses
{"example.com", false},
{"example.com:8080", false},
{"192.0.2.10", false},
{"192.0.2.10:8080", false},
{"0.0.0.0", false},
{"0.0.0.0:8080", false},
{"::", false},
{"[::]:8080", false},
{":8080", false},
}
for _, tc := range testcases {
result := addressIsLocalhost(tc.address)
if result != tc.result {
t.Errorf("addressIsLocalhost(%q)=%v, expected %v", tc.address, result, tc.result)
}
}
}