Address code review: fix SSRF protection, cache key safety, error handling

- Add protocol validation (http/https only) to prevent SSRF via file:/ftp: URLs
- Hash both apiBase+apiKey together in cache key to prevent collision attacks
- Simplify frontend error detection (use TypeError instanceof instead of fragile string check)
- Make smoke test error code assertion more precise with regex

Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-03-10 12:31:56 +00:00
parent 7f7016b4ad
commit 2482567ac2
3 changed files with 11 additions and 9 deletions

View File

@ -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' });
}

View File

@ -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';

View File

@ -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 () => {