Skip to content

⚔️ Vanguard: [Advanced Security Enhancement/Fix] IDOR in Teacher Notes#224

Open
ldsgroups225 wants to merge 1 commit intomasterfrom
fix/vanguard-teacher-notes-idor-790790760363773327
Open

⚔️ Vanguard: [Advanced Security Enhancement/Fix] IDOR in Teacher Notes#224
ldsgroups225 wants to merge 1 commit intomasterfrom
fix/vanguard-teacher-notes-idor-790790760363773327

Conversation

@ldsgroups225
Copy link
Copy Markdown
Owner

@ldsgroups225 ldsgroups225 commented Mar 31, 2026

Vulnerability Path: The updateStudentNote and deleteStudentNote server functions in apps/teacher/src/teacher/functions/student-notes.ts lacked a getTeacherContext() call to enforce an active session and retrieve the user's schoolId. Consequently, these functions passed an empty teacherId and omitted passing a schoolId to the backend data functions. The backend queries in packages/data-ops/src/queries/teacher-notes.ts were executing UPDATE and DELETE statements on the conductRecords table solely by noteId, opening a direct IDOR/BOLA vulnerability where a user in Tenant A could modify or delete notes belonging to Tenant B by guessing the noteId.

Action Taken: Added getTeacherContext() to the updateStudentNote and deleteStudentNote server functions to explicitly retrieve the active session context and explicitly pass the schoolId and teacherId arguments. Updated the backend updateStudentNote and deleteStudentNote query parameters to mandate schoolId and applied a strict and(eq(conductRecords.id, params.noteId), eq(conductRecords.schoolId, params.schoolId)) condition to the Drizzle WHERE clauses, effectively eliminating the IDOR/BOLA vulnerability by strictly enforcing tenant isolation.


PR created automatically by Jules for task 790790760363773327 started by @ldsgroups225

Summary by CodeRabbit

  • Bug Fixes
    • Improved security for student note operations with enhanced authorization checks and proper school context enforcement during updates and deletions.

* Fixed missing tenant-isolation in updateStudentNote and deleteStudentNote queries by enforcing a schoolId check in the where clause
* Fixed missing session validation in updateStudentNote and deleteStudentNote server functions by adding getTeacherContext() to ensure an active session
* Passed down retrieved context schoolId and teacherId to backend queries
@google-labs-jules
Copy link
Copy Markdown

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

📝 Walkthrough

Walkthrough

This change addresses a multi-tenant isolation vulnerability (IDOR/BOLA) in student note operations. updateStudentNote and deleteStudentNote functions now retrieve teacher authentication context, enforce authorization checks, propagate schoolId to backend data operations, and constrain database queries to include both noteId and schoolId conditions.

Changes

Cohort / File(s) Summary
Security Documentation
.jules/vanguard.md
Added vulnerability entry documenting IDOR/BOLA issue where operations omitted context retrieval and tenant enforcement, with intended fix specifications.
Server Function Authorization
apps/teacher/src/teacher/functions/student-notes.ts
Added getTeacherContext() calls with early-exit unauthorized check; now forwards schoolId and teacherId from authenticated context to data layer functions.
Data Query Tenant Enforcement
packages/data-ops/src/queries/teacher-notes.ts
Updated updateStudentNote and deleteStudentNote to require schoolId parameter and constrain WHERE clauses to match both noteId and schoolId for tenant isolation.

Sequence Diagram

sequenceDiagram
    participant Client
    participant ServerFn as Server Function<br/>(student-notes.ts)
    participant AuthCtx as getTeacherContext()
    participant DataLayer as Data Query<br/>(teacher-notes.ts)
    participant DB as Database

    Client->>ServerFn: updateStudentNote(noteId, ...)
    ServerFn->>AuthCtx: getTeacherContext()
    AuthCtx-->>ServerFn: { teacherId, schoolId } or null
    alt No Auth Context
        ServerFn-->>Client: { success: false, error: 'UNAUTHORIZED' }
    else Authorized
        ServerFn->>DataLayer: updateStudentNote({ schoolId, noteId, teacherId, ... })
        DataLayer->>DB: UPDATE conductRecords<br/>WHERE id = noteId<br/>AND schoolId = schoolId
        DB-->>DataLayer: row updated
        DataLayer-->>ServerFn: { success: true }
        ServerFn-->>Client: { success: true }
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • PR #195: Implements the same multi-tenant fix pattern—adding schoolId parameters, tightening Drizzle WHERE clauses in data-ops mutations, and updating server call sites.
  • PR #133: Implements the same multi-tenant authorization guard pattern using getTeacherContext() and context-derived schoolId/teacherId in teacher server functions.
  • PR #149: Modifies the teacher-notes flow to propagate and enforce schoolId constraints in both data-ops and server handlers.

