ZhaoShanGeng commited on
Commit
620492b
·
1 Parent(s): 2f3f89e

security: 修复前端 XSS 注入风险

Browse files

- 添加 escapeHtml() 和 escapeJs() 转义函数
- tokens.js: 转义所有 token 数据(refresh_token, access_token, email, projectId)
- ui.js: 转义 showToast/showConfirm/showLoading 的用户输入
- quota.js: 转义所有 API 返回的数据(modelId, email, message 等)

Files changed (4) hide show
  1. public/js/quota.js +21 -14
  2. public/js/tokens.js +40 -24
  3. public/js/ui.js +13 -5
  4. public/js/utils.js +22 -0
public/js/quota.js CHANGED
@@ -50,7 +50,7 @@ async function loadTokenQuotaSummary(refreshToken) {
50
  quotaCache.set(refreshToken, data.data);
51
  renderQuotaSummary(summaryEl, data.data);
52
  } else {
53
- const errMsg = data.message || '未知错误';
54
  summaryEl.innerHTML = `<span class="quota-summary-error">📊 ${errMsg}</span>`;
55
  }
56
  } catch (error) {
@@ -81,12 +81,13 @@ function renderQuotaSummary(summaryEl, quotaData) {
81
 
82
  const percentage = minQuota.remaining * 100;
83
  const percentageText = `${percentage.toFixed(2)}%`;
84
- const shortName = minModel.replace('models/', '').replace('publishers/google/', '').split('/').pop();
 
85
  const barColor = percentage > 50 ? '#10b981' : percentage > 20 ? '#f59e0b' : '#ef4444';
86
 
87
  summaryEl.innerHTML = `
88
  <span class="quota-summary-icon">📊</span>
89
- <span class="quota-summary-model" title="${minModel}">${shortName}</span>
90
  <span class="quota-summary-bar"><span style="width:${percentage}%;background:${barColor}"></span></span>
91
  <span class="quota-summary-pct">${percentageText}</span>
92
  `;
@@ -150,9 +151,11 @@ async function loadQuotaDetail(cardId, refreshToken) {
150
  const percentage = quota.remaining * 100;
151
  const percentageText = `${percentage.toFixed(2)}%`;
152
  const barColor = percentage > 50 ? '#10b981' : percentage > 20 ? '#f59e0b' : '#ef4444';
153
- const shortName = modelId.replace('models/', '').replace('publishers/google/', '').split('/').pop();
 
 
154
  groupHtml += `
155
- <div class="quota-detail-row" title="${modelId} - 重置: ${quota.resetTime}">
156
  <span class="quota-detail-icon">${icon}</span>
157
  <span class="quota-detail-name">${shortName}</span>
158
  <span class="quota-detail-bar"><span style="width:${percentage}%;background:${barColor}"></span></span>
@@ -167,11 +170,11 @@ async function loadQuotaDetail(cardId, refreshToken) {
167
  html += renderGroup(grouped.gemini, '💎');
168
  html += renderGroup(grouped.other, '🔧');
169
  html += '</div>';
170
- html += `<button class="btn btn-info btn-xs quota-refresh-btn" onclick="refreshInlineQuota('${cardId}', '${refreshToken}')">🔄 刷新额度</button>`;
171
 
172
  detailEl.innerHTML = html;
173
  } else {
174
- const errMsg = data.message || '未知错误';
175
  detailEl.innerHTML = `<div class="quota-error-small">加载失败: ${errMsg}</div>`;
176
  }
177
  } catch (error) {
@@ -213,7 +216,9 @@ async function showQuotaModal(refreshToken) {
213
  const email = t.email || '未知';
214
  const shortEmail = email.length > 20 ? email.substring(0, 17) + '...' : email;
215
  const isActive = index === activeIndex;
216
- return `<button type="button" class="quota-tab${isActive ? ' active' : ''}" data-index="${index}" onclick="switchQuotaAccountByIndex(${index})" title="${email}">${shortEmail}</button>`;
 
 
217
  }).join('');
218
 
219
  const modal = document.createElement('div');
@@ -315,11 +320,11 @@ async function loadQuotaData(refreshToken, forceRefresh = false) {
315
  quotaCache.set(refreshToken, data.data);
316
  renderQuotaModal(quotaContent, data.data);
317
  } else {
318
- quotaContent.innerHTML = `<div class="quota-error">加载失败: ${data.message}</div>`;
319
  }
320
  } catch (error) {
321
  if (quotaContent) {
322
- quotaContent.innerHTML = `<div class="quota-error">加载失败: ${error.message}</div>`;
323
  }
324
  } finally {
325
  if (refreshBtn) {
@@ -363,20 +368,22 @@ function renderQuotaModal(quotaContent, quotaData) {
363
 
364
  const renderGroup = (items, title) => {
365
  if (items.length === 0) return '';
366
- let groupHtml = `<div class="quota-group-title">${title}</div><div class="quota-grid">`;
367
  items.forEach(({ modelId, quota }) => {
368
  const percentage = quota.remaining * 100;
369
  const percentageText = `${percentage.toFixed(2)}%`;
370
  const barColor = percentage > 50 ? '#10b981' : percentage > 20 ? '#f59e0b' : '#ef4444';
371
- const shortName = modelId.replace('models/', '').replace('publishers/google/', '');
 
 
372
  groupHtml += `
373
  <div class="quota-item">
374
- <div class="quota-model-name" title="${modelId}">${shortName}</div>
375
  <div class="quota-bar-container">
376
  <div class="quota-bar" style="width: ${percentage}%; background: ${barColor};"></div>
377
  </div>
378
  <div class="quota-info-row">
379
- <span class="quota-reset">重置: ${quota.resetTime}</span>
380
  <span class="quota-percentage">${percentageText}</span>
381
  </div>
382
  </div>
 
50
  quotaCache.set(refreshToken, data.data);
51
  renderQuotaSummary(summaryEl, data.data);
52
  } else {
53
+ const errMsg = escapeHtml(data.message || '未知错误');
54
  summaryEl.innerHTML = `<span class="quota-summary-error">📊 ${errMsg}</span>`;
55
  }
56
  } catch (error) {
 
81
 
82
  const percentage = minQuota.remaining * 100;
83
  const percentageText = `${percentage.toFixed(2)}%`;
84
+ const shortName = escapeHtml(minModel.replace('models/', '').replace('publishers/google/', '').split('/').pop());
85
+ const safeMinModel = escapeHtml(minModel);
86
  const barColor = percentage > 50 ? '#10b981' : percentage > 20 ? '#f59e0b' : '#ef4444';
87
 
88
  summaryEl.innerHTML = `
89
  <span class="quota-summary-icon">📊</span>
90
+ <span class="quota-summary-model" title="${safeMinModel}">${shortName}</span>
91
  <span class="quota-summary-bar"><span style="width:${percentage}%;background:${barColor}"></span></span>
92
  <span class="quota-summary-pct">${percentageText}</span>
93
  `;
 
151
  const percentage = quota.remaining * 100;
152
  const percentageText = `${percentage.toFixed(2)}%`;
153
  const barColor = percentage > 50 ? '#10b981' : percentage > 20 ? '#f59e0b' : '#ef4444';
154
+ const shortName = escapeHtml(modelId.replace('models/', '').replace('publishers/google/', '').split('/').pop());
155
+ const safeModelId = escapeHtml(modelId);
156
+ const safeResetTime = escapeHtml(quota.resetTime);
157
  groupHtml += `
158
+ <div class="quota-detail-row" title="${safeModelId} - 重置: ${safeResetTime}">
159
  <span class="quota-detail-icon">${icon}</span>
160
  <span class="quota-detail-name">${shortName}</span>
161
  <span class="quota-detail-bar"><span style="width:${percentage}%;background:${barColor}"></span></span>
 
170
  html += renderGroup(grouped.gemini, '💎');
171
  html += renderGroup(grouped.other, '🔧');
172
  html += '</div>';
173
+ html += `<button class="btn btn-info btn-xs quota-refresh-btn" onclick="refreshInlineQuota('${escapeJs(cardId)}', '${escapeJs(refreshToken)}')">🔄 刷新额度</button>`;
174
 
175
  detailEl.innerHTML = html;
176
  } else {
177
+ const errMsg = escapeHtml(data.message || '未知错误');
178
  detailEl.innerHTML = `<div class="quota-error-small">加载失败: ${errMsg}</div>`;
179
  }
180
  } catch (error) {
 
216
  const email = t.email || '未知';
217
  const shortEmail = email.length > 20 ? email.substring(0, 17) + '...' : email;
218
  const isActive = index === activeIndex;
219
+ const safeEmail = escapeHtml(email);
220
+ const safeShortEmail = escapeHtml(shortEmail);
221
+ return `<button type="button" class="quota-tab${isActive ? ' active' : ''}" data-index="${index}" onclick="switchQuotaAccountByIndex(${index})" title="${safeEmail}">${safeShortEmail}</button>`;
222
  }).join('');
223
 
224
  const modal = document.createElement('div');
 
320
  quotaCache.set(refreshToken, data.data);
321
  renderQuotaModal(quotaContent, data.data);
322
  } else {
323
+ quotaContent.innerHTML = `<div class="quota-error">加载失败: ${escapeHtml(data.message)}</div>`;
324
  }
325
  } catch (error) {
326
  if (quotaContent) {
327
+ quotaContent.innerHTML = `<div class="quota-error">加载失败: ${escapeHtml(error.message)}</div>`;
328
  }
329
  } finally {
330
  if (refreshBtn) {
 
368
 
369
  const renderGroup = (items, title) => {
370
  if (items.length === 0) return '';
371
+ let groupHtml = `<div class="quota-group-title">${escapeHtml(title)}</div><div class="quota-grid">`;
372
  items.forEach(({ modelId, quota }) => {
373
  const percentage = quota.remaining * 100;
374
  const percentageText = `${percentage.toFixed(2)}%`;
375
  const barColor = percentage > 50 ? '#10b981' : percentage > 20 ? '#f59e0b' : '#ef4444';
376
+ const shortName = escapeHtml(modelId.replace('models/', '').replace('publishers/google/', ''));
377
+ const safeModelId = escapeHtml(modelId);
378
+ const safeResetTime = escapeHtml(quota.resetTime);
379
  groupHtml += `
380
  <div class="quota-item">
381
+ <div class="quota-model-name" title="${safeModelId}">${shortName}</div>
382
  <div class="quota-bar-container">
383
  <div class="quota-bar" style="width: ${percentage}%; background: ${barColor};"></div>
384
  </div>
385
  <div class="quota-info-row">
386
+ <span class="quota-reset">重置: ${safeResetTime}</span>
387
  <span class="quota-percentage">${percentageText}</span>
388
  </div>
389
  </div>
public/js/tokens.js CHANGED
@@ -56,50 +56,58 @@ function renderTokens(tokens) {
56
  expiredTokensToRefresh.push(token.refresh_token);
57
  }
58
 
 
 
 
 
 
 
 
 
59
  return `
60
- <div class="token-card ${!token.enable ? 'disabled' : ''} ${isExpired ? 'expired' : ''} ${isRefreshing ? 'refreshing' : ''}" id="card-${cardId}">
61
  <div class="token-header">
62
  <span class="status ${token.enable ? 'enabled' : 'disabled'}">
63
  ${token.enable ? '✅ 启用' : '❌ 禁用'}
64
  </span>
65
  <div class="token-header-right">
66
- <button class="btn-icon" onclick="showTokenDetail('${token.refresh_token}')" title="编辑全部">✏️</button>
67
- <span class="token-id">#${token.refresh_token.substring(0, 6)}</span>
68
  </div>
69
  </div>
70
  <div class="token-info">
71
  <div class="info-row">
72
  <span class="info-label">🎫</span>
73
- <span class="info-value sensitive-info" title="${token.access_token_suffix}">${token.access_token_suffix}</span>
74
  </div>
75
- <div class="info-row editable" onclick="editField(event, '${token.refresh_token}', 'projectId', '${(token.projectId || '').replace(/'/g, "\\'")}')" title="点击编辑">
76
  <span class="info-label">📦</span>
77
- <span class="info-value sensitive-info">${token.projectId || '点击设置'}</span>
78
  <span class="info-edit-icon">✏️</span>
79
  </div>
80
- <div class="info-row editable" onclick="editField(event, '${token.refresh_token}', 'email', '${(token.email || '').replace(/'/g, "\\'")}')" title="点击编辑">
81
  <span class="info-label">📧</span>
82
- <span class="info-value sensitive-info">${token.email || '点击设置'}</span>
83
  <span class="info-edit-icon">✏️</span>
84
  </div>
85
- <div class="info-row ${isExpired ? 'expired-text' : ''}" id="expire-row-${cardId}">
86
  <span class="info-label">⏰</span>
87
- <span class="info-value">${isRefreshing ? '🔄 刷新中...' : expireStr}${isExpired && !isRefreshing ? ' (已过期)' : ''}</span>
88
  </div>
89
  </div>
90
- <div class="token-quota-inline" id="quota-inline-${cardId}">
91
- <div class="quota-inline-header" onclick="toggleQuotaExpand('${cardId}', '${token.refresh_token}')">
92
- <span class="quota-inline-summary" id="quota-summary-${cardId}">📊 加载中...</span>
93
- <span class="quota-inline-toggle" id="quota-toggle-${cardId}">▼</span>
94
  </div>
95
- <div class="quota-inline-detail hidden" id="quota-detail-${cardId}"></div>
96
  </div>
97
  <div class="token-actions">
98
- <button class="btn btn-info btn-xs" onclick="showQuotaModal('${token.refresh_token}')" title="查看额度">📊 详情</button>
99
- <button class="btn ${token.enable ? 'btn-warning' : 'btn-success'} btn-xs" onclick="toggleToken('${token.refresh_token}', ${!token.enable})" title="${token.enable ? '禁用' : '启用'}">
100
  ${token.enable ? '⏸️ 禁用' : '▶️ 启用'}
101
  </button>
102
- <button class="btn btn-danger btn-xs" onclick="deleteToken('${token.refresh_token}')" title="删除">🗑️ 删除</button>
103
  </div>
104
  </div>
105
  `}).join('');
