
This one is nothing too fancy, no injection, no RCE, just a missing-auth bug. But it sat on a live, public app, tts.niaefeup.pt (now fixed), and let anyone on the internet, no login, not even a university account, pull up a random student's full name, student number, class schedule and photo. Who they are, what they look like, and where they'll physically be and when. My first assigned CVE, CVE-2026-88065.
TTS is NIAEFEUP's own timetable tool, a backend built by the Informatics students' association at FEUP to help students build and check their schedules. To do that, it needs data that normally lives behind a login on Sigarra, the university's official academic system: course lists, enrolled students, class allocations, student photos. Since nobody wants every student authenticating against Sigarra just to look up a timetable, TTS does the fetching itself, using what I'd guess is a single internal "master" account that stays logged into Sigarra and scrapes data on everyone's behalf.
That's a reasonable design. The problem is what happens after the scrape.
Several of TTS's own API endpoints, the ones that hand back whatever the master account just pulled from Sigarra, had no authentication check of their own. So it didn't matter who was asking, or whether they were logged into TTS at all. Anyone could call these endpoints directly and get back data that Sigarra itself would never show without a login.
Chaining them together, you can walk from "a course exists" to "here is a specific student's name, student number, class schedule and photo", with zero credentials at any point:
GET /api/course/2025
# list the courses for a given year, pick a target (e.g. a specific Master's)
GET /api/course_units/<course_id>/2025/<semester>/
# list that course's curricular units for a semester
GET /api/course_unit/<unit_id>/exchange/metadata
# full name + student number (NMEC) of every student enrolled
GET /api/class/<unit_id>/
# which class and time slot each of those students is allocated to
GET /api/student/<nmec>/photo
# that student's photo, keyed off the NMEC from the step above
exchange/metadata. This endpoint returns full names and student numbers for everyone enrolled in a course unit. On Sigarra, that same data is restricted to authenticated users with the right privileges. Here, it was open to anyone./api/student/<nmec>/photo. Given a student number, the backend fetches that student's photo from Sigarra through its own session and serves it straight back, no check on whether the requester has any business asking.Chained together, these endpoints hand over a photo, a full name, a student number and a weekly class schedule for any student at the university, to anyone, no account, no login, no affiliation to the university needed. That's identity, face and physical location, enough on its own to track a specific person down in real life. tts.niaefeup.pt was live and public the whole time this was open, and on top of the personal safety angle it's also a straightforward GDPR problem: that much PII exposed with no legal basis or access control behind it isn't a grey area.
The project's own urls.py already had the right pattern elsewhere, an exchange_admin_required()-style decorator, it just wasn't applied consistently. The fix was to require authentication across the board on every endpoint touching student data, restrict photos and enrollment lists to the student themselves or a verified admin, and drop the unauthenticated Sigarra-proxy behavior entirely. All patched in tts-be 2.1.0.
CVSS 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N), no privileges or user interaction needed, pure confidentiality impact.