Back to Blog
critical SEVERITY8 min read

How unlimited batch API calls happen in React JSX and how to fix it

A missing batch size limit in `BatchModeRunner.jsx` allowed users to trigger unlimited LLM API calls by pasting thousands of items into the batch input field. This could exhaust shared API quotas in organizational settings where a single API key is distributed across multiple users. The fix introduces a hard cap of 25 items (`MAX_BATCH_SIZE = 25`) enforced directly in the `canRun()` validation function.

O
By Orbis AppSec
Published July 24, 2026Reviewed July 24, 2026

Answer Summary

This vulnerability is an unbounded API consumption issue (CWE-770: Allocation of Resources Without Limits or Throttling) in a React JSX component (`BatchModeRunner.jsx`). Because the `canRun()` function in `BatchModeRunner.jsx` never checked how many items were in the batch queue, an attacker or careless user could submit thousands of LLM API calls in a single batch run, exhausting shared API quotas. The fix adds a `MAX_BATCH_SIZE` constant of 25 and enforces it as a guard condition inside `canRun()`, blocking the Run Batch button whenever the item count exceeds the limit.

Vulnerability at a Glance

cweCWE-770
fixAdded `MAX_BATCH_SIZE = 25` constant and enforced `items.length > MAX_BATCH_SIZE` check inside `canRun()`
riskExhaustion of shared API quotas; financial and availability impact in organizational deployments
languageJavaScript (React JSX)
root causeThe `canRun()` function in `BatchModeRunner.jsx` never validated `items.length` against a maximum, allowing arbitrarily large batches
vulnerabilityUnbounded LLM API Batch Consumption

How unlimited batch API calls happen in React JSX and how to fix it

The Problem in Plain Terms

The BatchModeRunner.jsx component handles one of the most sensitive user-facing operations in this application: sending items to an LLM provider using a real API key. But until this fix landed, there was nothing stopping a user from pasting 10,000 lines into the batch input and clicking Run Batch — triggering 10,000 sequential LLM API calls, all authenticated with a shared organizational key.

This is the kind of vulnerability that doesn't look dangerous at first glance. There's no SQL injection, no XSS, no memory corruption. It's a missing > check in a single if statement. But in an organizational deployment where one API key is shared across a team, it's a direct path to quota exhaustion, unexpected billing spikes, and denial of service for every other user on that key.


The Vulnerability Explained

What canRun() Was Supposed to Do

The canRun() function in BatchModeRunner.jsx acts as a gate: it returns false if the component isn't ready to run, disabling the Run Batch button. Before the fix, it looked like this:

const canRun = () => {
  if (!apiKey || items.length === 0 || !batchFieldId) return false
  return agent.inputs
    .filter((i) => i.required && i.id !== batchFieldId)
    .every((i) => {
      // ... checks for required fields
    })
}

Notice what it checks:
- ✅ Is there an API key?
- ✅ Is the items array non-empty?
- ✅ Is a batch field selected?
- ❌ Is the items array within a safe size? — Never checked.

The check items.length === 0 guards against an empty batch, but there was no corresponding upper-bound check. The runBatch function (imported from ../lib/batchRunner) processes items with a fixed concurrency of 3 — meaning it runs 3 LLM calls in parallel — but it never limits how many total items it processes. A batch of 5,000 items runs at concurrency 3 until all 5,000 are done.

The Attack Scenario

The exploitation path is straightforward and requires no technical sophistication:

  1. An attacker (or a careless user) with access to a shared API key opens the Batch Mode interface in BatchModeRunner.jsx.
  2. They paste thousands of items into the batch input textarea — the component parses these via parsePastedLines() and populates the items state.
  3. canRun() returns true because items.length > 0, the API key is present, and batchFieldId is set.
  4. They click Run Batch. The runBatch() function begins processing all items with concurrency: 3, making thousands of authenticated LLM API calls.
  5. Every other user on the same API key hits rate limits or quota caps. Depending on the provider and plan, this may also incur significant financial charges.

Because API calls go directly from the browser to the LLM provider, there is no server-side layer to catch or throttle this. The only defense is the client-side canRun() gate — which is exactly why plugging this gap matters.

Why This Is Specific to LLM Workloads

Traditional API abuse is bad. LLM API abuse is expensive. Each call to a frontier model can consume thousands of tokens, with costs measured in cents per call. A batch of 1,000 items at $0.01/call is $10 — a batch of 100,000 items is $1,000, all potentially charged to a shared organizational account before anyone notices.