@@ -309,6 +317,14 @@ function showTokenDetail(refreshToken) {
309
  return;
310
  }
311
 
 
 
 
 
 
 
 
 
312
  const modal = document.createElement('div');
313
  modal.className = 'modal form-modal';
314
  modal.innerHTML = `
@@ -316,27 +332,27 @@ function showTokenDetail(refreshToken) {
316
  <div class="modal-title">📝 Token详情</div>
317
  <div class="form-group compact">
318
  <label>🎫 Access Token (只读)</label>
319
- <div class="token-display">${token.access_token || ''}</div>
320
  </div>
321
  <div class="form-group compact">
322
  <label>🔄 Refresh Token (只读)</label>
323
- <div class="token-display">${token.refresh_token}</div>
324
  </div>
325
  <div class="form-group compact">
326
  <label>📦 Project ID</label>
327
- <input type="text" id="editProjectId" value="${token.projectId || ''}" placeholder="项目ID">
328
  </div>
329
  <div class="form-group compact">
330
  <label>📧 邮箱</label>
331
- <input type="email" id="editEmail" value="${token.email || ''}" placeholder="账号邮箱">
332
  </div>
333
  <div class="form-group compact">
334
  <label>⏰ 过期时间</label>
335
- <input type="text" value="${new Date(token.timestamp + token.expires_in * 1000).toLocaleString('zh-CN')}" readonly style="background: var(--bg); cursor: not-allowed;">
336
  </div>
337
  <div class="modal-actions">
338
  <button class="btn btn-secondary" onclick="this.closest('.modal').remove()">取消</button>
339
- <button class="btn btn-success" onclick="saveTokenDetail('${refreshToken}')">💾 保存</button>
340
  </div>
341
  </div>
342
  `;
 
