@inventec/dsh-onetree-log 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/lib/client.js +1 -1
- package/lib/extract.js +41 -1
- package/lib/index.js +6 -5
- package/lib/store.js +7 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,8 +27,9 @@ AMI MegaRAC OneTree 一键日志(OpenBMC dreport dump)上传与分析框架
|
|
|
27
27
|
|
|
28
28
|
## 安全
|
|
29
29
|
|
|
30
|
-
- 解包三防:zip-slip(绝对路径/`..`/NUL
|
|
31
|
-
|
|
30
|
+
- 解包三防:zip-slip(绝对路径/`..`/NUL)、符号链接与硬链接条目直接拒绝
|
|
31
|
+
(tar 路径解析期拒绝;zip 路径经系统工具解包后置扫描,发现符号链接
|
|
32
|
+
清除并整体拒绝)、单文件 1GiB / 总量 2GiB / 条目 20k 上限
|
|
32
33
|
- 文件预览路径二次校验(resolveFile 拒绝逃逸)
|
|
33
34
|
- 信任围栏与 dsh-obmc-web 同款:Host 须 loopback 或部署信任域、
|
|
34
35
|
Sec-Fetch-Site 不得 cross-site、Origin 须同 Host 主机名
|
package/lib/client.js
CHANGED
|
@@ -231,7 +231,7 @@ window.__ModuleLoader__.load({
|
|
|
231
231
|
},
|
|
232
232
|
}, data.content),
|
|
233
233
|
data && data.truncated && h('div', { style: { fontSize: '11px', opacity: 0.6, marginTop: '4px' } },
|
|
234
|
-
`(仅显示前 ${humanBytes(data.content ? data.content.length : 0)},完整内容请到服务器查看解压目录)`),
|
|
234
|
+
`(仅显示前 ${humanBytes(data.previewBytes ?? (data.content ? data.content.length : 0))},完整内容请到服务器查看解压目录)`),
|
|
235
235
|
)
|
|
236
236
|
}
|
|
237
237
|
|
package/lib/extract.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* - `.tar.zst` / `.tzst`:Node ≥22.15 的 zlib zstd 流;无原生支持时回退系统 `zstd -dc`
|
|
8
8
|
* - `.zip`:系统 `unzip`(回退 `tar`),先列目校验再解压(execFile,无 shell)
|
|
9
9
|
*
|
|
10
|
-
* 防护:zip-slip(绝对路径 / `..` / NUL
|
|
10
|
+
* 防护:zip-slip(绝对路径 / `..` / NUL)、符号链接与硬链接直接拒绝
|
|
11
|
+
* (tar 路径解析期拒绝;zip 路径经系统工具解包后置扫描拒绝)、
|
|
11
12
|
* 单文件 1GiB / 总量 2GiB / 条目数 20k 上限,超限立即中止并抛错。
|
|
12
13
|
*/
|
|
13
14
|
import fs from 'node:fs'
|
|
@@ -249,6 +250,43 @@ function assertSafeEntryNames(listing) {
|
|
|
249
250
|
}
|
|
250
251
|
}
|
|
251
252
|
|
|
253
|
+
// zip 交给系统工具解包,解包前只能拿到条目名(unzip -Z1 / tar -tf),
|
|
254
|
+
// 链接属性不可见。落盘后置扫描兜底(2026-09-28 审计修复):zip 内符号
|
|
255
|
+
// 链接条目会被系统工具落成真实符号链接,若不拒绝,previewFile 的
|
|
256
|
+
// statSync/readSync 会跟链读出 destDir 之外的文件——与 tar 路径
|
|
257
|
+
// 「链接条目直接拒绝」同语义。返回相对路径列表,非空即整体拒绝。
|
|
258
|
+
function scanSymlinkEntries(root) {
|
|
259
|
+
const found = []
|
|
260
|
+
const stack = [root]
|
|
261
|
+
while (stack.length > 0 && found.length < 16) {
|
|
262
|
+
const dir = stack.pop()
|
|
263
|
+
let entries
|
|
264
|
+
try {
|
|
265
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
266
|
+
} catch {
|
|
267
|
+
continue // 竞态/权限:读取失败的分支不再深挖
|
|
268
|
+
}
|
|
269
|
+
for (const entry of entries) {
|
|
270
|
+
const full = path.join(dir, entry.name)
|
|
271
|
+
if (entry.isSymbolicLink()) {
|
|
272
|
+
found.push(path.relative(root, full) || entry.name)
|
|
273
|
+
continue
|
|
274
|
+
}
|
|
275
|
+
if (entry.isDirectory()) stack.push(full)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return found
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function rejectZipLinkEntries(destDir) {
|
|
282
|
+
const links = scanSymlinkEntries(destDir)
|
|
283
|
+
if (links.length === 0) return
|
|
284
|
+
for (const rel of links) {
|
|
285
|
+
try { fs.unlinkSync(path.join(destDir, rel)) } catch { /* 尽力清理,不影响拒绝语义 */ }
|
|
286
|
+
}
|
|
287
|
+
throw new Error(`检测到链接条目(符号链接),已拒绝: ${links.join(', ')}`)
|
|
288
|
+
}
|
|
289
|
+
|
|
252
290
|
async function extractZip(archivePath, destDir) {
|
|
253
291
|
fs.mkdirSync(destDir, { recursive: true })
|
|
254
292
|
let listing
|
|
@@ -262,10 +300,12 @@ async function extractZip(archivePath, destDir) {
|
|
|
262
300
|
}
|
|
263
301
|
assertSafeEntryNames(listing)
|
|
264
302
|
await execFilePromise('tar', ['-xf', archivePath, '-C', destDir])
|
|
303
|
+
rejectZipLinkEntries(destDir)
|
|
265
304
|
return
|
|
266
305
|
}
|
|
267
306
|
assertSafeEntryNames(listing)
|
|
268
307
|
await execFilePromise('unzip', ['-o', '-q', archivePath, '-d', destDir])
|
|
308
|
+
rejectZipLinkEntries(destDir)
|
|
269
309
|
}
|
|
270
310
|
|
|
271
311
|
// ── 入口 ─────────────────────────────────────────────────────────────────
|
package/lib/index.js
CHANGED
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
import fs from 'node:fs'
|
|
21
21
|
import path from 'node:path'
|
|
22
22
|
import { pipeline } from 'node:stream/promises'
|
|
23
|
-
import { Readable } from 'node:stream'
|
|
24
23
|
import { DumpStore } from './store.js'
|
|
25
24
|
|
|
26
25
|
export const name = '@inventec/dsh-onetree-log'
|
|
@@ -62,7 +61,6 @@ function readJsonBody(req) {
|
|
|
62
61
|
/** 上传字节流 → 临时文件(限额流式落盘,不进内存)。 */
|
|
63
62
|
async function saveUpload(req, destPath) {
|
|
64
63
|
let received = 0
|
|
65
|
-
const counter = new Readable().wrap(req) // 仅用于错误传播;直接流 req
|
|
66
64
|
const out = fs.createWriteStream(destPath)
|
|
67
65
|
await new Promise((resolve, reject) => {
|
|
68
66
|
req.on('data', chunk => {
|
|
@@ -82,7 +80,6 @@ async function saveUpload(req, destPath) {
|
|
|
82
80
|
req.on('error', reject)
|
|
83
81
|
out.on('error', reject)
|
|
84
82
|
})
|
|
85
|
-
void counter
|
|
86
83
|
}
|
|
87
84
|
|
|
88
85
|
function sanitizeUploadName(raw) {
|
|
@@ -151,11 +148,14 @@ function previewFile(absPath) {
|
|
|
151
148
|
if (looksBinary(buf)) {
|
|
152
149
|
return { binary: true, size: stat.size, truncated: stat.size > len }
|
|
153
150
|
}
|
|
151
|
+
const content = buf.toString('utf8')
|
|
154
152
|
return {
|
|
155
153
|
binary: false,
|
|
156
154
|
size: stat.size,
|
|
157
155
|
truncated: stat.size > len,
|
|
158
|
-
content
|
|
156
|
+
content,
|
|
157
|
+
// UTF-8 实际字节数(content.length 是 UTF-16 字符数,中文日志会偏小)
|
|
158
|
+
previewBytes: Buffer.byteLength(content, 'utf8'),
|
|
159
159
|
}
|
|
160
160
|
} finally {
|
|
161
161
|
fs.closeSync(handle)
|
|
@@ -188,7 +188,8 @@ async function dispatch(req, res, log) {
|
|
|
188
188
|
return writeJson(res, 200, detail)
|
|
189
189
|
}
|
|
190
190
|
if (method === 'DELETE') {
|
|
191
|
-
|
|
191
|
+
const removed = store.remove(id)
|
|
192
|
+
return writeJson(res, removed ? 200 : 404, removed ? { ok: true } : { ok: false, error: 'dump 不存在' })
|
|
192
193
|
}
|
|
193
194
|
return writeJson(res, 405, { error: 'method not allowed' })
|
|
194
195
|
}
|
package/lib/store.js
CHANGED
|
@@ -77,7 +77,7 @@ export class DumpStore {
|
|
|
77
77
|
return this.cache.get(key)
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
/** ingest 完成后的完整详情:meta +
|
|
80
|
+
/** ingest 完成后的完整详情:meta + 现算的文件清单(每次调用全量重建)。 */
|
|
81
81
|
detail(id) {
|
|
82
82
|
const meta = this.get(id)
|
|
83
83
|
if (!meta) return undefined
|
|
@@ -167,13 +167,10 @@ export class DumpStore {
|
|
|
167
167
|
remove(id) {
|
|
168
168
|
const meta = this.get(id)
|
|
169
169
|
if (!meta) return false
|
|
170
|
-
// path-dir:rootDir
|
|
170
|
+
// path-dir:rootDir 是用户目录,绝不能删;登记目录里只有 meta.json,
|
|
171
|
+
// 与其他类型一样整体清掉即可
|
|
171
172
|
const home = path.join(this.dumpsDir, id)
|
|
172
|
-
|
|
173
|
-
fs.rmSync(home, { recursive: true, force: true })
|
|
174
|
-
} else {
|
|
175
|
-
fs.rmSync(home, { recursive: true, force: true })
|
|
176
|
-
}
|
|
173
|
+
fs.rmSync(home, { recursive: true, force: true })
|
|
177
174
|
this.cache.delete(id)
|
|
178
175
|
return true
|
|
179
176
|
}
|
|
@@ -183,7 +180,9 @@ export class DumpStore {
|
|
|
183
180
|
const meta = this.get(id)
|
|
184
181
|
if (!meta) throw new Error('dump 不存在')
|
|
185
182
|
const rel = String(relPath ?? '')
|
|
186
|
-
|
|
183
|
+
// 只按路径段拒绝 '..'('a..b.log' 这类合法文件名不误伤);越界仍由下方
|
|
184
|
+
// 前缀包含检查兜底,fail-safe 方向不变
|
|
185
|
+
if (rel.split(/[\\/]+/).includes('..') || rel.startsWith('/') || rel.includes('\0')) {
|
|
187
186
|
throw new Error('非法的文件路径')
|
|
188
187
|
}
|
|
189
188
|
const target = path.resolve(meta.rootDir, ...rel.split('/').filter(s => s !== '' && s !== '.'))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inventec/dsh-onetree-log",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "AMI MegaRAC OneTree 一键日志(OpenBMC dreport dump)上传与分析框架:设置面板「OneTree 日志」上传压缩包或登记服务器本地路径,安全解包、自动探测 dump 根、文件清单分类,为第二阶段逐域解析提供内核。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|