fix(XSS): 🐛 fix some possible bugs in XSS

This commit is contained in:
Dawn
2025-09-24 10:02:59 +08:00
parent a676b3b70b
commit 946dce4bf8
6 changed files with 54 additions and 22 deletions

3
.gitattributes vendored
View File

@@ -59,7 +59,6 @@ scripts/** linguist-vendored
# 确保主要代码文件被正确识别
*.vue linguist-detectable
*.ts linguist-detectable
*.tsx linguist-detectable
*.rs linguist-detectable
## 语言统计控制:排除 Ruby / Shell / Objective-C++
@@ -87,7 +86,7 @@ tauri-plugin-hula/ios/** linguist-vendored
tauri-plugin-hula/*/** linguist-vendored
tauri-plugin-hula/src/** -linguist-vendored
# 如果需要把上面这些都算作 Rust而不是排除,可改用如下(示例)
# 如果需要把上面这些都算作 Rust而不是排除
# *.rb linguist-language=Rust
# *.sh linguist-language=Rust
# *.mm linguist-language=Rust

2
package.json vendored
View File

@@ -177,6 +177,6 @@
}
},
"pnpm": {
"ignoredBuiltDependencies": ["esbuild", "vue-demi", "@parcel/watcher", "sharp", "tlbs-map-vue"]
"ignoredBuiltDependencies": ["@parcel/watcher", "esbuild", "sharp", "tlbs-map-vue", "vue-demi"]
}
}

View File

@@ -1,5 +1,5 @@
import chalk from 'chalk'
import { execSync } from 'child_process'
import { execFileSync } from 'child_process'
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
@@ -16,7 +16,7 @@ async function runScript(scriptPath, description) {
console.log(chalk.blue(`\n[HuLa ${new Date().toLocaleTimeString()}] 开始${description}...\n`))
try {
execSync(`node ${scriptPath}`, { stdio: 'inherit' })
execFileSync('node', [scriptPath], { stdio: 'inherit' })
const duration = ((performance.now() - startTime) / 1000).toFixed(2)
console.log(chalk.green(`\n${description}完成 (${duration}s)\n`))
return true

View File

@@ -370,11 +370,12 @@ export const useCommon = () => {
const author = dom.name
// 创建一个img标签节点作为头像
const imgNode = document.createElement('img')
if (isSafeUrl(dom.avatar)) {
imgNode.src = dom.avatar
const avatarUrl = AvatarUtils.getAvatarUrl(dom.avatar)
if (isSafeUrl(avatarUrl)) {
imgNode.src = avatarUrl
} else {
// 设置为默认头像或空
imgNode.src = 'avatar/001.png'
imgNode.src = '/avatar/001.png'
}
imgNode.style.cssText = `
width: 20px;

View File

@@ -284,15 +284,25 @@ export const useMsgInput = (messageInputDom: Ref) => {
return [...new Set(atUserIds)]
}
// 在 HTML 字符串中安全解析为 Document 对象
const parseHtmlSafely = (html: string) => {
if (!html) return null
if (typeof DOMParser !== 'undefined') {
return new DOMParser().parseFromString(html, 'text/html')
}
return null
}
/** 去除html标签(用于鉴别回复时是否有输入内容) */
const stripHtml = (html: string) => {
try {
// 检查是否是表情包
if (html.includes('data-type="emoji"')) {
const tmp = document.createElement('div')
tmp.innerHTML = html
const imgElement = tmp.querySelector<HTMLImageElement>('img[data-type]')
if (imgElement && imgElement.src) {
const doc = parseHtmlSafely(html)
const imgElement = doc?.querySelector<HTMLImageElement>('img[data-type]')
if (imgElement?.src) {
return (msgInput.value = imgElement.src)
}
}
@@ -301,20 +311,27 @@ export const useMsgInput = (messageInputDom: Ref) => {
return html
}
const tmp = document.createElement('div')
tmp.innerHTML = html
const replyDiv = tmp.querySelector('#replyDiv')
if (replyDiv) {
replyDiv.remove()
const doc = parseHtmlSafely(html)
if (!doc || !doc.body) {
return html.replace(/<[^>]*>/g, '').trim()
}
const replyDiv = doc.querySelector('#replyDiv')
replyDiv?.remove()
// 检查是否包含粘贴的图片有temp-image id的图片元素
const pastedImage = tmp.querySelector('#temp-image')
const pastedImage = doc.querySelector('#temp-image')
if (pastedImage) {
return 'image' // 返回非空字符串,表示有内容
}
return tmp.textContent?.trim() || tmp.innerText?.trim() || ''
const textContent = doc.body.textContent?.trim()
if (textContent) return textContent
const innerText = (doc.body as HTMLElement).innerText?.trim?.()
if (innerText) return innerText
return ''
} catch (error) {
console.error('Error in stripHtml:', error)
return ''

View File

@@ -33,9 +33,24 @@ export class AvatarUtils {
* @returns 头像字符串或URL
*/
public static getAvatarUrl(avatar: string): string {
if (AvatarUtils.isDefaultAvatar(avatar)) {
const DEFAULT = '/avatar/001.png'
const rawAvatar = avatar.trim()
if (AvatarUtils.isDefaultAvatar(rawAvatar)) {
return `/avatar/${rawAvatar}.webp`
}
try {
const parsed = new URL(avatar)
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
return parsed.toString()
}
} catch {
// 如果是自家预置文件名,可进一步做白名单/正则校验
if (/^[a-z0-9_-]+$/i.test(avatar)) {
return `/avatar/${avatar}.webp`
}
return avatar
}
return DEFAULT
}
}