264 lines
No EOL
10 KiB
JavaScript
264 lines
No EOL
10 KiB
JavaScript
import { jest } from '@jest/globals';
|
|
|
|
// Mock the path and url modules for testing
|
|
jest.unstable_mockModule('path', () => ({
|
|
resolve: jest.fn(),
|
|
extname: jest.fn(),
|
|
sep: '/',
|
|
isAbsolute: jest.fn(),
|
|
normalize: jest.fn()
|
|
}));
|
|
|
|
jest.unstable_mockModule('url', () => ({
|
|
pathToFileURL: jest.fn()
|
|
}));
|
|
|
|
// Mock the root directory
|
|
jest.unstable_mockModule('../dist/index.js', () => ({
|
|
rootDir: '/app/root'
|
|
}));
|
|
|
|
// Import after mocking
|
|
const { secureImportModule, hasExport, getExport } = await import('../dist/utils/plugins.js');
|
|
const path = await import('path');
|
|
const url = await import('url');
|
|
|
|
describe('Plugins utilities', () => {
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
|
|
// Default mock implementations
|
|
path.normalize.mockImplementation((p) => p);
|
|
path.resolve.mockImplementation((root, rel) => `${root}/${rel}`);
|
|
path.extname.mockImplementation((p) => {
|
|
const parts = p.split('.');
|
|
return parts.length > 1 ? `.${parts[parts.length - 1]}` : '';
|
|
});
|
|
path.isAbsolute.mockImplementation((p) => p.startsWith('/'));
|
|
url.pathToFileURL.mockImplementation((p) => ({ href: `file://${p}` }));
|
|
});
|
|
|
|
describe('secureImportModule', () => {
|
|
describe('input validation', () => {
|
|
test('should reject non-string module paths', async () => {
|
|
await expect(secureImportModule(null)).rejects.toThrow('Module path must be a string');
|
|
await expect(secureImportModule(undefined)).rejects.toThrow('Module path must be a string');
|
|
await expect(secureImportModule(123)).rejects.toThrow('Module path must be a string');
|
|
});
|
|
|
|
test('should reject empty module paths', async () => {
|
|
await expect(secureImportModule('')).rejects.toThrow('Module path cannot be empty');
|
|
});
|
|
|
|
test('should reject paths that are too long', async () => {
|
|
const longPath = 'a'.repeat(1025) + '.js';
|
|
await expect(secureImportModule(longPath)).rejects.toThrow('Module path too long');
|
|
});
|
|
});
|
|
|
|
describe('security pattern validation', () => {
|
|
test('should reject directory traversal attempts', async () => {
|
|
await expect(secureImportModule('../evil.js')).rejects.toThrow('Module path contains blocked pattern');
|
|
await expect(secureImportModule('../../system.js')).rejects.toThrow('Module path contains blocked pattern');
|
|
await expect(secureImportModule('folder/../escape.js')).rejects.toThrow('Module path contains blocked pattern');
|
|
});
|
|
|
|
test('should reject double slash patterns', async () => {
|
|
await expect(secureImportModule('folder//file.js')).rejects.toThrow('Module path contains blocked pattern');
|
|
await expect(secureImportModule('//root.js')).rejects.toThrow('Module path contains blocked pattern');
|
|
});
|
|
|
|
test('should reject null byte injections', async () => {
|
|
await expect(secureImportModule('file\0.js')).rejects.toThrow('Module path contains blocked pattern');
|
|
});
|
|
|
|
test('should reject control characters', async () => {
|
|
await expect(secureImportModule('file\x01.js')).rejects.toThrow('Module path contains blocked pattern');
|
|
await expect(secureImportModule('file\x1f.js')).rejects.toThrow('Module path contains blocked pattern');
|
|
});
|
|
|
|
test('should reject node_modules access', async () => {
|
|
await expect(secureImportModule('node_modules/evil.js')).rejects.toThrow('Module path contains blocked pattern');
|
|
await expect(secureImportModule('NODE_MODULES/evil.js')).rejects.toThrow('Module path contains blocked pattern');
|
|
});
|
|
|
|
test('should reject sensitive file access', async () => {
|
|
await expect(secureImportModule('package.json')).rejects.toThrow('Module path contains blocked pattern');
|
|
await expect(secureImportModule('.env')).rejects.toThrow('Module path contains blocked pattern');
|
|
await expect(secureImportModule('config/.ENV')).rejects.toThrow('Module path contains blocked pattern');
|
|
});
|
|
|
|
test('should reject paths that are too deep', async () => {
|
|
const deepPath = 'a/'.repeat(25) + 'file.js';
|
|
path.normalize.mockReturnValue(deepPath);
|
|
|
|
await expect(secureImportModule(deepPath)).rejects.toThrow('Module path too deep');
|
|
});
|
|
});
|
|
|
|
describe('file extension validation', () => {
|
|
test('should reject invalid file extensions', async () => {
|
|
path.extname.mockReturnValue('.txt');
|
|
|
|
await expect(secureImportModule('file.txt')).rejects.toThrow('Only .js, .mjs files can be imported');
|
|
});
|
|
|
|
test('should reject files without extensions', async () => {
|
|
path.extname.mockReturnValue('');
|
|
|
|
await expect(secureImportModule('file')).rejects.toThrow('Only .js, .mjs files can be imported');
|
|
});
|
|
|
|
test('should accept valid .js extension validation', async () => {
|
|
path.extname.mockReturnValue('.js');
|
|
path.isAbsolute.mockReturnValue(false);
|
|
path.resolve.mockReturnValue('/app/root/valid.js');
|
|
path.normalize.mockImplementation((p) => p);
|
|
|
|
// This will fail at import but pass extension validation
|
|
await expect(secureImportModule('valid.js')).rejects.toThrow('Failed to import module');
|
|
});
|
|
|
|
test('should accept valid .mjs extension validation', async () => {
|
|
path.extname.mockReturnValue('.mjs');
|
|
path.isAbsolute.mockReturnValue(false);
|
|
path.resolve.mockReturnValue('/app/root/valid.mjs');
|
|
path.normalize.mockImplementation((p) => p);
|
|
|
|
// This will fail at import but pass extension validation
|
|
await expect(secureImportModule('valid.mjs')).rejects.toThrow('Failed to import module');
|
|
});
|
|
});
|
|
|
|
describe('path resolution security', () => {
|
|
test('should reject absolute paths', async () => {
|
|
path.extname.mockReturnValue('.js');
|
|
path.isAbsolute.mockReturnValue(true);
|
|
|
|
await expect(secureImportModule('/absolute/path.js')).rejects.toThrow('Absolute paths are not allowed');
|
|
});
|
|
|
|
test('should reject paths outside application root', async () => {
|
|
path.extname.mockReturnValue('.js');
|
|
path.isAbsolute.mockReturnValue(false);
|
|
path.resolve.mockReturnValue('/outside/root/file.js');
|
|
path.normalize.mockImplementation((p) => p);
|
|
|
|
await expect(secureImportModule('file.js')).rejects.toThrow('Module path outside of application root');
|
|
});
|
|
|
|
test('should detect symbolic link traversal', async () => {
|
|
path.extname.mockReturnValue('.js');
|
|
path.isAbsolute.mockReturnValue(false);
|
|
path.resolve.mockReturnValue('/app/root/../outside.js');
|
|
path.normalize.mockImplementation((p) => p);
|
|
|
|
await expect(secureImportModule('file.js')).rejects.toThrow('Path traversal detected');
|
|
});
|
|
|
|
test('should validate paths within application root', async () => {
|
|
path.extname.mockReturnValue('.js');
|
|
path.isAbsolute.mockReturnValue(false);
|
|
path.resolve.mockReturnValue('/app/root/plugins/test.js');
|
|
path.normalize.mockImplementation((p) => p);
|
|
|
|
// This will fail at import but pass path validation
|
|
await expect(secureImportModule('plugins/test.js')).rejects.toThrow('Failed to import module');
|
|
});
|
|
});
|
|
|
|
describe('import operation and error handling', () => {
|
|
test('should handle import failures gracefully', async () => {
|
|
path.extname.mockReturnValue('.js');
|
|
path.isAbsolute.mockReturnValue(false);
|
|
path.resolve.mockReturnValue('/app/root/file.js');
|
|
path.normalize.mockImplementation((p) => p);
|
|
|
|
// Since we can't easily mock import() in ES modules,
|
|
// this will naturally fail and test our error handling
|
|
await expect(secureImportModule('file.js')).rejects.toThrow('Failed to import module');
|
|
});
|
|
|
|
test('should handle non-Error exceptions in path validation', async () => {
|
|
path.extname.mockReturnValue('.js');
|
|
path.isAbsolute.mockReturnValue(false);
|
|
path.resolve.mockImplementation(() => {
|
|
throw "Non-error exception";
|
|
});
|
|
|
|
await expect(secureImportModule('file.js')).rejects.toThrow('Module import failed');
|
|
});
|
|
|
|
test('should handle unknown errors gracefully', async () => {
|
|
path.extname.mockImplementation(() => {
|
|
throw { unknown: 'error' };
|
|
});
|
|
|
|
await expect(secureImportModule('file.js')).rejects.toThrow('Module import failed due to unknown error');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('hasExport', () => {
|
|
test('should return true for existing exports', () => {
|
|
const module = { testFunction: () => {}, testValue: 'test' };
|
|
|
|
expect(hasExport(module, 'testFunction')).toBe(true);
|
|
expect(hasExport(module, 'testValue')).toBe(true);
|
|
});
|
|
|
|
test('should return false for non-existent exports', () => {
|
|
const module = { testFunction: () => {} };
|
|
|
|
expect(hasExport(module, 'nonExistent')).toBe(false);
|
|
});
|
|
|
|
test('should return false for undefined exports', () => {
|
|
const module = { testValue: undefined };
|
|
|
|
expect(hasExport(module, 'testValue')).toBe(false);
|
|
});
|
|
|
|
test('should handle edge cases', () => {
|
|
const module = {};
|
|
|
|
expect(hasExport(module, 'toString')).toBe(true); // inherited from Object.prototype
|
|
expect(hasExport(module, 'constructor')).toBe(true); // inherited from Object.prototype
|
|
expect(hasExport(module, 'nonExistentMethod')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('getExport', () => {
|
|
test('should return export values correctly', () => {
|
|
const testFunction = () => 'test';
|
|
const module = {
|
|
testFunction,
|
|
testValue: 'hello'
|
|
};
|
|
|
|
expect(getExport(module, 'testFunction')).toBe(testFunction);
|
|
expect(getExport(module, 'testValue')).toBe('hello');
|
|
});
|
|
|
|
test('should return undefined for non-existent exports', () => {
|
|
const module = { testFunction: () => {} };
|
|
|
|
expect(getExport(module, 'nonExistent')).toBeUndefined();
|
|
});
|
|
|
|
test('should return actual values including falsy ones', () => {
|
|
const module = {
|
|
zero: 0,
|
|
false: false,
|
|
empty: '',
|
|
null: null
|
|
};
|
|
|
|
expect(getExport(module, 'zero')).toBe(0);
|
|
expect(getExport(module, 'false')).toBe(false);
|
|
expect(getExport(module, 'empty')).toBe('');
|
|
expect(getExport(module, 'null')).toBe(null);
|
|
});
|
|
});
|
|
});
|