diff --git a/backend-integration/api-proxy.js b/backend-integration/api-proxy.js index da394046..b6d77f47 100644 --- a/backend-integration/api-proxy.js +++ b/backend-integration/api-proxy.js @@ -451,8 +451,8 @@ const userModelCache = new Map(); const USER_MODEL_CACHE_TTL_MS = 60 * 60 * 1000; // 1 小时 function getUserCacheKey(apiBase, apiKey) { - const hash = crypto.createHash('sha256').update(apiKey).digest('hex').slice(0, 16); - return apiBase + '::' + hash; + const hash = crypto.createHash('sha256').update(apiBase + '\0' + apiKey).digest('hex').slice(0, 32); + return 'user-models::' + hash; } function getCachedUserModels(apiBase, apiKey) { @@ -636,9 +636,12 @@ async function handleDetectModels(req, res) { return jsonResponse(res, 400, { error: true, code: 'INVALID_API_KEY', message: 'API Key 格式无效(含非法字符)' }); } - // 校验 api_base 格式 + // 校验 api_base 格式和协议(仅允许 http/https,防止 SSRF) try { - new URL(api_base); + const parsedUrl = new URL(api_base); + if (parsedUrl.protocol !== 'https:' && parsedUrl.protocol !== 'http:') { + return jsonResponse(res, 400, { error: true, code: 'INVALID_API_BASE', message: 'API Base URL 仅支持 http/https 协议' }); + } } catch (_e) { return jsonResponse(res, 400, { error: true, code: 'INVALID_API_BASE', message: 'API Base URL 格式无效,请输入完整 URL(如 https://api.openai.com)' }); } diff --git a/persona-studio/frontend/index.html b/persona-studio/frontend/index.html index f3652962..0fe20170 100644 --- a/persona-studio/frontend/index.html +++ b/persona-studio/frontend/index.html @@ -202,14 +202,12 @@ btn.textContent = '🔍 重新检测'; } catch (fetchErr) { var errMsg = '检测失败'; - if (fetchErr instanceof TypeError && fetchErr.message.includes('fetch')) { + if (fetchErr instanceof TypeError) { errMsg = '服务器代理未部署或不可达,请联系管理员'; } else if (fetchErr.name === 'AbortError') { errMsg = '请求超时,请检查网络连接'; - } else if (fetchErr.message && fetchErr.message.includes('NetworkError')) { - errMsg = '跨域被拦截或网络不可达,请确认服务器已部署'; } else { - errMsg = '检测失败: ' + (fetchErr.message || '请检查网络连接'); + errMsg = '网络错误: ' + (fetchErr.message || '请检查网络连接'); } statusEl.textContent = errMsg; statusEl.className = 'detect-status detect-error'; diff --git a/tests/smoke/apikey-detect.test.js b/tests/smoke/apikey-detect.test.js index ac51ec8b..1de95911 100644 --- a/tests/smoke/apikey-detect.test.js +++ b/tests/smoke/apikey-detect.test.js @@ -64,7 +64,8 @@ describe('POST /api/ps/apikey/detect-models', () => { }); expect(res.status).toBe(502); expect(res.body.error).toBe(true); - expect(['DETECT_FAILED', 'DNS_ERROR', 'NETWORK_ERROR', 'TIMEOUT']).toContain(res.body.code); + // DNS resolution failure returns DNS_ERROR; other network issues may return NETWORK_ERROR + expect(res.body.code).toMatch(/^(DNS_ERROR|NETWORK_ERROR|TIMEOUT)$/); }); test('returns INVALID_API_BASE for malformed URL', async () => {