Skip to content

fix(security): cap /api/file + MCP read_file at 5 MiB (RAN-9)#67

Merged
aksOps merged 1 commit into
mainfrom
fix/ran-9-max-file-bytes-cap
Apr 25, 2026
Merged

fix(security): cap /api/file + MCP read_file at 5 MiB (RAN-9)#67
aksOps merged 1 commit into
mainfrom
fix/ran-9-max-file-bytes-cap

Conversation

@aksOps

@aksOps aksOps commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • /api/file and the read_file MCP tool previously loaded entire files into JVM heap via Files.readString(...) before applying any line slice. A multi-GB file in an indexed codebase would OOM the serving process → trivial DoS against the read-only API/MCP surface.
  • Adds a configurable cap (serving.max_file_bytes, default 5 MiB) enforced for both entry points via a single SafeFileReader:
    • No line range → check on-disk size first and reject before reading.
    • With a range → stream through BufferedReader, apply start/end filter, and track accumulated UTF-8 byte count against the cap.
  • REST returns HTTP 413 (CONTENT_TOO_LARGE); MCP returns the usual JSON {\"error\": ...} payload. Error message: File exceeds max size: N bytes (max M bytes).
  • Config plumbed end-to-end: ServingConfig.maxFileBytesConfigDefaults.builtIn() (5 MiB) → ConfigMergerEnvVarOverlay (CODEIQ_SERVING_MAXFILEBYTES) → snake_case YAML with camelCase alias → UnifiedConfigAdapter → legacy CodeIqConfig.maxFileBytes (setter clamps to >=1).
  • Default documented in docs/codeiq.yml.example.

Closes RAN-9.

Test plan

  • SafeFileReaderTest — whole-file reject, line-range streaming, line-range reject mid-stream, negative startLine clamping.
  • GraphControllerTest — 413 on oversize whole-file read; 200 on narrow line range when whole file > cap.
  • McpToolsTest\"exceeds max size\" error for oversize read; line-range passthrough; line-range reject.
  • UnifiedConfigAdapterTest — explicit maxFileBytes overrides default; absent value falls back to CodeIqConfig default (5 MiB).
  • ConfigResolverTest / ConfigValidatorTest / ConfigExplainSubcommandTest — updated for the new ServingConfig record shape.
  • Full scoped run green locally: Tests run: 231, Failures: 0, Errors: 0, Skipped: 0.

Acceptance (from RAN-9)

  • Both endpoints enforce a configurable max-bytes cap.
  • Default cap documented in codeiq.yml.example.
  • Unit/integration tests cover oversize rejection + line-range path.

Comment thread src/main/java/io/github/randomcodespace/iq/api/GraphController.java Fixed
Comment thread src/main/java/io/github/randomcodespace/iq/api/GraphController.java Fixed
Comment thread src/main/java/io/github/randomcodespace/iq/api/GraphController.java Dismissed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7be4f6a670

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/main/java/io/github/randomcodespace/iq/api/SafeFileReader.java
Comment thread src/main/java/io/github/randomcodespace/iq/api/SafeFileReader.java
@aksOps aksOps enabled auto-merge (squash) April 25, 2026 13:18
…N-9)

Without a size guard, /api/file and the read_file MCP tool loaded entire
files into the JVM heap before the optional line slice was applied. A
multi-GB file in an indexed codebase trivially OOM'd the serving process,
giving anyone with HTTP access a DoS lever against the read-only API and
MCP endpoints.

Adds a configurable cap (default 5 MiB, key: serving.max_file_bytes) and
routes both entry points through a single SafeFileReader:

- No line range → check on-disk size first; reject before reading.
- With a line range → stream via BufferedReader, apply start/end filter,
  and track accumulated UTF-8 byte count against the cap.

REST returns HTTP 413 (CONTENT_TOO_LARGE); MCP returns the usual JSON
{\"error\": ...} payload. Both emit a message shaped like
\"File exceeds max size: N bytes (max M bytes)\".

Config is plumbed end-to-end through the unified config stack:
ServingConfig.maxFileBytes, ConfigDefaults.builtIn() (5 MiB),
ConfigMerger, EnvVarOverlay (CODEIQ_SERVING_MAXFILEBYTES), snake_case
YAML key with camelCase alias, UnifiedConfigAdapter → legacy
CodeIqConfig.maxFileBytes (with a >=1 clamp in the setter).

Tests added:
- SafeFileReaderTest: whole-file reject, line-range streaming, line-range
  reject mid-stream, negative startLine clamping.
- GraphControllerTest: 413 on oversize whole-file read, 200 on narrow
  line range when the whole file exceeds cap.
- McpToolsTest: \"exceeds max size\" error for oversize read, line-range
  passthrough, line-range reject.
- UnifiedConfigAdapterTest: explicit maxFileBytes overrides default;
  absent value falls back to CodeIqConfig default (5 MiB).
- ConfigResolverTest / ConfigValidatorTest / ConfigExplainSubcommandTest:
  updated for the new ServingConfig record shape.

Default documented in docs/codeiq.yml.example.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@aksOps aksOps force-pushed the fix/ran-9-max-file-bytes-cap branch from 7be4f6a to f91c37e Compare April 25, 2026 13:32
@sonarqubecloud

Copy link
Copy Markdown

@aksOps aksOps merged commit 19c6619 into main Apr 25, 2026
9 checks passed
@aksOps aksOps deleted the fix/ran-9-max-file-bytes-cap branch April 25, 2026 13:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants