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:
parent
7f7016b4ad
commit
2482567ac2
|
|
@ -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)' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue