diff --git a/tests/security-fixes.test.js b/tests/security-fixes.test.js
new file mode 100644
index 0000000..fe854f3
--- /dev/null
+++ b/tests/security-fixes.test.js
@@ -0,0 +1,325 @@
+/**
+ * Security Fixes Integration Test Suite
+ *
+ * 测试最近修复的安全问题:
+ * 1. XSS 防护 - sanitizeProviderData
+ * 2. 路径遍历防护 - 路径验证逻辑
+ * 3. 文件锁超时机制
+ * 4. 健康检查方法调用
+ */
+
+import { describe, test, expect } from '@jest/globals';
+import { fetch } from 'undici';
+
+const TEST_SERVER_BASE_URL = process.env.TEST_SERVER_BASE_URL || 'http://localhost:3000';
+const TEST_API_KEY = process.env.TEST_API_KEY || '123456';
+
+describe('Security Fixes Integration Tests', () => {
+
+ describe('XSS Protection', () => {
+ test('should remove script tags from customName', async () => {
+ const maliciousName = 'TestProvider';
+ const response = await fetch(`${TEST_SERVER_BASE_URL}/api/providers`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${TEST_API_KEY}`
+ },
+ body: JSON.stringify({
+ providerType: 'openai-custom',
+ providerConfig: {
+ customName: maliciousName,
+ OPENAI_CUSTOM_BASE_URL: 'https://api.example.com',
+ OPENAI_CUSTOM_API_KEY: 'test-key'
+ }
+ })
+ });
+
+ const data = await response.json();
+ expect(data.provider.customName).not.toContain('');
+ expect(data.provider.customName).toContain('TestProvider');
+ });
+
+ test('should reject javascript: protocol', async () => {
+ const maliciousName = 'javascript:alert("XSS")';
+ const response = await fetch(`${TEST_SERVER_BASE_URL}/api/providers`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${TEST_API_KEY}`
+ },
+ body: JSON.stringify({
+ providerType: 'openai-custom',
+ providerConfig: {
+ customName: maliciousName,
+ OPENAI_CUSTOM_BASE_URL: 'https://api.example.com',
+ OPENAI_CUSTOM_API_KEY: 'test-key'
+ }
+ })
+ });
+
+ const data = await response.json();
+ expect(data.provider.customName).toBe('');
+ });
+
+ test('should reject data: protocol', async () => {
+ const maliciousName = 'data:text/html,';
+ const response = await fetch(`${TEST_SERVER_BASE_URL}/api/providers`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${TEST_API_KEY}`
+ },
+ body: JSON.stringify({
+ providerType: 'openai-custom',
+ providerConfig: {
+ customName: maliciousName,
+ OPENAI_CUSTOM_BASE_URL: 'https://api.example.com',
+ OPENAI_CUSTOM_API_KEY: 'test-key'
+ }
+ })
+ });
+
+ const data = await response.json();
+ expect(data.provider.customName).toBe('');
+ });
+
+ test('should remove HTML event handlers', async () => {
+ const maliciousName = '';
+ const response = await fetch(`${TEST_SERVER_BASE_URL}/api/providers`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${TEST_API_KEY}`
+ },
+ body: JSON.stringify({
+ providerType: 'openai-custom',
+ providerConfig: {
+ customName: maliciousName,
+ OPENAI_CUSTOM_BASE_URL: 'https://api.example.com',
+ OPENAI_CUSTOM_API_KEY: 'test-key'
+ }
+ })
+ });
+
+ const data = await response.json();
+ expect(data.provider.customName).not.toContain('onerror');
+ expect(data.provider.customName).not.toContain('
{
+ const maliciousName = '<script>alert(1)</script>';
+ const response = await fetch(`${TEST_SERVER_BASE_URL}/api/providers`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${TEST_API_KEY}`
+ },
+ body: JSON.stringify({
+ providerType: 'openai-custom',
+ providerConfig: {
+ customName: maliciousName,
+ OPENAI_CUSTOM_BASE_URL: 'https://api.example.com',
+ OPENAI_CUSTOM_API_KEY: 'test-key'
+ }
+ })
+ });
+
+ const data = await response.json();
+ expect(data.provider.customName).not.toContain('<');
+ expect(data.provider.customName).not.toContain('>');
+ });
+
+ test('should preserve normal text', async () => {
+ const normalName = 'My Test Provider 123';
+ const response = await fetch(`${TEST_SERVER_BASE_URL}/api/providers`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${TEST_API_KEY}`
+ },
+ body: JSON.stringify({
+ providerType: 'openai-custom',
+ providerConfig: {
+ customName: normalName,
+ OPENAI_CUSTOM_BASE_URL: 'https://api.example.com',
+ OPENAI_CUSTOM_API_KEY: 'test-key'
+ }
+ })
+ });
+
+ const data = await response.json();
+ expect(data.provider.customName).toBe(normalName);
+ });
+ });
+
+ describe('Path Traversal Protection', () => {
+ test('should reject paths with ..', async () => {
+ const maliciousPath = '../../../etc/passwd';
+ const response = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${TEST_API_KEY}`
+ },
+ body: JSON.stringify({
+ SYSTEM_PROMPT_FILE_PATH: maliciousPath
+ })
+ });
+
+ expect(response.status).toBe(200);
+
+ const getResponse = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
+ headers: {
+ 'Authorization': `Bearer ${TEST_API_KEY}`
+ }
+ });
+ const config = await getResponse.json();
+ expect(config.SYSTEM_PROMPT_FILE_PATH).not.toBe(maliciousPath);
+ });
+
+ test('should accept valid paths within working directory', async () => {
+ const validPath = 'configs/my_prompt.txt';
+ const response = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${TEST_API_KEY}`
+ },
+ body: JSON.stringify({
+ SYSTEM_PROMPT_FILE_PATH: validPath
+ })
+ });
+
+ expect(response.status).toBe(200);
+
+ const getResponse = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
+ headers: {
+ 'Authorization': `Bearer ${TEST_API_KEY}`
+ }
+ });
+ const config = await getResponse.json();
+ expect(config.SYSTEM_PROMPT_FILE_PATH).toBe(validPath);
+ });
+ });
+
+ describe('Health Check Configuration', () => {
+ test('should save scheduled health check settings', async () => {
+ const response = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${TEST_API_KEY}`
+ },
+ body: JSON.stringify({
+ SCHEDULED_HEALTH_CHECK: {
+ enabled: true,
+ startupRun: true,
+ interval: 300000,
+ providerTypes: ['openai-custom']
+ }
+ })
+ });
+
+ expect(response.status).toBe(200);
+
+ const getResponse = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
+ headers: {
+ 'Authorization': `Bearer ${TEST_API_KEY}`
+ }
+ });
+ const config = await getResponse.json();
+ expect(config.SCHEDULED_HEALTH_CHECK).toBeDefined();
+ expect(config.SCHEDULED_HEALTH_CHECK.enabled).toBe(true);
+ expect(config.SCHEDULED_HEALTH_CHECK.interval).toBe(300000);
+ });
+
+ test('should enforce minimum interval', async () => {
+ const response = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${TEST_API_KEY}`
+ },
+ body: JSON.stringify({
+ SCHEDULED_HEALTH_CHECK: {
+ enabled: true,
+ interval: 30000
+ }
+ })
+ });
+
+ expect(response.status).toBe(200);
+
+ const getResponse = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
+ headers: {
+ 'Authorization': `Bearer ${TEST_API_KEY}`
+ }
+ });
+ const config = await getResponse.json();
+ expect(config.SCHEDULED_HEALTH_CHECK.interval).toBeGreaterThanOrEqual(60000);
+ });
+ });
+
+ describe('Configuration Validation', () => {
+ test('should reject invalid port numbers', async () => {
+ const response = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${TEST_API_KEY}`
+ },
+ body: JSON.stringify({
+ SERVER_PORT: 99999
+ })
+ });
+
+ expect(response.status).toBe(200);
+
+ const getResponse = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
+ headers: {
+ 'Authorization': `Bearer ${TEST_API_KEY}`
+ }
+ });
+ const config = await getResponse.json();
+ expect(config.SERVER_PORT).not.toBe(99999);
+ });
+
+ test('should reject excessive retry counts', async () => {
+ const response = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${TEST_API_KEY}`
+ },
+ body: JSON.stringify({
+ REQUEST_MAX_RETRIES: 999
+ })
+ });
+
+ expect(response.status).toBe(200);
+
+ const getResponse = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
+ headers: {
+ 'Authorization': `Bearer ${TEST_API_KEY}`
+ }
+ });
+ const config = await getResponse.json();
+ expect(config.REQUEST_MAX_RETRIES).toBeLessThanOrEqual(100);
+ });
+
+ test('should mask API key in response', async () => {
+ const response = await fetch(`${TEST_SERVER_BASE_URL}/api/config`, {
+ headers: {
+ 'Authorization': `Bearer ${TEST_API_KEY}`
+ }
+ });
+ const config = await response.json();
+
+ if (config.REQUIRED_API_KEY) {
+ expect(config.REQUIRED_API_KEY).toContain('*');
+ }
+ });
+ });
+});
diff --git a/tests/security-fixes.unit.test.js b/tests/security-fixes.unit.test.js
new file mode 100644
index 0000000..d648710
--- /dev/null
+++ b/tests/security-fixes.unit.test.js
@@ -0,0 +1,205 @@
+/**
+ * Unit Tests for Security Fixes
+ *
+ * 这些是不需要运行服务器的纯单元测试
+ * 可以直接运行: npm test -- tests/security-fixes.unit.test.js
+ */
+
+import { describe, test, expect } from '@jest/globals';
+
+// ========== 模拟 sanitizeProviderData 函数 ==========
+function sanitizeProviderData(provider) {
+ if (!provider || typeof provider !== 'object') return provider;
+ const sanitized = { ...provider };
+ if (typeof sanitized.customName === 'string') {
+ let name = sanitized.customName;
+
+ // 拒绝包含危险协议
+ if (/(?:data|javascript|vbscript)\s*:/i.test(name)) {
+ sanitized.customName = '';
+ return sanitized;
+ }
+
+ // 移除所有 HTML 标签
+ name = name.replace(/<[^>]*>/g, '');
+
+ // 移除 HTML 事件处理器属性
+ name = name.replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]*)/gi, '');
+
+ // 移除潜在的 HTML 实体编码攻击
+ name = name.replace(/&[#\w]+;/g, '');
+
+ sanitized.customName = name.trim();
+ }
+ return sanitized;
+}
+
+// ========== 模拟 withTimeout 函数 ==========
+function withTimeout(promise, ms = 30000) {
+ return Promise.race([
+ promise,
+ new Promise((_, reject) =>
+ setTimeout(() => reject(new Error(`Operation timeout after ${ms}ms`)), ms)
+ )
+ ]);
+}
+
+// ========== 模拟路径验证逻辑 ==========
+import path from 'path';
+
+function validatePath(inputPath, cwd) {
+ const resolved = path.resolve(cwd, inputPath);
+ const relativePath = path.relative(cwd, resolved);
+ const isInsideCwd = !path.isAbsolute(relativePath) && !relativePath.startsWith('..') && relativePath !== '..';
+
+ const isWindows = process.platform === 'win32';
+ const normalizedResolved = (isWindows ? resolved.toLowerCase() : resolved).replace(/\\/g, '/');
+ const normalizedCwd = (isWindows ? cwd.toLowerCase() : cwd).replace(/\\/g, '/');
+ const startsWithCwd = normalizedResolved.startsWith(normalizedCwd + '/') || normalizedResolved === normalizedCwd;
+
+ return isInsideCwd && startsWithCwd;
+}
+
+describe('Unit Tests - sanitizeProviderData', () => {
+ test('should remove script tags', () => {
+ const input = { customName: 'TestProvider' };
+ const result = sanitizeProviderData(input);
+ expect(result.customName).not.toContain('');
+ expect(result.customName).toContain('TestProvider');
+ });
+
+ test('should reject javascript: protocol', () => {
+ const input = { customName: 'javascript:alert("XSS")' };
+ const result = sanitizeProviderData(input);
+ expect(result.customName).toBe('');
+ });
+
+ test('should reject data: protocol', () => {
+ const input = { customName: 'data:text/html,' };
+ const result = sanitizeProviderData(input);
+ expect(result.customName).toBe('');
+ });
+
+ test('should reject vbscript: protocol', () => {
+ const input = { customName: 'vbscript:msgbox("XSS")' };
+ const result = sanitizeProviderData(input);
+ expect(result.customName).toBe('');
+ });
+
+ test('should remove all HTML tags', () => {
+ const input = { customName: '