Skip to content

[WEB-8095] fix: scope page-version reads to the URL project (GHSA-g49r/ghcr)#9380

Open
mguptahub wants to merge 3 commits into
previewfrom
web-8095/page-version-project-scope
Open

[WEB-8095] fix: scope page-version reads to the URL project (GHSA-g49r/ghcr)#9380
mguptahub wants to merge 3 commits into
previewfrom
web-8095/page-version-project-scope

Conversation

@mguptahub

@mguptahub mguptahub commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Fixes the cross-project page-version disclosure described in GHSA-g49r-p85q-qq2w (dup GHSA-ghcr-frqr-6pqr).

Problem

ProjectPagePermission.has_permission verified the caller was an active member of the URL project_id, but then resolved the page with Page.objects.get(id=page_id, workspace__slug=slug) — never confirming the page belongs to that project. PageVersionEndpoint.get filtered versions the same way. A member of Project A could call GET /workspaces/<slug>/projects/<A>/pages/<pageB>/versions/, where pageB is a public page in Project B (attacker not a member of B), pass the membership check on A, and read Bs page versions. Leak is limited to non-private pages (private pages are owner-gated on CE).

Fix

Page-to-Project is an M2M through ProjectPage.

  • app/permissions/page.py + utils/permissions/page.py: scope the page lookup to projects__id=project_id; deny (403) when the page does not belong to the URL project.
  • PageVersionEndpoint.get: scope list/detail querysets to page__projects__id=project_id (defense in depth). distinct() on the list guards against duplicate rows when a page has both an active and a soft-deleted ProjectPage link to the same project.

Sibling page endpoints (PageViewSet, PagesDescriptionViewSet, PageDuplicateEndpoint) already scope their querysets by projects__id, so the permission gap was latent for them.

Tests

tests/contract/app/test_page_version_project_scope_app.py — 4 contract tests (cross-project list/detail denied, same-project public allowed, non-member denied). Fail-before verified (the two denied tests return 200 before the fix). ruff + manage.py check green.

EE counterpart: WEB-8096 (makeplane/plane-ee).

Summary by CodeRabbit

  • Bug Fixes
    • Tightened page access so lookups are scoped to the requested project, preventing cross-project page reads within the same workspace.
    • Enforced project-scoped permissions for page version listing and retrieval.
    • Denied access when the page’s project link is inactive/soft-deleted, including for public pages.
  • Tests
    • Added/extended contract tests to verify allowed and denied access scenarios for page versions across different projects and revoked links.

…r/ghcr)

ProjectPagePermission verified the caller was a member of the URL
project_id but then resolved the page by workspace + page_id only, and
PageVersionEndpoint filtered versions the same way. A member of one
project could read the page versions of a public page belonging to a
different project in the same workspace via that project's URL
(GHSA-g49r-p85q-qq2w / GHSA-ghcr-frqr-6pqr).

- Scope the page lookup in ProjectPagePermission to projects__id via the
  ProjectPage M2M (both app/ and utils/ copies); deny when the page does
  not belong to the URL project.
- Scope PageVersionEndpoint list/detail querysets to
  page__projects__id=project_id (defense in depth); distinct() on the
  list guards against active + soft-deleted ProjectPage duplicates.
- Add contract regression tests (fail-before verified).

Co-authored-by: Plane AI <noreply@plane.so>
Copilot AI review requested due to automatic review settings July 9, 2026 09:49
@coderabbitai

coderabbitai Bot commented Jul 9, 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

Run ID: 901a7442-7fd9-44a9-95c2-a4f29cc081ec

📥 Commits

Reviewing files that changed from the base of the PR and between 01e6ef8 and 8882731.

📒 Files selected for processing (1)
  • apps/api/plane/app/views/page/version.py

📝 Walkthrough

Walkthrough

Page permission and page-version lookups now require an active link to the requested project_id before returning data. Contract tests cover cross-project denial, same-project access, soft-deleted links, and missing project membership.

Changes

Project-scoped page version access control