56
  expiredTokensToRefresh.push(token.refresh_token);
57
  }
58
 
59
+ // 转义所有用户数据防止 XSS
60
+ const safeRefreshToken = escapeJs(token.refresh_token);
61
+ const safeAccessTokenSuffix = escapeHtml(token.access_token_suffix || '');
62
+ const safeProjectId = escapeHtml(token.projectId || '');
63
+ const safeEmail = escapeHtml(token.email || '');
64
+ const safeProjectIdJs = escapeJs(token.projectId || '');
65
+ const safeEmailJs = escapeJs(token.email || '');
66
+
67
  return `
68
+ <div class="token-card ${!token.enable ? 'disabled' : ''} ${isExpired ? 'expired' : ''} ${isRefreshing ? 'refreshing' : ''}" id="card-${escapeHtml(cardId)}">
69
  <div class="token-header">
70
  <span class="status ${token.enable ? 'enabled' : 'disabled'}">
71
  ${token.enable ? '✅ 启用' : '❌ 禁用'}
72
  </span>
73
  <div class="token-header-right">
74
+ <button class="btn-icon" onclick="showTokenDetail('${safeRefreshToken}')" title="编辑全部">✏️</button>
75
+ <span class="token-id">#${escapeHtml(token.refresh_token.substring(0, 6))}</span>
76
  </div>
77
  </div>
78
  <div class="token-info">
79
  <div class="info-row">
80
  <span class="info-label">🎫</span>
81
+ <span class="info-value sensitive-info" title="${safeAccessTokenSuffix}">${safeAccessTokenSuffix}</span>
82
  </div>
83
+ <div class="info-row editable" onclick="editField(event, '${safeRefreshToken}', 'projectId', '${safeProjectIdJs}')" title="点击编辑">
84
  <span class="info-label">📦</span>
85
+ <span class="info-value sensitive-info">${safeProjectId || '点击设置'}</span>
86
  <span class="info-edit-icon">✏️</span>
87
  </div>
88
+ <div class="info-row editable" onclick="editField(event, '${safeRefreshToken}', 'email', '${safeEmailJs}')" title="点击编辑">
89
  <span class="info-label">📧</span>
90
+ <span class="info-value sensitive-info">${safeEmail || '点击设置'}</span>
91
  <span class="info-edit-icon">✏️</span>
92
  </div>
93
+ <div class="info-row ${isExpired ? 'expired-text' : ''}" id="expire-row-${escapeHtml(cardId)}">
94
  <span class="info-label">⏰</span>
95
+ <span class="info-value">${isRefreshing ? '🔄 刷新中...' : escapeHtml(expireStr)}${isExpired && !isRefreshing ? ' (已过期)' : ''}</span>
96
  </div>
97
  </div>
98
+ <div class="token-quota-inline" id="quota-inline-${escapeHtml(cardId)}">
99
+ <div class="quota-inline-header" onclick="toggleQuotaExpand('${escapeJs(cardId)}', '${safeRefreshToken}')">
100
+ <span class="quota-inline-summary" id="quota-summary-${escapeHtml(cardId)}">📊 加载中...</span>
101
+ <span class="quota-inline-toggle" id="quota-toggle-${escapeHtml(cardId)}">▼</span>
102
  </div>
103
+ <div class="quota-inline-detail hidden" id="quota-detail-${escapeHtml(cardId)}"></div>
104
  </div>
105
  <div class="token-actions">
106
+ <button class="btn btn-info btn-xs" onclick="showQuotaModal('${safeRefreshToken}')" title="查看额度">📊 详情</button>
107
+ <button class="btn ${token.enable ? 'btn-warning' : 'btn-success'} btn-xs" onclick="toggleToken('${safeRefreshToken}', ${!token.enable})" title="${token.enable ? '禁用' : '启用'}">
108
  ${token.enable ? '⏸️ 禁用' : '▶️ 启用'}
109
  </button>
110
+ <button class="btn btn-danger btn-xs" onclick="deleteToken('${safeRefreshToken}')" title="删除">🗑️ 删除</button>
111
  </div>
112
  </div>
113
  `}).join('');
 
