Skip to content

Commit b87a7ed

Browse files
authored
Merge pull request #1201 from courageJ/fix-response-body-limits
Enforce response body limits in kubelet-to-gcm
2 parents 3dd4a0e + 9837a33 commit b87a7ed

7 files changed

Lines changed: 307 additions & 6 deletions

File tree

kubelet-to-gcm/monitor/config/initialize.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,20 @@ package config
1818

1919
import (
2020
"fmt"
21-
"io/ioutil"
2221
"net/http"
2322
"strings"
2423
"time"
2524

2625
"github.com/GoogleCloudPlatform/k8s-stackdriver/kubelet-to-gcm/monitor"
26+
"github.com/GoogleCloudPlatform/k8s-stackdriver/kubelet-to-gcm/monitor/util"
2727
)
2828

2929
const (
3030
gceMetaDataEndpoint = "http://169.254.169.254"
3131
gceMetaDataPrefix = "/computeMetadata/v1"
32+
// maxResponseBodySize is the maximum size of the response body we will read.
33+
// 10MB is more than enough for GCE metadata responses.
34+
maxResponseBodySize = 10 * 1024 * 1024
3235
)
3336

3437
// NewConfigs returns the SourceConfigs for all monitored endpoints, and
@@ -117,7 +120,7 @@ func getGCEMetaData(uri string) ([]byte, error) {
117120
return nil, fmt.Errorf("Failed request %q for GCE metadata: %v", uri, err)
118121
}
119122
defer resp.Body.Close()
120-
body, err := ioutil.ReadAll(resp.Body)
123+
body, err := util.ReadWithLimit(resp.Body, maxResponseBodySize)
121124
if err != nil {
122125
return nil, fmt.Errorf("Failed to read body for request %q for GCE metadata: %v", uri, err)
123126
}

kubelet-to-gcm/monitor/controller/client.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ package controller
1919
import (
2020
"fmt"
2121
"io"
22-
"io/ioutil"
2322
"net/http"
2423
"net/url"
2524
"strings"
2625

26+
"github.com/GoogleCloudPlatform/k8s-stackdriver/kubelet-to-gcm/monitor/util"
2727
"github.com/prometheus/common/expfmt"
2828
"github.com/prometheus/common/model"
2929
)
@@ -72,6 +72,12 @@ func NewMetrics(body []byte) (*Metrics, error) {
7272
return metrics, nil
7373
}
7474

