Skip to content

🛡️ Sentinel: [CRITICAL] Fix TOCTOU and DoS vulnerability in file reading - #75

Open
ivangegovdve-sudo wants to merge 1 commit into
mainfrom
sentinel-fix-toctou-read-7814890576515838238
Open

🛡️ Sentinel: [CRITICAL] Fix TOCTOU and DoS vulnerability in file reading#75
ivangegovdve-sudo wants to merge 1 commit into
mainfrom
sentinel-fix-toctou-read-7814890576515838238

Conversation

@ivangegovdve-sudo

Copy link
Copy Markdown
Owner

🚨 Severity: CRITICAL
💡 Vulnerability: The JSON file-backed adapters (JsonFilePracticeRepository, CheckpointStore, JsonFileProgressRepository, JsonFileProgressSnapshotStore) previously used path.stat().st_size to check if a file exceeded 10MB before reading the entire file into memory using path.read_text(). This introduced two critical risks:

  1. TOCTOU (Time-of-Check to Time-of-Use): A file could be modified and grow beyond the 10MB limit in the split second between stat() checking the size and read_text() reading the file, leading to unbounded memory consumption.
  2. OOM DoS via Device Files: If a malicious user or misconfiguration provided a device file (like /dev/zero), st_size would report 0 bytes, bypassing the limit check, but read_text() would stream data infinitely until the process crashed with an Out-Of-Memory error.

🎯 Impact: Potential Denial of Service (DoS) and application crash via memory exhaustion.

🔧 Fix:

  • Replaced the vulnerable st_size check and read_text() combination with a secure bounded read pattern.
  • Added .is_file() checks to ensure the path points to a regular file, rejecting directories or block devices gracefully.
  • Used f.read(10 * 1024 * 1024 + 1) to safely read up to 10MB plus 1 byte.
  • Explicitly check if the read content length exceeds the 10MB limit, throwing the exact same ValueError as before if it does.
  • Added a Sentinel journal entry documenting this critical security learning to prevent future reintroduction.

Verification:

  • Validated all 4 file adapters correctly raise a ValueError if the bounded read exceeds 10MB.
  • Ran the test suite (uv run pytest) and confirmed all tests pass.
  • Verified linting (uv run ruff check .) and type checking (uv run mypy) pass successfully.

PR created automatically by Jules for task 7814890576515838238 started by @ivangegovdve-sudo

Co-authored-by: ivangegovdve-sudo <225339531+ivangegovdve-sudo@users.noreply.github.com>
@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses critical TOCTOU (Time-of-Check to Time-of-Use) and Denial of Service (DoS) vulnerabilities in the application's JSON file-backed adapters. By replacing an insecure file size check with a robust bounded read mechanism and adding explicit file type validation, the changes prevent potential memory exhaustion and application crashes that could arise from malicious or oversized input files. This significantly enhances the stability and security of file processing within the system.

Highlights

  • Enhanced File Reading Security: Replaced a vulnerable file size check (path.stat().st_size) and full file read (path.read_text()) with a secure, bounded read pattern to prevent TOCTOU and DoS vulnerabilities.
  • Robust File Type Validation: Introduced explicit .is_file() checks before attempting to read, gracefully rejecting non-regular files like device files to prevent Out-Of-Memory (OOM) DoS attacks.
  • Precise Size Limit Enforcement: Implemented a bounded read using f.read(limit + 1) and an explicit length check to ensure files strictly adhere to the 10MB size limit, raising a ValueError if exceeded.
  • Security Documentation: Added a new entry to the Sentinel journal (.jules/sentinel.md) detailing the TOCTOU and OOM DoS vulnerability and its prevention, serving as a critical learning and prevention measure.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@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: 2e9cb77d87

ℹ️ 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 on lines +113 to +116
content = f.read(10 * 1024 * 1024 + 1)

