Skip to content
Permalink
Browse files

fix #636: mitingate long header attack

License: MIT
Signed-off-by: Alexey Novikov <alexey@novikov.io>
  • Loading branch information...
alekswn committed Mar 7, 2019
1 parent 5081310 commit 53d624e701af8abff7b37361874af3159657262d
@@ -13,8 +13,9 @@ import (
)

const (
configKey = "ipfsproxy"
envConfigKey = "cluster_ipfsproxy"
configKey = "ipfsproxy"
envConfigKey = "cluster_ipfsproxy"
minMaxHeaderBytes = 4096
)

// Default values for Config.
@@ -28,6 +29,7 @@ const (
DefaultIdleTimeout = 60 * time.Second
DefaultExtractHeadersPath = "/api/v0/version"
DefaultExtractHeadersTTL = 5 * time.Minute
DefaultMaxHeaderBytes = minMaxHeaderBytes
)

// Config allows to customize behaviour of IPFSProxy.
@@ -53,6 +55,10 @@ type Config struct {
// Maximum duration before timing out write of the response
WriteTimeout time.Duration

// Maximum cumulative size of HTTP request headers in bytes
// accepted by the server
MaxHeaderBytes int

// Server-side amount of time a Keep-Alive connection will be
// kept idle before being reused
IdleTimeout time.Duration
@@ -88,6 +94,7 @@ type jsonConfig struct {
ReadHeaderTimeout string `json:"read_header_timeout"`
WriteTimeout string `json:"write_timeout"`
IdleTimeout string `json:"idle_timeout"`
MaxHeaderBytes int `json:"max_header_bytes"`

ExtractHeadersExtra []string `json:"extract_headers_extra,omitempty"`
ExtractHeadersPath string `json:"extract_headers_path,omitempty"`
@@ -118,6 +125,7 @@ func (cfg *Config) Default() error {
cfg.ExtractHeadersExtra = nil
cfg.ExtractHeadersPath = DefaultExtractHeadersPath
cfg.ExtractHeadersTTL = DefaultExtractHeadersTTL
cfg.MaxHeaderBytes = DefaultMaxHeaderBytes

return nil
}
@@ -173,6 +181,10 @@ func (cfg *Config) Validate() error {
err = errors.New("ipfsproxy.extract_headers_ttl is invalid")
}

if cfg.MaxHeaderBytes < minMaxHeaderBytes {
err = fmt.Errorf("ipfsproxy.max_header_size must be greater or equal to %d", minMaxHeaderBytes)
}

return err
}

@@ -219,6 +231,12 @@ func (cfg *Config) applyJSONConfig(jcfg *jsonConfig) error {
return err
}

if jcfg.MaxHeaderBytes == 0 {
cfg.MaxHeaderBytes = DefaultMaxHeaderBytes
} else {
cfg.MaxHeaderBytes = jcfg.MaxHeaderBytes
}

if extra := jcfg.ExtractHeadersExtra; extra != nil && len(extra) > 0 {
cfg.ExtractHeadersExtra = extra
}
@@ -255,6 +273,7 @@ func (cfg *Config) toJSONConfig() (jcfg *jsonConfig, err error) {
jcfg.ReadHeaderTimeout = cfg.ReadHeaderTimeout.String()
jcfg.WriteTimeout = cfg.WriteTimeout.String()
jcfg.IdleTimeout = cfg.IdleTimeout.String()
jcfg.MaxHeaderBytes = cfg.MaxHeaderBytes
jcfg.NodeHTTPS = cfg.NodeHTTPS

jcfg.ExtractHeadersExtra = cfg.ExtractHeadersExtra
@@ -14,13 +14,22 @@ var cfgJSON = []byte(`
"read_timeout": "10m0s",
"read_header_timeout": "5s",
"write_timeout": "10m0s",
"idle_timeout": "1m0s",
"idle_timeout": "1m0s",
"max_header_bytes": 16384,
"extract_headers_extra": [],
"extract_headers_path": "/api/v0/version",
"extract_headers_ttl": "5m"
}
`)

func TestLoadEmptyJSON(t *testing.T) {
cfg := &Config{}
err := cfg.LoadJSON([]byte(`{}`))
if err != nil {
t.Fatal(err)
}
}

func TestLoadJSON(t *testing.T) {
cfg := &Config{}
err := cfg.LoadJSON(cfgJSON)
@@ -63,6 +72,14 @@ func TestLoadJSON(t *testing.T) {
if err == nil {
t.Error("expected error in extract_headers_ttl")
}
j = &jsonConfig{}
json.Unmarshal(cfgJSON, j)
j.MaxHeaderBytes = minMaxHeaderBytes - 1
tst, _ = json.Marshal(j)
err = cfg.LoadJSON(tst)
if err == nil {
t.Error("expected error in extract_headers_ttl")
}
}

func TestToJSON(t *testing.T) {
@@ -154,6 +154,7 @@ func New(cfg *Config) (*Server, error) {
ReadHeaderTimeout: cfg.ReadHeaderTimeout,
IdleTimeout: cfg.IdleTimeout,
Handler: handler,
MaxHeaderBytes: cfg.MaxHeaderBytes,
}

// See: https://github.com/ipfs/go-ipfs/issues/5168
@@ -24,14 +24,12 @@ func init() {
_ = logging.Logger
}

func testIPFSProxy(t *testing.T) (*Server, *test.IpfsMock) {
func testIPFSProxyWithConfig(t *testing.T, cfg *Config) (*Server, *test.IpfsMock) {
mock := test.NewIpfsMock()
nodeMAddr, _ := ma.NewMultiaddr(fmt.Sprintf("/ip4/%s/tcp/%d",
mock.Addr, mock.Port))
proxyMAddr, _ := ma.NewMultiaddr("/ip4/127.0.0.1/tcp/0")

cfg := &Config{}
cfg.Default()
cfg.NodeAddr = nodeMAddr
cfg.ListenAddr = proxyMAddr
cfg.ExtractHeadersExtra = []string{
@@ -49,6 +47,12 @@ func testIPFSProxy(t *testing.T) (*Server, *test.IpfsMock) {
return proxy, mock
}

func testIPFSProxy(t *testing.T) (*Server, *test.IpfsMock) {
cfg := &Config{}
cfg.Default()
return testIPFSProxyWithConfig(t, cfg)
}

func TestIPFSProxyVersion(t *testing.T) {
ctx := context.Background()
proxy, mock := testIPFSProxy(t)
@@ -617,3 +621,42 @@ func TestHeaderExtraction(t *testing.T) {
t.Error("should have refreshed the headers after TTL")
}
}

func TestAttackHeaderSize(t *testing.T) {
const testHeaderSize = minMaxHeaderBytes * 4
ctx := context.Background()
cfg := &Config{}
cfg.Default()
cfg.MaxHeaderBytes = testHeaderSize
proxy, mock := testIPFSProxyWithConfig(t, cfg)
defer mock.Close()
defer proxy.Shutdown(ctx)

type testcase struct {
headerSize int
expectedStatus int
}
testcases := []testcase{
testcase{testHeaderSize / 2, http.StatusNotFound},
testcase{testHeaderSize * 2, http.StatusRequestHeaderFieldsTooLarge},
}

req, err := http.NewRequest("POST", fmt.Sprintf("%s/foo", proxyURL(proxy)), nil)
if err != nil {
t.Fatal(err)
}
for _, tc := range testcases {
for size := 0; size < tc.headerSize; size += 8 {
req.Header.Add("Foo", "bar")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal("should forward requests to ipfs host: ", err)
}
res.Body.Close()
if res.StatusCode != tc.expectedStatus {
t.Errorf("proxy returned unexpected status %d, expected status code was %d",
res.StatusCode, tc.expectedStatus)
}
}
}
@@ -23,13 +23,16 @@ import (
const configKey = "restapi"
const envConfigKey = "cluster_restapi"

const minMaxHeaderBytes = 4096

// These are the default values for Config
const (
DefaultHTTPListenAddr = "/ip4/127.0.0.1/tcp/9094"
DefaultReadTimeout = 0
DefaultReadHeaderTimeout = 5 * time.Second
DefaultWriteTimeout = 0
DefaultIdleTimeout = 120 * time.Second
DefaultMaxHeaderBytes = minMaxHeaderBytes
)

// These are the default values for Config.
@@ -89,6 +92,10 @@ type Config struct {
// kept idle before being reused
IdleTimeout time.Duration

// Maximum cumulative size of HTTP request headers in bytes
// accepted by the server
MaxHeaderBytes int

// Listen address for the Libp2p REST API endpoint.
Libp2pListenAddr ma.Multiaddr

@@ -125,6 +132,7 @@ type jsonConfig struct {
ReadHeaderTimeout string `json:"read_header_timeout"`
WriteTimeout string `json:"write_timeout"`
IdleTimeout string `json:"idle_timeout"`
MaxHeaderBytes int `json:"max_header_bytes"`

Libp2pListenMultiaddress string `json:"libp2p_listen_multiaddress,omitempty"`
ID string `json:"id,omitempty"`
@@ -158,6 +166,7 @@ func (cfg *Config) Default() error {
cfg.ReadHeaderTimeout = DefaultReadHeaderTimeout
cfg.WriteTimeout = DefaultWriteTimeout
cfg.IdleTimeout = DefaultIdleTimeout
cfg.MaxHeaderBytes = DefaultMaxHeaderBytes

// libp2p
cfg.ID = ""
@@ -208,6 +217,8 @@ func (cfg *Config) Validate() error {
return errors.New("restapi.write_timeout is invalid")
case cfg.IdleTimeout < 0:
return errors.New("restapi.idle_timeout invalid")
case cfg.MaxHeaderBytes < minMaxHeaderBytes:
return fmt.Errorf("restapi.max_header_bytes must be not less then %d", minMaxHeaderBytes)
case cfg.BasicAuthCreds != nil && len(cfg.BasicAuthCreds) == 0:
return errors.New("restapi.basic_auth_creds should be null or have at least one entry")
case (cfg.pathSSLCertFile != "" || cfg.pathSSLKeyFile != "") && cfg.TLS == nil:
@@ -280,6 +291,12 @@ func (cfg *Config) loadHTTPOptions(jcfg *jsonConfig) error {
return err
}

if jcfg.MaxHeaderBytes == 0 {
cfg.MaxHeaderBytes = DefaultMaxHeaderBytes
} else {
cfg.MaxHeaderBytes = jcfg.MaxHeaderBytes
}

// CORS
cfg.CORSAllowedOrigins = jcfg.CORSAllowedOrigins
cfg.CORSAllowedMethods = jcfg.CORSAllowedMethods
@@ -390,6 +407,7 @@ func (cfg *Config) toJSONConfig() (jcfg *jsonConfig, err error) {
ReadHeaderTimeout: cfg.ReadHeaderTimeout.String(),
WriteTimeout: cfg.WriteTimeout.String(),
IdleTimeout: cfg.IdleTimeout.String(),
MaxHeaderBytes: cfg.MaxHeaderBytes,
BasicAuthCreds: cfg.BasicAuthCreds,
Headers: cfg.Headers,
CORSAllowedOrigins: cfg.CORSAllowedOrigins,
@@ -20,7 +20,8 @@ var cfgJSON = []byte(`
"read_timeout": "30s",
"read_header_timeout": "5s",
"write_timeout": "1m0s",
"idle_timeout": "2m0s",
"idle_timeout": "2m0s",
"max_header_bytes": 16384,
"basic_auth_credentials": null,
"cors_allowed_origins": ["myorigin"],
"cors_allowed_methods": ["GET"],
@@ -31,6 +32,14 @@ var cfgJSON = []byte(`
}
`)

func TestLoadEmptyJSON(t *testing.T) {
cfg := &Config{}
err := cfg.LoadJSON([]byte(`{}`))
if err != nil {
t.Fatal(err)
}
}

func TestLoadJSON(t *testing.T) {
cfg := &Config{}
err := cfg.LoadJSON(cfgJSON)
@@ -108,6 +117,15 @@ func TestLoadJSON(t *testing.T) {
if err == nil {
t.Error("expected error with private key")
}

j = &jsonConfig{}
json.Unmarshal(cfgJSON, j)
j.MaxHeaderBytes = minMaxHeaderBytes - 1
tst, _ = json.Marshal(j)
err = cfg.LoadJSON(tst)
if err == nil {
t.Error("expected error with MaxHeaderBytes")
}
}

func TestApplyEnvVars(t *testing.T) {
@@ -137,12 +137,14 @@ func NewAPIWithHost(ctx context.Context, cfg *Config, h host.Host) (*API, error)
WriteTimeout: cfg.WriteTimeout,
IdleTimeout: cfg.IdleTimeout,
Handler: handler,
MaxHeaderBytes: cfg.MaxHeaderBytes,
}

// See: https://github.com/ipfs/go-ipfs/issues/5168
// See: https://github.com/ipfs/ipfs-cluster/issues/548
// on why this is re-enabled.
s.SetKeepAlivesEnabled(true)
s.MaxHeaderBytes = cfg.MaxHeaderBytes

ctx, cancel := context.WithCancel(ctx)

0 comments on commit 53d624e

Please sign in to comment.
You can’t perform that action at this time.