Skip to content

Fix IDOR in GET /api/users/{user_id} — email leaked to any authenticated user , Closes #25 - #40

Merged
nazarli-shabnam merged 6 commits into
Devlaner:mainfrom
SankeerthNara:main
Jul 10, 2026
Merged

Fix IDOR in GET /api/users/{user_id} — email leaked to any authenticated user , Closes #25#40
nazarli-shabnam merged 6 commits into
Devlaner:mainfrom
SankeerthNara:main

Conversation

@SankeerthNara

@SankeerthNara SankeerthNara commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Any authenticated user could fetch any other user's email address and verification status by guessing/observing their UUID on GET /api/users/{user_id}, with no check that the requester shares a workspace with the target.

Related issues

Closes #25

Type of change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that changes existing behavior or API)
  • Refactor (no functional change)
  • Documentation only
  • Tests / CI / tooling

Areas touched

  • API (api/app/)
  • Frontend (apps/frontend/)
  • Canvas / Excalidraw
  • Real-time / WebSocket
  • Auth / OAuth
  • Workspaces / Boards / Elements
  • Database (SQLAlchemy models)
  • Infra / Docker / CI
  • Documentation

Database changes

N/A — no model changes, only response schema and query-layer additions.

Implementation notes

  • Added PublicUserResponse schema with reduced fields (id, username, display_name, avatar_url) — deliberately omits email / email_verified / names.
  • Added shares_workspace(db, user_id_a, user_id_b) to workspaces/repository.py. Treats self-lookup as trivially "shared," and checks WorkspaceMember for any common workspace (owners are members too, via create(), so no separate owner check is needed).
  • GET /api/users/{user_id} now returns full UserResponse only for self-lookups or shared-workspace members; everyone else gets PublicUserResponse via a Union response model.
  • No automated regression test included — the users module didn't have DB-backed integration test fixtures yet (only unit-style tests exist elsewhere, e.g. test_jwt.py). Verified manually instead (see below). Happy to follow up with test scaffolding in a separate PR if useful.

How to test

  1. Start deps and apply migrations:

    docker compose up -d
    cd api/app
    alembic upgrade head
    uvicorn app.main:app --reload
    
  2. Create two users (A, B) via POST /api/users, and log in as A via POST /api/auth/login.

  3. No shared workspace — confirm email is hidden:

    curl http://localhost:8000/api/users/<B_ID> -H "Authorization: Bearer <A_TOKEN>"
    # expect: only id, username, display_name, avatar_url
    
  4. Self-lookup — confirm full profile still returns:

    curl http://localhost:8000/api/users/<A_ID> -H "Authorization: Bearer <A_TOKEN>"
    # expect: full profile including email
    
  5. Add B to a workspace owned by A via POST /api/workspaces/{workspace_id}/members, then repeat step 3:
    # expect: full profile including email, now that they share a workspace

Checklist

  • My branch is up to date with main.
  • Code follows the layered module structure (router → service → repository) for backend changes.
  • I added type hints; uv run mypy . passes for all code touched by this change (5 pre-existing errors remain in app/core/redis.py and venv-bundled stubs, unrelated to this PR).
  • uv run ruff check . passes.
  • uv run pytest passes (37 passed).
  • npm run lint, npm run format:check, and npm run build pass (for frontend changes).
  • All user-facing strings are localized in src/i18n/ (for frontend changes).
  • I added or updated tests for new behavior.
  • I updated README / docs where relevant.
  • I confirmed no secrets, tokens, or .env files are committed.

Screenshots / recordings

N/A — backend-only API change.

Summary by CodeRabbit

  • New Features
    • Added a public user profile view (ID, username, display name, optional avatar).
    • Updated GET /users/{user_id} to return full details only when viewing your own profile or when both users share a workspace; otherwise it returns the public profile.
  • Bug Fixes
    • Prevents display names from exposing email-derived information when first/last name are not set.
  • Tests / CI
    • Added integration tests for public vs private user responses and 404 behavior.
    • CI now provisions a PostgreSQL test database and runs migrations before linting, type checking, and tests.