317
  return;
318
  }
319
 
320
+ // 转义所有用户数据防止 XSS
321
+ const safeAccessToken = escapeHtml(token.access_token || '');
322
+ const safeRefreshToken = escapeHtml(token.refresh_token);
323
+ const safeRefreshTokenJs = escapeJs(refreshToken);
324
+ const safeProjectId = escapeHtml(token.projectId || '');
325
+ const safeEmail = escapeHtml(token.email || '');
326
+ const expireTimeStr = escapeHtml(new Date(token.timestamp + token.expires_in * 1000).toLocaleString('zh-CN'));
327
+
328
  const modal = document.createElement('div');
329
  modal.className = 'modal form-modal';
330
  modal.innerHTML = `
 
332
  <div class="modal-title">📝 Token详情</div>
333
  <div class="form-group compact">
334
  <label>🎫 Access Token (只读)</label>
335
+ <div class="token-display">${safeAccessToken}</div>
336
  </div>
337
  <div class="form-group compact">
338
  <label>🔄 Refresh Token (只读)</label>
339
+ <div class="token-display">${safeRefreshToken}</div>
340
  </div>
341
  <div class="form-group compact">
342
  <label>📦 Project ID</label>
343
+ <input type="text" id="editProjectId" value="${safeProjectId}" placeholder="项目ID">
344
  </div>
345
  <div class="form-group compact">
346
  <label>📧 邮箱</label>
347
+ <input type="email" id="editEmail" value="${safeEmail}" placeholder="账号邮箱">
348
  </div>
349
  <div class="form-group compact">
350
  <label>⏰ 过期时间</label>
351
+ <input type="text" value="${expireTimeStr}" readonly style="background: var(--bg); cursor: not-allowed;">
352
  </div>
353
  <div class="modal-actions">
354
  <button class="btn btn-secondary" onclick="this.closest('.modal').remove()">取消</button>
355
+ <button class="btn btn-success" onclick="saveTokenDetail('${safeRefreshTokenJs}')">💾 保存</button>
356
  </div>
357
  </div>
358
  `;
