Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
162d588
Merge pull request #772 from XMOJ-Script-dev/dev
boomzero Feb 8, 2025
d724438
Merge pull request #781 from XMOJ-Script-dev/dev
boomzero Feb 21, 2025
8b4a2c1
Merge pull request #786 from XMOJ-Script-dev/dev
boomzero Feb 23, 2025
e40b292
Merge pull request #800 from XMOJ-Script-dev/dev
boomzero May 4, 2025
c8ed5aa
Merge pull request #804 from XMOJ-Script-dev/dev
boomzero Jun 4, 2025
e4f659f
Merge pull request #813 from XMOJ-Script-dev/dev
boomzero Jun 22, 2025
d24fe86
Merge pull request #819 from XMOJ-Script-dev/dev
boomzero Jul 10, 2025
1a75572
Merge pull request #823 from XMOJ-Script-dev/dev
boomzero Jul 25, 2025
b789bf2
Update Update.json
boomzero Jul 25, 2025
13058b6
Merge pull request #827 from XMOJ-Script-dev/dev
boomzero Aug 9, 2025
c22fd31
Merge pull request #836 from XMOJ-Script-dev/dev
boomzero Aug 22, 2025
6adac8d
Parse release notes from comment block
boomzero Aug 24, 2025
f86a45e
Update bug.yml
boomzero Aug 24, 2025
84474cc
Merge pull request #854 from XMOJ-Script-dev/dev
boomzero Sep 14, 2025
7950ae8
Merge pull request #857 from XMOJ-Script-dev/dev
boomzero Sep 27, 2025
dfe8cf8
Merge pull request #874 from XMOJ-Script-dev/dev
boomzero Oct 4, 2025
54d39cf
Update GitHub Actions workflow to skip bot triggers
PythonSmall-Q Feb 7, 2026
462621a
Merge pull request #891 from XMOJ-Script-dev/dev
PythonSmall-Q Feb 7, 2026
27863a7
Prevent UpdateVersion from running if last commit was by github-actio…
boomzero Feb 11, 2026
280bc24
Merge branch 'master' of github.com:XMOJ-Script-dev/XMOJ-Script
boomzero Feb 11, 2026
4a671fc
Merge pull request #908 from XMOJ-Script-dev/dev
boomzero Feb 16, 2026
e3bbe9e
Merge pull request #912 from XMOJ-Script-dev/dev
boomzero Feb 19, 2026
fab6593
Allow metadata updates on edited PRs after bot version commit
boomzero Feb 23, 2026
2f7e9d6
Allow metadata updates on edited PRs after bot version commit
boomzero Feb 23, 2026
e14e33c
Merge pull request #921 from XMOJ-Script-dev/dev
boomzero Feb 27, 2026
636b037
feat: add experimental webui for xmoj short messages
boomzero Mar 15, 2026
b7c77e6
fix: add proxy mode for webui login and api calls
boomzero Mar 15, 2026
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
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@
请参考 [官网介绍](https://www.xmoj-bbs.me) 。
如果您无法打开该网站,请前往[这里](https://scriptcat.org/zh-CN/script-show-page/1500/)安装。

### 实验功能:短消息 WebUI

仓库中新增了一个实验页面 `webui.html`,用于在不安装用户脚本时访问短消息相关能力(拉取会话、查看消息、发送消息、粘贴上传图片)。

- 推荐启动代理模式:`node tools/webui-proxy.mjs`,然后访问 `http://127.0.0.1:8787/webui.html`。
- 代理模式下支持直接输入用户名/密码登录并自动获取 `PHPSESSID`;同时也支持手动粘贴 `PHPSESSID`。
- 直连模式(直接打开页面)受浏览器跨域限制,账号密码登录可能不可用,此时建议改用代理模式。
Copy link

@cubic-dev-ai cubic-dev-ai bot Mar 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This line understates the limitation: direct mode username/password login is not merely unstable, it is unsupported by the current implementation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At README.md, line 80:

<comment>This line understates the limitation: direct mode username/password login is not merely unstable, it is unsupported by the current implementation.</comment>

<file context>
@@ -75,8 +75,10 @@
-- 安全策略:密码仅用于发起登录请求,不会保存;会话信息仅存储在当前标签页的 `sessionStorage`,可一键清除。
+- 推荐启动代理模式:`node tools/webui-proxy.mjs`,然后访问 `http://127.0.0.1:8787/webui.html`。
+- 代理模式下支持直接输入用户名/密码登录并自动获取 `PHPSESSID`;同时也支持手动粘贴 `PHPSESSID`。
+- 直连模式(直接打开页面)受浏览器跨域限制,账号密码登录可能不可用,此时建议改用代理模式。
+- 安全策略:密码仅用于发起登录请求,不会在前端存储;会话信息仅存储在当前标签页的 `sessionStorage`,可一键清除。
 - 维护策略:API 调用格式与现有脚本中的 `RequestAPI` 保持一致,便于后续复用和统一维护。
</file context>
Suggested change
- 直连模式(直接打开页面)受浏览器跨域限制,账号密码登录可能不可用,此时建议改用代理模式
- 直连模式(直接打开页面)受浏览器跨域限制,账号密码登录不可用,此时请手动填写 `PHPSESSID`,或改用代理模式
Fix with Cubic

- 安全策略:密码仅用于发起登录请求,不会在前端存储;会话信息仅存储在当前标签页的 `sessionStorage`,可一键清除。
- 维护策略:API 调用格式与现有脚本中的 `RequestAPI` 保持一致,便于后续复用和统一维护。

### 贡献
您想为我们的脚本添砖加瓦吗?快加入我们,为小明的OJ用户创造更美好的环境!(具体要求参见Code Of Conduct)

Expand Down Expand Up @@ -105,4 +115,3 @@
<!-- ALL-CONTRIBUTORS-LIST:END -->



4 changes: 2 additions & 2 deletions Update.json
Original file line number Diff line number Diff line change
Expand Up @@ -2937,7 +2937,7 @@
],
"Notes": "No release notes were provided for this release."
},
"1.10.0": {
"1.999990.0": {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wtf is this

"UpdateDate": 1753443146018,
"Prerelease": false,
"UpdateContents": [
Expand Down Expand Up @@ -3424,4 +3424,4 @@
"Notes": "<b>Bug 修复</b><br>\n- 修复了暗色模式下比赛排名表(contestrank-oi.php 和 contestrank-correct.php)颜色显示异常的问题(#916)<br>\n- 修复了 WebSocket 弹窗通知未遵循各功能独立弹窗开关(BBSPopup/MessagePopup)的问题(#919)"
}
}
}
}
3 changes: 3 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
<li class="nav-item">
<a class="nav-link" href="#Install">安装</a>
</li>
<li class="nav-item">
<a class="nav-link" href="webui.html">短消息 WebUI(实验)</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#Feedback">反馈</a>
</li>
Expand Down
146 changes: 146 additions & 0 deletions tools/webui-proxy.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#!/usr/bin/env node
import { createServer } from 'node:http';
import { readFile } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import { extname, join, normalize } from 'node:path';

