fix: 在提交界面显示并提交验证码 - #1019
Conversation
评测队列繁忙时 XMOJ 会启用 vcode.php 图片验证码,但提交界面是脚本自己 渲染的,服务端的验证码字段在替换 DOM 时被丢弃,提交因此静默失败。 - 替换页面前先检测服务端是否渲染了验证码字段,若有则在提交按钮上方 显示验证码图片与输入框,点击图片可更换 - 提交时附带 vcode 参数;队列不繁忙时 submit.php 会忽略该字段,因此 无条件携带是安全的 - submitpage.php 在队列 >10 时显示验证码,submit.php 在 >50 时才校验, 两者可能不一致:识别到"验证码错误"时展开验证码区域并允许重新提交 - 验证码为空时在客户端拦截。提交空验证码会让服务端标记 vfail,此后 验证码会从 4 位数字变成 8 位字母数字,直到会话结束 - 新增 AutoCaptcha 开关,通过 captchaSolve 自动识别 4 位数字验证码; 识别结果非 4 位数字时不填入,8 位字母验证码不送识别,均可手动填写 Closes #420 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reviewer's GuideImplements support for XMOJ’s native vcode.php captcha on the custom submit page, adds an AutoCaptcha feature that can auto-recognize 4‑digit captchas via an external solver, and ensures vcode is sent and error-handled correctly on all submit paths so submissions do not silently fail when the judge queue is busy. Sequence diagram for captcha-aware code submissionsequenceDiagram
actor User
participant SubmitPage
participant VCode as vcode.php
participant Solver as CaptchaSolver
participant Submit as submit.php
SubmitPage->>SubmitPage: Detect native vcode field
SubmitPage->>VCode: fetch captcha image
VCode-->>SubmitPage: Return one captcha image
SubmitPage->>SubmitPage: Render image and input
opt AutoCaptcha enabled and image is 4 digits
SubmitPage->>Solver: RequestCaptchaSolver(ImageBlob)
Solver-->>SubmitPage: Return four-digit answer
SubmitPage->>SubmitPage: Fill vcode when input is empty
end
User->>SubmitPage: Enter captcha and click Submit
SubmitPage->>SubmitPage: GetCaptchaParameter()
SubmitPage->>Submit: POST submission with vcode
alt Captcha accepted
Submit-->>SubmitPage: Redirect or submission result
else 验证码错误
Submit-->>SubmitPage: Return captcha error
SubmitPage->>VCode: fetch replacement captcha
VCode-->>SubmitPage: Return new captcha image
SubmitPage->>SubmitPage: Re-enable Submit and request retry
end
Flow diagram for captcha detection and recoveryflowchart TD
A[Render custom submit page] --> B{Native captcha shown?}
B -- No --> C[Show Submit button]
B -- Yes --> D[Download one captcha image]
D --> E{AutoCaptcha enabled and image is 4 digits?}
E -- Yes --> F[RequestCaptchaSolver]
F --> G{Exactly four digits returned?}
G -- Yes --> H[Fill vcode if input is empty]
G -- No --> I[Keep manual entry enabled]
E -- No --> I
H --> J[User submits with vcode]
I --> J
J --> K{submit.php response}
K -- Success --> L[Redirect or show result]
K -- 验证码错误 --> M[RefreshCaptcha and re-enable Submit]
C --> N[Submit without captcha when not displayed]
N --> K
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Deploying xmoj-script-dev-channel with
|
| Latest commit: |
029c492
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://a75f24c4.xmoj-script-dev-channel.pages.dev |
| Branch Preview URL: | https://fix-submit-captcha-420.xmoj-script-dev-channel.pages.dev |
There was a problem hiding this comment.
Hey - I've found 1 issue
Fixed security issues:
- Cross-site scripting (XSS) via untrusted HTML/JS injection in web rendering sinks (link)
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="XMOJ.user.js" line_range="4275-4276" />
<code_context>
+ if (UtilityEnabled("DebugMode")) {
+ console.log("Captcha solver returned:", SolverText);
+ }
+ const Digits = SolverText.replace(/\D/g, "");
+ return /^\d{4}$/.test(Digits) ? Digits : null;
+ };
+ const RefreshCaptcha = async (StatusMessage) => {
</code_context>
<issue_to_address>
**issue (bug_risk):** `SolveCaptcha` removes every non-digit character before validating the result, so a solver response that is not exactly four digits, such as `The answer is 1234`, is accepted and autofilled even though the stated contract requires non-exact responses to be discarded.
**Triggers:** When the solver returns prose or other text containing exactly four digits.
**Suggested fix:** Validate the raw trimmed response with `/^\d{4}$/` instead of stripping non-digit characters first.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 1 finding to address first, and this changes CAPTCHA enforcement handling and sends each challenge image to a third-party solver, so a faulty or compromised solver could interfere with the submission gate or expose challenge data. Reverting prevents future effects, but images already sent externally and any submissions accepted or rejected while it was active cannot be fully undone.
Blocking findings: XMOJ.user.js:4276
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| const Digits = SolverText.replace(/\D/g, ""); | ||
| return /^\d{4}$/.test(Digits) ? Digits : null; |
There was a problem hiding this comment.
issue (bug_risk): SolveCaptcha removes every non-digit character before validating the result, so a solver response that is not exactly four digits, such as The answer is 1234, is accepted and autofilled even though the stated contract requires non-exact responses to be discarded.
Triggers: When the solver returns prose or other text containing exactly four digits.
Suggested fix: Validate the raw trimmed response with /^\d{4}$/ instead of stripping non-digit characters first.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: beded4ad66
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const RefreshCaptcha = async (StatusMessage) => { | ||
| const RequestID = ++CaptchaRequestID; |
There was a problem hiding this comment.
Serialize captcha refresh requests
When a user clicks the captcha image twice before the first vcode.php fetch finishes, both requests can mutate the PHP session concurrently. CaptchaRequestID prevents the older response from updating the UI, but it cannot prevent that older server request from completing last and replacing the expected answer; the page then displays the newer image while the session expects the discarded one, so a correct submission is rejected and can escalate the session to the harder captcha. Serialize refreshes or otherwise ensure only the challenge that completes last on the server is displayed.
Useful? React with 👍 / 👎.
| // disagree; see GetCaptchaParameter below for how that gap is handled. | ||
| // Recognises the 4 digit variant only: https://github.com/boomzero/captchaSolve | ||
| // workers.dev is unreachable from mainland China, so this has to stay on a custom domain. | ||
| const CaptchaSolverURL = "https://captcha.xmoj-script.uk/"; |
There was a problem hiding this comment.
Provision the solver before enabling AutoCaptcha
This commit's description explicitly states that captcha.xmoj-script.uk does not exist yet, while AutoCaptcha is default-enabled because UtilityEnabled turns on every setting not listed in defaultOffItems. Consequently every affected submit page attempts an endpoint that cannot succeed, waits for its failure or 15-second timeout, and always falls back to manual entry despite advertising automatic recognition. Provision a working endpoint before enabling the setting by default, or keep the feature disabled until it is available.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
2 issues found and verified against the latest diff
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="XMOJ.user.js">
<violation number="1" location="XMOJ.user.js:4286">
P2: When users click the captcha image again before the previous refresh finishes, concurrent `vcode.php` requests can leave the displayed image and PHP-session answer out of sync. Serialize refreshes or disable the image until the current refresh completes.</violation>
<violation number="2" location="XMOJ.user.js:4428">
P2: When an ended-contest fallback submission is rejected with `验证码错误`, the callback only logs the response and leaves the captcha state unrecovered. Handle this response like the primary submission by refreshing the captcha and re-enabling retry.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| SetCaptchaStatus(StatusMessage || ""); | ||
| let ImageBlob; | ||
| try { | ||
| const CaptchaResponse = await fetch("https://www.xmoj.tech/vcode.php?" + Math.random(), {cache: "no-store"}); |
There was a problem hiding this comment.
P2: When users click the captcha image again before the previous refresh finishes, concurrent vcode.php requests can leave the displayed image and PHP-session answer out of sync. Serialize refreshes or disable the image until the current refresh completes.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At XMOJ.user.js, line 4286:
<comment>When users click the captcha image again before the previous refresh finishes, concurrent `vcode.php` requests can leave the displayed image and PHP-session answer out of sync. Serialize refreshes or disable the image until the current refresh completes.</comment>
<file context>
@@ -4210,6 +4232,111 @@ async function main() {
+ SetCaptchaStatus(StatusMessage || "");
+ let ImageBlob;
+ try {
+ const CaptchaResponse = await fetch("https://www.xmoj.tech/vcode.php?" + Math.random(), {cache: "no-store"});
+ ImageBlob = await CaptchaResponse.blob();
+ } catch (e) {
</file context>
| "referrer": location.href, | ||
| "method": "POST", | ||
| "body": "id=" + rPID + "&language=1&" + "source=" + encodeURIComponent(CodeMirrorElement.getValue()) + o2Switch | ||
| "body": "id=" + rPID + "&language=1&" + "source=" + encodeURIComponent(CodeMirrorElement.getValue()) + o2Switch + GetCaptchaParameter() |
There was a problem hiding this comment.
P2: When an ended-contest fallback submission is rejected with 验证码错误, the callback only logs the response and leaves the captcha state unrecovered. Handle this response like the primary submission by refreshing the captcha and re-enabling retry.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At XMOJ.user.js, line 4428:
<comment>When an ended-contest fallback submission is rejected with `验证码错误`, the callback only logs the response and leaves the captcha state unrecovered. Handle this response like the primary submission by refreshing the captcha and re-enabling retry.</comment>
<file context>
@@ -4277,7 +4425,7 @@ async function main() {
"referrer": location.href,
"method": "POST",
- "body": "id=" + rPID + "&language=1&" + "source=" + encodeURIComponent(CodeMirrorElement.getValue()) + o2Switch
+ "body": "id=" + rPID + "&language=1&" + "source=" + encodeURIComponent(CodeMirrorElement.getValue()) + o2Switch + GetCaptchaParameter()
}).then(async (Response) => {
if (Response.redirected) {
</file context>
实测 Workers AI 的视觉模型都读不准 XMOJ 的验证码:以 7602 为例, llava-1.5-7b 返回 600,llama-4-scout 返回 1834,mistral-small-3.1 返回 1234;另一张 7908 的图 llava 返回 9990。 四位数字的错误答案会通过校验被提交,失败后服务端置 vfail,验证码从 4 位数字升级为 8 位字母数字,直到会话结束,比留空更糟。因此把 AutoCaptcha 改为默认关闭,仅作为实验性功能保留接口。 顺带把默认关闭项提取为 DefaultOffSettings,供 UtilityEnabled 与设置 列表共用——两处都会给缺失的项写入默认值,不一致时先执行的一方说了算。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The AI solver is broken, but we are working on it |
vcode.php 用 imagettftext(15px, Vera.ttf) 在固定位置绘制 4 位数字, 每个字形在不同验证码之间逐像素相同,因此模板匹配可以直接读出来, 不需要把图片发给任何模型。 背景是一种随机色,文字是它的反色,噪点是第三种随机色:取出现次数 最多的颜色作为背景即可定位文字颜色。跳过 1 像素黑边,否则接近白色 的背景会让黑边被当成文字。切分出的字形与 10 个模板逐一比对,除了 奖励命中的笔画,也惩罚模板之外的墨点——否则带噪点的 0 会和 9 打平。 只有切出恰好 4 个字形、且每个字形的最佳得分领先第二名 4 分以上时 才填入,否则留空由用户填写:错误答案会触发 vfail,把验证码升级成 8 位字母数字,比留空更糟。 实测 300 张生成样本 227 正确 0 错误,40 张真实验证码填入 31 张, 14 张人工标注样本 13 正确 1 放弃 0 错误。因为不会填错,AutoCaptcha 恢复默认开启,同时移除 captchaSolve 依赖与相关 @connect。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Works! |
trim.85924D23-4E07-4D9D-8416-BD98CF66DEF7.MOV |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ef48f275fb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "referrer": location.href, | ||
| "method": "POST", | ||
| "body": (SearchParams.get("id") != null ? "id=" + SearchParams.get("id") : "cid=" + SearchParams.get("cid") + "&pid=" + SearchParams.get("pid")) + "&language=1&" + "source=" + encodeURIComponent(CodeMirrorElement.getValue()) + o2Switch | ||
| "body": (SearchParams.get("id") != null ? "id=" + SearchParams.get("id") : "cid=" + SearchParams.get("cid") + "&pid=" + SearchParams.get("pid")) + "&language=1&" + "source=" + encodeURIComponent(CodeMirrorElement.getValue()) + o2Switch + GetCaptchaParameter() |
There was a problem hiding this comment.
Validate the captcha in the actual submission handler
When a warning exposes PassCheck, or while the asynchronous compile check is running, the user can clear the captcha or click its still-enabled refresh image before PassCheck is clicked. The blank-answer guard exists only in the outer Submit handler, so this request sends an empty GetCaptchaParameter() and triggers the server's vfail escalation that the guard was intended to prevent. Recheck the visible captcha immediately before this POST.
Useful? React with 👍 / 👎.
验证码为空的拦截原来只在"提交"的处理函数里。检查未通过时页面会留下 "强制提交"按钮,而真正发出请求的是它:这段时间里用户可以点击验证码 图片换一张(会清空输入框)或手动清空,再点"强制提交"就会带着空验证码 发出请求,触发本要避免的 vfail 升级。 把拦截提取为 CaptchaIsMissing,在实际发出请求的 PassCheck 处理函数 开头也调用一次,并在拦截时重新隐藏"强制提交"按钮。 Reported-by: chatgpt-codex-connector Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Confirmed and fixed in 26723ca — thanks, this was a real hole. The blank-captcha guard was in the
Which is precisely what the guard existed to prevent. The check is now extracted as Covered by two new regression tests (26 checks total, run against the real source extracted from |
What does this PR aim to accomplish?:
Closes #420
When the judge queue is busy, XMOJ enables its native
vcode.phpimage captcha. The submit page isre-rendered from scratch by the script, so the server's captcha field was discarded and
vcodewasnever sent — submissions failed with the generic
提交失败!请关闭脚本后重试!.Two details from the upstream HUSTOJ source make this worse than it first appears:
submitpage.phprenders the field when pending solutions (result<4) is >10, butsubmit.phponly enforces it at >50. The two states drift apart, which is exactly what@langningchen described in the issue thread.
$_SESSION[vfail], which switchesvcode.phpfrom 4 digits to8 alphanumeric characters for the remainder of the session. Every failed submission was
therefore poisoning the session into the much harder captcha.
How does this PR accomplish the above?:
vcodefield beforeinnerHTMLreplaces the page, and render acaptcha image + input above the Submit button when present. Clicking the image fetches a new one.
vcodeon both submit paths.submit.phpignores the field when the queue is below theenforcement threshold, so sending it unconditionally is safe and closes the >10/>50 gap.
验证码错误in the response, reopen the captcha area and re-enable Submit for a retry,instead of reporting the generic failure message.
vfailand escalate the session to the 8-character challenge.
AutoCaptchasetting (green/A, on by default) reads the captcha locally in the browser —no network call, no Worker, no model:
vcode.phprewrites the session answer on every request, so a separate
<img>load would leave the pictureone challenge behind what the server expects;
vfailvariant, which isnever even decoded;
Why template matching rather than the AI solver
vcode.phprenders 4 digits withimagettftext($im, 15, 0, 4, 20, ..., "include/Vera.ttf", $vcode)— a fixed font at a fixed size and origin, so every glyph is pixel-identical between challenges.
That makes this an OCR problem with 10 known templates, not a vision-model problem.
Vision models were measured first, and all of them failed. On ground truth 7602:
llava-1.5-7b→600,llama-4-scout→1834,mistral-small-3.1-24b→1234. Over 7 labelledcaptchas each scored 0/7, before and after isolating the text and upscaling 6× — with perfectly
clean input LLaVA still drops the leading digit (
5341→341,5411→411).The matcher instead: takes the most common pixel as the background (the text is drawn in its exact
inverse), skips the 1px black border (a near-white background makes the border match the ink), splits
inked columns into glyphs, and scores each against the 10 templates. Scoring rewards covered strokes
and penalises ink the glyph does not explain — without that second term a noisy
0ties with9,which was the one real error found during testing. It answers only when the image splits into exactly
4 glyphs and each winner leads the runner-up by ≥4, otherwise it leaves the box empty.
That threshold is the whole safety argument: a wrong answer trips
vfailand escalates the sessionfrom 4 digits to 8 characters, so the matcher is tuned to never guess.
vcode.phpalgorithm, known labels)Because it never answers wrongly,
AutoCaptchaships on by default.captchaSolveand its@connectare removed — this needs no backend at all.Two notes for captchaSolve#10, which is already
merged and deployed and is a regression: Llama 4 Scout refuses the current prompt outright
(
*I apologize, but I am) because it contains the word CAPTCHA, and returns a constant8965when itdoes answer; and via the REST API the model returns
choices[0].text, soresponse.responseis worthconfirming against the binding. Reverting it restores strictly better (though still poor) behaviour.
Because both
UtilityEnabledand the settings list independently seed missing settings, thedefault-off list is hoisted into a shared
DefaultOffSettingsconst so the two cannot disagree.Testing
The queue is idle nearly all the time, so this state is close to unreproducible without flooding the
judge.
localStorage["UserScript-ForceCaptcha"] = "true"forces the captcha UI on instead.What was verified:
node --checkpasses.XMOJ.user.jsand executedunder stubs against real captcha pixels — 18 checks covering recognition, declining on an
ambiguous image, declining on a noise-bridged image, the 8-character skip,
AutoCaptchaoff,vcode.phpunreachable, the type-during-solve race, and "never wrong across 14 labelled captchas".Not verified — please check before merging: nothing has been exercised in a real browser. In
particular
createImageBitmap+<canvas>decoding of the GIF is stubbed in the tests, and theenforcement path (queue >50 →
验证码错误→ recovery) cannot be reached without a busy queue or alocal HUSTOJ with
$OJ_VCODEforced on.By submitting this pull request, I confirm the following:
dev.SubmitToEndedContestProblemfallback needs+ GetCaptchaParameter()on its request body.captcha area renders correctly before ticking this.
Left unchecked deliberately: items 3 and 11 are not fully satisfied. Please tick it after
browser verification.
🤖 Generated with Claude Code