Skip to content

fixed the middleware bypass risk #1834

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"autoprefixer": "^10.4.20",
"axios": "^1.6.2",
"bcrypt": "^5.1.1",
"bcryptjs": "^3.0.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"cmdk": "1.0.0",
Expand Down
17 changes: 13 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions src/app/api/mobile/search/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import db from '@/db';
import { CourseContent } from '@prisma/client';
import Fuse from 'fuse.js';
import { NextRequest, NextResponse } from 'next/server';
import { validateAuthHeader } from '@/lib/validateAuthHeader';

export type TSearchedVideos = {
id: number;
Expand All @@ -25,10 +26,12 @@ export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const searchQuery = searchParams.get('q');
const user = JSON.parse(request.headers.get('g') || '');

// Use the secure validation helper instead of direct parsing
const user = validateAuthHeader(request);

if (!user) {
return NextResponse.json({ message: 'User Not Found' }, { status: 400 });
return NextResponse.json({ message: 'Unauthorized' }, { status: 403 });
}

if (searchQuery && searchQuery.length > 2) {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/auth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import db from '@/db';
import CredentialsProvider from 'next-auth/providers/credentials';
import { JWTPayload, SignJWT, importJWK } from 'jose';
import bcrypt from 'bcrypt';
import bcrypt from 'bcryptjs';
import prisma from '@/db';
import { NextAuthOptions } from 'next-auth';
import { Session } from 'next-auth';
Expand Down
52 changes: 52 additions & 0 deletions src/lib/validateAuthHeader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { NextRequest } from 'next/server';

interface AuthUser {
id: string;
email: string;
[key: string]: any;
}

/**
* Validates the 'g' header to prevent authentication bypass attacks
*
* @param req NextRequest object
* @returns Validated user object or null if invalid
*/
export function validateAuthHeader(req: NextRequest): AuthUser | null {
try {
// Check for middleware subrequest attempt - should be blocked in middleware but add defense in depth
if (req.headers.get('x-middleware-subrequest')) {
console.warn('Possible auth bypass attempt detected: x-middleware-subrequest header present');
return null;
}

// Get the g header
const gHeader = req.headers.get('g');
if (!gHeader) {
return null;
}

// Parse the g header
const userData = JSON.parse(gHeader);

// Validate required fields
if (!userData.id || !userData.email) {
return null;
}

// Validate timestamp to prevent replay attacks (optional, 5 minute window)
const timestamp = userData.timestamp || 0;
const now = Date.now();
const fiveMinutes = 5 * 60 * 1000;

if (now - timestamp > fiveMinutes) {
console.warn('Auth header timestamp expired');
return null;
}

return userData;
} catch (error) {
console.error('Error validating auth header:', error);
return null;
}
}
28 changes: 25 additions & 3 deletions src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,26 +29,48 @@ export const verifyJWT = async (token: string): Promise<JWTPayload | null> => {
};

export const withMobileAuth = async (req: RequestWithUser) => {
// Security: Remove existing g header to prevent spoofing
const newHeaders = new Headers(req.headers);
newHeaders.delete('g');

// Security: Check for and block x-middleware-subrequest header manipulation
if (req.headers.get('x-middleware-subrequest')) {
// If someone is trying to spoof middleware, reject the request
return NextResponse.json({ message: 'Unauthorized request' }, { status: 403 });
}

// Continue with normal authentication flow
if (req.headers.get('Auth-Key')) {
return NextResponse.next();
// Even with Auth-Key, ensure g header is clean
return NextResponse.next({
request: {
headers: newHeaders,
},
});
}

const token = req.headers.get('Authorization');

if (!token) {
return NextResponse.json({ message: 'Unauthorized' }, { status: 403 });
}

const payload = await verifyJWT(token);
if (!payload) {
return NextResponse.json({ message: 'Unauthorized' }, { status: 403 });
}
const newHeaders = new Headers(req.headers);

/**
* Add a global object 'g'
* it holds the request claims and other keys
* easily pass around this key as request context
*
* Security: Sign the payload to prevent tampering
*/
newHeaders.set('g', JSON.stringify(payload));
const timestamp = Date.now();
const dataToSign = { ...payload, timestamp };

newHeaders.set('g', JSON.stringify(dataToSign));
return NextResponse.next({
request: {
headers: newHeaders,
Expand Down