AIClient-2-API/tests/security-fixes.unit.test.js
Wenaixi af3915311d fix: 修复代码审查发现的10个安全与正确性问题
- fix: provider-pool-manager: 移除 if(true) 占位符,改为读取凭据文件真实过期时间
- fix: provider-pool-manager: Math.min 展开大数组改为 reduce,防止栈溢出
- fix: provider-pool-manager: forceRefreshToken 调用前检查方法是否实现,不存在则 fallback
- fix: provider-api: handleAddProvider 默认路径统一为 configs/provider_pools.json
- fix: config-api: handleGetConfig 改为白名单字段过滤,REQUIRED_API_KEY 脱敏返回
- fix: api-server: 启动日志中 API Key 遮码处理
- fix: utils: generateUUID 改用 crypto.randomUUID() 替代 Math.random()
- fix: config-manager: renderProviderTags innerHTML 加 escHtml 防 XSS 注入
- fix: config-manager: PROVIDER_POOLS_FILE_PATH 未定义时加 || '' 兜底
- fix: section-config.css: white 改为 var(--bg-primary, white) 支持暗黑模式
- chore: .gitignore 添加 AGENTS.md
- chore: docker-compose.yml 添加代理环境变量

diff --git a/.gitignore b/.gitignore
index f752bb4..c375cbc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,4 +15,5 @@ api-potluck-keys.json
 api-potluck-data.json
 # Codex credentials
 configs/codex/
+AGENTS.md

diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 6977d13..8c08ef3 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -14,6 +14,12 @@ services:
       - ./configs:/app/configs
     environment:
       - ARGS=
+      - HTTP_PROXY=http://host.docker.internal:10801
+      - http_proxy=http://host.docker.internal:10801
+      - HTTPS_PROXY=http://host.docker.internal:10801
+      - https_proxy=http://host.docker.internal:10801
+      - NO_PROXY=localhost,127.0.0.1,host.docker.internal
+      - no_proxy=localhost,127.0.0.1,host.docker.internal
     healthcheck:
       test: ["CMD", "node", "healthcheck.js"]
       interval: 30s
