Skip to content

fix: 在提交界面显示并提交验证码 - #1019

Merged
boomzero merged 12 commits into
devfrom
fix/submit-captcha-420
Aug 23, 2026
Merged

fix: 在提交界面显示并提交验证码#1019
boomzero merged 12 commits into
devfrom
fix/submit-captcha-420

Conversation

@boomzero

@boomzero boomzero commented Aug 23, 2026

Copy link
Copy Markdown
Member

What does this PR aim to accomplish?:

Closes #420

When the judge queue is busy, XMOJ enables its native vcode.php image captcha. The submit page is
re-rendered from scratch by the script, so the server's captcha field was discarded and vcode was
never sent — submissions failed with the generic 提交失败!请关闭脚本后重试!.

Two details from the upstream HUSTOJ source make this worse than it first appears:

  • submitpage.php renders the field when pending solutions (result<4) is >10, but
    submit.php only enforces it at >50. The two states drift apart, which is exactly what
    @langningchen described in the issue thread.
  • A missing or wrong answer sets $_SESSION[vfail], which switches vcode.php from 4 digits to
    8 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?:

  • Detect the server-rendered vcode field before innerHTML replaces the page, and render a
    captcha image + input above the Submit button when present. Clicking the image fetches a new one.
  • Send vcode on both submit paths. submit.php ignores the field when the queue is below the
    enforcement threshold, so sending it unconditionally is safe and closes the >10/>50 gap.
  • Detect 验证码错误 in the response, reopen the captcha area and re-enable Submit for a retry,
    instead of reporting the generic failure message.
  • Refuse to submit an empty captcha client-side, specifically so the request cannot trip vfail
    and escalate the session to the 8-character challenge.
  • New AutoCaptcha setting (green/A, on by default) reads the captcha locally in the browser
    no network call, no Worker, no model:
    • the challenge is downloaded once and that same copy is both shown and read — vcode.php
      rewrites the session answer on every request, so a separate <img> load would leave the picture
      one challenge behind what the server expects;
    • the GIF header width (15px per character) identifies the 8-character vfail variant, which is
      never even decoded;
    • a running solve never overwrites what the user has already started typing.

Why template matching rather than the AI solver

