-
Notifications
You must be signed in to change notification settings - Fork 0
fix: bound HTTP response reads and use sync.Once for config init #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -28,21 +28,24 @@ import ( | |||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| var ( | ||||||||||||||||||||||||||
| serverURL string | ||||||||||||||||||||||||||
| clientID string | ||||||||||||||||||||||||||
| tokenFile string | ||||||||||||||||||||||||||
| tokenStoreMode string | ||||||||||||||||||||||||||
| flagServerURL *string | ||||||||||||||||||||||||||
| flagClientID *string | ||||||||||||||||||||||||||
| flagTokenFile *string | ||||||||||||||||||||||||||
| flagTokenStore *string | ||||||||||||||||||||||||||
| configInitialized bool | ||||||||||||||||||||||||||
| retryClient *retry.Client | ||||||||||||||||||||||||||
| tokenStore credstore.Store[credstore.Token] | ||||||||||||||||||||||||||
| serverURL string | ||||||||||||||||||||||||||
| clientID string | ||||||||||||||||||||||||||
| tokenFile string | ||||||||||||||||||||||||||
| tokenStoreMode string | ||||||||||||||||||||||||||
| flagServerURL *string | ||||||||||||||||||||||||||
| flagClientID *string | ||||||||||||||||||||||||||
| flagTokenFile *string | ||||||||||||||||||||||||||
| flagTokenStore *string | ||||||||||||||||||||||||||
| configOnce sync.Once | ||||||||||||||||||||||||||
| retryClient *retry.Client | ||||||||||||||||||||||||||
| tokenStore credstore.Store[credstore.Token] | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const defaultKeyringService = "authgate-device-cli" | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // maxResponseBodySize limits HTTP response body reads to prevent memory exhaustion (DoS). | ||||||||||||||||||||||||||
| const maxResponseBodySize = 1 << 20 // 1 MB | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Timeout configuration for different operations | ||||||||||||||||||||||||||
| const ( | ||||||||||||||||||||||||||
| deviceCodeRequestTimeout = 10 * time.Second | ||||||||||||||||||||||||||
|
|
@@ -107,11 +110,12 @@ func init() { | |||||||||||||||||||||||||
| // initConfig parses flags and initializes configuration | ||||||||||||||||||||||||||
| // Separated from init() to avoid conflicts with test flag parsing | ||||||||||||||||||||||||||
| func initConfig() { | ||||||||||||||||||||||||||
| if configInitialized { | ||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| configInitialized = true | ||||||||||||||||||||||||||
| configOnce.Do(func() { | ||||||||||||||||||||||||||
| doInitConfig() | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| func doInitConfig() { | ||||||||||||||||||||||||||
| flag.Parse() | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Priority: flag > env > default | ||||||||||||||||||||||||||
|
|
@@ -438,7 +442,7 @@ func requestDeviceCode(ctx context.Context) (*oauth2.DeviceAuthResponse, error) | |||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| defer resp.Body.Close() | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| body, err := io.ReadAll(resp.Body) | ||||||||||||||||||||||||||
| body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodySize)) | ||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||
| return nil, fmt.Errorf("failed to read response: %w", err) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
@@ -638,7 +642,7 @@ func exchangeDeviceCode( | |||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| defer resp.Body.Close() | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| body, err := io.ReadAll(resp.Body) | ||||||||||||||||||||||||||
| body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodySize)) | ||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||
| return nil, fmt.Errorf("failed to read response: %w", err) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
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) | |
| } | |
| // 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
AI
Mar 11, 2026
There was a problem hiding this comment.
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.
| 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
AI
Mar 11, 2026
There was a problem hiding this comment.
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).
| 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
AI
Mar 11, 2026
There was a problem hiding this comment.
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
AI
Mar 11, 2026
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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).