Skip to content

[SECUR-236] fix: grouped-paginator cursor bound + Project mass-assignment - #9486

Open
mguptahub wants to merge 4 commits into
previewfrom
secur-236/appscan-ce-hardening
Open

[SECUR-236] fix: grouped-paginator cursor bound + Project mass-assignment#9486
mguptahub wants to merge 4 commits into
previewfrom
secur-236/appscan-ce-hardening

Conversation

@mguptahub

@mguptahub mguptahub commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

AppScan DAST remediation for SECUR-236 (plane-ee counterpart: makeplane/plane-ee#8591). This PR was trimmed to its unique, non-overlapping fixes after two parts were found to fully overlap existing open PRs against preview:

What remains in this PR

🟠 MEDIUM + 🟡 LOW — grouped-paginator cursor bound (not addressed by #9429)
GroupedOffsetPaginator takes its per-group page size from the client cursor.value, not the capped limit. cursor=1000000:0:0 → up to 1M rows/group (bypasses max_per_page, DoS; reachable unauthenticated on public boards); cursor=-1:0:0 → negative queryset slice → ValueError → HTTP 500. Fix bounds the parsed cursor (0 <= value <= max_per_page, offset >= 0) centrally in paginate(). Regression tests: TestCursorBounds.

🟡 LOW — Project mass-assignment
ProjectSerializer (fields="__all__") left created_by/updated_by client-writable; BaseModel.save() only backfills created_by when None, so a supplied value survived — allowing ownership/attribution forgery. Fix: add them to read_only_fields.

Testing

  • Reproduced both cursor issues on a live plane-ce instance; TestCursorBounds covers reject (-1:0:0, 1000000:0:0, 20:-1:0) + valid/at-max cursors.
  • ruff lint + format clean.

Note

Merge order: this PR's per_page/auth coverage now lives in #9429 and #9335 respectively — those should land for full SECUR-236 coverage on CE.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved pagination reliability by rejecting invalid or out-of-range cursor parameters.
    • Prevented malformed cursors from affecting pagination results.
  • Security

    • Audit fields on projects can no longer be modified through client requests.

…imiting

AppScan DAST remediation, ported from the plane-ee fix (#8591) and
re-verified against a live plane-ce instance.

- paginator: reject non-positive per_page (per_page=0 -> ZeroDivisionError ->
  HTTP 500) and bound the client-supplied cursor value/offset. The grouped
  paginators use cursor.value as the per-group page size: a negative value
  slices the queryset with a negative stop (ValueError -> HTTP 500) and a huge
  value fetches far more than max_per_page rows per group (cap bypass / DoS).
  One central guard in BasePaginator.paginate(); regression tests added.
- auth: sign-in/sign-up (app + space) were plain Views with no rate limiting.
  Add the IP-based AuthenticationThrottle check plus a per-account throttle
  keyed on the normalized email (AuthenticationAccountThrottle). The IP key is
  bypassable by spoofing X-Forwarded-For (NUM_PROXIES unset); the per-account
  limiter caps credential guessing against a single account regardless of IP.
- project serializer: mark created_by/updated_by read-only (fields="__all__"
  left them client-writable, allowing project ownership/attribution forgery).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 28, 2026 09:19

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@makeplane

makeplane Bot commented Jul 28, 2026

Copy link
Copy Markdown

Linked to Plane Work Item(s)

This comment was auto-generated by Plane

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a27cce66-661b-4c87-84a9-63fd29b5aa72

📥 Commits

Reviewing files that changed from the base of the PR and between 687483e and 6bc666a.

📒 Files selected for processing (2)
  • apps/api/plane/tests/unit/utils/test_paginator.py
  • apps/api/plane/utils/paginator.py
💤 Files with no reviewable changes (2)
  • apps/api/plane/tests/unit/utils/test_paginator.py
  • apps/api/plane/utils/paginator.py

📝 Walkthrough

Walkthrough

The paginator now validates cursor bounds before execution, with tests for invalid and maximum-bound cursors. The project serializer marks created_by and updated_by as server-managed read-only fields.

Changes

Pagination validation

Layer / File(s) Summary
Paginator cursor bounds and validation coverage
apps/api/plane/utils/paginator.py, apps/api/plane/tests/unit/utils/test_paginator.py
BasePaginator.paginate() rejects negative offsets and cursor values outside the effective page-size range before paginator construction; tests cover invalid cursors, valid cursors, and the maximum accepted value.

Project audit-field protection

Layer / File(s) Summary
Project serializer audit fields
apps/api/plane/app/serializers/project.py
created_by and updated_by are added to ProjectSerializer.Meta.read_only_fields alongside existing server-managed fields.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: cursor bound validation in grouped pagination and ProjectSerializer mass-assignment hardening.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch secur-236/appscan-ce-hardening

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🧹 Nitpick comments (2)
apps/api/plane/authentication/rate_limit.py (1)

52-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No test coverage for the new account throttle.

This PR adds 17 paginator unit tests but nothing exercises AuthenticationAccountThrottle.get_cache_key (email vs. no-email fallback, normalization) or authentication_account_throttle_allows. Given this is security-critical brute-force protection, a few focused unit tests (cache-key bucketing by normalized email, fallback to IP when email absent, allow/deny across the rate window) would be valuable.

🤖 Prompt for 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.

In `@apps/api/plane/authentication/rate_limit.py` around lines 52 - 76, Add
focused unit tests for AuthenticationAccountThrottle.get_cache_key covering
normalized email bucketing, distinct emails, and IP fallback when email is
absent; also test authentication_account_throttle_allows across the configured
rate window to verify initial requests are allowed and excess requests are
denied.
apps/api/plane/authentication/views/app/email.py (1)

33-48: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the duplicated throttle-check-and-redirect block into a shared helper. The same 16-line pattern (call authentication_throttle_allows + authentication_account_throttle_allows, build a RATE_LIMIT_EXCEEDED AuthenticationException, and safe-redirect) is copy-pasted across all four sign-in/sign-up handlers. A single helper (e.g. in rate_limit.py, parameterized by is_app/is_space) would remove the duplication and prevent future drift in this security-critical gating logic.

  • apps/api/plane/authentication/views/app/email.py#L33-L48: replace with a call to a shared authentication_rate_limit_redirect(request, next_path, is_app=True) helper that returns None or the redirect.
  • apps/api/plane/authentication/views/app/email.py#L158-L172: same replacement, is_app=True.
  • apps/api/plane/authentication/views/space/email.py#L32-L47: same replacement, is_space=True.
  • apps/api/plane/authentication/views/space/email.py#L133-L147: same replacement, is_space=True.
🤖 Prompt for 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.

In `@apps/api/plane/authentication/views/app/email.py` around lines 33 - 48,
Extract the duplicated authentication throttle check and RATE_LIMIT_EXCEEDED
redirect into a shared authentication_rate_limit_redirect helper, parameterized
for app or space redirects and returning None when allowed. In
apps/api/plane/authentication/views/app/email.py lines 33-48 and 158-172,
replace each block with the helper using is_app=True; in
apps/api/plane/authentication/views/space/email.py lines 32-47 and 133-147,
replace each with is_space=True.
🤖 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 `@apps/api/plane/authentication/rate_limit.py`:
- Around line 52-69: Update AuthenticationAccountThrottle.get_cache_key so an
attacker cannot exhaust a victim’s throttle bucket solely by submitting the
victim’s email; derive the key from server-validated identity when available, or
combine the normalized email with the client identity. Preserve email
normalization and the existing IP fallback for requests without an email.
- Around line 63-64: Validate AUTHENTICATION_ACCOUNT_RATE_LIMIT before assigning
or using it in the authentication account throttle, ensuring it contains exactly
a numeric request count and a supported period in the expected “count/period”
format. Fall back to the existing default rate when the environment value is
empty or malformed, so AuthenticationAccountThrottle initialization remains safe
for every auth request.

---

Nitpick comments:
In `@apps/api/plane/authentication/rate_limit.py`:
- Around line 52-76: Add focused unit tests for
AuthenticationAccountThrottle.get_cache_key covering normalized email bucketing,
distinct emails, and IP fallback when email is absent; also test
authentication_account_throttle_allows across the configured rate window to
verify initial requests are allowed and excess requests are denied.

In `@apps/api/plane/authentication/views/app/email.py`:
- Around line 33-48: Extract the duplicated authentication throttle check and
RATE_LIMIT_EXCEEDED redirect into a shared authentication_rate_limit_redirect
helper, parameterized for app or space redirects and returning None when
allowed. In apps/api/plane/authentication/views/app/email.py lines 33-48 and
158-172, replace each block with the helper using is_app=True; in
apps/api/plane/authentication/views/space/email.py lines 32-47 and 133-147,
replace each with is_space=True.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5bfd2980-c1b1-4421-8d33-189cfabc326c

📥 Commits

Reviewing files that changed from the base of the PR and between a8e53b6 and 47ab37c.

📒 Files selected for processing (6)
  • apps/api/plane/app/serializers/project.py
  • apps/api/plane/authentication/rate_limit.py
  • apps/api/plane/authentication/views/app/email.py
  • apps/api/plane/authentication/views/space/email.py
  • apps/api/plane/tests/unit/utils/test_paginator.py
  • apps/api/plane/utils/paginator.py

Comment thread apps/api/plane/authentication/rate_limit.py Outdated
Comment thread apps/api/plane/authentication/rate_limit.py Outdated
…+ rate guard

- AuthenticationAccountThrottle keyed on (email + client IP) instead of email
  alone. An email-only key let anyone lock a victim out of their own account by
  spamming their address from other IPs (self-inflicted lockout DoS). Combining
  with the client IP prevents that while still capping single-source guessing.
- Guard the AUTHENTICATION_ACCOUNT_RATE_LIMIT env value: a malformed rate would
  raise in DRF parse_rate() on every auth POST (throttle is instantiated per
  request), taking authentication down instance-wide. Falls back to the default.
- Add regression tests for both.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 28, 2026 09:42

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@mguptahub

Copy link
Copy Markdown
Collaborator Author

Thanks @coderabbitai — both valid, fixed in 3257ee9:

  1. Email-only throttle key → victim lockout DoS (Major): AuthenticationAccountThrottle.get_cache_key now combines the normalized email and the client IP (email:<e>|ip:<ip>). An attacker on other IPs can no longer consume a victim's bucket, while single-source guessing against an account is still capped. (Fully defeating a spoofed-source distributed brute force additionally requires a trustworthy client IP — i.e. correct NUM_PROXIES/proxy config so X-Forwarded-For can't be forged — which is the deployment-side control noted on the tracking issue.)

  2. Malformed AUTHENTICATION_ACCOUNT_RATE_LIMIT crash (Minor): added _valid_rate_or_default() — the env value is validated as a well-formed DRF rate and falls back to 5/minute if not, so a misconfig can't take auth down instance-wide.

Added regression tests for both (tests/unit/authentication/test_rate_limit.py, 15 cases).

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@mguptahub Thanks for addressing both findings. Combining the normalized email with the client IP prevents cross-IP victim bucket exhaustion, and validating the configured DRF rate with a safe default removes the instance-wide misconfiguration failure mode. The regression coverage is appreciated.

🐇✅


✏️ Learnings added
Learnt from: mguptahub
URL: https://github.com/makeplane/plane/pull/9486

Timestamp: 2026-07-28T09:42:50.847Z
Learning: In `apps/api/plane/authentication/rate_limit.py`, `AuthenticationAccountThrottle` keys authentication attempts by normalized submitted email plus the client IP to prevent an attacker on a different IP from exhausting a victim account’s throttle bucket. Correct deployment configuration of trusted proxies and `NUM_PROXIES` remains required so client IP headers cannot be spoofed.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 28, 2026 10:01

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…9429 / #9335

Two parts of this PR fully overlapped existing open PRs against preview, so per
the "drop the most recent on full overlap" call they are removed here:

- per_page non-positive guard (get_per_page) -> covered identically by #9429.
- password sign-in/sign-up rate limiting -> covered (more cleanly, via a
  decorator) by #9335 (GHSA-349j-pjw5-67q4). Reverted rate_limit.py and
  email.py (app + space) and removed the auth unit tests.

This PR now carries only its unique, non-overlapping fixes:
- grouped-paginator cursor bound in paginate() (cap-bypass DoS + negative-slice
  500 that #9429 does not address), with TestCursorBounds.
- Project created_by/updated_by read-only (mass-assignment).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 28, 2026 12:35

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@mguptahub mguptahub changed the title [SECUR-236] fix: harden pagination bounds and auth brute-force rate limiting [SECUR-236] fix: grouped-paginator cursor bound + Project mass-assignment Jul 28, 2026
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.

2 participants