75+
const (
76+
// maxResponseBodySize is the maximum size of the response body we will read.
77+
// 10MB is more than enough for most prometheus metrics responses.
78+
maxResponseBodySize = 10 * 1024 * 1024
79+
)
80+
7581
// Client queries metrics from the controller process.
7682
type Client struct {
7783
client *http.Client
@@ -102,7 +108,7 @@ func (c *Client) doRequestAndParse(req *http.Request) (*Metrics, error) {
102108
return nil, fmt.Errorf("empty response from controller")
103109
}
104110
defer response.Body.Close()
105-
body, err := ioutil.ReadAll(response.Body)
111+
body, err := util.ReadWithLimit(response.Body, maxResponseBodySize)
106112
if err != nil {
107113
return nil, fmt.Errorf("failed to read response body - %v", err)
108114
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
Copyright 2017 Google Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controller
18+
19+
import (
20+
"net/http"
21+
"net/http/httptest"
22+
"net/url"
23+
"strings"
24+
"testing"
25+
26+
"github.com/GoogleCloudPlatform/k8s-stackdriver/kubelet-to-gcm/monitor/util"
27+
)
28+
29+
func TestDoRequestAndParse_SizeLimit(t *testing.T) {
30+
// Create a mock server that returns a response larger than maxResponseBodySize.
31+
largeDataSize := maxResponseBodySize + 1024
32+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
33+
// Write more than maxResponseBodySize bytes.
34+
data := make([]byte, largeDataSize)
35+
w.Write(data)
36+
}))
37+
defer ts.Close()
38+
39+
u, _ := url.Parse(ts.URL)
40+
client := &Client{
41+
client: ts.Client(),
42+
metricsURL: u,
43+
}
44+
45+
req, _ := http.NewRequest("GET", ts.URL, nil)
46+
_, err := client.doRequestAndParse(req)
47+
48+
if err == nil {
49+
t.Fatal("Expected error due to size limit, but got none")
50+
}
51+
52+
if !strings.Contains(err.Error(), util.ErrBodyTooLarge.Error()) {
53+
t.Errorf("Expected error containing %q, got %v", util.ErrBodyTooLarge, err)
54+
}
55+
}
56+
57+
func TestDoRequestAndParse_Success(t *testing.T) {
58+
metricsData := `
59+
# HELP node_collector_evictions_number Number of evictions
60+
# TYPE node_collector_evictions_number counter
61+
node_collector_evictions_number 10
62+
# HELP process_start_time_seconds Start time
63+
# TYPE process_start_time_seconds gauge
64+
process_start_time_seconds 1234567890
65+
`
66+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
67+
w.Write([]byte(metricsData))
68+
}))
69+
defer ts.Close()
70+
71+
u, _ := url.Parse(ts.URL)
72+
client := &Client{
73+
client: ts.Client(),
74+
metricsURL: u,
75+
}
76+
77+
req, _ := http.NewRequest("GET", ts.URL, nil)
78+
metrics, err := client.doRequestAndParse(req)
79+
80+
if err != nil {
81+
t.Fatalf("Unexpected error: %v", err)
82+
}
83+
84+
if metrics.NodeEvictions != 10 {
85+
t.Errorf("Expected 10 evictions, got %d", metrics.NodeEvictions)
86+
}
87+
if metrics.CreateTime != 1234567890 {
88+
t.Errorf("Expected 1234567890 create time, got %d", metrics.CreateTime)
89+
}
90+
}

kubelet-to-gcm/monitor/kubelet/client.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,19 @@ package kubelet
1919
import (
2020
"encoding/json"
2121
"fmt"
22-
"io/ioutil"
2322
"net/http"
2423
"net/url"
2524

25+
"github.com/GoogleCloudPlatform/k8s-stackdriver/kubelet-to-gcm/monitor/util"
2626
stats "k8s.io/kubelet/pkg/apis/stats/v1alpha1"
2727
)
2828

