-
Notifications
You must be signed in to change notification settings - Fork 23
Fix bug hunt issues: authorization, validation, and schema mismatches #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7673e3b
7946b2e
f929f38
a3d9b5d
4435784
04be217
be6b9a4
89fccaa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,79 +1,100 @@ | ||
| import { auth } from '@clerk/nextjs/server' | ||
| import { NextRequest, NextResponse } from 'next/server' | ||
| import mongoose from 'mongoose' | ||
| import { connectDB } from '@/lib/mongodb' | ||
| import { Announcement } from '@/models/Announcement' | ||
| import { auth } from "@clerk/nextjs/server"; | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
| import mongoose from "mongoose"; | ||
| import { connectDB } from "@/lib/mongodb"; | ||
| import { Announcement } from "@/models/Announcement"; | ||
|
|
||
| const ALLOWED_FIELDS = ['title', 'content', 'body', 'audience', 'category', 'pinned', 'expiresAt'] | ||
| const ALLOWED_FIELDS = ["title", "content", "audience", "category", "pinned"]; | ||
|
|
||
| export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { | ||
| const { userId } = await auth() | ||
| if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) | ||
| export async function PUT( | ||
| req: NextRequest, | ||
| ctx: { params: Promise<{ id: string }> }, | ||
| ) { | ||
| const { userId } = await auth(); | ||
| if (!userId) | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
|
|
||
| try { | ||
| const { id } = await ctx.params | ||
| const { id } = await ctx.params; | ||
|
|
||
| // Validate ObjectId | ||
| if (!mongoose.Types.ObjectId.isValid(id)) { | ||
| return NextResponse.json({ error: 'Invalid id' }, { status: 400 }) | ||
| return NextResponse.json({ error: "Invalid id" }, { status: 400 }); | ||
| } | ||
|
|
||
| await connectDB() | ||
| let body | ||
| await connectDB(); | ||
|
|
||
| let body; | ||
| try { | ||
| body = await req.json() | ||
| body = await req.json(); | ||
| } catch { | ||
| return NextResponse.json({ error: 'Invalid JSON request body' }, { status: 400 }) | ||
| return NextResponse.json( | ||
| { error: "Invalid JSON request body" }, | ||
| { status: 400 }, | ||
| ); | ||
| } | ||
|
|
||
| // Sanitize: only allow whitelisted fields | ||
| const sanitizedBody: Record<string, unknown> = {} | ||
| const sanitizedBody: Record<string, unknown> = {}; | ||
| for (const key of ALLOWED_FIELDS) { | ||
| if (key in body) { | ||
| sanitizedBody[key] = body[key] | ||
| sanitizedBody[key] = body[key]; | ||
| } | ||
| } | ||
|
|
||
| const announcement = await Announcement.findOneAndUpdate( | ||
| { _id: id }, | ||
| { _id: id, teacherId: userId }, | ||
| { $set: sanitizedBody }, | ||
| { new: true, runValidators: true, context: 'query' } | ||
| ) | ||
| if (!announcement) return NextResponse.json({ error: 'Not found' }, { status: 404 }) | ||
| return NextResponse.json(announcement) | ||
| { new: true, runValidators: true, context: "query" }, | ||
| ); | ||
| if (!announcement) | ||
| return NextResponse.json({ error: "Not found" }, { status: 404 }); | ||
| return NextResponse.json(announcement); | ||
| } catch (error) { | ||
| if (error instanceof Error) { | ||
| console.error('PUT /api/announcements/[id] error:', error.message) | ||
| console.error("PUT /api/announcements/[id] error:", error.message); | ||
| } | ||
| return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) | ||
| return NextResponse.json( | ||
| { error: "Internal server error" }, | ||
| { status: 500 }, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: string }> }) { | ||
| const { userId } = await auth() | ||
| if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) | ||
| export async function DELETE( | ||
| _req: NextRequest, | ||
| ctx: { params: Promise<{ id: string }> }, | ||
| ) { | ||
| const { userId } = await auth(); | ||
| if (!userId) | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
|
|
||
| try { | ||
| const { id } = await ctx.params | ||
| const { id } = await ctx.params; | ||
|
|
||
| // Validate ObjectId | ||
| if (!mongoose.Types.ObjectId.isValid(id)) { | ||
| return NextResponse.json({ error: 'Invalid id' }, { status: 400 }) | ||
| return NextResponse.json({ error: "Invalid id" }, { status: 400 }); | ||
| } | ||
|
|
||
| await connectDB() | ||
| const deleted = await Announcement.findOneAndDelete({ _id: id }) | ||
|
|
||
| await connectDB(); | ||
| const deleted = await Announcement.findOneAndDelete({ | ||
| _id: id, | ||
| teacherId: userId, | ||
| }); | ||
|
|
||
| if (!deleted) { | ||
| return NextResponse.json({ error: 'Not found' }, { status: 404 }) | ||
| return NextResponse.json({ error: "Not found" }, { status: 404 }); | ||
| } | ||
| return NextResponse.json({ success: true }) | ||
|
|
||
| return NextResponse.json({ success: true }); | ||
| } catch (error) { | ||
| if (error instanceof Error) { | ||
| console.error('DELETE /api/announcements/[id] error:', error.message) | ||
| console.error("DELETE /api/announcements/[id] error:", error.message); | ||
| } | ||
| return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) | ||
| return NextResponse.json( | ||
| { error: "Internal server error" }, | ||
| { status: 500 }, | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,79 +1,109 @@ | ||
| import { auth } from '@clerk/nextjs/server' | ||
| import { NextRequest, NextResponse } from 'next/server' | ||
| import mongoose from 'mongoose' | ||
| import { connectDB } from '@/lib/mongodb' | ||
| import { Assignment } from '@/models/Assignment' | ||
| import { auth } from "@clerk/nextjs/server"; | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
| import mongoose from "mongoose"; | ||
| import { connectDB } from "@/lib/mongodb"; | ||
| import { Assignment } from "@/models/Assignment"; | ||
|
|
||
| const ALLOWED_UPDATE_FIELDS = ['title', 'description', 'dueDate', 'deadline', 'subject', 'class', 'status', 'kanbanStatus', 'maxMarks'] | ||
| const ALLOWED_UPDATE_FIELDS = [ | ||
| "title", | ||
| "description", | ||
| "deadline", | ||
| "subject", | ||
| "class", | ||
| "status", | ||
| "kanbanStatus", | ||
| "maxMarks", | ||
| ]; | ||
|
|
||
| export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { | ||
| const { userId } = await auth() | ||
| if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) | ||
| export async function PUT( | ||
| req: NextRequest, | ||
| ctx: { params: Promise<{ id: string }> }, | ||
| ) { | ||
| const { userId } = await auth(); | ||
| if (!userId) | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
|
|
||
| try { | ||
| const { id } = await ctx.params | ||
| const { id } = await ctx.params; | ||
|
|
||
| // Validate ObjectId | ||
| if (!mongoose.Types.ObjectId.isValid(id)) { | ||
| return NextResponse.json({ error: 'Invalid id' }, { status: 400 }) | ||
| return NextResponse.json({ error: "Invalid id" }, { status: 400 }); | ||
| } | ||
|
|
||
| await connectDB() | ||
| let body | ||
| await connectDB(); | ||
|
|
||
| let body; | ||
| try { | ||
| body = await req.json() | ||
| body = await req.json(); | ||
| } catch { | ||
| return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 }) | ||
| return NextResponse.json( | ||
| { error: "Invalid JSON in request body" }, | ||
| { status: 400 }, | ||
| ); | ||
| } | ||
|
|
||
| // Sanitize: only allow whitelisted fields | ||
| const sanitizedBody: Record<string, unknown> = {} | ||
| const sanitizedBody: Record<string, unknown> = {}; | ||
| for (const key of ALLOWED_UPDATE_FIELDS) { | ||
| if (key in body) { | ||
| sanitizedBody[key] = body[key] | ||
| sanitizedBody[key] = body[key]; | ||
| } | ||
| } | ||
|
|
||
| const assignment = await Assignment.findOneAndUpdate( | ||
| { _id: id }, | ||
| { _id: id, teacherId: userId }, | ||
| sanitizedBody, | ||
| { new: true } | ||
| ) | ||
| if (!assignment) return NextResponse.json({ error: 'Not found' }, { status: 404 }) | ||
| return NextResponse.json(assignment) | ||
| { new: true }, | ||
| ); | ||
|
Comment on lines
+36
to
+58
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Mongoose findOneAndUpdate does not run schema validators by default. You must pass { runValidators: true } in the options to enable update validators. Citations:
🏁 Script executed: #!/bin/bash
node <<'EOF'
// Test 1: Does 'in' operator throw with null?
console.log("Test 1: 'in' operator with null");
try {
const body = null;
const key = "test";
const result = key in body;
console.log(" Result:", result);
} catch (e) {
console.log(" ✓ THROWS:", e.message);
}
// Test 2: Does 'in' operator work with object?
console.log("\nTest 2: 'in' operator with object");
try {
const body = { test: 1 };
const key = "test";
const result = key in body;
console.log(" Result:", result);
} catch (e) {
console.log(" Error:", e.message);
}
// Test 3: Does 'in' operator throw with array?
console.log("\nTest 3: 'in' operator with array");
try {
const body = [1, 2, 3];
const key = "0";
const result = key in body;
console.log(" Result:", result);
} catch (e) {
console.log(" Error:", e.message);
}
EOFRepository: JavaScript-Mastery-Pro/discord-challenge Length of output: 280 🌐 Web query:
💡 Result: In Mongoose, when using update validators with the runValidators: true option in operations like updateOne, findOneAndUpdate, etc., the context: "query" option controls the value of this within custom validator functions. By default, during update validation (without context: "query"), this in validators refers to an undefined or null value (or the global object in non-strict mode), because the full document may not be loaded in memory. Setting context: "query" changes this to the Query object. This allows validators to access other parts of the update using query methods like this.get('name') or this.getUpdate.$set.name, enabling cross-field validation based on the update data. Example from older docs (v5.x) and community usage: toySchema.path('color').validate(function(value) { if (this.getUpdate.$set.name.toLowerCase.indexOf('red') !== -1) { return value === 'red'; } return true; }, 'Must be red if name contains "red"'); Toy.updateOne({}, { color: 'blue', name: 'Red Power Ranger' }, { runValidators: true, context: 'query' }); This fails validation because name contains 'red' but color is not 'red'. In recent Mongoose versions (v6+ to v9+ as of 2026), the official docs state that this refers to the Query by default during update validation: "When running with update validators, this is the Query". The explicit context: "query" option appears deprecated or unnecessary, as the behavior is now default. However, Stack Overflow answers and some plugins still recommend it for compatibility. Usage example: Model.findOneAndUpdate( { _id: id }, { $set: { field: value } }, { runValidators: true, context: 'query' } // For cross-field validation access ); Note caveats: Update validators only run on specific operators ($set, $unset, etc.) and only on updated paths. Sources confirm this behavior across versions 5-9. Citations:
Add request body validation and enable Mongoose update validators. Sending JSON 🛡️ Proposed fix try {
body = await req.json();
} catch {
return NextResponse.json(
{ error: "Invalid JSON in request body" },
{ status: 400 },
);
}
+
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
+ return NextResponse.json(
+ { error: "Request body must be an object" },
+ { status: 400 },
+ );
+ }
// Sanitize: only allow whitelisted fields
const sanitizedBody: Record<string, unknown> = {};
for (const key of ALLOWED_UPDATE_FIELDS) {
if (key in body) {
sanitizedBody[key] = body[key];
}
}
const assignment = await Assignment.findOneAndUpdate(
{ _id: id, teacherId: userId },
sanitizedBody,
- { new: true },
+ { new: true, runValidators: true },
);🤖 Prompt for AI Agents |
||
| if (!assignment) | ||
| return NextResponse.json({ error: "Not found" }, { status: 404 }); | ||
| return NextResponse.json(assignment); | ||
| } catch (error) { | ||
| if (error instanceof Error) { | ||
| console.error('PUT /api/assignments/[id] error:', error.message) | ||
| console.error("PUT /api/assignments/[id] error:", error.message); | ||
| } | ||
| return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) | ||
| return NextResponse.json( | ||
| { error: "Internal server error" }, | ||
| { status: 500 }, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: string }> }) { | ||
| const { userId } = await auth() | ||
| if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) | ||
| export async function DELETE( | ||
| _req: NextRequest, | ||
| ctx: { params: Promise<{ id: string }> }, | ||
| ) { | ||
| const { userId } = await auth(); | ||
| if (!userId) | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
|
|
||
| try { | ||
| const { id } = await ctx.params | ||
| const { id } = await ctx.params; | ||
|
|
||
| // Validate ObjectId | ||
| if (!mongoose.Types.ObjectId.isValid(id)) { | ||
| return NextResponse.json({ error: 'Invalid id' }, { status: 400 }) | ||
| return NextResponse.json({ error: "Invalid id" }, { status: 400 }); | ||
| } | ||
|
|
||
| await connectDB() | ||
| const deleted = await Assignment.findOneAndDelete({ _id: id }) | ||
|
|
||
| await connectDB(); | ||
| const deleted = await Assignment.findOneAndDelete({ | ||
| _id: id, | ||
| teacherId: userId, | ||
| }); | ||
|
|
||
| if (!deleted) { | ||
| return NextResponse.json({ error: 'Not found' }, { status: 404 }) | ||
| return NextResponse.json({ error: "Not found" }, { status: 404 }); | ||
| } | ||
| return NextResponse.json({ success: true }) | ||
|
|
||
| return NextResponse.json({ success: true }); | ||
| } catch (error) { | ||
| if (error instanceof Error) { | ||
| console.error('DELETE /api/assignments/[id] error:', error.message) | ||
| console.error("DELETE /api/assignments/[id] error:", error.message); | ||
| } | ||
| return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) | ||
| return NextResponse.json( | ||
| { error: "Internal server error" }, | ||
| { status: 500 }, | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reject non-object JSON before sanitizing.
req.json()can parsenull, andkey in bodywill then throw, producing a 500 for an invalid request body.🛡️ Proposed fix
try { body = await req.json(); } catch { return NextResponse.json( { error: "Invalid JSON request body" }, { status: 400 }, ); } + + if (!body || typeof body !== "object" || Array.isArray(body)) { + return NextResponse.json( + { error: "Request body must be an object" }, + { status: 400 }, + ); + } // Sanitize: only allow whitelisted fields📝 Committable suggestion
🤖 Prompt for AI Agents