Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "web-access",
"description": "Complete web browsing and automation skill for Claude Code - intelligent tool selection, CDP browser automation, and site experience accumulation",
"version": "2.4.2",
"version": "2.5.0",
"author": {
"name": "一泽 Eze"
},
Expand Down
26 changes: 22 additions & 4 deletions scripts/cdp-proxy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,14 @@ async function readBody(req) {
return body;
}

// --- 写入路径白名单(防 /screenshot?file= 路径穿越) ---
function isSafeWritePath(p) {
if (typeof p !== 'string' || !p) return false;
const resolved = path.resolve(p);
const roots = [os.tmpdir(), process.cwd()].map(r => path.resolve(r));
return roots.some(root => resolved === root || resolved.startsWith(root + path.sep));
}

// --- HTTP API ---
const server = http.createServer(async (req, res) => {
const parsed = new URL(req.url, `http://localhost:${PORT}`);
Expand Down Expand Up @@ -538,14 +546,21 @@ const server = http.createServer(async (req, res) => {
// GET /screenshot?target=xxx&file=/tmp/x.png - 截图
else if (pathname === '/screenshot') {
const sid = await ensureSession(q.target);
const format = q.format || 'png';
const allowedFormats = new Set(['png', 'jpeg', 'webp']);
const format = allowedFormats.has(q.format) ? q.format : 'png';
const resp = await sendCDP('Page.captureScreenshot', {
format,
quality: format === 'jpeg' ? 80 : undefined,
}, sid);
if (q.file) {
fs.writeFileSync(q.file, Buffer.from(resp.result.data, 'base64'));
res.end(JSON.stringify({ saved: q.file }));
if (!isSafeWritePath(q.file)) {
res.statusCode = 400;
res.end(JSON.stringify({ error: 'file 必须位于系统临时目录或当前工作目录下' }));
return;
}
const dst = path.resolve(q.file);
fs.writeFileSync(dst, Buffer.from(resp.result.data, 'base64'));
res.end(JSON.stringify({ saved: dst }));
} else {
res.setHeader('Content-Type', 'image/' + format);
res.end(Buffer.from(resp.result.data, 'base64'));
Expand Down Expand Up @@ -607,7 +622,10 @@ async function main() {
http.get(`http://127.0.0.1:${PORT}/health`, { timeout: 2000 }, (res) => {
let d = '';
res.on('data', c => d += c);
res.on('end', () => resolve(d.includes('"ok"')));
res.on('end', () => {
try { resolve(JSON.parse(d).status === 'ok'); }
catch { resolve(false); }
});
}).on('error', () => resolve(false));
});
if (ok) {
Expand Down