29+
const (
30+
// maxResponseBodySize is the maximum size of the response body we will read.
31+
// 50MB should be enough for any Kubelet stats summary response.
32+
maxResponseBodySize = 50 * 1024 * 1024
33+
)
34+
2935
// Client contains all the information and methods to encapsulate
3036
// communication with the Kubelet.
3137
type Client struct {
@@ -61,7 +67,7 @@ func (k *Client) doRequestAndUnmarshal(client *http.Client, req *http.Request, v
6167
return fmt.Errorf("empty response from kubelet")
6268
}
6369
defer response.Body.Close()
64-
body, err := ioutil.ReadAll(response.Body)
70+
body, err := util.ReadWithLimit(response.Body, maxResponseBodySize)
6571
if err != nil {
6672
return fmt.Errorf("failed to read response body - %v", err)
6773
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
Copyright 2017 Google Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package kubelet
18+
19+
import (
20+
"encoding/json"
21+
"net/http"
22+
"net/http/httptest"
23+
"net/url"
24+
"strings"
25+
"testing"
26+
27+
"github.com/GoogleCloudPlatform/k8s-stackdriver/kubelet-to-gcm/monitor/util"
28+
stats "k8s.io/kubelet/pkg/apis/stats/v1alpha1"
29+
)
30+
31+
func TestDoRequestAndUnmarshal_SizeLimit(t *testing.T) {
32+
largeDataSize := maxResponseBodySize + 1024
33+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
34+
data := make([]byte, largeDataSize)
35+
w.Write(data)
36+
}))
37+
defer ts.Close()
38+
39+
u, _ := url.Parse(ts.URL)
40+
k := &Client{
41+
client: ts.Client(),
42+
summaryURL: u,
43+
}
44+
45+
req, _ := http.NewRequest("GET", ts.URL, nil)
46+
var value stats.Summary
47+
err := k.doRequestAndUnmarshal(ts.Client(), req, &value)
48+
49+
if err == nil {
50+
t.Fatal("Expected error due to size limit, but got none")
51+
}
52+
53+
if !strings.Contains(err.Error(), util.ErrBodyTooLarge.Error()) {
54+
t.Errorf("Expected error containing %q, got %v", util.ErrBodyTooLarge, err)
55+
}
56+
}
57+
58+
func TestDoRequestAndUnmarshal_Success(t *testing.T) {
59+
summary := &stats.Summary{
60+
Node: stats.NodeStats{
61+
NodeName: "test-node",
62+
},
63+
}
64+
data, _ := json.Marshal(summary)
65+
66+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
67+
w.Write(data)
68+
}))
69+
defer ts.Close()
70+
71+
u, _ := url.Parse(ts.URL)
72+
k := &Client{
73+
client: ts.Client(),
74+
summaryURL: u,
75+
}
76+
77+
req, _ := http.NewRequest("GET", ts.URL, nil)
78+
var result stats.Summary
79+
err := k.doRequestAndUnmarshal(ts.Client(), req, &result)
80+
81+
if err != nil {
82+
t.Fatalf("Unexpected error: %v", err)
83+
}
84+
85+
if result.Node.NodeName != "test-node" {
86+
t.Errorf("Expected test-node, got %s", result.Node.NodeName)
87+
}
88+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
Copyright 2026 Google Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package util
18+
19+
import (
20+
"errors"
21+
"io"
22+
)
23+
24+
var (
25+
// ErrBodyTooLarge is returned when the response body exceeds the configured limit.
26+
ErrBodyTooLarge = errors.New("response body too large")
27+
)
28+
29+
// ReadWithLimit reads from r until EOF or the limit is reached.
30+
// If the limit is exceeded, it returns ErrBodyTooLarge.
31+
func ReadWithLimit(r io.Reader, limit int64) ([]byte, error) {
32+
// Read up to limit + 1 bytes to detect overflow.
33+
data, err := io.ReadAll(io.LimitReader(r, limit+1))
34+
if err != nil {
35+
return nil, err
36+
}
37+
if int64(len(data)) > limit {
38+
return nil, ErrBodyTooLarge
39+
}
40+
return data, nil
41+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
Copyright 2026 Google Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package util
18+
19+
import (
20+
"bytes"
21+
"strings"
22+
"testing"
23+
)
24+
25+
func TestReadWithLimit(t *testing.T) {
26+
testCases := []struct {
27+
name string
28+
input string
29+
limit int64
30+
expected string
31+
expectedErr error
32+
}{
33+
{
34+
name: "under limit",
35+
input: "short",
36+
limit: 10,
37+
expected: "short",
38+
expectedErr: nil,
39+
},
40+
{
41+
name: "at limit",
42+
input: "exactlimit",
43+
limit: 10,
44+
expected: "exactlimit",
45+
expectedErr: nil,
46+
},
47+
{
48+
name: "over limit",
49+
input: "this is way too long",
50+
limit: 10,
51+
expected: "",
52+
expectedErr: ErrBodyTooLarge,
53+
},
54+
}
55+
56+
for _, tc := range testCases {
57+
t.Run(tc.name, func(t *testing.T) {
58+
got, err := ReadWithLimit(strings.NewReader(tc.input), tc.limit)
59+
if err != tc.expectedErr {
60+
t.Fatalf("Expected error %v, got %v", tc.expectedErr, err)
61+
}
62+
if tc.expectedErr == nil && !bytes.Equal(got, []byte(tc.expected)) {
63+
t.Errorf("Expected data %q, got %q", tc.expected, string(got))
64+
}
65+
})
66+
}
67+
}

0 commit comments

Comments
 (0)