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');
});
});
});