const HOST = process.env.HOST || '0.0.0.0';
Copy link

@cubic-dev-ai cubic-dev-ai bot Mar 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Default this proxy to loopback. Binding to 0.0.0.0 exposes the served working tree to the LAN by default.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tools/webui-proxy.mjs, line 7:

<comment>Default this proxy to loopback. Binding to `0.0.0.0` exposes the served working tree to the LAN by default.</comment>

<file context>
@@ -0,0 +1,146 @@
+import { createHash } from 'node:crypto';
+import { extname, join, normalize } from 'node:path';
+
+const HOST = process.env.HOST || '0.0.0.0';
+const PORT = Number(process.env.PORT || 8787);
+const ROOT = process.cwd();
</file context>
Fix with Cubic

const PORT = Number(process.env.PORT || 8787);
const ROOT = process.cwd();
Copy link

@cubic-dev-ai cubic-dev-ai bot Mar 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Resolve static files from the script/repository directory instead of process.cwd(), or the proxy serves the wrong tree when launched from another directory.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tools/webui-proxy.mjs, line 9:

<comment>Resolve static files from the script/repository directory instead of `process.cwd()`, or the proxy serves the wrong tree when launched from another directory.</comment>

<file context>
@@ -0,0 +1,146 @@
+
+const HOST = process.env.HOST || '0.0.0.0';
+const PORT = Number(process.env.PORT || 8787);
+const ROOT = process.cwd();
+
+const MIME = {
</file context>
Fix with Cubic


const MIME = {
'.html': 'text/html; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.mjs': 'application/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon'
};

const MAX_BODY = 15 * 1024 * 1024;

function json(res, code, payload) {
res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify(payload));
}

