cc-viewer 1.8.10 → 1.8.11
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/dist/assets/App-vpm6omRY.js +2 -0
- package/dist/assets/{MdxEditorPanel-CTCAo0Gg.js → MdxEditorPanel-CpknU7SR.js} +1 -1
- package/dist/assets/{Mobile-Uo_qQRwl.js → Mobile-BHIBdqHs.js} +1 -1
- package/dist/assets/{ProxyStatsModal-BnPXrvTb.js → ProxyStatsModal-DdV7wN2V.js} +1 -1
- package/dist/assets/{_baseUniq-D2aO8mv4.js → _baseUniq-C2FCldN4.js} +1 -1
- package/dist/assets/{arc-DOjcnBTh.js → arc-CSzd_bdT.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-Drea2H7X.js → architectureDiagram-Q4EWVU46-CjEbBe2R.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-Bc9eijBD.js → blockDiagram-DXYQGD6D-BYzl2ukW.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-D-umzFnM.js → c4Diagram-AHTNJAMY-0x0_8O_X.js} +1 -1
- package/dist/assets/{channel-DUff3FBt.js → channel-xMmQtLq9.js} +1 -1
- package/dist/assets/{chunk-4BX2VUAB-CFnsROfz.js → chunk-4BX2VUAB-DnFoMzQG.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-C95jMPjB.js → chunk-4TB4RGXK-Dc5reuwv.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-CeWf2Y2b.js → chunk-55IACEB6-DTKTLKcJ.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-ClUoTJxv.js → chunk-EDXVE4YY-BNqgeN9r.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-Dk62kqb8.js → chunk-FMBD7UC4-DzJVZ11t.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-CZuykSsu.js → chunk-OYMX7WX6-CjYwzMor.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-DYtx68Um.js → chunk-QZHKN3VN-C0KVKecr.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-rLSq0tSC.js → chunk-YZCP3GAM-3VhNeNVm.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-DdUtH789.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-DdUtH789.js +1 -0
- package/dist/assets/clone-C6d2XDvR.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-B5H6ewon.js → cose-bilkent-S5V4N54A-DGE02sJS.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-BO2R2WP7.js → dagre-KV5264BT-CY4eCcwS.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-Y3GNif-g.js → diagram-5BDNPKRD-DHq2THYn.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-Du3_TylQ.js → diagram-G4DWMVQ6-DD_76oJg.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-DB_Ynyjc.js → diagram-MMDJMWI5-BvQxk1jl.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-_Rfjm82W.js → diagram-TYMM5635-B_tMXPJl.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-D5XhBiiV.js → erDiagram-SMLLAGMA-DpuGbN9e.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-CxWJ7sAQ.js → flowDiagram-DWJPFMVM-BW0xGFBf.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-D-a8fh00.js → ganttDiagram-T4ZO3ILL-BSzTiT93.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-q5WMk0Ce.js → gitGraphDiagram-UUTBAWPF-1tsgBlcn.js} +1 -1
- package/dist/assets/{graph-Bi8xMAuo.js → graph-CwiXMdRR.js} +1 -1
- package/dist/assets/{index-BRtbpMRz.js → index-9e6BGquk.js} +1 -1
- package/dist/assets/{index-BWPHHS6P.js → index-BmpXfVQ8.js} +1 -1
- package/dist/assets/{index-O2Mx9uLd.js → index-CZWNaOo8.js} +1 -1
- package/dist/assets/{index-BBEH6Ppx.js → index-Cixs7aRD.js} +1 -1
- package/dist/assets/{index-C3wsDB-y.js → index-Ct6Fi103.js} +1 -1
- package/dist/assets/index-D1yGak8I.css +1 -0
- package/dist/assets/{index-3Z3ucdlB.js → index-DIAMN9QO.js} +1 -1
- package/dist/assets/{index-BB66PLo6.js → index-DhjwHRxk.js} +1 -1
- package/dist/assets/{index-C4Jei4gi.js → index-tnWldUm3.js} +2 -2
- package/dist/assets/{infoDiagram-42DDH7IO-CE2mUuLF.js → infoDiagram-42DDH7IO-Dk1ESZFk.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-C5sNQIVL.js → ishikawaDiagram-UXIWVN3A-DcPyNwkI.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-g7HXOeyX.js → journeyDiagram-VCZTEJTY-CZWieu1U.js} +1 -1
- package/dist/assets/{jszip.min-x3xOZxY_.js → jszip.min-Bq1JxqcN.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-D3Mus6hc.js → kanban-definition-6JOO6SKY-S1sSSTKx.js} +1 -1
- package/dist/assets/{layout-05qDuweB.js → layout-BQFiWq71.js} +1 -1
- package/dist/assets/{linear-DQEG_N-F.js → linear-BNV4po81.js} +1 -1
- package/dist/assets/{mermaid.core-DcaXqkk5.js → mermaid.core-DvwEdatr.js} +2 -2
- package/dist/assets/{min-DSpvw5zG.js → min-DFik8k4L.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-DomUIop7.js → mindmap-definition-QFDTVHPH-CebeX76u.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-COTLcC5G.js → pieDiagram-DEJITSTG-DMB5JbMP.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-DMg8C3Up.js → quadrantDiagram-34T5L4WZ-BAYzoLVo.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-DF9beYd7.js → requirementDiagram-MS252O5E-CUQoAuOc.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-MvERK2UP.js → sankeyDiagram-XADWPNL6-Dt0AQuLW.js} +1 -1
- package/dist/assets/seqResourceLoaders-C-QHRkUq.js +2 -0
- package/dist/assets/{sequenceDiagram-FGHM5R23-B8k_nyzc.js → sequenceDiagram-FGHM5R23-HyZFUlMU.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-D-9fTYas.js → stateDiagram-FHFEXIEX-B8x24fuD.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-2qtHlnwO.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-CbK1g9CO.js → timeline-definition-GMOUNBTQ-TnbjDVtB.js} +1 -1
- package/dist/assets/{vendor-antd-FhK5cQx7.js → vendor-antd-BTBRwbax.js} +1 -1
- package/dist/assets/{vendor-codemirror-DL3yyKRQ.js → vendor-codemirror-CPWlhuQ1.js} +1 -1
- package/dist/assets/{vendor-mdxeditor-DgyrvHzS.js → vendor-mdxeditor-Cy5Xg_90.js} +2 -2
- package/dist/assets/{vendor-qrcode-BAwwP1Nd.js → vendor-qrcode-E9ld0lSz.js} +1 -1
- package/dist/assets/{vendor-virtuoso-B_7lc2Ot.js → vendor-virtuoso-Bq6QMhgG.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-DYjGtMA5.js → vennDiagram-DHZGUBPP-BqgIjkBO.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-Djh5alV9.js → wardley-RL74JXVD-BFcIn2-Q.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-al50Jlxp.js → wardleyDiagram-NUSXRM2D-CYDWQLb6.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-Bl969-3U.js → xychartDiagram-5P7HB3ND-zfusE3BC.js} +1 -1
- package/dist/index.html +6 -6
- package/package.json +1 -1
- package/server/lib/skills-api.js +48 -26
- package/server/routes/files-fs.js +76 -1
- package/server/routes/skills.js +44 -26
- package/dist/assets/App-DJIVcbMa.js +0 -2
- package/dist/assets/classDiagram-6PBFFD2Q-DT8RQXlO.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-DT8RQXlO.js +0 -1
- package/dist/assets/clone-_dhVVwEt.js +0 -1
- package/dist/assets/index-D3GEPgde.css +0 -1
- package/dist/assets/seqResourceLoaders-DfTPNrje.js +0 -2
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-CO0tv8rD.js +0 -1
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
import { join, dirname, basename, resolve, sep } from 'node:path';
|
|
7
7
|
import { homedir, tmpdir } from 'node:os';
|
|
8
8
|
import { execFile, spawn } from 'node:child_process';
|
|
9
|
-
import { bumpWorkspacesVersion } from '../lib/file-access-policy.js';
|
|
9
|
+
import { bumpWorkspacesVersion, isReadAllowed } from '../lib/file-access-policy.js';
|
|
10
10
|
import { validateImportDir } from '../lib/file-api.js';
|
|
11
11
|
import { PROFILE_PATH, _projectName, _logDir } from '../interceptor.js';
|
|
12
12
|
import { LOG_DIR, getClaudeConfigDir } from '../../findcc.js';
|
|
@@ -659,6 +659,80 @@ function resolvePath(req, res, parsedUrl, isLocal, deps) {
|
|
|
659
659
|
});
|
|
660
660
|
}
|
|
661
661
|
|
|
662
|
+
// Lightweight existence probe for the chat markdown code-span feature: the web
|
|
663
|
+
// client batches path-looking inline code spans here and only renders them
|
|
664
|
+
// clickable when the file provably exists. Uniform {exists:false} for every
|
|
665
|
+
// not-readable-as-regular-file outcome (missing / outside allowlist / denied /
|
|
666
|
+
// directory) — never echoes policy reasons, so this is a strictly weaker oracle
|
|
667
|
+
// than /api/file-content's 404/403 split.
|
|
668
|
+
const FILES_EXISTS_MAX_BATCH = 50;
|
|
669
|
+
const FILES_EXISTS_MAX_PATH = 1024;
|
|
670
|
+
const FILES_EXISTS_MAX_BODY = 256 * 1024; // 50 × 1024 + JSON overhead ≪ this
|
|
671
|
+
|
|
672
|
+
function filesExists(req, res, parsedUrl, isLocal, deps) {
|
|
673
|
+
let body = '';
|
|
674
|
+
let aborted = false;
|
|
675
|
+
req.on('data', chunk => {
|
|
676
|
+
body += chunk;
|
|
677
|
+
if (body.length > FILES_EXISTS_MAX_BODY && !aborted) {
|
|
678
|
+
aborted = true;
|
|
679
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
680
|
+
res.end(JSON.stringify({ error: 'Request too large' }));
|
|
681
|
+
req.destroy();
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
req.on('end', () => {
|
|
685
|
+
if (aborted) return;
|
|
686
|
+
let parsed;
|
|
687
|
+
try { parsed = JSON.parse(body); } catch {
|
|
688
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
689
|
+
res.end(JSON.stringify({ error: 'Invalid request body' }));
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
const paths = parsed && parsed.paths;
|
|
693
|
+
if (!Array.isArray(paths)) {
|
|
694
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
695
|
+
res.end(JSON.stringify({ error: 'Invalid paths' }));
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
if (paths.length > FILES_EXISTS_MAX_BATCH) {
|
|
699
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
700
|
+
res.end(JSON.stringify({ error: 'Too many paths' }));
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
for (const p of paths) {
|
|
704
|
+
if (typeof p !== 'string' || p.length > FILES_EXISTS_MAX_PATH) {
|
|
705
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
706
|
+
res.end(JSON.stringify({ error: 'Invalid path' }));
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
const cwd = process.env.CCV_PROJECT_DIR || process.cwd();
|
|
711
|
+
const results = paths.map(p => ({ path: p, exists: probeFileExists(p, cwd) }));
|
|
712
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
713
|
+
res.end(JSON.stringify({ results }));
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function probeFileExists(p, cwd) {
|
|
718
|
+
try {
|
|
719
|
+
const isAbs = /^([a-zA-Z]:[\\/]|[\\/])/.test(p);
|
|
720
|
+
// Mirrors /api/file-content: a relative `..` segment can never be opened by
|
|
721
|
+
// the click path either, so report it as non-existent rather than 400.
|
|
722
|
+
// Segment-wise check — a legit filename like `foo..bar.md` must not match.
|
|
723
|
+
if (!isAbs && p.split(/[\\/]/).includes('..')) return false;
|
|
724
|
+
// Resolve BEFORE isReadAllowed: realpathSync inside the policy resolves
|
|
725
|
+
// relative input against process.cwd(), which is the app dir (not the
|
|
726
|
+
// project) in packaged Electron runs.
|
|
727
|
+
const absPath = isAbs ? p : resolve(cwd, p);
|
|
728
|
+
const policy = isReadAllowed(absPath);
|
|
729
|
+
if (!policy.ok) return false;
|
|
730
|
+
return statSync(policy.real).isFile();
|
|
731
|
+
} catch {
|
|
732
|
+
return false; // one bad path must not sink the whole batch
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
662
736
|
function createFile(req, res, parsedUrl, isLocal, deps) {
|
|
663
737
|
let body = '';
|
|
664
738
|
req.on('data', chunk => { body += chunk; if (body.length > deps.MAX_POST_BODY) req.destroy(); });
|
|
@@ -964,6 +1038,7 @@ export const filesFsRoutes = [
|
|
|
964
1038
|
{ method: 'POST', match: 'exact', path: '/api/reveal-file', handler: revealFile },
|
|
965
1039
|
{ method: 'POST', match: 'exact', path: '/api/open-file', handler: openFile },
|
|
966
1040
|
{ method: 'POST', match: 'exact', path: '/api/resolve-path', handler: resolvePath },
|
|
1041
|
+
{ method: 'POST', match: 'exact', path: '/api/files-exists', handler: filesExists },
|
|
967
1042
|
{ method: 'POST', match: 'exact', path: '/api/create-file', handler: createFile },
|
|
968
1043
|
{ method: 'POST', match: 'exact', path: '/api/open-terminal', handler: openTerminal },
|
|
969
1044
|
{ method: 'POST', match: 'exact', path: '/api/create-dir', handler: createDir },
|
package/server/routes/skills.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
|
|
3
3
|
import { join, resolve, sep, dirname } from 'node:path';
|
|
4
4
|
import { getClaudeConfigDir } from '../../findcc.js';
|
|
5
|
-
import { listSkills, moveSkill, deleteSkill, validateSkillName } from '../lib/skills-api.js';
|
|
5
|
+
import { listSkills, moveSkill, deleteSkill, validateSkillName, parseSkillFrontmatter } from '../lib/skills-api.js';
|
|
6
6
|
import { isAdminReq } from '../lib/is-admin.js';
|
|
7
7
|
|
|
8
8
|
async function skillsList(req, res) {
|
|
@@ -81,22 +81,35 @@ function skillsDelete(req, res, parsedUrl, isLocal) {
|
|
|
81
81
|
// 设计要点(与原实现一致):
|
|
82
82
|
// · 扩展名白名单只放 zip / md(忽略大小写);其他类型 415;
|
|
83
83
|
// · zip 内必须含 SKILL.md(任意子目录、忽略大小写),取最浅的那个所在目录作为 skill 根;
|
|
84
|
-
// ·
|
|
84
|
+
// · Strict spec check (unified across all three entries): SKILL.md frontmatter must contain
|
|
85
|
+
// a name (must pass validateSkillName) and a description; violations reject with
|
|
86
|
+
// INVALID_FRONTMATTER / MISSING_NAME / INVALID_NAME / MISSING_DESCRIPTION — no more
|
|
87
|
+
// filename / zip-root-dir fallback;
|
|
85
88
|
// · zip bomb 防护:单文件 ≤50MB,总解压 ≤200MB;拒绝 symlink entry。
|
|
86
89
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const nm = /^name\s*:\s*(.*)$/m.exec(m[1]);
|
|
91
|
-
if (!nm) return null;
|
|
92
|
-
return nm[1].trim().replace(/^["']|["']$/g, '');
|
|
93
|
-
};
|
|
90
|
+
// Shared size caps for the zip path (declared-size pass + actual-size re-check).
|
|
91
|
+
const MAX_SKILL_FILE = 50 * 1024 * 1024;
|
|
92
|
+
const MAX_SKILL_TOTAL = 200 * 1024 * 1024;
|
|
94
93
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
94
|
+
// Strict skill-spec validation shared by all three import entries (folder / zip / SKILL.md).
|
|
95
|
+
// Check order is fixed (tests depend on it): no frontmatter block → INVALID_FRONTMATTER;
|
|
96
|
+
// missing name → MISSING_NAME; invalid name → INVALID_NAME; missing description → MISSING_DESCRIPTION.
|
|
97
|
+
function requireSkillSpec(text) {
|
|
98
|
+
const fm = parseSkillFrontmatter(text);
|
|
99
|
+
if (!fm) {
|
|
100
|
+
throw Object.assign(new Error('SKILL.md is missing valid frontmatter'), { status: 400, code: 'INVALID_FRONTMATTER' });
|
|
101
|
+
}
|
|
102
|
+
if (!fm.name) {
|
|
103
|
+
throw Object.assign(new Error('SKILL.md frontmatter is missing a name'), { status: 400, code: 'MISSING_NAME' });
|
|
104
|
+
}
|
|
105
|
+
if (!validateSkillName(fm.name)) {
|
|
106
|
+
throw Object.assign(new Error(`Invalid skill name: ${fm.name}`), { status: 400, code: 'INVALID_NAME' });
|
|
107
|
+
}
|
|
108
|
+
if (!fm.description) {
|
|
109
|
+
throw Object.assign(new Error('SKILL.md frontmatter is missing a description'), { status: 400, code: 'MISSING_DESCRIPTION' });
|
|
110
|
+
}
|
|
111
|
+
return fm;
|
|
112
|
+
}
|
|
100
113
|
|
|
101
114
|
/**
|
|
102
115
|
* 解析一段 multipart body(已组装好的 Buffer)→ { skillName, files: [{relPath, data}] }。
|
|
@@ -136,7 +149,7 @@ export async function parseSkillUpload(buf, boundary, windowsReservedRe) {
|
|
|
136
149
|
|
|
137
150
|
if (isMd) {
|
|
138
151
|
const text = fileData.toString('utf8');
|
|
139
|
-
skillName =
|
|
152
|
+
skillName = requireSkillSpec(text).name;
|
|
140
153
|
skillFiles = [{ relPath: 'SKILL.md', data: fileData }];
|
|
141
154
|
} else {
|
|
142
155
|
const AdmZip = (await import('adm-zip')).default;
|
|
@@ -147,8 +160,6 @@ export async function parseSkillUpload(buf, boundary, windowsReservedRe) {
|
|
|
147
160
|
throw Object.assign(new Error('Invalid zip archive'), { status: 400, code: 'INVALID_ZIP' });
|
|
148
161
|
}
|
|
149
162
|
const entries = zip.getEntries();
|
|
150
|
-
const MAX_PER_FILE = 50 * 1024 * 1024;
|
|
151
|
-
const MAX_TOTAL_UNCOMPRESSED = 200 * 1024 * 1024;
|
|
152
163
|
let totalUncompressed = 0;
|
|
153
164
|
for (const e of entries) {
|
|
154
165
|
if (e.isDirectory) continue;
|
|
@@ -157,11 +168,11 @@ export async function parseSkillUpload(buf, boundary, windowsReservedRe) {
|
|
|
157
168
|
throw Object.assign(new Error('Symlinks not allowed in zip'), { status: 400, code: 'INVALID_ZIP' });
|
|
158
169
|
}
|
|
159
170
|
const sizeRaw = e.header?.size || 0;
|
|
160
|
-
if (sizeRaw >
|
|
171
|
+
if (sizeRaw > MAX_SKILL_FILE) {
|
|
161
172
|
throw Object.assign(new Error('File too large in archive'), { status: 400, code: 'ZIP_BOMB' });
|
|
162
173
|
}
|
|
163
174
|
totalUncompressed += sizeRaw;
|
|
164
|
-
if (totalUncompressed >
|
|
175
|
+
if (totalUncompressed > MAX_SKILL_TOTAL) {
|
|
165
176
|
throw Object.assign(new Error('Archive expands too large'), { status: 400, code: 'ZIP_BOMB' });
|
|
166
177
|
}
|
|
167
178
|
}
|
|
@@ -179,12 +190,16 @@ export async function parseSkillUpload(buf, boundary, windowsReservedRe) {
|
|
|
179
190
|
if (!bestSkillEntry) {
|
|
180
191
|
throw Object.assign(new Error('SKILL.md not found in zip'), { status: 400, code: 'MISSING_SKILL_MD' });
|
|
181
192
|
}
|
|
193
|
+
// Guard BEFORE inflating: header.size is attacker-controlled, but a declared oversize at
|
|
194
|
+
// least lets us reject before spending memory on getData() (actual-size re-check below
|
|
195
|
+
// still catches a lying small header).
|
|
196
|
+
if ((bestSkillEntry.header?.size || 0) > MAX_SKILL_FILE) {
|
|
197
|
+
throw Object.assign(new Error('File too large in archive'), { status: 400, code: 'ZIP_BOMB' });
|
|
198
|
+
}
|
|
182
199
|
const lastSlash = bestSkillEntry.entryName.lastIndexOf('/');
|
|
183
200
|
const skillRootPrefix = lastSlash >= 0 ? bestSkillEntry.entryName.slice(0, lastSlash + 1) : '';
|
|
184
201
|
const skillMdText = bestSkillEntry.getData().toString('utf8');
|
|
185
|
-
skillName =
|
|
186
|
-
|| (skillRootPrefix ? skillRootPrefix.replace(/\/$/, '').split('/').pop() : null)
|
|
187
|
-
|| fallbackBaseName(originalName, true);
|
|
202
|
+
skillName = requireSkillSpec(skillMdText).name;
|
|
188
203
|
|
|
189
204
|
// 二次校验:header.size 来自 zip 中央目录是攻击者可控的(可谎报 size=0),用真实 data.length 复核。
|
|
190
205
|
let actualTotal = 0;
|
|
@@ -194,11 +209,11 @@ export async function parseSkillUpload(buf, boundary, windowsReservedRe) {
|
|
|
194
209
|
const rel = skillRootPrefix ? e.entryName.slice(skillRootPrefix.length) : e.entryName;
|
|
195
210
|
if (!rel || rel.includes('..')) continue;
|
|
196
211
|
const data = e.getData();
|
|
197
|
-
if (data.length >
|
|
212
|
+
if (data.length > MAX_SKILL_FILE) {
|
|
198
213
|
throw Object.assign(new Error('File actual size too large'), { status: 400, code: 'ZIP_BOMB' });
|
|
199
214
|
}
|
|
200
215
|
actualTotal += data.length;
|
|
201
|
-
if (actualTotal >
|
|
216
|
+
if (actualTotal > MAX_SKILL_TOTAL) {
|
|
202
217
|
throw Object.assign(new Error('Archive actual size too large'), { status: 400, code: 'ZIP_BOMB' });
|
|
203
218
|
}
|
|
204
219
|
const finalRel = rel.split('/').pop().toLowerCase() === 'skill.md'
|
|
@@ -208,6 +223,8 @@ export async function parseSkillUpload(buf, boundary, windowsReservedRe) {
|
|
|
208
223
|
}
|
|
209
224
|
}
|
|
210
225
|
|
|
226
|
+
// defense-in-depth: requireSkillSpec already validated the name (and would have
|
|
227
|
+
// thrown first); keep this re-check so future refactors can't silently skip it.
|
|
211
228
|
if (!validateSkillName(skillName)) {
|
|
212
229
|
throw Object.assign(new Error(`Invalid skill name: ${skillName}`), { status: 400, code: 'INVALID_NAME' });
|
|
213
230
|
}
|
|
@@ -264,7 +281,8 @@ export function importSkillTo(req, res, { skillsRoot, windowsReserved }) {
|
|
|
264
281
|
const contentLength = parseInt(req.headers['content-length'] || '0', 10);
|
|
265
282
|
if (contentLength > MAX_UPLOAD) {
|
|
266
283
|
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
267
|
-
|
|
284
|
+
// code 字段供前端 skillImportErrorKey 映射成 i18n 文案,而不是裸英文
|
|
285
|
+
res.end(JSON.stringify({ error: 'File too large (max 100MB)', code: 'TOO_LARGE' }));
|
|
268
286
|
return;
|
|
269
287
|
}
|
|
270
288
|
const boundary = boundaryMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
@@ -276,7 +294,7 @@ export function importSkillTo(req, res, { skillsRoot, windowsReserved }) {
|
|
|
276
294
|
if (totalSize > MAX_UPLOAD) {
|
|
277
295
|
aborted = true;
|
|
278
296
|
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
279
|
-
res.end(JSON.stringify({ error: 'File too large (max 100MB)' }));
|
|
297
|
+
res.end(JSON.stringify({ error: 'File too large (max 100MB)', code: 'TOO_LARGE' }));
|
|
280
298
|
req.destroy();
|
|
281
299
|
return;
|
|
282
300
|
}
|