Nobody has started this yet — be first.
Business impact
An Application row contains a user's employer names, the roles they're interviewing for, and freeform private notes -- the kind of information someone very much does not want their current employer, a recruiter, or a stranger to see. Right now, any authenticated user can list every other user's applications just by calling GET /api/applications/ as themselves -- company names, roles, and notes included. This is a confidentiality breach across every account on the system, the kind of finding that turns into a mandatory disclosure, a trust-destroying incident for a tool whose entire premise is "track your confidential job search privately," and, per the project's own README (which documents user isolation as a core guarantee), a direct contradiction of a promise already made to users.
Problem
get_queryset() returns Application.objects.all().select_related("user").prefetch_related("status_changes") -- every application belonging to every user, not just the one making the request. The comment directly above the method still describes the intended (and previously actual) behavior of filtering by the requesting user before any lookup by id, which is exactly what makes this easy to miss on a quick read: the code "looks right" if you only read the explanation above it.
Current behavior
GET /api/applications/ returns every user's rows, not just the caller's (a list leak), and GET /api/applications/{id}/ for another user's application returns 403 instead of the documented 404 -- which itself proves the row was found in the first place.
Expected behavior
get_queryset() returns only applications owned by self.request.user. Listing as any user returns only that user's own applications. Fetching another user's application by id returns 404, matching the documented contract in README.md.
Steps to reproduce
python manage.py shell -c " from django.contrib.auth import get_user_model from applications.models import Application from rest_framework.test import APIClient User = get_user_model() alice = User.objects.create_user(username='alice6', password='pw-alice6-123') bob = User.objects.create_user(username='bob6', password='pw-bob6-12345') Application.objects.create(user=alice, company='Acme', role='Engineer', notes='confidential') c = APIClient(); c.force_authenticate(user=bob) print('list count (should be 0):', c.get('/api/applications/').data['count']) "
Why this matters
This is the exact scenario applications/permissions.py's IsOwner docstring names as a hypothetical: a queryset regression that removes the real defense, leaving only the weaker object-level backstop, which changes the error code (403 instead of 404) but does nothing about the list endpoint at all, since IsOwner.has_object_permission is only ever consulted for detail-route actions (retrieve/update/partial_update/destroy/timeline/status), never for list.
Suggested approach
Compare the current get_queryset() against the comment directly above it and against README.md's "Ownership" section, which documents the exact intended behavior this method is supposed to implement.
Acceptance criteria
Verification
python manage.py test practicetickets.ticket06_cross_user_application_leak -v 2
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
$git checkout -b fix/cross-user-application-leakFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/cross-user-application-leakSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign in.envhttp://127.0.0.1:8000/api/http://127.0.0.1:8000/admin/To run against Postgres via Docker instead:
docker compose up --build # postgres + web, migrations run automatically on startup
Work the tickets in practicetickets/ (TICKET_01 through TICKET_07); each names one dedicated test, addressed by its explicit dotted label since these modules are deliberately named so Django's default test*.py discovery never picks them up:
python manage.py test practicetickets.ticket01_login_credential_swap -v 2 # a single ticket
./practicetickets/run_tickets.sh # all 7, clean pass/fail summary
Two of the seven bugs (tickets 01 and 06) are real enough to also break the project's own pre-existing suite (python manage.py test) -- fixing them correctly makes those pass again too, with zero changes needed inside accounts/tests.py or applications/tests.py.
Level 1
Fix a bug
Read existing behaviour, correct it.