function parseCookie(setCookie = '') {
const match = setCookie.match(/PHPSESSID=([^;]+)/i);
return match ? match[1] : '';
}

function md5(input) {
return createHash('md5').update(input).digest('hex');

Check failure

Code scanning / CodeQL

Use of password hash with insufficient computational effort High

Password from
an access to password
is hashed insecurely.
Password from
an access to password
is hashed insecurely.

Copilot Autofix

AI 11 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

}

async function readBody(req) {
const chunks = [];
let size = 0;
for await (const chunk of req) {
size += chunk.length;
if (size > MAX_BODY) throw new Error('请求体过大');
chunks.push(chunk);
}
if (chunks.length === 0) return {};
const raw = Buffer.concat(chunks).toString('utf8');
return raw ? JSON.parse(raw) : {};
}

async function handleLogin(req, res) {
const body = await readBody(req);
const username = String(body.username || '').trim();
const password = String(body.password || '');
if (!username || !password) {
return json(res, 400, { success: false, message: '用户名或密码不能为空。' });
}

const form = new URLSearchParams();
form.set('user_id', username);
form.set('password', md5(password));

const response = await fetch('https://www.xmoj.tech/login.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: form.toString(),
redirect: 'manual'
});

const text = await response.text();
const setCookie = response.headers.get('set-cookie') || '';
const sessionId = parseCookie(setCookie);

if (!sessionId) {
const maybeSuccess = text.includes('history.go(-2);');
return json(res, 401, {
success: false,
message: maybeSuccess ? '登录结果无法解析,请重试。' : '用户名或密码错误。'
});
}

return json(res, 200, {
success: true,
username,
sessionId
});
}

async function handleApiProxy(req, res, action) {
const body = await readBody(req);
const response = await fetch(`https://api.xmoj-bbs.me/${action}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
Comment on lines +93 to +97

Check failure

Code scanning / CodeQL

Server-side request forgery Critical

The
URL
of this request depends on a
user-provided value
.

Copilot Autofix

AI 11 days ago

In general, to fix this kind of issue you should never let raw user input determine the full path of an internal/backend request. Instead, map user inputs to a controlled set of allowed actions or validate them against a strict pattern, and then build the backend path from that sanitized value. This ensures that a client cannot cause the server to query unexpected endpoints, even if the hostname is fixed.

In this specific file, the problem is that action is directly derived from the path /__webui/api/<action> and then used verbatim in fetch(`https://api.xmoj-bbs.me/${action}`, ...). The safest fix without changing visible behavior too much is:

  • Introduce a small validation/normalization function (inside this file) that:
    • Rejects any action containing path traversal (..), backslashes, or leading /.
    • Optionally constrains it to a simple character set (e.g., letters, digits, /_-).
  • Call this validator both at the routing point (to fail fast) and inside handleApiProxy (defense in depth).
  • If validation fails, return a 400 error with a simple JSON error message instead of performing the fetch.
  • Keep the scheme and hostname unchanged and still allow multi-segment paths like foo/bar if needed by the existing API.

Concretely:

  • Add a helper function sanitizeAction(action) above handleApiProxy.
  • In createServer where action is extracted (around lines 135–137), pass action through sanitizeAction; if it returns null, respond with 400.
  • In handleApiProxy, re-sanitize action and reject if invalid, then use the sanitized value to construct the URL.

No new imports are needed; we can implement this logic with plain JavaScript.

Suggested changeset 1
tools/webui-proxy.mjs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tools/webui-proxy.mjs b/tools/webui-proxy.mjs
--- a/tools/webui-proxy.mjs
+++ b/tools/webui-proxy.mjs
@@ -88,9 +88,22 @@
   });
 }
 
