Files
cg_api_secure-webshare/agent1_batch4.md

6.1 KiB
Raw Permalink Blame History

Batch 4 Security + Stability Verification Findings

Date: 2026-05-24 File: crates/cgcx-server/src/main.rs Lines analyzed: 11346


1. get_metadata and serve_file — Forward-submitted content edge cases

Status: No forward-specific breakage, but a functional gap exists.

Forward submissions are stored in the same contents / content_files tables as regular uploads. The web server has no forward-specific endpoints or logic (the forward system is handled entirely by the Telegram bot layer). Therefore:

  • get_metadata (line ~565) and serve_file (line ~640) treat forward-submitted content identically to direct uploads.
  • Status checks (Deleted / Blacklisted) and max_views pre-checks work the same for all content.

Finding: get_metadata does not increment views. This is correct for metadata alone, but it does mean a client can call get_metadata unlimited times on auto-destroy content without consuming the view budget. Combined with the serve_raw_file bypass (see §4), a user can enumerate metadata and download raw files without ever triggering auto-destroy.

File/line: main.rs:565625 (get_metadata), main.rs:640920 (serve_file)


2. serve_file view-increment logic — Concurrent requests for multi-file content

Status: Two issues found.

2a. Per-file view consumption breaks multi-file auto-destroy content

serve_file increments view_count once per file request (main.rs:825). If a content item contains 3 files, viewing all 3 files consumes 3 views. For content with max_views = 1, only the first file request succeeds before the content is marked for deletion. This means multi-file auto-destroy content is effectively unusable — users cannot view all files before the content self-destructs.

File/line: main.rs:825 (repo.increment_views(&content_id))

2b. TOCTOU race condition allows overserving past max_views

The flow in serve_file is:

  1. Read content.view_count from repo.get() (stale snapshot).
  2. Check content.view_count >= max → early GONE if true.
  3. Later, if not HEAD/range/conditional, call repo.increment_views() atomically.
  4. Check new_views >= max → trigger cleanup, but still stream the file.

Because there is no lock between the pre-check and the increment, two (or more) concurrent requests can both pass step 2, both increment, and both be served even if the total exceeds max_views. The DB increment is atomic, but the serve decision is not gated on the increment result.

Example with max_views = 1:

  • Request A: pre-check passes (view_count = 0)
  • Request B: pre-check passes (view_count = 0)
  • Request A: increment_views → 1, triggers cleanup, streams file
  • Request B: increment_views → 2, triggers cleanup, streams file

Both requests are served; the content is overserved.

File/line: main.rs:778790 (pre-check), main.rs:825850 (increment + post-check)


3. Password + auto-destroy interaction

Status: Partially fixed, but one bypass remains.

What is correct:

  • HEAD requests: is_head = method == Method::HEAD skips the increment block (main.rs:824).
  • Conditional requests: is_conditional = headers.contains_key(IF_NONE_MATCH) skips the increment block (main.rs:823).
  • Unauthorized requests: Password check happens before any view increment. An unauthorized request returns 401 Unauthorized without consuming a view (main.rs:799808).

What is broken:

  • serve_raw_file does not increment views at all (see §4). An attacker without a password can still be blocked by the auth check, but an authorized user can view password-protected auto-destroy content unlimited times via /raw without consuming views. The password gate works, but the view counter does not.

File/line: main.rs:799808 (auth before increment), main.rs:820826 (HEAD/conditional skip), main.rs:9231013 (serve_raw_file missing increment)


4. serve_raw_file — Missing view increment

Status: Confirmed bypass. Not intentional.

serve_raw_file (main.rs:923):

  • Checks status and max_views (main.rs:935941).
  • Checks password (main.rs:943954).
  • Streams the fully decrypted file (main.rs:9951013).
  • Never calls repo.increment_views().

This means:

  1. Auto-destroy bypass: Any client (authorized or not, depending on password) can download the raw file unlimited times without the view counter ever increasing. Content with max_views = 1 will never auto-destroy if accessed through /raw.
  2. Inconsistency: The regular serve_file endpoint consumes views, but /raw does not. This creates an obvious bypass path.

File/line: main.rs:9231013 (serve_raw_file)


# Issue Recommendation
1 serve_raw_file view bypass Add the same view-increment logic to serve_raw_file that exists in serve_file, including the HEAD / conditional / range skips.
2 Multi-file over-increment Consider incrementing views in get_metadata (or adding a separate "content view" session token) so that one metadata fetch + N file fetches counts as 1 view, not N+1. Alternatively, document that each file request = 1 view.
3 TOCTOU race on max_views Move the serve/deny decision into the DB transaction. Use UPDATE … RETURNING to atomically increment and read the new view count, and if the new count exceeds max, abort the transaction and return GONE before streaming. Alternatively, accept the soft limit behavior but add a CHECK constraint or second gate after increment that returns GONE if new_views > max.

Summary

  • §1: Forward content is handled safely; no forward-specific edge cases in the web handlers.
  • §2: serve_file has a race condition that allows concurrent requests to overserve past max_views. Multi-file content also consumes multiple views (one per file).
  • §3: HEAD, conditional, and unauthorized requests correctly avoid consuming views.
  • §4: serve_raw_file is a confirmed bypass — it never increments views, allowing unlimited access to auto-destroy content.