Poem

🐰 A teacher's notes now guarded well,
With context checked and tenants held,
Each schoolId firmly in its place,
No cross-school breach in this race!
Security hops from layer to layer, 🛡️

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main security fix—an IDOR vulnerability in teacher notes—and clearly identifies it as a Vanguard security enhancement, accurately reflecting the core change.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/vanguard-teacher-notes-idor-790790760363773327

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/teacher/src/teacher/functions/student-notes.ts (2)

147-167: ⚠️ Potential issue | 🟡 Minor

Security fix is correct, but backend errors are not handled.

The getTeacherContext() check and schoolId propagation correctly address the IDOR vulnerability. However, updateNote() can throw "Failed to update note" when no row is returned. This exception will propagate to the client unhandled.

As per coding guidelines: "Do not throw raw exceptions in server functions; handle errors appropriately before returning results."

🛡️ Proposed fix to wrap the call in try-catch
     const { updateStudentNote: updateNote } = await import('@repo/data-ops/queries/teacher-notes')
-    const note = await updateNote({
-      schoolId: context.schoolId,
-      noteId: data.noteId,
-      teacherId: context.teacherId,
-      title: data.title,
-      content: data.content,
-      priority: data.priority,
-    })
-
-    return {
-      success: true,
-      note,
+    try {
+      const note = await updateNote({
+        schoolId: context.schoolId,
+        noteId: data.noteId,
+        teacherId: context.teacherId,
+        title: data.title,
+        content: data.content,
+        priority: data.priority,
+      })
+      return { success: true, note }
+    } catch {
+      return { success: false, error: 'UPDATE_FAILED' }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/teacher/src/teacher/functions/student-notes.ts` around lines 147 - 167,
Wrap the call to updateNote (imported as updateStudentNote/updateNote) inside a
try-catch after the getTeacherContext() check so backend exceptions are not
propagated to the client; catch any thrown error from updateNote and return a
controlled failure response (e.g. { success: false, error: 'UPDATE_FAILED' } or
include a sanitized message) instead of letting the raw exception bubble up, and
log the original error server-side for diagnostics. Ensure this change occurs in
the same server function that calls getTeacherContext() and returns the note.

173-189: ⚠️ Potential issue | 🟡 Minor

Same error handling concern as updateStudentNote.

The security fix is correctly implemented. Database errors from deleteNote() should be caught and returned as a failure response rather than propagating as raw exceptions.

🛡️ Proposed fix
     const { deleteStudentNote: deleteNote } = await import('@repo/data-ops/queries/teacher-notes')
-    await deleteNote({
-      schoolId: context.schoolId,
-      noteId: data.noteId,
-      teacherId: context.teacherId,
-    })
-
-    return {
-      success: true,
+    try {
+      await deleteNote({
+        schoolId: context.schoolId,
+        noteId: data.noteId,
+        teacherId: context.teacherId,
+      })
+      return { success: true }
+    } catch {
+      return { success: false, error: 'DELETE_FAILED' }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/teacher/src/teacher/functions/student-notes.ts` around lines 173 - 189,
The deleteStudentNote handler currently calls deleteNote(...) (imported from
`@repo/data-ops/queries/teacher-notes`) without catching database errors; mirror
the updateStudentNote fix by wrapping the deleteNote call in a try/catch after
getTeacherContext() succeeds and return a failure response instead of letting
exceptions bubble up — e.g., catch any error from deleteNote and return {
success: false, error: error.message || 'INTERNAL_ERROR' } (or a standardized
error code) while preserving the existing UNAUTHORIZED check using
getTeacherContext().
packages/data-ops/src/queries/teacher-notes.ts (1)

130-161: ⚠️ Potential issue | 🟠 Major

ResultAsync pattern is mandatory for data-ops queries — refactor to return ResultAsync<T, DatabaseError> instead of throwing.

The schoolId constraint in the WHERE clause correctly prevents cross-tenant modifications. However, this function violates the mandatory pattern for packages/data-ops/src/queries/**/*.ts: it must return ResultAsync<T, DatabaseError> from @praha/byethrow and never throw raw exceptions. All other query files in this directory follow this pattern. Replace the throw new Error(...) with Result monad error handling and attach .mapErr(tapLogErr(logger, context)) for consistent error logging.

Additionally, teacherId is accepted as a parameter but never used in the query logic. If the intent is to restrict updates to only the teacher who created the note, add an authorization check to the WHERE clause; otherwise, remove the unused parameter.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/data-ops/src/queries/teacher-notes.ts` around lines 130 - 161, The
updateStudentNote function currently throws on failure and ignores the teacherId
param; refactor it to return
ResultAsync<{id,title,content,type,priority,createdAt}, DatabaseError> (from
`@praha/byethrow`) instead of throwing: convert the DB call block
(getDb().update(conductRecords).set(...).where(...).returning()) into a
ResultAsync, map the successful row to the expected return shape, and replace
the throw new Error('Failed to update note') with a ResultAsync failure using
the DatabaseError type; also attach .mapErr(tapLogErr(logger, {context:
'updateStudentNote', noteId: params.noteId})) to the result for consistent
logging. While refactoring either remove the unused teacherId parameter from
updateStudentNote or enforce authorization by adding
eq(conductRecords.teacherId, params.teacherId) to the WHERE clause so the
tenant+teacher constraint is applied.
🧹 Nitpick comments (1)
apps/teacher/src/teacher/functions/student-notes.ts (1)

107-125: Consider: Read operations lack tenant scoping.

While this PR correctly fixes the write operations, getStudentNotes, getBehaviorSummary, and getNotesTrend do not call getTeacherContext() or filter by schoolId. A teacher knowing a student ID from another school could potentially read their notes.

This is out of scope for this security fix but worth tracking separately. Based on learnings: "All database queries involving school-scoped data must include tenant filtering with where(eq(table.schoolId, ctx.schoolId))."

Do you want me to open an issue to track adding tenant scoping to the read operations?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/teacher/src/teacher/functions/student-notes.ts` around lines 107 - 125,
getStudentNotes (and similarly getBehaviorSummary and getNotesTrend) perform
read operations without tenant scoping; update each handler to call
getTeacherContext() to obtain ctx and ensure all DB queries filter by the
teacher's schoolId (e.g., add where(eq(table.schoolId, ctx.schoolId)) in the
query builder) so reads are limited to the teacher's school; locate these
handlers by the exported function names getStudentNotes, getBehaviorSummary,
getNotesTrend and add the context fetch and schoolId where-clause to the
imported query calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@apps/teacher/src/teacher/functions/student-notes.ts`:
- Around line 147-167: Wrap the call to updateNote (imported as
updateStudentNote/updateNote) inside a try-catch after the getTeacherContext()
check so backend exceptions are not propagated to the client; catch any thrown
error from updateNote and return a controlled failure response (e.g. { success:
false, error: 'UPDATE_FAILED' } or include a sanitized message) instead of
letting the raw exception bubble up, and log the original error server-side for
diagnostics. Ensure this change occurs in the same server function that calls
getTeacherContext() and returns the note.
- Around line 173-189: The deleteStudentNote handler currently calls
deleteNote(...) (imported from `@repo/data-ops/queries/teacher-notes`) without
catching database errors; mirror the updateStudentNote fix by wrapping the
deleteNote call in a try/catch after getTeacherContext() succeeds and return a
failure response instead of letting exceptions bubble up — e.g., catch any error
from deleteNote and return { success: false, error: error.message ||
'INTERNAL_ERROR' } (or a standardized error code) while preserving the existing
UNAUTHORIZED check using getTeacherContext().

In `@packages/data-ops/src/queries/teacher-notes.ts`:
- Around line 130-161: The updateStudentNote function currently throws on
failure and ignores the teacherId param; refactor it to return
ResultAsync<{id,title,content,type,priority,createdAt}, DatabaseError> (from
`@praha/byethrow`) instead of throwing: convert the DB call block
(getDb().update(conductRecords).set(...).where(...).returning()) into a
ResultAsync, map the successful row to the expected return shape, and replace
the throw new Error('Failed to update note') with a ResultAsync failure using
the DatabaseError type; also attach .mapErr(tapLogErr(logger, {context:
'updateStudentNote', noteId: params.noteId})) to the result for consistent
logging. While refactoring either remove the unused teacherId parameter from
updateStudentNote or enforce authorization by adding
eq(conductRecords.teacherId, params.teacherId) to the WHERE clause so the
tenant+teacher constraint is applied.

---

Nitpick comments:
In `@apps/teacher/src/teacher/functions/student-notes.ts`:
- Around line 107-125: getStudentNotes (and similarly getBehaviorSummary and
getNotesTrend) perform read operations without tenant scoping; update each
handler to call getTeacherContext() to obtain ctx and ensure all DB queries
filter by the teacher's schoolId (e.g., add where(eq(table.schoolId,
ctx.schoolId)) in the query builder) so reads are limited to the teacher's
school; locate these handlers by the exported function names getStudentNotes,
getBehaviorSummary, getNotesTrend and add the context fetch and schoolId
where-clause to the imported query calls.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8b8de68b-7df3-4c9f-842e-370b99b7267f

📥 Commits

Reviewing files that changed from the base of the PR and between 7e37ef8 and 8b133a6.

📒 Files selected for processing (3)
  • .jules/vanguard.md
  • apps/teacher/src/teacher/functions/student-notes.ts
  • packages/data-ops/src/queries/teacher-notes.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant