diff --git a/persona-studio/backend/routes/apikey.js b/persona-studio/backend/routes/apikey.js index d37aa650..ebfcb434 100644 --- a/persona-studio/backend/routes/apikey.js +++ b/persona-studio/backend/routes/apikey.js @@ -80,8 +80,8 @@ function fetchModels(apiBase, apiKey, timeoutMs) { }); }); - req.on('error', () => { - reject(new Error('API Base 不可访问')); + req.on('error', (err) => { + reject(new Error('API Base 不可访问: ' + err.message)); }); req.on('timeout', () => { @@ -177,7 +177,14 @@ router.post('/detect-models', async (req, res) => { }); } - // 检查缓存 + // 防止 header injection:API Key 不得包含换行符 + if (/[\r\n]/.test(api_key)) { + return res.status(400).json({ + error: true, + code: 'INVALID_API_KEY', + message: 'API Key 格式无效' + }); + } const cached = getCachedModels(api_base, api_key); if (cached) { return res.json({ @@ -229,6 +236,15 @@ router.post('/chat', async (req, res) => { }); } + // 防止 header injection + if (/[\r\n]/.test(api_key)) { + return res.status(400).json({ + error: true, + code: 'INVALID_API_KEY', + message: 'API Key 格式无效' + }); + } + if (!messages || !Array.isArray(messages) || messages.length === 0) { return res.status(400).json({ error: true, diff --git a/persona-studio/frontend/chat.js b/persona-studio/frontend/chat.js index dfa7d159..a3262bb1 100644 --- a/persona-studio/frontend/chat.js +++ b/persona-studio/frontend/chat.js @@ -105,15 +105,15 @@ async function sendMessage() { sendBtn.disabled = true; try { - var data; + let data; if (LOGIN_MODE === 'apikey') { // API Key 模式:通过后端代理调用用户的 API - var apiMessages = conversationHistory.slice(-20).map(function (msg) { + const apiMessages = conversationHistory.slice(-20).map(function (msg) { return { role: msg.role === 'assistant' ? 'assistant' : 'user', content: msg.content }; }); - var res = await fetch(API_BASE + '/api/ps/apikey/chat', { + const res = await fetch(API_BASE + '/api/ps/apikey/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -127,7 +127,7 @@ async function sendMessage() { data = await res.json(); } else { // 开发编号模式:使用原有后端接口 - var res = await fetch(API_BASE + '/api/ps/chat/message', { + const res = await fetch(API_BASE + '/api/ps/chat/message', { method: 'POST', headers: authHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify({ diff --git a/persona-studio/frontend/index.html b/persona-studio/frontend/index.html index 211d9e7d..cbdb8ec7 100644 --- a/persona-studio/frontend/index.html +++ b/persona-studio/frontend/index.html @@ -87,9 +87,9 @@ /* ---- 开发编号登录 ---- */ async function handleLogin(e) { e.preventDefault(); - var devId = document.getElementById('devIdInput').value.trim().toUpperCase(); - var errorEl = document.getElementById('errorMsg'); - var btn = document.getElementById('loginBtn'); + const devId = document.getElementById('devIdInput').value.trim().toUpperCase(); + const errorEl = document.getElementById('errorMsg'); + const btn = document.getElementById('loginBtn'); errorEl.style.display = 'none'; @@ -103,13 +103,13 @@ btn.textContent = '验证中…'; try { - var res = await fetch(API_BASE + '/api/ps/auth/login', { + const res = await fetch(API_BASE + '/api/ps/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dev_id: devId }) }); - var data = await res.json(); + const data = await res.json(); if (!res.ok || data.error) { errorEl.textContent = data.message || '登录失败,请检查编号'; @@ -138,12 +138,12 @@ /* ---- API Key 模型检测 ---- */ async function handleDetectModels() { - var apiBase = document.getElementById('apiBaseInput').value.trim(); - var apiKey = document.getElementById('apiKeyInput').value.trim(); - var errorEl = document.getElementById('errorMsg'); - var statusEl = document.getElementById('detectStatus'); - var modelContainer = document.getElementById('modelListContainer'); - var btn = document.getElementById('detectBtn'); + const apiBase = document.getElementById('apiBaseInput').value.trim(); + const apiKey = document.getElementById('apiKeyInput').value.trim(); + const errorEl = document.getElementById('errorMsg'); + const statusEl = document.getElementById('detectStatus'); + const modelContainer = document.getElementById('modelListContainer'); + const btn = document.getElementById('detectBtn'); errorEl.style.display = 'none'; modelContainer.style.display = 'none'; @@ -167,13 +167,13 @@ statusEl.style.display = 'block'; try { - var res = await fetch(API_BASE + '/api/ps/apikey/detect-models', { + const res = await fetch(API_BASE + '/api/ps/apikey/detect-models', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ api_base: apiBase, api_key: apiKey }) }); - var data = await res.json(); + const data = await res.json(); if (!res.ok || data.error) { statusEl.textContent = data.message || '未检测到模型,请检查 API Key'; @@ -183,7 +183,7 @@ return; } - var models = data.models || []; + const models = data.models || []; if (models.length === 0) { statusEl.textContent = '未检测到可用模型'; statusEl.className = 'detect-status detect-error'; diff --git a/tests/smoke/apikey-detect.test.js b/tests/smoke/apikey-detect.test.js index 43b7420d..8ba67d90 100644 --- a/tests/smoke/apikey-detect.test.js +++ b/tests/smoke/apikey-detect.test.js @@ -59,7 +59,7 @@ describe('POST /api/ps/apikey/detect-models', () => { test('returns error for unreachable API base', async () => { const res = await post('/api/ps/apikey/detect-models', { - api_base: 'http://127.0.0.1:19999', + api_base: 'http://invalid.test.local', api_key: 'sk-test-invalid' }); expect(res.status).toBe(502);