public/js/ui.js CHANGED
@@ -5,11 +5,14 @@ function showToast(message, type = 'info', title = '') {
5
  const titles = { success: '成功', error: '错误', warning: '警告', info: '提示' };
6
  const toast = document.createElement('div');
7
  toast.className = `toast ${type}`;
 
 
 
8
  toast.innerHTML = `
9
  <div class="toast-icon">${icons[type]}</div>
10
  <div class="toast-content">
11
- <div class="toast-title">${title || titles[type]}</div>
12
- <div class="toast-message">${message}</div>
13
  </div>
14
  `;
15
  document.body.appendChild(toast);
@@ -23,10 +26,13 @@ function showConfirm(message, title = '确认操作') {
23
  return new Promise((resolve) => {
24
  const modal = document.createElement('div');
25
  modal.className = 'modal';
 
 
 
26
  modal.innerHTML = `
27
  <div class="modal-content">
28
- <div class="modal-title">${title}</div>
29
- <div class="modal-message">${message}</div>
30
  <div class="modal-actions">
31
  <button class="btn btn-secondary" onclick="this.closest('.modal').remove(); window.modalResolve(false)">取消</button>
32
  <button class="btn btn-danger" onclick="this.closest('.modal').remove(); window.modalResolve(true)">确定</button>
@@ -43,7 +49,9 @@ function showLoading(text = '处理中...') {
43
  const overlay = document.createElement('div');
44
  overlay.className = 'loading-overlay';
45
  overlay.id = 'loadingOverlay';
46
- overlay.innerHTML = `<div class="spinner"></div><div class="loading-text">${text}</div>`;
 
 
47
  document.body.appendChild(overlay);
48
  }
49
 
 
5
  const titles = { success: '成功', error: '错误', warning: '警告', info: '提示' };
6
  const toast = document.createElement('div');
7
  toast.className = `toast ${type}`;
8
+ // 转义用户输入防止 XSS
9
+ const safeTitle = escapeHtml(title || titles[type]);
10
+ const safeMessage = escapeHtml(message);
11
  toast.innerHTML = `
12
  <div class="toast-icon">${icons[type]}</div>
13
  <div class="toast-content">
14
+ <div class="toast-title">${safeTitle}</div>
15
+ <div class="toast-message">${safeMessage}</div>
16
  </div>
17
  `;
18
  document.body.appendChild(toast);
 
26
  return new Promise((resolve) => {
27
  const modal = document.createElement('div');
28
  modal.className = 'modal';
29
+ // 转义用户输入防止 XSS
30
+ const safeTitle = escapeHtml(title);
31
+ const safeMessage = escapeHtml(message);
32
  modal.innerHTML = `
33
  <div class="modal-content">
34
+ <div class="modal-title">${safeTitle}</div>
35
+ <div class="modal-message">${safeMessage}</div>
36
  <div class="modal-actions">
37
  <button class="btn btn-secondary" onclick="this.closest('.modal').remove(); window.modalResolve(false)">取消</button>
38
  <button class="btn btn-danger" onclick="this.closest('.modal').remove(); window.modalResolve(true)">确定</button>
 
49
  const overlay = document.createElement('div');
50
  overlay.className = 'loading-overlay';
51
  overlay.id = 'loadingOverlay';
52
+ // 转义用户输入防止 XSS
53
+ const safeText = escapeHtml(text);
54
+ overlay.innerHTML = `<div class="spinner"></div><div class="loading-text">${safeText}</div>`;
55
  document.body.appendChild(overlay);
56
  }
57
 
public/js/utils.js CHANGED
@@ -1,3 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  // 字体大小设置
2
  function initFontSize() {
3
  const savedSize = localStorage.getItem('fontSize') || '18';
 
1
+ // HTML 转义函数 - 防止 XSS 注入
2
+ function escapeHtml(str) {
3
+ if (str === null || str === undefined) return '';
4
+ return String(str)
5
+ .replace(/&/g, '&amp;')
6
+ .replace(/</g, '&lt;')
7
+ .replace(/>/g, '&gt;')
8
+ .replace(/"/g, '&quot;')
9
+ .replace(/'/g, '&#39;');
10
+ }
11
+
12
+ // 转义用于 JavaScript 字符串的内容
13
+ function escapeJs(str) {
14
+ if (str === null || str === undefined) return '';
15
+ return String(str)
16
+ .replace(/\\/g, '\\\\')
17
+ .replace(/'/g, "\\'")
18
+ .replace(/"/g, '\\"')
19
+ .replace(/\n/g, '\\n')
20
+ .replace(/\r/g, '\\r');
21
+ }
22
+
23
  // 字体大小设置
24
  function initFontSize() {
25
  const savedSize = localStorage.getItem('fontSize') || '18';