Skip to content

Apply LimitedReader structs to all io.ReadAll calls.#8834

Open
ezekiel wants to merge 4 commits into
mainfrom
ezekiel/use-limited-reader
Open

Apply LimitedReader structs to all io.ReadAll calls.#8834
ezekiel wants to merge 4 commits into
mainfrom
ezekiel/use-limited-reader

Conversation

@ezekiel

@ezekiel ezekiel commented Jul 1, 2026

Copy link
Copy Markdown
Member

Fixes #8823

@ezekiel ezekiel requested a review from a team as a code owner July 1, 2026 16:21
@ezekiel ezekiel requested a review from aarongable July 1, 2026 16:21

@aarongable aarongable left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High-level comments:

  1. Let's not bother in all the test files (definitely foo_test.go, and maybe test/blah at your discretion)
  2. Let's crank down the limit for most of these (100MB is too many)
  3. Let's crank up the limit specifically for anything reading CRLs, since those could exceed 100MB in a mass revocation event
  4. Maybe a core.ReadAtMost(reader, limit) helper with accompanying core.DefaultMaxRead const? Also up to you.

ezekiel added 2 commits July 2, 2026 18:14
Reduce to still very generous ~300K normal reader size.
Increase to ~1G CRL reader size.
@ezekiel

ezekiel commented Jul 2, 2026

Copy link
Copy Markdown
Member Author

This reduced set of changes gets us good coverage for production code that we can deploy with lower impact risks. And we still have the option to put up a second iteration with a core helper.

@ezekiel ezekiel requested a review from aarongable July 2, 2026 22:11

@beautifulentropy beautifulentropy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is looking pretty good! My big overarching comment is that io.LimitedReader is weird. On its face you would assume that a read over the maximum bytes would result in an error, but it just returns an EOF to io.ReadAll() and since io.ReadAll() correctly treats EOF as "the input ended" an oversized body is silently truncated.

This is problematic in a few cases. A lot of the time the bytes we're reading are used as input to the next step. Best case is that the next call detects this and some class of "x is malformed" error ends up in our logs. Not terrible but not a strong signal that someone is attempting to exploit us. The worse case is when the data itself is still valid when truncated (YAML, CSV, etc.), here, if we're extremely unlucky, we'll silently continue with bad data.

Instead, we should use this as a chance set up a tripwire. Write a new core package function; a limited reader that detects when the input is expected-max-size + 1 and returns an error that we can fail on.

Comment thread bdns/dns.go
}

b, err := io.ReadAll(resp.Body)
b, err := io.ReadAll(&io.LimitedReader{R: resp.Body, N: 300_000})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per https://datatracker.ietf.org/doc/html/rfc8484#section-6, I think anything over 65_535 would be malformed.

Suggested change
b, err := io.ReadAll(&io.LimitedReader{R: resp.Body, N: 300_000})
b, err := io.ReadAll(&io.LimitedReader{R: resp.Body, N: 65_535})

Comment thread cmd/shell.go
validate.RegisterCustomTypeFunc(config.DurationCustomTypeFunc, config.Duration{})

inBytes, err := io.ReadAll(in)
inBytes, err := io.ReadAll(&io.LimitedReader{R: in, N: 300_000})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm struggling to see what the attack vector would be with loading one of our own configuration files. It's probably fine to keep this, but the interesting thought I had was that some of our configuration files are YAML and if you cut off YAML at the right boundary, it's still valid YAML. That said 300KB of ~20 char YAML would result in >14,000 lines.

Comment on lines +24 to 37
body, err := io.ReadAll(&io.LimitedReader{R: resp.Body, N: 1_000_000_000})
if err != nil {
return nil, err
}

if resp.StatusCode != http.StatusOK {
// Truncate the response body in case it's too big to be useful in logs.
if len(body) > 400 {
body = body[:400]
}
return nil, fmt.Errorf("http status %d for %q: %s", resp.StatusCode, url, string(body))
}

return body, nil

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an interesting pattern here that we should probably fix. We're reading up to 1 GB response body into memory before we check the http status of the response and then truncating to dump it into text. Instead we should branch on the status: If !OK we can read a smaller number of bytes to dump as text into the error. If OK then we can read up to 1 GB.

Comment thread salesforce/pardot.go
}

var respJSON oauthTokenResp
err = json.NewDecoder(resp.Body).Decode(&respJSON)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this line is also an unbounded read from the response body. We'll have to replace the call to json.NewDecoder() with a limited read followed by a json.Unmarshal().

Comment thread crl/storer/storer.go
} else {
defer prevObj.Body.Close()
prevBytes, err := io.ReadAll(prevObj.Body)
prevBytes, err := io.ReadAll(&io.LimitedReader{R: prevObj.Body, N: 1_000_000_000})

@beautifulentropy beautifulentropy Jul 8, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bounding the reader here and in the crl-checker above could lead to an interesting scenario. If we upload a CRL larger than 1 GB we can technically stop ourselves from reading a CRL that we uploaded. So I think the right call here would be:

  1. Make this configurable for the CRL components so that if we ever step on this rake we can simply update our configuration instead of building and deploying a new version of Boulder, and

  2. Do a check against the configurable maxSize value where we accumulate the CRL bytes (near case *cspb.UploadCRLRequest_CrlChunk:). That way we don't silently upload a file we can't read only to find out about it later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use bounded readers where we currently use io.ReadAll

3 participants