Skip to content

[P2][BUG] No file count limit on analyst output — unbounded array can exhaust memory and API credits #10

Description

@pratik-saptarshi

Summary

The Analyst agent is instructed to return approximately fileLimit source files, but this is a soft hint in the LLM prompt — not an enforced limit. validateAnalysis() validates only that the returned value is a non-empty string[] with no upper bound check. A model misalignment, prompt injection, or large repository can cause the analyst to return thousands of files. This exhausts browser memory (rendering a huge list), crashes the React UI, and burns through API credits running thousands of concurrent scan agents.

Panel verdict: P2, VERIFIED, EXISTING_DEFECT (Completeness Audit finding G-03).


Affected Files

File Lines Issue
src/scanner.ts L122–L129 validateAnalysis() has no upper bound on array length
src/server/routes.ts L60 MAX_PARALLEL = 16 caps concurrency but not total file count

Root Cause — Code Evidence

validateAnalysis() — no length cap (src/scanner.ts, lines 122–129):

export function validateAnalysis(json: string): string[] {
  const parsed = JSON.parse(json);
  if (!parsed || typeof parsed !== 'object') throw new Error('not an object');
  const files = (parsed as { files?: unknown }).files;
  if (!Array.isArray(files) || !files.every(f => typeof f === 'string' && f.length > 0)) {
    throw new Error('.files must be a non-empty string[]');
  }
  return files as string[];  // ← may be 10,000 files — no cap applied
}

Analyst prompt uses fileLimit as a soft hint:

function analystPrompt(repoPath: string, outputPath: string, fileLimit: number): string {
  // ...
  return `...Identify ~${fileLimit} source files...`; // ← '~' makes it approximate
  // LLMs may return 5x or 10x this limit on large monorepos
}

Impact

  • OOM crash: 10,000+ file paths sent to the React UI as a single JSON payload can exhaust browser memory.
  • Credit burn: At parallel=16, 10,000 files = ~625 API batches. At ~$0.01/scan, an accidental monorepo analysis costs $6.25 without warning.
  • No rate limiting: Concurrent agent lanes lack HTTP 429 backoff budgets (see also: G-04), so rapid API exhaustion causes unrecoverable scan failure.

Remediation

Step 1 — Enforce a hard cap in validateAnalysis()

const MAX_ANALYST_FILES = 500; // sensible default for a security scanner

export function validateAnalysis(json: string, maxFiles = MAX_ANALYST_FILES): string[] {
  const parsed = JSON.parse(json);
  if (!parsed || typeof parsed !== 'object') throw new Error('not an object');
  const files = (parsed as { files?: unknown }).files;
  if (!Array.isArray(files) || !files.every(f => typeof f === 'string' && f.length > 0)) {
    throw new Error('.files must be a non-empty string[]');
  }
  if (files.length > maxFiles) {
    console.warn(`[analyst] Truncating ${files.length} files to ${maxFiles}`);
    return (files as string[]).slice(0, maxFiles);
  }
  return files as string[];
}

Step 2 — Expose fileLimit cap as a user-configurable option

In src/server/routes.ts, add a fileLimit parameter to the POST /api/scans body with a maximum cap:

const MAX_FILE_LIMIT = 500;
const fileLimit = Math.min(
  Number.isFinite(body.fileLimit) ? Math.floor(body.fileLimit as number) : 50,
  MAX_FILE_LIMIT
);

Step 3 — Show an estimate before scanning

Before starting a scan, emit a preview event with the analyst's file count so the user can cancel if unexpected:

emit({ type: 'files-preview', count: files.length, capped: files.length > maxFiles });
// Wait for user confirmation if count > some threshold (e.g. > 200)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions