Skip to content

fix(organizations): close self-join via shadowed Organization policy subject#3868

Merged
PierreBrisorgueil merged 4 commits into
masterfrom
fix/org-self-join-policy
Jun 15, 2026
Merged

fix(organizations): close self-join via shadowed Organization policy subject#3868
PierreBrisorgueil merged 4 commits into
masterfrom
fix/org-self-join-policy

Conversation

@PierreBrisorgueil

Copy link
Copy Markdown
Contributor

Summary

  • What changed: The Organization CASL policy used a document-subject (Organization) that matched the /members route, shadowing the Membership path-subject and granting create Organization unconditionally — any authenticated non-member could inject a membership into any org. Fixed by carving /members and /requests out of the doc-subject and adding a symmetric carve-out on the admin path-subject so both routes land on the correct Membership owner/admin gate. The join-request flow (/requests) is preserved. An actor-role assertion was added to addMember as a mandatory guard.
  • Why: Security — the shadow allowed privilege escalation without owner/admin consent; exploit returned HTTP 200 on old code but created no membership row (a secondary accidental guard). The fix routes self-join attempts to the correct 403 gate and is verified by new e2e tests covering both the exploit path and the legitimate join-request flow.
  • Related issues: Closes 🔒 Org self-join via shadowed Organization policy subject #3851. Part of the security audit epic 🎯 Security audit hardening — Node + Vue (2026-06 audit) #3848.

Scope

  • Module(s) impacted: modules/organizations (policy + membership controller + tests)
  • Cross-module impact: none
  • Risk level: medium (policy logic change; membership creation path unchanged for legitimate flows)

Validation

  • npm run lint
  • npm test
  • Manual checks done (if applicable)

Guardrails check

  • No secrets or credentials introduced (.env*, secrets/**, keys, tokens)
  • No risky rename/move of core stack paths
  • Changes remain merge-friendly for downstream projects
  • Tests added or updated when behavior changed

Notes for reviewers

  • Security considerations: the self-join exploit exploited a CASL subject shadow — the Organization doc-subject was too broad. The fix is minimal: carve-outs on both path subjects so /members always resolves through the Membership gate.
  • Mergeability considerations: no schema or API contract changes — purely policy + guard logic.
  • Follow-up tasks: remaining items from 🎯 Security audit hardening — Node + Vue (2026-06 audit) #3848 (S1, S2, S4, S5/S6) are tracked separately.

@PierreBrisorgueil PierreBrisorgueil added the Fix A bug fix label Jun 14, 2026
@PierreBrisorgueil PierreBrisorgueil self-assigned this Jun 14, 2026
@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@PierreBrisorgueil, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 35 minutes and 21 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6320e5cc-cff8-4bdc-9d5b-cc5fecb057d3

📥 Commits

Reviewing files that changed from the base of the PR and between c31a5f0 and c299edb.

📒 Files selected for processing (5)
  • modules/organizations/controllers/organizations.membership.controller.js
  • modules/organizations/policies/organizations.policy.js
  • modules/organizations/tests/organizations.membership.controller.unit.tests.js
  • modules/organizations/tests/organizations.policy.unit.tests.js
  • modules/organizations/tests/organizations.selfJoin.e2e.tests.js
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/org-self-join-policy

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 and usage tips.

@codecov

codecov Bot commented Jun 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.49%. Comparing base (c31a5f0) to head (c299edb).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #3868   +/-   ##
=======================================
  Coverage   92.48%   92.49%           
=======================================
  Files         165      165           
  Lines        5400     5407    +7     
  Branches     1735     1741    +6     
=======================================
+ Hits         4994     5001    +7     
  Misses        326      326           
  Partials       80       80           
Flag Coverage Δ
integration 60.12% <41.66%> (-0.06%) ⬇️
unit 73.46% <100.00%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update c31a5f0...c299edb. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@PierreBrisorgueil PierreBrisorgueil marked this pull request as ready for review June 14, 2026 20:01
Copilot AI review requested due to automatic review settings June 14, 2026 20:01

Copilot AI left a comment

Copy link
Copy Markdown

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 hardens the organizations authorization model by preventing a CASL subject-resolution shadowing bug that allowed authenticated non-members to hit POST /api/organizations/:id/members and bypass the intended Membership (owner/admin) gate.

Changes:

  • Tighten Organization document-subject resolution to exclude /members routes so membership management routes resolve via the Membership path-subject.
  • Add a defense-in-depth role gate in addMember to require owner/admin (or platform admin) before proceeding.
  • Add new unit/e2e coverage to prevent regressions and verify the legitimate join-request flow remains functional.

Reviewed changes

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

Show a summary per file
File Description
modules/organizations/policies/organizations.policy.js Adjusts CASL subject registration/guards to prevent Organization doc-subject from shadowing /members.
modules/organizations/controllers/organizations.membership.controller.js Adds an explicit actor-role guard to addMember as a mandatory authorization backstop.
modules/organizations/tests/organizations.policy.unit.tests.js Expands unit tests to verify the updated Organization guard and path-subject ordering behavior.
modules/organizations/tests/organizations.membership.controller.unit.tests.js Adds unit tests asserting non-member and plain-member cannot call addMember.
modules/organizations/tests/organizations.selfJoin.e2e.tests.js Adds an end-to-end regression test for the self-join exploit and confirms join-request flow still works.

Comment on lines +81 to +85
// Actor-role gate (mirrors updateRole/remove): only an org owner/admin (or a global
// admin) may add a member. The type-level CASL `create Membership` check does not
// carry the `{organizationId}` org-scope condition, so a non-member / plain member
// must be rejected here. Without this, the Organization-subject shadowing bug aside,
// a plain member could still inject a membership into their own org.
Comment on lines +33 to 36
// shadows the Membership subject, letting any authenticated user inject a membership.
// Do NOT exclude /requests — that is the any-user JOIN-REQUEST flow, which legitimately
// relies on `create Organization` (excluding it would 403 legitimate join requests).
registerDocumentSubject('organization', 'Organization', (req) => {
@PierreBrisorgueil PierreBrisorgueil merged commit 351ed34 into master Jun 15, 2026
8 checks passed
@PierreBrisorgueil PierreBrisorgueil deleted the fix/org-self-join-policy branch June 15, 2026 06:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Fix A bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🔒 Org self-join via shadowed Organization policy subject

2 participants