+function sanitizeAction(action) {
+  if (typeof action !== 'string') return null;
+  // Disallow absolute paths or path traversal
+  if (action.startsWith('/') || action.includes('..') || action.includes('\\')) return null;
+  // Allow only safe characters: letters, digits, underscore, hyphen, slash
+  if (!/^[A-Za-z0-9_\/-]+$/.test(action)) return null;
+  return action;
+}
+
 async function handleApiProxy(req, res, action) {
+  const safeAction = sanitizeAction(action);
+  if (!safeAction) {
+    return json(res, 400, { success: false, message: '非法 action' });
+  }
   const body = await readBody(req);
-  const response = await fetch(`https://api.xmoj-bbs.me/${action}`, {
+  const response = await fetch(`https://api.xmoj-bbs.me/${safeAction}`, {
     method: 'POST',
     headers: { 'Content-Type': 'application/json' },
     body: JSON.stringify(body)
@@ -134,7 +145,9 @@
     if (pathname.startsWith('/__webui/api/') && req.method === 'POST') {
       const action = pathname.substring('/__webui/api/'.length);
       if (!action) return json(res, 400, { success: false, message: '缺少 action' });
-      return await handleApiProxy(req, res, action);
+      const safeAction = sanitizeAction(action);
+      if (!safeAction) return json(res, 400, { success: false, message: '非法 action' });
+      return await handleApiProxy(req, res, safeAction);
     }
 
     return await serveStatic(req, res, pathname);
EOF
@@ -88,9 +88,22 @@
});
}

function sanitizeAction(action) {
if (typeof action !== 'string') return null;
// Disallow absolute paths or path traversal
if (action.startsWith('/') || action.includes('..') || action.includes('\\')) return null;
// Allow only safe characters: letters, digits, underscore, hyphen, slash
if (!/^[A-Za-z0-9_\/-]+$/.test(action)) return null;
return action;
}

async function handleApiProxy(req, res, action) {
const safeAction = sanitizeAction(action);
if (!safeAction) {
return json(res, 400, { success: false, message: '非法 action' });
}
const body = await readBody(req);
const response = await fetch(`https://api.xmoj-bbs.me/${action}`, {
const response = await fetch(`https://api.xmoj-bbs.me/${safeAction}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
@@ -134,7 +145,9 @@
if (pathname.startsWith('/__webui/api/') && req.method === 'POST') {
const action = pathname.substring('/__webui/api/'.length);
if (!action) return json(res, 400, { success: false, message: '缺少 action' });
return await handleApiProxy(req, res, action);
const safeAction = sanitizeAction(action);
if (!safeAction) return json(res, 400, { success: false, message: '非法 action' });
return await handleApiProxy(req, res, safeAction);
}

return await serveStatic(req, res, pathname);
Copilot is powered by AI and may make mistakes. Always verify output.
const text = await response.text();
res.writeHead(response.status, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(text);
}

async function serveStatic(req, res, pathname) {
const clean = normalize(pathname).replace(/^\/+/, '');
const file = clean === '' ? 'index.html' : clean;
if (file.includes('..')) {
res.writeHead(403);
res.end('Forbidden');
return;
}
const fullPath = join(ROOT, file);
try {
const data = await readFile(fullPath);
const type = MIME[extname(fullPath)] || 'application/octet-stream';
res.writeHead(200, { 'Content-Type': type });
res.end(data);
} catch {
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Not Found');
}
}

createServer(async (req, res) => {
try {
const url = new URL(req.url || '/', `http://${req.headers.host}`);
const pathname = url.pathname;

if (pathname === '/__webui/health') {
return json(res, 200, { success: true, mode: 'proxy' });
}
if (pathname === '/__webui/login' && req.method === 'POST') {
return await handleLogin(req, res);
}
if (pathname.startsWith('/__webui/api/') && req.method === 'POST') {
const action = pathname.substring('/__webui/api/'.length);
if (!action) return json(res, 400, { success: false, message: '缺少 action' });
return await handleApiProxy(req, res, action);
}

return await serveStatic(req, res, pathname);
} catch (error) {
return json(res, 500, { success: false, message: error.message || 'Internal error' });
}
}).listen(PORT, HOST, () => {
console.log(`[webui-proxy] http://${HOST}:${PORT}`);
});
Loading
Loading