…d workspace membership

Any authenticated user could fetch any other user's email address and
verification status by guessing/observing their UUID, with no check
that the requester shares a workspace with the target.

- Add PublicUserResponse schema with reduced fields (id, username,
  display_name, avatar_url)
- Add shares_workspace() to workspaces/repository.py
- Gate GET /api/users/{user_id} to return full UserResponse only for
  self-lookups and shared-workspace members; PublicUserResponse
  otherwise

Fixes Devlaner#25

Signed-off-by: SankeerthNara <sankeerthnara@gmail.com>
Signed-off-by: SankeerthNara <sankeerthnara@gmail.com>
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@SankeerthNara, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

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.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e8a58c0d-074a-4dd3-8271-229db9145986

📥 Commits

Reviewing files that changed from the base of the PR and between a83a444 and 1216b73.

📒 Files selected for processing (1)
  • .github/workflows/api-ci.yml
📝 Walkthrough

Walkthrough

GET /users/{user_id} now returns a full user response for users sharing a workspace and a reduced public profile otherwise. Database-backed fixtures, regression tests, and CI PostgreSQL setup validate the behavior.

Changes

User Profile Privacy

Layer / File(s) Summary
Public profile contract and presentation
api/app/app/modules/users/schemas.py, api/app/app/modules/users/presenter.py
Adds PublicUserResponse and presentation logic exposing only public identity fields without email-based fallback data.
Workspace sharing check
api/app/app/modules/workspaces/repository.py
Adds shares_workspace, which checks identical users or overlapping workspace memberships.
Conditional user lookup response
api/app/app/modules/users/router.py
Updates GET /users/{user_id} to select UserResponse or PublicUserResponse based on workspace sharing.
Database-backed validation and CI
api/app/tests/conftest.py, api/app/tests/test_users.py, .github/workflows/api-ci.yml
Adds transactional database fixtures, endpoint regression tests, PostgreSQL CI services, and migration execution.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant get_user
  participant shares_workspace
  participant Database
  participant UserPresenter
  Client->>get_user: GET /users/{user_id}
  get_user->>shares_workspace: Check requester and target
  shares_workspace->>Database: Query workspace memberships
  Database-->>shares_workspace: Shared membership result
  alt Shared workspace
    shares_workspace-->>get_user: True
    get_user->>UserPresenter: Build UserResponse
    UserPresenter-->>Client: Full profile
  else No shared workspace
    shares_workspace-->>get_user: False
    get_user->>UserPresenter: Build PublicUserResponse
    UserPresenter-->>Client: Reduced profile
  end
Loading

Poem

I’m a rabbit with profiles tucked tight,
Public fields hop into sight.
Shared workspaces open the door,
Private details stay off the floor.
Ears up—privacy wins tonight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% 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 describes the main change: fixing the user profile IDOR and leaked email exposure.
Linked Issues check ✅ Passed The PR adds workspace-based authorization, a reduced public profile, and regression tests matching issue #25's acceptance criteria.
Out of Scope Changes check ✅ Passed The CI and test-fixture updates support the new regression tests and are scoped to the IDOR fix.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@nazarli-shabnam nazarli-shabnam self-assigned this Jul 10, 2026
@nazarli-shabnam nazarli-shabnam added the bug Something isn't working label Jul 10, 2026
@nazarli-shabnam nazarli-shabnam added this to the Enhancement Deadline milestone Jul 10, 2026

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

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 `@api/app/app/modules/users/presenter.py`:
- Around line 43-49: Update to_public_user_response and its display-name helper
display_name_of so public responses never derive display_name from user.email;
when first_name and last_name are absent, return a non-sensitive fallback such
as the username or a generic value, while preserving normal name formatting and
ensuring PublicUserResponse does not expose any email-derived data.

