Back to Blog
high SEVERITY7 min read

How Authorization Bypass and Balance Corruption happen in Node.js and how to fix it

A high-severity authorization bypass in `commands/profile/transfer.js` allowed any user to transfer coins directly to owner/admin accounts, bypassing privilege checks entirely. Compounding the issue, the absence of a numeric guard on `targetDb.coin` could corrupt balances with `NaN` when the field was uninitialized. Three targeted lines of code closed both attack surfaces without changing any valid transfer behavior.

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

This vulnerability is an authorization bypass (CWE-285) combined with improper input validation (CWE-20) in a Node.js coin-transfer command (`commands/profile/transfer.js`). Any authenticated user could transfer coins to owner/admin accounts because no privilege check was performed on the target JID, and an uninitialized `targetDb.coin` field could produce `NaN` balance corruption via the `+=` operation. The fix adds three guards: a null check on `targetDb`, an owner check via `ctx.checkOwner(target.jid)`, and a `Number.isFinite()` guard that resets the coin field to `0` before arithmetic.

Vulnerability at a Glance

cweCWE-285 (Improper Authorization), CWE-20 (Improper Input Validation)
fixAdded targetDb null check, ctx.checkOwner() guard, and Number.isFinite() normalization
riskUsers can transfer coins to privileged accounts; uninitialized balances become NaN
languageJavaScript (Node.js)
root causeNo existence, privilege, or numeric-type check before mutating targetDb.coin
vulnerabilityAuthorization Bypass + Improper Input Validation

How Authorization Bypass and Balance Corruption Happen in Node.js and How to Fix It

The commands/profile/transfer.js file handles one of the most sensitive operations in any token-economy application: moving value from one account to another. A flaw in the transfer handler meant that the destination account received no scrutiny at all — any JID a user supplied was accepted, including those belonging to owners and administrators. On top of that, a missing numeric guard meant that an uninitialized targetDb.coin field would silently corrupt the target balance with NaN. This post walks through both issues, the three-line fix that resolves them, and the broader lessons for Node.js developers building similar features.


The Vulnerability Explained

Missing Target Validation

The core of the transfer flow resolves the target account from user-supplied input and immediately mutates its balance:

// BEFORE — vulnerable code
const targetDb = ctx.getDb("users", target.jid);
targetDb.coin += coinAmount;
senderDb.coin -= coinAmount;
targetDb.save();

Three critical checks are absent here:

  1. No existence checkctx.getDb() can return null or undefined if the JID does not correspond to a real account. Calling .coin on null throws a runtime exception, but more importantly it means the code never verified the account exists before trusting it.

  2. No privilege check — There is nothing preventing a regular user from supplying the JID of an owner or administrator as target.jid. The resolved targetDb is used unconditionally, so a regular user can credit any account in the system, including privileged ones.

  3. No numeric guard on targetDb.coin — If coin is undefined (the field was never initialized), then undefined += coinAmount evaluates to NaN. Every subsequent += on that field will continue to produce NaN, permanently corrupting the balance record.

Attack Scenario

Imagine a bot platform where the owner's account controls a treasury. A malicious user discovers the owner's JID through a public leaderboard or group metadata. They craft a transfer command targeting that JID:

!transfer @owner 1000

Because transfer.js performs no privilege check, the command resolves the owner's database record, adds 1000 coins to it, and saves. The attacker has now artificially inflated an admin balance — useful for manipulating economy mechanics, bypassing coin-gated features, or simply griefing the platform. With no guard on targetDb.coin, if the owner's record happened to lack the coin field, the balance would be set to NaN, effectively destroying the account's economic state.

Real-World Impact

  • Economy manipulation: Regular users can credit privileged accounts, breaking any game or reward logic that treats owner balances as authoritative.
  • Privilege escalation via balance: If coin balance gates access to features, inflating an admin account could trigger unintended behaviors in downstream logic.
  • Silent data corruption: NaN propagates through arithmetic invisibly. A corrupted balance will not throw an error; it will simply return NaN for every future calculation until someone notices the broken record.

The Fix

The patch introduces exactly three lines immediately after targetDb is resolved:

// AFTER — fixed code
const targetDb = ctx.getDb("users", target.jid);
if (!targetDb) return await ctx.reply(ctx.format.info("Akun target tidak ditemukan!"));
if (ctx.checkOwner(target.jid)) return await ctx.reply(ctx.format.info("Tidak dapat mentransfer koin ke akun owner!"));
if (!Number.isFinite(targetDb.coin)) targetDb.coin = 0;
targetDb.coin += coinAmount;
senderDb.coin -= coinAmount;
targetDb.save();

Before vs. After

Concern Before After
Non-existent target Runtime crash or silent failure Early return with user-facing error
Owner/admin target Accepted unconditionally Blocked by ctx.checkOwner() guard
Uninitialized coin field NaN corruption via += Reset to 0 before arithmetic

Why Each Line Matters

Line 1 — Existence check

if (!targetDb) return await ctx.reply(ctx.format.info("Akun target tidak ditemukan!"));

This is a standard null-guard. If ctx.getDb() returns a falsy value, the function exits immediately with a clear message. No further code runs against an invalid record.

Line 2 — Privilege check

if (ctx.checkOwner(target.jid)) return await ctx.reply(ctx.format.info("Tidak dapat mentransfer koin ke akun owner!"));

