@xcanwin/manyoyo 5.2.5 → 5.2.8

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/bin/manyoyo.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { execSync, spawnSync } = require('child_process');
3
+ const { spawnSync } = require('child_process');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const os = require('os');
@@ -1014,7 +1014,7 @@
1014
1014
 
1015
1015
  async function api(url, options) {
1016
1016
  const requestOptions = Object.assign(
1017
- { headers: { 'Content-Type': 'application/json' } },
1017
+ { headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } },
1018
1018
  options || {}
1019
1019
  );
1020
1020
  const response = await fetch(url, requestOptions);
@@ -62,15 +62,43 @@
62
62
  renderer.link = function (href, title, text) {
63
63
  const safeHref = sanitizeMarkdownUrl(href);
64
64
  if (!safeHref) {
65
- return text || '';
65
+ return escapeHtml(text || '');
66
66
  }
67
+ // [P1-02] 移除 marked 已渲染链接文本中的 on* 事件属性,防止内联 HTML 注入 XSS
68
+ const safeText = String(text || '').replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]*)/gi, '');
67
69
  let output = '<a href="' + escapeHtml(safeHref) + '" target="_blank" rel="noopener noreferrer"';
68
70
  if (title) {
69
71
  output += ' title="' + escapeHtml(title) + '"';
70
72
  }
71
- output += '>' + (text || '') + '</a>';
73
+ output += '>' + safeText + '</a>';
72
74
  return output;
73
75
  };
76
+ // [P1-01] 重写 image 渲染器:
77
+ // - 外部 http/https 图片转为可点击链接,避免浏览器自动发起外部请求(追踪像素风险)
78
+ // - 相对路径图片正常渲染为 <img>
79
+ // - 危险协议(javascript:/data: 等)降级为纯文本
80
+ renderer.image = function (href, title, text) {
81
+ const safeHref = sanitizeMarkdownUrl(href);
82
+ if (!safeHref) {
83
+ return escapeHtml(text || '');
84
+ }
85
+ // 外部绝对 URL:转为链接,用户主动决定是否访问
86
+ if (/^https?:/i.test(safeHref)) {
87
+ const safeText = String(text || '').replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]*)/gi, '')
88
+ || escapeHtml(safeHref);
89
+ let output = '<a href="' + escapeHtml(safeHref) + '" target="_blank" rel="noopener noreferrer"';
90
+ if (title) {
91
+ output += ' title="' + escapeHtml(title) + '"';
92
+ }
93
+ return output + '>[\uD83D\uDDBC\uFE0F点击查看图片:' + safeText + ']</a>';
94
+ }
95
+ // 相对路径:正常渲染为图片
96
+ let output = '<img src="' + escapeHtml(safeHref) + '" alt="' + escapeHtml(text || '') + '"';
97
+ if (title) {
98
+ output += ' title="' + escapeHtml(title) + '"';
99
+ }
100
+ return output + '>';
101
+ };
74
102
 
75
103
  markedApi.use({
76
104
  gfm: true,
package/lib/web/server.js CHANGED
@@ -414,11 +414,11 @@ function clearWebAuthSession(state, req) {
414
414
  }
415
415
 
416
416
  function getWebAuthCookie(sessionId) {
417
- return `${WEB_AUTH_COOKIE_NAME}=${encodeURIComponent(sessionId)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${WEB_AUTH_TTL_SECONDS}`;
417
+ return `${WEB_AUTH_COOKIE_NAME}=${encodeURIComponent(sessionId)}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${WEB_AUTH_TTL_SECONDS}`;
418
418
  }
419
419
 
420
420
  function getWebAuthClearCookie() {
421
- return `${WEB_AUTH_COOKIE_NAME}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
421
+ return `${WEB_AUTH_COOKIE_NAME}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0`;
422
422
  }
423
423
 
424
424
  function getDefaultWebConfigPath() {
@@ -1455,6 +1455,14 @@ function sendWebUnauthorized(res, pathname) {
1455
1455
  }
1456
1456
 
1457
1457
  async function handleWebApi(req, res, pathname, ctx, state) {
1458
+ // [P2-03] 对非只读请求校验自定义头,防止 CSRF 攻击
1459
+ // 跨站请求无法设置自定义头(浏览器同源策略),合法前端请求统一携带此头
1460
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
1461
+ if (req.headers['x-requested-with'] !== 'XMLHttpRequest') {
1462
+ sendJson(res, 403, { error: 'CSRF check failed' });
1463
+ return true;
1464
+ }
1465
+ }
1458
1466
  const routes = [
1459
1467
  {
1460
1468
  method: 'GET',
@@ -1853,6 +1861,30 @@ async function startWebServer(options) {
1853
1861
  return;
1854
1862
  }
1855
1863
 
1864
+ // [P1-03] Origin 校验,防止跨站 WebSocket 劫持(CSWSH)
1865
+ // 浏览器发起的 WebSocket 请求必须携带 Origin 头,非浏览器客户端(如 curl)不携带则放行
1866
+ const requestOrigin = req.headers.origin;
1867
+ if (requestOrigin) {
1868
+ const allowedOrigins = new Set();
1869
+ if (ctx.serverHost === '0.0.0.0') {
1870
+ // 0.0.0.0 监听时,以请求的 Host 头构造允许来源
1871
+ const hostHeader = req.headers.host || '';
1872
+ if (hostHeader) {
1873
+ allowedOrigins.add(`http://${hostHeader}`);
1874
+ allowedOrigins.add(`https://${hostHeader}`);
1875
+ }
1876
+ } else {
1877
+ allowedOrigins.add(`http://${formatUrlHost(ctx.serverHost)}:${listenPort}`);
1878
+ if (ctx.serverHost === '127.0.0.1') {
1879
+ allowedOrigins.add(`http://localhost:${listenPort}`);
1880
+ }
1881
+ }
1882
+ if (allowedOrigins.size > 0 && !allowedOrigins.has(requestOrigin)) {
1883
+ sendWebSocketUpgradeError(socket, 403, 'Forbidden');
1884
+ return;
1885
+ }
1886
+ }
1887
+
1856
1888
  const authSession = getWebAuthSession(state, req);
1857
1889
  if (!authSession) {
1858
1890
  sendWebSocketUpgradeError(socket, 401, 'UNAUTHORIZED');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "5.2.5",
3
+ "version": "5.2.8",
4
4
  "imageVersion": "1.8.1-common",
5
5
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",
6
6
  "keywords": [