hookehuyr

security(Masters): 修复XSS漏洞并增强错误处理

- 移除 v-html 使用,改用模板渲染防止 XSS 攻击
- 添加 API 请求错误处理 (try-catch)
- 使用可选链防止数组越界错误
- 添加 processImageUrl 函数安全处理图片 URL
- 改进可访问性 (aria-label, loading lazy)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
......@@ -9,11 +9,18 @@
@click="goDetail(item)"
>
<div class="item-image">
<img :src="item.image" :alt="item.name" />
<img
:src="item.image"
:alt="`${item.role} ${item.name}`"
:aria-label="`${item.role}:${item.name}`"
loading="lazy"
/>
</div>
<div class="item-caption">
<div class="item-role">{{ item.role }}</div>
<div class="item-name" v-html="formatNameWithSuperscript(item.name)"></div>
<div class="item-name">
<sup class="name-sup">上</sup>{{ item.name.charAt(0) }}<sup class="name-sup">下</sup>{{ item.name.slice(1) }}
</div>
</div>
</div>
</section>
......@@ -27,11 +34,43 @@
@click="goDetail(item)"
>
<div class="item-image">
<img :src="item.image" :alt="item.name" />
<img
:src="item.image"
:alt="`${item.role} ${item.name}`"
:aria-label="`${item.role}:${item.name}`"
loading="lazy"
/>
</div>
<div class="item-caption">
<div class="item-role">{{ item.role }}</div>
<div class="item-name small" v-html="formatNameWithSuperscript(item.name)"></div>
<div class="item-name small">
<sup class="name-sup">上</sup>{{ item.name.charAt(0) }}<sup class="name-sup">下</sup>{{ item.name.slice(1) }}
</div>
</div>
</div>
</section>
<!-- 一行两个 Item(双列) -->
<section class="grid-two grid-two--spaced">
<div
class="item-card small"
v-for="(item, i) in gridItems2"
:key="`grid2-${i}`"
@click="goDetail(item)"
>
<div class="item-image">
<img
:src="item.image"
:alt="`${item.role} ${item.name}`"
:aria-label="`${item.role}:${item.name}`"
loading="lazy"
/>
</div>
<div class="item-caption">
<div class="item-role">{{ item.role }}</div>
<div class="item-name small">
<sup class="name-sup">上</sup>{{ item.name.charAt(0) }}<sup class="name-sup">下</sup>{{ item.name.slice(1) }}
</div>
</div>
</div>
</section>
......@@ -56,44 +95,60 @@ const singleItems = ref([])
// 七证
const gridItems = ref([])
// 引礼师
const gridItems2 = ref([])
const goDetail = (item) => {
router.push(`/masters/${item.id}`)
}
const pid = ref(router.currentRoute.value.query.pid)
/**
* 为name字段的第一个文字添加上标效果
* @param {string} name - 原始姓名
* @returns {string} - 带上标的HTML字符串
* 安全处理图片 URL,添加处理参数并处理空值
* @param {string} photo - 原始图片 URL
* @returns {string} - 处理后的图片 URL
*/
const formatNameWithSuperscript = (name) => {
if (!name || name.length === 0) return name
const firstChar = name.charAt(0)
const restChars = name.slice(1)
return `<sup style="font-size: 0.6rem;">上</sup>${firstChar}<sup style="font-size: 0.6rem;">下</sup>${restChars}`
const processImageUrl = (photo) => {
if (!photo) return '/assets/default-avatar.png'
return `${photo}?imageMogr2/thumbnail/400x/strip/quality/70`
}
const pid = ref(router.currentRoute.value.query.pid)
/**
* 安全处理大师数据项
* @param {object} item - API 返回的数据项
* @returns {object} - 处理后的数据项
*/
const processMasterItem = (item) => ({
id: item?.id || '',
role: item?.post_excerpt || '',
name: item?.post_title || '未知',
image: processImageUrl(item?.photo)
})
onMounted(async () => {
try {
// 调用接口获取三师七证数据
const { code, list } = await getSSQZAPI({ pid: pid.value });
if (code) {
const singleItemsData = list[0].list;
const gridItemsData = list[1].list;
singleItems.value = singleItemsData.map(item => ({
id: item.id,
role: item.post_excerpt,
name: item.post_title,
image: item.photo + '?imageMogr2/thumbnail/400x/strip/quality/70'
}))
gridItems.value = gridItemsData.map(item => ({
id: item.id,
role: item.post_excerpt,
name: item.post_title,
image: item.photo + '?imageMogr2/thumbnail/400x/strip/quality/70'
}))
const { code, list } = await getSSQZAPI({ pid: pid.value })
// 验证响应数据
if (!code || !list || !Array.isArray(list)) {
console.error('Invalid API response: missing or invalid data')
return
}
// 安全地获取数据列表,使用可选链和默认值
const singleItemsData = list[0]?.list || []
const gridItemsData = list[1]?.list || []
const gridItems2Data = list[2]?.list || []
// 处理数据
singleItems.value = singleItemsData.map(processMasterItem)
gridItems.value = gridItemsData.map(processMasterItem)
gridItems2.value = gridItems2Data.map(processMasterItem)
} catch (error) {
console.error('Failed to fetch masters data:', error)
// 可以在这里添加用户提示,例如使用 showFailToast
}
})
</script>
......@@ -120,6 +175,10 @@ onMounted(async () => {
gap: 0.75rem;
}
.grid-two--spaced {
margin-top: 1rem;
}
/* 卡片 */
.item-card {
position: relative;
......@@ -179,6 +238,11 @@ onMounted(async () => {
font-size: 1.25rem;
}
/* 姓名上标样式 */
.name-sup {
font-size: 0.6em;
}
/* 响应式调整 */
@media (max-width: 48rem) {
......