Skip to content

fix: bound HTTP response reads and use sync.Once for config init#16

Open
appleboy wants to merge 1 commit intomainfrom
worktree-roktree
Open

fix: bound HTTP response reads and use sync.Once for config init#16
appleboy wants to merge 1 commit intomainfrom
worktree-roktree

Conversation

@appleboy
Copy link
Member

Summary

  • H-1: Wrap all 5 io.ReadAll(resp.Body) calls with io.LimitReader(resp.Body, 1MB) to prevent memory exhaustion from malicious/compromised servers (DoS)
  • H-2: Replace non-thread-safe configInitialized bool with sync.Once for safe concurrent config initialization

Test plan

  • make test — all existing tests pass
  • make lint — no warnings

🤖 Generated with Claude Code

- Limit all HTTP response body reads to 1 MB using io.LimitReader to prevent memory exhaustion
- Replace non-thread-safe configInitialized bool with sync.Once for safe concurrent initialization

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 11, 2026 14:27
@codecov-commenter
Copy link

Codecov Report

❌ Patch coverage is 33.33333% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
main.go 33.33% 6 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the CLI’s HTTP handling and config initialization by bounding HTTP response body reads to mitigate memory-exhaustion DoS risks and making config initialization concurrency-safe.

Changes:

  • Limit all HTTP io.ReadAll(resp.Body) usages to a 1MB maximum via io.LimitReader.
  • Replace the non-thread-safe config initialization guard with sync.Once (via configOnce).
Comments suppressed due to low confidence (1)

main.go:449

  • io.LimitReader will truncate the body at maxResponseBodySize without indicating that truncation happened. That can lead to confusing downstream errors (e.g., json.Unmarshal returning unexpected EOF) and makes it hard to distinguish "malicious/oversized response" from "invalid JSON". Consider reading maxResponseBodySize+1 bytes and explicitly erroring if the limit is exceeded (and optionally returning a dedicated "response too large" error) before attempting to parse/use the body.
	body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodySize))
	if err != nil {
		return nil, fmt.Errorf("failed to read response: %w", err)
	}


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +878 to 881
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodySize))
if err != nil {
return fmt.Errorf("failed to read response: %w", err)
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

When reading the API response body, io.LimitReader prevents large allocations but doesn't indicate truncation. If the upstream returns >maxResponseBodySize, you'll operate on a truncated body. Consider detecting and returning a specific "response too large" error (e.g., read max+1 and check length).

Copilot uses AI. Check for mistakes.
Comment on lines 875 to 879
defer resp.Body.Close()
}

body, err := io.ReadAll(resp.Body)
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodySize))
if err != nil {
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

In the 401 retry path, the first resp.Body is only closed via the earlier deferred close, so it stays open while refreshAccessToken and the retry request run. That can temporarily hold onto connections/resources unnecessarily. Consider explicitly closing (and optionally draining) the original resp.Body immediately before starting the refresh/retry, and only deferring the close for the response you actually read from.

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +47
// maxResponseBodySize limits HTTP response body reads to prevent memory exhaustion (DoS).
const maxResponseBodySize = 1 << 20 // 1 MB
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

This change introduces a new security boundary (maxResponseBodySize) but there are no tests exercising the oversized-response path (e.g., server returns >1MB, client should fail in a well-defined way). Since this file already has extensive HTTP/flow tests, consider adding a test case that returns a payload just over the limit and asserts the resulting error (ideally a dedicated "response too large" error if you add explicit truncation detection).

Copilot uses AI. Check for mistakes.
Comment on lines +645 to 648
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodySize))
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

Same pattern as elsewhere: io.LimitReader truncates silently at maxResponseBodySize, so an oversized response will be treated as valid input for later parsing/handling and may surface as misleading unexpected EOF/JSON errors. Consider reading maxResponseBodySize+1 and returning an explicit "response too large" error when exceeded.

Suggested change
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodySize))
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
// Read up to maxResponseBodySize+1 bytes so we can detect oversized responses
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodySize+1))
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
if len(body) > int(maxResponseBodySize) {
return nil, fmt.Errorf("response too large")
}

Copilot uses AI. Check for mistakes.
Comment on lines +703 to 706
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodySize))
if err != nil {
return fmt.Errorf("failed to read response: %w", err)
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

io.ReadAll(io.LimitReader(...)) will not tell you whether the server sent more than maxResponseBodySize; it just stops at the limit. Consider detecting oversize bodies (read max+1, check length) so callers get a clear "response too large" error rather than parse/format errors from truncated content.

Suggested change
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodySize))
if err != nil {
return fmt.Errorf("failed to read response: %w", err)
}
limitedReader := io.LimitReader(resp.Body, int64(maxResponseBodySize)+1)
body, err := io.ReadAll(limitedReader)
if err != nil {
return fmt.Errorf("failed to read response: %w", err)
}
if int64(len(body)) > int64(maxResponseBodySize) {
return fmt.Errorf("failed to read response: response body too large")
}

Copilot uses AI. Check for mistakes.
Comment on lines +753 to 756
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodySize))
if err != nil {
return credstore.Token{}, fmt.Errorf("failed to read response: %w", err)
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

Because io.LimitReader truncates without error, a response slightly over maxResponseBodySize will be truncated and then unmarshaled/printed, producing confusing errors and potentially incomplete diagnostics. Consider explicitly checking for truncation (read max+1 and error if exceeded).

Suggested change
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodySize))
if err != nil {
return credstore.Token{}, fmt.Errorf("failed to read response: %w", err)
}
// Read up to maxResponseBodySize+1 bytes so we can detect truncation explicitly.
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodySize+1))
if err != nil {
return credstore.Token{}, fmt.Errorf("failed to read response: %w", err)
}
if len(body) > maxResponseBodySize {
return credstore.Token{}, fmt.Errorf("failed to read response: body exceeds maximum allowed size")
}

Copilot uses AI. Check for mistakes.
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.

3 participants