security(workplaces): rate-limit unauthenticated connector pairing#40
Open
DeryFerd wants to merge 1 commit into
Open
security(workplaces): rate-limit unauthenticated connector pairing#40DeryFerd wants to merge 1 commit into
DeryFerd wants to merge 1 commit into
Conversation
Co-authored-by: Cursor <cursoragent@cursor.com>
anvie
reviewed
May 16, 2026
Owner
anvie
left a comment
There was a problem hiding this comment.
Thank you for the PR! Overall solid implementation with clear threat model and good alignment with existing login rate limiter patterns.
Key findings from review:
- Rate limiter uses time.monotonic() and threading.Lock - correct and consistent with auth.py patterns.
- Pruning on every is_rate_limited() call prevents unbounded growth.
- Minor race condition: is_rate_limited() and record_attempt() are called separately. Consider merging into an atomic check_and_record().
- getattr fallback for config values could silently default to 30 if config not loaded. Direct import would be safer.
- No logging when rate limit triggers - useful for monitoring false positives in production.
- Tests are clean but missing window expiry test and edge case (None IP) test.
Full review report saved in artifacts/pr40-review.md.
APPROVED with minor suggestions - nothing blocking.
Best,
Aisyah
Robin Syihab's agent.
anvie
added a commit
that referenced
this pull request
May 16, 2026
Task #260 — Review PR #40 by DeryFerd: add per-IP rate limiter for unauthenticated connector pairing endpoint. Implementation approved with minor suggestions (atomic check-and-record, logging, tests). - artifacts/pr40-review.md: full review report
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Cloud workplaces pair through
POST /api/connector/pair, which is intentionally unauthenticated so the Evonet binary can exchange a short pairing code for a long-livedconnector_token. That design makes sense for the connector flow, but it also means anyone who can reach the server can keep guessing codes until one works.Pairing codes are six characters from a reduced alphabet (uppercase letters and digits, with ambiguous characters removed). The search space is large enough that random guessing is not practical in a single request, yet still small enough that a patient attacker could hammer the endpoint for the full TTL window (default five minutes) without hitting any throttle today.
This PR adds a simple per-IP rate limiter for pairing attempts. Before
finalize_pairing()runs, the route checks whether the client IP has already used up its budget in the current window. If so, it returns 429 with a short error message. Every pairing request counts toward the limit, including wrong or expired codes, because those are exactly the guesses an attacker would send.The limiter lives in
backend/connector_pair_rate_limit.pyand mirrors the in-memory style already used for failed login attempts inroutes/auth.py: thread-safe timestamps keyed by IP, old entries pruned on each check. Defaults are aligned with pairing semantics:CONNECTOR_PAIRING_CODE_TTL(default 300 seconds)CONNECTOR_PAIR_MAX_ATTEMPTS(default 30), configurable via.envLegitimate pairing should need only a handful of tries (typo, clock skew, code regenerated once). Thirty attempts per IP per code lifetime is generous for real connectors while making online brute force much slower.
What changed
backend/connector_pair_rate_limit.pywithis_rate_limited(),record_attempt(), andreset_for_tests().routes/workplaces.py: rate check +record_attempt()at the start ofapi_connector_pair().config.py:CONNECTOR_PAIR_MAX_ATTEMPTSenv knob (min 5, max 200).unit_tests/test_connector_pair_rate_limit.py: under-limit allowed, at-limit blocked, per-IP isolation.No change to how codes are generated, stored, or validated — only how often a single IP may call the pair endpoint.
Risk / compatibility
request.remote_addr. If you terminate TLS or proxy in front of Evonic, ensure the deployment forwards the real client IP (or all pairing traffic may appear as one address).CONNECTOR_PAIR_MAX_ATTEMPTSor TTL if needed.Validation
python -m pytest unit_tests/test_connector_pair_rate_limit.py -q