Fix jwt-token

This commit is contained in:
2025-09-14 21:07:43 +02:00
parent d6293dd8ba
commit 019eb8c2b2
20 changed files with 7185 additions and 29 deletions

View File

@@ -0,0 +1,663 @@
const { describe, it, beforeEach, afterEach, before, after } = require('mocha');
const { expect } = require('chai');
const sinon = require('sinon');
const jwt = require('jsonwebtoken');
const { setupTestEnvironment, teardownTestEnvironment, cleanDatabase, createTestUser, createTestTenant, createTestDevice, generateTestToken } = require('../setup');
describe('Security Tests', () => {
let models, sequelize;
before(async () => {
({ models, sequelize } = await setupTestEnvironment());
});
after(async () => {
await teardownTestEnvironment();
});
beforeEach(async () => {
await cleanDatabase();
});
describe('Authentication Security', () => {
it('should prevent JWT token manipulation', async () => {
const tenant = await createTestTenant();
const user = await createTestUser({ tenant_id: tenant.id });
const validToken = generateTestToken(user, tenant);
const [header, payload, signature] = validToken.split('.');
// Test various token manipulation attempts
const manipulationTests = [
{
name: 'Modified payload',
token: header + '.' + Buffer.from(JSON.stringify({
...JSON.parse(Buffer.from(payload, 'base64').toString()),
role: 'admin' // Attempt privilege escalation
})).toString('base64') + '.' + signature
},
{
name: 'Modified signature',
token: header + '.' + payload + '.' + 'tampered_signature'
},
{
name: 'Wrong algorithm',
token: jwt.sign({
userId: user.id,
tenantId: tenant.id
}, 'secret', { algorithm: 'HS256' }) // Different algorithm
},
{
name: 'Expired token',
token: jwt.sign({
userId: user.id,
tenantId: tenant.id,
exp: Math.floor(Date.now() / 1000) - 3600 // Expired 1 hour ago
}, process.env.JWT_SECRET || 'test-secret')
}
];
for (const test of manipulationTests) {
try {
const decoded = jwt.verify(test.token, process.env.JWT_SECRET || 'test-secret');
// If we get here, the token was accepted when it shouldn't be
if (test.name === 'Wrong algorithm') {
// This might be valid depending on configuration
continue;
}
expect.fail(`Token manipulation test "${test.name}" should have failed`);
} catch (error) {
// Expected behavior - token should be rejected
expect(error.name).to.be.oneOf(['JsonWebTokenError', 'TokenExpiredError', 'NotBeforeError']);
}
}
});
it('should enforce tenant boundaries in JWT tokens', async () => {
const tenant1 = await createTestTenant({ slug: 'tenant1' });
const tenant2 = await createTestTenant({ slug: 'tenant2' });
const user1 = await createTestUser({ tenant_id: tenant1.id });
const user2 = await createTestUser({ tenant_id: tenant2.id });
// Create device for tenant1
const device1 = await createTestDevice({
id: 111,
tenant_id: tenant1.id,
is_approved: true
});
// User from tenant2 tries to access tenant1's device
const crossTenantToken = generateTestToken(user2, tenant2);
// Simulate middleware that would check tenant access
const checkTenantAccess = (token, targetTenantId) => {
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'test-secret');
return decoded.tenantId === targetTenantId;
};
const hasAccess = checkTenantAccess(crossTenantToken, tenant1.id);
expect(hasAccess).to.be.false;
// User from tenant1 should have access to their own tenant
const validToken = generateTestToken(user1, tenant1);
const hasValidAccess = checkTenantAccess(validToken, tenant1.id);
expect(hasValidAccess).to.be.true;
});
it('should handle brute force login attempts', async () => {
const tenant = await createTestTenant();
const user = await createTestUser({
tenant_id: tenant.id,
username: 'brutetest',
password: 'SecurePassword123!'
});
// Mock rate limiting storage
const rateLimitStore = new Map();
const checkRateLimit = (identifier, maxAttempts = 5, windowMs = 15 * 60 * 1000) => {
const now = Date.now();
const attempts = rateLimitStore.get(identifier) || { count: 0, resetTime: now + windowMs };
if (now > attempts.resetTime) {
// Reset window
attempts.count = 0;
attempts.resetTime = now + windowMs;
}
attempts.count++;
rateLimitStore.set(identifier, attempts);
return attempts.count <= maxAttempts;
};
// Simulate multiple failed login attempts
for (let i = 0; i < 10; i++) {
const allowed = checkRateLimit('brutetest');
if (i < 5) {
expect(allowed).to.be.true;
} else {
expect(allowed).to.be.false; // Should be blocked after 5 attempts
}
}
});
});
describe('Authorization Security', () => {
it('should prevent privilege escalation attempts', async () => {
const tenant = await createTestTenant();
const regularUser = await createTestUser({
tenant_id: tenant.id,
role: 'user'
});
const adminUser = await createTestUser({
tenant_id: tenant.id,
role: 'admin'
});
// Test role-based access control
const checkPermission = (user, action) => {
const permissions = {
'user': ['view_detections', 'view_devices'],
'admin': ['view_detections', 'view_devices', 'manage_devices', 'manage_users', 'view_system'],
'system_admin': ['*'] // All permissions
};
const userPermissions = permissions[user.role] || [];
return userPermissions.includes(action) || userPermissions.includes('*');
};
// Regular user should not have admin permissions
expect(checkPermission(regularUser, 'manage_devices')).to.be.false;
expect(checkPermission(regularUser, 'manage_users')).to.be.false;
expect(checkPermission(regularUser, 'view_system')).to.be.false;
// But should have basic permissions
expect(checkPermission(regularUser, 'view_detections')).to.be.true;
expect(checkPermission(regularUser, 'view_devices')).to.be.true;
// Admin should have admin permissions
expect(checkPermission(adminUser, 'manage_devices')).to.be.true;
expect(checkPermission(adminUser, 'manage_users')).to.be.true;
});
it('should enforce IP address restrictions', async () => {
const tenant = await createTestTenant({
ip_restrictions: '192.168.1.0/24,10.0.0.0/8'
});
const checkIPRestriction = (clientIP, allowedRanges) => {
if (!allowedRanges) return true;
const isIPInRange = (ip, range) => {
if (range.includes('/')) {
// CIDR notation
const [network, prefixLength] = range.split('/');
const prefix = parseInt(prefixLength);
// Simplified check for testing
if (prefix === 24) {
const networkPrefix = network.substring(0, network.lastIndexOf('.'));
const ipPrefix = ip.substring(0, ip.lastIndexOf('.'));
return networkPrefix === ipPrefix;
}
if (prefix === 8) {
const networkPrefix = network.split('.')[0];
const ipPrefix = ip.split('.')[0];
return networkPrefix === ipPrefix;
}
}
return ip === range;
};
const ranges = allowedRanges.split(',');
return ranges.some(range => isIPInRange(clientIP, range.trim()));
};
const allowedIPs = [
'192.168.1.100',
'192.168.1.50',
'10.0.0.15',
'10.5.3.100'
];
const blockedIPs = [
'203.0.113.1', // External IP
'172.16.0.1', // Different private range
'192.168.2.100' // Wrong subnet
];
allowedIPs.forEach(ip => {
expect(checkIPRestriction(ip, tenant.ip_restrictions)).to.be.true;
});
blockedIPs.forEach(ip => {
expect(checkIPRestriction(ip, tenant.ip_restrictions)).to.be.false;
});
});
it('should prevent unauthorized data modification', async () => {
const tenant1 = await createTestTenant();
const tenant2 = await createTestTenant();
const user1 = await createTestUser({ tenant_id: tenant1.id, role: 'admin' });
const user2 = await createTestUser({ tenant_id: tenant2.id, role: 'admin' });
const device1 = await createTestDevice({
id: 123,
tenant_id: tenant1.id
});
// User2 attempts to modify device belonging to tenant1
const unauthorizedUpdate = async () => {
return await models.Device.update(
{ name: 'Hacked Device' },
{
where: {
id: device1.id,
tenant_id: user2.tenant_id // Wrong tenant
}
}
);
};
const result = await unauthorizedUpdate();
expect(result[0]).to.equal(0); // No rows affected
// Verify device was not modified
const device = await models.Device.findByPk(device1.id);
expect(device.name).to.not.equal('Hacked Device');
});
});
describe('Input Validation Security', () => {
it('should prevent SQL injection attempts', async () => {
const tenant = await createTestTenant();
const device = await createTestDevice({ tenant_id: tenant.id });
// SQL injection payloads
const injectionPayloads = [
"'; DROP TABLE drone_detections; --",
"' OR '1'='1",
"1; DELETE FROM devices WHERE 1=1; --",
"' UNION SELECT * FROM users --"
];
for (const payload of injectionPayloads) {
try {
// Attempt to use payload in various contexts
await models.DroneDetection.findAll({
where: {
tenant_id: tenant.id,
// Using parameterized queries should prevent injection
drone_id: payload
}
});
// The query should execute safely without SQL injection
// (Sequelize uses parameterized queries by default)
} catch (error) {
// If there's an error, it should be a validation error, not a SQL error
expect(error.name).to.not.include('SQL');
}
}
});
it('should validate and sanitize detection data', async () => {
const tenant = await createTestTenant();
const device = await createTestDevice({
tenant_id: tenant.id,
is_approved: true
});
const maliciousInputs = [
{
name: 'XSS attempt in coordinates',
data: {
device_id: device.id,
geo_lat: '<script>alert("xss")</script>',
geo_lon: 18.0686,
device_timestamp: Date.now(),
drone_type: 2
}
},
{
name: 'Extremely large coordinates',
data: {
device_id: device.id,
geo_lat: 999999.999999,
geo_lon: -999999.999999,
device_timestamp: Date.now(),
drone_type: 2
}
},
{
name: 'Invalid data types',
data: {
device_id: device.id,
geo_lat: null,
geo_lon: undefined,
device_timestamp: 'invalid_timestamp',
drone_type: 'invalid_type'
}
},
{
name: 'Buffer overflow attempt',
data: {
device_id: device.id,
geo_lat: 59.3293,
geo_lon: 18.0686,
device_timestamp: Date.now(),
drone_type: 2,
additional_data: 'A'.repeat(10000) // Very long string
}
}
];
for (const test of maliciousInputs) {
try {
await models.DroneDetection.create({
...test.data,
tenant_id: tenant.id
});
// If creation succeeds, verify data was sanitized
const detection = await models.DroneDetection.findOne({
where: { device_id: device.id },
order: [['id', 'DESC']]
});
if (detection) {
// Coordinates should be valid numbers
if (detection.geo_lat !== null) {
expect(detection.geo_lat).to.be.a('number');
expect(detection.geo_lat).to.be.within(-90, 90);
}
if (detection.geo_lon !== null) {
expect(detection.geo_lon).to.be.a('number');
expect(detection.geo_lon).to.be.within(-180, 180);
}
}
} catch (error) {
// Expected for invalid data - should be validation error
expect(error.name).to.be.oneOf(['SequelizeValidationError', 'SequelizeDatabaseError']);
}
}
});
it('should prevent path traversal attacks', async () => {
// Test potential file path manipulation
const pathTraversalPayloads = [
'../../../etc/passwd',
'..\\..\\..\\windows\\system32\\config\\sam',
'/etc/shadow',
'C:\\Windows\\System32\\drivers\\etc\\hosts',
'%2e%2e%2f%2e%2e%2f%2e%2e%2fbootini', // URL encoded
'....//....//....//etc/passwd'
];
pathTraversalPayloads.forEach(payload => {
// Test file path validation function
const isValidPath = (path) => {
// Should reject paths with traversal attempts
return !path.includes('..') &&
!path.includes('%2e') &&
!path.startsWith('/') &&
!path.match(/^[a-zA-Z]:\\/);
};
expect(isValidPath(payload)).to.be.false;
});
// Valid paths should pass
const validPaths = [
'device_logs.txt',
'reports/detection_summary.pdf',
'data/export.csv'
];
validPaths.forEach(path => {
const isValidPath = (path) => {
return !path.includes('..') &&
!path.includes('%2e') &&
!path.startsWith('/') &&
!path.match(/^[a-zA-Z]:\\/);
};
expect(isValidPath(path)).to.be.true;
});
});
});
describe('Data Protection Security', () => {
it('should protect sensitive data in database', async () => {
const tenant = await createTestTenant();
const user = await createTestUser({
tenant_id: tenant.id,
username: 'testuser',
email: 'test@example.com',
password: 'SecurePassword123!'
});
// Verify password is hashed, not stored in plain text
expect(user.password_hash).to.exist;
expect(user.password_hash).to.not.equal('SecurePassword123!');
expect(user.password_hash.length).to.be.greaterThan(20); // Hashed passwords are longer
// Verify sensitive fields are not exposed in JSON
const userJSON = user.toJSON();
expect(userJSON.password_hash).to.be.undefined; // Should be hidden
expect(userJSON.username).to.exist; // Public fields should remain
});
it('should enforce data retention policies', async () => {
const tenant = await createTestTenant();
const device = await createTestDevice({ tenant_id: tenant.id });
// Create old detections (simulate 1 year old data)
const oldDetections = [];
const oneYearAgo = new Date();
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
for (let i = 0; i < 10; i++) {
oldDetections.push({
device_id: device.id,
tenant_id: tenant.id,
geo_lat: 59.3293,
geo_lon: 18.0686,
device_timestamp: oneYearAgo,
drone_type: 2,
rssi: -60,
freq: 2400,
drone_id: 1000 + i,
threat_level: 'low',
createdAt: oneYearAgo,
updatedAt: oneYearAgo
});
}
await models.DroneDetection.bulkCreate(oldDetections);
// Create recent detections
const recentDetections = [];
for (let i = 0; i < 5; i++) {
recentDetections.push({
device_id: device.id,
tenant_id: tenant.id,
geo_lat: 59.3293,
geo_lon: 18.0686,
device_timestamp: new Date(),
drone_type: 2,
rssi: -60,
freq: 2400,
drone_id: 2000 + i,
threat_level: 'medium',
createdAt: new Date(),
updatedAt: new Date()
});
}
await models.DroneDetection.bulkCreate(recentDetections);
// Simulate data retention cleanup (delete data older than 6 months)
const sixMonthsAgo = new Date();
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
const deleteResult = await models.DroneDetection.destroy({
where: {
tenant_id: tenant.id,
createdAt: {
[models.Sequelize.Op.lt]: sixMonthsAgo
}
}
});
expect(deleteResult).to.equal(10); // Should delete old records
// Verify recent data remains
const remainingDetections = await models.DroneDetection.findAll({
where: { tenant_id: tenant.id }
});
expect(remainingDetections).to.have.length(5);
});
it('should anonymize exported data', async () => {
const tenant = await createTestTenant();
const user = await createTestUser({ tenant_id: tenant.id });
const device = await createTestDevice({
tenant_id: tenant.id,
geo_lat: 59.3293,
geo_lon: 18.0686
});
const detection = await models.DroneDetection.create({
device_id: device.id,
tenant_id: tenant.id,
geo_lat: 59.3293,
geo_lon: 18.0686,
device_timestamp: new Date(),
drone_type: 2,
rssi: -60,
freq: 2400,
drone_id: 12345,
threat_level: 'medium'
});
// Function to anonymize data for export
const anonymizeForExport = (data) => {
return {
...data,
// Remove exact coordinates, use general area
geo_lat: Math.round(data.geo_lat * 100) / 100, // Reduce precision
geo_lon: Math.round(data.geo_lon * 100) / 100,
// Remove device-specific identifiers
device_id: null,
// Hash drone ID instead of exposing it
drone_id_hash: require('crypto').createHash('sha256').update(data.drone_id.toString()).digest('hex').substring(0, 8)
};
};
const anonymized = anonymizeForExport(detection.toJSON());
expect(anonymized.device_id).to.be.null;
expect(anonymized.drone_id_hash).to.exist;
expect(anonymized.drone_id_hash).to.not.equal(detection.drone_id);
expect(anonymized.geo_lat).to.be.lessThan(detection.geo_lat + 0.005); // Reduced precision
});
});
describe('API Security', () => {
it('should prevent API abuse and rate limiting bypass', async () => {
const device = await createTestDevice({ is_approved: true });
// Simulate rate limiting for detection endpoint
const requestCounts = new Map();
const checkRateLimit = (deviceId, maxPerMinute = 60) => {
const now = Date.now();
const windowStart = Math.floor(now / 60000) * 60000; // 1-minute windows
const key = `${deviceId}-${windowStart}`;
const count = requestCounts.get(key) || 0;
requestCounts.set(key, count + 1);
return count < maxPerMinute;
};
// Test normal usage - should be allowed
for (let i = 0; i < 50; i++) {
expect(checkRateLimit(device.id)).to.be.true;
}
// Test excessive usage - should be blocked
for (let i = 0; i < 20; i++) {
const allowed = checkRateLimit(device.id);
if (i < 10) {
expect(allowed).to.be.true;
} else {
expect(allowed).to.be.false;
}
}
});
it('should validate API request size limits', async () => {
const validateRequestSize = (data, maxSizeKB = 100) => {
const dataSize = JSON.stringify(data).length;
const maxSizeBytes = maxSizeKB * 1024;
return dataSize <= maxSizeBytes;
};
// Normal request should pass
const normalRequest = {
device_id: 123,
geo_lat: 59.3293,
geo_lon: 18.0686,
device_timestamp: Date.now(),
drone_type: 2
};
expect(validateRequestSize(normalRequest)).to.be.true;
// Oversized request should fail
const oversizedRequest = {
...normalRequest,
malicious_payload: 'A'.repeat(200 * 1024) // 200KB of data
};
expect(validateRequestSize(oversizedRequest)).to.be.false;
});
it('should prevent CSRF attacks', async () => {
const tenant = await createTestTenant();
const user = await createTestUser({ tenant_id: tenant.id });
// Generate CSRF token
const generateCSRFToken = (userId, secret = 'csrf-secret') => {
return require('crypto')
.createHmac('sha256', secret)
.update(`${userId}-${Date.now()}`)
.digest('hex');
};
// Validate CSRF token
const validateCSRFToken = (token, userId, secret = 'csrf-secret', maxAge = 3600000) => {
try {
// In a real implementation, you'd store token metadata
// This is a simplified validation
return token && token.length === 64; // Valid format
} catch (error) {
return false;
}
};
const validToken = generateCSRFToken(user.id);
expect(validateCSRFToken(validToken, user.id)).to.be.true;
// Invalid tokens should fail
expect(validateCSRFToken('invalid_token', user.id)).to.be.false;
expect(validateCSRFToken(null, user.id)).to.be.false;
});
});
});