fix: 排序魔法数字提取常量 + sidebar-config 远程校验类型守卫

- InstalledAppsModal 提取 ORIGIN_PRIORITY 具名常量与 originPriority(),
  消除排序比较函数中的 0/1 魔法数字(保留 hasOrigin 双来源语义)
- useCatalog.loadSidebarConfig 新增 isValidSidebarEntry 类型守卫,对远程
  sidebar-config.json 入口做字段类型/长度/type 白名单校验,防止畸形数据进入
  渲染层(type 白名单与 typedefinition.ts 的 SidebarEntry.type 严格一致)
- debian/changelog 测试版本号 5.2.1.1-test
- 验证:vue-tsc 0 / eslint 0 / dpkg-buildpackage 打包成功

审查项实证:
- 改进③(filteredApps 多次 filter) 误报:当前已为单 filter 遍历(.24 已改),
  审查贴旧片段且退化 a.origin 会破坏双来源,不改
- 改进①/② 为真改进,已采纳并保真实现
This commit is contained in:
xiyidaiwa
2026-08-13 20:43:43 +08:00
parent 0f62a7e36a
commit 7be90f13ce
3 changed files with 54 additions and 20 deletions
+45 -16
View File
@@ -69,6 +69,35 @@ export const loadCategories = async () => {
}
};
// 远程 sidebar-config.json 入口的类型守卫:在写入渲染层前拦截异常字段。
// 字段类型/长度限制防止畸形数据(如超长 name 撑破布局、非法 type 触发未知分支)。
// type 白名单必须与 global/typedefinition.ts 的 SidebarEntry.type 保持一致。
const VALID_SIDEBAR_TYPES = ["category", "search", "link", "homeList"] as const;
const isValidSidebarEntry = (e: unknown): e is SidebarEntry => {
if (typeof e !== "object" || e === null) return false;
const entry = e as Record<string, unknown>;
if (
typeof entry.id !== "string" ||
entry.id.length === 0 ||
entry.id.length > 64
)
return false;
if (
typeof entry.name !== "string" ||
entry.name.length === 0 ||
entry.name.length > 128
)
return false;
if (entry.icon !== undefined && typeof entry.icon !== "string") return false;
if (
entry.type !== undefined &&
!VALID_SIDEBAR_TYPES.includes(entry.type as (typeof VALID_SIDEBAR_TYPES)[number])
)
return false;
if (entry.value !== undefined && typeof entry.value !== "string") return false;
return true;
};
export const loadSidebarConfig = async () => {
try {
const arch = window.apm_store.arch || "amd64";
@@ -86,23 +115,23 @@ export const loadSidebarConfig = async () => {
const entries = Array.isArray(data) ? data : data.entries || [];
for (const entry of entries) {
if (entry.id && entry.name) {
const existing = entryMap.get(entry.id);
if (existing) {
// 多仓库共有入口,合并来源
if (existing.origins && !existing.origins.includes(mode)) {
existing.origins.push(mode);
}
} else {
entryMap.set(entry.id, {
id: entry.id,
name: entry.name,
icon: entry.icon || "",
type: entry.type || "category",
value: entry.value || entry.id,
origins: [mode],
});
// 严格校验远程配置,避免畸形/恶意字段进入渲染层(Vue 模板自动转义已兜底 XSS)
if (!isValidSidebarEntry(entry)) continue;
const existing = entryMap.get(entry.id);
if (existing) {
// 多仓库共有入口,合并来源
if (existing.origins && !existing.origins.includes(mode)) {
existing.origins.push(mode);
}
} else {
entryMap.set(entry.id, {
id: entry.id,
name: entry.name,
icon: entry.icon ?? "",
type: entry.type ?? "category",
value: entry.value ?? entry.id,
origins: [mode],
});
}
}
} catch (e) {