From 8f044849d47df23f3cf10f4ae2872982ac9e3278 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Fri, 10 Apr 2026 04:17:31 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20get=5Ftree=20returns=20404=20(not=20403)?= =?UTF-8?q?=20for=20inaccessible=20trees=20=E2=80=94=20don't=20leak=20reso?= =?UTF-8?q?urce=20existence?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- backend/app/api/endpoints/trees.py | 5 +-- backend/tests/test_trees.py | 52 ++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/backend/app/api/endpoints/trees.py b/backend/app/api/endpoints/trees.py index c24edca2..1ac4e9d8 100644 --- a/backend/app/api/endpoints/trees.py +++ b/backend/app/api/endpoints/trees.py @@ -392,9 +392,10 @@ async def get_tree( ) if not tree.is_active or not can_access_tree(current_user, tree): + # Always 404, never 403. A 403 confirms the resource exists. raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="You don't have access to this tree" + status_code=status.HTTP_404_NOT_FOUND, + detail="Tree not found" ) return build_full_tree_response(tree) diff --git a/backend/tests/test_trees.py b/backend/tests/test_trees.py index 300a50f6..8a79c6fc 100644 --- a/backend/tests/test_trees.py +++ b/backend/tests/test_trees.py @@ -447,3 +447,55 @@ class TestVisibilityFilter: assert "author_name" in trees[0] # visibility key should be present assert "visibility" in trees[0] + + @pytest.mark.asyncio + async def test_get_tree_returns_404_not_403_for_other_account_tree( + self, client: AsyncClient, auth_headers: dict, test_db: AsyncSession + ): + """Account A must not learn that Account B's private tree exists.""" + from app.models.tree import Tree + from app.models.account import Account + from app.models.user import User + from app.core.security import get_password_hash + import uuid + + # Create a second account and user + account_b = Account(name="Other Corp", display_code="OTH00001") + test_db.add(account_b) + await test_db.flush() + + user_b = User( + email=f"user-b-{uuid.uuid4().hex[:6]}@example.com", + name="User B", + password_hash=get_password_hash("TestPass123!"), + is_active=True, + account_id=account_b.id, + account_role="engineer", + ) + test_db.add(user_b) + await test_db.flush() + + # Create a private tree belonging to account_b + private_tree = Tree( + name="Secret Tree", + account_id=account_b.id, + author_id=user_b.id, + visibility="private", + tree_type="troubleshooting", + tree_structure={"id": "root", "type": "start", "children": []}, + is_active=True, + is_default=False, + is_public=False, + status="published", + ) + test_db.add(private_tree) + await test_db.commit() + + response = await client.get( + f"/api/v1/trees/{private_tree.id}", + headers=auth_headers, + ) + assert response.status_code == 404, ( + f"Expected 404 but got {response.status_code} — " + "leaking tree existence to wrong tenant" + )