const request = require('supertest'); const app = require('../../src/app'); const User = require('../../src/models/User'); const Bookmark = require('../../src/models/Bookmark'); const dbConnection = require('../../src/database/connection'); describe('Security Tests', () => { let testUser; let authToken; beforeAll(async () => { // Clean up any existing test data await dbConnection.query('DELETE FROM users WHERE email LIKE $1', ['%security-test%']); // Create test user testUser = await User.create({ email: 'security-test@example.com', password: 'TestPassword123!' }); await testUser.verifyEmail(); // Login to get auth token const loginResponse = await request(app) .post('/api/auth/login') .send({ email: 'security-test@example.com', password: 'TestPassword123!' }); const cookies = loginResponse.headers['set-cookie']; authToken = cookies.find(cookie => cookie.includes('authToken')).split('authToken=')[1].split(';')[0]; }); afterAll(async () => { // Clean up test data await dbConnection.query('DELETE FROM bookmarks WHERE user_id = $1', [testUser.id]); await testUser.delete(); await dbConnection.end(); }); describe('SQL Injection Prevention', () => { describe('Authentication Endpoints', () => { it('should prevent SQL injection in login email field', async () => { const maliciousEmail = "admin@example.com'; DROP TABLE users; --"; const response = await request(app) .post('/api/auth/login') .send({ email: maliciousEmail, password: 'password123' }) .expect(401); expect(response.body.code).toBe('INVALID_CREDENTIALS'); // Verify users table still exists by checking our test user const user = await User.findById(testUser.id); expect(user).toBeTruthy(); }); it('should prevent SQL injection in registration email field', async () => { const maliciousEmail = "test@example.com'; INSERT INTO users (email, password_hash) VALUES ('hacker@evil.com', 'hash'); --"; const response = await request(app) .post('/api/auth/register') .send({ email: maliciousEmail, password: 'TestPassword123!' }) .expect(400); expect(response.body.code).toBe('REGISTRATION_FAILED'); // Verify no unauthorized user was created const hackerUser = await User.findByEmail('hacker@evil.com'); expect(hackerUser).toBeNull(); }); it('should prevent SQL injection in password reset email field', async () => { const maliciousEmail = "test@example.com'; UPDATE users SET is_verified = true WHERE email = 'unverified@example.com'; --"; const response = await request(app) .post('/api/auth/forgot-password') .send({ email: maliciousEmail }) .expect(200); // Should return success message regardless (security feature) expect(response.body.message).toContain('password reset link has been sent'); }); }); describe('Bookmark Endpoints', () => { it('should prevent SQL injection in bookmark search', async () => { // Create a test bookmark first await Bookmark.create(testUser.id, { title: 'Secret Bookmark', url: 'https://secret.com', folder: 'Private' }); const maliciousSearch = "test'; DROP TABLE bookmarks; --"; const response = await request(app) .get(`/api/bookmarks?search=${encodeURIComponent(maliciousSearch)}`) .set('Cookie', `authToken=${authToken}`) .expect(200); expect(response.body).toHaveProperty('bookmarks'); expect(response.body).toHaveProperty('pagination'); // Verify bookmarks table still exists const bookmarks = await Bookmark.findByUserId(testUser.id); expect(bookmarks.bookmarks).toHaveLength(1); }); it('should prevent SQL injection in folder filter', async () => { const maliciousFolder = "Work'; DELETE FROM bookmarks WHERE user_id = '" + testUser.id + "'; --"; const response = await request(app) .get(`/api/bookmarks?folder=${encodeURIComponent(maliciousFolder)}`) .set('Cookie', `authToken=${authToken}`) .expect(200); expect(response.body).toHaveProperty('bookmarks'); // Verify bookmarks weren't deleted const bookmarks = await Bookmark.findByUserId(testUser.id); expect(bookmarks.bookmarks.length).toBeGreaterThan(0); }); it('should prevent SQL injection in bookmark creation', async () => { const maliciousBookmark = { title: "Test'; DROP TABLE bookmarks; --", url: 'https://malicious.com', folder: "Folder'; UPDATE users SET email = 'hacked@evil.com' WHERE id = '" + testUser.id + "'; --" }; const response = await request(app) .post('/api/bookmarks') .set('Cookie', `authToken=${authToken}`) .send(maliciousBookmark) .expect(201); expect(response.body.bookmark.title).toBe("Test'; DROP TABLE bookmarks; --"); // Verify user email wasn't changed const user = await User.findById(testUser.id); expect(user.email).toBe('security-test@example.com'); // Verify bookmarks table still exists const bookmarks = await Bookmark.findByUserId(testUser.id); expect(bookmarks.bookmarks.length).toBeGreaterThan(0); }); }); describe('User Profile Endpoints', () => { it('should prevent SQL injection in profile update', async () => { const maliciousEmail = "hacker@evil.com'; UPDATE users SET password_hash = 'hacked' WHERE email = 'security-test@example.com'; --"; const response = await request(app) .put('/api/user/profile') .set('Cookie', `authToken=${authToken}`) .send({ email: maliciousEmail }) .expect(400); expect(response.body.code).toBe('INVALID_EMAIL'); // Verify user data wasn't compromised const user = await User.findById(testUser.id); expect(user.email).toBe('security-test@example.com'); expect(user.password_hash).not.toBe('hacked'); }); }); }); describe('XSS Prevention', () => { describe('Input Sanitization', () => { it('should handle XSS attempts in bookmark title', async () => { const xssPayload = ''; const bookmarkData = { title: xssPayload, url: 'https://xss-test.com', folder: 'XSS Test' }; const response = await request(app) .post('/api/bookmarks') .set('Cookie', `authToken=${authToken}`) .send(bookmarkData) .expect(201); // The title should be stored as-is (backend doesn't sanitize HTML) // Frontend should handle XSS prevention during rendering expect(response.body.bookmark.title).toBe(xssPayload); // Verify it's stored correctly in database const bookmark = await Bookmark.findByIdAndUserId(response.body.bookmark.id, testUser.id); expect(bookmark.title).toBe(xssPayload); }); it('should handle XSS attempts in bookmark URL', async () => { const xssUrl = 'javascript:alert("XSS")'; const bookmarkData = { title: 'XSS URL Test', url: xssUrl, folder: 'XSS Test' }; const response = await request(app) .post('/api/bookmarks') .set('Cookie', `authToken=${authToken}`) .send(bookmarkData) .expect(400); expect(response.body.code).toBe('VALIDATION_ERROR'); expect(response.body.error).toContain('Invalid URL format'); }); it('should handle XSS attempts in search parameters', async () => { const xssSearch = ''; const response = await request(app) .get(`/api/bookmarks?search=${encodeURIComponent(xssSearch)}`) .set('Cookie', `authToken=${authToken}`) .expect(200); expect(response.body).toHaveProperty('bookmarks'); expect(response.body).toHaveProperty('pagination'); // Search should work normally, returning empty results expect(response.body.bookmarks).toHaveLength(0); }); }); describe('Response Headers', () => { it('should include security headers in responses', async () => { const response = await request(app) .get('/api/bookmarks') .set('Cookie', `authToken=${authToken}`) .expect(200); // Check for security headers (set by helmet middleware) expect(response.headers).toHaveProperty('x-content-type-options'); expect(response.headers).toHaveProperty('x-frame-options'); expect(response.headers).toHaveProperty('x-xss-protection'); expect(response.headers['x-content-type-options']).toBe('nosniff'); }); }); }); describe('Authentication Security', () => { describe('Token Security', () => { it('should reject requests with invalid JWT tokens', async () => { const response = await request(app) .get('/api/bookmarks') .set('Cookie', 'authToken=invalid.jwt.token') .expect(401); expect(response.body).toHaveProperty('error'); }); it('should reject requests with expired tokens', async () => { // Create a token with very short expiration const jwt = require('jsonwebtoken'); const expiredToken = jwt.sign( { userId: testUser.id, email: testUser.email }, process.env.JWT_SECRET, { expiresIn: '1ms' } // Expires immediately ); // Wait a moment to ensure token is expired await new Promise(resolve => setTimeout(resolve, 10)); const response = await request(app) .get('/api/bookmarks') .set('Cookie', `authToken=${expiredToken}`) .expect(401); expect(response.body).toHaveProperty('error'); }); it('should reject requests with malformed tokens', async () => { const malformedTokens = [ 'not.a.jwt', 'header.payload', // Missing signature 'header.payload.signature.extra', // Too many parts '', // Empty token 'Bearer token-without-bearer-prefix' ]; for (const token of malformedTokens) { const response = await request(app) .get('/api/bookmarks') .set('Cookie', `authToken=${token}`) .expect(401); expect(response.body).toHaveProperty('error'); } }); }); describe('Session Security', () => { it('should set secure cookie attributes in production', async () => { // Temporarily set NODE_ENV to production const originalEnv = process.env.NODE_ENV; process.env.NODE_ENV = 'production'; const response = await request(app) .post('/api/auth/login') .send({ email: 'security-test@example.com', password: 'TestPassword123!' }) .expect(200); const cookies = response.headers['set-cookie']; const authCookie = cookies.find(cookie => cookie.includes('authToken')); expect(authCookie).toContain('HttpOnly'); expect(authCookie).toContain('SameSite=Strict'); expect(authCookie).toContain('Secure'); // Should be secure in production // Restore original environment process.env.NODE_ENV = originalEnv; }); it('should clear cookies on logout', async () => { const response = await request(app) .post('/api/auth/logout') .set('Cookie', `authToken=${authToken}`) .expect(200); const cookies = response.headers['set-cookie']; const clearedCookie = cookies.find(cookie => cookie.includes('authToken')); expect(clearedCookie).toContain('authToken=;'); expect(clearedCookie).toContain('HttpOnly'); }); }); describe('Password Security', () => { it('should not expose password hashes in API responses', async () => { const response = await request(app) .get('/api/user/profile') .set('Cookie', `authToken=${authToken}`) .expect(200); expect(response.body.user).not.toHaveProperty('password_hash'); expect(response.body.user).not.toHaveProperty('verification_token'); expect(response.body.user).not.toHaveProperty('reset_token'); }); it('should enforce password strength requirements', async () => { const weakPasswords = [ 'weak', '12345678', 'password', 'Password', 'Password123', 'Password!' ]; for (const password of weakPasswords) { const response = await request(app) .post('/api/auth/register') .send({ email: `weak-${Date.now()}@example.com`, password: password }) .expect(400); expect(response.body.code).toBe('REGISTRATION_FAILED'); } }); it('should hash passwords before storage', async () => { const testEmail = `hash-test-${Date.now()}@example.com`; const testPassword = 'TestPassword123!'; await request(app) .post('/api/auth/register') .send({ email: testEmail, password: testPassword }) .expect(201); const user = await User.findByEmail(testEmail); expect(user.password_hash).toBeDefined(); expect(user.password_hash).not.toBe(testPassword); expect(user.password_hash.length).toBeGreaterThan(50); // bcrypt hashes are long // Cleanup await user.delete(); }); }); }); describe('Rate Limiting Security', () => { it('should enforce rate limits on sensitive endpoints', async () => { const requests = []; // Make multiple rapid requests to trigger rate limiting for (let i = 0; i < 6; i++) { requests.push( request(app) .post('/api/auth/login') .send({ email: 'nonexistent@example.com', password: 'wrongpassword' }) ); } const responses = await Promise.all(requests); // Should have at least one rate-limited response const rateLimitedResponse = responses.find(res => res.status === 429); expect(rateLimitedResponse).toBeDefined(); expect(rateLimitedResponse.body.code).toBe('RATE_LIMIT_EXCEEDED'); }, 10000); it('should include rate limit headers', async () => { const response = await request(app) .post('/api/auth/login') .send({ email: 'test@example.com', password: 'wrongpassword' }) .expect(401); // Rate limiting middleware should add these headers expect(response.headers).toHaveProperty('x-ratelimit-limit'); expect(response.headers).toHaveProperty('x-ratelimit-remaining'); }); }); describe('Data Validation Security', () => { it('should validate and sanitize input lengths', async () => { const longString = 'a'.repeat(10000); const response = await request(app) .post('/api/bookmarks') .set('Cookie', `authToken=${authToken}`) .send({ title: longString, url: 'https://example.com', folder: longString }) .expect(400); expect(response.body.code).toBe('VALIDATION_ERROR'); }); it('should validate email formats strictly', async () => { const invalidEmails = [ 'not-an-email', '@example.com', 'user@', 'user..name@example.com', 'user name@example.com', 'user@example', 'user@.example.com' ]; for (const email of invalidEmails) { const response = await request(app) .post('/api/auth/register') .send({ email: email, password: 'TestPassword123!' }) .expect(400); expect(response.body.code).toBe('REGISTRATION_FAILED'); } }); it('should validate URL formats in bookmarks', async () => { const invalidUrls = [ 'not-a-url', 'ftp://example.com', // Only HTTP/HTTPS should be allowed 'javascript:alert("xss")', 'data:text/html,', 'file:///etc/passwd' ]; for (const url of invalidUrls) { const response = await request(app) .post('/api/bookmarks') .set('Cookie', `authToken=${authToken}`) .send({ title: 'Test Bookmark', url: url }) .expect(400); expect(response.body.code).toBe('VALIDATION_ERROR'); } }); }); describe('Error Information Disclosure', () => { it('should not expose sensitive information in error messages', async () => { const response = await request(app) .post('/api/auth/login') .send({ email: 'security-test@example.com', password: 'wrongpassword' }) .expect(401); // Should not reveal whether email exists or password is wrong expect(response.body.error).toBe('Invalid email or password'); expect(response.body.error).not.toContain('password'); expect(response.body.error).not.toContain('email'); expect(response.body.error).not.toContain('user'); }); it('should not expose database errors to clients', async () => { // This test would require mocking database to throw an error // For now, we'll test that 500 errors don't expose internal details const response = await request(app) .get('/api/bookmarks/invalid-uuid-format') .set('Cookie', `authToken=${authToken}`) .expect(404); expect(response.body).toHaveProperty('error'); expect(response.body.error).not.toContain('database'); expect(response.body.error).not.toContain('query'); expect(response.body.error).not.toContain('SQL'); }); }); });