if len(content) > 10 * 1024 * 1024:
raise ValueError(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Enforce size limit in bytes, not UTF-8 characters

This bounded-read check is done in text mode, so f.read(10 * 1024 * 1024 + 1) and len(content) both operate on characters rather than bytes. A UTF-8 JSON file containing multibyte characters can be larger than 10MB on disk while still passing this guard, which is a regression from the previous byte-based st_size limit and weakens the stated DoS protection for non-ASCII payloads. Please enforce the cap in binary mode (byte count) before decoding.

Useful? React with 👍 / 👎.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request addresses TOCTOU vulnerabilities and OOM DoS risks in file-backed JSON repositories and stores by implementing a more robust file reading mechanism. This involves checking if a path is a regular file, performing bounded reads, and verifying content length before JSON parsing. The changes are also documented in .jules/sentinel.md. Feedback includes the need to add try...except blocks for json.loads() and handle empty files in checkpoint_store.py for robustness and consistency, refactor the duplicated secure file reading logic into a shared utility function to improve maintainability, and refine the try...except block structure in json_file_progress_snapshot_store.py for clarity and consistency.

Comment on lines +245 to 246
parsed = json.loads(content)
return parsed if isinstance(parsed, dict) else {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The call to json.loads(content) is not wrapped in a try...except block and does not handle empty or whitespace-only files. If content is empty or not valid JSON, this will raise a json.JSONDecodeError, causing an unhandled exception. Other adapters in this PR handle this case for robustness. For consistency and to prevent crashes, you should handle this potential error.

Suggested change
parsed = json.loads(content)
return parsed if isinstance(parsed, dict) else {}
try:
parsed = json.loads(content) if content.strip() else {}
return parsed if isinstance(parsed, dict) else {}
except json.JSONDecodeError:
return {}

Comment on lines +236 to 246
if not path.is_file():
return {}

with open(path, encoding="utf-8") as f:
content = f.read(10 * 1024 * 1024 + 1)

if len(content) > 10 * 1024 * 1024:
raise ValueError(f"Checkpoint file {path} exceeds 10MB size limit")
parsed = json.loads(path.read_text(encoding="utf-8"))

parsed = json.loads(content)
return parsed if isinstance(parsed, dict) else {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This new secure file reading logic is a great improvement. However, this pattern is now duplicated across four different files:

  • checkpoint_store.py
  • json_file_practice_repository.py
  • json_file_progress_repository.py
  • json_file_progress_snapshot_store.py

This duplication introduces a few issues:

  1. Maintainability: If this logic needs to be updated in the future (e.g., changing the size limit), it must be changed in all four places, which is error-prone.
  2. Magic Number: The size limit 10 * 1024 * 1024 is a magic number repeated in each implementation.

To address this, I recommend refactoring this logic into a single, shared utility function. This _read_json function is a great candidate to be moved to a shared utility module. Within that utility, you could define the size limit as a constant (e.g., _MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024) to be used by the function. This would centralize the security-critical code, eliminate duplication, and improve maintainability.

Comment on lines 42 to 54
try:
parsed = json.loads(self._file_path.read_text(encoding="utf-8"))
with open(self._file_path, encoding="utf-8") as f:
content = f.read(10 * 1024 * 1024 + 1)

if len(content) > 10 * 1024 * 1024:
raise ValueError(
f"Progress snapshot file {self._file_path} exceeds 10MB size limit"
)

parsed = json.loads(content)
except (OSError, json.JSONDecodeError):
return {}
return parsed if isinstance(parsed, dict) else {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The structure of this try...except block can be improved for clarity and consistency. The final return statement is outside the try block, which can make the control flow harder to follow. Additionally, unlike other adapters, this doesn't explicitly handle empty files before attempting to parse JSON.

Moving the successful return path inside the try block and adding a check for empty content would make the code more readable and align it better with the other adapters.

Suggested change
try:
parsed = json.loads(self._file_path.read_text(encoding="utf-8"))
with open(self._file_path, encoding="utf-8") as f:
content = f.read(10 * 1024 * 1024 + 1)
if len(content) > 10 * 1024 * 1024:
raise ValueError(
f"Progress snapshot file {self._file_path} exceeds 10MB size limit"
)
parsed = json.loads(content)
except (OSError, json.JSONDecodeError):
return {}
return parsed if isinstance(parsed, dict) else {}
try:
with open(self._file_path, encoding="utf-8") as f:
content = f.read(10 * 1024 * 1024 + 1)
if len(content) > 10 * 1024 * 1024:
raise ValueError(
f"Progress snapshot file {self._file_path} exceeds 10MB size limit"
)
if not content.strip():
return {}
parsed = json.loads(content)
return parsed if isinstance(parsed, dict) else {}
except (OSError, json.JSONDecodeError):
return {}

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.

1 participant