Layer / File(s) Summary
Permission scoping for page access
apps/api/plane/app/permissions/page.py, apps/api/plane/utils/permissions/page.py
ProjectPagePermission.has_permission now resolves pages through project_pages for the requested project_id, requires an active link, and returns False when no matching page is found.
Page version endpoint project scoping
apps/api/plane/app/views/page/version.py
PageVersionEndpoint.get now scopes both the single-version and list-version queries by project_id through project_pages and requires active links.
Contract tests for cross-project version access
apps/api/plane/tests/contract/app/test_page_version_project_scope_app.py
The contract test module adds helpers, setup, and cases covering denied cross-project access, allowed same-project access, soft-deleted links, and missing project membership.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested reviewers: pablohashescobar, dheeru0198

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.85% 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
Title check ✅ Passed The title clearly summarizes the main change: scoping page-version reads to the URL project.
Description check ✅ Passed The description is detailed and covers the problem, fix, and tests, but it doesn't follow the template's Type of Change and References sections.
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 web-8095/page-version-project-scope

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.

@makeplane

makeplane Bot commented Jul 9, 2026

Copy link
Copy Markdown

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.

Pull request overview

This PR fixes a cross-project information disclosure in the Pages API by ensuring page and page-version reads are scoped to the project_id from the URL, preventing a user who is a member of Project A from reading versions of a (public) page that belongs to Project B in the same workspace.

Changes:

  • Scope ProjectPagePermission page resolution to the URL project (instead of workspace + page id only).
  • Scope PageVersionEndpoint list/detail queries to project_id for defense in depth.
  • Add contract regression tests covering cross-project deny + same-project allow behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
apps/api/plane/utils/permissions/page.py Scopes permission-time page lookup to the URL project to prevent cross-project access.
apps/api/plane/app/permissions/page.py Same permission scoping change in the app permissions module.
apps/api/plane/app/views/page/version.py Scopes page-version list/detail queries to the URL project to close the disclosure path.
apps/api/plane/tests/contract/app/test_page_version_project_scope_app.py Adds regression contract tests for cross-project page-version access.

Comment thread apps/api/plane/utils/permissions/page.py
Comment thread apps/api/plane/app/permissions/page.py
Comment thread apps/api/plane/app/views/page/version.py Outdated
Comment thread apps/api/plane/app/views/page/version.py Outdated

@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

🤖 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/app/permissions/page.py`:
- Around line 46-50: The Page.objects.filter lookup in the permission check is
still matching soft-deleted project-page links, which can keep access open after
revocation. Update the query in the page permission logic to also require the
related ProjectPage link to be active by adding a deleted_at is null condition
alongside projects__id=project_id, and keep the change within the
permission-check path that returns False when no page is found.

In `@apps/api/plane/app/views/page/version.py`:
- Around line 25-27: The PageVersion detail lookup in PageVersionView/page
version retrieval can be duplicated by the page__projects join, causing .get()
to raise MultipleObjectsReturned. Update the queryset used in the page version
lookup to add .distinct() before .get(), keeping the existing filters on
workspace__slug, page__projects__id, page_id, and pk so the lookup remains
unique even when ProjectPage has both active and soft-deleted rows.
🪄 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

Run ID: 21872407-7ad5-4d04-bef7-46dcd5609447

📥 Commits

Reviewing files that changed from the base of the PR and between 4fc79a2 and 1d43e15.

📒 Files selected for processing (4)
  • apps/api/plane/app/permissions/page.py
  • apps/api/plane/app/views/page/version.py
  • apps/api/plane/tests/contract/app/test_page_version_project_scope_app.py
  • apps/api/plane/utils/permissions/page.py

Comment thread apps/api/plane/app/permissions/page.py
Comment thread apps/api/plane/app/views/page/version.py Outdated
mguptahub and others added 2 commits July 9, 2026 15:33
… a project

Address CodeRabbit + Copilot review on #9380: projects__id=project_id
matched even soft-deleted ProjectPage links, so a page removed from the
project (link revoked) would still pass, and the version detail get()
could raise MultipleObjectsReturned on active + soft-deleted rows.

Put both conditions on the same project_pages relation in one filter so
they match a single ProjectPage row that is active:
project_pages__project_id=project_id + project_pages__deleted_at__isnull
=True. The partial-unique constraint (project, page WHERE deleted_at IS
NULL) then guarantees at most one row, so get() stays unambiguous and the
list needs no distinct(). Add a revoked-link regression test.

Co-authored-by: Plane AI <noreply@plane.so>
…eObjectsReturned guard

Address CodeRabbit review on #9380. The active-link filter already keeps
the page__project_pages join to a single row via the partial-unique
constraint, but add distinct() to the detail get() as defense in depth so
the join can never surface MultipleObjectsReturned (a 500) even if that
invariant were ever violated.

Co-authored-by: Plane AI <noreply@plane.so>
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