Skip to content

[BUG] [v0.0.7] get_shared_session() has TOCTOU race on max_views — concurrent requests can exceed the view limit #159

@katotomoya0105

Description

@katotomoya0105

Summary

In src/cortex-app-server/src/share.rs, the get_shared_session handler checks whether view_count >= max_views and then, in a separate operation, calls increment_view(). Because these two steps are not atomic, concurrent requests can both pass the check before either increments the counter, allowing the view limit to be exceeded.

Affected Code

// src/cortex-app-server/src/share.rs  lines 347-354
// Check if max views exceeded
if let Some(max) = share.max_views
    && share.view_count >= max
{
    return Err(AppError::Gone("Share view limit reached".to_string()));
}

// Increment view count  ← separate, non-atomic step
state.share_manager.increment_view(&token).await;

The get() call (line 334) returns a clone of the SharedSessionData. The check is performed on that stale clone. By the time increment_view() acquires the write lock, another concurrent request may have already incremented the counter past max_views.

Root Cause

The check-then-act pattern here is a classic Time-of-Check / Time-of-Use (TOCTOU) race:

  1. Request A reads view_count = 9, max_views = 10 → passes check.
  2. Request B reads view_count = 9, max_views = 10 → passes check.
  3. Request A calls increment_viewview_count = 10.
  4. Request B calls increment_viewview_count = 11 (exceeds limit).

Impact

  • Share links with max_views set can be accessed more times than the creator intended.
  • Under high concurrency the overshoot is unbounded (N concurrent requests all pass the check simultaneously).

Suggested Fix

Add a combined check_and_increment method to ShareManager that performs the check and increment atomically under a single write lock:

/// Atomically check max_views and increment if allowed.
/// Returns Ok(new_count) if allowed, Err if limit reached or share not found.
pub async fn check_and_increment_view(
    &self,
    token: &str,
) -> Result<u32, &'static str> {
    let mut shares = self.shares.write().await;
    let share = shares.get_mut(token).ok_or("not found")?;
    if let Some(max) = share.max_views {
        if share.view_count >= max {
            return Err("limit reached");
        }
    }
    share.view_count += 1;
    Ok(share.view_count)
}

Then replace the two-step check + increment in the handler with a single call to this method.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions