@xcanwin/manyoyo 5.2.5 → 5.2.9
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
package/lib/plugin/playwright.js
CHANGED
|
@@ -1339,7 +1339,7 @@ class PlaywrightPlugin {
|
|
|
1339
1339
|
const scenes = this.resolveTargets('all');
|
|
1340
1340
|
for (const sceneName of scenes) {
|
|
1341
1341
|
const url = `http://${host}:${this.scenePort(sceneName)}/mcp`;
|
|
1342
|
-
this.writeStdout(`claude mcp add --transport http playwright-${sceneName} ${url}`);
|
|
1342
|
+
this.writeStdout(`claude mcp add --transport http -s user playwright-${sceneName} ${url}`);
|
|
1343
1343
|
}
|
|
1344
1344
|
this.writeStdout('');
|
|
1345
1345
|
for (const sceneName of scenes) {
|
package/lib/web/frontend/app.js
CHANGED
|
@@ -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 += '>' +
|
|
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=
|
|
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=
|
|
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');
|