Files
automaker/libs/platform/tests/security.test.ts
Test User c7ebdb1f80 feat: Implement API key authentication with rate limiting and secure comparison
- Added rate limiting to the authentication middleware to prevent brute-force attacks.
- Introduced a secure comparison function to mitigate timing attacks during API key validation.
- Created a new rate limiter class to track failed authentication attempts and block requests after exceeding the maximum allowed failures.
- Updated the authentication middleware to handle rate limiting and secure key comparison.
- Enhanced error handling for rate-limited requests, providing appropriate responses to clients.
2025-12-24 14:49:47 -05:00

384 lines
14 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import path from 'path';
describe('security.ts', () => {
let originalEnv: NodeJS.ProcessEnv;
beforeEach(() => {
// Save original environment
originalEnv = { ...process.env };
// Reset modules to get fresh state
vi.resetModules();
});
afterEach(() => {
// Restore original environment
process.env = originalEnv;
});
describe('initAllowedPaths', () => {
it('should load ALLOWED_ROOT_DIRECTORY if set', async () => {
process.env.ALLOWED_ROOT_DIRECTORY = '/projects';
delete process.env.DATA_DIR;
const { initAllowedPaths, getAllowedPaths } = await import('../src/security');
initAllowedPaths();
const allowed = getAllowedPaths();
expect(allowed).toContain(path.resolve('/projects'));
});
it('should load DATA_DIR if set', async () => {
delete process.env.ALLOWED_ROOT_DIRECTORY;
process.env.DATA_DIR = '/data/directory';
const { initAllowedPaths, getAllowedPaths } = await import('../src/security');
initAllowedPaths();
const allowed = getAllowedPaths();
expect(allowed).toContain(path.resolve('/data/directory'));
});
it('should load both ALLOWED_ROOT_DIRECTORY and DATA_DIR if both set', async () => {
process.env.ALLOWED_ROOT_DIRECTORY = '/projects';
process.env.DATA_DIR = '/app/data';
const { initAllowedPaths, getAllowedPaths } = await import('../src/security');
initAllowedPaths();
const allowed = getAllowedPaths();
expect(allowed).toContain(path.resolve('/projects'));
expect(allowed).toContain(path.resolve('/app/data'));
});
it('should handle missing environment variables gracefully', async () => {
delete process.env.ALLOWED_ROOT_DIRECTORY;
delete process.env.DATA_DIR;
const { initAllowedPaths } = await import('../src/security');
expect(() => initAllowedPaths()).not.toThrow();
});
});
describe('isPathAllowed', () => {
it('should allow paths within ALLOWED_ROOT_DIRECTORY', async () => {
process.env.ALLOWED_ROOT_DIRECTORY = '/allowed';
delete process.env.DATA_DIR;
const { initAllowedPaths, isPathAllowed } = await import('../src/security');
initAllowedPaths();
expect(isPathAllowed('/allowed/file.txt')).toBe(true);
expect(isPathAllowed('/allowed/subdir/file.txt')).toBe(true);
});
it('should deny paths outside ALLOWED_ROOT_DIRECTORY', async () => {
process.env.ALLOWED_ROOT_DIRECTORY = '/allowed';
delete process.env.DATA_DIR;
const { initAllowedPaths, isPathAllowed } = await import('../src/security');
initAllowedPaths();
expect(isPathAllowed('/not-allowed/file.txt')).toBe(false);
expect(isPathAllowed('/etc/passwd')).toBe(false);
});
it('should always allow DATA_DIR paths', async () => {
process.env.ALLOWED_ROOT_DIRECTORY = '/projects';
process.env.DATA_DIR = '/app/data';
const { initAllowedPaths, isPathAllowed } = await import('../src/security');
initAllowedPaths();
// DATA_DIR paths are always allowed
expect(isPathAllowed('/app/data/settings.json')).toBe(true);
expect(isPathAllowed('/app/data/credentials.json')).toBe(true);
});
it('should deny all paths in strict mode when no restrictions configured', async () => {
delete process.env.ALLOWED_ROOT_DIRECTORY;
delete process.env.DATA_DIR;
delete process.env.SECURITY_MODE; // Default to strict
const { initAllowedPaths, isPathAllowed } = await import('../src/security');
initAllowedPaths();
// In strict mode, paths are denied when no ALLOWED_ROOT_DIRECTORY is set
expect(isPathAllowed('/any/path')).toBe(false);
expect(isPathAllowed('/etc/passwd')).toBe(false);
});
it('should allow all paths in permissive mode when no restrictions configured', async () => {
delete process.env.ALLOWED_ROOT_DIRECTORY;
delete process.env.DATA_DIR;
process.env.SECURITY_MODE = 'permissive';
const { initAllowedPaths, isPathAllowed } = await import('../src/security');
initAllowedPaths();
// In permissive mode, all paths are allowed when no restrictions configured
expect(isPathAllowed('/any/path')).toBe(true);
expect(isPathAllowed('/etc/passwd')).toBe(true);
});
it('should deny non-DATA_DIR paths in strict mode when only DATA_DIR is configured', async () => {
delete process.env.ALLOWED_ROOT_DIRECTORY;
process.env.DATA_DIR = '/data';
delete process.env.SECURITY_MODE; // Default to strict
const { initAllowedPaths, isPathAllowed } = await import('../src/security');
initAllowedPaths();
// DATA_DIR should be allowed
expect(isPathAllowed('/data/file.txt')).toBe(true);
// Other paths should be denied in strict mode
expect(isPathAllowed('/any/path')).toBe(false);
});
it('should allow all paths in permissive mode when only DATA_DIR is configured', async () => {
delete process.env.ALLOWED_ROOT_DIRECTORY;
process.env.DATA_DIR = '/data';
process.env.SECURITY_MODE = 'permissive';
const { initAllowedPaths, isPathAllowed } = await import('../src/security');
initAllowedPaths();
// DATA_DIR should be allowed
expect(isPathAllowed('/data/file.txt')).toBe(true);
// Other paths should also be allowed in permissive mode
expect(isPathAllowed('/any/path')).toBe(true);
});
});
describe('validatePath', () => {
it('should return resolved path for allowed paths', async () => {
process.env.ALLOWED_ROOT_DIRECTORY = '/allowed';
delete process.env.DATA_DIR;
const { initAllowedPaths, validatePath } = await import('../src/security');
initAllowedPaths();
const result = validatePath('/allowed/file.txt');
expect(result).toBe(path.resolve('/allowed/file.txt'));
});
it('should throw error for paths outside allowed directories', async () => {
process.env.ALLOWED_ROOT_DIRECTORY = '/allowed';
delete process.env.DATA_DIR;
const { initAllowedPaths, validatePath, PathNotAllowedError } =
await import('../src/security');
initAllowedPaths();
expect(() => validatePath('/not-allowed/file.txt')).toThrow(PathNotAllowedError);
});
it('should resolve relative paths', async () => {
const cwd = process.cwd();
process.env.ALLOWED_ROOT_DIRECTORY = cwd;
delete process.env.DATA_DIR;
const { initAllowedPaths, validatePath } = await import('../src/security');
initAllowedPaths();
const result = validatePath('./file.txt');
expect(result).toBe(path.resolve(cwd, './file.txt'));
});
it('should throw in strict mode when no restrictions configured', async () => {
delete process.env.ALLOWED_ROOT_DIRECTORY;
delete process.env.DATA_DIR;
delete process.env.SECURITY_MODE; // Default to strict
const { initAllowedPaths, validatePath, PathNotAllowedError } =
await import('../src/security');
initAllowedPaths();
// In strict mode, paths are denied when no ALLOWED_ROOT_DIRECTORY is set
expect(() => validatePath('/any/path')).toThrow(PathNotAllowedError);
});
it('should not throw in permissive mode when no restrictions configured', async () => {
delete process.env.ALLOWED_ROOT_DIRECTORY;
delete process.env.DATA_DIR;
process.env.SECURITY_MODE = 'permissive';
const { initAllowedPaths, validatePath } = await import('../src/security');
initAllowedPaths();
// In permissive mode, all paths are allowed when no restrictions configured
expect(() => validatePath('/any/path')).not.toThrow();
});
});
describe('getAllowedPaths', () => {
it('should return empty array when no paths configured', async () => {
delete process.env.ALLOWED_ROOT_DIRECTORY;
delete process.env.DATA_DIR;
const { initAllowedPaths, getAllowedPaths } = await import('../src/security');
initAllowedPaths();
const allowed = getAllowedPaths();
expect(Array.isArray(allowed)).toBe(true);
expect(allowed).toHaveLength(0);
});
it('should return configured paths', async () => {
process.env.ALLOWED_ROOT_DIRECTORY = '/projects';
process.env.DATA_DIR = '/data';
const { initAllowedPaths, getAllowedPaths } = await import('../src/security');
initAllowedPaths();
const allowed = getAllowedPaths();
expect(allowed).toContain(path.resolve('/projects'));
expect(allowed).toContain(path.resolve('/data'));
});
});
describe('getAllowedRootDirectory', () => {
it('should return the configured root directory', async () => {
process.env.ALLOWED_ROOT_DIRECTORY = '/projects';
const { initAllowedPaths, getAllowedRootDirectory } = await import('../src/security');
initAllowedPaths();
expect(getAllowedRootDirectory()).toBe(path.resolve('/projects'));
});
it('should return null when not configured', async () => {
delete process.env.ALLOWED_ROOT_DIRECTORY;
const { initAllowedPaths, getAllowedRootDirectory } = await import('../src/security');
initAllowedPaths();
expect(getAllowedRootDirectory()).toBeNull();
});
});
describe('path traversal attack prevention', () => {
it('should block basic path traversal with ../', async () => {
process.env.ALLOWED_ROOT_DIRECTORY = '/allowed';
delete process.env.DATA_DIR;
const { initAllowedPaths, isPathAllowed } = await import('../src/security');
initAllowedPaths();
expect(isPathAllowed('/allowed/../etc/passwd')).toBe(false);
expect(isPathAllowed('/allowed/subdir/../../etc/passwd')).toBe(false);
});
it('should block path traversal with multiple ../ sequences', async () => {
process.env.ALLOWED_ROOT_DIRECTORY = '/allowed/deep/nested';
delete process.env.DATA_DIR;
const { initAllowedPaths, isPathAllowed } = await import('../src/security');
initAllowedPaths();
expect(isPathAllowed('/allowed/deep/nested/../../../etc/passwd')).toBe(false);
expect(isPathAllowed('/allowed/deep/nested/../../../../root')).toBe(false);
});
it('should block standalone .. in path components', async () => {
process.env.ALLOWED_ROOT_DIRECTORY = '/allowed';
delete process.env.DATA_DIR;
const { initAllowedPaths, isPathAllowed } = await import('../src/security');
initAllowedPaths();
expect(isPathAllowed('/allowed/foo/..bar')).toBe(true); // This is a valid filename, not traversal
expect(isPathAllowed('/allowed/foo/../bar')).toBe(true); // Resolves within allowed
expect(isPathAllowed('/allowed/../notallowed')).toBe(false);
});
it('should handle edge case of path ending with /..', async () => {
process.env.ALLOWED_ROOT_DIRECTORY = '/allowed/subdir';
delete process.env.DATA_DIR;
const { initAllowedPaths, isPathAllowed } = await import('../src/security');
initAllowedPaths();
expect(isPathAllowed('/allowed/subdir/..')).toBe(false);
expect(isPathAllowed('/allowed/subdir/../..')).toBe(false);
});
it('should properly resolve and block complex traversal attempts', async () => {
process.env.ALLOWED_ROOT_DIRECTORY = '/home/user/projects';
delete process.env.DATA_DIR;
const { initAllowedPaths, isPathAllowed } = await import('../src/security');
initAllowedPaths();
// Attempt to escape via complex path
expect(isPathAllowed('/home/user/projects/app/../../../etc/shadow')).toBe(false);
// Valid path that uses .. but stays within allowed
expect(isPathAllowed('/home/user/projects/app/../lib/file.ts')).toBe(true);
});
it('should validate path throws on traversal attacks', async () => {
process.env.ALLOWED_ROOT_DIRECTORY = '/allowed';
delete process.env.DATA_DIR;
const { initAllowedPaths, validatePath, PathNotAllowedError } =
await import('../src/security');
initAllowedPaths();
expect(() => validatePath('/allowed/../etc/passwd')).toThrow(PathNotAllowedError);
expect(() => validatePath('/allowed/../../root/.ssh/id_rsa')).toThrow(PathNotAllowedError);
});
it('should handle paths with mixed separators (cross-platform)', async () => {
process.env.ALLOWED_ROOT_DIRECTORY = '/allowed';
delete process.env.DATA_DIR;
const { initAllowedPaths, isPathAllowed } = await import('../src/security');
initAllowedPaths();
// Node's path.resolve handles these correctly on each platform
const maliciousPath = path.resolve('/allowed', '..', 'etc', 'passwd');
expect(isPathAllowed(maliciousPath)).toBe(false);
});
it('should correctly identify paths at the boundary', async () => {
process.env.ALLOWED_ROOT_DIRECTORY = '/allowed';
delete process.env.DATA_DIR;
const { initAllowedPaths, isPathAllowed } = await import('../src/security');
initAllowedPaths();
// The allowed directory itself should be allowed
expect(isPathAllowed('/allowed')).toBe(true);
expect(isPathAllowed('/allowed/')).toBe(true);
// Parent of allowed should not be allowed
expect(isPathAllowed('/')).toBe(false);
// Sibling directories should not be allowed
expect(isPathAllowed('/allowed2')).toBe(false);
expect(isPathAllowed('/allowedextra')).toBe(false);
});
});
describe('getDataDirectory', () => {
it('should return the configured data directory', async () => {
process.env.DATA_DIR = '/data';
const { initAllowedPaths, getDataDirectory } = await import('../src/security');
initAllowedPaths();
expect(getDataDirectory()).toBe(path.resolve('/data'));
});
it('should return null when not configured', async () => {
delete process.env.DATA_DIR;
const { initAllowedPaths, getDataDirectory } = await import('../src/security');
initAllowedPaths();
expect(getDataDirectory()).toBeNull();
});
});
});