In `@api/app/app/modules/users/router.py`:
- Around line 43-56: Add a regression test for the get_user endpoint verifying
that a user without a shared workspace receives a PublicUserResponse when
retrieving another user, and that the response excludes email and
email_verified. Cover the cross-workspace case through the existing
authentication, database, and API test fixtures.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f6dbcf36-a5c1-48ee-ad3f-4b5a5122ac4d

📥 Commits

Reviewing files that changed from the base of the PR and between 66912eb and 1178e58.

📒 Files selected for processing (4)
  • api/app/app/modules/users/presenter.py
  • api/app/app/modules/users/router.py
  • api/app/app/modules/users/schemas.py
  • api/app/app/modules/workspaces/repository.py

Comment thread api/app/app/modules/users/presenter.py Outdated
Comment thread api/app/app/modules/users/router.py
@nazarli-shabnam

Copy link
Copy Markdown
Member

@SankeerthNara fix the comments and tag me please

@nazarli-shabnam nazarli-shabnam left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice fix overall, this closes the direct email-by-ID enumeration in #25. Two things I need addressed before merge though:

1. to_public_user_response still leaks email via display_nameapi/app/app/modules/users/presenter.py:47 calls display_name_of(user), and that function (line 7-15) falls back to user.email.split("@", 1)[0] whenever first_name/last_name are blank. UserCreate.first_name/last_name aren't validated as non-empty, so any user who registers with first_name=""/last_name="" still has their email's local-part exposed in the "public" response to strangers — which is the exact class of leak #25 was filed for, just one hop removed. The public path should never derive from email; fall back to username instead (or something that isn't PII-derived).

2. No regression test, and there's no fixture to write one withconftest.py only has a bare TestClient, no db session or user/workspace factories. For a security-sensitive endpoint like this I don't want to merge on manual curl verification alone — next refactor of shares_workspace or the response model silently reopens the hole with nothing to catch it. Please add a minimal db-backed fixture (session + a couple of user/workspace helpers) and a test asserting: no shared workspace → no email/email_verified/no email-derived display_name in the response; shared workspace or self → full profile.

Requesting changes for these two — happy to re-review once they're in.

Signed-off-by: SankeerthNara <sankeerthnara@gmail.com>
@SankeerthNara

Copy link
Copy Markdown
Contributor Author

hey @nazarli-shabnam, i fixed the changes you pointed out.
Thank you for pointing out my mistakes.
I would happy to do any furhter changes

@nazarli-shabnam nazarli-shabnam left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed after c85d63c.

1. Email-leak-via-display_name — fixed. public_display_name_of (presenter.py) no longer touches user.email at all, falls back to username. Confirmed no other field in PublicUserResponse/to_public_user_response derives from email or the private names. Good.

2. Test fixture / regression coverage — still not addressed. conftest.py is unchanged, still just the bare TestClient, no db session or user/workspace factories, and no test was added for this endpoint. This was the other blocking point last round — still need it before merge, same as before: a minimal db-backed fixture plus a test pinning "no shared workspace → reduced profile, no email" and "self/shared workspace → full profile," so the next change to shares_workspace or the response model can't silently reopen #25.

Leaving this as requesting changes until the test lands - happy to approve as soon as it's in.
@SankeerthNara

…evlaner#25)

Signed-off-by: SankeerthNara <sankeerthnara@gmail.com>
@SankeerthNara

Copy link
Copy Markdown
Contributor Author

Added regression tests in tests/test_users.py covering: no-shared-workspace hides email, self-lookup and shared-workspace both return full profile, and a dedicated test for the display_name email-leak fix. Added minimal DB fixtures in conftest.py (transactional per-test session against a loomy_test Postgres DB, plus a get_current_user override for auth). All 42 tests pass, ruff clean, mypy clean (same 5 pre-existing unrelated errors as before this PR).

@SankeerthNara

Copy link
Copy Markdown
Contributor Author