The Fix

What Changed

The fix is compact but precise. It introduces a named constant at the top of the file:

const MAX_BATCH_SIZE = 25

And then enforces it inside canRun():

// Before
if (!apiKey || items.length === 0 || !batchFieldId) return false

// After
if (!apiKey || items.length === 0 || items.length > MAX_BATCH_SIZE || !batchFieldId) return false

Before the fixcanRun() only checked that the batch was non-empty:

const canRun = () => {
  if (!apiKey || items.length === 0 || !batchFieldId) return false
  return agent.inputs
    .filter((i) => i.required && i.id !== batchFieldId)
    .every((i) => { /* required field checks */ })
}

After the fixcanRun() now enforces both a lower and upper bound on batch size:

const canRun = () => {
  if (!apiKey || items.length === 0 || items.length > MAX_BATCH_SIZE || !batchFieldId) return false
  return agent.inputs
    .filter((i) => i.required && i.id !== batchFieldId)
    .every((i) => { /* required field checks */ })
}

Why This Specific Change Works

The canRun() return value is used to disable the Run Batch button in the UI. By returning false whenever items.length > MAX_BATCH_SIZE, the button stays disabled and runBatch() is never called — the LLM API is never reached.

Using a named constant (MAX_BATCH_SIZE = 25) rather than a magic number (items.length > 25) is intentional good practice: it makes the limit self-documenting, easy to audit, and straightforward to adjust in one place if the limit needs to change.

The value of 25 is a deliberate, conservative choice. It allows legitimate batch use cases (running a prompt against a small dataset, testing across a set of inputs) while making quota exhaustion attacks impractical.


Prevention & Best Practices

Always Validate Both Bounds on Array Inputs

Whenever user-controlled data populates an array that drives API calls, validate both the minimum and maximum:

// Weak — only checks lower bound
if (items.length === 0) return false

// Strong — checks both bounds
if (items.length === 0 || items.length > MAX_BATCH_SIZE) return false

This pattern applies anywhere user input determines how many times an external service is called: batch processors, bulk importers, multi-recipient email senders, etc.

Define Limits as Named Constants

Magic numbers buried in conditions are easy to miss in code review and hard to adjust consistently:

// Hard to audit, hard to change
if (items.length > 25) return false

// Self-documenting, easy to find and update
const MAX_BATCH_SIZE = 25
if (items.length > MAX_BATCH_SIZE) return false

Don't Rely Solely on Concurrency Limits

The runBatch() function already used concurrency: 3 to prevent hammering the API in parallel. This is a throughput control, not a volume control. Concurrency limits are important, but they don't cap total consumption — pair them with batch size limits.

Add Server-Side Rate Limiting When Possible

Client-side guards like canRun() can be bypassed by a determined attacker who calls the underlying API directly. For production systems, server-side rate limiting (e.g., per-user, per-key, per-minute quotas) provides a defense-in-depth layer that cannot be circumvented from the browser.

Relevant Standards


Key Takeaways

  • items.length === 0 is not the same as items.length <= MAX_BATCH_SIZE — the original canRun() in BatchModeRunner.jsx guarded against empty batches but left the upper bound completely open.
  • Concurrency limiting is not rate limitingrunBatch()'s concurrency: 3 controlled parallelism, not total call volume; a batch of 10,000 items would still make 10,000 API calls.
  • Client-side gates must check upper bounds on any array that drives external API calls, especially when those calls carry shared credentials.
  • Named constants like MAX_BATCH_SIZE make security limits visible and maintainable — a reviewer can immediately see the intent and the value without decoding a magic number.
  • LLM API calls have direct financial cost — unlike many resource exhaustion issues, unbounded LLM batch calls can translate to real charges on a shared account within minutes.

How Orbis AppSec Detected This

  • Source: User-pasted text in the batch input textarea, parsed by parsePastedLines() into the items state array in BatchModeRunner.jsx
  • Sink: The runBatch() call triggered when canRun() returns true, which dispatches LLM API calls for every item in the items array
  • Missing control: The canRun() function checked items.length === 0 (lower bound) but never checked items.length > [limit] (upper bound), leaving total batch size unbounded
  • CWE: CWE-770 — Allocation of Resources Without Limits or Throttling
  • Fix: Added const MAX_BATCH_SIZE = 25 and enforced items.length > MAX_BATCH_SIZE as a guard condition in canRun() at BatchModeRunner.jsx

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 BatchModeRunner.jsx is a reminder that resource exhaustion issues don't require sophisticated attack techniques — sometimes a missing > comparison in a guard function is all it takes. The canRun() function was doing its job of preventing invalid states, but it was never asked to prevent excessive states. Adding items.length > MAX_BATCH_SIZE to that check closes the gap with minimal code change and zero impact on legitimate use cases.

