@lifeaitools/rdc-skills 0.13.3 → 0.13.4
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/package.json +1 -1
- package/scripts/rdc-brochure.mjs +67 -3
package/package.json
CHANGED
package/scripts/rdc-brochure.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { execSync, spawnSync } from 'node:child_process';
|
|
|
7
7
|
import { createHash } from 'node:crypto';
|
|
8
8
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, copyFileSync } from 'node:fs';
|
|
9
9
|
import { homedir, tmpdir } from 'node:os';
|
|
10
|
-
import { dirname, basename, extname, join, resolve, relative } from 'node:path';
|
|
10
|
+
import { dirname, basename, extname, join, resolve, relative, sep } from 'node:path';
|
|
11
11
|
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
12
12
|
import { createRequire } from 'node:module';
|
|
13
13
|
|
|
@@ -142,15 +142,48 @@ function loadAdmZip() {
|
|
|
142
142
|
try { return createRequire(join(cacheDir, 'package.json'))('adm-zip'); } catch { return null; }
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
// Reject any archive member that resolves outside destDir (zip-slip / path
|
|
146
|
+
// traversal). Returns the validated absolute destination path. Throws on escape.
|
|
147
|
+
function safeJoinOrThrow(destDir, entryName) {
|
|
148
|
+
const root = resolve(destDir);
|
|
149
|
+
const dest = resolve(root, entryName);
|
|
150
|
+
if (dest !== root && !dest.startsWith(root + sep)) {
|
|
151
|
+
throw new Error(`zip-slip: entry "${entryName}" escapes destination directory`);
|
|
152
|
+
}
|
|
153
|
+
return dest;
|
|
154
|
+
}
|
|
155
|
+
|
|
145
156
|
function extractZip(zipPath, destDir) {
|
|
146
157
|
mkdirSync(destDir, { recursive: true });
|
|
158
|
+
const root = resolve(destDir);
|
|
147
159
|
const AdmZip = loadAdmZip();
|
|
148
160
|
if (AdmZip) {
|
|
149
|
-
new AdmZip(zipPath)
|
|
161
|
+
const zip = new AdmZip(zipPath);
|
|
162
|
+
const entries = zip.getEntries();
|
|
163
|
+
// First pass: validate every entry resolves inside destDir. A brochure
|
|
164
|
+
// source zip must never contain `..` or absolute members — reject the whole
|
|
165
|
+
// archive rather than silently skipping (fail-closed).
|
|
166
|
+
for (const entry of entries) {
|
|
167
|
+
safeJoinOrThrow(root, entry.entryName);
|
|
168
|
+
}
|
|
169
|
+
// Second pass: extract validated entries individually. Do NOT use the
|
|
170
|
+
// blanket extractAllTo — adm-zip does not sanitize entry paths.
|
|
171
|
+
for (const entry of entries) {
|
|
172
|
+
if (entry.isDirectory) {
|
|
173
|
+
mkdirSync(safeJoinOrThrow(root, entry.entryName), { recursive: true });
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const dest = safeJoinOrThrow(root, entry.entryName);
|
|
177
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
178
|
+
zip.extractEntryTo(entry, root, /* maintainEntryPath */ true, /* overwrite */ true);
|
|
179
|
+
}
|
|
150
180
|
return;
|
|
151
181
|
}
|
|
152
|
-
// Fallback: platform-native extractor.
|
|
182
|
+
// Fallback: platform-native extractor. Validate members via a listing pass
|
|
183
|
+
// BEFORE extracting, since the bulk extract commands cannot apply per-entry
|
|
184
|
+
// guards. Reject any `..` segment or absolute/leading-slash member.
|
|
153
185
|
log('adm-zip unavailable — falling back to platform unzip');
|
|
186
|
+
assertNativeZipSafe(zipPath);
|
|
154
187
|
let r;
|
|
155
188
|
if (process.platform === 'win32') {
|
|
156
189
|
r = spawnSync('powershell', ['-NoProfile', '-Command',
|
|
@@ -161,6 +194,37 @@ function extractZip(zipPath, destDir) {
|
|
|
161
194
|
if (!r || r.status !== 0) throw new Error('unzip failed (no pure-Node lib and platform extractor failed)');
|
|
162
195
|
}
|
|
163
196
|
|
|
197
|
+
// List archive members with a native tool and reject any traversal/absolute
|
|
198
|
+
// entry. Throws if a member is unsafe or the listing cannot be produced.
|
|
199
|
+
function assertNativeZipSafe(zipPath) {
|
|
200
|
+
let names = [];
|
|
201
|
+
if (process.platform === 'win32') {
|
|
202
|
+
const ps = spawnSync('powershell', ['-NoProfile', '-Command',
|
|
203
|
+
`Add-Type -AssemblyName System.IO.Compression.FileSystem; ` +
|
|
204
|
+
`[System.IO.Compression.ZipFile]::OpenRead('${zipPath}').Entries | ForEach-Object { $_.FullName }`],
|
|
205
|
+
{ encoding: 'utf8' });
|
|
206
|
+
if (ps.status !== 0) throw new Error('cannot list zip members for traversal check (PowerShell)');
|
|
207
|
+
names = String(ps.stdout || '').split(/\r?\n/);
|
|
208
|
+
} else {
|
|
209
|
+
const zi = spawnSync('zipinfo', ['-1', zipPath], { encoding: 'utf8' });
|
|
210
|
+
if (zi.status === 0) {
|
|
211
|
+
names = String(zi.stdout || '').split(/\r?\n/);
|
|
212
|
+
} else {
|
|
213
|
+
const ul = spawnSync('unzip', ['-Z1', zipPath], { encoding: 'utf8' });
|
|
214
|
+
if (ul.status !== 0) throw new Error('cannot list zip members for traversal check (unzip -Z1)');
|
|
215
|
+
names = String(ul.stdout || '').split(/\r?\n/);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
for (const raw of names) {
|
|
219
|
+
const name = raw.trim();
|
|
220
|
+
if (!name) continue;
|
|
221
|
+
const norm = name.replace(/\\/g, '/');
|
|
222
|
+
if (norm.startsWith('/') || /^[A-Za-z]:/.test(norm) || norm.split('/').some((seg) => seg === '..')) {
|
|
223
|
+
throw new Error(`zip-slip: entry "${name}" escapes destination directory`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
164
228
|
// --- stage input -----------------------------------------------------------
|
|
165
229
|
function stageInput() {
|
|
166
230
|
const stageDir = join(workRoot, 'src');
|