en//
Web SecurityAppsecAPI SecurityBypass

Never Trust the Client: How I Manipulated the Leaderboard of an App with Anti-Fraud Logic

A technical dive into bypassing client-side anti-fraud checks, tampering with WebSocket/HTTP payloads, and why backend validation is non-negotiable.

1. Inspecting the End-of-Match Request

By intercepting traffic through Burp Suite, I captured the request sent as soon as a match finished:

POST /api/match/finalize HTTP/2
Host: target-app.com
Content-Type: application/json

{
  "selfScore": 7.4,
  "opponentScore": 6.8,
  "selfScanValidity": {
    "microMotionScore": "0.82",
    "staticImageSuspicion": "low",
    "sampledFrames": 120,
    "landmarkStability": "high"
  }
}
The selfScore field immediately stood out. If the browser is responsible for calculating and sending my own score, the question was obvious:

What happens if I simply modify this value before it reaches the backend?

2. The First Attempt: Interception Latency
I started with the most direct approach: intercepting the request in Burp Suite and manually altering selfScore.

However, the matches operate in real-time. While holding the HTTP request in Burp to edit the JSON payload, the opponent's browser had already delivered its result to the server.

My Request       ---> [ Paused in Burp Suite for editing ]
Opponent Request ---> [ Delivered to Server ]
                      └─> Match Finalized by Server

[ My Modified Request Arrives ] ---> Returned: Match Already Closed / Invalid
Because of the delay introduced by manual editing, my request arrived too late. The issue wasn't the payload modification itself, but the time required to perform it. I needed to automate the payload tampering directly at runtime.

3. Client-Side Hooking via Browser Console
To eliminate latency, I moved the interception logic directly into the browser's JavaScript runtime before starting a match.

Instead of catching the network traffic upstream in Burp, I overwrote window.fetch to intercept outgoing payloads matching the /finalize endpoint:

JavaScript
const originalFetch = window.fetch;
window.fetch = function (...args) {
    const [url, config] = args;
    
    if (typeof url === 'string' && url.includes('/api/match/finalize')) {
        let body = JSON.parse(config.body);
        body.selfScore = 9.8; // Force high score
        config.body = JSON.stringify(body);
    }
    
    return originalFetch.apply(this, args);
};
Match Concludes -> App Prepares Payload -> Hook Intercepts Payload -> Modifies Score -> Request Sent -> Server
With the monkey-patch active, the modified request was transmitted instantaneously without network delay.

However, the result was unexpected: despite hardcoding a score close to 10.0, my final score evaluated to:

1.0

4. Uncovering the Anti-Fraud Triggers
The score resetting to 1.0 wasn't a generic server error—it was an active anti-fraud defense triggering on anomalous data.

While testing scripts, my camera had been mostly static or obscured. Looking closely at selfScanValidity:

staticImageSuspicion: High

microMotionScore: Near Zero

I was effectively submitting:

Claimed Score: 9.8

Camera Micro-Motion: None

Static Image Suspicion: Very High

The backend heuristic engine flagged the inconsistency and penalized the score to 1.0.

Adjusting Camera Integrity
I re-tested while standing normally in front of the camera, ensuring organic motion, blinking, and natural lighting variations. The selfScanValidity metrics were now fully legitimate.

Yet, forcing arbitrary scores like 9.8 or 9.9 still resulted in a penalization down to 1.0.

5. Catching Secondary Transports (navigator.sendBeacon)
During testing, I noticed certain matches bypassed my window.fetch hook entirely.

The application utilized navigator.sendBeacon() as a fallback mechanism to guarantee payload delivery when users navigated away or unmounted components. Observing fetch alone was insufficient.

I expanded the client-side monkey-patching to cover both transport APIs:

JavaScript
const originalBeacon = navigator.sendBeacon;
navigator.sendBeacon = function (url, data) {
    if (url.includes('/api/match/finalize')) {
        // Parse, manipulate payload, and forward
    }
    return originalBeacon.apply(this, arguments);
};
Now every exit path was consistently captured.

6. Relative Manipulation vs. Absolute Forging
Why were legitimate camera metrics with high scores still flagged?

The backend appeared to correlate data from both participants in the match. If User A reports a huge score differential (9.9 vs 5.2), but User B's submitted telemetry contradicts that disparity, the backend flags the variance as an anomaly.

Instead of hardcoding unrealistic absolute values (9.9), I changed the strategy to relative score manipulation.

The request payload already contained opponentScore calculated by the local client runtime. I updated the script to dynamically compute a winning score relative to the opponent:

JavaScript
// Instead of: selfScore = 9.9
// Compute relative delta:
body.selfScore = body.opponentScore + 0.8;
Opponent Score: 6.2  ---> Modifying Self Score to: 7.0
Opponent Score: 7.1  ---> Modifying Self Score to: 7.9
By retaining authentic camera biometric data and applying a subtle relative score differential, the backend accepted the payload as a legitimate match outcome.

7. The Core Architectural Vulnerability
I didn't need to break the camera computer vision models or reverse-engineer every proprietary anti-fraud heuristic.

The execution flow remained intact:

Real user sits in front of the camera.

Client application evaluates real camera metrics.

Authentic biometric telemetry (selfScanValidity) is preserved.

Script adjusts selfScore relative to opponentScore.

Backend accepts the tampered score.

The root issue was trusting client-submitted state for business logic execution.

[ Unstrusted Client Runtime ]
   └── Calculates Scores & Heuristics
   └── Issues Final Result ---> [ Backend Server ] (Trusts incoming state)
No matter how sophisticated client-side telemetry or obfuscation is, anything executed on the user's device can be intercepted and manipulated.

8. Remediation Strategies
Security cannot rely on code obfuscation, anti-debugging scripts, or client-side detection logic.

Secure Architecture Pattern:
Client as Data Collector: The browser should only collect raw telemetry or feature vectors (e.g., frame landmarks, motion metrics).

Server as Decision Engine: Raw data is submitted to the backend, where scoring algorithms and anti-fraud heuristics execute server-side.

Cross-Validation: The backend validates telemetry from both peers before issuing state changes to rankings or databases.

Browser (Client)
   │
   └── Send Raw Telemetry Data Only
          │
          ▼
Server Backend
   ├── Validate Biometric Metrics
   ├── Compute Match Score Server-Side
   └── Commit to Database / Leaderboard
Conclusion
Vulnerabilities aren't always explicit memory corruptions, injection flaws, or broken access controls. Frequently, the most critical security flaws stem from architectural design assumptions.

When designing application features, always evaluate:
If the client tells the server what the result is, what prevents a user from changing that result before sending it?

Never trust the client.