Skip to content

[INFRA-778] fix(security): scope ExportIssuesEndpoint.get to the requesting user's own exports - #9707

Open
mguptahub wants to merge 2 commits into
previewfrom
infra-778/export-history-scope-to-initiator
Open

[INFRA-778] fix(security): scope ExportIssuesEndpoint.get to the requesting user's own exports#9707
mguptahub wants to merge 2 commits into
previewfrom
infra-778/export-history-scope-to-initiator

Conversation

@mguptahub

@mguptahub mguptahub commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • ExportIssuesEndpoint.get filtered ExporterHistory by workspace__slug only, with no initiated_by filter — any workspace ADMIN/MEMBER could list every other member's export history.
  • ExporterHistorySerializer includes url (a presigned S3 download link, 7-day expiry, no authentication required to use) and token.
  • Since an export defaults to the initiator's own projects (including fully private ones) when no project list is supplied in the create request, any workspace member could read another member's private project data simply by listing exports and using the disclosed url — no crafted request needed, and revoking the original user's access doesn't revoke the download.
  • Fix: add initiated_by=request.user to the queryset filter, per the advisory's own suggested fix.
  • Out of scope (advisory's own "worth considering" hardening, not required to close the reported vulnerability): dropping url/token from the serializer in favor of an authenticated per-request redirect endpoint, and shortening the 7-day presigned-URL expiry.

Test plan

  • New regression tests in apps/api/plane/tests/contract/app/test_export_history_scope.py: a workspace member cannot see another member's export history via GET; a member still sees and can retrieve their own (including the url field)
  • Verified fail-before/pass-after via git stash — the cross-member-visibility test fails against pre-fix code
  • ruff check / ruff format --check clean

Summary by CodeRabbit

  • Bug Fixes

    • Export history is now limited to exports initiated by the signed-in member.
    • Members can no longer view or access download links for exports created by others.
    • Export initiators retain access to their own export records and download links.
  • Tests

    • Added coverage to verify export-history access controls.
…esting user's own exports

ExportIssuesEndpoint.get filtered ExporterHistory by workspace__slug only,
with no initiated_by filter, so any workspace ADMIN/MEMBER could list
every other member's export history. ExporterHistorySerializer includes
url (a presigned S3 link, 7-day expiry, no auth required to use) and
token. Since an export defaults to the initiator's own projects
(including fully private ones) when no project list is supplied, any
workspace member could read another member's private project data by
listing exports and using the disclosed url — no crafted request needed,
and revoking the user's access doesn't revoke the download.

Add initiated_by=request.user to the queryset filter, per the advisory's
own suggested fix. 2 new tests, fail-before verified.

Co-authored-by: Plane AI <noreply@plane.so>
Copilot AI lite review requested due to automatic review settings August 28, 2026 09:58
@makeplane

makeplane Bot commented Aug 28, 2026

Copy link
Copy Markdown

Linked to Plane Work Item(s)

This comment was auto-generated by Plane

@coderabbitai

coderabbitai Bot commented Aug 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: 5e62c44f-793c-4c12-9f9d-635cb4e658d1

📥 Commits

Reviewing files that changed from the base of the PR and between 96d4ae9 and 1b2716c.

📒 Files selected for processing (1)
  • apps/api/plane/tests/contract/app/test_export_history_scope.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/plane/tests/contract/app/test_export_history_scope.py

Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The export history endpoint now returns only issue exports initiated by the requesting user. Contract tests verify cross-member visibility and presigned URL access.

Changes

Export history access scoping

Layer / File(s) Summary
Apply initiator filtering
apps/api/plane/app/views/exporter/base.py
The export history query now filters records by initiated_by=request.user.
Verify cross-member access
apps/api/plane/tests/contract/app/test_export_history_scope.py
Contract fixtures and tests verify that non-initiators cannot access another member’s export, while initiators can retrieve their own record and presigned URL.

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

Merge Risk: ⚪ Minimal · up to 1b271

The change limits export-history results to the requesting user while preserving access to that user's own exports; no actionable merge-blocking risk remains after normal checks and review.

Suggested reviewers: dheeru0198

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 2 files. 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 identifies the security fix and the affected endpoint. It accurately summarizes the main change.
Description check ✅ Passed The description provides a detailed vulnerability summary, fix, scope boundaries, regression tests, and validation results. It does not use all template headings, but it contains the required substant…
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.
Full details: Description check

Explanation

The description provides a detailed vulnerability summary, fix, scope boundaries, regression tests, and validation results. It does not use all template headings, but it contains the required substantive information.

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch infra-778/export-history-scope-to-initiator

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.

@mguptahub

Copy link
Copy Markdown
Collaborator Author

/code-review: clean, no findings. Confirmed this is the only route serving ExporterHistory data (no parallel endpoint reintroduces the leak) and the test correctly exercises both negative and positive cases.

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

Scopes ExportIssuesEndpoint.get export-history listing to the requesting user to prevent cross-member disclosure of export metadata (including presigned download URLs/tokens), and adds contract-level regression coverage to ensure the vulnerability stays closed.

Changes:

  • Restrict ExporterHistory GET queryset by initiated_by=request.user in ExportIssuesEndpoint.get.
  • Add contract regression tests verifying a member cannot see another member’s export history, while still being able to see their own.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
apps/api/plane/app/views/exporter/base.py Adds initiated_by=request.user to the export-history queryset to prevent cross-member exposure.
apps/api/plane/tests/contract/app/test_export_history_scope.py New contract regression tests covering cross-member visibility and positive control for own exports.
Suppressed comments (2)

apps/api/plane/tests/contract/app/test_export_history_scope.py:77

  • Use status.HTTP_200_OK instead of the literal 200 to match the established pattern in other contract tests and make the assertion self-explanatory.
        assert response.status_code == 200, response.data

apps/api/plane/tests/contract/app/test_export_history_scope.py:85

  • Use status.HTTP_200_OK instead of the literal 200 to match the established pattern in other contract tests and make the assertion self-explanatory.
        assert response.status_code == 200, response.data

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread apps/api/plane/tests/contract/app/test_export_history_scope.py
Address Copilot finding on PR #9707: this repo's contract tests
consistently assert HTTP codes via rest_framework.status constants.

Co-authored-by: Plane AI <noreply@plane.so>
@mguptahub

Copy link
Copy Markdown
Collaborator Author

Re the /code-review finding on reusing session_client/create_bot_user instead of local _make_user/_client_for helpers: checked before acting on it. Neither fixture actually fits this test's need — session_client is force_authenticate-tied to the single create_user fixture (can't produce a second, distinct authenticated user), and create_bot_user creates a bot-flagged user, not a regular member. The local _make_user/_client_for pattern here matches the existing precedent already in test_project_member_is_active_authz.py (pre-existing file, not from this PR), which has the identical local _make_user helper for the same reason.

Leaving as-is rather than introducing a new shared fixture unrequested — that would be a conftest.py change affecting other test files, better done as its own deliberate cleanup than folded into this security fix. Noting it as a legitimate future refactor candidate, not fixing reactively here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants