Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions src/main/services/backup-restore.js
Original file line number Diff line number Diff line change
Expand Up @@ -283,12 +283,15 @@ async function restorePartialLibrary(root, filePath, manifest) {
const unzipped = await streamBackupZip(filePath, {
coverWriter: async (name, data) => {
const dest = path.join(coversDir, name)
const exists = await fs.access(dest).then(
() => true,
() => false,
)
if (exists) return
await fs.writeFile(dest, data)
// CodeQL #15:access 预检 + writeFile 存在 check-then-write 竞态,
// 改用 wx 独占创建标志——已存在即 EEXIST 原子失败,语义与原
// 「不覆盖既有封面」一致且无竞态窗口
try {
await fs.writeFile(dest, data, { flag: 'wx' })
} catch (err) {
if (errorCode(err) === 'EEXIST') return
throw err
}
writtenCovers.push(dest)
covers += 1
},
Expand Down
24 changes: 18 additions & 6 deletions src/main/services/civitai.js
Original file line number Diff line number Diff line change
Expand Up @@ -469,9 +469,23 @@ export async function fetchCivitaiModelType(modelId) {
return { found: true, type: raw.type.trim(), tags }
}

/** stripHtml 单次解码的常见实体表(命名 + 数字引用,键为原文形态) */
const HTML_TEXT_ENTITIES = {
'&': '&',
'&lt;': '<',
'&gt;': '>',
'&quot;': '"',
'&#39;': "'",
'&nbsp;': ' ',
}

/**
* 剥离 Civitai 描述中的 HTML 标签并压缩空白(描述为富文本 HTML,
* 备注为纯文本;实体仅处理常见命名实体,其余由浏览器渲染时自然容错)。
* CodeQL #2:实体解码为单趟替换——链式逐个 replace 会让前一步的输出
* 成为后一步的输入(`&amp;lt;` 二次解码为 `<`),单趟 + 表映射只解码
* 一层,语义与 HTML 规范一致。输出仅流向纯文本 sink(Vue 插值/textarea/
* CSV/Markdown 导出),无 HTML 渲染路径。
* 纯函数,便于单测。
* @param {string} html HTML 文本
* @returns {string} 纯文本(已修剪)
Expand All @@ -482,12 +496,10 @@ export function stripHtml(html) {
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<\/(p|div|li|h[1-6])>/gi, '\n')
.replace(/<[^>]*>/g, '')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&nbsp;/g, ' ')
.replace(
/&(?:amp|lt|gt|quot|#39|nbsp);/g,
(m) => HTML_TEXT_ENTITIES[m] ?? m,
)
.replace(/\n{3,}/g, '\n\n')
.trim()
}
Expand Down
10 changes: 8 additions & 2 deletions src/renderer/src/store/model-export.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,16 @@ function csvCell(value, numeric = false) {
return /[",\r\n]/.test(safe) ? `"${safe.replace(/"/g, '""')}"` : safe
}

/** Markdown 单元格转义(竖线/换行) */
/** Markdown 单元格转义(反斜杠/竖线/换行) */
function mdCell(value) {
const text = value === null || value === undefined ? '' : String(value)
return text.replace(/\|/g, '\\|').replace(/\r?\n/g, ' ')
// CodeQL #12:先转义反斜杠再转义竖线——只转 | 时输入自带的 \ 会
// 「吞掉」后续转义符(`a\` + `|` → `a\\|` 渲染为字面 \ 加分隔符 |,
// 造成表格单元格注入)
return text
.replace(/\\/g, '\\\\')
.replace(/\|/g, '\\|')
.replace(/\r?\n/g, ' ')
}

/**
Expand Down
6 changes: 6 additions & 0 deletions tests/civitai.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,12 @@ describe('stripHtml', () => {
expect(stripHtml('<b>bold</b> text')).toBe('bold text')
})

it('实体只解码一层(CodeQL #2 单趟解码回归)', () => {
// 链式 replace 会把 &amp;lt; 二次解码为 <;单趟解码保持一层语义
expect(stripHtml('&amp;lt;script&amp;gt;')).toBe('&lt;script&gt;')
expect(stripHtml('&amp;amp;')).toBe('&amp;')
})

it('连续空行压缩与非字符串容错', () => {
expect(stripHtml('<p>a</p><div></div><div></div><p>b</p>')).not.toMatch(
/\n{3,}/,
Expand Down
13 changes: 13 additions & 0 deletions tests/settingsBackup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import {
buildCsv,
buildExportRows,
buildMarkdown,
} from '../src/renderer/src/store/model-export'
import { setupComponentTest } from './helpers/componentTest'

Expand Down Expand Up @@ -512,3 +513,15 @@ describe('CSV 导出公式注入中和(O-4)', () => {
expect(csv.split('\r\n')).toHaveLength(3)
})
})

describe('Markdown 导出转义(CodeQL #12)', () => {
it('反斜杠先行转义:输入自带的 \\ 不会吞掉竖线转义符分裂单元格', () => {
// 输入 a\|b → 输出字面序列 a\\\|b(\\ 渲染为字面 \,\| 渲染为字面 |)
expect(buildMarkdown([csvRow({ name: 'a\\|b' })])).toContain('a\\\\\\|b')
})

it('普通竖线仍按 \\| 转义、换行折叠为空格', () => {
const md = buildMarkdown([csvRow({ note: 'x|y\nz' })])
expect(md).toContain('x\\|y z')
})
})
Loading