安全加固:console_log.meshtastic 默认关闭(不打印解密明文),session_secure 默认 true,config.yaml 回写权限改 0600,公开接口错误信息脱敏(/api/health、/api/channels、列表接口,text-messages 移除 mqtt_remote_host),bot PSK 不回显(psk_set+更新保持原值),前端 help 页 DOMPurify 客户端消毒,后端 v1.6.0

This commit is contained in:
2026-08-20 17:19:40 +08:00
parent ba9be5b68b
commit b0202062af
15 changed files with 84 additions and 35 deletions
+17
View File
@@ -8,6 +8,7 @@
"name": "meshmap_frontend",
"version": "0.0.0",
"dependencies": {
"dompurify": "^3.4.14",
"leaflet": "^1.9.4",
"vue": "^3.5.34"
},
@@ -789,6 +790,13 @@
"undici-types": "~7.16.0"
}
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@vitejs/plugin-vue": {
"version": "6.0.7",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz",
@@ -993,6 +1001,15 @@
"node": ">=8"
}
},
"node_modules/dompurify": {
"version": "3.4.14",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.14.tgz",
"integrity": "sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/enhanced-resolve": {
"version": "5.23.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.23.0.tgz",
+1
View File
@@ -9,6 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
"dompurify": "^3.4.14",
"leaflet": "^1.9.4",
"vue": "^3.5.34"
},
+9 -6
View File
@@ -98,18 +98,20 @@ watch(currentChannelID, () => {
}
})
function botPayload(form: { node_num: string | number | null; long_name: string; short_name: string; default_channel_id: string; topic_prefix?: string; psk?: string; nodeinfo_broadcast_enabled?: boolean; nodeinfo_broadcast_interval_seconds?: string | number; enabled: boolean }): BotNodePayload {
function botPayload(form: { node_num: string | number | null; long_name: string; short_name: string; default_channel_id: string; topic_prefix?: string; psk?: string; nodeinfo_broadcast_enabled?: boolean; nodeinfo_broadcast_interval_seconds?: string | number; enabled: boolean }, isCreate = false): BotNodePayload {
// <input type="number"> 的 v-model 会把绑定值转成 number,而 number 上没有 trim
// 直接调用会抛 "node_num.trim is not a function" 让保存失败。统一转成 string 再 trim。
const nodeNumText = form.node_num == null ? '' : String(form.node_num).trim()
const interval = Number(form.nodeinfo_broadcast_interval_seconds || 3600)
const psk = form.psk?.trim() || ''
return {
node_num: nodeNumText ? Number(nodeNumText) : null,
long_name: form.long_name.trim(),
short_name: form.short_name.trim(),
default_channel_id: form.default_channel_id.trim(),
topic_prefix: form.topic_prefix?.trim() || 'msh/CN',
psk: form.psk?.trim() || 'AQ==',
// 创建时缺省 AQ==;更新时不携带 psk(后端保持原值,避免密钥被重置)。
...(psk || isCreate ? { psk: psk || 'AQ==' } : {}),
nodeinfo_broadcast_enabled: form.nodeinfo_broadcast_enabled ?? true,
nodeinfo_broadcast_interval_seconds: Number.isFinite(interval) && interval > 0 ? interval : 3600,
enabled: form.enabled,
@@ -123,7 +125,8 @@ function resetEdits() {
short_name: bot.short_name,
default_channel_id: bot.default_channel_id,
topic_prefix: bot.topic_prefix,
psk: bot.psk || 'AQ==',
// PSK 不回显,编辑时留空表示保持不变。
psk: '',
nodeinfo_broadcast_enabled: bot.nodeinfo_broadcast_enabled,
nodeinfo_broadcast_interval_seconds: String(bot.nodeinfo_broadcast_interval_seconds || 3600),
enabled: bot.enabled,
@@ -253,7 +256,7 @@ async function createBot() {
error.value = ''
message.value = ''
try {
await createBotNode(botPayload(newBot.value))
await createBotNode(botPayload(newBot.value, true))
newBot.value = { node_num: '', long_name: '', short_name: '', default_channel_id: 'LongFast', topic_prefix: 'msh/CN', psk: 'AQ==', nodeinfo_broadcast_enabled: true, nodeinfo_broadcast_interval_seconds: '3600', enabled: true }
message.value = '机器人已创建'
await refreshBots()
@@ -503,7 +506,7 @@ onBeforeUnmount(() => {
<label>短名称<input v-model="edits[bot.id].short_name" /></label>
<label>默认频道<input v-model="edits[bot.id].default_channel_id" /></label>
<label>MQTT 根地址<input v-model="edits[bot.id].topic_prefix" placeholder="msh/CN" /></label>
<label>频道密钥 PSK<input v-model="edits[bot.id].psk" placeholder="AQ==" /></label>
<label>频道密钥 PSK<input v-model="edits[bot.id].psk" placeholder="留空保持不变" /></label>
<label>NodeInfo 间隔秒数<input v-model="edits[bot.id].nodeinfo_broadcast_interval_seconds" type="number" min="60" /></label>
<label class="inline"><input v-model="edits[bot.id].nodeinfo_broadcast_enabled" type="checkbox" /> 定期广播 NodeInfo</label>
<label class="inline"><input v-model="edits[bot.id].enabled" type="checkbox" /> 启用</label>
@@ -537,7 +540,7 @@ onBeforeUnmount(() => {
<span><strong>{{ selectedBot.node_num }}</strong><small>Node Num</small></span>
<span><strong>{{ selectedBot.default_channel_id }}</strong><small>默认频道</small></span>
<span><strong>{{ selectedBot.topic_prefix || 'msh/CN' }}</strong><small>MQTT 根地址</small></span>
<span><strong>{{ selectedBot.psk || 'AQ==' }}</strong><small>频道 PSK</small></span>
<span><strong>{{ selectedBot.psk_set ? '已配置' : '默认' }}</strong><small>频道 PSK</small></span>
<span><strong>{{ selectedBot.private_key_set ? '已生成' : '未生成' }}</strong><small>机器人密钥</small></span>
<span class="public-key"><strong>{{ selectedBot.public_key || '-' }}</strong><small>Public Key</small></span>
<span><strong>{{ selectedBot.nodeinfo_broadcast_enabled ? `${selectedBot.nodeinfo_broadcast_interval_seconds}s` : '关闭' }}</strong><small>NodeInfo 广播</small></span>
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import DOMPurify from 'dompurify'
import { getAdminHelpContent, previewAdminHelpContent, saveAdminHelpContent } from '../api'
import type { HelpContent } from '../types'
@@ -25,7 +26,7 @@ async function loadHelpContent() {
const response = await getAdminHelpContent()
latest.value = response.item
markdown.value = response.item.markdown
previewHtml.value = response.item.html
previewHtml.value = DOMPurify.sanitize(response.item.html)
} catch (err) {
error.value = err instanceof Error ? err.message : String(err)
} finally {
@@ -38,7 +39,7 @@ async function previewHelpContent() {
error.value = ''
try {
const response = await previewAdminHelpContent(markdown.value)
previewHtml.value = response.html
previewHtml.value = DOMPurify.sanitize(response.html)
} catch (err) {
error.value = err instanceof Error ? err.message : String(err)
} finally {
@@ -67,7 +68,7 @@ async function saveHelpContent() {
const response = await saveAdminHelpContent(markdown.value)
latest.value = response.item
markdown.value = response.item.markdown
previewHtml.value = response.item.html
previewHtml.value = DOMPurify.sanitize(response.item.html)
message.value = `帮助内容已保存为版本 #${response.item.id}`
} catch (err) {
error.value = err instanceof Error ? err.message : String(err)
+2 -2
View File
@@ -48,7 +48,7 @@ async function toggleBotLLMQueue(bot: BotNode) {
enabled: bot.enabled,
default_channel_id: bot.default_channel_id,
topic_prefix: bot.topic_prefix,
psk: bot.psk,
nodeinfo_broadcast_enabled: bot.nodeinfo_broadcast_enabled,
nodeinfo_broadcast_interval_seconds: bot.nodeinfo_broadcast_interval_seconds,
llm_queue_enabled: !bot.llm_queue_enabled,
@@ -71,7 +71,7 @@ async function toggleBotIncludeChannel(bot: BotNode) {
enabled: bot.enabled,
default_channel_id: bot.default_channel_id,
topic_prefix: bot.topic_prefix,
psk: bot.psk,
nodeinfo_broadcast_enabled: bot.nodeinfo_broadcast_enabled,
nodeinfo_broadcast_interval_seconds: bot.nodeinfo_broadcast_interval_seconds,
llm_queue_enabled: bot.llm_queue_enabled,
+3 -1
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import DOMPurify from 'dompurify'
import { getHelpContent } from '../api'
const loading = ref(false)
@@ -11,7 +12,8 @@ async function loadHelpContent() {
error.value = ''
try {
const response = await getHelpContent()
html.value = response.item.html
// 服务端已用 bluemonday 消毒,这里再用 DOMPurify 做客户端纵深防御。
html.value = DOMPurify.sanitize(response.item.html)
} catch (err) {
error.value = err instanceof Error ? err.message : String(err)
} finally {
+1 -1
View File
@@ -525,7 +525,7 @@ export interface BotNode {
enabled: boolean
default_channel_id: string
topic_prefix: string
psk: string
psk_set: boolean
public_key: string
private_key_set: boolean
nodeinfo_broadcast_enabled: boolean