What happened?
The Arena's integrity/proctoring penalty is driven by a flag count that the browser computes and sends in the submit request, and the server trusts it. So the "more than 5 flags → base points only, out of the ranked top 10" rule is trivially bypassable: a solver who sends flags: 0 is never penalized, no matter how much they paste or tab-switch.
To be clear about what already works vs. what doesn't:
- The penalty logic itself is already implemented and correct —
src/server/leaderboard.ts (scoreChallenge) gives anyone with more than FLAG_LIMIT (5) flags only BASE_POINTS and drops them from the ranked positions.
- The flag count is already stored in
submissions.flags.
- The gap is the source of the number:
src/app/api/submit/route.ts stores body.flags verbatim (flags: typeof body.flags === "number" ? Math.max(0, Math.round(body.flags)) : 0). The count originates in src/components/cp-arena/useIntegrityMonitor.ts on the client and is sent by ArenaWorkspace in the submit body. Because the server never establishes the count itself, the penalty can be zeroed out by the client.
Steps to reproduce
- Open today's Problem of the Day and solve it while deliberately triggering integrity flags (paste code, switch tabs, blur the window) — normally this would flag the submission.
- When submitting, edit the
POST /api/submit request body (dev tools, or any HTTP client) and set "flags": 0 (or omit it).
- The submission is recorded with 0 flags: full speed-bounty points, still eligible for the ranked top 10, despite the violations.
Affected page / URL
/api/submit (Arena solve workspace). Source: src/app/api/submit/route.ts, src/server/leaderboard.ts, src/components/cp-arena/useIntegrityMonitor.ts, src/components/cp-arena/ArenaWorkspace.tsx.
Where did you see this?
Both — the trust boundary is the same locally and on the live site.
Suggested fix — record integrity events server-side
Move the count to somewhere the client can't overwrite:
- Add a server endpoint (e.g.
POST /api/attempt/flag) that the workspace calls when useIntegrityMonitor detects an event, keyed to the user's attempts row for today's ranked problem. The server increments a per-attempt counter (and can persist a breakdown).
- At submit time, derive the flag count from that server-side record, not from the request body. Stop trusting
body.flags (keep it, if at all, only as an explicitly-untrusted diagnostic).
- Caveat / expectations: browser-observed signals (paste, tab-switch, blur) can still be suppressed by a determined user who refrains from firing them, so this raises the bar rather than being a hard guarantee. Pair it with server-side sanity checks (e.g. implausibly fast solves) and manual review of flagged sessions.
What happened?
The Arena's integrity/proctoring penalty is driven by a flag count that the browser computes and sends in the submit request, and the server trusts it. So the "more than 5 flags → base points only, out of the ranked top 10" rule is trivially bypassable: a solver who sends
flags: 0is never penalized, no matter how much they paste or tab-switch.To be clear about what already works vs. what doesn't:
src/server/leaderboard.ts(scoreChallenge) gives anyone with more thanFLAG_LIMIT(5) flags onlyBASE_POINTSand drops them from the ranked positions.submissions.flags.src/app/api/submit/route.tsstoresbody.flagsverbatim (flags: typeof body.flags === "number" ? Math.max(0, Math.round(body.flags)) : 0). The count originates insrc/components/cp-arena/useIntegrityMonitor.tson the client and is sent byArenaWorkspacein the submit body. Because the server never establishes the count itself, the penalty can be zeroed out by the client.Steps to reproduce
POST /api/submitrequest body (dev tools, or any HTTP client) and set"flags": 0(or omit it).Affected page / URL
/api/submit(Arena solve workspace). Source:src/app/api/submit/route.ts,src/server/leaderboard.ts,src/components/cp-arena/useIntegrityMonitor.ts,src/components/cp-arena/ArenaWorkspace.tsx.Where did you see this?
Both — the trust boundary is the same locally and on the live site.
Suggested fix — record integrity events server-side
Move the count to somewhere the client can't overwrite:
POST /api/attempt/flag) that the workspace calls whenuseIntegrityMonitordetects an event, keyed to the user'sattemptsrow for today's ranked problem. The server increments a per-attempt counter (and can persist a breakdown).body.flags(keep it, if at all, only as an explicitly-untrusted diagnostic).