administrar-actions.ts: "use server" file exports roster getters with no in-function gate (getDocenteMemberships, getAlumnoAssignments, getAlumnoAssignmentsForPeriodos, getComisionesWithCounts, getPeriodoDetail...)
Found by audit-general-cc during MARS-744 static walk (pre-existing, all roles, not lector-specific). src/lib/administrar-actions.ts is a "use server" file; its getters at lines ~177-490 (e.g. getDocenteMemberships:383, getAlumnoAssignments:425, getAlumnoAssignmentsForPeriodos:456) have NO requireRosterManager/cap check inside the function (only the write actions and searchTeacherUsers/searchStudentUsers call requireRosterManager). They return student/docente name+email. Callers today are pages that gate themselves (administrar layout requireCap manageRoster, alumnos/comisiones pages requireCap viewStudents|manageRoster) and editar-form.tsx (client) imports some of them. NOT VERIFIED: whether unreferenced exports are callable as server actions by an arbitrary authenticated user (Next registers exports of a use-server file as actions; production pruning of unused ones is version dependent). Verify by executing, not by reading: call a getter as an alumno session against the deployed build. Fix direction: put the gate inside each getter (requireRosterManager or the same cap the caller page uses) or move the read helpers out of the use-server file so they are plain server-only modules. Do not narrow any page permission to make a list match (ownership vs visibility rule).
Activity
-
Merged from MARS-749 (filed in parallel by support-evolutiva-cc, now closed as a duplicate of this). Adds the source verification and, more importantly, the BOUND on the claim. VERIFIED by source read 2026-09-19: the module is "use server", so every export is a registered action endpoint, not an internal helper. getDocenteMemberships(:383) SELECTs u.displayName + u.email for every docente in a periodo; getAlumnoAssignments(:425) SELECTs u.displayName AS studentName + u.email AS studentEmail for the ENTIRE student roster of a periodo plus JTP and ayudante names. So the exposure shape is a bulk name+email dump of a whole cohort from a single periodoId argument. The mutators ARE gated (requireRosterManager at 492, 512, 542, 578, 606, 650, 703, 739, 770, 799, 843...) and getAssignableTeachersForComision self-gates at :248 on manageRoster OR viewStudents, which is the predicate the ungated siblings should have been using. does NOT help: it blocks client bundling, not server-action invocation. src/middleware.ts performs NO authentication - it stamps x-mars-pathname and nothing else - so there is no global gate behind these. The (protected) layout redirects an unauthenticated session to /login but does not cap-check. NOT VERIFIED, and the claim must not be made without it: whether an action ID for these exports is obtainable by a caller. The only importers are src/app/(protected)/alumnos/page.tsx and docentes/page.tsx, both SERVER components, so the IDs are probably not in any client bundle; Next registers "use server" exports in the server manifest regardless, so the open question is whether an ID can be derived or leaked, not whether the endpoint exists. Nobody has attempted an invocation. This is defence-in-depth with an UNPROVEN exploit path - do not call it a breach, and do not downgrade it on that basis either, because the gate costs one line. The UI routes are correctly gated and are not the issue: alumnos/page.tsx:36 requireCap(viewStudents, manageRoster), docentes/page.tsx:22 requireCap(manageRoster). Structural fix preferred over adding gates one by one: move plain data getters OUT of the "use server" module into a server-only data module the pages import directly. A getter has no business being an action endpoint. audit-general-cc's alternative is requireRosterManager inside each. Also raised by audit-general-cc and unverified by anyone: /api/profile-image serves profile photos to any authenticated user, and whether student photos render on case pages was not checked.
-
Typo repair on the previous note: one phrase was lost to a shell substitution. The sentence reading "... does NOT help: it blocks client bundling, not server-action invocation" refers to the import "server-only" statement at the top of administrar-actions.ts. That import blocks the module from being bundled into a client component; it does nothing to stop the exported functions being invoked as server actions. Nothing else in that note was affected.
-
coder-mars-ca
-
Reassigned coder-mars-cc -> coder-mars-ca by support-evolutiva-cc: Elazar direct, relayed by coder-mars-cc 2026-09-19 05:39 ART (tag coder-mars-cc-mu84z5ae8oie) - coder-mars-ca replaces it as Mars coder + PM and coder-mars-cc has stopped work. coder-mars-ca verified live on the hub at reassign time. Scope of this reassignment: only the four items support-evolutiva-cc filed from the MARS-744 lector walk. The other nine open Mars items still on coder-mars-cc are flagged to coder-mars-ca and pmmaster-evolutiva-cc, not moved by me - not my lane.
-
Class-A design audit requested; inventorying server-action exports and caller scopes. No authz edit yet.
-
Source inventory: use-server read exports include 12 ungated getters; PII bulk getters getDocenteMemberships and getAlumnoAssignments(*) are used only by manageRoster pages. getComisionDetail is also used by scoped teacher detail; period/comision selector getters serve viewStudents clinical/exam pages. getAssignableTeachersForComision is already gated. A uniform manageRoster gate would break existing teacher flows. Independent Class-A design ping sent before edits; no live alumno action invocation attempted, exploitability remains unverified.
-
Pre-push structural diff in clean clone: 13 read-only getters moved from use-server administrar-actions.ts into server-only administrar-reads.ts; server page/layout and exam-scores imports updated; existing scoped getAssignableTeachersForComision stays an action. Self-check corpus updated: 3 light selectors probed, 2 bulk email-roster reads explicitly not probed. 980/980 tests, tsc, diff --check pass. Independent Class-A diff review requested before push; no prod build/runtime verification yet.
-
822605576a42 / v2.85.13 pushed to origin/main after independent Class-A source PASS. 13 ungated reads moved from use-server to server-only module, caller imports updated; 980/980 tests and push-gate lint/typegen passed. Production deploy and runtime-log checks pending; no authenticated action-ID invocation exercised.
-
Post-push PTD: 822605576a42/v2.85.13, GitHub exact-commit Vercel status success; Vercel REST deployment dpl_DcmjZX9pTTj7otEcPQ5tDk6xhkuY is READY for mars project; live cache-busted /api/health returns same deploymentId/version. Vercel build events: 144 rows, 0 fatal, build completed in /vercel/output; stderr limited to existing Sass @import and middleware convention warnings. Runtime-log REST stream authenticated but returned 0 bytes over 20s even while /api/health returned 200; this is NO COVERAGE, not zero errors. No authenticated alumno action-ID or allowed teacher/admin roster path exercised; row remains inProgress pending dynamic authz verification/usable runtime signal.
-
2026-10-06 standing-query audit: changed roster readers in src/lib/administrar-reads.ts emit no success appEvents; protected caller pages also do not emit a read-success event. Therefore existing appEvents rows, even after demonstrated general authenticated traffic, cannot prove these specific getter invocations or action-ID authorization. This seat Supabase execute_sql returned FgaApiAuthenticationError Unauthorized; no coder-mars-ca DB credential exists. Exact-commit deploy/health proof remains valid, but authenticated allowed/denied path execution remains unmeasured. No fixture or borrowed credential used.
-
Scope correction to my preceding note: I checked the changed getter module for success logging, not every caller page. The supported claim is that the 13 moved getter functions themselves emit no success appEvents; any broader assertion about all caller-page telemetry is unverified. A route-level event, if present, would still need to be tied to the specific getter/action-ID path before it could close this gate.
-
Source fix 8226055/v2.85.13 and production deploy are verified; closing Class-A dynamic gate requires an authorized authenticated alumno action-ID denial plus allowed roster-path exercise, neither available from this seat. Getter success is not logged; existing appEvents and empty Vercel runtime stream cannot prove invocation. No student credential/session may be borrowed or fabricated. Await a named consenting actor or independently authorized test session; source structural PASS is not runtime proof.
-
coder-mars-cc