security: cap server-driven pagination to prevent client DoS - #410
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
WalkthroughThe module adds a public limit of 2,000 pagination requests. Activity-by-date and goal pagination now raise ChangesPagination safety
Estimated code review effort: 2 (Simple) | ~10 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
6da91d3 to
03257c5
Compare
03257c5 to
502726d
Compare
502726d to
118ce7e
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/test_garmin_unit.py`:
- Around line 1562-1572: Add a normal-termination pagination test alongside
test_get_goals_pagination_is_capped, mocking garmin.connectapi to return
non-empty pages followed by an empty page. Assert garmin.get_goals() completes
successfully, verifies the expected pagination requests including the advanced
start values, and exercises the empty-page break path.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 3e18f957-1c61-4cb9-bc2d-5c7663b972e2
📒 Files selected for processing (1)
tests/test_garmin_unit.py
118ce7e to
b4c7a26
Compare
get_activities_by_date() and get_goals() paginated with 'while True:' and broke only on an empty page, so loop termination depended entirely on the server. A hostile/compromised/MITM server returning a non-empty page for every 'start' offset hung the client forever with unbounded memory growth (report 3993 PoC: ~3.2M iterations in 5s, eventual OOM). Add MAX_PAGINATED_REQUESTS = 2000 and convert both loops to bounded for/else that raises GarminConnectConnectionError when the cap is hit - fail loudly rather than silently truncate. Normal termination on an empty page is unchanged. The other while-True loops (fit.py) iterate over a local BytesIO and are not affected.
b4c7a26 to
04c3188
Compare
Addresses CodeRabbit review on PR #410: the capped-path test existed but the empty-page break path was untested. Two non-empty pages then an empty page; asserts results, call count, and start advancing 0/30/60. Params dict is mutated between calls, so start values are snapshotted in the side_effect at call time.
Summary
Fixes a client-side denial-of-service from an external security audit (report 3995's sibling, report 3993, CVSS 4.0 8.2 / High).
get_activities_by_date()andget_goals()paginated withwhile True:and broke only when the server returned an empty (falsy) page. There was no cap on page count, total rows, or thestartoffset — loop termination depended entirely on the server.Impact
A hostile / compromised / MITM server that returns a non-empty page for every
startoffset hangs the embedding application indefinitely with unbounded memory growth (report PoC: ~3.2M iterations and 3.2M+ accumulated list items in 5 seconds; eventual OOM). Availability only — no confidentiality/integrity impact.The other
while True:loops in the package (fit.py) iterate over a localBytesIOand are bounded;get_activitiesis already caller-bounded bylimit. Only these two paginators were affected.Fix
New module constant
MAX_PAGINATED_REQUESTS = 2000(at 20–30 items/page that is 40k–60k items — far beyond any legitimate account). Both loops are now boundedfor _ in range(MAX_PAGINATED_REQUESTS)with afor/elsethat raisesGarminConnectConnectionError("Pagination exceeded …")when the cap is hit — failing loudly rather than silently truncating, since a server feeding that many pages is hostile or broken and the data would be suspect anyway. Normal termination (empty page →break) is unchanged.Tests
test_get_activities_by_date_pagination_is_capped— never-empty server → raisesGarminConnectConnectionErrorafter exactlyMAX_PAGINATED_REQUESTScallstest_get_goals_pagination_is_capped— same for goalstest_get_activities_by_date_paginates_until_emptystill passes (normal termination unchanged)Full suite: 261 passed; the 13
test_workout_constantsfailures and thetest_typed.pyimport error (missing optionalpydantic) are pre-existing — verified identical on a clean tree.Summary by CodeRabbit