Merge pull request #66 from luojiyin1987/fix/harden-command-exec-and-js-escaping

fix: harden command execution and JS literal escaping
This commit is contained in:
Jim Liu 宝玉
2026-03-08 22:10:45 -05:00
committed by GitHub
3 changed files with 105 additions and 57 deletions
@@ -1,10 +1,10 @@
import { execSync } from "child_process"; import { spawnSync } from "node:child_process";
import process from "node:process";
export function applyAutocorrect(filePath: string): boolean { export function applyAutocorrect(filePath: string): boolean {
try { const npxCmd = process.platform === "win32" ? "npx.cmd" : "npx";
execSync(`npx autocorrect-node --fix "${filePath}"`, { stdio: "inherit" }); const result = spawnSync(npxCmd, ["autocorrect-node", "--fix", filePath], {
return true; stdio: "inherit",
} catch { });
return false; return result.status === 0;
}
} }
@@ -1,6 +1,6 @@
import path from "node:path"; import path from "node:path";
import { readFile } from "node:fs/promises"; import { readFile } from "node:fs/promises";
import { execSync } from "node:child_process"; import { execFileSync } from "node:child_process";
import type { CliArgs } from "../types"; import type { CliArgs } from "../types";
const GOOGLE_MULTIMODAL_MODELS = [ const GOOGLE_MULTIMODAL_MODELS = [
@@ -76,14 +76,43 @@ async function postGoogleJsonViaCurl<T>(
): Promise<T> { ): Promise<T> {
const proxy = getHttpProxy(); const proxy = getHttpProxy();
const bodyStr = JSON.stringify(body); const bodyStr = JSON.stringify(body);
const proxyArgs = proxy ? `-x "${proxy}"` : ""; const args = [
"-s",
"--connect-timeout",
"30",
"--max-time",
"300",
...(proxy ? ["-x", proxy] : []),
url,
"-H",
"Content-Type: application/json",
"-H",
`x-goog-api-key: ${apiKey}`,
"-d",
"@-",
];
const result = execSync( let result = "";
`curl -s --connect-timeout 30 --max-time 300 ${proxyArgs} "${url}" -H "Content-Type: application/json" -H "x-goog-api-key: ${apiKey}" -d @-`, try {
{ input: bodyStr, maxBuffer: 100 * 1024 * 1024, timeout: 310000 }, result = execFileSync("curl", args, {
); input: bodyStr,
encoding: "utf8",
maxBuffer: 100 * 1024 * 1024,
timeout: 310000,
});
} catch (error) {
const e = error as { message?: string; stderr?: string | Buffer };
const stderrText =
typeof e.stderr === "string"
? e.stderr
: e.stderr
? e.stderr.toString("utf8")
: "";
const details = stderrText.trim() || e.message || "curl request failed";
throw new Error(`Google API request failed via curl: ${details}`);
}
const parsed = JSON.parse(result.toString()) as any; const parsed = JSON.parse(result) as any;
if (parsed.error) { if (parsed.error) {
throw new Error( throw new Error(
`Google API error (${parsed.error.code}): ${parsed.error.message}`, `Google API error (${parsed.error.code}): ${parsed.error.message}`,
@@ -1,4 +1,4 @@
import { execSync, spawnSync } from 'node:child_process'; import { spawnSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import process from 'node:process'; import process from 'node:process';
@@ -10,30 +10,42 @@ function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
} }
function ab(cmd: string, json = false): string { function quoteForLog(arg: string): string {
const fullCmd = `agent-browser --session ${SESSION} ${cmd}${json ? ' --json' : ''}`; return /[\s"'\\]/.test(arg) ? JSON.stringify(arg) : arg;
console.log(`[ab] ${fullCmd}`);
try {
const result = execSync(fullCmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
return result.trim();
} catch (e: unknown) {
const err = e as { stdout?: string; stderr?: string; message?: string };
console.error(`[ab] Error: ${err.stderr || err.message}`);
return err.stdout || '';
}
} }
function abRaw(args: string[]): { success: boolean; output: string } { function toSafeJsStringLiteral(value: string): string {
return JSON.stringify(value)
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029');
}
function runAgentBrowser(args: string[]): { success: boolean; output: string } {
const result = spawnSync('agent-browser', ['--session', SESSION, ...args], { const result = spawnSync('agent-browser', ['--session', SESSION, ...args], {
encoding: 'utf8', encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'] stdio: ['pipe', 'pipe', 'pipe']
}); });
const output = result.stdout || result.stderr || '';
return { return {
success: result.status === 0, success: result.status === 0,
output: result.stdout || result.stderr || '' output
}; };
} }
function ab(args: string[], json = false): string {
const fullArgs = json ? [...args, '--json'] : args;
console.log(`[ab] agent-browser --session ${SESSION} ${fullArgs.map(quoteForLog).join(' ')}`);
const result = runAgentBrowser(fullArgs);
if (!result.success) {
console.error(`[ab] Error: ${result.output.trim()}`);
}
return result.output.trim();
}
function abRaw(args: string[]): { success: boolean; output: string } {
return runAgentBrowser(args);
}
interface SnapshotElement { interface SnapshotElement {
ref: string; ref: string;
role: string; role: string;
@@ -99,17 +111,17 @@ async function postToWeChat(options: WeChatOptions): Promise<void> {
} }
console.log('[wechat] Opening WeChat Official Account...'); console.log('[wechat] Opening WeChat Official Account...');
ab(`open ${WECHAT_URL} --headed`); ab(['open', WECHAT_URL, '--headed']);
await sleep(5000); await sleep(5000);
console.log('[wechat] Checking login status...'); console.log('[wechat] Checking login status...');
let url = ab('get url'); let url = ab(['get', 'url']);
console.log(`[wechat] Current URL: ${url}`); console.log(`[wechat] Current URL: ${url}`);
const waitForLogin = async (timeoutMs = 120_000): Promise<boolean> => { const waitForLogin = async (timeoutMs = 120_000): Promise<boolean> => {
const start = Date.now(); const start = Date.now();
while (Date.now() - start < timeoutMs) { while (Date.now() - start < timeoutMs) {
url = ab('get url'); url = ab(['get', 'url']);
if (url.includes('/cgi-bin/home')) return true; if (url.includes('/cgi-bin/home')) return true;
console.log('[wechat] Waiting for login...'); console.log('[wechat] Waiting for login...');
await sleep(3000); await sleep(3000);
@@ -126,7 +138,7 @@ async function postToWeChat(options: WeChatOptions): Promise<void> {
await sleep(2000); await sleep(2000);
console.log('[wechat] Getting page snapshot...'); console.log('[wechat] Getting page snapshot...');
let snapshot = ab('snapshot'); let snapshot = ab(['snapshot']);
console.log(snapshot); console.log(snapshot);
console.log('[wechat] Looking for "图文" menu...'); console.log('[wechat] Looking for "图文" menu...');
@@ -134,16 +146,16 @@ async function postToWeChat(options: WeChatOptions): Promise<void> {
if (!tuWenRef) { if (!tuWenRef) {
console.log('[wechat] Using eval to find and click menu...'); console.log('[wechat] Using eval to find and click menu...');
ab(`eval "document.querySelectorAll('.new-creation__menu .new-creation__menu-item')[2].click()"`); ab(['eval', "document.querySelectorAll('.new-creation__menu .new-creation__menu-item')[2].click()"]);
} else { } else {
console.log(`[wechat] Clicking menu ref: ${tuWenRef}`); console.log(`[wechat] Clicking menu ref: ${tuWenRef}`);
ab(`click ${tuWenRef}`); ab(['click', tuWenRef]);
} }
await sleep(4000); await sleep(4000);
console.log('[wechat] Checking for new tab...'); console.log('[wechat] Checking for new tab...');
const tabsOutput = ab('tab'); const tabsOutput = ab(['tab']);
console.log(`[wechat] Tabs: ${tabsOutput}`); console.log(`[wechat] Tabs: ${tabsOutput}`);
const tabLines = tabsOutput.split('\n'); const tabLines = tabsOutput.split('\n');
@@ -153,14 +165,14 @@ async function postToWeChat(options: WeChatOptions): Promise<void> {
const tabMatch = tabsOutput.match(/\[(\d+)\].*(?:appmsg|edit)/i); const tabMatch = tabsOutput.match(/\[(\d+)\].*(?:appmsg|edit)/i);
if (tabMatch) { if (tabMatch) {
console.log(`[wechat] Switching to editor tab ${tabMatch[1]}...`); console.log(`[wechat] Switching to editor tab ${tabMatch[1]}...`);
ab(`tab ${tabMatch[1]}`); ab(['tab', tabMatch[1]]);
} else { } else {
const lastTabMatch = tabsOutput.match(/\[(\d+)\]/g); const lastTabMatch = tabsOutput.match(/\[(\d+)\]/g);
if (lastTabMatch && lastTabMatch.length > 1) { if (lastTabMatch && lastTabMatch.length > 1) {
const lastTab = lastTabMatch[lastTabMatch.length - 1].match(/\d+/)?.[0]; const lastTab = lastTabMatch[lastTabMatch.length - 1].match(/\d+/)?.[0];
if (lastTab) { if (lastTab) {
console.log(`[wechat] Switching to last tab ${lastTab}...`); console.log(`[wechat] Switching to last tab ${lastTab}...`);
ab(`tab ${lastTab}`); ab(['tab', lastTab]);
} }
} }
} }
@@ -168,38 +180,44 @@ async function postToWeChat(options: WeChatOptions): Promise<void> {
await sleep(3000); await sleep(3000);
url = ab('get url'); url = ab(['get', 'url']);
console.log(`[wechat] Editor URL: ${url}`); console.log(`[wechat] Editor URL: ${url}`);
console.log('[wechat] Getting editor snapshot...'); console.log('[wechat] Getting editor snapshot...');
snapshot = ab('snapshot'); snapshot = ab(['snapshot']);
console.log(snapshot.substring(0, 2000)); console.log(snapshot.substring(0, 2000));
console.log('[wechat] Uploading images...'); console.log('[wechat] Uploading images...');
const fileInputSelector = '.js_upload_btn_container input[type=file]'; const fileInputSelector = '.js_upload_btn_container input[type=file]';
const fileInputSelectorJs = toSafeJsStringLiteral(fileInputSelector);
ab(`eval "document.querySelector('${fileInputSelector}').style.display = 'block'"`); ab(['eval', `{
const input = document.querySelector(${fileInputSelectorJs});
if (input) input.style.display = 'block';
}`]);
await sleep(500); await sleep(500);
const uploadResult = abRaw(['upload', `"${fileInputSelector}"`, ...absoluteImages]); const uploadResult = abRaw(['upload', fileInputSelector, ...absoluteImages]);
console.log(`[wechat] Upload result: ${uploadResult.output}`); console.log(`[wechat] Upload result: ${uploadResult.output}`);
if (!uploadResult.success) { if (!uploadResult.success) {
console.log('[wechat] Using alternative upload method...'); console.log('[wechat] Using alternative upload method...');
for (const img of absoluteImages) { for (const img of absoluteImages) {
console.log(`[wechat] Uploading: ${img}`); console.log(`[wechat] Uploading: ${img}`);
ab(`eval " const imgUrlJs = toSafeJsStringLiteral(`file://${img}`);
const input = document.querySelector('${fileInputSelector}'); const imgFileNameJs = toSafeJsStringLiteral(path.basename(img));
ab(['eval', `
const input = document.querySelector(${fileInputSelectorJs});
if (input) { if (input) {
const dt = new DataTransfer(); const dt = new DataTransfer();
fetch('file://${img}').then(r => r.blob()).then(b => { fetch(${imgUrlJs}).then(r => r.blob()).then(b => {
const file = new File([b], '${path.basename(img)}', { type: 'image/png' }); const file = new File([b], ${imgFileNameJs}, { type: 'image/png' });
dt.items.add(file); dt.items.add(file);
input.files = dt.files; input.files = dt.files;
input.dispatchEvent(new Event('change', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true }));
}); });
} }
"`); `]);
await sleep(2000); await sleep(2000);
} }
} }
@@ -208,13 +226,14 @@ async function postToWeChat(options: WeChatOptions): Promise<void> {
await sleep(10000); await sleep(10000);
console.log('[wechat] Filling title...'); console.log('[wechat] Filling title...');
snapshot = ab('snapshot -i'); snapshot = ab(['snapshot', '-i']);
const titleRef = findElementByText(snapshot, 'title') || findElementByText(snapshot, '标题'); const titleRef = findElementByText(snapshot, 'title') || findElementByText(snapshot, '标题');
if (titleRef) { if (titleRef) {
ab(`fill ${titleRef} "${title.replace(/"/g, '\\"')}"`); ab(['fill', titleRef, title]);
} else { } else {
ab(`eval "const t = document.querySelector('#title'); if(t) { t.value = '${title.replace(/'/g, "\\'")}'; t.dispatchEvent(new Event('input', {bubbles: true})); }"`); const titleJs = toSafeJsStringLiteral(title);
ab(['eval', `const t = document.querySelector('#title'); if(t) { t.value = ${titleJs}; t.dispatchEvent(new Event('input', {bubbles: true})); }`]);
} }
await sleep(500); await sleep(500);
@@ -222,9 +241,9 @@ async function postToWeChat(options: WeChatOptions): Promise<void> {
const editorRef = findElementByText(snapshot, 'js_pmEditorArea') || findElementByText(snapshot, 'textbox'); const editorRef = findElementByText(snapshot, 'js_pmEditorArea') || findElementByText(snapshot, 'textbox');
if (editorRef) { if (editorRef) {
ab(`click ${editorRef}`); ab(['click', editorRef]);
} else { } else {
ab(`eval "document.querySelector('.js_pmEditorArea')?.click()"`); ab(['eval', "document.querySelector('.js_pmEditorArea')?.click()"]);
} }
await sleep(500); await sleep(500);
@@ -233,11 +252,11 @@ async function postToWeChat(options: WeChatOptions): Promise<void> {
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const line = lines[i]; const line = lines[i];
if (line.length > 0) { if (line.length > 0) {
const escapedLine = line.replace(/"/g, '\\"').replace(/'/g, "\\'"); const lineJs = toSafeJsStringLiteral(line);
ab(`eval "document.execCommand('insertText', false, '${escapedLine}')"`); ab(['eval', `document.execCommand('insertText', false, ${lineJs})`]);
} }
if (i < lines.length - 1) { if (i < lines.length - 1) {
ab('press Enter'); ab(['press', 'Enter']);
} }
await sleep(100); await sleep(100);
} }
@@ -249,9 +268,9 @@ async function postToWeChat(options: WeChatOptions): Promise<void> {
console.log('[wechat] Saving as draft...'); console.log('[wechat] Saving as draft...');
const submitRef = findElementByText(snapshot, 'js_submit') || findElementByText(snapshot, '保存'); const submitRef = findElementByText(snapshot, 'js_submit') || findElementByText(snapshot, '保存');
if (submitRef) { if (submitRef) {
ab(`click ${submitRef}`); ab(['click', submitRef]);
} else { } else {
ab(`eval "document.querySelector('#js_submit')?.click()"`); ab(['eval', "document.querySelector('#js_submit')?.click()"]);
} }
await sleep(3000); await sleep(3000);
console.log('[wechat] Draft saved!'); console.log('[wechat] Draft saved!');
@@ -261,7 +280,7 @@ async function postToWeChat(options: WeChatOptions): Promise<void> {
if (!keepOpen) { if (!keepOpen) {
console.log('[wechat] Closing browser...'); console.log('[wechat] Closing browser...');
ab('close'); ab(['close']);
} else { } else {
console.log('[wechat] Done. Browser window left open.'); console.log('[wechat] Done. Browser window left open.');
} }