Skip to content
Merged
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@ next-env.d.ts
!README.md
!readme.md
*.sql
.env.local
320 changes: 320 additions & 0 deletions __tests__/api-security.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
/**
* API Endpoint Security Tests
* Tests for authentication, authorization, and input validation in API routes
*/

import { describe, test, expect, jest } from '@jest/globals';

describe('API Security Tests', () => {

describe('Authentication Endpoints', () => {
test('should require valid authentication for protected endpoints', async () => {
const protectedEndpoints = [
'/api/admin/users',
'/api/admin/hackathons',
'/api/admin/internships',
'/api/premium/verify-payment',
'/api/ai'
];

// Mock unauthenticated request
const mockRequestUrl = 'http://localhost:3000/api/admin/users';

protectedEndpoints.forEach(endpoint => {
// These endpoints should require authentication
expect(endpoint.includes('/api/admin/') || endpoint.includes('/api/premium/') || endpoint === '/api/ai').toBe(true);
});
});

test('should validate admin access for admin endpoints', () => {
const adminEndpoints = [
'/api/admin/users',
'/api/admin/hackathons',
'/api/admin/internships',
'/api/admin/tests'
];

adminEndpoints.forEach(endpoint => {
expect(endpoint.startsWith('/api/admin/')).toBe(true);
});
});
});

describe('Rate Limiting', () => {
test('should implement rate limiting for sensitive endpoints', () => {
const rateLimitedEndpoints = [
'/api/ai',
'/api/auth/signin',
'/api/auth/signup',
'/api/premium/verify-payment'
];

// Mock rate limit implementation
const rateLimit = new Map();
const RATE_LIMIT = 30; // requests per minute
const WINDOW = 60 * 1000; // 1 minute

function checkRateLimit(ip: string): boolean {
const now = Date.now();
const userRequests = rateLimit.get(ip) || [];
const recentRequests = userRequests.filter((timestamp: number) => now - timestamp < WINDOW);

if (recentRequests.length >= RATE_LIMIT) {
return false;
}

recentRequests.push(now);
rateLimit.set(ip, recentRequests);
return true;
}

// Test rate limiting logic
for (let i = 0; i < RATE_LIMIT; i++) {
expect(checkRateLimit('test-ip')).toBe(true);
}

// Should be blocked after limit
expect(checkRateLimit('test-ip')).toBe(false);
});
});

describe('Input Validation', () => {
test('should validate email format in API requests', () => {
const emails = [
{ email: 'valid@example.com', valid: true },
{ email: 'invalid-email', valid: false },
{ email: 'test@', valid: false },
{ email: '@example.com', valid: false },
{ email: 'testscript@example.com', valid: true } // This is actually a valid email format
];

const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

emails.forEach(({ email, valid }) => {
expect(emailRegex.test(email)).toBe(valid);
});
});

test('should validate payment data structure', () => {
const validPayment = {
orderId: 'order_123',
paymentId: 'pay_123',
signature: 'signature_123',
planId: 'premium',
userId: 'user_123'
};

const invalidPayments = [
{ orderId: '', paymentId: 'pay_123', signature: 'sig', planId: 'premium', userId: 'user' },
{ orderId: 'order', paymentId: '', signature: 'sig', planId: 'premium', userId: 'user' },
{ orderId: 'order', paymentId: 'pay', signature: '', planId: 'premium', userId: 'user' },
{ orderId: 'order', paymentId: 'pay', signature: 'sig', planId: 'free', userId: 'user' }, // Free plan should be rejected
{ orderId: 'order', paymentId: 'pay', signature: 'sig', planId: 'premium', userId: '' }
];

function validatePaymentData(data: any): boolean {
return !!(data.orderId && data.paymentId && data.signature && data.planId && data.userId && data.planId !== 'free');
}

expect(validatePaymentData(validPayment)).toBe(true);

invalidPayments.forEach(payment => {
expect(validatePaymentData(payment)).toBe(false);
});
});

test('should validate hackathon data structure', () => {
const validHackathon = {
title: 'Valid Hackathon',
slug: 'valid-hackathon',
description: 'A valid description',
start_date: '2025-01-01T00:00:00Z',
end_date: '2025-01-02T00:00:00Z',
organizer: 'Valid Organizer',
status: 'published'
};

const invalidHackathons = [
{ ...validHackathon, title: '' }, // Empty title
{ ...validHackathon, slug: '' }, // Empty slug
{ ...validHackathon, title: '<script>alert("xss")</script>' }, // XSS attempt
{ ...validHackathon, start_date: 'invalid-date' }, // Invalid date
{ ...validHackathon, organizer: '' } // Empty organizer
];

function validateHackathonData(data: any): boolean {
if (!data.title || !data.slug || !data.organizer) return false;
if (data.title.includes('<script>') || data.title.includes('</script>')) return false;
if (data.start_date && !Date.parse(data.start_date)) return false;
return true;
}

expect(validateHackathonData(validHackathon)).toBe(true);

invalidHackathons.forEach(hackathon => {
expect(validateHackathonData(hackathon)).toBe(false);
});
});
});

describe('Authorization Checks', () => {
test('should verify user ownership for user-specific operations', () => {
const mockUser = { id: 'user123', email: 'test@example.com' };
const userResource = { user_id: 'user123', data: 'some data' };
const otherUserResource = { user_id: 'user456', data: 'other data' };

function checkOwnership(user: any, resource: any): boolean {
return user.id === resource.user_id;
}

expect(checkOwnership(mockUser, userResource)).toBe(true);
expect(checkOwnership(mockUser, otherUserResource)).toBe(false);
});

test('should validate admin permissions for admin operations', () => {
const adminUser = { id: 'admin1', email: 'admin@example.com', is_admin: true };
const regularUser = { id: 'user1', email: 'user@example.com', is_admin: false };

function isAdmin(user: any): boolean {
return user.is_admin === true;
}

expect(isAdmin(adminUser)).toBe(true);
expect(isAdmin(regularUser)).toBe(false);
});
});

describe('Data Sanitization', () => {
test('should sanitize user input to prevent XSS', () => {
const maliciousInputs = [
'<script>alert("xss")</script>',
'javascript:alert("xss")',
'<img src="x" onerror="alert(1)">',
'<iframe src="javascript:alert(1)"></iframe>',
'"><script>alert("xss")</script>'
];

function sanitizeInput(input: string): string {
return input
.replace(/[<>]/g, '')
.replace(/javascript:/gi, '')
.replace(/data:/gi, '')
.trim()
.substring(0, 1000);
}

maliciousInputs.forEach(input => {
const sanitized = sanitizeInput(input);
expect(sanitized).not.toContain('<script>');
expect(sanitized).not.toContain('</script>');
expect(sanitized).not.toContain('javascript:');
expect(sanitized).not.toContain('<iframe>');
});
});

test('should prevent SQL injection in search queries', () => {
const maliciousQueries = [
"'; DROP TABLE users; --",
"1 OR 1=1",
"'; DELETE FROM profiles; --",
"admin'--",
"1' UNION SELECT password FROM users--"
];

function isSafeQuery(query: string): boolean {
const sqlPatterns = [
/(\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|UNION|SCRIPT)\b)/i,
/(\b(OR|AND)\s+\d+\s*=\s*\d+)/i,
/(--|\/\*|\*\/|;)/,
/(\b(WAITFOR|DELAY|SLEEP)\b)/i,
];

return !sqlPatterns.some(pattern => pattern.test(query));
}

maliciousQueries.forEach(query => {
expect(isSafeQuery(query)).toBe(false);
});

// Safe queries should pass
const safeQueries = ['user search', 'hackathon name', 'normal text'];
safeQueries.forEach(query => {
expect(isSafeQuery(query)).toBe(true);
});
});
});

describe('File Upload Security', () => {
test('should validate file types and sizes', () => {
const validFiles = [
{ type: 'image/jpeg', size: 1024 * 1024 }, // 1MB
{ type: 'image/png', size: 500 * 1024 }, // 500KB
{ type: 'application/pdf', size: 2 * 1024 * 1024 } // 2MB
];

const invalidFiles = [
{ type: 'application/javascript', size: 1024 }, // Dangerous type
{ type: 'text/html', size: 1024 }, // Dangerous type
{ type: 'image/jpeg', size: 10 * 1024 * 1024 }, // Too large (10MB)
{ type: 'application/exe', size: 1024 } // Executable
];

const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
const maxSize = 5 * 1024 * 1024; // 5MB

function validateFile(file: any): boolean {
return allowedTypes.includes(file.type) && file.size <= maxSize;
}

validFiles.forEach(file => {
expect(validateFile(file)).toBe(true);
});

invalidFiles.forEach(file => {
expect(validateFile(file)).toBe(false);
});
});
});

describe('Session Security', () => {
test('should validate session tokens', () => {
const validToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
const invalidTokens = [
'',
'invalid-token',
'Bearer',
'expired-token-format'
];

function isValidTokenFormat(token: string): boolean {
// Basic JWT format check (3 parts separated by dots)
const parts = token.split('.');
return parts.length === 3 && parts.every(part => part.length > 0);
}

expect(isValidTokenFormat(validToken)).toBe(true);

invalidTokens.forEach(token => {
expect(isValidTokenFormat(token)).toBe(false);
});
});

test('should handle session expiration', () => {
const currentTime = Date.now();
const sessions = [
{ expiresAt: currentTime + 3600000, valid: true }, // Expires in 1 hour
{ expiresAt: currentTime - 3600000, valid: false }, // Expired 1 hour ago
{ expiresAt: currentTime + 86400000, valid: true }, // Expires in 1 day
{ expiresAt: currentTime - 1, valid: false } // Expired 1ms ago
];

function isSessionValid(session: any): boolean {
return session.expiresAt > Date.now();
}

sessions.forEach(session => {
expect(isSessionValid(session)).toBe(session.valid);
});
});
});
});
Loading