For developers building components that drive external API calls from user-controlled inputs: always think about both ends of the spectrum. Empty input is the obvious case. Unbounded input is the dangerous one.


References

Frequently Asked Questions

What is unbounded API consumption in a batch runner?

It occurs when a UI component allows users to queue an unlimited number of API requests without any cap, enabling a single user to exhaust rate limits or quotas that are shared across an organization.

How do you prevent unlimited API calls in React JSX components?

Define a named constant for the maximum allowed batch size (e.g., `MAX_BATCH_SIZE = 25`) and enforce it in the submission guard function before enabling the run button or triggering the API calls.

What CWE is unbounded batch API consumption?

CWE-770: Allocation of Resources Without Limits or Throttling — which covers scenarios where an application fails to restrict how many resources (here, API calls) a single user action can consume.

Is concurrency limiting (e.g., `concurrency: 3`) enough to prevent this?

No. Concurrency limiting controls how many requests run in parallel, but it does not cap the total number of requests. A batch of 10,000 items with concurrency 3 will still make 10,000 API calls — just three at a time.

Can static analysis detect unbounded batch sizes in React components?

Yes. Tools like Semgrep can flag submission handlers or guard functions that check array length for emptiness (`length === 0`) but never check for an upper bound (`length > MAX`), which is the exact pattern found here.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #815

Related Articles

critical

How arbitrary code execution via injected protobuf definition type fields happens in Node.js and how to fix it

A critical arbitrary code execution vulnerability (CVE-2026-41242) was discovered in protobufjs versions prior to 7.5.5 and 8.0.1, allowing attackers to inject malicious code through crafted protobuf definition type fields. The fix upgrades the dependency from the vulnerable version 6.11.4 to 7.5.5, which properly sanitizes type field inputs during protobuf parsing. This vulnerability is especially dangerous because protobufjs is widely used in Node.js applications for serialization, meaning a s

high

How command injection happens in Node.js child_process and how to fix it

A high-severity command injection vulnerability was discovered in `bootstrap.js` at line 34, where the `exec()` function was used to run shell commands constructed from a `folder` variable. By replacing `exec()` with `execFile()` and passing arguments as an array, the fix eliminates the shell interpolation entirely, closing the door on command injection attacks that could have affected all downstream consumers of this Node.js library.

high

How unsafe pickle deserialization happens in NumPy's np.load() and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `tools/ardy-engine/retarget.py` where `np.load()` was called with `allow_pickle=True`, enabling attackers to embed malicious pickle payloads in `.npz` files. The fix was a single-character change—switching `allow_pickle=True` to `allow_pickle=False`—that eliminates the deserialization attack vector while preserving the file's legitimate array data loading functionality.

high

How SSRF and Credential Leakage via Absolute URLs happens in axios and how to fix it

A high-severity vulnerability (CVE-2025-27152) in axios versions prior to 1.8.2 allowed Server-Side Request Forgery (SSRF) attacks and credential leakage when making HTTP requests with absolute URLs. This vulnerability was fixed by upgrading from axios 1.7.4 to 1.8.2 in both package.json and package-lock.json, eliminating the attack vector that could have exposed authentication tokens and enabled unauthorized server-side requests.

high

How pickle-based arbitrary code execution happens in PyTorch and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `scripts/export_joyvasa_audio.py` where `torch.load()` was called with `weights_only=False`, allowing any pickle-serialized Python object — including malicious code — to execute during checkpoint loading. The fix switches to `weights_only=True` and explicitly allowlists only the two non-standard classes the checkpoint actually requires: `argparse.Namespace` and `pathlib.PosixPath`. This closes a real code execution path tha

critical

How WebSocket protocol length header abuse happens in Node.js and how to fix it

A critical vulnerability (CVE-2026-54466) in websocket-driver versions prior to 0.7.5 allows attackers to corrupt WebSocket messages by manipulating protocol length headers. This can lead to data integrity issues, denial of service, or potentially arbitrary code execution in affected Node.js applications. The fix involves upgrading the websocket-driver dependency to version 0.7.5.