Skip to content

Commit 2f6f04d

Browse files
authored
Security: limit data read from external sources (#3076)
1 parent 2c6dd95 commit 2f6f04d

File tree

6 files changed

+45
-16
lines changed

6 files changed

+45
-16
lines changed

auth/client/iam/client.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func (hb HTTPClient) OAuthAuthorizationServerMetadata(ctx context.Context, webDI
7070
var metadata oauth.AuthorizationServerMetadata
7171
var data []byte
7272

73-
if data, err = io.ReadAll(response.Body); err != nil {
73+
if data, err = core.LimitedReadAll(response.Body); err != nil {
7474
return nil, fmt.Errorf("unable to read response: %w", err)
7575
}
7676
if err = json.Unmarshal(data, &metadata); err != nil {
@@ -165,7 +165,7 @@ func (hb HTTPClient) AccessToken(ctx context.Context, tokenEndpoint string, data
165165
}
166166

167167
var responseData []byte
168-
if responseData, err = io.ReadAll(response.Body); err != nil {
168+
if responseData, err = core.LimitedReadAll(response.Body); err != nil {
169169
return token, fmt.Errorf("unable to read response: %w", err)
170170
}
171171
if err = json.Unmarshal(responseData, &token); err != nil {
@@ -202,7 +202,6 @@ func (hb HTTPClient) PostAuthorizationResponse(ctx context.Context, vp vc.Verifi
202202
}
203203

204204
func (hb HTTPClient) OpenIdConfiguration(ctx context.Context, serverURL string) (*oauth.OpenIDConfigurationMetadata, error) {
205-
206205
metadataURL, err := oauth.IssuerIdToWellKnown(serverURL, oauth.OpenIdConfigurationWellKnown, hb.strictMode)
207206
if err != nil {
208207
return nil, err
@@ -224,7 +223,7 @@ func (hb HTTPClient) OpenIdConfiguration(ctx context.Context, serverURL string)
224223
var metadata oauth.OpenIDConfigurationMetadata
225224
var data []byte
226225

227-
if data, err = io.ReadAll(response.Body); err != nil {
226+
if data, err = core.LimitedReadAll(response.Body); err != nil {
228227
return nil, fmt.Errorf("unable to read response: %w", err)
229228
}
230229
if err = json.Unmarshal(data, &metadata); err != nil {
@@ -261,7 +260,7 @@ func (hb HTTPClient) OpenIdCredentialIssuerMetadata(ctx context.Context, webDID
261260
var metadata oauth.OpenIDCredentialIssuerMetadata
262261
var data []byte
263262

264-
if data, err = io.ReadAll(response.Body); err != nil {
263+
if data, err = core.LimitedReadAll(response.Body); err != nil {
265264
return nil, fmt.Errorf("unable to read response: %w", err)
266265
}
267266
if err = json.Unmarshal(data, &metadata); err != nil {
@@ -300,7 +299,7 @@ func (hb HTTPClient) AccessTokenOid4vci(ctx context.Context, presentationDefinit
300299
}
301300

302301
var responseData []byte
303-
if responseData, err = io.ReadAll(response.Body); err != nil {
302+
if responseData, err = core.LimitedReadAll(response.Body); err != nil {
304303
return nil, fmt.Errorf("unable to read response: %w", err)
305304
}
306305

@@ -406,7 +405,7 @@ func (hb HTTPClient) doRequest(ctx context.Context, request *http.Request, targe
406405

407406
var data []byte
408407

409-
if data, err = io.ReadAll(response.Body); err != nil {
408+
if data, err = core.LimitedReadAll(response.Body); err != nil {
410409
return fmt.Errorf("unable to read response: %w", err)
411410
}
412411
if err = json.Unmarshal(data, &target); err != nil {

core/http_client.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,21 @@ import (
3434
// If the response body is longer than this, it will be truncated.
3535
const HttpResponseBodyLogClipAt = 200
3636

37+
// DefaultMaxHttpResponseSize is a default maximum size of an HTTP response body that will be read.
38+
// Very large or unbounded HTTP responses can cause denial-of-service, so it's good to limit how much data is read.
39+
// This of course heavily depends on the use case, but 1MB is a reasonable default.
40+
const DefaultMaxHttpResponseSize = 1024 * 1024
41+
42+
// LimitedReadAll reads the given reader until the DefaultMaxHttpResponseSize is reached.
43+
// It returns an error if more data is available than DefaultMaxHttpResponseSize.
44+
func LimitedReadAll(reader io.Reader) ([]byte, error) {
45+
result, err := io.ReadAll(io.LimitReader(reader, DefaultMaxHttpResponseSize+1))
46+
if len(result) > DefaultMaxHttpResponseSize {
47+
return nil, fmt.Errorf("data to read exceeds max. safety limit of %d bytes", DefaultMaxHttpResponseSize)
48+
}
49+
return result, err
50+
}
51+
3752
// HttpError describes an error returned when invoking a remote server.
3853
type HttpError struct {
3954
error
@@ -51,7 +66,7 @@ func TestResponseCode(expectedStatusCode int, response *http.Response) error {
5166
// It logs using the given logger, unless nil is passed.
5267
func TestResponseCodeWithLog(expectedStatusCode int, response *http.Response, log *logrus.Entry) error {
5368
if response.StatusCode != expectedStatusCode {
54-
responseData, _ := io.ReadAll(response.Body)
69+
responseData, _ := LimitedReadAll(response.Body)
5570
if log != nil {
5671
// Cut off the response body to 100 characters max to prevent logging of large responses
5772
responseBodyString := string(responseData)

core/http_client_test.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import (
2525
"github.com/sirupsen/logrus/hooks/test"
2626
"github.com/stretchr/testify/assert"
2727
"github.com/stretchr/testify/require"
28-
io2 "io"
28+
"io"
2929
stdHttp "net/http"
3030
"net/http/httptest"
3131
"net/url"
@@ -162,7 +162,7 @@ type readCloser []byte
162162

163163
func (r readCloser) Read(p []byte) (n int, err error) {
164164
copy(p, r)
165-
return len(r), io2.EOF
165+
return len(r), io.EOF
166166
}
167167

168168
func (r readCloser) Close() error {
@@ -174,3 +174,20 @@ func newErrorTokenBuilder() AuthorizationTokenGenerator {
174174
return "", errors.New("error")
175175
}
176176
}
177+
178+
func TestLimitedReadAll(t *testing.T) {
179+
t.Run("less than limit", func(t *testing.T) {
180+
data := strings.Repeat("a", 10)
181+
result, err := LimitedReadAll(strings.NewReader(data))
182+
183+
assert.NoError(t, err)
184+
assert.Equal(t, []byte(data), result)
185+
})
186+
t.Run("more than limit", func(t *testing.T) {
187+
data := strings.Repeat("a", DefaultMaxHttpResponseSize+1)
188+
result, err := LimitedReadAll(strings.NewReader(data))
189+
190+
assert.EqualError(t, err, "data to read exceeds max. safety limit of 1048576 bytes")
191+
assert.Nil(t, result)
192+
})
193+
}

vcr/openid4vci/issuer_client.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import (
2828
"github.com/nuts-foundation/nuts-node/auth/oauth"
2929
"github.com/nuts-foundation/nuts-node/core"
3030
"github.com/nuts-foundation/nuts-node/vcr/log"
31-
"io"
3231
"net/http"
3332
"net/http/httptrace"
3433
"net/url"
@@ -168,7 +167,7 @@ func httpDo(httpClient core.HTTPRequestDoer, httpRequest *http.Request, result i
168167
return fmt.Errorf("http request error: %w", err)
169168
}
170169
defer httpResponse.Body.Close()
171-
responseBody, err := io.ReadAll(httpResponse.Body)
170+
responseBody, err := core.LimitedReadAll(httpResponse.Body)
172171
if err != nil {
173172
return fmt.Errorf("read error (%s): %w", httpRequest.URL, err)
174173
}

vcr/revocation/statuslist2021_verifier.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222
"encoding/json"
2323
"errors"
2424
"fmt"
25-
"io"
2625
"net/http"
2726
"strconv"
2827
"time"
@@ -199,7 +198,7 @@ func (cs *StatusList2021) download(statusListCredential string) (*vc.VerifiableC
199198
Debug("Failed to close response body")
200199
}
201200
}()
202-
body, err := io.ReadAll(res.Body)
201+
body, err := core.LimitedReadAll(res.Body) // default minimum size is 16kb (PII entropy), so 1mb is already unlikely
203202
if res.StatusCode > 299 || err != nil {
204203
return nil, errors.Join(fmt.Errorf("fetching StatusList2021Credential from '%s' failed", statusListCredential), err)
205204
}

vdr/didweb/web.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ import (
2323
"errors"
2424
"fmt"
2525
"github.com/nuts-foundation/go-did/did"
26+
"github.com/nuts-foundation/nuts-node/core"
2627
"github.com/nuts-foundation/nuts-node/vdr/resolver"
27-
"io"
2828
"mime"
2929
"net/http"
3030
"time"
@@ -107,7 +107,7 @@ func (w Resolver) Resolve(id did.DID, _ *resolver.ResolveMetadata) (*did.Documen
107107
}
108108

109109
// Read document
110-
data, err := io.ReadAll(httpResponse.Body)
110+
data, err := core.LimitedReadAll(httpResponse.Body)
111111
if err != nil {
112112
return nil, nil, fmt.Errorf("did:web HTTP response read error: %w", err)
113113
}

0 commit comments

Comments
 (0)