fix: XSS 风险修复

- showToast 改用 textContent 设置消息内容,防止 HTML 注入
- createHoverText 对所有用户输入进行 escapeHtml 转义
- 续订/支付历史/编辑支付模态框中的 subscription.name 和
  payment.note 使用 escapeHtml 转义
- debug 页面 adminUsername 转义
- 添加全局 escapeHtml 工具函数
This commit is contained in:
wangwangit
2026-05-19 12:06:21 +08:00
parent b76e047050
commit e2fc130f99
3 changed files with 19 additions and 11 deletions

View File

@@ -38,7 +38,7 @@ async function handleDebug(request, env) {
<div class="info">
<h3>配置信息</h3>
<p class="${debugInfo.configExists ? 'success' : 'error'}">配置存在: ${debugInfo.configExists ? '✓' : '✗'}</p>
<p>管理员用户名: ${debugInfo.adminUsername}</p>
<p>管理员用户名: ${String(debugInfo.adminUsername || '').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</p>
<p class="${debugInfo.hasJwtSecret ? 'success' : 'error'}">JWT密钥: ${debugInfo.hasJwtSecret ? '✓' : '✗'} (长度: ${debugInfo.jwtSecretLength})</p>
</div>

View File

@@ -734,6 +734,12 @@
return response;
}
// HTML 转义,防止 XSS
function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
}
// 农历转换工具函数 - 前端版本
const lunarCalendar = {
// 农历数据 (1900-2100年)
@@ -1039,7 +1045,8 @@ const lunarBiz = {
type === 'error' ? 'exclamation-circle' :
type === 'warning' ? 'exclamation-triangle' : 'info-circle';
toast.innerHTML = '<div class="flex items-center"><i class="fas fa-' + icon + ' mr-2"></i><span>' + message + '</span></div><span class="toast-close" onclick="this.parentElement.remove()">&times;</span>';
toast.innerHTML = '<div class="flex items-center"><i class="fas fa-' + icon + ' mr-2"></i><span class="toast-msg"></span></div><span class="toast-close" onclick="this.parentElement.remove()">&times;</span>';
toast.querySelector('.toast-msg').textContent = message;
container.appendChild(toast);
setTimeout(() => toast.classList.add('show'), 50);
@@ -1147,12 +1154,12 @@ const lunarBiz = {
// 创建带悬浮提示的文本元素
function createHoverText(text, maxLength = 30, className = 'text-sm text-gray-900') {
if (!text || text.length <= maxLength) {
return '<div class="' + className + '">' + text + '</div>';
return '<div class="' + className + '">' + escapeHtml(text) + '</div>';
}
const truncated = text.substring(0, maxLength) + '...';
const truncated = escapeHtml(text.substring(0, maxLength)) + '...';
return '<div class="hover-container">' +
'<div class="hover-text ' + className + '" data-full-text="' + text.replace(/"/g, '&quot;') + '">' +
'<div class="hover-text ' + className + '" data-full-text="' + escapeHtml(text) + '">' +
truncated +
'</div>' +
'<div class="hover-tooltip"></div>' +
@@ -1778,7 +1785,7 @@ const lunarBiz = {
' <div class="relative top-20 mx-auto p-5 border w-full max-w-md shadow-lg rounded-md bg-white" onclick="event.stopPropagation()">' +
' <div class="flex justify-between items-center pb-3 border-b">' +
' <h3 class="text-xl font-semibold text-gray-900">' +
' <i class="fas fa-sync-alt mr-2"></i>手动续订 - ' + subscription.name +
' <i class="fas fa-sync-alt mr-2"></i>手动续订 - ' + escapeHtml(subscription.name) +
' </h3>' +
' <button onclick="closeRenewFormModal()" class="text-gray-400 hover:text-gray-500">' +
' <i class="fas fa-times text-2xl"></i>' +
@@ -2035,7 +2042,7 @@ const lunarBiz = {
periodHtml = '<div class="mt-1 ml-6 text-xs text-gray-500"><i class="fas fa-clock mr-1"></i>计费周期: ' + startStr + ' - ' + endStr + '</div>';
}
const noteHtml = payment.note ? '<div class="mt-1 ml-6 text-sm text-gray-600">' + payment.note + '</div>' : '';
const noteHtml = payment.note ? '<div class="mt-1 ml-6 text-sm text-gray-600">' + escapeHtml(payment.note) + '</div>' : '';
const paymentDataJson = JSON.stringify(payment).replace(/"/g, '&quot;');
return `
<div class="border-b border-gray-200 py-3 hover:bg-gray-50">
@@ -2077,7 +2084,7 @@ const lunarBiz = {
<div class="relative top-20 mx-auto p-5 border w-full max-w-2xl shadow-lg rounded-md bg-white" onclick="event.stopPropagation()">
<div class="flex justify-between items-center pb-3 border-b">
<h3 class="text-xl font-semibold text-gray-900">
<i class="fas fa-history mr-2"></i>${subscription.name} - 支付历史
<i class="fas fa-history mr-2"></i>${escapeHtml(subscription.name)} - 支付历史
</h3>
<button onclick="closePaymentHistoryModal()" class="text-gray-400 hover:text-gray-500">
<i class="fas fa-times text-2xl"></i>
@@ -2193,7 +2200,7 @@ const lunarBiz = {
<form id="editPaymentForm" class="mt-4 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">订阅名称</label>
<input type="text" value="${subscription.name}" disabled
<input type="text" value="${escapeHtml(subscription.name)}" disabled
class="w-full px-3 py-2 border border-gray-300 rounded-md bg-gray-100">
</div>
@@ -2211,7 +2218,7 @@ const lunarBiz = {
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">备注</label>
<input type="text" id="editPaymentNote" value="${payment.note || ''}"
<input type="text" id="editPaymentNote" value="${escapeHtml(payment.note || '')}"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500">
</div>

View File

@@ -643,7 +643,8 @@
type === 'error' ? 'exclamation-circle' :
type === 'warning' ? 'exclamation-triangle' : 'info-circle';
toast.innerHTML = '<div class="flex items-center"><i class="fas fa-' + icon + ' mr-2"></i><span>' + message + '</span></div><span class="toast-close" onclick="this.parentElement.remove()">&times;</span>';
toast.innerHTML = '<div class="flex items-center"><i class="fas fa-' + icon + ' mr-2"></i><span class="toast-msg"></span></div><span class="toast-close" onclick="this.parentElement.remove()">&times;</span>';
toast.querySelector('.toast-msg').textContent = message;
container.appendChild(toast);
setTimeout(() => toast.classList.add('show'), 50);