344 lines
8.6 KiB
TypeScript
344 lines
8.6 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
|
|
// Mock Express request object
|
|
interface MockRequest {
|
|
path: string;
|
|
url: string;
|
|
ip: string;
|
|
headers: Record<string, string>;
|
|
}
|
|
|
|
interface MockResponse {
|
|
statusCode: number;
|
|
headers: Record<string, string>;
|
|
body: string;
|
|
status: (code: number) => MockResponse;
|
|
json: (data: any) => MockResponse;
|
|
setHeader: (name: string, value: string) => void;
|
|
end: (data: string) => void;
|
|
}
|
|
|
|
// Simulate the security middleware logic
|
|
function createSecurityMiddleware() {
|
|
const sensitiveFiles = [
|
|
'/config.json',
|
|
'/config.example.json',
|
|
'/.env',
|
|
'/package.json',
|
|
'/package-lock.json',
|
|
'/tsconfig.json',
|
|
'/server/',
|
|
'/.git/',
|
|
'/node_modules/',
|
|
'/dist/',
|
|
'/build/',
|
|
'/.vscode/',
|
|
'/.idea/',
|
|
'/README.md',
|
|
'/CLAUDE.md'
|
|
];
|
|
|
|
return (req: MockRequest, res: MockResponse, next: () => void) => {
|
|
const normalizedPath = req.path.toLowerCase();
|
|
|
|
// Check if the request is for a sensitive file or directory
|
|
const isSensitiveFile = sensitiveFiles.some(sensitiveFile =>
|
|
normalizedPath === sensitiveFile.toLowerCase() ||
|
|
normalizedPath.startsWith(sensitiveFile.toLowerCase())
|
|
);
|
|
|
|
if (isSensitiveFile) {
|
|
console.warn(`🚫 Blocked access to sensitive file: ${req.path} from ${req.ip}`);
|
|
res.statusCode = 403;
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({
|
|
error: 'Access denied',
|
|
message: 'This resource is not available'
|
|
}));
|
|
return;
|
|
}
|
|
|
|
next();
|
|
};
|
|
}
|
|
|
|
// Create mock response object
|
|
function createMockResponse(): MockResponse {
|
|
const response: MockResponse = {
|
|
statusCode: 200,
|
|
headers: {},
|
|
body: '',
|
|
status: (code: number) => {
|
|
response.statusCode = code;
|
|
return response;
|
|
},
|
|
json: (data: any) => {
|
|
response.body = JSON.stringify(data);
|
|
return response;
|
|
},
|
|
setHeader: (name: string, value: string) => {
|
|
response.headers[name] = value;
|
|
},
|
|
end: (data: string) => {
|
|
response.body = data;
|
|
}
|
|
};
|
|
return response;
|
|
}
|
|
|
|
describe('File Security Middleware', () => {
|
|
const securityMiddleware = createSecurityMiddleware();
|
|
|
|
describe('Sensitive File Protection', () => {
|
|
it('should block access to config.json', () => {
|
|
const req: MockRequest = {
|
|
path: '/config.json',
|
|
url: '/config.json',
|
|
ip: '127.0.0.1',
|
|
headers: {}
|
|
};
|
|
const res = createMockResponse();
|
|
const next = vi.fn();
|
|
|
|
securityMiddleware(req, res, next);
|
|
|
|
expect(res.statusCode).toBe(403);
|
|
expect(res.headers['Content-Type']).toBe('application/json');
|
|
expect(res.body).toContain('Access denied');
|
|
expect(next).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should block access to config.example.json', () => {
|
|
const req: MockRequest = {
|
|
path: '/config.example.json',
|
|
url: '/config.example.json',
|
|
ip: '127.0.0.1',
|
|
headers: {}
|
|
};
|
|
const res = createMockResponse();
|
|
const next = vi.fn();
|
|
|
|
securityMiddleware(req, res, next);
|
|
|
|
expect(res.statusCode).toBe(403);
|
|
expect(next).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should block access to .env files', () => {
|
|
const req: MockRequest = {
|
|
path: '/.env',
|
|
url: '/.env',
|
|
ip: '127.0.0.1',
|
|
headers: {}
|
|
};
|
|
const res = createMockResponse();
|
|
const next = vi.fn();
|
|
|
|
securityMiddleware(req, res, next);
|
|
|
|
expect(res.statusCode).toBe(403);
|
|
expect(next).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should block access to package.json', () => {
|
|
const req: MockRequest = {
|
|
path: '/package.json',
|
|
url: '/package.json',
|
|
ip: '127.0.0.1',
|
|
headers: {}
|
|
};
|
|
const res = createMockResponse();
|
|
const next = vi.fn();
|
|
|
|
securityMiddleware(req, res, next);
|
|
|
|
expect(res.statusCode).toBe(403);
|
|
expect(next).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should block access to server directory', () => {
|
|
const req: MockRequest = {
|
|
path: '/server/index.ts',
|
|
url: '/server/index.ts',
|
|
ip: '127.0.0.1',
|
|
headers: {}
|
|
};
|
|
const res = createMockResponse();
|
|
const next = vi.fn();
|
|
|
|
securityMiddleware(req, res, next);
|
|
|
|
expect(res.statusCode).toBe(403);
|
|
expect(next).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should block access to git directory', () => {
|
|
const req: MockRequest = {
|
|
path: '/.git/config',
|
|
url: '/.git/config',
|
|
ip: '127.0.0.1',
|
|
headers: {}
|
|
};
|
|
const res = createMockResponse();
|
|
const next = vi.fn();
|
|
|
|
securityMiddleware(req, res, next);
|
|
|
|
expect(res.statusCode).toBe(403);
|
|
expect(next).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should block access to node_modules', () => {
|
|
const req: MockRequest = {
|
|
path: '/node_modules/package/index.js',
|
|
url: '/node_modules/package/index.js',
|
|
ip: '127.0.0.1',
|
|
headers: {}
|
|
};
|
|
const res = createMockResponse();
|
|
const next = vi.fn();
|
|
|
|
securityMiddleware(req, res, next);
|
|
|
|
expect(res.statusCode).toBe(403);
|
|
expect(next).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should block access to README.md', () => {
|
|
const req: MockRequest = {
|
|
path: '/README.md',
|
|
url: '/README.md',
|
|
ip: '127.0.0.1',
|
|
headers: {}
|
|
};
|
|
const res = createMockResponse();
|
|
const next = vi.fn();
|
|
|
|
securityMiddleware(req, res, next);
|
|
|
|
expect(res.statusCode).toBe(403);
|
|
expect(next).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should block access to CLAUDE.md', () => {
|
|
const req: MockRequest = {
|
|
path: '/CLAUDE.md',
|
|
url: '/CLAUDE.md',
|
|
ip: '127.0.0.1',
|
|
headers: {}
|
|
};
|
|
const res = createMockResponse();
|
|
const next = vi.fn();
|
|
|
|
securityMiddleware(req, res, next);
|
|
|
|
expect(res.statusCode).toBe(403);
|
|
expect(next).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should be case insensitive', () => {
|
|
const req: MockRequest = {
|
|
path: '/CONFIG.JSON',
|
|
url: '/CONFIG.JSON',
|
|
ip: '127.0.0.1',
|
|
headers: {}
|
|
};
|
|
const res = createMockResponse();
|
|
const next = vi.fn();
|
|
|
|
securityMiddleware(req, res, next);
|
|
|
|
expect(res.statusCode).toBe(403);
|
|
expect(next).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Allowed File Access', () => {
|
|
it('should allow access to API endpoints', () => {
|
|
const req: MockRequest = {
|
|
path: '/api/workflow-runs',
|
|
url: '/api/workflow-runs',
|
|
ip: '127.0.0.1',
|
|
headers: {}
|
|
};
|
|
const res = createMockResponse();
|
|
const next = vi.fn();
|
|
|
|
securityMiddleware(req, res, next);
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(next).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should allow access to static assets', () => {
|
|
const req: MockRequest = {
|
|
path: '/assets/main.js',
|
|
url: '/assets/main.js',
|
|
ip: '127.0.0.1',
|
|
headers: {}
|
|
};
|
|
const res = createMockResponse();
|
|
const next = vi.fn();
|
|
|
|
securityMiddleware(req, res, next);
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(next).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should allow access to root path', () => {
|
|
const req: MockRequest = {
|
|
path: '/',
|
|
url: '/',
|
|
ip: '127.0.0.1',
|
|
headers: {}
|
|
};
|
|
const res = createMockResponse();
|
|
const next = vi.fn();
|
|
|
|
securityMiddleware(req, res, next);
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(next).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should allow access to legitimate files', () => {
|
|
const req: MockRequest = {
|
|
path: '/favicon.ico',
|
|
url: '/favicon.ico',
|
|
ip: '127.0.0.1',
|
|
headers: {}
|
|
};
|
|
const res = createMockResponse();
|
|
const next = vi.fn();
|
|
|
|
securityMiddleware(req, res, next);
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(next).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Security Logging', () => {
|
|
it('should log blocked access attempts', () => {
|
|
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
|
|
const req: MockRequest = {
|
|
path: '/config.json',
|
|
url: '/config.json',
|
|
ip: '192.168.1.100',
|
|
headers: {}
|
|
};
|
|
const res = createMockResponse();
|
|
const next = vi.fn();
|
|
|
|
securityMiddleware(req, res, next);
|
|
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining('🚫 Blocked access to sensitive file: /config.json from 192.168.1.100')
|
|
);
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
});
|
|
}); |