hey @nazarli-shabnam , i think i fixed the requested changes.
please have a look and notify me for any other issues

@nazarli-shabnam nazarli-shabnam left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed after d6e3f1d.

Regression tests — good coverage. test_users.py covers the four cases that matter: no shared workspace hides email, self-lookup and shared-workspace both return the full profile, and the display_name-from-email regression specifically. Thanks for adding these.

New blocker: the test suite now requires a live Postgres, and CI doesn't have one. conftest.py's db fixture connects to postgresql://postgres:postgres@localhost:15432/loomy_test, and client now depends on db. .github/workflows/api-ci.yml runs uv run pytest on a bare ubuntu-latest runner — no services: block, no loomy_test database ever created. This doesn't just affect the new tests: test_api_auth.py and test_ws_auth_handshake.py already use client, so as it stands this change breaks the entire suite in CI, not just adds coverage locally. Needs a Postgres service (and db creation/migration step) wired into api-ci.yml, or the fixture needs to fall back to something that doesn't need real infra (e.g. sqlite for these tests, or a testcontainers-style spin-up) — whichever this repo's pattern is elsewhere.

Also, unrelated nit while I was in here: double-checked the PR description's mypy claim ("5 pre-existing errors in app/core/redis.py and venv-bundled stubs, unrelated to this PR") — ran uv run mypy . and uv run ruff check . fresh against main, the PR's actual merge-base, and this PR's head. All three come back completely clean (0 errors) on both. Not blocking, just flagging so the description doesn't stay misleading if this lands.

Requesting changes again for the CI/Postgres gap — everything else here looks solid.

Signed-off-by: SankeerthNara <sankeerthnara@gmail.com>

@nazarli-shabnam nazarli-shabnam left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the contribution!

@SankeerthNara

Copy link
Copy Markdown
Contributor Author

Are there any changes to make @nazarli-shabnam

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

🧹 Nitpick comments (1)
.github/workflows/api-ci.yml (1)

9-13: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add a permissions: block to restrict the GITHUB_TOKEN scope.

The lint-and-test job has no explicit permissions: block, so the workflow uses the repository's default token permissions — which may include write access. Since this job only runs linting, migrations, and tests, it only needs read access to the repository contents.

🔒 Proposed fix: add minimal permissions block
   lint-and-test:
     runs-on: ubuntu-latest
+    permissions:
+      contents: read
     defaults:
       run:
         working-directory: api/app
🤖 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 @.github/workflows/api-ci.yml around lines 9 - 13, Add a permissions block to
the lint-and-test job in the workflow, granting only contents: read for the
GITHUB_TOKEN. Keep the restriction scoped to the lint-and-test job and leave its
existing run configuration unchanged.

Source: Linters/SAST tools

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

Nitpick comments:
In @.github/workflows/api-ci.yml:
- Around line 9-13: Add a permissions block to the lint-and-test job in the
workflow, granting only contents: read for the GITHUB_TOKEN. Keep the
restriction scoped to the lint-and-test job and leave its existing run
configuration unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a8d54acc-8903-4fce-a1ec-2704e0677671

📥 Commits

Reviewing files that changed from the base of the PR and between 1178e58 and a83a444.

📒 Files selected for processing (5)
  • .github/workflows/api-ci.yml
  • api/app/app/modules/users/presenter.py
  • api/app/tests/__init__.py
  • api/app/tests/conftest.py
  • api/app/tests/test_users.py

@SankeerthNara

Copy link
Copy Markdown
Contributor Author

Sorry i pressed re-review by mistake

@nazarli-shabnam

Copy link
Copy Markdown
Member

@SankeerthNara fix the coderabbit comment and you are good to go

Signed-off-by: SankeerthNara <sankeerthnara@gmail.com>
@nazarli-shabnam
nazarli-shabnam merged commit 012352d into Devlaner:main Jul 10, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

IDOR: GET /api/users/{user_id} leaks any user's email to any authenticated user

3 participants