diff --git a/src/providers/provider-pool-manager.js b/src/providers/provider-pool-manager.js
index f039e48..5875f1f 100644
--- a/src/providers/provider-pool-manager.js
+++ b/src/providers/provider-pool-manager.js
@@ -112,7 +112,12 @@ export class ProviderPoolManager {

                 if (configPath && fs.existsSync(configPath)) {
                     try {
-                        if (true) {
+                        const fileContent = fs.readFileSync(configPath, 'utf-8');
+                        const credData = JSON.parse(fileContent);
+                        const expiryTime = credData.expiry_date || credData.expiry || credData.expires_at;
+                        const nearExpiryMs = (currentConfig?.CRON_NEAR_MINUTES || 10) * 60 * 1000;
+                        const isNearExpiry = expiryTime && (expiryTime - Date.now()) < nearExpiryMs;
+                        if (isNearExpiry) {
                             this._log('warn', `Node ${providerStatus.uuid} (${providerType}) is near expiration. Enqueuing refresh...`);
                             this._enqueueRefresh(providerType, providerStatus);
                         }
@@ -389,7 +394,16 @@ export class ProviderPoolManager {
             // 调用适配器的 refreshToken 方法(内部封装了具体的刷新逻辑)
             if (typeof serviceAdapter.refreshToken === 'function') {
                 const startTime = Date.now();
-                force ? await serviceAdapter.forceRefreshToken() : await serviceAdapter.refreshToken()
+                if (force) {
+                    if (typeof serviceAdapter.forceRefreshToken === 'function') {
+                        await serviceAdapter.forceRefreshToken();
+                    } else {
+                        this._log('warn', `forceRefreshToken not implemented for ${providerType}, falling back to refreshToken`);
+                        await serviceAdapter.refreshToken();
+                    }
+                } else {
+                    await serviceAdapter.refreshToken();
+                }
                 const duration = Date.now() - startTime;
                 this._log('info', `Token refresh successful for node ${providerStatus.uuid} (Duration: ${duration}ms)`);

@@ -452,7 +466,7 @@ export class ProviderPoolManager {
         const lastSelectionSeq = config._lastSelectionSeq || 0;
         if (minSeqInPool === -1) {
             const pool = this.providerStatus[providerStatus.type] || [];
-            minSeqInPool = Math.min(...pool.map(p => p.config._lastSelectionSeq || 0));
+            minSeqInPool = pool.reduce((min, p) => Math.min(min, p.config._lastSelectionSeq || 0), Infinity);
         }
         const relativeSeq = Math.max(0, lastSelectionSeq - minSeqInPool);
         const cappedRelativeSeq = Math.min(relativeSeq, 100);
@@ -1819,14 +1833,14 @@ export class ProviderPoolManager {
                 continue;
             }

-            const checkStartTime = Date.now();
+            const providerCheckStart = Date.now();
             const checkModelName = provider.config.checkModelName || ProviderPoolManager.DEFAULT_HEALTH_CHECK_MODELS[providerType] || 'unknown';
             const displayName = customName || uuid.substring(0, 8);
-
+
             try {
                 // Perform health check (health check is based on providerTypes configuration, not per-provider checkHealth flag)
                 const result = await this._checkProviderHealth(providerType, provider.config);
-                const checkDuration = Date.now() - checkStartTime;
+                const checkDuration = Date.now() - providerCheckStart;

                 if (!result.success) {
                     // Provider is unhealthy
@@ -1840,7 +1854,7 @@ export class ProviderPoolManager {
                     this.markProviderHealthy(providerType, provider.config, false, result.modelName);
                 }
             } catch (error) {
-                const checkDuration = Date.now() - checkStartTime;
+                const checkDuration = Date.now() - providerCheckStart;
                 failCount++;
                 this._log('error', `[ScheduledHealthCheck] ${displayName} (${providerType}) EXCEPTION: ${error.message} (${checkDuration}ms)`);
                 this.markProviderUnhealthyImmediately(providerType, provider.config, error.message);
diff --git a/src/services/api-server.js b/src/services/api-server.js
index ad9bdc9..c1a850f 100644
--- a/src/services/api-server.js
+++ b/src/services/api-server.js
@@ -313,7 +313,7 @@ async function startServer() {
         logger.info(`  System Prompt Mode: ${CONFIG.SYSTEM_PROMPT_MODE}`);
         logger.info(`  Host: ${CONFIG.HOST}`);
         logger.info(`  Port: ${CONFIG.SERVER_PORT}`);
-        logger.info(`  Required API Key: ${CONFIG.REQUIRED_API_KEY}`);
+        logger.info(`  Required API Key: ${CONFIG.REQUIRED_API_KEY ? CONFIG.REQUIRED_API_KEY.slice(0, 4) + '****' : '(none)'}`);
         logger.info(`  Prompt Logging: ${CONFIG.PROMPT_LOG_MODE}${CONFIG.PROMPT_LOG_FILENAME ? ` (to ${CONFIG.PROMPT_LOG_FILENAME})` : ''}`);
         logger.info(`------------------------------------------`);
         logger.info(`\nUnified API Server running on http://${CONFIG.HOST}:${CONFIG.SERVER_PORT}`);
@@ -355,14 +355,14 @@ async function startServer() {
             setInterval(heartbeatAndRefreshToken, CONFIG.CRON_NEAR_MINUTES * 60 * 1000);
         }
         // 服务器完全启动后,执行初始健康检查
-         const poolManager = getProviderPoolManager();
-         if (poolManager) {
-             logger.info('[Initialization] Performing initial health checks for provider pools...');
-             poolManager.performHealthChecks();
-         }
+        const poolManager = getProviderPoolManager();
+        if (poolManager) {
+            logger.info('[Initialization] Performing initial health checks for provider pools...');
+            poolManager.performHealthChecks();
+        }

         // 定时健康检查
-         const scheduledConfig = CONFIG.SCHEDULED_HEALTH_CHECK;
+        const scheduledConfig = CONFIG.SCHEDULED_HEALTH_CHECK;
         if (scheduledConfig?.enabled) {
             // 设计决策:只验证最小值 60000ms,不设最大值。
             // 前端有 max=3600000 (1小时) 的 UI 限制,但后端允许更大值以支持特殊需求。
diff --git a/src/ui-modules/config-api.js b/src/ui-modules/config-api.js
index 92c4dac..f6f2b0e 100644
--- a/src/ui-modules/config-api.js
+++ b/src/ui-modules/config-api.js
@@ -57,11 +57,48 @@ export async function handleGetConfig(req, res, currentConfig) {
         }
     }

+    // 白名单过滤:只返回前端需要的字段,避免泄露凭据路径、内部状态等敏感信息
+    const safeConfig = {
+        HOST: currentConfig.HOST,
+        SERVER_PORT: currentConfig.SERVER_PORT,
+        MODEL_PROVIDER: currentConfig.MODEL_PROVIDER,
+        SYSTEM_PROMPT_FILE_PATH: currentConfig.SYSTEM_PROMPT_FILE_PATH,
+        SYSTEM_PROMPT_MODE: currentConfig.SYSTEM_PROMPT_MODE,
+        PROMPT_LOG_BASE_NAME: currentConfig.PROMPT_LOG_BASE_NAME,
+        PROMPT_LOG_MODE: currentConfig.PROMPT_LOG_MODE,
+        REQUEST_MAX_RETRIES: currentConfig.REQUEST_MAX_RETRIES,
+        REQUEST_BASE_DELAY: currentConfig.REQUEST_BASE_DELAY,
+        CREDENTIAL_SWITCH_MAX_RETRIES: currentConfig.CREDENTIAL_SWITCH_MAX_RETRIES,
+        CRON_NEAR_MINUTES: currentConfig.CRON_NEAR_MINUTES,
+        CRON_REFRESH_TOKEN: currentConfig.CRON_REFRESH_TOKEN,
+        LOGIN_EXPIRY: currentConfig.LOGIN_EXPIRY,
+        PROVIDER_POOLS_FILE_PATH: currentConfig.PROVIDER_POOLS_FILE_PATH,
+        MAX_ERROR_COUNT: currentConfig.MAX_ERROR_COUNT,
+        WARMUP_TARGET: currentConfig.WARMUP_TARGET,
+        REFRESH_CONCURRENCY_PER_PROVIDER: currentConfig.REFRESH_CONCURRENCY_PER_PROVIDER,
+        providerFallbackChain: currentConfig.providerFallbackChain,
+        modelFallbackMapping: currentConfig.modelFallbackMapping,
+        PROXY_URL: currentConfig.PROXY_URL,
+        PROXY_ENABLED_PROVIDERS: currentConfig.PROXY_ENABLED_PROVIDERS,
+        TLS_SIDECAR_ENABLED: currentConfig.TLS_SIDECAR_ENABLED,
+        TLS_SIDECAR_ENABLED_PROVIDERS: currentConfig.TLS_SIDECAR_ENABLED_PROVIDERS,
+        TLS_SIDECAR_PORT: currentConfig.TLS_SIDECAR_PORT,
+        TLS_SIDECAR_PROXY_URL: currentConfig.TLS_SIDECAR_PROXY_URL,
+        LOG_ENABLED: currentConfig.LOG_ENABLED,
+        LOG_OUTPUT_MODE: currentConfig.LOG_OUTPUT_MODE,
+        LOG_LEVEL: currentConfig.LOG_LEVEL,
+        LOG_DIR: currentConfig.LOG_DIR,
+        LOG_INCLUDE_REQUEST_ID: currentConfig.LOG_INCLUDE_REQUEST_ID,
+        LOG_INCLUDE_TIMESTAMP: currentConfig.LOG_INCLUDE_TIMESTAMP,
+        LOG_MAX_FILE_SIZE: currentConfig.LOG_MAX_FILE_SIZE,
+        LOG_MAX_FILES: currentConfig.LOG_MAX_FILES,
+        SCHEDULED_HEALTH_CHECK: currentConfig.SCHEDULED_HEALTH_CHECK,
+        // 脱敏:只返回是否设置了 API Key,不返回原文
+        REQUIRED_API_KEY: currentConfig.REQUIRED_API_KEY ? '******' : '',
+        systemPrompt,
+    };
     res.writeHead(200, { 'Content-Type': 'application/json' });
-    res.end(JSON.stringify({
-        ...currentConfig,
-        systemPrompt
-    }));
+    res.end(JSON.stringify(safeConfig));
     return true;
 }

@@ -129,8 +166,10 @@ export async function handleUpdateConfig(req, res, currentConfig) {
                  providerTypes: Array.isArray(incoming?.providerTypes) ? incoming.providerTypes : []
              };

-             // 如果定时器已存在且 enabled,重新加载 timer(interval 变化时)
-             if (globalThis.reloadHealthCheckTimer && currentConfig.SCHEDULED_HEALTH_CHECK.enabled) {
+             // 如果定时器已存在且 enabled,仅在 interval 实际变化时重新加载 timer
+             const previousInterval = currentConfig.SCHEDULED_HEALTH_CHECK._activeInterval;
+             if (globalThis.reloadHealthCheckTimer && currentConfig.SCHEDULED_HEALTH_CHECK.enabled && newInterval !== previousInterval) {
+                 currentConfig.SCHEDULED_HEALTH_CHECK._activeInterval = newInterval;
                  globalThis.reloadHealthCheckTimer(newInterval);
              }
         }
diff --git a/src/ui-modules/provider-api.js b/src/ui-modules/provider-api.js
index 2789de3..a2a02fc 100644
--- a/src/ui-modules/provider-api.js
+++ b/src/ui-modules/provider-api.js
@@ -115,7 +115,7 @@ export async function handleAddProvider(req, res, currentConfig, providerPoolMan
         providerConfig.errorCount = providerConfig.errorCount || 0;
         providerConfig.lastErrorTime = providerConfig.lastErrorTime || null;

-        const filePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'provider_pools.json';
+        const filePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json';
         let providerPools = {};

         // Load existing pools
diff --git a/src/utils/provider-utils.js b/src/utils/provider-utils.js
index 905b8e8..d7e9134 100644
--- a/src/utils/provider-utils.js
+++ b/src/utils/provider-utils.js
@@ -97,11 +97,7 @@ export const PROVIDER_MAPPINGS = [
  * @returns {string} UUID 字符串
  */
 export function generateUUID() {
-    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
-        const r = Math.random() * 16 | 0;
-        const v = c === 'x' ? r : (r & 0x3 | 0x8);
-        return v.toString(16);
-    });
+    return crypto.randomUUID();
 }

 /**
diff --git a/static/app/config-manager.js b/static/app/config-manager.js
index 130bfdc..36ac1cb 100644
--- a/static/app/config-manager.js
+++ b/static/app/config-manager.js
@@ -53,10 +53,11 @@ function renderProviderTags(container, configs, isRequired) {
     // 过滤掉不可见的提供商
     const visibleConfigs = configs.filter(c => c.visible !== false);

+    const escHtml = s => String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
     container.innerHTML = visibleConfigs.map(c => `
-        <button type="button" class="provider-tag" data-value="${c.id}">
-            <i class="fas ${c.icon || 'fa-server'}"></i>
-            <span>${c.name}</span>
+        <button type="button" class="provider-tag" data-value="${escHtml(c.id)}">
+            <i class="fas ${escHtml(c.icon || 'fa-server')}"></i>
+            <span>${escHtml(c.name)}</span>
         </button>
     `).join('');

@@ -157,7 +158,7 @@ async function loadConfiguration() {
         if (cronNearMinutesEl) cronNearMinutesEl.value = data.CRON_NEAR_MINUTES || 1;
         if (cronRefreshTokenEl) cronRefreshTokenEl.checked = data.CRON_REFRESH_TOKEN || false;
         if (loginExpiryEl) loginExpiryEl.value = data.LOGIN_EXPIRY || 3600;
-        if (providerPoolsFilePathEl) providerPoolsFilePathEl.value = data.PROVIDER_POOLS_FILE_PATH;
+        if (providerPoolsFilePathEl) providerPoolsFilePathEl.value = data.PROVIDER_POOLS_FILE_PATH || '';
         if (maxErrorCountEl) maxErrorCountEl.value = data.MAX_ERROR_COUNT || 10;
         if (warmupTargetEl) warmupTargetEl.value = data.WARMUP_TARGET || 0;
         if (refreshConcurrencyPerProviderEl) refreshConcurrencyPerProviderEl.value = data.REFRESH_CONCURRENCY_PER_PROVIDER || 1;
@@ -248,7 +249,7 @@ async function loadConfiguration() {
         const scheduledHealthCheckIntervalEl = document.getElementById('scheduledHealthCheckInterval');

         if (data.SCHEDULED_HEALTH_CHECK) {
-            if (scheduledHealthCheckEnabledEl) scheduledHealthCheckEnabledEl.checked = data.SCHEDULED_HEALTH_CHECK.enabled !== false;
+            if (scheduledHealthCheckEnabledEl) scheduledHealthCheckEnabledEl.checked = data.SCHEDULED_HEALTH_CHECK.enabled === true;
             if (scheduledHealthCheckStartupRunEl) scheduledHealthCheckStartupRunEl.checked = data.SCHEDULED_HEALTH_CHECK.startupRun !== false;
             if (scheduledHealthCheckIntervalEl) scheduledHealthCheckIntervalEl.value = data.SCHEDULED_HEALTH_CHECK.interval || 600000;
         } else {
diff --git a/static/components/section-config.css b/static/components/section-config.css
index e16338b..aebf6c4 100644
--- a/static/components/section-config.css
+++ b/static/components/section-config.css
@@ -173,7 +173,7 @@ textarea.form-control {
     width: 18px;
     left: 3px;
     bottom: 2px;
-    background-color: white;
+    background-color: var(--bg-primary, white);
     transition: var(--transition);
     border-radius: 50%;
     box-shadow: 0 1px 3px var(--neutral-shadow-30);
2026-04-03 02:56:34 +08:00

205 lines
8 KiB
JavaScript

/**
* 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: '<script>alert("XSS")</script>TestProvider' };
const result = sanitizeProviderData(input);
expect(result.customName).not.toContain('<script>');
expect(result.customName).not.toContain('</script>');
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,<script>alert(1)</script>' };
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: '<div><img src=x><span>Test</span></div>' };
const result = sanitizeProviderData(input);
expect(result.customName).toBe('Test');
expect(result.customName).not.toContain('<');
expect(result.customName).not.toContain('>');
});
test('should remove event handlers', () => {
const input = { customName: 'Test onclick="alert(1)" Provider' };
const result = sanitizeProviderData(input);
expect(result.customName).not.toContain('onclick');
expect(result.customName).toContain('Test');
expect(result.customName).toContain('Provider');
});
test('should remove HTML entities', () => {
const input = { customName: 'Test&lt;script&gt;Provider&#39;' };
const result = sanitizeProviderData(input);
expect(result.customName).not.toContain('&lt;');
expect(result.customName).not.toContain('&gt;');
expect(result.customName).not.toContain('&#39;');
});
test('should preserve normal text', () => {
const input = { customName: 'My Test Provider 123' };
const result = sanitizeProviderData(input);
expect(result.customName).toBe('My Test Provider 123');
});
test('should handle empty string', () => {
const input = { customName: '' };
const result = sanitizeProviderData(input);
expect(result.customName).toBe('');
});
test('should handle null/undefined', () => {
expect(sanitizeProviderData(null)).toBe(null);
expect(sanitizeProviderData(undefined)).toBe(undefined);
});
test('should handle object without customName', () => {
const input = { uuid: '123', type: 'test' };
const result = sanitizeProviderData(input);
expect(result).toEqual(input);
});
test('should handle complex XSS vectors', () => {
const vectors = [
'<img src=x onerror="alert(1)">',
'<svg onload="alert(1)">',
'<iframe src="javascript:alert(1)">',
'"><script>alert(1)</script>',
];
vectors.forEach(vector => {
const result = sanitizeProviderData({ customName: vector });
expect(result.customName).not.toContain('<script>');
expect(result.customName).not.toContain('onerror');
expect(result.customName).not.toContain('onload');
expect(result.customName).not.toContain('javascript');
});
});
});
describe('Unit Tests - withTimeout', () => {
test('should complete before timeout', async () => {
const fastOperation = Promise.resolve('success');
const result = await withTimeout(fastOperation, 1000);
expect(result).toBe('success');
});
test('should reject slow operations after timeout', async () => {
const slowOperation = new Promise(resolve => setTimeout(() => resolve('done'), 2000));
await expect(withTimeout(slowOperation, 100)).rejects.toThrow('Operation timeout after 100ms');
});
test('should use default timeout of 30 seconds', async () => {
const slowOperation = new Promise(resolve => setTimeout(() => resolve('done'), 35000));
await expect(withTimeout(slowOperation)).rejects.toThrow('Operation timeout after 30000ms');
}, 35000);
test('should propagate operation errors', async () => {
const failingOperation = Promise.reject(new Error('Operation failed'));
await expect(withTimeout(failingOperation, 1000)).rejects.toThrow('Operation failed');
});
});
describe('Unit Tests - Path Validation', () => {
test('should accept paths within working directory', () => {
const cwd = process.platform === 'win32' ? 'C:\\Users\\Test\\Project' : '/home/user/project';
expect(validatePath('configs/test.txt', cwd)).toBe(true);
expect(validatePath('./configs/test.txt', cwd)).toBe(true);
expect(validatePath('test.txt', cwd)).toBe(true);
});
test('should reject paths with directory traversal', () => {
const cwd = process.platform === 'win32' ? 'C:\\Users\\Test\\Project' : '/home/user/project';
expect(validatePath('../../../etc/passwd', cwd)).toBe(false);
expect(validatePath('configs/../../etc/passwd', cwd)).toBe(false);
});
test('should reject absolute paths outside working directory', () => {
const cwd = process.platform === 'win32' ? 'C:\\Users\\Test\\Project' : '/home/user/project';
expect(validatePath('/etc/passwd', cwd)).toBe(false);
expect(validatePath('/tmp/test.txt', cwd)).toBe(false);
});
test('should handle Windows paths correctly', () => {
if (process.platform === 'win32') {
const cwd = 'C:\\Users\\Test\\Project';
expect(validatePath('configs\\test.txt', cwd)).toBe(true);
expect(validatePath('CONFIGS\\TEST.TXT', cwd)).toBe(true);
}
});
});