This is the authorization fix. ctx.checkOwner(target.jid) consults the application's own privilege registry to determine whether the resolved JID belongs to an owner. If it does, the transfer is rejected before any balance mutation occurs. Developers maintaining similar systems should note that this check uses the resolved JID from targetDb, not the raw user input — ensuring that JID normalization or aliasing cannot bypass the guard.

Line 3 — Numeric normalization

if (!Number.isFinite(targetDb.coin)) targetDb.coin = 0;

Number.isFinite() returns false for undefined, null, NaN, Infinity, and -Infinity — exactly the set of values that would corrupt arithmetic. By resetting coin to 0 before +=, the code guarantees that the resulting balance is always a valid finite number, regardless of the record's prior state.


Key Takeaways

  • ctx.getDb() can return null — every database lookup in transfer.js (and similar command handlers) must be followed by an existence check before any field access.
  • Resolving a JID from user input is not the same as authorizing a transfer to that JIDctx.checkOwner(target.jid) must be called on the resolved target, not assumed safe because the user supplied it.
  • undefined += number silently produces NaN — use Number.isFinite() to normalize any persisted numeric field before arithmetic, especially in economy or balance logic.
  • Three lines of code closed two distinct vulnerability classes — existence checks, privilege checks, and type normalization are cheap to add and expensive to omit.
  • The fix is scoped to the vulnerable path only — valid transfers between non-privileged accounts with initialized balances behave exactly as before.

How Orbis AppSec Detected This

  • Source: User-controlled target.jid value passed to ctx.getDb("users", target.jid) in commands/profile/transfer.js
  • Sink: targetDb.coin += coinAmount — an unconditional balance mutation on the resolved record with no privilege or type check
  • Missing control: No null check on targetDb, no call to ctx.checkOwner() before mutation, and no Number.isFinite() guard before the += operation
  • CWE: CWE-285 (Improper Authorization) and CWE-20 (Improper Input Validation)
  • Fix: Added a null guard on targetDb, an owner privilege check via ctx.checkOwner(target.jid), and a Number.isFinite() normalization of targetDb.coin before the arithmetic operation

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

The vulnerability in commands/profile/transfer.js is a textbook example of how missing validation at the operation boundary — rather than at the input boundary — creates exploitable security gaps. The raw JID was presumably validated as a string, but the resolved database record was never checked for existence, privilege, or data integrity. The result was a transfer handler that would accept any target, including owners, and silently corrupt balances when fields were uninitialized.

The fix is minimal, readable, and does not change behavior for legitimate transfers. It demonstrates a principle worth internalizing: every state-mutating operation should validate its resolved entities, not just its raw inputs. In token-economy systems, balance manipulation can have cascading effects on game logic, access control, and user trust. Catching these issues before they reach production — through automated scanning and code review — is far cheaper than recovering from them after the fact.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #85

Related Articles

critical

How Broken Object-Level Authorization happens in Express.js and how to fix it

A critical authorization flaw in `src/v1/routes/index.js` allowed any authenticated API key holder to access arbitrary budgets by manipulating the `budgetSyncId` URL parameter. The fix introduces an environment-based allowlist that validates budget access before processing requests.

high

How Missing Rate Limiting Enables Denial of Service Attacks in Node.js and How to Fix It

The k-skill-proxy server exposed multiple public API endpoints (`/health`, `/v1/vworld/search`, `/v1/fine-dust/report`, `/v1/assembly/bills`) without consistent rate limiting middleware, leaving them vulnerable to denial-of-service attacks. A `buildRateLimiter` function existed but wasn't applied to all endpoints. This fix ensures rate limiting is enforced on all public endpoints, preventing resource exhaustion attacks.

high

How Sandboxed Iframe Popup Restriction Bypass happens in Electron and how to fix it

A high-severity flaw in Electron (CVE-2026-70608) allowed sandboxed iframes to bypass the `allow-popups` sandbox restriction through the internal OpenURL navigation path, letting malicious or compromised embedded content spawn unauthorized popup windows. The fix upgrades Electron from 40.10.6 to 41.10.3 (also patched in 42.0.1 and 39.8.10), closing the navigation-layer gap without requiring any application code changes.

critical

How Missing Authentication on DELETE Endpoints Happens in Python aiohttp and How to Fix It

A critical missing authentication vulnerability in `pz_minimax.py` allowed any network-connected user to delete stored MiniMax prompts via the `DELETE /pz_easyuse/minimax-prompts/{index}` endpoint without any access control. An attacker could enumerate sequential indices to wipe all user-created prompts from the shared JSON file. The fix restricts the DELETE endpoint to localhost-only requests by checking `request.remote` against loopback addresses.

critical

How Information Disclosure Vulnerabilities Happen in Python APIs and How to Fix It

A critical information disclosure vulnerability in the Hermes plugin dashboard API was exposing sensitive filesystem paths, credential file locations, and configuration details without authentication. The fix redacts this sensitive information from API responses, replacing absolute paths with boolean status indicators to prevent attackers from locating and targeting credential files.

critical

How Missing Authentication on DELETE Endpoints Happens in Node.js Express and How to Fix It

A critical authentication bypass vulnerability was discovered in the skill-cabinet server where the DELETE /api/skills/:id endpoint allowed any unauthenticated user to delete arbitrary skills from the filesystem. The fix implements loopback origin validation to ensure only requests from localhost can perform destructive operations, while also consolidating delete functionality into a single, protected endpoint.