dosProtection.test.tsโข12.9 kB
/**
* Tests for DOS Protection utilities
*
* Coverage for DOS vulnerability fixes:
* - SafeRegex timeout protection
* - Pattern validation and escaping
* - Safe glob conversion
* - Rate limiting
*/
import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
import {
SafeRegex,
DOSProtection,
safeTest,
safeMatch,
escapeRegex,
globToRegex,
safeSplit,
safeReplace
} from '../../../../src/security/dosProtection.js';
describe('SafeRegex', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(console, 'warn').mockImplementation(() => {});
jest.spyOn(console, 'error').mockImplementation(() => {});
SafeRegex.clearCache();
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('test()', () => {
it('should safely test patterns', () => {
expect(SafeRegex.test(/hello/, 'hello world')).toBe(true);
expect(SafeRegex.test(/foo/, 'bar')).toBe(false);
});
it('should handle string patterns', () => {
// FIX: Use String.raw for escaped backslashes (SonarCloud S7780)
expect(SafeRegex.test(String.raw`\d+`, '123')).toBe(true);
expect(SafeRegex.test(String.raw`\d+`, 'abc')).toBe(false);
});
it('should reject dangerous patterns', () => {
// Intentional test case for ReDoS detection
// This dangerous pattern is used to verify SafeRegex.test() correctly detects
// and blocks catastrophic backtracking patterns
// codeql[js/polynomial-redos]
const dangerous = '(.+)+$';
expect(SafeRegex.test(dangerous, 'aaaaaaaaaa')).toBe(false);
// FIX: Check for full formatted message string
// Previously: Expected partial string match
// Now: Checks for complete message format
expect(console.warn).toHaveBeenCalledWith(
expect.stringContaining('Dangerous pattern detected')
);
});
it('should enforce input length limits', () => {
const longInput = 'a'.repeat(20000);
expect(SafeRegex.test(/a+/, longInput)).toBe(false);
expect(console.warn).toHaveBeenCalledWith(
expect.stringContaining('Input too long')
);
});
it('should handle invalid input gracefully', () => {
expect(SafeRegex.test(/test/, null as any)).toBe(false);
expect(SafeRegex.test(/test/, undefined as any)).toBe(false);
expect(SafeRegex.test(/test/, '' as any)).toBe(false);
});
it('should reset global regex lastIndex', () => {
const globalRegex = /test/g;
globalRegex.lastIndex = 5;
SafeRegex.test(globalRegex, 'test test');
expect(globalRegex.lastIndex).toBe(0);
});
});
describe('match()', () => {
it('should safely match patterns', () => {
const result = SafeRegex.match('hello world', /hello/);
// FIX: RegExpMatchArray has extra properties (index, input, groups)
// Previously: Used toEqual which checks all properties
// Now: Check array content with toMatchObject or check specific values
expect(result).toBeTruthy();
expect(result?.[0]).toBe('hello');
expect(result?.length).toBe(1);
});
it('should return null for no match', () => {
expect(SafeRegex.match('foo', /bar/)).toBeNull();
});
it('should enforce length limits', () => {
const longInput = 'a'.repeat(20000);
expect(SafeRegex.match(longInput, /a+/)).toBeNull();
expect(console.warn).toHaveBeenCalledWith(
expect.stringContaining('Input too long')
);
});
it('should reject dangerous patterns', () => {
// Intentional test case for ReDoS detection
// This dangerous pattern verifies SafeRegex.match() correctly detects
// and blocks catastrophic backtracking patterns
// codeql[js/polynomial-redos]
const dangerous = '(.+)+$';
expect(SafeRegex.match('aaaaaaaaaa', dangerous)).toBeNull();
});
});
describe('escape()', () => {
it('should escape regex special characters', () => {
expect(SafeRegex.escape('.*+?^${}()|[]\\'))
.toBe('\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\');
});
it('should handle normal strings', () => {
expect(SafeRegex.escape('hello world')).toBe('hello world');
});
it('should handle empty and invalid input', () => {
expect(SafeRegex.escape('')).toBe('');
expect(SafeRegex.escape(null as any)).toBe('');
expect(SafeRegex.escape(undefined as any)).toBe('');
});
});
describe('globToRegex()', () => {
it('should convert simple glob patterns', () => {
const regex = SafeRegex.globToRegex('*.js');
expect(regex).toBeTruthy();
expect(regex!.test('file.js')).toBe(true);
expect(regex!.test('file.ts')).toBe(false);
expect(regex!.test('dir/file.js')).toBe(false); // * doesn't match /
});
it('should handle ** glob patterns', () => {
const regex = SafeRegex.globToRegex('src/**/*.ts');
expect(regex).toBeTruthy();
expect(regex!.test('src/file.ts')).toBe(true);
expect(regex!.test('src/dir/file.ts')).toBe(true);
expect(regex!.test('src/a/b/c/file.ts')).toBe(true);
expect(regex!.test('file.ts')).toBe(false);
});
it('should handle ? wildcards', () => {
const regex = SafeRegex.globToRegex('file?.txt');
expect(regex).toBeTruthy();
expect(regex!.test('file1.txt')).toBe(true);
expect(regex!.test('fileA.txt')).toBe(true);
expect(regex!.test('file.txt')).toBe(false);
expect(regex!.test('file12.txt')).toBe(false);
});
it('should reject overly long patterns', () => {
const longGlob = '*'.repeat(2000);
expect(SafeRegex.globToRegex(longGlob)).toBeNull();
expect(console.warn).toHaveBeenCalledWith(
expect.stringContaining('Glob pattern too long')
);
});
it('should handle invalid input', () => {
expect(SafeRegex.globToRegex('')).toBeNull();
expect(SafeRegex.globToRegex(null as any)).toBeNull();
});
});
});
describe('DOSProtection', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(console, 'warn').mockImplementation(() => {});
jest.spyOn(console, 'error').mockImplementation(() => {});
DOSProtection.cleanup();
});
afterEach(() => {
jest.restoreAllMocks();
DOSProtection.cleanup();
});
describe('safeSplit()', () => {
it('should split strings safely', () => {
expect(DOSProtection.safeSplit('a,b,c', ',')).toEqual(['a', 'b', 'c']);
expect(DOSProtection.safeSplit('a b c', /\s+/)).toEqual(['a', 'b', 'c']);
});
it('should handle length limits', () => {
const longString = 'a'.repeat(200000);
expect(DOSProtection.safeSplit(longString, ',')).toEqual([]);
});
it('should handle empty input', () => {
expect(DOSProtection.safeSplit('', ',')).toEqual(['']);
expect(DOSProtection.safeSplit(null as any, ',')).toEqual([]);
});
it('should apply split limits', () => {
expect(DOSProtection.safeSplit('a,b,c,d,e', ',', 3))
.toEqual(['a', 'b', 'c,d,e']);
});
});
describe('safeReplace()', () => {
it('should replace patterns safely', () => {
expect(DOSProtection.safeReplace('hello world', /world/, 'universe'))
.toBe('hello universe');
});
it('should block dangerous patterns', () => {
// NOSONAR - Intentionally vulnerable regex pattern for testing DOS protection
// This pattern is used to verify our security features correctly detect and block
// catastrophic backtracking patterns. It's contained within DOSProtection wrapper.
// codeql[js/polynomial-redos]
const dangerous = /(.+)+$/; // NOSONAR
const input = 'aaaaaaaaaa';
expect(DOSProtection.safeReplace(input, dangerous, 'x'))
.toBe(input); // Returns original on danger
expect(console.warn).toHaveBeenCalledWith(
expect.stringContaining('Dangerous replace pattern blocked')
);
});
it('should handle string patterns', () => {
expect(DOSProtection.safeReplace('hello', 'l', 'L'))
.toBe('heLlo'); // Only first occurrence
});
it('should handle length limits', () => {
const longString = 'a'.repeat(200000);
expect(DOSProtection.safeReplace(longString, /a/, 'b'))
.toBe('');
});
});
describe('rateLimit()', () => {
it('should allow operations within rate limit', () => {
for (let i = 0; i < 50; i++) {
expect(DOSProtection.rateLimit('test-op', 100)).toBe(true);
}
});
it('should block operations exceeding rate limit', () => {
// Fill up the limit
for (let i = 0; i < 100; i++) {
DOSProtection.rateLimit('test-op', 100);
}
// Next one should be blocked
expect(DOSProtection.rateLimit('test-op', 100)).toBe(false);
expect(console.warn).toHaveBeenCalledWith(
expect.stringContaining('Rate limit exceeded')
);
});
it('should track different operations separately', () => {
// Use up op1 limit
for (let i = 0; i < 10; i++) {
DOSProtection.rateLimit('op1', 10);
}
expect(DOSProtection.rateLimit('op1', 10)).toBe(false);
// op2 should still work
expect(DOSProtection.rateLimit('op2', 10)).toBe(true);
});
});
});
describe('Convenience functions', () => {
it('should export bound functions', () => {
expect(safeTest).toBeDefined();
expect(safeMatch).toBeDefined();
expect(escapeRegex).toBeDefined();
expect(globToRegex).toBeDefined();
expect(safeSplit).toBeDefined();
expect(safeReplace).toBeDefined();
});
it('should work correctly', () => {
expect(safeTest(/test/, 'test')).toBe(true);
// FIX: Use String.raw for escaped backslashes (SonarCloud S7780)
expect(escapeRegex('.*')).toBe(String.raw`\.\*`);
expect(safeSplit('a,b', ',')).toEqual(['a', 'b']);
});
});
describe('Performance Tests (Reviewer Recommendation)', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(console, 'warn').mockImplementation(() => {});
SafeRegex.clearCache();
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should timeout slow regex execution', () => {
// Intentional test case for timeout protection
// This nested quantifier pattern verifies SafeRegex enforces timeouts
// and prevents actual DOS attacks during testing
// codeql[js/polynomial-redos]
const slowPattern = '(a+)+$';
const maliciousInput = 'a'.repeat(100) + 'X';
const start = Date.now();
const result = SafeRegex.test(slowPattern, maliciousInput);
const duration = Date.now() - start;
// Should reject dangerous pattern quickly without executing
expect(result).toBe(false);
expect(duration).toBeLessThan(10); // Should fail fast, not timeout
expect(console.warn).toHaveBeenCalledWith(
expect.stringContaining('Dangerous pattern detected')
);
});
it('should handle large inputs efficiently', () => {
const largeInput = 'a'.repeat(5000);
const simplePattern = /test/;
const start = Date.now();
const result = SafeRegex.test(simplePattern, largeInput);
const duration = Date.now() - start;
expect(result).toBe(false);
expect(duration).toBeLessThan(50); // Should be fast for simple patterns
});
it('should detect timeout for complex patterns', () => {
// This tests that we properly detect and handle slow patterns
// NOSONAR - Intentionally complex regex pattern for testing timeout detection
// This pattern is vulnerable to super-linear runtime but is safely contained
// within SafeRegex.test() which enforces timeouts and prevents actual DOS.
// codeql[js/polynomial-redos]
const complexPattern = /^(([a-z])+.)+$/; // NOSONAR
const testInput = 'abcdefghijklmnopqrstuvwxyz'.repeat(10);
const start = Date.now();
SafeRegex.test(complexPattern, testInput, { timeout: 50 });
const duration = Date.now() - start;
// Should complete within reasonable time
expect(duration).toBeLessThan(100);
});
it('should efficiently cache patterns', () => {
// FIX: Use String.raw for escaped backslashes (SonarCloud S7780)
const pattern = String.raw`\d+`;
// First call should compile
SafeRegex.test(pattern, '123');
// Second call should use cache (much faster)
const start = Date.now();
for (let i = 0; i < 100; i++) {
SafeRegex.test(pattern, '456');
}
const duration = Date.now() - start;
expect(duration).toBeLessThan(10); // Cached access should be very fast
});
it('should limit pattern cache size', () => {
// Test that we don't have unbounded memory growth
for (let i = 0; i < 1500; i++) {
SafeRegex.test(`pattern${i}`, 'test');
}
// Cache should be limited (implementation specific)
// Just ensure no errors and reasonable performance
expect(true).toBe(true);
});
});