fix(XSS): 🐛 fix some possible bugs in XSS
This commit is contained in:
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -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
2
package.json
vendored
@@ -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"]
|
||||
}
|
||||
}
|
||||
|
||||
4
scripts/check-all.js
vendored
4
scripts/check-all.js
vendored
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ''
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user