vcode.php renders 4 digits with imagettftext($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-7b600, llama-4-scout1834, mistral-small-3.1-24b1234. Over 7 labelled
captchas each scored 0/7, before and after isolating the text and upscaling 6× — with perfectly
clean input LLaVA still drops the leading digit (5341341, 5411411).

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 0 ties with 9,
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 vfail and escalates the session
from 4 digits to 8 characters, so the matcher is tuned to never guess.

result
300 generated captchas (exact vcode.php algorithm, known labels) 227 correct, 0 wrong, 73 declined
40 live captchas from xmoj.tech 31 auto-filled (78%), 9 declined
14 hand-labelled live captchas 13 correct, 0 wrong, 1 declined

Because it never answers wrongly, AutoCaptcha ships on by default. captchaSolve and its
@connect are 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 constant 8965 when it
does answer; and via the REST API the model returns choices[0].text, so response.response is worth
confirming against the binding. Reverting it restores strictly better (though still poor) behaviour.

Because both UtilityEnabled and the settings list independently seed missing settings, the
default-off list is hoisted into a shared DefaultOffSettings const 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 --check passes.
  • The recogniser and the captcha UI block were extracted verbatim from XMOJ.user.js and executed
    under stubs against real captcha pixels — 18 checks covering recognition, declining on an
    ambiguous image, declining on a noise-bridged image, the 8-character skip, AutoCaptcha off,
    vcode.php unreachable, the type-during-solve race, and "never wrong across 14 labelled captchas".
  • Accuracy measured as in the table above.

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 the
enforcement path (queue >50 → 验证码错误 → recovery) cannot be reached without a busy queue or a
local HUSTOJ with $OJ_VCODE forced on.


By submitting this pull request, I confirm the following:

  1. ✅ Read and understood the contributor's guide; this PR is based on and targets dev.
  2. ✅ The changes are commented in-code.
  3. ⚠️ Partially. Logic is unit-tested under stubs (see above), but nothing was run in a browser.
  4. ✅ Squashed into meaningful commits (plus CI's version bumps).
  5. ✅ No other PR covers this. Note fix: 修复已结束比赛提交回退功能失效 #1017 #1018 touches the same submit handler; when it lands, its
    SubmitToEndedContestProblem fallback needs + GetCaptchaParameter() on its request body.
  6. Cannot verify — needs a human. No browser testing was possible here. Please confirm the
    captcha area renders correctly before ticking this.

  • I have read the above and my PR is ready for review. Check this box to confirm

Left unchecked deliberately: items 3 and 11 are not fully satisfied. Please tick it after
browser verification.

🤖 Generated with Claude Code

评测队列繁忙时 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>
@sourcery-ai

sourcery-ai Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Implements 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 submission

sequenceDiagram
    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
Loading

Flow diagram for captcha detection and recovery

flowchart 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
Loading

File-Level Changes

Change Details Files
Wire the userscript to the external captcha solver service for automatic 4‑digit captcha recognition.
  • Add captcha.xmoj-script.uk to the @connect whitelist so GM_xmlhttpRequest can reach the solver
  • Introduce CaptchaSolverURL constant and use it as the POST target for raw image data
  • Implement RequestCaptchaSolver and SolveCaptcha helpers that POST the captcha image, parse the response, and accept only exactly four digits
XMOJ.user.js
Add configurable AutoCaptcha feature flag to the settings model/UI.
  • Extend the feature list with an AutoCaptcha option (type A, default-on) labeled for automatic captcha recognition with manual fallback
  • Reuse UtilityEnabled("AutoCaptcha") checks in captcha logic to gate solver behavior
XMOJ.user.js
Render a first‑class captcha UI on the submit page and keep it in sync with the server’s vcode.php behavior.
  • Detect presence of the native vcode field/img before replacing submitpage.php innerHTML, with an additional localStorage UserScript-ForceCaptcha hook for forced testing
  • Inject a CaptchaElement block (label, input, image, status text) above the Submit button in the custom submit page markup
  • Manage captcha image lifecycle via a single Blob/ObjectURL, ensuring vcode.php is fetched only once per challenge and that previous object URLs are revoked
  • Use GIF header width to infer captcha length (4‑digit vs 8‑char) and update status messaging accordingly, skipping auto-solve for the 8‑character variant
  • Add click handler on the captcha image to refresh the challenge and an Enter key handler on the input to trigger submit
XMOJ.user.js
Integrate captcha value into both submit request paths and handle server-side captcha failures gracefully.
  • Add GetCaptchaParameter helper that returns an encoded &vcode= parameter when the input is non-empty
  • Append GetCaptchaParameter to the main submit POST body and to the contest-fallback submit path so vcode is always sent when present
  • On submit response, detect "验证码错误" in the returned HTML, refresh the captcha, show a specific error message, re-enable Submit, and focus the captcha input instead of surfacing a generic failure
XMOJ.user.js
Prevent blank-captcha submissions from poisoning the PHP session into the harder 8‑character captcha mode.
  • Before sending a solution, check whether the captcha UI is visible and the input is blank; if so, block submission client-side, show a red error explaining that the queue is busy and captcha must be filled, restore the Submit button state, and focus the captcha field
XMOJ.user.js

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@hendragon-bot hendragon-bot Bot added the user-script This issue or pull request is related to the main user script label Aug 23, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 23, 2026

Copy link
Copy Markdown

Deploying xmoj-script-dev-channel with  Cloudflare Pages  Cloudflare Pages

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

View logs

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread XMOJ.user.js Outdated
Comment on lines +4275 to +4276
const Digits = SolverText.replace(/\D/g, "");
return /^\d{4}$/.test(Digits) ? Digits : null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread XMOJ.user.js
Comment on lines +4278 to +4279
const RefreshCaptcha = async (StatusMessage) => {
const RequestID = ++CaptchaRequestID;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment thread XMOJ.user.js Outdated
// 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/";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Comment thread XMOJ.user.js
SetCaptchaStatus(StatusMessage || "");
let ImageBlob;
try {
const CaptchaResponse = await fetch("https://www.xmoj.tech/vcode.php?" + Math.random(), {cache: "no-store"});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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>

Comment thread XMOJ.user.js
"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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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>

Comment thread XMOJ.user.js
Comment thread XMOJ.user.js Outdated
boomzero and others added 4 commits August 23, 2026 11:13
实测 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>
@boomzero

Copy link
Copy Markdown
Member Author

The AI solver is broken, but we are working on it

boomzero and others added 3 commits August 23, 2026 11:44
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>
@boomzero

Copy link
Copy Markdown
Member Author

Works!

@boomzero

Copy link
Copy Markdown
Member Author
trim.85924D23-4E07-4D9D-8416-BD98CF66DEF7.MOV

@boomzero

boomzero commented Aug 23, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread XMOJ.user.js
"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()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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>
@boomzero

Copy link
Copy Markdown
Member Author

Confirmed and fixed in 26723ca — thanks, this was a real hole.

The blank-captcha guard was in the 提交 handler, but the request that actually reaches submit.php is the 强制提交 (PassCheck) handler. RefreshCaptcha clears #vcode on every refresh, so the sequence was:

  1. captcha filled, 提交 pressed, guard passes;
  2. a check fails and leaves 强制提交 on screen;
  3. the user clicks the captcha image for a new one (clearing the box) or empties it by hand;
  4. 强制提交 POSTs with no vcode → server rejects → vfail → the session's captcha escalates from 4 digits to 8 characters.

Which is precisely what the guard existed to prevent.

The check is now extracted as CaptchaIsMissing() and called at the top of the PassCheck handler as well, immediately before the POST. It also re-hides 强制提交 when it blocks, so the button cannot be clicked repeatedly into the same state.

Covered by two new regression tests (26 checks total, run against the real source extracted from XMOJ.user.js): one walks the exact fill → clear → 强制提交 sequence above and asserts the POST is blocked with no vcode parameter, and one asserts the guard stays inert when no captcha is on screen, so ordinary submissions are unaffected.

@boomzero
boomzero merged commit 8e9b445 into dev Aug 23, 2026
6 checks passed
@boomzero
boomzero deleted the fix/submit-captcha-420 branch August 23, 2026 04:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L user-script This issue or pull request is related to the main user script

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant