fix(security): remove curl|bash, enforce HTTPS-only downloads

- Replace all curl|bash install suggestions with brew/npm
- downloadFile: HTTPS-only, reject http:// URLs
- downloadFile: add redirect limit (max 5)
- Remove unused http import from md-to-html scripts
- Add Security Guidelines section to CLAUDE.md
- Update SKILL.md profile dir references
This commit is contained in:
Jim Liu 宝玉
2026-03-06 16:03:10 -06:00
parent 6e533f938f
commit c15a44b439
7 changed files with 312 additions and 134 deletions
@@ -182,7 +182,7 @@ async function checkBun(): Promise<void> {
if (result.status === 0) {
log('Bun runtime', true, `v${result.stdout?.toString().trim()}`);
} else {
log('Bun runtime', false, 'Cannot run bun. Install: curl -fsSL https://bun.sh/install | bash');
log('Bun runtime', false, 'Cannot run bun. Install: brew install oven-sh/bun/bun (macOS) or npm install -g bun');
}
}
+16 -6
View File
@@ -1,7 +1,6 @@
import fs from 'node:fs';
import { mkdir, writeFile } from 'node:fs/promises';
import https from 'node:https';
import http from 'node:http';
import os from 'node:os';
import path from 'node:path';
import process from 'node:process';
@@ -106,18 +105,25 @@ function extractTitleFromMarkdown(markdown: string): string {
return '';
}
function downloadFile(url: string, destPath: string): Promise<void> {
function downloadFile(url: string, destPath: string, maxRedirects = 5): Promise<void> {
return new Promise((resolve, reject) => {
const protocol = url.startsWith('https') ? https : http;
if (!url.startsWith('https://')) {
reject(new Error(`Refusing non-HTTPS download: ${url}`));
return;
}
if (maxRedirects <= 0) {
reject(new Error('Too many redirects'));
return;
}
const file = fs.createWriteStream(destPath);
const request = protocol.get(url, { headers: { 'User-Agent': 'Mozilla/5.0' } }, (response) => {
const request = https.get(url, { headers: { 'User-Agent': 'Mozilla/5.0' } }, (response) => {
if (response.statusCode === 301 || response.statusCode === 302) {
const redirectUrl = response.headers.location;
if (redirectUrl) {
file.close();
fs.unlinkSync(destPath);
downloadFile(redirectUrl, destPath).then(resolve).catch(reject);
downloadFile(redirectUrl, destPath, maxRedirects - 1).then(resolve).catch(reject);
return;
}
}
@@ -155,7 +161,11 @@ function getImageExtension(urlOrPath: string): string {
}
async function resolveImagePath(imagePath: string, baseDir: string, tempDir: string): Promise<string> {
if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
if (imagePath.startsWith('http://')) {
console.error(`[md-to-html] Skipping non-HTTPS image: ${imagePath}`);
return '';
}
if (imagePath.startsWith('https://')) {
const hash = createHash('md5').update(imagePath).digest('hex').slice(0, 8);
const ext = getImageExtension(imagePath);
const localPath = path.join(tempDir, `remote_${hash}.${ext}`);